Skip to content
352 linesCodeBlameRaw
1/**
2 * Deprecated: g1t no longer runs agents of its own. Kept only until
3 * apps/mobile drops its Customize screens, which import it; delete it
4 * afterwards.
5 */
6/**
7 * Customize (docs.g1t.sh/guides/customize/): one place for what shapes
8 * how a workspace's agents work, in three parts, each with what the
9 * workspace has (**Yours**) and what it can add (**Discover**). It is the
10 * Marketplace's successor on the site and the phone.
11 *
12 * - **Skills**: what agents know how to do, in the open SKILL.md format
13 * (./skill-library.ts, ./skill-format.ts): g1t's foundational skills,
14 * the workspace's library, and every skill published in a marketplace.
15 * - **Connectors**: what agents can reach: MCP servers (./claude-plugin.ts
16 * `.mcp.json`, the directory in ./connectors.ts, and any address an
17 * owner adds), and g1t's own integrations (GitHub, trackers, alerts,
18 * model providers).
19 * - **Plugins**: folders in the open plugin format (./claude-plugin.ts),
20 * found in marketplaces, each bringing skills, agents and connectors to
21 * g1t's agents and everything else to Claude Code on a runner.
22 *
23 * Everything installable keeps the names the open ecosystem uses:
24 * `name@marketplace` for a plugin, `plugin:skill` for a skill inside one.
25 * Wire shapes are snake_case. No imports beyond types, so it runs
26 * anywhere: services, the site, the phone.
27 */
28
29import type { FetchPlan, MarketplaceSource, McpServerConfig, PluginLayout, SubagentFile } from "./claude-plugin";
30import type { ConnectorCategory } from "./connectors";
31import type { ListingKind } from "./marketplace";
32
33/** A tool an MCP server offers, as it describes it. */
34export type McpTool = {
35 name: string;
36 description: string;
37 /** `read` when the server says the tool only reads; `write` otherwise, which is also what an unknown tool is. */
38 kind: "read" | "write";
39 /** Its arguments, as the server describes them (JSON Schema). */
40 input_schema: Record<string, unknown>;
41};
42
43/** What a skill was attached to. */
44export type SkillScope = "agent" | "team" | "workspace";
45
46export type CustomizeSection = "skills" | "connectors" | "plugins";
47
48export const CUSTOMIZE_SECTIONS: readonly CustomizeSection[] = ["skills", "connectors", "plugins"];
49
50export type CustomizeSectionInfo = {
51 id: CustomizeSection;
52 title: string;
53 /** One line, for the section's row. */
54 description: string;
55 /** Where the site shows it, with `:workspace` to fill in. */
56 sitePath: string;
57};
58
59export const CUSTOMIZE: Record<CustomizeSection, CustomizeSectionInfo> = {
60 skills: {
61 id: "skills",
62 title: "Skills",
63 description: "What agents know how to do: the skills every agent has, your workspace's library, and public skills to add.",
64 sitePath: "/:workspace/-/customize/skills",
65 },
66 connectors: {
67 id: "connectors",
68 title: "Connectors",
69 description: "What agents can reach: MCP servers and the tools your team already uses, connected once and given to agents.",
70 sitePath: "/:workspace/-/customize/connectors",
71 },
72 plugins: {
73 id: "plugins",
74 title: "Plugins",
75 description: "Skills, agents and connectors published together, from any marketplace in the open plugin format.",
76 sitePath: "/:workspace/-/customize/plugins",
77 },
78};
79
80/** The part of Customize that shows a listing of this kind. */
81export function sectionOfListing(kind: ListingKind): CustomizeSection {
82 if (kind === "integration" || kind === "connector") return "connectors";
83 if (kind === "skill") return "skills";
84 return "plugins";
85}
86
87/** Customize's pages, under `/<workspace>/-/customize`. */
88export function customizePath(section: CustomizeSection | "" | "requests" | "marketplaces", workspace: string, rest = ""): string {
89 const base = `/${encodeURIComponent(workspace)}/-/customize`;
90 return `${base}${section ? `/${section}` : ""}${rest ? `/${rest}` : ""}`;
91}
92
93// ── Marketplaces ─────────────────────────────────────────────────────────
94
95/** A marketplace a workspace reads plugins from, as its page lists it. */
96export type Marketplace = {
97 /** Its name, as its manifest says and as `name@marketplace` uses. */
98 name: string;
99 source: MarketplaceSource;
100 /** Built in: every workspace has it, and it can't be removed. */
101 builtin: boolean;
102 about: string | null;
103 owner: string | null;
104 /** How many plugins it lists, and how many of those carry skills. */
105 plugins: number;
106 /** RFC 3339: when its manifest was last read, and why it couldn't be, if so. */
107 fetched_at: string | null;
108 problem: string | null;
109 added_by: string | null;
110 added_at: string | null;
111 can_remove: boolean;
112};
113
114/** Whether a plugin from Discover can be installed here, now. */
115export type PluginAvailability =
116 /** Installable now. */
117 | { kind: "available" }
118 /** The workspace has it (switched off, if `enabled` is false). */
119 | { kind: "installed"; enabled: boolean }
120 /** Listed but not something g1t can fetch (npm, a command, a git host g1t doesn't read), with why. It still works in Claude Code. */
121 | { kind: "unavailable"; why: string }
122 /** g1t's own, announced but not released. */
123 | { kind: "soon" };
124
125/** What a plugin would bring, worked out from its layout, before or after installing. */
126export type PluginComponents = {
127 /** Skills by name (`plugin:name` to agents). */
128 skills: { name: string; description: string | null; path: string }[];
129 /** Subagents by name, as hireable starting points. */
130 agents: { name: string; description: string | null; path: string }[];
131 /** Commands: the older one-file form of a skill. */
132 commands: { name: string; path: string }[];
133 /** MCP servers it names: remote ones become connectors; stdio ones are for Claude Code only. */
134 mcp_servers: { name: string; type: McpServerConfig["type"]; url: string | null; remote: boolean }[];
135 /** What only Claude Code on a runner uses. */
136 hooks: boolean;
137 lsp: boolean;
138 bin: boolean;
139 output_styles: boolean;
140};
141
142/** One plugin as Discover lists it, from a marketplace. */
143export type PluginListing = {
144 /** `name@marketplace`. */
145 ref: string;
146 name: string;
147 marketplace: string;
148 display_name: string | null;
149 description: string;
150 version: string | null;
151 author: string | null;
152 homepage: string | null;
153 category: string | null;
154 tags: string[];
155 keywords: string[];
156 /** Where g1t would read it from, when it can. */
157 fetch: FetchPlan;
158 availability: PluginAvailability;
159 /** Skills the entry names outright (a skills-only plugin), so Discover can list them without fetching. */
160 skills: string[];
161};
162
163/** A plugin the workspace installed. */
164export type PluginInstall = {
165 id: string;
166 /** `name@marketplace`. */
167 ref: string;
168 name: string;
169 marketplace: string;
170 display_name: string | null;
171 description: string;
172 version: string | null;
173 /** The commit it was read at, when the source has one; the ref otherwise. */
174 sha: string | null;
175 fetch: FetchPlan;
176 components: PluginComponents;
177 /** What couldn't be read when it was installed. */
178 problems: string[];
179 enabled: boolean;
180 installed_by: string;
181 installed_at: string;
182 /** A newer version in the marketplace, when the listing moved on. */
183 update: { version: string | null; sha: string | null } | null;
184};
185
186/** A plugin's page: the listing, the install if any, and what it holds, read from its source. */
187export type PluginDetail = {
188 listing: PluginListing;
189 install: PluginInstall | null;
190 components: PluginComponents | null;
191 readme: string | null;
192 /** The plugin's manifest, when it has one. */
193 manifest: { name: string; version: string | null; license: string | null; repository: string | null } | null;
194 /** The subagents in full, for the hire flow. */
195 agents: SubagentFile[];
196 problems: string[];
197 /** The marketplace it is listed in, for the header. */
198 source: { marketplace: string; owner: string | null };
199};
200
201// ── Skills in Discover ───────────────────────────────────────────────────
202
203/** One skill as Discover lists it: a skill inside a marketplace's plugin. */
204export type SkillListing = {
205 /** `plugin:skill`. */
206 ref: string;
207 name: string;
208 plugin: string;
209 marketplace: string;
210 /** The plugin's description until the skill's own is read. */
211 description: string;
212 category: string | null;
213 author: string | null;
214 /** Whether the workspace's library already has it from this plugin. */
215 added: boolean;
216 /** Whether the plugin it is in can be fetched here. */
217 availability: PluginAvailability;
218};
219
220// ── Connectors ───────────────────────────────────────────────────────────
221
222/** How a connector signs in to its server. */
223export type ConnectorAuth = "none" | "header" | "oauth";
224
225/** Where a connector came from. */
226export type ConnectorFrom =
227 | { kind: "directory"; id: string }
228 | { kind: "plugin"; ref: string; server: string }
229 | { kind: "address" }
230 /** An MCP server kept on one agent before connectors existed. */
231 | { kind: "agent"; handle: string };
232
233/** Whether a connector can be used now. */
234export type ConnectorState = "connected" | "needs_sign_in" | "needs_key" | "problem";
235
236/** A workspace's connector: an MCP server connected once, then given to agents. */
237export type WorkspaceConnector = {
238 /** `con_…`. */
239 id: string;
240 /** 2 to 32 lowercase letters, digits and hyphens; tools are offered as `<name>__<tool>`. */
241 name: string;
242 /** For the page: the directory's name, or the name again. */
243 title: string;
244 url: string;
245 category: ConnectorCategory | null;
246 about: string | null;
247 auth: ConnectorAuth;
248 state: ConnectorState;
249 /** Why it is a problem, when it is. */
250 problem: string | null;
251 from: ConnectorFrom;
252 tools: McpTool[];
253 /** RFC 3339: when its tools were last listed. */
254 checked_at: string | null;
255 /** The header's name when `auth` is header (its value is never shown). */
256 header_name: string | null;
257 /** For OAuth: who it is signed in as, if the server said, and when the sign-in was done. */
258 signed_in_as: string | null;
259 signed_in_at: string | null;
260 grants: ConnectorGrant[];
261 added_by: string;
262 added_at: string;
263};
264
265/** Where a connector reaches: every agent, a team's, or one. */
266export type ConnectorGrant = {
267 id: string;
268 scope: SkillScope;
269 /** The agent's handle, the team's slug, or null for the workspace. */
270 target: string | null;
271 label: string;
272 granted_by: string;
273 granted_at: string;
274};
275
276/** What an owner sends to add a connector. */
277export type ConnectorInput = {
278 /** From the directory (its id), a plugin's server (`ref` and `server`), or by address. */
279 from: { kind: "directory"; id: string } | { kind: "plugin"; ref: string; server: string } | { kind: "address"; name: string; url: string };
280 auth: ConnectorAuth;
281 /** For `header`: the header's name and its value, kept sealed. */
282 header?: { name: string; value: string } | null;
283};
284
285/** Starting an OAuth sign-in for a connector: where the browser goes. */
286export type ConnectorSignIn = { url: string };
287
288// ── Agents and what reaches them ─────────────────────────────────────────
289
290/** A connector as one agent's Abilities tab lists it. */
291export type AgentConnector = {
292 id: string;
293 name: string;
294 title: string;
295 state: ConnectorState;
296 /** How the agent has it. */
297 via: SkillScope | "agent-only";
298 via_label: string;
299 tools: number;
300};
301
302// ── The bundle a runner gets ─────────────────────────────────────────────
303
304/**
305 * What a job's Claude Code receives from the workspace's Customize
306 * (docs.g1t.sh/guides/self-hosted-runners/, "What a run gets from
307 * Customize"): plugins to fetch at their pinned commit, the library's skills
308 * as files, and connectors with the headers that sign them in. Read with
309 * the job's agent token, once per job.
310 */
311export type CustomizeBundle = {
312 plugins: { ref: string; name: string; fetch: FetchPlan; version: string | null }[];
313 /** Library skills, each a folder: SKILL.md and its files, as `path` and content. */
314 skills: { name: string; plugin: string | null; files: { path: string; content: string; encoding: "utf8" | "base64" }[] }[];
315 /** Connectors the job's agent has, with what to send; secrets included, so this never leaves the runner. */
316 connectors: { name: string; url: string; headers: Record<string, string> }[];
317};
318
319/** Something to hire from a plugin's `agents/` folder, as a template. */
320export type PluginAgentTemplate = {
321 /** `plugin:agent`. */
322 ref: string;
323 plugin: string;
324 marketplace: string;
325 name: string;
326 description: string;
327 /** The system prompt, as the agent's instructions. */
328 prompt: string;
329 model: string | null;
330 skills: string[];
331 tools: string[];
332};
333
334/** The layout of a plugin turned into what it brings. */
335export function componentsOf(layout: PluginLayout, servers: McpServerConfig[], skillDescriptions: Record<string, string | null> = {}, agentDescriptions: Record<string, string | null> = {}): PluginComponents {
336 return {
337 skills: layout.skills.map((s) => ({ name: s.name, description: skillDescriptions[s.path] ?? null, path: s.path })),
338 agents: layout.agents.map((a) => ({ name: a.name, description: agentDescriptions[a.path] ?? null, path: a.path })),
339 commands: layout.commands.map((c) => ({ name: c.name, path: c.path })),
340 mcp_servers: servers.map((s) => ({ name: s.name, type: s.type, url: s.url, remote: s.type === "http" || s.type === "sse" })),
341 hooks: layout.hooks.length > 0,
342 lsp: layout.lsp,
343 bin: layout.bin,
344 output_styles: layout.outputStyles,
345 };
346}
347
348/** The most marketplaces a workspace adds, and the most plugins it installs. */
349export const MAX_MARKETPLACES = 20;
350export const MAX_PLUGIN_INSTALLS = 100;
351/** The most connectors a workspace keeps. */
352export const MAX_CONNECTORS = 50;