@bunshin-cc/bunshin 0.5.0 → 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 +268 -54
  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 },
@@ -5258,9 +5259,9 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
5258
5259
  const from = reflectCursor(session, turns);
5259
5260
  const fresh = sinceTurn(scan, from);
5260
5261
  const signals = { ...sessionSignals({ scan: fresh, startedAt: session.startedAt, lastActivityAt: session.lastActivityAt, partial: from > 0 }), ...outcome ? { outcome: outcome.status } : {} };
5261
- const gate = sessionGate(signals, { now: now.toISOString(), cacheTtlMs: config.reflect.cacheTtlMs });
5262
5262
  const id = episodeIdFor(view.ctx.id, session.sessionId);
5263
5263
  const previous = v2.readEpisode(id);
5264
+ const gate = sessionGate(signals, { now: now.toISOString(), cacheTtlMs: config.reflect.cacheTtlMs });
5264
5265
  const upload = config.upload ?? "redacted";
5265
5266
  const calls = v2.readCalls(session.sessionId);
5266
5267
  const snippets = [.../* @__PURE__ */ new Set([...v2.readSnippets(session.sessionId), ...scan.snippets])];
@@ -5279,7 +5280,8 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
5279
5280
  suggestionIds: [.../* @__PURE__ */ new Set([...previous?.suggestionIds ?? [], ...session.suggestionIds])],
5280
5281
  // "edge" only while this machine's reflections cover every turn; new work is open again.
5281
5282
  reflected: previous?.reflected === "edge" && from >= turns ? "edge" : "none",
5282
- gate,
5283
+ // Nothing new since the last reflection: the header keeps the gate that judged that work.
5284
+ gate: turns <= from && previous?.gate ? previous.gate : gate,
5283
5285
  upload,
5284
5286
  turns,
5285
5287
  reflectedTurns: from,
@@ -8180,22 +8182,44 @@ Common flags: --api-url URL (default: this project's, else https://api.bunshin.c
8180
8182
  // src/v2/runner.ts
8181
8183
  var runner_exports = {};
8182
8184
  __export(runner_exports, {
8185
+ LAUNCHD_LABEL: () => LAUNCHD_LABEL,
8183
8186
  ROUTINE_PROMPT: () => ROUTINE_PROMPT,
8184
8187
  cmdRunner: () => cmdRunner,
8185
8188
  connection: () => connection,
8189
+ cronLine: () => cronLine,
8190
+ installPrompt: () => installPrompt,
8191
+ installSchedule: () => installSchedule,
8192
+ launchdPlist: () => launchdPlist,
8193
+ readInstalled: () => readInstalled,
8194
+ removeSchedule: () => removeSchedule,
8186
8195
  renderJob: () => renderJob,
8187
8196
  runOnce: () => runOnce,
8197
+ runnerHome: () => runnerHome,
8198
+ scheduledCommand: () => scheduledCommand,
8188
8199
  setupText: () => setupText
8189
8200
  });
8201
+ import { spawnSync as spawnSync3 } from "node:child_process";
8190
8202
  import fs23 from "node:fs";
8191
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
+ }
8192
8215
  function connection(args, cwd) {
8193
- const store = Store.discover(cwd);
8216
+ const saved = readInstalled();
8217
+ const store = saved && path20.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
8194
8218
  const config = store?.config();
8195
- const key = process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || "";
8196
- 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(/\/$/, "");
8197
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");
8198
- 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"}`;
8199
8223
  return { apiUrl, key, name: name.replace(/[^\w.:@/-]/g, "-").slice(0, 80) };
8200
8224
  }
8201
8225
  async function call(c, method, route, body, timeoutMs = 6e4) {
@@ -8228,9 +8252,10 @@ async function lease(c, args, max = 1) {
8228
8252
  return (await call(c, "POST", "/lease", { max, runner: c.name, ...kinds ? { kinds } : {} }, 12e4)).body.jobs;
8229
8253
  }
8230
8254
  function modelConfig(args) {
8231
- const backend = str5(args.flags.backend) ?? "auto";
8255
+ const saved = readInstalled();
8256
+ const backend = str5(args.flags.backend) ?? saved?.backend ?? "auto";
8232
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");
8233
- const model = str5(args.flags.model) ?? process.env.BUNSHIN_MODEL;
8258
+ const model = str5(args.flags.model) ?? process.env.BUNSHIN_MODEL ?? saved?.model;
8234
8259
  const harness = backend === "codex-cli" ? "codex" : backend === "pi-cli" ? "pi" : "claude-code";
8235
8260
  return { ...DEFAULT_CONFIG, backend, harness, ...model ? { model } : {}, ...model && backend === "codex-cli" ? { codexModel: model } : {}, ...model && backend === "pi-cli" ? { piModel: model } : {} };
8236
8261
  }
@@ -8243,7 +8268,7 @@ async function runOnce(c, args, out, err) {
8243
8268
  const [job] = await lease(c, args);
8244
8269
  if (!job) break;
8245
8270
  const started = Date.now();
8246
- 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() });
8247
8272
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
8248
8273
  if (!res.ok) {
8249
8274
  const r2 = await call(c, "POST", `/jobs/${job.id}/fail`, { error: res.error.slice(0, 1900), model, runner: c.name });
@@ -8280,8 +8305,183 @@ If you cannot answer it, run: bunshin runner fail ${job.id} --error "<why>"
8280
8305
  Then run \`bunshin runner next\` again until it says there are no jobs.
8281
8306
  `;
8282
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
+ }
8283
8468
  function setupText(target) {
8284
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
+ `;
8285
8485
  case "claude-routine":
8286
8486
  return `Claude Code routine (claude.ai/code \u2192 Routines, or /schedule in Claude Code)
8287
8487
 
@@ -8349,7 +8549,7 @@ CMD ["bunshin", "runner", "--watch", "--backend", "claude-cli", "--model", "clau
8349
8549
  17 3 * * * BUNSHIN_RUNNER_KEY=<key> bunshin runner --once --backend claude-cli --model "claude-opus-5-5[1m]" >> ~/.bunshin/runner.log 2>&1
8350
8550
  `;
8351
8551
  default:
8352
- 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
8353
8553
 
8354
8554
  Or give the server a key and it runs model work itself (wrangler secret put \u2026):
8355
8555
  ANTHROPIC_API_KEY (+ BUNSHIN_MODEL, default claude-opus-5-5)
@@ -8358,12 +8558,14 @@ Or give the server a key and it runs model work itself (wrangler secret put \u20
8358
8558
  `;
8359
8559
  }
8360
8560
  }
8361
- async function cmdRunner(args, cwd, out, err) {
8561
+ async function cmdRunner(args, cwd, out, err, opts = {}) {
8362
8562
  const sub = args._[1] ?? "";
8363
8563
  if (sub === "setup") {
8364
8564
  out(setupText(args._[2] ?? ""));
8365
8565
  return 0;
8366
8566
  }
8567
+ if (sub === "install") return install(args, cwd, out, err, opts);
8568
+ if (sub === "uninstall") return uninstall(out, opts);
8367
8569
  const c = connection(args, cwd);
8368
8570
  if (sub === "status") {
8369
8571
  const s = (await call(c, "GET", "/status")).body;
@@ -8409,7 +8611,7 @@ async function cmdRunner(args, cwd, out, err) {
8409
8611
  `);
8410
8612
  return 0;
8411
8613
  }
8412
- 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)`);
8413
8615
  const watch = args.flags.watch === true;
8414
8616
  const interval = Number(str5(args.flags.interval) ?? 300) * 1e3;
8415
8617
  for (; ; ) {
@@ -8422,7 +8624,7 @@ async function cmdRunner(args, cwd, out, err) {
8422
8624
  await new Promise((resolve) => setTimeout(resolve, interval));
8423
8625
  }
8424
8626
  }
8425
- 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;
8426
8628
  var init_runner = __esm({
8427
8629
  "src/v2/runner.ts"() {
8428
8630
  "use strict";
@@ -8431,6 +8633,16 @@ var init_runner = __esm({
8431
8633
  DEFAULT_API = "https://api.bunshin.cc";
8432
8634
  KINDS2 = ["reflect", "merge", "create"];
8433
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" };
8434
8646
  NPX = "npx -y @bunshin-cc/bunshin@latest";
8435
8647
  ROUTINE_PROMPT = `You are this company's Bunshin runner. Bunshin queues model work (reflecting on work sessions,
8436
8648
  reconciling the shared documents); you answer it, then stop.
@@ -8447,7 +8659,7 @@ reconciling the shared documents); you answer it, then stop.
8447
8659
  // src/v2/importer.ts
8448
8660
  import { execFileSync as execFileSync3 } from "node:child_process";
8449
8661
  import fs24 from "node:fs";
8450
- import path20 from "node:path";
8662
+ import path21 from "node:path";
8451
8663
  function rewriteRepoPaths(text2, repoName2, pack) {
8452
8664
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
8453
8665
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -8486,7 +8698,7 @@ function buildDocs(files, opts) {
8486
8698
  const main2 = `${dir}/SKILL.md`;
8487
8699
  const text2 = rw(files.get(main2).content);
8488
8700
  const fm = frontmatterDescription(text2);
8489
- const name = path20.posix.basename(dir);
8701
+ const name = path21.posix.basename(dir);
8490
8702
  const bundle = {};
8491
8703
  for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
8492
8704
  claimed.add(p);
@@ -8587,11 +8799,11 @@ function workingTreeFiles(root) {
8587
8799
  return fs24.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
8588
8800
  if (e.name === ".git" || e.name === "node_modules") return [];
8589
8801
  const rel = base ? `${base}/${e.name}` : e.name;
8590
- 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] : [];
8591
8803
  });
8592
8804
  })(root);
8593
8805
  for (const rel of list2) {
8594
- const abs = path20.join(root, rel);
8806
+ const abs = path21.join(root, rel);
8595
8807
  let buf;
8596
8808
  try {
8597
8809
  buf = fs24.readFileSync(abs);
@@ -8633,7 +8845,7 @@ function diffDocs(prev, next) {
8633
8845
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
8634
8846
  }
8635
8847
  function readSkillSystem(root, opts) {
8636
- const repoName2 = path20.basename(path20.resolve(root));
8848
+ const repoName2 = path21.basename(path21.resolve(root));
8637
8849
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
8638
8850
  const tree = build(workingTreeFiles(root));
8639
8851
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -8710,7 +8922,7 @@ var init_importer = __esm({
8710
8922
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8711
8923
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
8712
8924
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
8713
- 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);
8714
8926
  fingerprint = (d) => JSON.stringify(d);
8715
8927
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
8716
8928
 
@@ -8747,7 +8959,7 @@ __export(skills_exports, {
8747
8959
  parsePin: () => parsePin
8748
8960
  });
8749
8961
  import fs25 from "node:fs";
8750
- import path21 from "node:path";
8962
+ import path22 from "node:path";
8751
8963
  function requireOnline(store, what) {
8752
8964
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
8753
8965
  }
@@ -8756,9 +8968,9 @@ async function cmdImport2(store, args, cwd, out, err) {
8756
8968
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
8757
8969
  return 1;
8758
8970
  }
8759
- const root = path21.resolve(cwd, args._[2]);
8971
+ const root = path22.resolve(cwd, args._[2]);
8760
8972
  if (!fs25.existsSync(root)) throw new Error(`${root} does not exist`);
8761
- const pack = str6(args.flags.pack) ?? path21.basename(root);
8973
+ const pack = str6(args.flags.pack) ?? path22.basename(root);
8762
8974
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
8763
8975
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
8764
8976
  const kinds = /* @__PURE__ */ new Map();
@@ -8793,7 +9005,7 @@ async function cmdImport2(store, args, cwd, out, err) {
8793
9005
  const propose = args.flags["propose-rewrites"];
8794
9006
  if (propose) {
8795
9007
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
8796
- 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);
8797
9009
  }
8798
9010
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
8799
9011
  return 0;
@@ -8933,7 +9145,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
8933
9145
  }
8934
9146
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
8935
9147
  if (graph) {
8936
- fs25.writeFileSync(path21.resolve(cwd, graph), renderSkillGraph(report));
9148
+ fs25.writeFileSync(path22.resolve(cwd, graph), renderSkillGraph(report));
8937
9149
  out(`graph written to ${graph}
8938
9150
  `);
8939
9151
  }
@@ -9032,8 +9244,8 @@ async function cmdLibrary(store, args, cwd, out, err) {
9032
9244
  `;
9033
9245
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
9034
9246
  if (file) {
9035
- const abs = path21.resolve(cwd, file);
9036
- fs25.mkdirSync(path21.dirname(abs), { recursive: true });
9247
+ const abs = path22.resolve(cwd, file);
9248
+ fs25.mkdirSync(path22.dirname(abs), { recursive: true });
9037
9249
  fs25.writeFileSync(abs, line);
9038
9250
  out(`${file}: ${line}`);
9039
9251
  } else out(line);
@@ -9044,11 +9256,11 @@ async function cmdLibrary(store, args, cwd, out, err) {
9044
9256
  const to = str6(args.flags.to);
9045
9257
  if (!to) throw new Error("--to <dir> is required");
9046
9258
  const pinFile = str6(args.flags.pin);
9047
- 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] ?? "");
9048
9260
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
9049
9261
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
9050
9262
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
9051
- const dir = path21.resolve(cwd, to);
9263
+ const dir = path22.resolve(cwd, to);
9052
9264
  if (fs25.existsSync(dir) && fs25.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
9053
9265
  const pack = str6(args.flags.pack);
9054
9266
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
@@ -9089,11 +9301,11 @@ __export(reflect_exports, {
9089
9301
  runReflection: () => runReflection
9090
9302
  });
9091
9303
  import fs26 from "node:fs";
9092
- import path22 from "node:path";
9304
+ import path23 from "node:path";
9093
9305
  import { createHash as createHash7 } from "node:crypto";
9094
9306
  async function runReflection(store, opts) {
9095
9307
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
9096
- 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`);
9097
9309
  const deadline = Date.now() + 66e4;
9098
9310
  for (; ; ) {
9099
9311
  try {
@@ -9466,7 +9678,7 @@ __export(cli_exports, {
9466
9678
  version: () => version
9467
9679
  });
9468
9680
  import fs27 from "node:fs";
9469
- import path23 from "node:path";
9681
+ import path24 from "node:path";
9470
9682
  function parseArgs(argv) {
9471
9683
  const out = { _: [], flags: {} };
9472
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"]);
@@ -9577,7 +9789,7 @@ async function main(argv, opts = {}) {
9577
9789
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
9578
9790
  const agent = harness === "claude-code" ? "claude" : harness;
9579
9791
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
9580
- const existing = new Store(path23.join(cwd, ".bunshin")).config();
9792
+ const existing = new Store(path24.join(cwd, ".bunshin")).config();
9581
9793
  const apiUrl = str7(args.flags["api-url"]);
9582
9794
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
9583
9795
  let value = "";
@@ -9620,7 +9832,7 @@ Key saved privately outside the project.
9620
9832
  if (cmd === "mcp") {
9621
9833
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
9622
9834
  const root = str7(args.flags.root);
9623
- 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() });
9624
9836
  return 0;
9625
9837
  }
9626
9838
  if (cmd === "demo") {
@@ -9703,7 +9915,7 @@ ${HELP2}`);
9703
9915
  function openStore(args, cwd) {
9704
9916
  const root = str7(args.flags.root);
9705
9917
  if (root) {
9706
- const store = new Store(path23.resolve(root));
9918
+ const store = new Store(path24.resolve(root));
9707
9919
  store.ensureLayout();
9708
9920
  return store;
9709
9921
  }
@@ -9762,7 +9974,7 @@ function cmdStatus(store, args, out, v2) {
9762
9974
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
9763
9975
  const metrics = store.readMetrics(void 0);
9764
9976
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
9765
- 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")] : [];
9766
9978
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
9767
9979
  try {
9768
9980
  return fs27.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
@@ -10009,7 +10221,7 @@ async function cmdReflect(store, args, cwd, out, err) {
10009
10221
  }
10010
10222
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
10011
10223
  const result = await runReflection2(store, {
10012
- sessionId: str7(args.flags.session) ?? path23.basename(transcript).replace(/\.jsonl?$/, ""),
10224
+ sessionId: str7(args.flags.session) ?? path24.basename(transcript).replace(/\.jsonl?$/, ""),
10013
10225
  transcriptPath: transcript,
10014
10226
  cwd,
10015
10227
  force: bool5(args.flags.force)
@@ -10181,7 +10393,9 @@ Usage: bunshin <command> [options]
10181
10393
  reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
10182
10394
  runner next | submit <job> <file|-> | fail <job> --error <why>
10183
10395
  agent mode, for a Claude Code routine or Codex cloud task
10184
- 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]
10185
10399
  reflect --fork --session <id> [--focus "<text>"] (what the session-end hook detaches)
10186
10400
 
10187
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.0",
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": {