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 | */ |
| 14 | import type { FailureCode, InstallRequest, InstallRequestStatus, ListingKind, Result } from "@g1t/contracts"; | |
| 15 | import { CONNECTORS } from "@g1t/contracts/connectors"; | |
| 16 | import { 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. | |
| 19 | const ok = <T>(value: T): Result<T> => ({ ok: true, value }); | |
| 20 | const fail = (code: FailureCode, message: string): Result<never> => ({ ok: false, error: { code, message } }); | |
| 21 | ||
| 22 | /** Something the workspace can add, as a request names it. */ | |
| 23 | export 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. */ | |
| 26 | export 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 | ||
| 38 | type 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 | ||
| 51 | const 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. */ |
| 54 | export 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. */ | |
| 72 | export 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 | */ | |
| 79 | export 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 | */ | |
| 96 | export 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. */ | |
| 137 | export type Answered = { request: InstallRequest; requested_by_id: string }; | |
| 138 | ||
| 139 | /** An owner adds (`done`) or turns down (`declined`) an open request. */ | |
| 140 | export 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 | */ | |
| 166 | export 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`. */ | |
| 181 | export function requestsPath(slug: string): string { | |
| 182 | return `/${slug}/-/marketplace/requests`; | |
| 183 | } | |
| 184 | ||
| 185 | /** Where the thing a request asked for is, once added. */ | |
| 186 | export 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. */ | |
| 193 | export 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 | } |