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