Skip to content
201 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.1/**
2 * The Marketplace's install requests (docs.g1t.sh/guides/marketplace/): a
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.3 * member asks the workspace's owners to add an extension or an
4 * integration, and an owner adds it or turns it down. Kept in
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.5 * `install_requests`, one open request per person and listing.
6 *
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.7 * Only what can be added today can be asked for: a published extension,
8 * or a connector the catalog marks available for a workspace
9 * (@g1t/contracts/connectors). Agents aren't asked for here: they start
10 * from templates in Agents mode, and owners make them. Who is told, and
11 * how, is index.ts's; this file is the rules and the table, tested on
12 * SQLite.
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.13 */
14import type { FailureCode, InstallRequest, InstallRequestStatus, ListingKind, Result } from "@g1t/contracts";
15import { CONNECTORS } from "@g1t/contracts/connectors";
16import { ANSWERED_REQUESTS_DAYS, MAX_OPEN_REQUESTS, extensionById, listingRef, parseListing } from "@g1t/contracts/marketplace";
17
18// Plain values rather than @g1t/contracts' helpers, so this file runs under Node's tests.
19const ok = <T>(value: T): Result<T> => ({ ok: true, value });
20const fail = (code: FailureCode, message: string): Result<never> => ({ ok: false, error: { code, message } });
21
22/** Something the workspace can add, as a request names it. */
23export type Listing = { ref: string; kind: ListingKind; id: string; name: string };
24
25/** The listing `value` names, when it is one that can be added today; null otherwise. */
26export function findListing(value: unknown): Listing | null {
27 const parsed = parseListing(value);
28 if (!parsed) return null;
29 if (parsed.kind === "extension") {
30 const extension = extensionById(parsed.id);
31 return extension?.status === "available" ? { ref: listingRef("extension", extension.id), kind: "extension", id: extension.id, name: extension.name } : null;
32 }
33 const connector = CONNECTORS.find((c) => c.id === parsed.id);
34 if (!connector || connector.status !== "available" || !connector.scopes.includes("workspace")) return null;
35 return { ref: listingRef("integration", connector.id), kind: "integration", id: connector.id, name: connector.name };
36}
37
38type Row = {
39 id: string;
40 listing: string;
41 name: string;
42 note: string | null;
43 requested_by: string;
44 requested_by_id: string;
45 requested_at: string;
46 status: string;
47 resolved_by: string | null;
48 resolved_at: string | null;
49};
50
51const COLUMNS = "id, listing, name, note, requested_by, requested_by_id, requested_at, status, resolved_by, resolved_at";
52
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.53/** A row as a request; null for one whose listing is no longer a kind the Marketplace has. */
54export function toRequest(row: Row): InstallRequest | null {
55 const parsed = parseListing(row.listing);
56 if (!parsed) return null;
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.57 return {
58 id: row.id,
59 listing: row.listing,
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.60 kind: parsed.kind,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.61 name: row.name,
62 note: row.note,
63 requested_by: row.requested_by,
64 requested_at: row.requested_at,
65 status: (["open", "done", "declined"].includes(row.status) ? row.status : "open") as InstallRequestStatus,
66 resolved_by: row.resolved_by,
67 resolved_at: row.resolved_at,
68 };
69}
70
71/** Who is asking or answering. */
72export type Person = { id: string; username: string };
73
74/**
75 * Requests as `viewer` sees them: every one in the workspace for an owner,
76 * their own for anyone else. Open ones first, then the latest; answered
77 * ones for `ANSWERED_REQUESTS_DAYS`. At most 200.
78 */
79export async function listRequests(db: D1Database, workspaceId: string, viewer: Person, owner: boolean, now = new Date()): Promise<InstallRequest[]> {
80 const since = new Date(now.getTime() - ANSWERED_REQUESTS_DAYS * 86_400_000).toISOString();
81 const mine = owner ? "" : " AND requested_by_id = ?3";
82 const statement = db.prepare(
83 `SELECT ${COLUMNS} FROM install_requests
84 WHERE workspace_id = ?1 AND (status = 'open' OR resolved_at >= ?2)${mine}
85 ORDER BY status = 'open' DESC, requested_at DESC LIMIT 200`,
86 );
87 const bound = owner ? statement.bind(workspaceId, since) : statement.bind(workspaceId, since, viewer.id);
88 const { results } = await bound.all<Row>();
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.89 return results.map(toRequest).filter((request) => request !== null);
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.90}
91
92/**
93 * Opens a request for `listing`, as `id`. A conflict when the person has
94 * one open for it already; a limit when they have `MAX_OPEN_REQUESTS` open.
95 */
96export async function openRequest(
97 db: D1Database,
98 workspaceId: string,
99 id: string,
100 listing: Listing,
101 by: Person,
102 note: string | null,
103 now = new Date(),
104): Promise<Result<InstallRequest>> {
105 const open = await db
106 .prepare("SELECT COUNT(*) AS n FROM install_requests WHERE workspace_id = ? AND requested_by_id = ? AND status = 'open'")
107 .bind(workspaceId, by.id)
108 .first<{ n: number }>();
109 if ((open?.n ?? 0) >= MAX_OPEN_REQUESTS) {
110 return fail("limit", `You have ${MAX_OPEN_REQUESTS} requests waiting on an owner. Ask again once some are answered.`);
111 }
112 const row: Row = {
113 id,
114 listing: listing.ref,
115 name: listing.name,
116 note,
117 requested_by: by.username,
118 requested_by_id: by.id,
119 requested_at: now.toISOString(),
120 status: "open",
121 resolved_by: null,
122 resolved_at: null,
123 };
124 // The unique index on open requests turns a second press into nothing.
125 const inserted = await db
126 .prepare(
127 `INSERT INTO install_requests (id, workspace_id, listing, name, note, requested_by, requested_by_id, requested_at, status)
128 VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open') ON CONFLICT DO NOTHING RETURNING id`,
129 )
130 .bind(row.id, workspaceId, row.listing, row.name, row.note, row.requested_by, row.requested_by_id, row.requested_at)
131 .first<{ id: string }>();
132 if (!inserted) return fail("conflict", `You've already asked for ${listing.name}. The workspace's owners have your request.`);
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.133 return ok(toRequest(row)!);
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.134}
135
136/** A request answered, with who asked, for telling them. */
137export type Answered = { request: InstallRequest; requested_by_id: string };
138
139/** An owner adds (`done`) or turns down (`declined`) an open request. */
140export async function resolveRequest(
141 db: D1Database,
142 workspaceId: string,
143 id: string,
144 status: Exclude<InstallRequestStatus, "open">,
145 by: Person,
146 now = new Date(),
147): Promise<Result<Answered>> {
148 if (status !== "done" && status !== "declined") return fail("invalid", "Answer a request with done or declined.");
149 const row = await db
150 .prepare(
151 `UPDATE install_requests SET status = ?, resolved_by = ?, resolved_at = ?
152 WHERE id = ? AND workspace_id = ? AND status = 'open' RETURNING ${COLUMNS}`,
153 )
154 .bind(status, by.username, now.toISOString(), id, workspaceId)
155 .first<Row>();
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.156 const request = row ? toRequest(row) : null;
157 if (row && request) return ok({ request, requested_by_id: row.requested_by_id });
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.158 const exists = await db.prepare("SELECT status FROM install_requests WHERE id = ? AND workspace_id = ?").bind(id, workspaceId).first<{ status: string }>();
159 return exists ? fail("conflict", "That request was already answered.") : fail("not_found", "There is no such request.");
160}
161
162/**
163 * Every open request for `listing`, marked added: an owner added it, so
164 * whoever asked for it has what they asked for.
165 */
166export async function resolveListing(db: D1Database, workspaceId: string, listing: string, by: Person, now = new Date()): Promise<Answered[]> {
167 const { results } = await db
168 .prepare(
169 `UPDATE install_requests SET status = 'done', resolved_by = ?, resolved_at = ?
170 WHERE workspace_id = ? AND listing = ? AND status = 'open' RETURNING ${COLUMNS}`,
171 )
172 .bind(by.username, now.toISOString(), workspaceId, listing)
173 .all<Row>();
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.174 return results.flatMap((row) => {
175 const request = toRequest(row);
176 return request ? [{ request, requested_by_id: row.requested_by_id }] : [];
177 });
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.178}
179
180/** Where a request is looked at in the workspace `slug`. */
181export function requestsPath(slug: string): string {
182 return `/${slug}/-/marketplace/requests`;
183}
184
185/** Where the thing a request asked for is, once added. */
186export function listingPath(slug: string, listing: string): string {
187 const parsed = parseListing(listing);
188 if (parsed?.kind === "extension") return `/${slug}/-/marketplace/extensions/${parsed.id}`;
189 return `/${slug}/-/marketplace/integrations`;
190}
191
192/** What the person who asked is told when their request is answered. */
193export function answerLine(request: InstallRequest, by: string): { title: string; body: string } {
194 if (request.status === "declined") {
195 return { title: `${by} turned down your request for ${request.name}`, body: "Ask them in chat if you want to know why." };
196 }
197 return {
198 title: `${request.name} was added to the workspace`,
The Marketplace adds what g1t doesn't do and connects what you already use: extensions (Mail, Support, CRM, Recruiting, On-call, and bridges to a helpdesk, CRM or ERP you run, all Soon), starter kits, and integrations connected by the workspace or by each person; agents aren't installed there. Agent templates live in Agents, as starting points you configure, at Agents → Templates, and the Marketplace's old agent addresses lead there. The marketplace and agents guides say how.199 body: request.kind === "extension" ? `${by} installed it.` : `${by} connected it.`,
The Marketplace is where a workspace finds what to add, reached from Apps: the agent catalog of roles with who already does each job and Add to workspace; every integration with whether it's connected; g1t's own extensions, listed as Soon; and requests, where members ask and owners add or turn down, each told by a notification. Underneath, an extension is a manifest (publisher and tier, source and version from a tag, scopes, the domains its data goes to, its page on g1tusercontent.com, no price) and an install pins its version with an off switch, a budget and the audit log. Apps lists only what was added from the Marketplace, with pins; g1t's own pages stay in their sidebars, and AI Gateway joins Workspace's. The marketplace guide says how.200 };
201}