Skip to content
259 linesCodeBlameRaw
1/**
2 * A workspace's skill library (docs.g1t.sh/guides/agent-skills/): the
3 * skills it wrote, imported, saved from a session or follows from a
4 * repository, each a SKILL.md folder (skill-format.ts), every change a new
5 * version. A skill does nothing until it is attached: to one agent, to a
6 * team (every agent on it), or to the whole workspace. Each attachment
7 * pins the version its agents use, so an edit never reaches an agent until
8 * someone moves the pin ("update available").
9 *
10 * Who may do what:
11 *
12 * - **Everyone in the workspace** sees the library, and can save a draft
13 * from a session they can see.
14 * - **Team maintainers** write and import skills, edit the ones they
15 * wrote, publish drafts, and attach skills to the teams they maintain.
16 * - **Owners** do all of that for any skill, attach skills to agents and
17 * to the whole workspace, delete skills, and link the repository the
18 * library follows.
19 *
20 * Served by the agents service (`POST /rpc/<method>`); wire shapes are
21 * snake_case.
22 */
23import type { ServiceBinding } from "./clients";
24import type { User } from "./identity";
25import type { Result } from "./result";
26import type { SkillFile } from "./skill-format";
27
28/** Where a skill is attached. */
29export type SkillScope = "agent" | "team" | "workspace";
30
31export type SkillAttachment = {
32 id: string;
33 scope: SkillScope;
34 /** The agent's handle or the team's slug; null for the whole workspace. */
35 target: string | null;
36 /** How it reads: "@margo", "QA", "Every agent". */
37 label: string;
38 /** The version the agents it reaches use. */
39 version: number;
40 attached_by: string;
41 attached_at: string;
42 /** Whether the viewer may detach it or move its version. */
43 can_change: boolean;
44};
45
46/** Where a version came from. */
47export type SkillOrigin =
48 | { kind: "written" }
49 | { kind: "upload"; filename: string }
50 /** Read from a folder of a repository at one commit. */
51 | { kind: "repository"; repo: string; path: string; ref: string; commit: string }
52 /** Drafted by an agent from a finished session. */
53 | { kind: "session"; session_id: string; agent: string; title: string }
54 /** From `.g1t/skills/<name>/` in the repository the library follows. */
55 | { kind: "mirror"; repo: string; path: string; commit: string };
56
57/** A draft is waiting for a person to review it; only published skills can be attached. */
58export type SkillStatus = "published" | "draft";
59
60export type LibrarySkill = {
61 id: string;
62 name: string;
63 /** When to use it. */
64 description: string;
65 status: SkillStatus;
66 /** The newest version. */
67 version: number;
68 tools: string[];
69 /** Needs the agent's own computer: marked, and its scripts aren't run, until agents have one. */
70 requires_computer: boolean;
71 /** Files besides SKILL.md. */
72 files: number;
73 bytes: number;
74 /** Where the newest version came from. */
75 origin: SkillOrigin;
76 /** Follows the repository the library is linked to: it is changed there, not here. */
77 mirrored: boolean;
78 attachments: SkillAttachment[];
79 created_by: string;
80 created_at: string;
81 updated_by: string;
82 updated_at: string;
83 /** Whether the viewer may edit it (or publish it, for a draft). */
84 can_edit: boolean;
85 /** Whether the viewer may delete it (or discard it, for a draft). */
86 can_delete: boolean;
87};
88
89/** One file of a version, as the skill's page shows it. */
90export type SkillFileEntry = {
91 path: string;
92 bytes: number;
93 encoding: "utf8" | "base64";
94 /** Text files' content, up to 200 KB; null for others. */
95 content: string | null;
96 /** Under `scripts/`: run only on an agent's computer. */
97 script: boolean;
98};
99
100export type SkillVersionEntry = {
101 version: number;
102 description: string;
103 note: string | null;
104 origin: SkillOrigin;
105 bytes: number;
106 files: number;
107 created_by: string;
108 created_at: string;
109};
110
111export type SkillDetail = {
112 skill: LibrarySkill;
113 /** The version shown: the newest unless another was asked for. */
114 shown: number;
115 skill_md: string;
116 /** The instructions: SKILL.md after its front-matter. */
117 instructions: string;
118 tools: string[];
119 requires_computer: boolean;
120 /** Front-matter keys g1t doesn't read, kept as written. */
121 extra: Record<string, unknown>;
122 files: SkillFileEntry[];
123 versions: SkillVersionEntry[];
124};
125
126/** The repository the library follows: `.g1t/skills/<name>/` on its default branch. */
127export type SkillMirror = {
128 /** `workspace/name`. */
129 repo: string;
130 branch: string;
131 /** The commit last read. */
132 commit: string | null;
133 synced_at: string | null;
134 /** What went wrong the last time it was read, if it did. */
135 error: string | null;
136 linked_by: string;
137 linked_at: string;
138};
139
140export type SkillLibrary = {
141 skills: LibrarySkill[];
142 mirror: SkillMirror | null;
143 /** May write and import skills: owners and team maintainers. */
144 can_write: boolean;
145 /** Owners: attach to agents and the whole workspace, delete any skill, link a repository. */
146 can_manage: boolean;
147 /** The teams the viewer may attach skills to. */
148 teams: { slug: string; name: string }[];
149 /** The agents an owner may attach skills to. */
150 agents: { handle: string; display_name: string }[];
151};
152
153/** A skill written or changed in the editor. */
154export type SkillInput = {
155 name: string;
156 /** When to use it. */
157 description: string;
158 /** The instructions, in Markdown. */
159 instructions: string;
160 tools?: string[];
161 requires_computer?: boolean;
162 /** Files to add, or to replace at the same path; the current version's others are kept. */
163 add_files?: SkillFile[] | null;
164 /** Paths of the current version's files to leave out. */
165 remove_files?: string[] | null;
166 /** What changed, shown in its history. */
167 note?: string | null;
168 /** Move every attachment the editor may change to the new version (the default). */
169 update_attachments?: boolean;
170};
171
172/** Where an imported skill comes from. */
173export type SkillImport =
174 /** A SKILL.md, or a zip of a skill's folder, as standard base64. */
175 | { kind: "upload"; filename: string; data_base64: string }
176 /** A folder in a repository the viewer can read, at a branch, tag or commit (the default branch when absent). */
177 | { kind: "repository"; repo: string; path: string; ref?: string | null };
178
179/** One skill an agent has, as its Skills tab lists it. */
180export type AgentSkillLine = {
181 /** A foundational skill's id, or a library skill's. */
182 id: string;
183 name: string;
184 description: string;
185 foundational: boolean;
186 /** Whether it is on for this agent (owners turn skills off per agent). */
187 on: boolean;
188 /** How the agent has it; null for a foundational skill. */
189 via: SkillScope | null;
190 via_label: string | null;
191 attachment_id: string | null;
192 /** The version it uses: a release like "2026.10" for foundational skills. */
193 version: string;
194 /** A newer published version, if there is one. */
195 update: number | null;
196 requires_computer: boolean;
197 tools: string[];
198 /** Whether the viewer may move this attachment's version or detach it. */
199 can_change: boolean;
200};
201
202export type AgentSkills = {
203 handle: string;
204 skills: AgentSkillLine[];
205 /** Library skills past the limit of 100, which the agent doesn't get. */
206 over_limit: number;
207};
208
209export interface SkillLibraryApi {
210 library(workspace: string, viewer: User): Promise<Result<SkillLibrary>>;
211 skill(workspace: string, viewer: User, name: string, version?: number | null): Promise<Result<SkillDetail>>;
212 /** A new skill (`name` null) or a new version of one; a draft is published by saving it. */
213 saveSkill(workspace: string, viewer: User, name: string | null, input: SkillInput): Promise<Result<SkillDetail>>;
214 /** A new skill, or with `replace` a new version of the one with its name. */
215 importSkill(workspace: string, viewer: User, source: SkillImport, replace?: boolean): Promise<Result<SkillDetail>>;
216 /** `target` is an agent's handle or a team's slug; null for the whole workspace. Pins the newest version. */
217 attachSkill(workspace: string, viewer: User, name: string, scope: SkillScope, target: string | null): Promise<Result<SkillDetail>>;
218 detachSkill(workspace: string, viewer: User, name: string, attachment: string): Promise<Result<SkillDetail>>;
219 /** Moves an attachment to another version: the newest when `version` is null. */
220 pinSkill(workspace: string, viewer: User, name: string, attachment: string, version: number | null): Promise<Result<SkillDetail>>;
221 /** Removes a skill and its attachments; for a draft, discards it. */
222 deleteSkill(workspace: string, viewer: User, name: string): Promise<Result<null>>;
223 /** An agent's skills: g1t's foundational ones and the library's that reach it. */
224 agentSkills(workspace: string, viewer: User, handle: string): Promise<Result<AgentSkills>>;
225 /** The session's agent drafts a skill from its transcript, billed as its work; a person reviews it before it is published. */
226 draftFromSession(workspace: string, viewer: User, session: string): Promise<Result<SkillDetail>>;
227 /** Links the repository the library follows (`workspace/name`), or unlinks it (null), and reads it. Owners. */
228 setMirror(workspace: string, viewer: User, repo: string | null): Promise<Result<{ mirror: SkillMirror | null; changed: string[]; problems: string[] }>>;
229 /** Reads the linked repository again. */
230 syncMirror(workspace: string, viewer: User): Promise<Result<{ mirror: SkillMirror | null; changed: string[]; problems: string[] }>>;
231}
232
233async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
234 const response = await service.fetch(`https://service/rpc/${method}`, {
235 method: "POST",
236 headers: { "content-type": "application/json" },
237 body: JSON.stringify(args),
238 });
239 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
240 return (await response.json()) as T;
241}
242
243export function skillLibraryClient(service: ServiceBinding): SkillLibraryApi {
244 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
245 return {
246 library: (workspace, viewer) => call("skill_library", { workspace, viewer }),
247 skill: (workspace, viewer, name, version) => call("skill", { workspace, viewer, name, version: version ?? null }),
248 saveSkill: (workspace, viewer, name, input) => call("save_skill", { workspace, viewer, name, input }),
249 importSkill: (workspace, viewer, source, replace) => call("import_skill", { workspace, viewer, source, replace: replace === true }),
250 attachSkill: (workspace, viewer, name, scope, target) => call("attach_skill", { workspace, viewer, name, scope, target }),
251 detachSkill: (workspace, viewer, name, attachment) => call("detach_skill", { workspace, viewer, name, attachment }),
252 pinSkill: (workspace, viewer, name, attachment, version) => call("pin_skill", { workspace, viewer, name, attachment, version }),
253 deleteSkill: (workspace, viewer, name) => call("delete_skill", { workspace, viewer, name }),
254 agentSkills: (workspace, viewer, handle) => call("agent_skills", { workspace, viewer, handle }),
255 draftFromSession: (workspace, viewer, session) => call("draft_skill", { workspace, viewer, session }),
256 setMirror: (workspace, viewer, repo) => call("set_skill_mirror", { workspace, viewer, repo }),
257 syncMirror: (workspace, viewer) => call("sync_skill_mirror", { workspace, viewer }),
258 };
259}