| 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 | */ |
| 23 | import type { ServiceBinding } from "./clients"; |
| 24 | import type { User } from "./identity"; |
| 25 | import type { Result } from "./result"; |
| 26 | import type { SkillFile } from "./skill-format"; |
| 27 | |
| 28 | /** Where a skill is attached. */ |
| 29 | export type SkillScope = "agent" | "team" | "workspace"; |
| 30 | |
| 31 | export 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. */ |
| 47 | export 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. */ |
| 58 | export type SkillStatus = "published" | "draft"; |
| 59 | |
| 60 | export 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. */ |
| 90 | export 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 | |
| 100 | export 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 | |
| 111 | export 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. */ |
| 127 | export 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 | |
| 140 | export 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. */ |
| 154 | export 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. */ |
| 173 | export 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. */ |
| 180 | export 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 | |
| 202 | export 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 | |
| 209 | export 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 | |
| 233 | async 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 | |
| 243 | export 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 | } |