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 | /** |
| 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. | 2 | * The Marketplace (routes/workspace/marketplace/): what adds functionality |
| 3 | * to a workspace, read against what it has and what its people asked for. | |
| 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. | 4 | * |
| 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. | 5 | * Two kinds of listing: extensions, which add pages, data, cards and agent |
| 6 | * roles (g1t's own are listed before their first release), and | |
| 7 | * integrations, which connect what a team already uses so agents can work | |
| 8 | * with it. Owners add them; anyone else asks, and the agents service keeps | |
| 9 | * the request (`install_requests`). Agents aren't listed: they start from | |
| 10 | * templates in Agents mode (lib/agent-templates.ts). | |
| 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. | 11 | * |
| 12 | * No Workers or React imports, so it can be tested under Node. | |
| 13 | */ | |
| 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. | 14 | import type { ExtensionInstall, ExtensionManifest, InstallRequest, InstallRequestStatus, ListingTier } from "@g1t/contracts"; |
| 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. | 15 | import { type ConnectorView, connectorPath } from "@g1t/contracts/connectors"; |
| 16 | import { listingRef, parseListing } from "@g1t/contracts/marketplace"; | |
| 17 | ||
| 18 | import type { ConnectedState } from "./connectors"; | |
| 19 | ||
| 20 | /** The Marketplace's pages, under `/<workspace>/-/marketplace`. */ | |
| 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. | 21 | export function marketplacePath(slug: string, page: "" | "extensions" | "integrations" | "requests" = ""): string { |
| 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. | 22 | return `/${slug}/-/marketplace${page ? `/${page}` : ""}`; |
| 23 | } | |
| 24 | ||
| 25 | /** Who stands behind a listing, in a word and a sentence. */ | |
| 26 | export const TIERS: Record<ListingTier, { label: string; about: string }> = { | |
| 27 | official: { label: "Official", about: "Built and supported by g1t." }, | |
| 28 | verified: { label: "Verified", about: "From a reviewed publisher, whose code and permissions are checked before it is listed." }, | |
| 29 | community: { label: "Community", about: "From anyone, shared from a public repository. Pages run sandboxed; server code waits for an isolated sandbox." }, | |
| 30 | internal: { label: "Internal", about: "Built by your own people and agents, and seen only by your workspace." }, | |
| 31 | }; | |
| 32 | ||
| 33 | /** Whether `request` is `username`'s and still waiting. */ | |
| 34 | function openFor(request: InstallRequest, ref: string, username: string): boolean { | |
| 35 | return request.status === "open" && request.listing === ref && request.requested_by.toLowerCase() === username.toLowerCase(); | |
| 36 | } | |
| 37 | ||
| 38 | /** Open requests for `ref`. */ | |
| 39 | function openCount(requests: InstallRequest[], ref: string): number { | |
| 40 | return requests.filter((request) => request.status === "open" && request.listing === ref).length; | |
| 41 | } | |
| 42 | ||
| 43 | /** An integration a workspace can connect, with whether it has. */ | |
| 44 | export type IntegrationListing = { | |
| 45 | ref: string; | |
| 46 | view: ConnectorView; | |
| 47 | /** How it is doing, when it is connected. */ | |
| 48 | connected: ConnectedState | null; | |
| 49 | /** Where an owner connects it, or manages it once connected. */ | |
| 50 | href: string | null; | |
| 51 | requested: boolean; | |
| 52 | waiting: number; | |
| 53 | }; | |
| 54 | ||
| 55 | /** | |
| 56 | * The integrations a workspace can connect today, connected ones first, | |
| 57 | * each in catalog order. `connected` comes from the integrations service | |
| 58 | * and the GitHub App (lib/connected.server.ts). | |
| 59 | */ | |
| 60 | export function integrationListings( | |
| 61 | views: ConnectorView[], | |
| 62 | connected: Record<string, ConnectedState>, | |
| 63 | requests: InstallRequest[], | |
| 64 | username: string, | |
| 65 | slug: string, | |
| 66 | ): IntegrationListing[] { | |
| 67 | const listings = views | |
| 68 | .filter((view) => view.status === "available") | |
| 69 | .map((view) => { | |
| 70 | const ref = listingRef("integration", view.id); | |
| 71 | const state = connected[view.id] ?? null; | |
| 72 | const setup = view.href ? connectorPath(view.href, slug) : null; | |
| 73 | return { | |
| 74 | ref, | |
| 75 | view, | |
| 76 | connected: state, | |
| 77 | href: state?.manage ?? setup, | |
| 78 | requested: requests.some((request) => openFor(request, ref, username)), | |
| 79 | waiting: openCount(requests, ref), | |
| 80 | }; | |
| 81 | }); | |
| 82 | return [...listings.filter((l) => l.connected), ...listings.filter((l) => !l.connected)]; | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * The integrations the catalog lists as coming: a workspace's, then those | |
| 87 | * each person connects for themselves (a calendar, a mailbox), once each. | |
| 88 | */ | |
| 89 | export function comingIntegrations(views: ConnectorView[], personal: ConnectorView[] = []): ConnectorView[] { | |
| 90 | const workspace = views.filter((view) => view.status === "soon"); | |
| 91 | const seen = new Set(workspace.map((view) => view.id)); | |
| 92 | const own = personal.filter((view) => view.status === "soon" && !seen.has(view.id) && !views.some((v) => v.id === view.id)); | |
| 93 | return [...workspace, ...own]; | |
| 94 | } | |
| 95 | ||
| 96 | /** An extension in the Marketplace, with whether the workspace has it. */ | |
| 97 | export type ExtensionListing = { | |
| 98 | ref: string; | |
| 99 | manifest: ExtensionManifest; | |
| 100 | install: ExtensionInstall | null; | |
| 101 | requested: boolean; | |
| 102 | waiting: number; | |
| 103 | }; | |
| 104 | ||
| 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. | 105 | /** Where extensions are grouped: connected systems apart from the rest. */ |
| 106 | export const CONNECTED_SYSTEMS = "Connected systems"; | |
| 107 | ||
| 108 | /** Whether an extension bridges a system the team already runs. */ | |
| 109 | export function isConnectedSystem(manifest: Pick<ExtensionManifest, "category">): boolean { | |
| 110 | return manifest.category === CONNECTED_SYSTEMS; | |
| 111 | } | |
| 112 | ||
| 113 | /** | |
| 114 | * A starting set of extensions for one kind of team, installed together | |
| 115 | * once each is published. Ids are first-party extensions'. | |
| 116 | */ | |
| 117 | export type StarterKit = { id: string; name: string; about: string; extensions: string[] }; | |
| 118 | ||
| 119 | export const STARTER_KITS: StarterKit[] = [ | |
| 120 | { id: "customers", name: "Customer team", about: "Mail on your domain, support conversations and a pipeline.", extensions: ["mail", "support", "crm"] }, | |
| 121 | { id: "engineering", name: "Engineering extras", about: "On-call rotations, and the helpdesk your customers already write to.", extensions: ["on-call", "helpdesk-bridge"] }, | |
| 122 | { id: "operations", name: "People and operations", about: "Hiring, and the orders and invoices in your ERP.", extensions: ["recruiting", "erp-bridge"] }, | |
| 123 | ]; | |
| 124 | ||
| 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. | 125 | /** Every extension listed, published ones first, each with its install. */ |
| 126 | export function extensionListings(manifests: ExtensionManifest[], installs: ExtensionInstall[], requests: InstallRequest[], username: string): ExtensionListing[] { | |
| 127 | const listings = manifests.map((manifest) => { | |
| 128 | const ref = listingRef("extension", manifest.id); | |
| 129 | return { | |
| 130 | ref, | |
| 131 | manifest, | |
| 132 | install: installs.find((install) => install.listing === ref) ?? null, | |
| 133 | requested: requests.some((request) => openFor(request, ref, username)), | |
| 134 | waiting: openCount(requests, ref), | |
| 135 | }; | |
| 136 | }); | |
| 137 | return [...listings.filter((l) => l.manifest.status === "available"), ...listings.filter((l) => l.manifest.status !== "available")]; | |
| 138 | } | |
| 139 | ||
| 140 | /** One extension's page in the Marketplace. */ | |
| 141 | export function extensionPath(slug: string, id: string): string { | |
| 142 | return `/${slug}/-/marketplace/extensions/${id}`; | |
| 143 | } | |
| 144 | ||
| 145 | /** Where an extension runs, in words. */ | |
| 146 | export function runtimeWords(manifest: Pick<ExtensionManifest, "runtime" | "publisher">): string { | |
| 147 | return manifest.runtime === "hosted" ? `Runs on g1t` : `Runs on ${manifest.publisher.name}'s servers`; | |
| 148 | } | |
| 149 | ||
| 150 | /** Whether an integration listing matches what someone typed: its name, category words, capabilities or keywords. */ | |
| 151 | export function integrationMatches(listing: IntegrationListing, query: string): boolean { | |
| 152 | const wanted = query.trim().toLowerCase(); | |
| 153 | if (!wanted) return true; | |
| 154 | const { view } = listing; | |
| 155 | return [view.name, view.description, view.category, ...view.capabilities, ...view.keywords].some((word) => word.toLowerCase().includes(wanted)); | |
| 156 | } | |
| 157 | ||
| 158 | /** The requests still waiting on an owner. */ | |
| 159 | export function openRequests(requests: InstallRequest[]): InstallRequest[] { | |
| 160 | return requests.filter((request) => request.status === "open"); | |
| 161 | } | |
| 162 | ||
| 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. | 163 | /** Where an owner goes to add what a request asks for: the extension's page, or the integration's setup page. */ |
| 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. | 164 | export function addPath(request: Pick<InstallRequest, "listing">, slug: string, views: ConnectorView[]): string | null { |
| 165 | const parsed = parseListing(request.listing); | |
| 166 | if (!parsed) return null; | |
| 167 | if (parsed.kind === "extension") return extensionPath(slug, parsed.id); | |
| 168 | const view = views.find((v) => v.id === parsed.id); | |
| 169 | return view?.href ? connectorPath(view.href, slug) : null; | |
| 170 | } | |
| 171 | ||
| 172 | /** What the Marketplace's forms ask for, read from one. */ | |
| 173 | export type MarketplaceForm = | |
| 174 | | { intent: "request"; listing: string; note: string | null } | |
| 175 | | { intent: "resolve"; id: string; status: Exclude<InstallRequestStatus, "open"> } | |
| 176 | | { intent: "install"; extension: string } | |
| 177 | | { intent: "switch"; listing: string; enabled: boolean } | |
| 178 | | { intent: "uninstall"; listing: string }; | |
| 179 | ||
| 180 | /** A Marketplace form's request; null when it asks for nothing that makes sense. */ | |
| 181 | export function marketplaceForm(form: FormData): MarketplaceForm | null { | |
| 182 | const intent = form.get("intent"); | |
| 183 | if (intent === "request") { | |
| 184 | const listing = String(form.get("listing") ?? ""); | |
| 185 | if (!parseListing(listing)) return null; | |
| 186 | const note = String(form.get("note") ?? "").trim(); | |
| 187 | return { intent, listing, note: note || null }; | |
| 188 | } | |
| 189 | if (intent === "resolve") { | |
| 190 | const id = String(form.get("id") ?? ""); | |
| 191 | const status = form.get("status"); | |
| 192 | if (!id || (status !== "done" && status !== "declined")) return null; | |
| 193 | return { intent, id, status }; | |
| 194 | } | |
| 195 | const listing = String(form.get("listing") ?? ""); | |
| 196 | const extension = parseListing(listing)?.kind === "extension" ? listing : null; | |
| 197 | if (intent === "install") { | |
| 198 | const id = String(form.get("extension") ?? ""); | |
| 199 | return /^[a-z][a-z0-9-]{1,31}$/.test(id) ? { intent, extension: id } : null; | |
| 200 | } | |
| 201 | if (intent === "switch" && extension) { | |
| 202 | const enabled = form.get("enabled"); | |
| 203 | return enabled === "on" || enabled === "off" ? { intent, listing: extension, enabled: enabled === "on" } : null; | |
| 204 | } | |
| 205 | if (intent === "uninstall" && extension) return { intent, listing: extension }; | |
| 206 | return null; | |
| 207 | } |