{"version":3,"file":"builtin-skills.d.ts","sourceRoot":"","sources":["../../src/core/builtin-skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAQH,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAChC,2EAA2E;IAC3E,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC5B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC;CAC9C;AAED,eAAO,MAAM,cAAc,EAAE,SAAS,YAAY,EA0BjD,CAAC;AAcF,6DAA6D;AAC7D,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA2BxF;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,GAAG,SAAS,CAI1F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,EAAE,CAQpG","sourcesContent":["/**\n * Skills hoocode itself ships.\n *\n * hoocode reads skills from `~/.agents/skills`, `.hoocode/skills`, `.claude/skills`\n * and installed packages — every source except its own. That gap is why it\n * shipped three subagents and zero skills while telling users skills are the\n * extension unit: there was simply nowhere for a first-party skill to live.\n *\n * The obstacle is that a skill's `<location>` has to be a real readable path —\n * the model loads a skill by `read`ing it — and the Bun-compiled binary has no\n * `templates/` beside it. So rather than resolve the package directory (which\n * differs across npm/pnpm/source/binary layouts and would give the compiled\n * binary a silently degraded skill set), every install materializes the same\n * embedded copy into a cache directory. One code path, same behaviour\n * everywhere.\n *\n * The cache is keyed by content hash, so an upgrade writes a new directory and\n * a dev build that changes a skill without changing the version still takes\n * effect. It is a cache, not user-editable state: `~/.agents/skills` is where a\n * user's own skills go, and nothing here ever writes there.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../config.js\";\nimport { EMBEDDED_SKILLS } from \"../init-templates.generated.js\";\n\n/** What decides whether a built-in skill is registered this session. */\nexport interface BuiltinSkillGate {\n\t/** The `enablePluginTools` setting (the plugin system's master switch). */\n\tenablePluginTools: boolean;\n}\n\nexport interface BuiltinSkill {\n\t/** Directory name under `templates/skills`, and the skill's own name. */\n\tname: string;\n\t/** Why hoocode ships it. Documentation, and the catalog test reads it. */\n\tsummary: string;\n\t/**\n\t * Registered only when this returns true. A skill costs its description on\n\t * every turn, so one that only makes sense alongside a feature rides that\n\t * feature's switch rather than the default user's token budget.\n\t *\n\t * Omit for a skill that should always be available.\n\t */\n\tgate?: (options: BuiltinSkillGate) => boolean;\n}\n\nexport const BUILTIN_SKILLS: readonly BuiltinSkill[] = [\n\t{\n\t\tname: \"artifact-design\",\n\t\tsummary:\n\t\t\t\"Craft for a self-contained HTML visual: read the treatment the request calls for, write the color/type/layout plan before the markup, and avoid the looks generated design keeps landing on.\",\n\t\t// No feature switch to ride - a visual can be asked for in any session,\n\t\t// and there is no setting that predicts it. Ungated, with the cost kept\n\t\t// to one tight description line.\n\t},\n\t{\n\t\tname: \"canvas-design\",\n\t\tsummary:\n\t\t\t\"The canvas half of design craft: template-string markup, no dependencies and no build, owning the theme outright, rendering state two operators both mutate, and what actions cost while an instance is open.\",\n\t\t// Costs nothing per turn: `disable-model-invocation` keeps it out of\n\t\t// <available_skills>, and canvasBuildBrief names its path at the one moment\n\t\t// it is needed. Materialization is unconditional, so the path is always\n\t\t// there to name.\n\t},\n\t{\n\t\tname: \"plugin-authoring\",\n\t\tsummary:\n\t\t\t\"The craft half of ProposePlugin/UpdatePlugin: when a capability is worth extracting, naming it so it triggers again, portability, and the hook trap.\",\n\t\t// Useless without the tools it describes, and those are off by default,\n\t\t// so this costs nothing for a user who never enables the plugin system.\n\t\tgate: (options) => options.enablePluginTools,\n\t},\n];\n\n/** Stable short hash of the embedded skill tree; the cache directory's name. */\nfunction contentHash(): string {\n\tconst hash = createHash(\"sha256\");\n\tfor (const key of Object.keys(EMBEDDED_SKILLS).sort()) {\n\t\thash.update(key);\n\t\thash.update(\"\\0\");\n\t\thash.update(EMBEDDED_SKILLS[key] ?? \"\");\n\t\thash.update(\"\\0\");\n\t}\n\treturn hash.digest(\"hex\").slice(0, 12);\n}\n\n/** Root of the materialized copy for the current content. */\nexport function builtinSkillsCacheDir(agentDir: string = getAgentDir()): string {\n\treturn join(agentDir, \"cache\", \"builtin-skills\", contentHash());\n}\n\n/**\n * Write the embedded skills to the cache directory if they are not already\n * there, and return its path.\n *\n * Returns null when nothing could be written — a read-only home, a full disk.\n * That is a degraded session, not a broken one: the caller contributes no skill\n * paths and hoocode runs exactly as it did before these existed.\n */\nexport function materializeBuiltinSkills(agentDir: string = getAgentDir()): string | null {\n\tconst root = builtinSkillsCacheDir(agentDir);\n\ttry {\n\t\tfor (const [relativePath, content] of Object.entries(EMBEDDED_SKILLS)) {\n\t\t\tconst target = join(root, relativePath);\n\t\t\t// Content is hash-addressed, so an existing file with the right size is\n\t\t\t// already correct; re-reading beats re-writing on every startup.\n\t\t\tif (existsSync(target) && readFileSync(target, \"utf-8\") === content) continue;\n\t\t\tmkdirSync(dirname(target), { recursive: true });\n\t\t\t// Write-then-rename so a killed process never leaves a half-written\n\t\t\t// SKILL.md that would parse as a malformed skill on the next run.\n\t\t\tconst temp = `${target}.${process.pid}.tmp`;\n\t\t\twriteFileSync(temp, content, \"utf-8\");\n\t\t\trenameSync(temp, target);\n\t\t}\n\t\treturn root;\n\t} catch {\n\t\t// Drop a partial tree so the next run rebuilds it rather than loading a\n\t\t// half-written skill. The cleanup gets its own guard: `force` swallows\n\t\t// ENOENT but not ENOTDIR, and a cleanup that throws would turn the\n\t\t// degraded path back into a crash — which is the failure this whole\n\t\t// branch exists to prevent.\n\t\ttry {\n\t\t\trmSync(root, { recursive: true, force: true });\n\t\t} catch {}\n\t\treturn null;\n\t}\n}\n\n/**\n * Absolute path to the canvas-design guide, or undefined if it is not on disk.\n *\n * `canvas-design` is hidden from the per-turn skill list, so nothing would ever\n * surface it without this: `/new-canvas` names the path in its build brief, at\n * the one moment the guidance is worth loading. Materialization is\n * unconditional, so the file is normally there whether or not any skill is\n * contributed — but a degraded session (read-only home, full disk) has no cache\n * at all, and then the brief simply ships without the line.\n */\nexport function canvasDesignGuidePath(agentDir: string = getAgentDir()): string | undefined {\n\tif (!materializeBuiltinSkills(agentDir)) return undefined;\n\tconst guide = join(builtinSkillsCacheDir(agentDir), \"canvas-design\", \"SKILL.md\");\n\treturn existsSync(guide) ? guide : undefined;\n}\n\n/**\n * The skill directories to load this session, after gating.\n *\n * Returns per-skill directories rather than the root so a gated-off skill is\n * genuinely absent rather than loaded and filtered later — the load is what\n * costs the description on every turn.\n */\nexport function builtinSkillPaths(gate: BuiltinSkillGate, agentDir: string = getAgentDir()): string[] {\n\tconst enabled = BUILTIN_SKILLS.filter((skill) => !skill.gate || skill.gate(gate));\n\tif (enabled.length === 0) return [];\n\n\tconst root = materializeBuiltinSkills(agentDir);\n\tif (!root) return [];\n\n\treturn enabled.map((skill) => join(root, skill.name)).filter((dir) => existsSync(dir));\n}\n"]}