@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.
- package/dist/bin.js +265 -52
- 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,
|
|
957
|
-
return anchor ? `repo:${repo}/${
|
|
956
|
+
function repoSectionId(repo, path25, anchor = "") {
|
|
957
|
+
return anchor ? `repo:${repo}/${path25}#${anchor}` : `repo:${repo}/${path25}`;
|
|
958
958
|
}
|
|
959
|
-
function docKind(
|
|
960
|
-
const base =
|
|
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(
|
|
966
|
-
const dir =
|
|
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,
|
|
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([...
|
|
1115
|
-
else if (!state.get(d)) dfs(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(
|
|
1203
|
-
return `harness:${
|
|
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(
|
|
1330
|
-
const p =
|
|
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
|
|
1422
|
-
const entity =
|
|
1423
|
-
const skill =
|
|
1424
|
-
const trigger =
|
|
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
|
|
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"])
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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) ??
|
|
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,
|
|
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(
|
|
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 =
|
|
9037
|
-
fs25.mkdirSync(
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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([
|
|
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) ??
|
|
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
|
|
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)
|