Skip to content
207 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/**
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.14import 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.15import { type ConnectorView, connectorPath } from "@g1t/contracts/connectors";
16import { listingRef, parseListing } from "@g1t/contracts/marketplace";
17
18import 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.21export 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. */
26export 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. */
34function 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`. */
39function 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. */
44export 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 */
60export 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 */
89export 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. */
97export 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. */
106export const CONNECTED_SYSTEMS = "Connected systems";
107
108/** Whether an extension bridges a system the team already runs. */
109export 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 */
117export type StarterKit = { id: string; name: string; about: string; extensions: string[] };
118
119export 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. */
126export 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. */
141export function extensionPath(slug: string, id: string): string {
142 return `/${slug}/-/marketplace/extensions/${id}`;
143}
144
145/** Where an extension runs, in words. */
146export 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. */
151export 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. */
159export 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.164export 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. */
173export 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. */
181export 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}