@bunshin-cc/bunshin 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +265 -52
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -953,17 +953,17 @@ var init_document = __esm({
953
953
  });
954
954
 
955
955
  // ../core/src/v2/sections/markdown.ts
956
- function repoSectionId(repo, path24, anchor = "") {
957
- return anchor ? `repo:${repo}/${path24}#${anchor}` : `repo:${repo}/${path24}`;
956
+ function repoSectionId(repo, path25, anchor = "") {
957
+ return anchor ? `repo:${repo}/${path25}#${anchor}` : `repo:${repo}/${path25}`;
958
958
  }
959
- function docKind(path24) {
960
- const base = path24.split("/").pop()?.toLowerCase() ?? "";
959
+ function docKind(path25) {
960
+ const base = path25.split("/").pop()?.toLowerCase() ?? "";
961
961
  if (base === "testing.md") return "verification";
962
962
  if (base === "skill.md") return "skill";
963
963
  return "spec";
964
964
  }
965
- function defaultPaths(path24) {
966
- const dir = path24.split("/").slice(0, -1).join("/");
965
+ function defaultPaths(path25) {
966
+ const dir = path25.split("/").slice(0, -1).join("/");
967
967
  if (!dir || dir.startsWith(".claude/") || dir.startsWith(".codex/")) return [];
968
968
  return [`${dir}/**`];
969
969
  }
@@ -1108,11 +1108,11 @@ function lintSkills(sections) {
1108
1108
  }
1109
1109
  const state = /* @__PURE__ */ new Map();
1110
1110
  const cycles = /* @__PURE__ */ new Set();
1111
- const dfs = (n, path24) => {
1111
+ const dfs = (n, path25) => {
1112
1112
  state.set(n, 1);
1113
1113
  for (const d of edges[n] ?? []) {
1114
- if (state.get(d) === 1) cycles.add([...path24.slice(path24.indexOf(d)), d].join(" -> "));
1115
- else if (!state.get(d)) dfs(d, [...path24, d]);
1114
+ if (state.get(d) === 1) cycles.add([...path25.slice(path25.indexOf(d)), d].join(" -> "));
1115
+ else if (!state.get(d)) dfs(d, [...path25, d]);
1116
1116
  }
1117
1117
  state.set(n, 2);
1118
1118
  };
@@ -1199,8 +1199,8 @@ function parsePythonSkills(text2) {
1199
1199
  }
1200
1200
  return { skills, constants };
1201
1201
  }
1202
- function harnessSectionId(path24, symbol) {
1203
- return `harness:${path24}#${symbol}`;
1202
+ function harnessSectionId(path25, symbol) {
1203
+ return `harness:${path25}#${symbol}`;
1204
1204
  }
1205
1205
  var DEF_RE;
1206
1206
  var init_python = __esm({
@@ -1326,8 +1326,8 @@ function globToRegExp(glob) {
1326
1326
  globCache.set(glob, out);
1327
1327
  return out;
1328
1328
  }
1329
- function matchesGlob(path24, glob) {
1330
- const p = path24.replace(/^\.\//, "").replace(/\\/g, "/");
1329
+ function matchesGlob(path25, glob) {
1330
+ const p = path25.replace(/^\.\//, "").replace(/\\/g, "/");
1331
1331
  return globToRegExp(glob.replace(/^\.\//, "")).test(p);
1332
1332
  }
1333
1333
  var DEFAULT_EMBEDDERS, globCache;
@@ -1418,10 +1418,10 @@ function push(sections, reader, query) {
1418
1418
  for (const s of recallable(sections, reader)) {
1419
1419
  const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
1420
1420
  for (const t of triggers) {
1421
- const path24 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1422
- const entity = path24 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1423
- const skill = path24 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1424
- const trigger = path24 ? `path:${path24}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1421
+ const path25 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1422
+ const entity = path25 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1423
+ const skill = path25 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1424
+ const trigger = path25 ? `path:${path25}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1425
1425
  if (trigger) {
1426
1426
  hits.set(s.id, { section: s, trigger, anchor: t.anchor });
1427
1427
  break;
@@ -1666,6 +1666,7 @@ var init_redact = __esm({
1666
1666
  { kind: "aws-key", re: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA)[0-9A-Z]{16}\b/g },
1667
1667
  { kind: "github-pat", re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
1668
1668
  { kind: "github-fine-pat", re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
1669
+ { kind: "bunshin-key", re: /\bbsk_[A-Za-z0-9]{8}_[A-Za-z0-9]{16,}\b/g },
1669
1670
  { kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
1670
1671
  { kind: "openai-key", re: /\bsk-(?:proj-|live-)?[A-Za-z0-9_-]{20,}\b/g },
1671
1672
  { kind: "slack-token", re: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g },
@@ -8181,22 +8182,44 @@ Common flags: --api-url URL (default: this project's, else https://api.bunshin.c
8181
8182
  // src/v2/runner.ts
8182
8183
  var runner_exports = {};
8183
8184
  __export(runner_exports, {
8185
+ LAUNCHD_LABEL: () => LAUNCHD_LABEL,
8184
8186
  ROUTINE_PROMPT: () => ROUTINE_PROMPT,
8185
8187
  cmdRunner: () => cmdRunner,
8186
8188
  connection: () => connection,
8189
+ cronLine: () => cronLine,
8190
+ installPrompt: () => installPrompt,
8191
+ installSchedule: () => installSchedule,
8192
+ launchdPlist: () => launchdPlist,
8193
+ readInstalled: () => readInstalled,
8194
+ removeSchedule: () => removeSchedule,
8187
8195
  renderJob: () => renderJob,
8188
8196
  runOnce: () => runOnce,
8197
+ runnerHome: () => runnerHome,
8198
+ scheduledCommand: () => scheduledCommand,
8189
8199
  setupText: () => setupText
8190
8200
  });
8201
+ import { spawnSync as spawnSync3 } from "node:child_process";
8191
8202
  import fs23 from "node:fs";
8192
8203
  import os5 from "node:os";
8204
+ import path20 from "node:path";
8205
+ function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
8206
+ return path20.join(home, ".bunshin", "runner");
8207
+ }
8208
+ function readInstalled(home) {
8209
+ try {
8210
+ return JSON.parse(fs23.readFileSync(path20.join(runnerHome(home), "config.json"), "utf8"));
8211
+ } catch {
8212
+ return null;
8213
+ }
8214
+ }
8193
8215
  function connection(args, cwd) {
8194
- const store = Store.discover(cwd);
8216
+ const saved = readInstalled();
8217
+ const store = saved && path20.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
8195
8218
  const config = store?.config();
8196
- const key = process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || "";
8197
- const apiUrl = (str5(args.flags["api-url"]) ?? process.env.BUNSHIN_API_URL ?? config?.apiUrl ?? DEFAULT_API).replace(/\/$/, "");
8219
+ const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
8220
+ const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
8198
8221
  if (!key) throw new Error("no key: set BUNSHIN_RUNNER_KEY (create a runner key in the console: Runners \u2192 New runner key) or run inside a connected project");
8199
- const name = str5(args.flags.name) ?? process.env.BUNSHIN_RUNNER_NAME ?? `${os5.hostname().split(".")[0] || "runner"}`;
8222
+ const name = str5(args.flags.name) ?? process.env.BUNSHIN_RUNNER_NAME ?? saved?.name ?? `${os5.hostname().split(".")[0] || "runner"}`;
8200
8223
  return { apiUrl, key, name: name.replace(/[^\w.:@/-]/g, "-").slice(0, 80) };
8201
8224
  }
8202
8225
  async function call(c, method, route, body, timeoutMs = 6e4) {
@@ -8229,9 +8252,10 @@ async function lease(c, args, max = 1) {
8229
8252
  return (await call(c, "POST", "/lease", { max, runner: c.name, ...kinds ? { kinds } : {} }, 12e4)).body.jobs;
8230
8253
  }
8231
8254
  function modelConfig(args) {
8232
- const backend = str5(args.flags.backend) ?? "auto";
8255
+ const saved = readInstalled();
8256
+ const backend = str5(args.flags.backend) ?? saved?.backend ?? "auto";
8233
8257
  if (!["auto", "sdk", "claude-cli", "codex-cli", "pi-cli"].includes(backend)) throw new Error("--backend must be auto, sdk, claude-cli, codex-cli or pi-cli");
8234
- const model = str5(args.flags.model) ?? process.env.BUNSHIN_MODEL;
8258
+ const model = str5(args.flags.model) ?? process.env.BUNSHIN_MODEL ?? saved?.model;
8235
8259
  const harness = backend === "codex-cli" ? "codex" : backend === "pi-cli" ? "pi" : "claude-code";
8236
8260
  return { ...DEFAULT_CONFIG, backend, harness, ...model ? { model } : {}, ...model && backend === "codex-cli" ? { codexModel: model } : {}, ...model && backend === "pi-cli" ? { piModel: model } : {} };
8237
8261
  }
@@ -8244,7 +8268,7 @@ async function runOnce(c, args, out, err) {
8244
8268
  const [job] = await lease(c, args);
8245
8269
  if (!job) break;
8246
8270
  const started = Date.now();
8247
- const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3 });
8271
+ const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: workDir() });
8248
8272
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
8249
8273
  if (!res.ok) {
8250
8274
  const r2 = await call(c, "POST", `/jobs/${job.id}/fail`, { error: res.error.slice(0, 1900), model, runner: c.name });
@@ -8281,8 +8305,183 @@ If you cannot answer it, run: bunshin runner fail ${job.id} --error "<why>"
8281
8305
  Then run \`bunshin runner next\` again until it says there are no jobs.
8282
8306
  `;
8283
8307
  }
8308
+ function workDir() {
8309
+ const dir = path20.join(runnerHome(), "work");
8310
+ fs23.mkdirSync(dir, { recursive: true });
8311
+ return dir;
8312
+ }
8313
+ function which(name) {
8314
+ for (const d of [...(process.env.PATH || "").split(path20.delimiter), path20.join(os5.homedir(), ".local", "bin")]) {
8315
+ if (d && fs23.existsSync(path20.join(d, name))) return path20.join(d, name);
8316
+ }
8317
+ return null;
8318
+ }
8319
+ function scheduledCommand(backend) {
8320
+ const node = path20.dirname(process.execPath);
8321
+ const npx = fs23.existsSync(path20.join(node, "npx")) ? path20.join(node, "npx") : which("npx") ?? "npx";
8322
+ const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
8323
+ const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path20.dirname(bin)] : [], path20.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
8324
+ return { argv: [npx, "-y", PKG, "runner", "--once"], path: dirs.join(path20.delimiter) };
8325
+ }
8326
+ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
8327
+ const cmd = scheduledCommand(backend);
8328
+ return `<?xml version="1.0" encoding="UTF-8"?>
8329
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
8330
+ <plist version="1.0">
8331
+ <dict>
8332
+ <key>Label</key><string>${LAUNCHD_LABEL}</string>
8333
+ <key>ProgramArguments</key>
8334
+ <array>${cmd.argv.map((a) => `<string>${xml(a)}</string>`).join("")}</array>
8335
+ <key>EnvironmentVariables</key>
8336
+ <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
8337
+ <key>WorkingDirectory</key><string>${xml(home)}</string>
8338
+ <key>StartInterval</key><integer>${everyMinutes * 60}</integer>
8339
+ <key>StandardOutPath</key><string>${xml(path20.join(home, "runner.log"))}</string>
8340
+ <key>StandardErrorPath</key><string>${xml(path20.join(home, "runner.log"))}</string>
8341
+ <key>ProcessType</key><string>Background</string>
8342
+ <key>LowPriorityIO</key><true/>
8343
+ </dict>
8344
+ </plist>
8345
+ `;
8346
+ }
8347
+ function cronLine(backend, everyMinutes, home = runnerHome()) {
8348
+ const cmd = scheduledCommand(backend);
8349
+ const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
8350
+ const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
8351
+ return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path20.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
8352
+ }
8353
+ function launchAgentsDir(opts) {
8354
+ return opts.launchAgents ?? path20.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
8355
+ }
8356
+ function installSchedule(backend, everyMinutes, opts = {}) {
8357
+ const platform = opts.platform ?? process.platform;
8358
+ const run2 = opts.run ?? realRun;
8359
+ if (platform === "darwin") {
8360
+ const dir = launchAgentsDir(opts);
8361
+ const plist = path20.join(dir, `${LAUNCHD_LABEL}.plist`);
8362
+ fs23.mkdirSync(dir, { recursive: true });
8363
+ fs23.writeFileSync(plist, launchdPlist(backend, everyMinutes));
8364
+ const domain = `gui/${process.getuid?.() ?? 501}`;
8365
+ run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
8366
+ const r = run2("launchctl", ["bootstrap", domain, plist]);
8367
+ if (r.status !== 0) throw new Error(`launchctl bootstrap: ${(r.stderr || r.stdout).trim() || `exit ${r.status}`}`);
8368
+ return { kind: "launchd", where: plist, argv: scheduledCommand(backend).argv };
8369
+ }
8370
+ if (platform === "linux" || platform === "freebsd" || platform === "openbsd") {
8371
+ const current = run2("crontab", ["-l"]);
8372
+ const kept = (current.status === 0 ? current.stdout : "").split("\n").filter((l) => l.trim() && !l.includes(CRON_MARK));
8373
+ const r = run2("crontab", ["-"], [...kept, cronLine(backend, everyMinutes), ""].join("\n"));
8374
+ if (r.status !== 0) throw new Error(`crontab: ${(r.stderr || r.stdout).trim() || `exit ${r.status}`}`);
8375
+ return { kind: "cron", where: "crontab", argv: scheduledCommand(backend).argv };
8376
+ }
8377
+ return { kind: "none", where: "", argv: scheduledCommand(backend).argv };
8378
+ }
8379
+ function removeSchedule(opts = {}) {
8380
+ const platform = opts.platform ?? process.platform;
8381
+ const run2 = opts.run ?? realRun;
8382
+ if (platform === "darwin") {
8383
+ const plist = path20.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
8384
+ run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
8385
+ if (!fs23.existsSync(plist)) return "no schedule";
8386
+ fs23.rmSync(plist, { force: true });
8387
+ return `removed ${plist}`;
8388
+ }
8389
+ const current = run2("crontab", ["-l"]);
8390
+ if (current.status !== 0 || !current.stdout.includes(CRON_MARK)) return "no schedule";
8391
+ run2("crontab", ["-"], current.stdout.split("\n").filter((l) => !l.includes(CRON_MARK)).join("\n"));
8392
+ return "removed the crontab line";
8393
+ }
8394
+ async function install(args, cwd, out, err, opts = {}) {
8395
+ const c = connection(args, cwd);
8396
+ try {
8397
+ await call(c, "GET", "/status");
8398
+ } catch (e) {
8399
+ const m = e instanceof Error ? e.message : String(e);
8400
+ err(/40[13]/.test(m) ? `This key cannot answer model jobs. Create one in the console: Runners \u2192 New runner key (admins only).
8401
+ ` : `Could not reach ${c.apiUrl}: ${m}
8402
+ `);
8403
+ return 1;
8404
+ }
8405
+ const config = modelConfig(args);
8406
+ const backend = resolveBackend(config);
8407
+ if (!backend) {
8408
+ err("No model on this computer: install Claude Code (or Codex, or pi), sign in, and paste the command again.\n");
8409
+ return 1;
8410
+ }
8411
+ const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
8412
+ const home = runnerHome();
8413
+ fs23.mkdirSync(home, { recursive: true, mode: 448 });
8414
+ const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
8415
+ fs23.writeFileSync(path20.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
8416
+ fs23.chmodSync(path20.join(home, "config.json"), 384);
8417
+ let schedule;
8418
+ try {
8419
+ schedule = installSchedule(backend, every, opts);
8420
+ } catch (e) {
8421
+ err(`Saved the runner, but could not schedule it: ${e instanceof Error ? e.message : String(e)}
8422
+ Run it yourself: npx -y ${PKG} runner --once
8423
+ `);
8424
+ return 1;
8425
+ }
8426
+ out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
8427
+ `);
8428
+ out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG} runner --once\` on a schedule yourself.
8429
+ ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path20.join(home, "runner.log")}
8430
+ `);
8431
+ out(` Stop: npx -y ${PKG} runner uninstall
8432
+ `);
8433
+ if (args.flags.run === false) return 0;
8434
+ out(`
8435
+ Answering what waits now\u2026
8436
+ `);
8437
+ const r = await runOnce(c, args, out, err);
8438
+ out(r.done || r.failed ? `${r.done} answered, ${r.failed} failed.
8439
+ ` : `Nothing waiting. The next nightly's work is answered on schedule.
8440
+ `);
8441
+ return r.failed && !r.done ? 1 : 0;
8442
+ }
8443
+ function uninstall(out, opts = {}) {
8444
+ out(`${removeSchedule(opts)}
8445
+ `);
8446
+ const file = path20.join(runnerHome(), "config.json");
8447
+ if (fs23.existsSync(file)) {
8448
+ fs23.rmSync(file, { force: true });
8449
+ out(`removed ${file} (revoke the key in the console: Agent keys)
8450
+ `);
8451
+ }
8452
+ return 0;
8453
+ }
8454
+ function installPrompt(key) {
8455
+ return `Set up this computer as my team's Bunshin runner: it answers Bunshin's queued model work (lessons
8456
+ from work sessions, merging the shared documents) with this computer's own Claude Code / Codex / pi
8457
+ sign-in, about once an hour while the computer is on.
8458
+
8459
+ 1. Run: npx -y ${PKG} runner install --key ${key}
8460
+ 2. If it says there is no model on this computer, check \`claude --version\` (or codex / pi). If none
8461
+ is installed or signed in, tell me the one command to install or sign in, wait for me, then run
8462
+ step 1 again.
8463
+ 3. If it says the key cannot answer jobs or the API cannot be reached, stop and show me its message.
8464
+ 4. Finish with what it printed last: how many jobs it answered and how to stop it.
8465
+
8466
+ Do not change any repository or other files.`;
8467
+ }
8284
8468
  function setupText(target) {
8285
8469
  switch (target) {
8470
+ case "this-computer":
8471
+ return `This computer (Claude Code, Codex or pi signed in): one paste
8472
+
8473
+ 1. Console \u2192 Runners \u2192 New runner key. The page shows this command with the key in it:
8474
+
8475
+ npx -y ${PKG} runner install --key <runner key>
8476
+
8477
+ It checks the key, picks the model this computer is signed in to, answers every hour while the
8478
+ computer is on (launchd on macOS, cron on Linux) and answers what waits now.
8479
+ Stop: npx -y ${PKG} runner uninstall
8480
+
8481
+ Or paste this into Claude Code (or Codex, or pi) on that computer, and it does the rest:
8482
+
8483
+ ${installPrompt("<runner key>").replace(/^/gm, " ")}
8484
+ `;
8286
8485
  case "claude-routine":
8287
8486
  return `Claude Code routine (claude.ai/code \u2192 Routines, or /schedule in Claude Code)
8288
8487
 
@@ -8350,7 +8549,7 @@ CMD ["bunshin", "runner", "--watch", "--backend", "claude-cli", "--model", "clau
8350
8549
  17 3 * * * BUNSHIN_RUNNER_KEY=<key> bunshin runner --once --backend claude-cli --model "claude-opus-5-5[1m]" >> ~/.bunshin/runner.log 2>&1
8351
8550
  `;
8352
8551
  default:
8353
- return `bunshin runner setup <target>: claude-routine | codex-cloud | github-action | docker | cron
8552
+ return `bunshin runner setup <target>: this-computer | claude-routine | codex-cloud | github-action | docker | cron
8354
8553
 
8355
8554
  Or give the server a key and it runs model work itself (wrangler secret put \u2026):
8356
8555
  ANTHROPIC_API_KEY (+ BUNSHIN_MODEL, default claude-opus-5-5)
@@ -8359,12 +8558,14 @@ Or give the server a key and it runs model work itself (wrangler secret put \u20
8359
8558
  `;
8360
8559
  }
8361
8560
  }
8362
- async function cmdRunner(args, cwd, out, err) {
8561
+ async function cmdRunner(args, cwd, out, err, opts = {}) {
8363
8562
  const sub = args._[1] ?? "";
8364
8563
  if (sub === "setup") {
8365
8564
  out(setupText(args._[2] ?? ""));
8366
8565
  return 0;
8367
8566
  }
8567
+ if (sub === "install") return install(args, cwd, out, err, opts);
8568
+ if (sub === "uninstall") return uninstall(out, opts);
8368
8569
  const c = connection(args, cwd);
8369
8570
  if (sub === "status") {
8370
8571
  const s = (await call(c, "GET", "/status")).body;
@@ -8410,7 +8611,7 @@ async function cmdRunner(args, cwd, out, err) {
8410
8611
  `);
8411
8612
  return 0;
8412
8613
  }
8413
- if (sub && sub !== "run") throw new Error(`unknown runner command "${sub}" (run, next, submit, fail, status, setup)`);
8614
+ if (sub && sub !== "run") throw new Error(`unknown runner command "${sub}" (run, next, submit, fail, status, install, uninstall, setup)`);
8414
8615
  const watch = args.flags.watch === true;
8415
8616
  const interval = Number(str5(args.flags.interval) ?? 300) * 1e3;
8416
8617
  for (; ; ) {
@@ -8423,7 +8624,7 @@ async function cmdRunner(args, cwd, out, err) {
8423
8624
  await new Promise((resolve) => setTimeout(resolve, interval));
8424
8625
  }
8425
8626
  }
8426
- var DEFAULT_API, KINDS2, str5, NPX, ROUTINE_PROMPT;
8627
+ var DEFAULT_API, KINDS2, str5, LAUNCHD_LABEL, CRON_MARK, PKG, BIN_OF, xml, realRun, BACKEND_NAME, NPX, ROUTINE_PROMPT;
8427
8628
  var init_runner = __esm({
8428
8629
  "src/v2/runner.ts"() {
8429
8630
  "use strict";
@@ -8432,6 +8633,16 @@ var init_runner = __esm({
8432
8633
  DEFAULT_API = "https://api.bunshin.cc";
8433
8634
  KINDS2 = ["reflect", "merge", "create"];
8434
8635
  str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
8636
+ LAUNCHD_LABEL = "cc.bunshin.runner";
8637
+ CRON_MARK = "# bunshin-runner";
8638
+ PKG = "@bunshin-cc/bunshin@latest";
8639
+ BIN_OF = { "claude-cli": "claude", "codex-cli": "codex", "pi-cli": "pi" };
8640
+ xml = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8641
+ realRun = (cmd, args, input) => {
8642
+ const r = spawnSync3(cmd, args, { encoding: "utf8", ...input !== void 0 ? { input } : {} });
8643
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? r.error?.message ?? "" };
8644
+ };
8645
+ BACKEND_NAME = { sdk: "the Anthropic API key in this environment", "claude-cli": "Claude Code", "codex-cli": "Codex", "pi-cli": "pi" };
8435
8646
  NPX = "npx -y @bunshin-cc/bunshin@latest";
8436
8647
  ROUTINE_PROMPT = `You are this company's Bunshin runner. Bunshin queues model work (reflecting on work sessions,
8437
8648
  reconciling the shared documents); you answer it, then stop.
@@ -8448,7 +8659,7 @@ reconciling the shared documents); you answer it, then stop.
8448
8659
  // src/v2/importer.ts
8449
8660
  import { execFileSync as execFileSync3 } from "node:child_process";
8450
8661
  import fs24 from "node:fs";
8451
- import path20 from "node:path";
8662
+ import path21 from "node:path";
8452
8663
  function rewriteRepoPaths(text2, repoName2, pack) {
8453
8664
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
8454
8665
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -8487,7 +8698,7 @@ function buildDocs(files, opts) {
8487
8698
  const main2 = `${dir}/SKILL.md`;
8488
8699
  const text2 = rw(files.get(main2).content);
8489
8700
  const fm = frontmatterDescription(text2);
8490
- const name = path20.posix.basename(dir);
8701
+ const name = path21.posix.basename(dir);
8491
8702
  const bundle = {};
8492
8703
  for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
8493
8704
  claimed.add(p);
@@ -8588,11 +8799,11 @@ function workingTreeFiles(root) {
8588
8799
  return fs24.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
8589
8800
  if (e.name === ".git" || e.name === "node_modules") return [];
8590
8801
  const rel = base ? `${base}/${e.name}` : e.name;
8591
- return e.isDirectory() ? walk(path20.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
8802
+ return e.isDirectory() ? walk(path21.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
8592
8803
  });
8593
8804
  })(root);
8594
8805
  for (const rel of list2) {
8595
- const abs = path20.join(root, rel);
8806
+ const abs = path21.join(root, rel);
8596
8807
  let buf;
8597
8808
  try {
8598
8809
  buf = fs24.readFileSync(abs);
@@ -8634,7 +8845,7 @@ function diffDocs(prev, next) {
8634
8845
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
8635
8846
  }
8636
8847
  function readSkillSystem(root, opts) {
8637
- const repoName2 = path20.basename(path20.resolve(root));
8848
+ const repoName2 = path21.basename(path21.resolve(root));
8638
8849
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
8639
8850
  const tree = build(workingTreeFiles(root));
8640
8851
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -8711,7 +8922,7 @@ var init_importer = __esm({
8711
8922
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8712
8923
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
8713
8924
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
8714
- isUpdateHook = (p, text2) => /auto-?update/i.test(path20.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
8925
+ isUpdateHook = (p, text2) => /auto-?update/i.test(path21.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
8715
8926
  fingerprint = (d) => JSON.stringify(d);
8716
8927
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
8717
8928
 
@@ -8748,7 +8959,7 @@ __export(skills_exports, {
8748
8959
  parsePin: () => parsePin
8749
8960
  });
8750
8961
  import fs25 from "node:fs";
8751
- import path21 from "node:path";
8962
+ import path22 from "node:path";
8752
8963
  function requireOnline(store, what) {
8753
8964
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
8754
8965
  }
@@ -8757,9 +8968,9 @@ async function cmdImport2(store, args, cwd, out, err) {
8757
8968
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
8758
8969
  return 1;
8759
8970
  }
8760
- const root = path21.resolve(cwd, args._[2]);
8971
+ const root = path22.resolve(cwd, args._[2]);
8761
8972
  if (!fs25.existsSync(root)) throw new Error(`${root} does not exist`);
8762
- const pack = str6(args.flags.pack) ?? path21.basename(root);
8973
+ const pack = str6(args.flags.pack) ?? path22.basename(root);
8763
8974
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
8764
8975
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
8765
8976
  const kinds = /* @__PURE__ */ new Map();
@@ -8794,7 +9005,7 @@ async function cmdImport2(store, args, cwd, out, err) {
8794
9005
  const propose = args.flags["propose-rewrites"];
8795
9006
  if (propose) {
8796
9007
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
8797
- await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path21.basename(root), out, err);
9008
+ await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path22.basename(root), out, err);
8798
9009
  }
8799
9010
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
8800
9011
  return 0;
@@ -8934,7 +9145,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
8934
9145
  }
8935
9146
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
8936
9147
  if (graph) {
8937
- fs25.writeFileSync(path21.resolve(cwd, graph), renderSkillGraph(report));
9148
+ fs25.writeFileSync(path22.resolve(cwd, graph), renderSkillGraph(report));
8938
9149
  out(`graph written to ${graph}
8939
9150
  `);
8940
9151
  }
@@ -9033,8 +9244,8 @@ async function cmdLibrary(store, args, cwd, out, err) {
9033
9244
  `;
9034
9245
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
9035
9246
  if (file) {
9036
- const abs = path21.resolve(cwd, file);
9037
- fs25.mkdirSync(path21.dirname(abs), { recursive: true });
9247
+ const abs = path22.resolve(cwd, file);
9248
+ fs25.mkdirSync(path22.dirname(abs), { recursive: true });
9038
9249
  fs25.writeFileSync(abs, line);
9039
9250
  out(`${file}: ${line}`);
9040
9251
  } else out(line);
@@ -9045,11 +9256,11 @@ async function cmdLibrary(store, args, cwd, out, err) {
9045
9256
  const to = str6(args.flags.to);
9046
9257
  if (!to) throw new Error("--to <dir> is required");
9047
9258
  const pinFile = str6(args.flags.pin);
9048
- const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs25.readFileSync(path21.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
9259
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs25.readFileSync(path22.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
9049
9260
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
9050
9261
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
9051
9262
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
9052
- const dir = path21.resolve(cwd, to);
9263
+ const dir = path22.resolve(cwd, to);
9053
9264
  if (fs25.existsSync(dir) && fs25.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
9054
9265
  const pack = str6(args.flags.pack);
9055
9266
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
@@ -9090,11 +9301,11 @@ __export(reflect_exports, {
9090
9301
  runReflection: () => runReflection
9091
9302
  });
9092
9303
  import fs26 from "node:fs";
9093
- import path22 from "node:path";
9304
+ import path23 from "node:path";
9094
9305
  import { createHash as createHash7 } from "node:crypto";
9095
9306
  async function runReflection(store, opts) {
9096
9307
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
9097
- const lock = path22.join(store.root, `reflect-${createHash7("sha256").update(opts.sessionId).digest("hex")}.lock`);
9308
+ const lock = path23.join(store.root, `reflect-${createHash7("sha256").update(opts.sessionId).digest("hex")}.lock`);
9098
9309
  const deadline = Date.now() + 66e4;
9099
9310
  for (; ; ) {
9100
9311
  try {
@@ -9467,7 +9678,7 @@ __export(cli_exports, {
9467
9678
  version: () => version
9468
9679
  });
9469
9680
  import fs27 from "node:fs";
9470
- import path23 from "node:path";
9681
+ import path24 from "node:path";
9471
9682
  function parseArgs(argv) {
9472
9683
  const out = { _: [], flags: {} };
9473
9684
  const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once"]);
@@ -9578,7 +9789,7 @@ async function main(argv, opts = {}) {
9578
9789
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
9579
9790
  const agent = harness === "claude-code" ? "claude" : harness;
9580
9791
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
9581
- const existing = new Store(path23.join(cwd, ".bunshin")).config();
9792
+ const existing = new Store(path24.join(cwd, ".bunshin")).config();
9582
9793
  const apiUrl = str7(args.flags["api-url"]);
9583
9794
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
9584
9795
  let value = "";
@@ -9621,7 +9832,7 @@ Key saved privately outside the project.
9621
9832
  if (cmd === "mcp") {
9622
9833
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
9623
9834
  const root = str7(args.flags.root);
9624
- await runMcpServer2({ cwd, ...root ? { store: new Store(path23.resolve(root)) } : {}, version: version() });
9835
+ await runMcpServer2({ cwd, ...root ? { store: new Store(path24.resolve(root)) } : {}, version: version() });
9625
9836
  return 0;
9626
9837
  }
9627
9838
  if (cmd === "demo") {
@@ -9704,7 +9915,7 @@ ${HELP2}`);
9704
9915
  function openStore(args, cwd) {
9705
9916
  const root = str7(args.flags.root);
9706
9917
  if (root) {
9707
- const store = new Store(path23.resolve(root));
9918
+ const store = new Store(path24.resolve(root));
9708
9919
  store.ensureLayout();
9709
9920
  return store;
9710
9921
  }
@@ -9763,7 +9974,7 @@ function cmdStatus(store, args, out, v2) {
9763
9974
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
9764
9975
  const metrics = store.readMetrics(void 0);
9765
9976
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
9766
- const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path23.join(process.env.CODEX_HOME ?? path23.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path23.dirname(store.root)).file])] : config.harness === "claude-code" ? [path23.join(homeDir(), ".claude", "settings.json"), path23.join(path23.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path23.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path23.join(path23.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
9977
+ const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path24.join(process.env.CODEX_HOME ?? path24.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path24.dirname(store.root)).file])] : config.harness === "claude-code" ? [path24.join(homeDir(), ".claude", "settings.json"), path24.join(path24.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path24.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path24.join(path24.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
9767
9978
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
9768
9979
  try {
9769
9980
  return fs27.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
@@ -10010,7 +10221,7 @@ async function cmdReflect(store, args, cwd, out, err) {
10010
10221
  }
10011
10222
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
10012
10223
  const result = await runReflection2(store, {
10013
- sessionId: str7(args.flags.session) ?? path23.basename(transcript).replace(/\.jsonl?$/, ""),
10224
+ sessionId: str7(args.flags.session) ?? path24.basename(transcript).replace(/\.jsonl?$/, ""),
10014
10225
  transcriptPath: transcript,
10015
10226
  cwd,
10016
10227
  force: bool5(args.flags.force)
@@ -10182,7 +10393,9 @@ Usage: bunshin <command> [options]
10182
10393
  reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
10183
10394
  runner next | submit <job> <file|-> | fail <job> --error <why>
10184
10395
  agent mode, for a Claude Code routine or Codex cloud task
10185
- runner status | setup [claude-routine|codex-cloud|github-action|docker|cron]
10396
+ runner install --key <runner key> [--every <minutes>] [--no-run] | uninstall
10397
+ this computer answers it every hour (launchd / cron)
10398
+ runner status | setup [this-computer|claude-routine|codex-cloud|github-action|docker|cron]
10186
10399
  reflect --fork --session <id> [--focus "<text>"] (what the session-end hook detaches)
10187
10400
 
10188
10401
  lessons [list] [--kind fact|workflow] [--q <query>] [--json] (legacy lesson store)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {