57f54e37c5
* add bundled git-archaeology skill, auto-installed for opencode and claude ships a SKILL.md teaching agents the underused git history primitives (pickaxe -S/-G, -L for function/line ranges, --reverse blame, deleted-file recovery) so they stop scrolling git log -p when blame comes up empty. introduces a lightweight bundled-skill path alongside the existing addSkill (npx skills add) flow used for external skills like agent-browser. SKILL.md is inlined into dist/cli.mjs via esbuild's text loader and written to <home>/.agents/skills/<name>/SKILL.md at runtime — no network, no version drift, no per-run install cost. * fix: register vitest plugin to load .md as text for bundled-skill tests * fix: drop vite type import from vitest plugin (vite isn't a direct dep) * fix: load bundled skills via readFileSync so source mode works esbuild's text loader only applies to the npm-bundled dist/cli.mjs path. the preview / oss path runs cli.ts directly with node (PULLFROG_FORCE_LOCAL_CLI=1 in runCli.ts#runLocalCli), where node has no idea how to import .md files — ERR_UNKNOWN_FILE_EXTENSION crashes the action before any agent starts. switch to runtime readFileSync that checks both candidate locations: - source mode: <actionRoot>/skills/<name>/SKILL.md (relative to utils/skills.ts) - bundled mode: <distDir>/skills/<name>/SKILL.md (esbuild copies the tree) drops the no-longer-needed esbuild text loader, vitest .md plugin, and ambient *.md type declaration. wiki/skills.md updated with the why. * fix: write bundled skills to per-agent dirs so claude actually registers them
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { log } from "./cli.ts";
|
|
import { getDevDependencyVersion } from "./version.ts";
|
|
|
|
const skillsVersion = getDevDependencyVersion("skills");
|
|
|
|
/**
|
|
* skills bundled with the action runtime. the SKILL.md files live in
|
|
* `action/skills/<name>/SKILL.md` and are read at runtime — no esbuild loader,
|
|
* no codegen. this matters because the preview / oss path runs `cli.ts` from
|
|
* source (see `runCli.ts#runLocalCli`) where esbuild loaders don't apply.
|
|
*/
|
|
const BUNDLED_SKILL_NAMES = ["git-archaeology"] as const;
|
|
|
|
/**
|
|
* resolve the on-disk path of a bundled SKILL.md by checking the two locations
|
|
* the file may live in:
|
|
* - source mode (`runLocalCli`): `<actionRoot>/skills/<name>/SKILL.md`,
|
|
* reached as `../skills/...` from `utils/skills.ts`.
|
|
* - bundled mode (npx published package): `<distDir>/skills/<name>/SKILL.md`,
|
|
* reached as `./skills/...` from `dist/cli.mjs`.
|
|
*
|
|
* the bundled-mode copy is produced by an esbuild post-build step in
|
|
* `esbuild.config.js`.
|
|
*/
|
|
function resolveSkillPath(name: string): string {
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const candidates = [
|
|
join(here, "..", "skills", name, "SKILL.md"),
|
|
join(here, "skills", name, "SKILL.md"),
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (existsSync(candidate)) return candidate;
|
|
}
|
|
throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
|
|
}
|
|
|
|
/**
|
|
* each agent has its own auto-scan dir under HOME. we write to all of them so
|
|
* the same `installBundledSkills` call works regardless of which agent is
|
|
* running, without coupling skills.ts to agent identity.
|
|
*
|
|
* verified empirically (PR #565):
|
|
* - OpenCode registers skills from `$HOME/.agents/skills/` and `.opencode/skills/`.
|
|
* - Claude Code only registers skills from `$HOME/.claude/skills/` —
|
|
* it does NOT scan `.agents/skills/`, so writing only there leaves the
|
|
* skill on disk but invisible to Claude's `Skill` tool.
|
|
*/
|
|
const SKILL_TARGET_DIRS = [".opencode/skills", ".claude/skills", ".agents/skills"] as const;
|
|
|
|
/**
|
|
* write all bundled skills into the fake HOME so OpenCode / Claude Code discover
|
|
* them via their auto-scan directories.
|
|
*
|
|
* called once per agent run from each agent's `run()`. cheap (small file
|
|
* writes), no network, idempotent.
|
|
*/
|
|
export function installBundledSkills(params: { home: string }): void {
|
|
for (const name of BUNDLED_SKILL_NAMES) {
|
|
const content = readFileSync(resolveSkillPath(name), "utf8");
|
|
for (const targetDir of SKILL_TARGET_DIRS) {
|
|
const skillDir = join(params.home, targetDir, name);
|
|
mkdirSync(skillDir, { recursive: true });
|
|
writeFileSync(join(skillDir, "SKILL.md"), content);
|
|
}
|
|
}
|
|
log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
|
|
}
|
|
|
|
/**
|
|
* install a skill globally via the `skills` CLI.
|
|
*
|
|
* runs `npx skills add <ref> --skill <name> -g` with `cwd` set to os tmpdir
|
|
* so npm doesn't walk up and find a project-level `.npmrc` with pnpm-specific
|
|
* settings (e.g. `public-hoist-pattern`) that break npx binary resolution.
|
|
* the `-g` flag writes to `$HOME/.agents/skills/` which is controlled by
|
|
* `params.env.HOME` (the fake HOME), so cwd has no effect on install location.
|
|
*/
|
|
export function addSkill(params: {
|
|
ref: string;
|
|
skill: string;
|
|
env: Record<string, string>;
|
|
agent: string;
|
|
}): void {
|
|
const result = spawnSync(
|
|
"npx",
|
|
[
|
|
"-y",
|
|
`skills@${skillsVersion}`,
|
|
"add",
|
|
params.ref,
|
|
"--skill",
|
|
params.skill,
|
|
"-g",
|
|
"-a",
|
|
params.agent,
|
|
"-y",
|
|
],
|
|
{
|
|
cwd: tmpdir(),
|
|
env: { ...process.env, ...params.env },
|
|
stdio: "pipe",
|
|
timeout: 30_000,
|
|
}
|
|
);
|
|
if (result.status === 0) {
|
|
log.info(`installed ${params.skill} skill (${params.agent})`);
|
|
} else {
|
|
const stderr = (result.stderr?.toString() || "").trim();
|
|
const errorMsg = result.error ? result.error.message : stderr;
|
|
log.info(`${params.skill} skill install failed: ${errorMsg}`);
|
|
}
|
|
}
|