@bunshin-cc/bunshin 0.9.0 → 0.10.0
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 +709 -403
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -155,8 +155,8 @@ async function askClaudeCli(opts) {
|
|
|
155
155
|
return raw;
|
|
156
156
|
}
|
|
157
157
|
async function askCodexCli(opts) {
|
|
158
|
-
const
|
|
159
|
-
const outFile = path.join(
|
|
158
|
+
const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-codex-"));
|
|
159
|
+
const outFile = path.join(dir2, "answer.txt");
|
|
160
160
|
const args = ["exec", "--skip-git-repo-check", "--ephemeral", "-s", "read-only", "-o", outFile];
|
|
161
161
|
if (opts.model) args.push("-m", opts.model);
|
|
162
162
|
if (opts.effort) args.push("-c", `model_reasoning_effort="${opts.effort}"`);
|
|
@@ -170,7 +170,7 @@ ${opts.user}`;
|
|
|
170
170
|
await run("codex", args, { stdin: prompt, cwd: opts.cwd, timeoutMs: opts.timeoutMs ?? 3e5, backend: "codex-cli" });
|
|
171
171
|
return fs.readFileSync(outFile, "utf8");
|
|
172
172
|
} finally {
|
|
173
|
-
fs.rmSync(
|
|
173
|
+
fs.rmSync(dir2, { recursive: true, force: true });
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
async function askPiCli(opts) {
|
|
@@ -312,8 +312,8 @@ import path2 from "node:path";
|
|
|
312
312
|
function newId(prefix) {
|
|
313
313
|
return `${prefix}_${randomBytes(4).toString("hex")}`;
|
|
314
314
|
}
|
|
315
|
-
function ensureDir(
|
|
316
|
-
fs2.mkdirSync(
|
|
315
|
+
function ensureDir(dir2, mode = 448) {
|
|
316
|
+
fs2.mkdirSync(dir2, { recursive: true, mode });
|
|
317
317
|
}
|
|
318
318
|
function realish(p) {
|
|
319
319
|
const abs = path2.resolve(p);
|
|
@@ -450,9 +450,9 @@ function trustStore(root) {
|
|
|
450
450
|
const real = realish(root);
|
|
451
451
|
const list2 = trustedStores();
|
|
452
452
|
if (list2.includes(real)) return;
|
|
453
|
-
const
|
|
454
|
-
fs3.mkdirSync(
|
|
455
|
-
fs3.chmodSync(
|
|
453
|
+
const dir2 = configDir();
|
|
454
|
+
fs3.mkdirSync(dir2, { recursive: true, mode: 448 });
|
|
455
|
+
fs3.chmodSync(dir2, 448);
|
|
456
456
|
const file = trustFile();
|
|
457
457
|
const tmp = `${file}.${process.pid}.${randomBytes2(3).toString("hex")}.tmp`;
|
|
458
458
|
fs3.writeFileSync(tmp, JSON.stringify({ stores: [...list2, real] }, null, 2) + "\n", { mode: 384 });
|
|
@@ -616,13 +616,13 @@ var init_store = __esm({
|
|
|
616
616
|
}
|
|
617
617
|
static candidates(cwd) {
|
|
618
618
|
const out = [];
|
|
619
|
-
let
|
|
619
|
+
let dir2 = path4.resolve(cwd);
|
|
620
620
|
for (; ; ) {
|
|
621
|
-
const candidate = path4.join(
|
|
621
|
+
const candidate = path4.join(dir2, ".bunshin");
|
|
622
622
|
if (fileExists(path4.join(candidate, "config.json"))) out.push(candidate);
|
|
623
|
-
const parent = path4.dirname(
|
|
624
|
-
if (parent ===
|
|
625
|
-
|
|
623
|
+
const parent = path4.dirname(dir2);
|
|
624
|
+
if (parent === dir2) return out;
|
|
625
|
+
dir2 = parent;
|
|
626
626
|
}
|
|
627
627
|
}
|
|
628
628
|
static init(root, config) {
|
|
@@ -1200,10 +1200,10 @@ function isLocalLayer(layer) {
|
|
|
1200
1200
|
return kind === "user" || kind === "robot";
|
|
1201
1201
|
}
|
|
1202
1202
|
function withScope(layers, scope) {
|
|
1203
|
-
const
|
|
1204
|
-
if (!
|
|
1203
|
+
const path29 = scopePath(scope?.tree, scope?.source);
|
|
1204
|
+
if (!path29.length) return layers;
|
|
1205
1205
|
const i = layers.findIndex((l) => isLocalLayer(l));
|
|
1206
|
-
return i < 0 ? [...layers, ...
|
|
1206
|
+
return i < 0 ? [...layers, ...path29] : [...layers.slice(0, i), ...path29, ...layers.slice(i)];
|
|
1207
1207
|
}
|
|
1208
1208
|
function readerFor(ctx, ownerClearance, scope) {
|
|
1209
1209
|
if (ctx.kind === "robot" && !scope?.source) scope = { ...scope, source: `robot:${ctx.id}` };
|
|
@@ -1250,11 +1250,11 @@ function baseAffinity(section, reader) {
|
|
|
1250
1250
|
const kind = layerKind(section.layer);
|
|
1251
1251
|
if (kind === "group" || kind === "source") {
|
|
1252
1252
|
if (!reader.layers.includes(section.layer)) return 0.05;
|
|
1253
|
-
const
|
|
1253
|
+
const path29 = reader.layers.filter((l) => {
|
|
1254
1254
|
const k = layerKind(l);
|
|
1255
1255
|
return k === "group" || k === "source";
|
|
1256
1256
|
});
|
|
1257
|
-
return 0.6 + 0.4 * ((
|
|
1257
|
+
return 0.6 + 0.4 * ((path29.indexOf(section.layer) + 1) / path29.length);
|
|
1258
1258
|
}
|
|
1259
1259
|
if (isLocalLayer(section.layer)) return reader.layers.includes(section.layer) ? 1 : 0;
|
|
1260
1260
|
return 0.5;
|
|
@@ -1454,19 +1454,19 @@ var init_document = __esm({
|
|
|
1454
1454
|
function repoLayer(repo) {
|
|
1455
1455
|
return `source:${repo.includes("/") ? "repo" : "folder"}:${repo}`;
|
|
1456
1456
|
}
|
|
1457
|
-
function repoSectionId(repo,
|
|
1458
|
-
return anchor ? `repo:${repo}/${
|
|
1457
|
+
function repoSectionId(repo, path29, anchor = "") {
|
|
1458
|
+
return anchor ? `repo:${repo}/${path29}#${anchor}` : `repo:${repo}/${path29}`;
|
|
1459
1459
|
}
|
|
1460
|
-
function docKind(
|
|
1461
|
-
const base =
|
|
1460
|
+
function docKind(path29) {
|
|
1461
|
+
const base = path29.split("/").pop()?.toLowerCase() ?? "";
|
|
1462
1462
|
if (base === "testing.md") return "verification";
|
|
1463
1463
|
if (base === "skill.md") return "skill";
|
|
1464
1464
|
return "spec";
|
|
1465
1465
|
}
|
|
1466
|
-
function defaultPaths(
|
|
1467
|
-
const
|
|
1468
|
-
if (!
|
|
1469
|
-
return [`${
|
|
1466
|
+
function defaultPaths(path29) {
|
|
1467
|
+
const dir2 = path29.split("/").slice(0, -1).join("/");
|
|
1468
|
+
if (!dir2 || dir2.startsWith(".claude/") || dir2.startsWith(".codex/")) return [];
|
|
1469
|
+
return [`${dir2}/**`];
|
|
1470
1470
|
}
|
|
1471
1471
|
function parseRepoDoc(doc, opts) {
|
|
1472
1472
|
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1609,11 +1609,11 @@ function lintSkills(sections) {
|
|
|
1609
1609
|
}
|
|
1610
1610
|
const state = /* @__PURE__ */ new Map();
|
|
1611
1611
|
const cycles = /* @__PURE__ */ new Set();
|
|
1612
|
-
const dfs = (n,
|
|
1612
|
+
const dfs = (n, path29) => {
|
|
1613
1613
|
state.set(n, 1);
|
|
1614
1614
|
for (const d of edges[n] ?? []) {
|
|
1615
|
-
if (state.get(d) === 1) cycles.add([...
|
|
1616
|
-
else if (!state.get(d)) dfs(d, [...
|
|
1615
|
+
if (state.get(d) === 1) cycles.add([...path29.slice(path29.indexOf(d)), d].join(" -> "));
|
|
1616
|
+
else if (!state.get(d)) dfs(d, [...path29, d]);
|
|
1617
1617
|
}
|
|
1618
1618
|
state.set(n, 2);
|
|
1619
1619
|
};
|
|
@@ -1700,8 +1700,8 @@ function parsePythonSkills(text2) {
|
|
|
1700
1700
|
}
|
|
1701
1701
|
return { skills, constants };
|
|
1702
1702
|
}
|
|
1703
|
-
function harnessSectionId(
|
|
1704
|
-
return `harness:${
|
|
1703
|
+
function harnessSectionId(path29, symbol) {
|
|
1704
|
+
return `harness:${path29}#${symbol}`;
|
|
1705
1705
|
}
|
|
1706
1706
|
var DEF_RE;
|
|
1707
1707
|
var init_python = __esm({
|
|
@@ -1827,8 +1827,8 @@ function globToRegExp(glob) {
|
|
|
1827
1827
|
globCache.set(glob, out);
|
|
1828
1828
|
return out;
|
|
1829
1829
|
}
|
|
1830
|
-
function matchesGlob(
|
|
1831
|
-
const p =
|
|
1830
|
+
function matchesGlob(path29, glob) {
|
|
1831
|
+
const p = path29.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
1832
1832
|
return globToRegExp(glob.replace(/^\.\//, "")).test(p);
|
|
1833
1833
|
}
|
|
1834
1834
|
var DEFAULT_EMBEDDERS, globCache;
|
|
@@ -1920,10 +1920,10 @@ function push(sections, reader, query) {
|
|
|
1920
1920
|
for (const s of recallable(sections, reader).filter((x) => reachesReader(x, reader))) {
|
|
1921
1921
|
const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
|
|
1922
1922
|
for (const t of triggers) {
|
|
1923
|
-
const
|
|
1924
|
-
const entity =
|
|
1925
|
-
const skill =
|
|
1926
|
-
const trigger =
|
|
1923
|
+
const path29 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
|
|
1924
|
+
const entity = path29 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
|
|
1925
|
+
const skill = path29 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
|
|
1926
|
+
const trigger = path29 ? `path:${path29}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
|
|
1927
1927
|
if (trigger) {
|
|
1928
1928
|
hits.set(s.id, { section: s, trigger, anchor: t.anchor });
|
|
1929
1929
|
break;
|
|
@@ -2078,6 +2078,53 @@ var init_engine = __esm({
|
|
|
2078
2078
|
}
|
|
2079
2079
|
});
|
|
2080
2080
|
|
|
2081
|
+
// ../core/src/v2/recall/router.ts
|
|
2082
|
+
function routerMessage(prompt, ctx, inContext2) {
|
|
2083
|
+
const lines = ["Session context:"];
|
|
2084
|
+
lines.push(ctx.first ? `- First prompt of this session: ${ctx.first.slice(0, 600)}` : "- This is the first prompt of the session.");
|
|
2085
|
+
if (ctx.recent?.length) lines.push("- Recent user prompts (oldest first):", ...ctx.recent.slice(-3).map((r) => ` \xB7 ${r.slice(0, 300)}`));
|
|
2086
|
+
if (ctx.reply) lines.push(`- Agent's last reply: ${ctx.reply.slice(0, 1500)}`);
|
|
2087
|
+
lines.push(`- Already in the agent's context: ${inContext2.length ? inContext2.join(", ") : "nothing from the library"}`);
|
|
2088
|
+
return `${lines.join("\n")}
|
|
2089
|
+
|
|
2090
|
+
Latest user prompt (verbatim, may be truncated):
|
|
2091
|
+
<<<
|
|
2092
|
+
${prompt.slice(0, 6e3)}
|
|
2093
|
+
>>>
|
|
2094
|
+
Return the JSON line.`;
|
|
2095
|
+
}
|
|
2096
|
+
function parseRouterAnswer(text2, items, skip = []) {
|
|
2097
|
+
const m = /\{[\s\S]*\}/.exec(text2);
|
|
2098
|
+
if (!m) return null;
|
|
2099
|
+
let o;
|
|
2100
|
+
try {
|
|
2101
|
+
o = JSON.parse(m[0]);
|
|
2102
|
+
} catch {
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
const skipped = new Set(skip);
|
|
2106
|
+
const byId = new Map(items.filter((i) => !skipped.has(i.id)).map((i) => [i.id, i]));
|
|
2107
|
+
const ids = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && byId.has(x)) : [];
|
|
2108
|
+
const serve = typeof o.serve === "string" && byId.has(o.serve) ? o.serve : void 0;
|
|
2109
|
+
const rules = [...new Set(ids(o.rules).filter((id) => byId.get(id).lesson && id !== serve))].slice(0, MAX_RULES);
|
|
2110
|
+
const hint = ids(o.hints).filter((id) => id !== serve && !rules.includes(id)).slice(0, 2);
|
|
2111
|
+
return { ...serve ? { serve } : {}, rules, hint };
|
|
2112
|
+
}
|
|
2113
|
+
var MAX_RULES, RULES;
|
|
2114
|
+
var init_router = __esm({
|
|
2115
|
+
"../core/src/v2/recall/router.ts"() {
|
|
2116
|
+
"use strict";
|
|
2117
|
+
MAX_RULES = 3;
|
|
2118
|
+
RULES = `You are the prompt-time recall router for an engineering team's agent library. A coding agent is about to act on the user's latest prompt. You see the prompt plus session context: the session's first prompt, recent user prompts, the agent's last reply, and the items already in the agent's context. Decide what, if anything, to inject now.
|
|
2119
|
+
|
|
2120
|
+
- "serve": at most ONE skill or document, injected in full (long). Serve only when this prompt starts or clearly redirects work that the item is the procedure for, and it is not already in context. Follow-ups, approvals, status questions, chatter, and continuing work already under way need none.
|
|
2121
|
+
- "rules": at most ${MAX_RULES} LESSONS (short corrections or preferences learned from this team before), injected in full. Include a lesson only when what the agent will DO in response to this prompt \u2014 including the steps the work it starts normally leads to (a ticket ends in a PR and a report; a fix ends in evidence and a merge) \u2014 is the lesson's situation, even if the user does not mention the lesson. Not for shared topic words, not for preferences that would not change the agent's next actions, not if already in context. When unsure, include none.
|
|
2122
|
+
- "hints": up to 2 other possibly relevant document ids, shown as titles only.
|
|
2123
|
+
|
|
2124
|
+
Only use ids from the library. Output exactly one line of JSON and nothing else: {"serve": "<id or null>", "rules": ["<lesson id>", ...], "hints": ["<id>", ...]}`;
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2081
2128
|
// ../core/src/v2/guidelines.ts
|
|
2082
2129
|
function reflectionGuidelines(sections, reader, opts = {}) {
|
|
2083
2130
|
const covering = opts.layer ? /* @__PURE__ */ new Set(["org", opts.layer, ...opts.tree ? ancestorScopes(opts.tree, opts.layer) : []]) : void 0;
|
|
@@ -3161,6 +3208,7 @@ var init_v2 = __esm({
|
|
|
3161
3208
|
init_python();
|
|
3162
3209
|
init_text_index();
|
|
3163
3210
|
init_engine();
|
|
3211
|
+
init_router();
|
|
3164
3212
|
init_reflect();
|
|
3165
3213
|
init_digest();
|
|
3166
3214
|
init_reflect_context();
|
|
@@ -3325,17 +3373,17 @@ function listDocs(root) {
|
|
|
3325
3373
|
if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs6.existsSync(path5.join(root, rel)) && !deployedSkill(root, rel)).sort();
|
|
3326
3374
|
const out = [];
|
|
3327
3375
|
let visited = 0;
|
|
3328
|
-
const walk = (
|
|
3376
|
+
const walk = (dir2, rel, depth) => {
|
|
3329
3377
|
if (depth > 8 || visited++ > 2e4) return;
|
|
3330
3378
|
let entries;
|
|
3331
3379
|
try {
|
|
3332
|
-
entries = fs6.readdirSync(
|
|
3380
|
+
entries = fs6.readdirSync(dir2, { withFileTypes: true });
|
|
3333
3381
|
} catch {
|
|
3334
3382
|
return;
|
|
3335
3383
|
}
|
|
3336
3384
|
for (const e of entries) {
|
|
3337
3385
|
if (e.isDirectory()) {
|
|
3338
|
-
if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(
|
|
3386
|
+
if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(dir2, e.name, ".git"))) walk(path5.join(dir2, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
|
|
3339
3387
|
} else if (e.isFile()) {
|
|
3340
3388
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
3341
3389
|
if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
|
|
@@ -4074,6 +4122,14 @@ var init_src = __esm({
|
|
|
4074
4122
|
});
|
|
4075
4123
|
|
|
4076
4124
|
// src/v2/store.ts
|
|
4125
|
+
var store_exports = {};
|
|
4126
|
+
__export(store_exports, {
|
|
4127
|
+
V2Store: () => V2Store,
|
|
4128
|
+
addUnique: () => addUnique,
|
|
4129
|
+
fileSafe: () => fileSafe,
|
|
4130
|
+
hexId: () => hexId,
|
|
4131
|
+
normalizeSession: () => normalizeSession
|
|
4132
|
+
});
|
|
4077
4133
|
import fs8 from "node:fs";
|
|
4078
4134
|
import path7 from "node:path";
|
|
4079
4135
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
|
|
@@ -4192,12 +4248,12 @@ var init_store2 = __esm({
|
|
|
4192
4248
|
return s;
|
|
4193
4249
|
}
|
|
4194
4250
|
listSessions() {
|
|
4195
|
-
const
|
|
4196
|
-
if (!fileExists(
|
|
4251
|
+
const dir2 = this.p("sessions");
|
|
4252
|
+
if (!fileExists(dir2)) return [];
|
|
4197
4253
|
const out = [];
|
|
4198
|
-
for (const f of fs8.readdirSync(
|
|
4254
|
+
for (const f of fs8.readdirSync(dir2)) {
|
|
4199
4255
|
if (!f.endsWith(".json") || f === "current.json") continue;
|
|
4200
|
-
const raw = readJson(path7.join(
|
|
4256
|
+
const raw = readJson(path7.join(dir2, f), null);
|
|
4201
4257
|
if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
|
|
4202
4258
|
}
|
|
4203
4259
|
return out;
|
|
@@ -4295,12 +4351,12 @@ var init_store2 = __esm({
|
|
|
4295
4351
|
}
|
|
4296
4352
|
}
|
|
4297
4353
|
listDir(name) {
|
|
4298
|
-
const
|
|
4299
|
-
if (!fileExists(
|
|
4354
|
+
const dir2 = this.p(name);
|
|
4355
|
+
if (!fileExists(dir2)) return [];
|
|
4300
4356
|
const out = [];
|
|
4301
|
-
for (const f of fs8.readdirSync(
|
|
4357
|
+
for (const f of fs8.readdirSync(dir2)) {
|
|
4302
4358
|
if (!f.endsWith(".json")) continue;
|
|
4303
|
-
const v = readJson(path7.join(
|
|
4359
|
+
const v = readJson(path7.join(dir2, f), null);
|
|
4304
4360
|
if (v) out.push(v);
|
|
4305
4361
|
}
|
|
4306
4362
|
return out;
|
|
@@ -4374,8 +4430,8 @@ function removeFile(file) {
|
|
|
4374
4430
|
} catch {
|
|
4375
4431
|
}
|
|
4376
4432
|
}
|
|
4377
|
-
function pruneDirs(
|
|
4378
|
-
let d =
|
|
4433
|
+
function pruneDirs(dir2, stop) {
|
|
4434
|
+
let d = dir2;
|
|
4379
4435
|
while (d.startsWith(stop) && d !== stop) {
|
|
4380
4436
|
try {
|
|
4381
4437
|
if (fs9.readdirSync(d).length) return;
|
|
@@ -4386,16 +4442,16 @@ function pruneDirs(dir, stop) {
|
|
|
4386
4442
|
d = path8.dirname(d);
|
|
4387
4443
|
}
|
|
4388
4444
|
}
|
|
4389
|
-
function listFiles(
|
|
4445
|
+
function listFiles(dir2, base = dir2) {
|
|
4390
4446
|
let entries;
|
|
4391
4447
|
try {
|
|
4392
|
-
entries = fs9.readdirSync(
|
|
4448
|
+
entries = fs9.readdirSync(dir2, { withFileTypes: true });
|
|
4393
4449
|
} catch {
|
|
4394
4450
|
return [];
|
|
4395
4451
|
}
|
|
4396
4452
|
const out = [];
|
|
4397
4453
|
for (const e of entries) {
|
|
4398
|
-
const abs = path8.join(
|
|
4454
|
+
const abs = path8.join(dir2, e.name);
|
|
4399
4455
|
if (e.isDirectory()) out.push(...listFiles(abs, base));
|
|
4400
4456
|
else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
|
|
4401
4457
|
}
|
|
@@ -4403,8 +4459,8 @@ function listFiles(dir, base = dir) {
|
|
|
4403
4459
|
}
|
|
4404
4460
|
function childDir(root, name) {
|
|
4405
4461
|
if (!name || !SAFE_NAME.test(name)) return null;
|
|
4406
|
-
const
|
|
4407
|
-
return path8.dirname(path8.resolve(
|
|
4462
|
+
const dir2 = path8.join(root, name);
|
|
4463
|
+
return path8.dirname(path8.resolve(dir2)) === path8.resolve(root) && insideRoot(root, dir2) ? dir2 : null;
|
|
4408
4464
|
}
|
|
4409
4465
|
function layerRank(layer, tree) {
|
|
4410
4466
|
const kind = layerKind(layer);
|
|
@@ -4433,17 +4489,17 @@ function planDeploy(bundle, p, harnesses) {
|
|
|
4433
4489
|
const contents = /* @__PURE__ */ new Map();
|
|
4434
4490
|
const dirs = [];
|
|
4435
4491
|
for (const root of roots) {
|
|
4436
|
-
const
|
|
4437
|
-
const body =
|
|
4438
|
-
if (!
|
|
4492
|
+
const dir2 = childDir(root, name);
|
|
4493
|
+
const body = dir2 && safeJoin(dir2, "SKILL.md");
|
|
4494
|
+
if (!dir2 || !body) {
|
|
4439
4495
|
warnings.push(`skipped skill ${name} in ${root}: its folder leads outside ${root}`);
|
|
4440
4496
|
continue;
|
|
4441
4497
|
}
|
|
4442
|
-
dirs.push(
|
|
4498
|
+
dirs.push(dir2);
|
|
4443
4499
|
targets.push({ abs: body, rel: "SKILL.md", role: "body" });
|
|
4444
4500
|
contents.set(body, s.body);
|
|
4445
4501
|
for (const [rel, f] of Object.entries(s.skill.files ?? {})) {
|
|
4446
|
-
const abs = safeJoin(
|
|
4502
|
+
const abs = safeJoin(dir2, rel);
|
|
4447
4503
|
if (!abs || rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
4448
4504
|
targets.push({ abs, rel, role: "file", ...f.mode ? { mode: f.mode } : {} });
|
|
4449
4505
|
contents.set(abs, f.content);
|
|
@@ -4583,10 +4639,10 @@ function captureDoc(c, doc, out) {
|
|
|
4583
4639
|
if (chosen) byRel.set(key, chosen);
|
|
4584
4640
|
}
|
|
4585
4641
|
if (doc.kind === "skill") {
|
|
4586
|
-
for (const
|
|
4587
|
-
for (const rel of listFiles(
|
|
4642
|
+
for (const dir2 of doc.dirs ?? []) {
|
|
4643
|
+
for (const rel of listFiles(dir2)) {
|
|
4588
4644
|
if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
|
|
4589
|
-
const abs = path8.join(
|
|
4645
|
+
const abs = path8.join(dir2, rel);
|
|
4590
4646
|
const content = readText(abs);
|
|
4591
4647
|
if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
|
|
4592
4648
|
byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
|
|
@@ -4630,23 +4686,23 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
4630
4686
|
continue;
|
|
4631
4687
|
}
|
|
4632
4688
|
for (const name of names) {
|
|
4633
|
-
const
|
|
4634
|
-
if (owned.has(
|
|
4635
|
-
const skillMd = readText(path8.join(
|
|
4689
|
+
const dir2 = path8.join(root, name);
|
|
4690
|
+
if (owned.has(dir2) || incoming.has(name) || only && name !== only) continue;
|
|
4691
|
+
const skillMd = readText(path8.join(dir2, "SKILL.md"));
|
|
4636
4692
|
if (skillMd === null) continue;
|
|
4637
4693
|
let born = 0;
|
|
4638
4694
|
try {
|
|
4639
|
-
const st = fs9.statSync(path8.join(
|
|
4695
|
+
const st = fs9.statSync(path8.join(dir2, "SKILL.md"));
|
|
4640
4696
|
born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
|
|
4641
4697
|
} catch {
|
|
4642
4698
|
continue;
|
|
4643
4699
|
}
|
|
4644
4700
|
if (!(born >= since - BORN_SLACK_MS) && !only) continue;
|
|
4645
4701
|
const files = {};
|
|
4646
|
-
for (const rel of listFiles(
|
|
4702
|
+
for (const rel of listFiles(dir2).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
|
|
4647
4703
|
if (rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
4648
|
-
const content = readText(path8.join(
|
|
4649
|
-
if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(
|
|
4704
|
+
const content = readText(path8.join(dir2, rel));
|
|
4705
|
+
if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir2, rel)) ? { mode: "x" } : {} };
|
|
4650
4706
|
}
|
|
4651
4707
|
const hash = sha(JSON.stringify([skillMd, files]));
|
|
4652
4708
|
if (state.created[name]?.hash === hash) continue;
|
|
@@ -4767,11 +4823,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4767
4823
|
const prevByAbs = new Map((prev?.targets ?? []).map((t) => [`${t.abs}#${t.role}`, t]));
|
|
4768
4824
|
const foreign = /* @__PURE__ */ new Set();
|
|
4769
4825
|
if (d.kind === "skill" && !prev) {
|
|
4770
|
-
for (const
|
|
4771
|
-
const cur = readText(path8.join(
|
|
4826
|
+
for (const dir2 of d.dirs ?? []) {
|
|
4827
|
+
const cur = readText(path8.join(dir2, "SKILL.md"));
|
|
4772
4828
|
if (cur === null || cur === d.section.body || state.created[d.name]) continue;
|
|
4773
|
-
foreign.add(
|
|
4774
|
-
report.warnings.push(`${
|
|
4829
|
+
foreign.add(dir2);
|
|
4830
|
+
report.warnings.push(`${dir2} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
|
|
4775
4831
|
}
|
|
4776
4832
|
delete state.created[d.name];
|
|
4777
4833
|
}
|
|
@@ -4780,7 +4836,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4780
4836
|
targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
|
|
4781
4837
|
continue;
|
|
4782
4838
|
}
|
|
4783
|
-
if ([...foreign].some((
|
|
4839
|
+
if ([...foreign].some((dir2) => t.abs.startsWith(dir2 + path8.sep))) continue;
|
|
4784
4840
|
if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
|
|
4785
4841
|
report.warnings.push(`${t.abs} not written: it leads outside its folder`);
|
|
4786
4842
|
continue;
|
|
@@ -4810,20 +4866,20 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4810
4866
|
}
|
|
4811
4867
|
}
|
|
4812
4868
|
if (d.kind === "skill") {
|
|
4813
|
-
for (const
|
|
4814
|
-
if ((d.dirs ?? []).includes(
|
|
4815
|
-
removeFile(path8.join(
|
|
4816
|
-
pruneDirs(
|
|
4817
|
-
}
|
|
4818
|
-
for (const
|
|
4819
|
-
const manifest = safeJoin(
|
|
4820
|
-
if (foreign.has(
|
|
4869
|
+
for (const dir2 of prev?.dirs ?? []) {
|
|
4870
|
+
if ((d.dirs ?? []).includes(dir2) || !removable(dir2)) continue;
|
|
4871
|
+
removeFile(path8.join(dir2, MANIFEST));
|
|
4872
|
+
pruneDirs(dir2, path8.dirname(dir2));
|
|
4873
|
+
}
|
|
4874
|
+
for (const dir2 of d.dirs ?? []) {
|
|
4875
|
+
const manifest = safeJoin(dir2, MANIFEST);
|
|
4876
|
+
if (foreign.has(dir2) || !manifest || !fs9.existsSync(path8.join(dir2, "SKILL.md"))) continue;
|
|
4821
4877
|
writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
|
|
4822
4878
|
}
|
|
4823
4879
|
report.skills++;
|
|
4824
4880
|
} else if (d.kind === "pack") report.packFiles += targets.length;
|
|
4825
4881
|
if (d.kind === "instructions") report.instructions++;
|
|
4826
|
-
const dirs = d.dirs?.filter((
|
|
4882
|
+
const dirs = d.dirs?.filter((dir2) => !foreign.has(dir2));
|
|
4827
4883
|
next[id] = { sectionId: id, kind: d.kind, name: d.name, version: d.section.version, targets, ...dirs ? { dirs } : {} };
|
|
4828
4884
|
}
|
|
4829
4885
|
for (const [id, doc] of Object.entries(state.docs)) {
|
|
@@ -4840,14 +4896,14 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4840
4896
|
report.removed++;
|
|
4841
4897
|
}
|
|
4842
4898
|
}
|
|
4843
|
-
for (const
|
|
4844
|
-
if (!removable(
|
|
4845
|
-
removeFile(path8.join(
|
|
4846
|
-
pruneDirs(
|
|
4847
|
-
if (fs9.existsSync(
|
|
4848
|
-
const left = listFiles(
|
|
4849
|
-
if (left.length) report.warnings.push(`${
|
|
4850
|
-
else pruneDirs(
|
|
4899
|
+
for (const dir2 of doc.dirs ?? []) {
|
|
4900
|
+
if (!removable(dir2)) continue;
|
|
4901
|
+
removeFile(path8.join(dir2, MANIFEST));
|
|
4902
|
+
pruneDirs(dir2, path8.dirname(dir2));
|
|
4903
|
+
if (fs9.existsSync(dir2)) {
|
|
4904
|
+
const left = listFiles(dir2);
|
|
4905
|
+
if (left.length) report.warnings.push(`${dir2} kept: it has files Bunshin did not write`);
|
|
4906
|
+
else pruneDirs(dir2, path8.dirname(dir2));
|
|
4851
4907
|
}
|
|
4852
4908
|
}
|
|
4853
4909
|
}
|
|
@@ -4883,9 +4939,9 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4883
4939
|
const packs = new Set(plan.skills.concat([...plan.docs.values()].map((d) => d.section)).map((s) => s.pack).filter((x) => !!x));
|
|
4884
4940
|
if (harnesses.has("claude-code")) {
|
|
4885
4941
|
for (const pack of packs) {
|
|
4886
|
-
const
|
|
4887
|
-
if (!
|
|
4888
|
-
const link = path8.join(
|
|
4942
|
+
const dir2 = childDir(p.library, pack);
|
|
4943
|
+
if (!dir2 || !insideRoot(dir2, path8.join(dir2, ".claude", "x"))) continue;
|
|
4944
|
+
const link = path8.join(dir2, ".claude", "skills");
|
|
4889
4945
|
try {
|
|
4890
4946
|
fs9.lstatSync(link);
|
|
4891
4947
|
if (fs9.readlinkSync(link) !== p.claudeSkills) {
|
|
@@ -4940,10 +4996,10 @@ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
|
|
|
4940
4996
|
const s = readState(paths, config.orgId);
|
|
4941
4997
|
return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
|
|
4942
4998
|
}
|
|
4943
|
-
function materialize(sections,
|
|
4999
|
+
function materialize(sections, dir2) {
|
|
4944
5000
|
let files = 0;
|
|
4945
5001
|
const put = (rel, content, mode) => {
|
|
4946
|
-
const abs = safeJoin(
|
|
5002
|
+
const abs = safeJoin(dir2, rel);
|
|
4947
5003
|
if (!abs) return;
|
|
4948
5004
|
ensureDir(path8.dirname(abs));
|
|
4949
5005
|
writeFile(abs, content, mode);
|
|
@@ -4970,20 +5026,20 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
4970
5026
|
for (const h of harnesses) {
|
|
4971
5027
|
const spec = LINK_DIRS[h];
|
|
4972
5028
|
if (!spec) continue;
|
|
4973
|
-
const
|
|
5029
|
+
const dir2 = path8.join(abs, spec.rel);
|
|
4974
5030
|
const canonical = path8.resolve(p[spec.key]);
|
|
4975
5031
|
const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
|
|
4976
5032
|
let entries = [];
|
|
4977
5033
|
try {
|
|
4978
|
-
entries = fs9.readdirSync(
|
|
5034
|
+
entries = fs9.readdirSync(dir2, { withFileTypes: true });
|
|
4979
5035
|
} catch {
|
|
4980
5036
|
}
|
|
4981
5037
|
for (const e of entries) {
|
|
4982
5038
|
if (!e.isSymbolicLink()) continue;
|
|
4983
|
-
const link = path8.join(
|
|
5039
|
+
const link = path8.join(dir2, e.name);
|
|
4984
5040
|
let target = "";
|
|
4985
5041
|
try {
|
|
4986
|
-
target = path8.resolve(
|
|
5042
|
+
target = path8.resolve(dir2, fs9.readlinkSync(link));
|
|
4987
5043
|
} catch {
|
|
4988
5044
|
continue;
|
|
4989
5045
|
}
|
|
@@ -4997,7 +5053,7 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
4997
5053
|
out.removed++;
|
|
4998
5054
|
}
|
|
4999
5055
|
for (const name of want) {
|
|
5000
|
-
const link = path8.join(
|
|
5056
|
+
const link = path8.join(dir2, name);
|
|
5001
5057
|
try {
|
|
5002
5058
|
fs9.lstatSync(link);
|
|
5003
5059
|
out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
|
|
@@ -5005,7 +5061,7 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
5005
5061
|
} catch {
|
|
5006
5062
|
}
|
|
5007
5063
|
try {
|
|
5008
|
-
ensureDir(
|
|
5064
|
+
ensureDir(dir2);
|
|
5009
5065
|
fs9.symlinkSync(path8.join(canonical, name), link, "dir");
|
|
5010
5066
|
excluded.push(path8.join(spec.rel, name));
|
|
5011
5067
|
out.linked++;
|
|
@@ -5013,7 +5069,7 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
5013
5069
|
out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
|
|
5014
5070
|
}
|
|
5015
5071
|
}
|
|
5016
|
-
pruneDirs(
|
|
5072
|
+
pruneDirs(dir2, abs);
|
|
5017
5073
|
}
|
|
5018
5074
|
out.kept = excluded.length > 0;
|
|
5019
5075
|
gitExclude(abs, excluded);
|
|
@@ -5918,19 +5974,19 @@ function renderFeed(f) {
|
|
|
5918
5974
|
return out.join("\n").trimEnd() + "\n";
|
|
5919
5975
|
}
|
|
5920
5976
|
function latestFeed(v2) {
|
|
5921
|
-
const
|
|
5977
|
+
const dir2 = v2.p("ui");
|
|
5922
5978
|
let best = null;
|
|
5923
5979
|
try {
|
|
5924
|
-
for (const f2 of fs13.readdirSync(
|
|
5980
|
+
for (const f2 of fs13.readdirSync(dir2)) {
|
|
5925
5981
|
if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
|
|
5926
|
-
const m = fs13.statSync(path9.join(
|
|
5982
|
+
const m = fs13.statSync(path9.join(dir2, f2)).mtimeMs;
|
|
5927
5983
|
if (!best || m > best.m) best = { f: f2, m };
|
|
5928
5984
|
}
|
|
5929
5985
|
} catch {
|
|
5930
5986
|
return null;
|
|
5931
5987
|
}
|
|
5932
5988
|
if (!best) return null;
|
|
5933
|
-
const f = readJson(path9.join(
|
|
5989
|
+
const f = readJson(path9.join(dir2, best.f), null);
|
|
5934
5990
|
return f && f.v === UI_FEED_VERSION ? f : null;
|
|
5935
5991
|
}
|
|
5936
5992
|
var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
|
|
@@ -6447,6 +6503,202 @@ var init_reflect3 = __esm({
|
|
|
6447
6503
|
}
|
|
6448
6504
|
});
|
|
6449
6505
|
|
|
6506
|
+
// src/v2/route.ts
|
|
6507
|
+
var route_exports = {};
|
|
6508
|
+
__export(route_exports, {
|
|
6509
|
+
ROUTER_MODEL: () => ROUTER_MODEL,
|
|
6510
|
+
lastReply: () => lastReply,
|
|
6511
|
+
promptContext: () => promptContext,
|
|
6512
|
+
readAnswer: () => readAnswer,
|
|
6513
|
+
readJob: () => readJob,
|
|
6514
|
+
routeJob: () => routeJob,
|
|
6515
|
+
sweepRouter: () => sweepRouter,
|
|
6516
|
+
writeJob: () => writeJob
|
|
6517
|
+
});
|
|
6518
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
6519
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6520
|
+
import fs16 from "node:fs";
|
|
6521
|
+
import path11 from "node:path";
|
|
6522
|
+
function readJson2(file) {
|
|
6523
|
+
try {
|
|
6524
|
+
return JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
6525
|
+
} catch {
|
|
6526
|
+
return null;
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
function lastReply(transcriptPath, maxBytes = 256e3) {
|
|
6530
|
+
if (!transcriptPath) return void 0;
|
|
6531
|
+
try {
|
|
6532
|
+
const fd = fs16.openSync(transcriptPath, "r");
|
|
6533
|
+
try {
|
|
6534
|
+
const size = fs16.fstatSync(fd).size;
|
|
6535
|
+
const len = Math.min(size, maxBytes);
|
|
6536
|
+
const buf = Buffer.alloc(len);
|
|
6537
|
+
fs16.readSync(fd, buf, 0, len, size - len);
|
|
6538
|
+
const lines = buf.toString("utf8").split("\n");
|
|
6539
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6540
|
+
let m;
|
|
6541
|
+
try {
|
|
6542
|
+
m = JSON.parse(lines[i]);
|
|
6543
|
+
} catch {
|
|
6544
|
+
continue;
|
|
6545
|
+
}
|
|
6546
|
+
if (m?.type !== "assistant" || m.isSidechain) continue;
|
|
6547
|
+
const c = m.message?.content;
|
|
6548
|
+
const text2 = Array.isArray(c) ? c.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("\n") : typeof c === "string" ? c : "";
|
|
6549
|
+
if (text2.trim()) return redact2(text2.trim()).slice(0, 1500);
|
|
6550
|
+
}
|
|
6551
|
+
} finally {
|
|
6552
|
+
fs16.closeSync(fd);
|
|
6553
|
+
}
|
|
6554
|
+
} catch {
|
|
6555
|
+
}
|
|
6556
|
+
return void 0;
|
|
6557
|
+
}
|
|
6558
|
+
function promptContext(session) {
|
|
6559
|
+
const recent = session.recentPrompts ?? [];
|
|
6560
|
+
if (!recent.length) return {};
|
|
6561
|
+
const reply = lastReply(session.transcriptPath);
|
|
6562
|
+
return { ...session.task ? { first: session.task.slice(0, 1e3) } : {}, recent, ...reply ? { reply } : {} };
|
|
6563
|
+
}
|
|
6564
|
+
async function routerIndex(store, v2, source) {
|
|
6565
|
+
const file = path11.join(dir(v2), `index-${createHash5("sha256").update(source).digest("hex").slice(0, 12)}.json`);
|
|
6566
|
+
const cached = readJson2(file);
|
|
6567
|
+
if (cached && Date.now() - Date.parse(cached.fetchedAt) < INDEX_TTL_MS) return cached;
|
|
6568
|
+
try {
|
|
6569
|
+
const res = await v2api(
|
|
6570
|
+
store.config(),
|
|
6571
|
+
"POST",
|
|
6572
|
+
"/recall/index",
|
|
6573
|
+
{ ...source ? { source } : {}, ...cached ? { have: cached.hash } : {} },
|
|
6574
|
+
{ timeoutMs: 15e3 }
|
|
6575
|
+
);
|
|
6576
|
+
const next = res.unchanged && cached ? { ...cached, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() } : { hash: res.hash, system: res.system ?? "", items: res.items ?? [], source, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
6577
|
+
if (!next.system) throw new Error("the server sent no index");
|
|
6578
|
+
writeJsonAtomic(file, next);
|
|
6579
|
+
return next;
|
|
6580
|
+
} catch (err) {
|
|
6581
|
+
if (cached) return cached;
|
|
6582
|
+
throw err;
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
function askClaude(systemFile, message, model, cwd) {
|
|
6586
|
+
const bin = process.env.BUNSHIN_CLAUDE_BIN || "claude";
|
|
6587
|
+
const args = [
|
|
6588
|
+
"-p",
|
|
6589
|
+
"--model",
|
|
6590
|
+
model,
|
|
6591
|
+
"--effort",
|
|
6592
|
+
"low",
|
|
6593
|
+
"--settings",
|
|
6594
|
+
JSON.stringify({ disableAllHooks: true }),
|
|
6595
|
+
"--setting-sources",
|
|
6596
|
+
"",
|
|
6597
|
+
"--tools",
|
|
6598
|
+
"",
|
|
6599
|
+
"--no-session-persistence",
|
|
6600
|
+
"--exclude-dynamic-system-prompt-sections",
|
|
6601
|
+
"--system-prompt-file",
|
|
6602
|
+
systemFile,
|
|
6603
|
+
"--output-format",
|
|
6604
|
+
"json",
|
|
6605
|
+
message
|
|
6606
|
+
];
|
|
6607
|
+
return new Promise((resolve, reject) => {
|
|
6608
|
+
const child = spawn4(bin, args, { cwd, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
|
|
6609
|
+
let out = "";
|
|
6610
|
+
let err = "";
|
|
6611
|
+
let done = false;
|
|
6612
|
+
const finish = (fn) => {
|
|
6613
|
+
if (done) return;
|
|
6614
|
+
done = true;
|
|
6615
|
+
clearTimeout(timer);
|
|
6616
|
+
child.kill("SIGTERM");
|
|
6617
|
+
fn();
|
|
6618
|
+
};
|
|
6619
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`claude took over ${CLAUDE_TIMEOUT_MS / 1e3} s`))), CLAUDE_TIMEOUT_MS);
|
|
6620
|
+
child.stdout.on("data", (d) => {
|
|
6621
|
+
out += d.toString();
|
|
6622
|
+
try {
|
|
6623
|
+
const parsed = JSON.parse(out);
|
|
6624
|
+
finish(() => resolve(parsed));
|
|
6625
|
+
} catch {
|
|
6626
|
+
}
|
|
6627
|
+
});
|
|
6628
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
6629
|
+
child.on("error", (e) => finish(() => reject(e)));
|
|
6630
|
+
child.on("close", (code2) => finish(() => reject(new Error(`claude exited ${code2}: ${(err || out).trim().slice(0, 300)}`))));
|
|
6631
|
+
});
|
|
6632
|
+
}
|
|
6633
|
+
async function routeJob(store, sessionId, seq) {
|
|
6634
|
+
const v2 = new V2Store(store);
|
|
6635
|
+
const config = store.config();
|
|
6636
|
+
const job = readJob(v2, sessionId);
|
|
6637
|
+
if (!job || job.seq !== seq || job.cancelled) return null;
|
|
6638
|
+
const started = Date.parse(job.at) || Date.now();
|
|
6639
|
+
const write = (a) => {
|
|
6640
|
+
const answer = { seq, at: (/* @__PURE__ */ new Date()).toISOString(), ms: Date.now() - started, ...a };
|
|
6641
|
+
writeJsonAtomic(answerFile(v2, sessionId, seq), answer);
|
|
6642
|
+
v2.log("v2-router", { session: sessionId, seq, serve: answer.serve ?? null, rules: answer.rules ?? [], hint: answer.hint ?? [], ms: answer.ms, modelMs: answer.modelMs, error: answer.error, dense: job.dense?.serve ?? null, usage: answer.usage });
|
|
6643
|
+
return answer;
|
|
6644
|
+
};
|
|
6645
|
+
try {
|
|
6646
|
+
const session = v2.readSession(sessionId);
|
|
6647
|
+
if (!session?.lastPrompt) return write({ error: "no prompt in the session file" });
|
|
6648
|
+
if (!canCall(config)) return write({ error: "offline: the router's index comes from the server" });
|
|
6649
|
+
const index = await routerIndex(store, v2, session.source ?? "");
|
|
6650
|
+
const systemFile = path11.join(dir(v2), `system-${index.hash}.txt`);
|
|
6651
|
+
if (!fs16.existsSync(systemFile)) fs16.writeFileSync(systemFile, index.system);
|
|
6652
|
+
const cwd = path11.join(dir(v2), "cwd");
|
|
6653
|
+
fs16.mkdirSync(cwd, { recursive: true });
|
|
6654
|
+
const inContext2 = session.inContext ?? [...session.loaded, ...session.pushed];
|
|
6655
|
+
const titles = new Map((v2.readBundle()?.sections ?? []).map((s) => [s.id, s.skill?.name ?? s.title]));
|
|
6656
|
+
const message = routerMessage(session.lastPrompt, promptContext(session), inContext2.map((id) => titles.get(id)).filter((t) => !!t));
|
|
6657
|
+
const res = await askClaude(systemFile, message, config.recall.routerModel || process.env.BUNSHIN_ROUTER_MODEL || ROUTER_MODEL, cwd);
|
|
6658
|
+
const u = res.usage ?? {};
|
|
6659
|
+
const usage = { input: u.input_tokens ?? 0, cacheRead: u.cache_read_input_tokens ?? 0, cacheWrite: u.cache_creation_input_tokens ?? 0, output: u.output_tokens ?? 0, ...u.output_tokens_details?.thinking_tokens ? { thinking: u.output_tokens_details.thinking_tokens } : {} };
|
|
6660
|
+
const meta = { ...res.duration_ms ? { modelMs: res.duration_ms } : {}, usage };
|
|
6661
|
+
if (res.is_error) return write({ ...meta, error: `claude: ${(res.result ?? "error").slice(0, 300)}` });
|
|
6662
|
+
const picks = parseRouterAnswer(res.result ?? "", index.items, inContext2);
|
|
6663
|
+
if (!picks) return write({ ...meta, error: `not JSON: ${(res.result ?? "").slice(0, 120)}` });
|
|
6664
|
+
return write({ ...picks, ...meta });
|
|
6665
|
+
} catch (err) {
|
|
6666
|
+
return write({ error: (err instanceof Error ? err.message : String(err)).slice(0, 300) });
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
function sweepRouter(v2, now = Date.now()) {
|
|
6670
|
+
try {
|
|
6671
|
+
for (const f of fs16.readdirSync(dir(v2))) {
|
|
6672
|
+
if (!/\.(job|answer)\.json$/.test(f)) continue;
|
|
6673
|
+
const p = path11.join(dir(v2), f);
|
|
6674
|
+
if (now - fs16.statSync(p).mtimeMs > 864e5) fs16.rmSync(p, { force: true });
|
|
6675
|
+
}
|
|
6676
|
+
} catch {
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
var ROUTER_MODEL, INDEX_TTL_MS, CLAUDE_TIMEOUT_MS, dir, safe2, jobFile, answerFile, readJob, writeJob, readAnswer;
|
|
6680
|
+
var init_route = __esm({
|
|
6681
|
+
"src/v2/route.ts"() {
|
|
6682
|
+
"use strict";
|
|
6683
|
+
init_v22();
|
|
6684
|
+
init_llm();
|
|
6685
|
+
init_redact2();
|
|
6686
|
+
init_util();
|
|
6687
|
+
init_api();
|
|
6688
|
+
init_store2();
|
|
6689
|
+
ROUTER_MODEL = "claude-haiku-5-5";
|
|
6690
|
+
INDEX_TTL_MS = 10 * 6e4;
|
|
6691
|
+
CLAUDE_TIMEOUT_MS = 6e4;
|
|
6692
|
+
dir = (v2) => path11.join(v2.dir, "router");
|
|
6693
|
+
safe2 = (id) => id.replace(/[^\w.-]/g, "_");
|
|
6694
|
+
jobFile = (v2, session) => path11.join(dir(v2), `${safe2(session)}.job.json`);
|
|
6695
|
+
answerFile = (v2, session, seq) => path11.join(dir(v2), `${safe2(session)}.${seq}.answer.json`);
|
|
6696
|
+
readJob = (v2, session) => readJson2(jobFile(v2, session));
|
|
6697
|
+
writeJob = (v2, session, job) => writeJsonAtomic(jobFile(v2, session), job);
|
|
6698
|
+
readAnswer = (v2, session, seq) => readJson2(answerFile(v2, session, seq));
|
|
6699
|
+
}
|
|
6700
|
+
});
|
|
6701
|
+
|
|
6450
6702
|
// src/v2/uicmd.ts
|
|
6451
6703
|
var uicmd_exports = {};
|
|
6452
6704
|
__export(uicmd_exports, {
|
|
@@ -6459,51 +6711,51 @@ __export(uicmd_exports, {
|
|
|
6459
6711
|
modSource: () => modSource
|
|
6460
6712
|
});
|
|
6461
6713
|
import { spawnSync } from "node:child_process";
|
|
6462
|
-
import
|
|
6714
|
+
import fs17 from "node:fs";
|
|
6463
6715
|
import os3 from "node:os";
|
|
6464
|
-
import
|
|
6716
|
+
import path12 from "node:path";
|
|
6465
6717
|
import { fileURLToPath } from "node:url";
|
|
6466
6718
|
function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
|
|
6467
|
-
return
|
|
6719
|
+
return path12.join(home, ".bunshin", "claude-code-ui");
|
|
6468
6720
|
}
|
|
6469
6721
|
function modSource() {
|
|
6470
|
-
const here =
|
|
6471
|
-
for (const
|
|
6472
|
-
if (
|
|
6722
|
+
const here = path12.dirname(fileURLToPath(import.meta.url));
|
|
6723
|
+
for (const dir2 of [path12.join(here, "claude-code-ui"), path12.join(here, "..", "claude-code-ui"), path12.resolve(here, "../../../../adapters/claude-code-ui")]) {
|
|
6724
|
+
if (fs17.existsSync(path12.join(dir2, ".claude-plugin", "plugin.json"))) return dir2;
|
|
6473
6725
|
}
|
|
6474
6726
|
return null;
|
|
6475
6727
|
}
|
|
6476
6728
|
function installedModVersion(home) {
|
|
6477
6729
|
try {
|
|
6478
|
-
return JSON.parse(
|
|
6730
|
+
return JSON.parse(fs17.readFileSync(path12.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
|
|
6479
6731
|
} catch {
|
|
6480
6732
|
return null;
|
|
6481
6733
|
}
|
|
6482
6734
|
}
|
|
6483
6735
|
function copyDir(from, to) {
|
|
6484
|
-
|
|
6485
|
-
for (const e of
|
|
6736
|
+
fs17.mkdirSync(to, { recursive: true });
|
|
6737
|
+
for (const e of fs17.readdirSync(from, { withFileTypes: true })) {
|
|
6486
6738
|
if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
|
|
6487
6739
|
if (e.name === "node_modules") continue;
|
|
6488
|
-
const a =
|
|
6489
|
-
const b =
|
|
6740
|
+
const a = path12.join(from, e.name);
|
|
6741
|
+
const b = path12.join(to, e.name);
|
|
6490
6742
|
if (e.isDirectory()) copyDir(a, b);
|
|
6491
|
-
else
|
|
6743
|
+
else fs17.copyFileSync(a, b);
|
|
6492
6744
|
}
|
|
6493
6745
|
}
|
|
6494
6746
|
function installMod(version2, opts = {}) {
|
|
6495
6747
|
const src = modSource();
|
|
6496
6748
|
if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
|
|
6497
6749
|
const home = modHome(opts.home);
|
|
6498
|
-
const
|
|
6499
|
-
|
|
6500
|
-
copyDir(src,
|
|
6501
|
-
const manifest =
|
|
6502
|
-
const m = JSON.parse(
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6750
|
+
const dir2 = path12.join(home, MOD_PLUGIN);
|
|
6751
|
+
fs17.rmSync(dir2, { recursive: true, force: true });
|
|
6752
|
+
copyDir(src, dir2);
|
|
6753
|
+
const manifest = path12.join(dir2, ".claude-plugin", "plugin.json");
|
|
6754
|
+
const m = JSON.parse(fs17.readFileSync(manifest, "utf8"));
|
|
6755
|
+
fs17.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
|
|
6756
|
+
fs17.mkdirSync(path12.join(home, ".claude-plugin"), { recursive: true });
|
|
6757
|
+
fs17.writeFileSync(
|
|
6758
|
+
path12.join(home, ".claude-plugin", "marketplace.json"),
|
|
6507
6759
|
JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
|
|
6508
6760
|
);
|
|
6509
6761
|
const run2 = opts.run ?? spawnSync;
|
|
@@ -6517,7 +6769,7 @@ function installMod(version2, opts = {}) {
|
|
|
6517
6769
|
};
|
|
6518
6770
|
if (!step(["plugin", "marketplace", "add", home])) step(["plugin", "marketplace", "update", MOD_MARKETPLACE]);
|
|
6519
6771
|
if (!step(["plugin", "install", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`, "--scope", "user"])) step(["plugin", "update", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`]);
|
|
6520
|
-
return { dir, version: version2, steps };
|
|
6772
|
+
return { dir: dir2, version: version2, steps };
|
|
6521
6773
|
}
|
|
6522
6774
|
async function cmdUi(store, args, out, err, version2) {
|
|
6523
6775
|
const sub = args._[1];
|
|
@@ -6538,7 +6790,7 @@ async function cmdUi(store, args, out, err, version2) {
|
|
|
6538
6790
|
`);
|
|
6539
6791
|
for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
|
|
6540
6792
|
`);
|
|
6541
|
-
out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " +
|
|
6793
|
+
out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " + path12.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
|
|
6542
6794
|
}
|
|
6543
6795
|
return installed || !failed.length ? 0 : 1;
|
|
6544
6796
|
}
|
|
@@ -6619,19 +6871,19 @@ __export(update_exports, {
|
|
|
6619
6871
|
updateCheckDue: () => updateCheckDue,
|
|
6620
6872
|
updateNotice: () => updateNotice
|
|
6621
6873
|
});
|
|
6622
|
-
import
|
|
6623
|
-
import
|
|
6874
|
+
import fs18 from "node:fs";
|
|
6875
|
+
import path13 from "node:path";
|
|
6624
6876
|
function readState2() {
|
|
6625
6877
|
try {
|
|
6626
|
-
return JSON.parse(
|
|
6878
|
+
return JSON.parse(fs18.readFileSync(stateFile(), "utf8"));
|
|
6627
6879
|
} catch {
|
|
6628
6880
|
return {};
|
|
6629
6881
|
}
|
|
6630
6882
|
}
|
|
6631
6883
|
function writeState(s) {
|
|
6632
6884
|
try {
|
|
6633
|
-
|
|
6634
|
-
|
|
6885
|
+
fs18.mkdirSync(path13.dirname(stateFile()), { recursive: true });
|
|
6886
|
+
fs18.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
|
|
6635
6887
|
} catch {
|
|
6636
6888
|
}
|
|
6637
6889
|
}
|
|
@@ -6681,14 +6933,14 @@ var init_update = __esm({
|
|
|
6681
6933
|
PKG = "@bunshin-cc/bunshin";
|
|
6682
6934
|
LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
|
|
6683
6935
|
DAY = 864e5;
|
|
6684
|
-
stateFile = () =>
|
|
6936
|
+
stateFile = () => path13.join(homeDir(), ".bunshin", "update.json");
|
|
6685
6937
|
disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
|
|
6686
6938
|
}
|
|
6687
6939
|
});
|
|
6688
6940
|
|
|
6689
6941
|
// src/v2/hooks.ts
|
|
6690
|
-
import
|
|
6691
|
-
import
|
|
6942
|
+
import fs19 from "node:fs";
|
|
6943
|
+
import path14 from "node:path";
|
|
6692
6944
|
function inContext(s) {
|
|
6693
6945
|
return s.inContext ?? [...s.loaded, ...s.pushed];
|
|
6694
6946
|
}
|
|
@@ -6755,10 +7007,10 @@ async function keepModCurrent(ctx, config) {
|
|
|
6755
7007
|
const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
6756
7008
|
const current = version2();
|
|
6757
7009
|
if (!modSource2() || installedModVersion2() === current) return;
|
|
6758
|
-
const marker =
|
|
6759
|
-
if (
|
|
6760
|
-
|
|
6761
|
-
|
|
7010
|
+
const marker = path14.join(modHome2(), `.attempted-${current}`);
|
|
7011
|
+
if (fs19.existsSync(marker)) return;
|
|
7012
|
+
fs19.mkdirSync(modHome2(), { recursive: true });
|
|
7013
|
+
fs19.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
|
|
6762
7014
|
ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
|
|
6763
7015
|
} catch {
|
|
6764
7016
|
}
|
|
@@ -6887,35 +7139,6 @@ function skillHint(sec, others) {
|
|
|
6887
7139
|
const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
|
|
6888
7140
|
return `Bunshin: the \`${sec.skill.name}\` skill may apply: ${sec.skill.description.replace(/\s+/g, " ").trim()} If it does, invoke it (Skill tool, or \`/${sec.skill.name}\`) before doing this by hand.${also}`;
|
|
6889
7141
|
}
|
|
6890
|
-
function lastReply(transcriptPath, maxBytes = 256e3) {
|
|
6891
|
-
if (!transcriptPath) return void 0;
|
|
6892
|
-
try {
|
|
6893
|
-
const fd = fs18.openSync(transcriptPath, "r");
|
|
6894
|
-
try {
|
|
6895
|
-
const size = fs18.fstatSync(fd).size;
|
|
6896
|
-
const len = Math.min(size, maxBytes);
|
|
6897
|
-
const buf = Buffer.alloc(len);
|
|
6898
|
-
fs18.readSync(fd, buf, 0, len, size - len);
|
|
6899
|
-
const lines = buf.toString("utf8").split("\n");
|
|
6900
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6901
|
-
let m;
|
|
6902
|
-
try {
|
|
6903
|
-
m = JSON.parse(lines[i]);
|
|
6904
|
-
} catch {
|
|
6905
|
-
continue;
|
|
6906
|
-
}
|
|
6907
|
-
if (m?.type !== "assistant" || m.isSidechain) continue;
|
|
6908
|
-
const c = m.message?.content;
|
|
6909
|
-
const text2 = Array.isArray(c) ? c.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("\n") : typeof c === "string" ? c : "";
|
|
6910
|
-
if (text2.trim()) return redact2(text2.trim()).slice(0, 1500);
|
|
6911
|
-
}
|
|
6912
|
-
} finally {
|
|
6913
|
-
fs18.closeSync(fd);
|
|
6914
|
-
}
|
|
6915
|
-
} catch {
|
|
6916
|
-
}
|
|
6917
|
-
return void 0;
|
|
6918
|
-
}
|
|
6919
7142
|
async function remoteAuto(ctx, prompt, exclude, source, context) {
|
|
6920
7143
|
const config = ctx.store.config();
|
|
6921
7144
|
if (!canCall(config) || !config.recall.useMothership) return null;
|
|
@@ -6939,6 +7162,14 @@ function invokedSkill(prompt, sections) {
|
|
|
6939
7162
|
const name = m[1].split(":").pop().toLowerCase();
|
|
6940
7163
|
return sections.find((s) => s.skill && s.skill.name.toLowerCase() === name);
|
|
6941
7164
|
}
|
|
7165
|
+
function startRoute(v2, ctx, sessionId, now) {
|
|
7166
|
+
const prev = readJob(v2, sessionId);
|
|
7167
|
+
if (prev && !prev.delivered && !prev.cancelled) v2.log("v2-router-undelivered", { session: sessionId, seq: prev.seq, answered: !!readAnswer(v2, sessionId, prev.seq) });
|
|
7168
|
+
const job = { seq: (prev?.seq ?? 0) + 1, at: now };
|
|
7169
|
+
writeJob(v2, sessionId, job);
|
|
7170
|
+
ctx.detach(["--root", ctx.store.root, "route", "--session", sessionId, "--seq", String(job.seq), "--quiet"], { cwd: ctx.cwd });
|
|
7171
|
+
return job;
|
|
7172
|
+
}
|
|
6942
7173
|
async function onPrompt(ctx) {
|
|
6943
7174
|
const prompt = ctx.input.prompt ?? "";
|
|
6944
7175
|
if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
|
|
@@ -6960,7 +7191,9 @@ async function onPrompt(ctx) {
|
|
|
6960
7191
|
const config = ctx.store.config();
|
|
6961
7192
|
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
6962
7193
|
const injected = inContext(session);
|
|
7194
|
+
const job = config.recall.router === "claude-cli" && canCall(config) ? startRoute(v2, ctx, session.sessionId, now) : void 0;
|
|
6963
7195
|
const named = invokedSkill(prompt, view.sections);
|
|
7196
|
+
if (named && job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
|
|
6964
7197
|
if (named) {
|
|
6965
7198
|
const why = `you ran /${named.skill.name}`;
|
|
6966
7199
|
const deployed = deployEnabled(config, view.ctx);
|
|
@@ -6987,6 +7220,12 @@ async function onPrompt(ctx) {
|
|
|
6987
7220
|
if (reply) context.reply = reply;
|
|
6988
7221
|
const remote = await remoteAuto(ctx, prompt, injected, session.source, Object.keys(context).length ? context : void 0);
|
|
6989
7222
|
const local = (id) => view.sections.find((s) => s.id === id);
|
|
7223
|
+
if (job && !remote?.router) {
|
|
7224
|
+
const dense = remote ? { serve: remote.section?.id, hint: (remote.hint ?? []).map((h) => h.id), reason: remote.reason } : { serve: res.section?.id, hint: res.hint.map((h) => h.id), reason: res.reason };
|
|
7225
|
+
writeJob(v2, session.sessionId, { ...job, dense: { ...dense, hint: dense.hint.slice(0, 3) } });
|
|
7226
|
+
return withLive({});
|
|
7227
|
+
}
|
|
7228
|
+
if (job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
|
|
6990
7229
|
if (remote) {
|
|
6991
7230
|
const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
|
|
6992
7231
|
if (remote.section) {
|
|
@@ -7059,15 +7298,69 @@ function toolPaths(input) {
|
|
|
7059
7298
|
}
|
|
7060
7299
|
return [...new Set(out)];
|
|
7061
7300
|
}
|
|
7062
|
-
function
|
|
7301
|
+
async function routedText(v2, ctx, session) {
|
|
7302
|
+
const config = ctx.store.config();
|
|
7303
|
+
if (config.recall.router !== "claude-cli") return "";
|
|
7304
|
+
const job = readJob(v2, session.sessionId);
|
|
7305
|
+
if (!job || job.delivered || job.cancelled) return "";
|
|
7306
|
+
const answer = readAnswer(v2, session.sessionId, job.seq);
|
|
7307
|
+
if (!answer) return "";
|
|
7308
|
+
const fallback = !!answer.error;
|
|
7309
|
+
writeJob(v2, session.sessionId, { ...job, delivered: { at: (/* @__PURE__ */ new Date()).toISOString(), ...ctx.input.tool_name ? { tool: ctx.input.tool_name } : {}, ...fallback ? { fallback } : {} } });
|
|
7310
|
+
const seen = new Set(inContext(session));
|
|
7311
|
+
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
7312
|
+
const find = async (id) => {
|
|
7313
|
+
const here = view.sections.find((s) => s.id === id);
|
|
7314
|
+
if (here || !canCall(config)) return here;
|
|
7315
|
+
try {
|
|
7316
|
+
return (await v2api(config, "GET", `/sections/${encodeURIComponent(id)}`, void 0, { timeoutMs: Math.max(300, Math.min(2e3, ctx.deadline - Date.now() - 300)) })).section;
|
|
7317
|
+
} catch {
|
|
7318
|
+
return void 0;
|
|
7319
|
+
}
|
|
7320
|
+
};
|
|
7321
|
+
const serveId = fallback ? job.dense?.serve : answer.serve;
|
|
7322
|
+
const fresh = (ids2) => [...new Set(ids2.filter((id) => !!id && !seen.has(id)))];
|
|
7323
|
+
const [doc] = await Promise.all(fresh([serveId]).map(find));
|
|
7324
|
+
const rules = (await Promise.all(fresh(fallback ? [] : answer.rules ?? []).filter((id) => id !== doc?.id).map(find))).filter((s) => !!s);
|
|
7325
|
+
const hints = fresh(fallback ? job.dense?.hint ?? [] : answer.hint ?? []).filter((id) => id !== doc?.id && !session.hinted?.includes(id));
|
|
7326
|
+
const hintTitles = hints.map((id) => view.sections.find((s) => s.id === id)?.title).filter((t) => !!t);
|
|
7327
|
+
v2.log("v2-router-delivered", { session: session.sessionId, seq: job.seq, tool: ctx.input.tool_name, afterMs: Date.now() - Date.parse(job.at), serve: doc?.id ?? null, rules: rules.map((r) => r.id), hint: hints, fallback, ...answer.error ? { error: answer.error } : {} });
|
|
7328
|
+
const ids = [...doc ? [doc.id] : [], ...rules.map((r) => r.id)];
|
|
7329
|
+
if (!ids.length && !hintTitles.length) return "";
|
|
7330
|
+
v2.updateSession(session.sessionId, ctx.cwd, (s) => {
|
|
7331
|
+
addUnique(s.served, ...ids, ...hints);
|
|
7332
|
+
addUnique(s.loaded, ...ids);
|
|
7333
|
+
markInContext(s, ...ids);
|
|
7334
|
+
s.hinted = s.hinted ?? [];
|
|
7335
|
+
addUnique(s.hinted, ...hints);
|
|
7336
|
+
});
|
|
7337
|
+
const why = fallback ? "your request matches it" : "the router picked it for the user's latest request";
|
|
7338
|
+
updateFeed(v2, session.sessionId, (f) => {
|
|
7339
|
+
for (const r of [...rules].reverse()) addServed(f, { id: r.id, title: r.title, kind: r.kind, how: "auto", why: "a lesson learned before applies here" });
|
|
7340
|
+
if (doc) addServed(f, { id: doc.id, title: doc.title, kind: doc.kind, ...doc.skill ? { skill: doc.skill.name } : {}, how: "auto", why });
|
|
7341
|
+
if (doc) addEvent(f, "served", `served ${label({ ...doc.skill ? { skill: doc.skill.name } : {}, title: doc.title, kind: doc.kind })} \u2014 ${fallback ? "dense (router failed)" : "router"}`);
|
|
7342
|
+
if (rules.length) addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
|
|
7343
|
+
});
|
|
7344
|
+
return [
|
|
7345
|
+
doc ? renderSection(doc, why, { deployed: !!doc.skill && deployEnabled(config, view.ctx) }) : "",
|
|
7346
|
+
...rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })),
|
|
7347
|
+
hintTitles.length ? hintLine(hintTitles, config.harness) : ""
|
|
7348
|
+
].filter(Boolean).join("\n\n");
|
|
7349
|
+
}
|
|
7350
|
+
async function onPreToolUse(ctx) {
|
|
7063
7351
|
const v2 = new V2Store(ctx.store);
|
|
7064
7352
|
const session = touch(v2, ctx, (s) => {
|
|
7065
7353
|
s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7066
7354
|
});
|
|
7067
7355
|
const live = liveUpdate(v2, ctx, session);
|
|
7356
|
+
let routed = "";
|
|
7357
|
+
try {
|
|
7358
|
+
routed = await routedText(v2, ctx, session);
|
|
7359
|
+
} catch {
|
|
7360
|
+
}
|
|
7068
7361
|
const withLive = (out) => {
|
|
7069
|
-
|
|
7070
|
-
return
|
|
7362
|
+
const text3 = [live, routed, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
|
|
7363
|
+
return text3 === (out.hookSpecificOutput?.additionalContext ?? "") ? out : additional("PreToolUse", text3);
|
|
7071
7364
|
};
|
|
7072
7365
|
const raw = toolPaths(ctx.input.tool_input);
|
|
7073
7366
|
if (!raw.length) return withLive({});
|
|
@@ -7076,7 +7369,7 @@ function onPreToolUse(ctx) {
|
|
|
7076
7369
|
const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
|
|
7077
7370
|
const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
|
|
7078
7371
|
if (!paths.length && !text2) return withLive({});
|
|
7079
|
-
const seen = new Set(inContext(session));
|
|
7372
|
+
const seen = new Set(inContext(v2.readSession(session.sessionId) ?? session));
|
|
7080
7373
|
const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
|
|
7081
7374
|
if (!hits.length) return withLive({});
|
|
7082
7375
|
const ids = hits.map((h) => h.section.id);
|
|
@@ -7327,8 +7620,10 @@ var init_hooks = __esm({
|
|
|
7327
7620
|
init_signals();
|
|
7328
7621
|
init_store2();
|
|
7329
7622
|
init_ui();
|
|
7623
|
+
init_route();
|
|
7330
7624
|
init_source();
|
|
7331
7625
|
init_view();
|
|
7626
|
+
init_route();
|
|
7332
7627
|
NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
|
|
7333
7628
|
NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
7334
7629
|
CHECKPOINT_MS = 30 * 6e4;
|
|
@@ -7342,8 +7637,8 @@ var init_hooks = __esm({
|
|
|
7342
7637
|
});
|
|
7343
7638
|
|
|
7344
7639
|
// src/hooks.ts
|
|
7345
|
-
import
|
|
7346
|
-
import
|
|
7640
|
+
import fs20 from "node:fs";
|
|
7641
|
+
import path15 from "node:path";
|
|
7347
7642
|
function normalizeEvent(raw) {
|
|
7348
7643
|
return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
|
|
7349
7644
|
}
|
|
@@ -7411,19 +7706,19 @@ async function handleHook(rawEvent, input, opts = {}) {
|
|
|
7411
7706
|
}
|
|
7412
7707
|
function deferToProject(cwd, env = process.env) {
|
|
7413
7708
|
if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
|
|
7414
|
-
let
|
|
7709
|
+
let dir2 = path15.resolve(cwd);
|
|
7415
7710
|
for (let i = 0; i < 40; i++) {
|
|
7416
7711
|
for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
|
|
7417
7712
|
try {
|
|
7418
|
-
const text2 =
|
|
7713
|
+
const text2 = fs20.readFileSync(path15.join(dir2, f), "utf8");
|
|
7419
7714
|
if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
|
|
7420
7715
|
} catch {
|
|
7421
7716
|
}
|
|
7422
7717
|
}
|
|
7423
|
-
if (
|
|
7424
|
-
const up =
|
|
7425
|
-
if (up ===
|
|
7426
|
-
|
|
7718
|
+
if (fs20.existsSync(path15.join(dir2, ".git"))) return false;
|
|
7719
|
+
const up = path15.dirname(dir2);
|
|
7720
|
+
if (up === dir2) return false;
|
|
7721
|
+
dir2 = up;
|
|
7427
7722
|
}
|
|
7428
7723
|
return false;
|
|
7429
7724
|
}
|
|
@@ -7469,11 +7764,11 @@ var init_hooks2 = __esm({
|
|
|
7469
7764
|
});
|
|
7470
7765
|
|
|
7471
7766
|
// src/project-hooks.ts
|
|
7472
|
-
import
|
|
7473
|
-
import
|
|
7767
|
+
import fs21 from "node:fs";
|
|
7768
|
+
import path16 from "node:path";
|
|
7474
7769
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
7475
7770
|
function codexProjectHooks(cwd) {
|
|
7476
|
-
const legacyFile =
|
|
7771
|
+
const legacyFile = path16.join(cwd, ".codex", "hooks.json");
|
|
7477
7772
|
const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
|
|
7478
7773
|
encoding: "utf8",
|
|
7479
7774
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -7487,14 +7782,14 @@ function codexProjectHooks(cwd) {
|
|
|
7487
7782
|
} catch {
|
|
7488
7783
|
return { file: legacyFile, shared: false, legacyFile };
|
|
7489
7784
|
}
|
|
7490
|
-
const linked =
|
|
7785
|
+
const linked = fs21.realpathSync(common) !== fs21.realpathSync(directory);
|
|
7491
7786
|
if (linked) {
|
|
7492
|
-
if (
|
|
7787
|
+
if (path16.basename(common) !== ".git" || !fs21.statSync(common).isDirectory()) {
|
|
7493
7788
|
throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
|
|
7494
7789
|
}
|
|
7495
|
-
root =
|
|
7790
|
+
root = path16.dirname(common);
|
|
7496
7791
|
}
|
|
7497
|
-
const file =
|
|
7792
|
+
const file = fs21.realpathSync(root) === fs21.realpathSync(cwd) ? legacyFile : path16.join(root, ".codex", "hooks.json");
|
|
7498
7793
|
return { file, shared: linked, legacyFile };
|
|
7499
7794
|
}
|
|
7500
7795
|
var init_project_hooks = __esm({
|
|
@@ -7506,6 +7801,7 @@ var init_project_hooks = __esm({
|
|
|
7506
7801
|
// src/init.ts
|
|
7507
7802
|
var init_exports = {};
|
|
7508
7803
|
__export(init_exports, {
|
|
7804
|
+
ALL_TOOLS_MATCHER: () => ALL_TOOLS_MATCHER,
|
|
7509
7805
|
BORROW_COMMAND: () => BORROW_COMMAND,
|
|
7510
7806
|
CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
|
|
7511
7807
|
HOOK_EVENTS: () => HOOK_EVENTS,
|
|
@@ -7527,8 +7823,8 @@ __export(init_exports, {
|
|
|
7527
7823
|
runInit: () => runInit
|
|
7528
7824
|
});
|
|
7529
7825
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7530
|
-
import
|
|
7531
|
-
import
|
|
7826
|
+
import fs22 from "node:fs";
|
|
7827
|
+
import path17 from "node:path";
|
|
7532
7828
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
7533
7829
|
function isBunshinEntry(entry) {
|
|
7534
7830
|
return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
|
|
@@ -7553,7 +7849,7 @@ function removeBunshinHooks(settings) {
|
|
|
7553
7849
|
else delete out.hooks;
|
|
7554
7850
|
return out;
|
|
7555
7851
|
}
|
|
7556
|
-
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
|
|
7852
|
+
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false, allTools = false } = {}) {
|
|
7557
7853
|
const base = removeBunshinHooks(settings);
|
|
7558
7854
|
const hooks = { ...base.hooks ?? {} };
|
|
7559
7855
|
for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
|
|
@@ -7561,7 +7857,8 @@ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code
|
|
|
7561
7857
|
const hook = bunshinHookEntry(arg, timeout, bin);
|
|
7562
7858
|
hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
|
|
7563
7859
|
if (harness === "codex") delete hook._bunshin;
|
|
7564
|
-
|
|
7860
|
+
const m = matcher && allTools && event === "PreToolUse" ? ALL_TOOLS_MATCHER : matcher;
|
|
7861
|
+
groups.push(m ? { matcher: m, hooks: [hook] } : { hooks: [hook] });
|
|
7565
7862
|
hooks[event] = groups;
|
|
7566
7863
|
}
|
|
7567
7864
|
return { ...base, hooks };
|
|
@@ -7573,16 +7870,16 @@ function countBunshinHooks(settings) {
|
|
|
7573
7870
|
);
|
|
7574
7871
|
}
|
|
7575
7872
|
function claudeSettingsPath() {
|
|
7576
|
-
return
|
|
7873
|
+
return path17.join(homeDir(), ".claude", "settings.json");
|
|
7577
7874
|
}
|
|
7578
7875
|
function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
7579
|
-
const file = scope === "global" ?
|
|
7876
|
+
const file = scope === "global" ? path17.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path17.join(cwd, ".pi/extensions/bunshin.js");
|
|
7580
7877
|
const marker = "// Managed by Bunshin setup.";
|
|
7581
7878
|
try {
|
|
7582
|
-
const previous = fileExists(file) ?
|
|
7879
|
+
const previous = fileExists(file) ? fs22.readFileSync(file, "utf8") : "";
|
|
7583
7880
|
if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
|
|
7584
7881
|
if (remove) {
|
|
7585
|
-
if (previous)
|
|
7882
|
+
if (previous) fs22.unlinkSync(file);
|
|
7586
7883
|
return { file, installed: 0, changed: !!previous };
|
|
7587
7884
|
}
|
|
7588
7885
|
const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
|
|
@@ -7591,24 +7888,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
|
7591
7888
|
export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
7592
7889
|
`;
|
|
7593
7890
|
if (text2 !== previous) {
|
|
7594
|
-
ensureDir(
|
|
7595
|
-
|
|
7891
|
+
ensureDir(path17.dirname(file));
|
|
7892
|
+
fs22.writeFileSync(file, text2);
|
|
7596
7893
|
}
|
|
7597
7894
|
return { file, installed: 1, changed: text2 !== previous };
|
|
7598
7895
|
} catch (error) {
|
|
7599
7896
|
return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
7600
7897
|
}
|
|
7601
7898
|
}
|
|
7602
|
-
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
|
|
7899
|
+
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false, allTools = false } = {}) {
|
|
7603
7900
|
try {
|
|
7604
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7605
|
-
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
|
|
7901
|
+
const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
|
|
7902
|
+
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user, allTools });
|
|
7606
7903
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7607
7904
|
if (changed) {
|
|
7608
|
-
ensureDir(
|
|
7905
|
+
ensureDir(path17.dirname(file));
|
|
7609
7906
|
if (fileExists(file)) {
|
|
7610
7907
|
try {
|
|
7611
|
-
|
|
7908
|
+
fs22.copyFileSync(file, `${file}.bunshin-backup`);
|
|
7612
7909
|
} catch {
|
|
7613
7910
|
}
|
|
7614
7911
|
}
|
|
@@ -7623,23 +7920,23 @@ function mcpLauncher(bin) {
|
|
|
7623
7920
|
if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
|
|
7624
7921
|
let script = process.argv[1] ?? "";
|
|
7625
7922
|
try {
|
|
7626
|
-
script =
|
|
7923
|
+
script = fs22.realpathSync(script);
|
|
7627
7924
|
} catch {
|
|
7628
7925
|
}
|
|
7629
7926
|
return { command: process.execPath, args: [script, "mcp"] };
|
|
7630
7927
|
}
|
|
7631
7928
|
function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
|
|
7632
7929
|
try {
|
|
7633
|
-
const current = fileExists(file) ?
|
|
7930
|
+
const current = fileExists(file) ? fs22.readFileSync(file, "utf8") : null;
|
|
7634
7931
|
const ours = current === null || current === content;
|
|
7635
7932
|
if (remove) {
|
|
7636
|
-
if (current !== null && ours)
|
|
7933
|
+
if (current !== null && ours) fs22.unlinkSync(file);
|
|
7637
7934
|
return { file, installed: 0, changed: current !== null && ours };
|
|
7638
7935
|
}
|
|
7639
|
-
if (!ours) return { file, installed: 0, changed: false, error: `a different ${
|
|
7936
|
+
if (!ours) return { file, installed: 0, changed: false, error: `a different ${path17.basename(file)} is already there; left as it is` };
|
|
7640
7937
|
if (current === null) {
|
|
7641
|
-
|
|
7642
|
-
|
|
7938
|
+
fs22.mkdirSync(path17.dirname(file), { recursive: true });
|
|
7939
|
+
fs22.writeFileSync(file, content);
|
|
7643
7940
|
}
|
|
7644
7941
|
return { file, installed: 1, changed: current === null };
|
|
7645
7942
|
} catch (err) {
|
|
@@ -7648,7 +7945,7 @@ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND })
|
|
|
7648
7945
|
}
|
|
7649
7946
|
function installMcpConfig({ file, launcher, remove = false }) {
|
|
7650
7947
|
try {
|
|
7651
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7948
|
+
const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
|
|
7652
7949
|
const servers = { ...before.mcpServers ?? {} };
|
|
7653
7950
|
if (remove) delete servers.bunshin;
|
|
7654
7951
|
else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
|
|
@@ -7656,7 +7953,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7656
7953
|
if (!Object.keys(servers).length) delete after.mcpServers;
|
|
7657
7954
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7658
7955
|
if (changed) {
|
|
7659
|
-
if (!Object.keys(after).length && fileExists(file))
|
|
7956
|
+
if (!Object.keys(after).length && fileExists(file)) fs22.unlinkSync(file);
|
|
7660
7957
|
else if (Object.keys(after).length) writeJsonAtomic(file, after);
|
|
7661
7958
|
}
|
|
7662
7959
|
return { file, installed: servers.bunshin ? 1 : 0, changed };
|
|
@@ -7665,7 +7962,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7665
7962
|
}
|
|
7666
7963
|
}
|
|
7667
7964
|
function installUserMcp({ launcher, remove = false, claude = "claude" }) {
|
|
7668
|
-
const file =
|
|
7965
|
+
const file = path17.join(homeDir(), ".claude.json");
|
|
7669
7966
|
const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
|
|
7670
7967
|
try {
|
|
7671
7968
|
let had = false;
|
|
@@ -7691,22 +7988,22 @@ function runInit(opts = {}) {
|
|
|
7691
7988
|
const cwd = opts.cwd ?? process.cwd();
|
|
7692
7989
|
const warnings = [];
|
|
7693
7990
|
const scope = opts.scope ?? "repo";
|
|
7694
|
-
const root = scope === "global" ?
|
|
7695
|
-
if (
|
|
7991
|
+
const root = scope === "global" ? path17.join(homeDir(), ".bunshin") : path17.join(cwd, ".bunshin");
|
|
7992
|
+
if (fs22.existsSync(root) && !new Store(root).trusted && fs22.readdirSync(root).length) {
|
|
7696
7993
|
const aside = `${root}.untrusted-${Date.now()}`;
|
|
7697
|
-
|
|
7994
|
+
fs22.renameSync(root, aside);
|
|
7698
7995
|
warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
|
|
7699
7996
|
}
|
|
7700
|
-
const existed = fileExists(
|
|
7997
|
+
const existed = fileExists(path17.join(root, "config.json"));
|
|
7701
7998
|
const store = new Store(root);
|
|
7702
7999
|
store.ensureLayout();
|
|
7703
|
-
|
|
8000
|
+
fs22.chmodSync(root, 448);
|
|
7704
8001
|
const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
|
|
7705
|
-
if (scope === "repo" && (hookScope === "project" || fileExists(
|
|
7706
|
-
const ignore =
|
|
7707
|
-
const current = fileExists(ignore) ?
|
|
8002
|
+
if (scope === "repo" && (hookScope === "project" || fileExists(path17.join(cwd, ".git")))) {
|
|
8003
|
+
const ignore = path17.join(cwd, ".gitignore");
|
|
8004
|
+
const current = fileExists(ignore) ? fs22.readFileSync(ignore, "utf8") : "";
|
|
7708
8005
|
if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
|
|
7709
|
-
|
|
8006
|
+
fs22.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
|
|
7710
8007
|
`);
|
|
7711
8008
|
}
|
|
7712
8009
|
}
|
|
@@ -7736,17 +8033,17 @@ function runInit(opts = {}) {
|
|
|
7736
8033
|
}
|
|
7737
8034
|
if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
|
|
7738
8035
|
const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
|
|
7739
|
-
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ??
|
|
7740
|
-
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
|
|
8036
|
+
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path17.join(process.env.CODEX_HOME ?? path17.join(homeDir(), ".codex"), "hooks.json") : hookScope === "project" ? path17.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
|
|
8037
|
+
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile, allTools: config.recall.router === "claude-cli" });
|
|
7741
8038
|
if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
|
|
7742
|
-
const local =
|
|
7743
|
-
if (fileExists(local) &&
|
|
8039
|
+
const local = path17.join(cwd, ".claude", "settings.local.json");
|
|
8040
|
+
if (fileExists(local) && path17.resolve(local) !== path17.resolve(file)) installClaudeHooks({ file: local, remove: true });
|
|
7744
8041
|
}
|
|
7745
8042
|
if (projectHooks?.shared) {
|
|
7746
8043
|
hooks.sharedWithWorktrees = true;
|
|
7747
|
-
if (!hooks.error && !opts.removeHooks) ensureDir(
|
|
8044
|
+
if (!hooks.error && !opts.removeHooks) ensureDir(path17.dirname(projectHooks.legacyFile));
|
|
7748
8045
|
}
|
|
7749
|
-
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) &&
|
|
8046
|
+
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs22.realpathSync(projectHooks.file) !== fs22.realpathSync(projectHooks.legacyFile)) {
|
|
7750
8047
|
const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
|
|
7751
8048
|
if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
|
|
7752
8049
|
}
|
|
@@ -7756,15 +8053,15 @@ function runInit(opts = {}) {
|
|
|
7756
8053
|
let learnCommand;
|
|
7757
8054
|
if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
|
|
7758
8055
|
const user = hookScope === "user";
|
|
7759
|
-
mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file:
|
|
8056
|
+
mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file: path17.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
|
|
7760
8057
|
if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
|
|
7761
|
-
learnCommand = installLearnCommand({ file:
|
|
8058
|
+
learnCommand = installLearnCommand({ file: path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
|
|
7762
8059
|
if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
|
|
7763
8060
|
}
|
|
7764
8061
|
let borrowCommand;
|
|
7765
8062
|
if (opts.removeHooks || !opts.noHooks) {
|
|
7766
8063
|
const user = hookScope === "user";
|
|
7767
|
-
const file = config.harness === "claude-code" ?
|
|
8064
|
+
const file = config.harness === "claude-code" ? path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path17.join(process.env.CODEX_HOME ?? path17.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path17.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path17.join(cwd, ".pi", "prompts", "borrow.md") : null;
|
|
7768
8065
|
if (file) {
|
|
7769
8066
|
borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
|
|
7770
8067
|
if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
|
|
@@ -7790,7 +8087,7 @@ function otherHarnessInstructions() {
|
|
|
7790
8087
|
" Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
|
|
7791
8088
|
].join("\n");
|
|
7792
8089
|
}
|
|
7793
|
-
var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
|
|
8090
|
+
var PRE_TOOL_MATCHER, ALL_TOOLS_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
|
|
7794
8091
|
var init_init = __esm({
|
|
7795
8092
|
"src/init.ts"() {
|
|
7796
8093
|
"use strict";
|
|
@@ -7800,6 +8097,7 @@ var init_init = __esm({
|
|
|
7800
8097
|
init_project_hooks();
|
|
7801
8098
|
init_trust();
|
|
7802
8099
|
PRE_TOOL_MATCHER = "Read|Edit|Write|MultiEdit|NotebookEdit";
|
|
8100
|
+
ALL_TOOLS_MATCHER = "*";
|
|
7803
8101
|
HOOK_EVENTS = [
|
|
7804
8102
|
{ event: "SessionStart", arg: "session-start", timeout: 10 },
|
|
7805
8103
|
{ event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
|
|
@@ -7921,9 +8219,9 @@ __export(commands_exports, {
|
|
|
7921
8219
|
setupRobot: () => setupRobot,
|
|
7922
8220
|
v2Status: () => v2Status
|
|
7923
8221
|
});
|
|
7924
|
-
import
|
|
7925
|
-
import
|
|
7926
|
-
import { createHash as
|
|
8222
|
+
import fs23 from "node:fs";
|
|
8223
|
+
import path18 from "node:path";
|
|
8224
|
+
import { createHash as createHash6, randomBytes as randomBytes4 } from "node:crypto";
|
|
7927
8225
|
function cmdReview(store, args, cwd, out, err) {
|
|
7928
8226
|
const v2 = new V2Store(store);
|
|
7929
8227
|
const sub = args._[1] ?? "list";
|
|
@@ -8093,11 +8391,11 @@ function cmdSuggest(store, args, cwd, out, err) {
|
|
|
8093
8391
|
err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
|
|
8094
8392
|
return 1;
|
|
8095
8393
|
}
|
|
8096
|
-
const harnessDir =
|
|
8097
|
-
const file = str3(args.flags.file) ?
|
|
8394
|
+
const harnessDir = path18.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
|
|
8395
|
+
const file = str3(args.flags.file) ? path18.resolve(cwd, str3(args.flags.file)) : path18.join(harnessDir, rel);
|
|
8098
8396
|
let content;
|
|
8099
8397
|
try {
|
|
8100
|
-
content =
|
|
8398
|
+
content = fs23.readFileSync(file, "utf8");
|
|
8101
8399
|
} catch {
|
|
8102
8400
|
err(`bunshin suggest: cannot read ${file}
|
|
8103
8401
|
`);
|
|
@@ -8218,15 +8516,15 @@ async function cmdQuestions(store, args, out, err) {
|
|
|
8218
8516
|
return 0;
|
|
8219
8517
|
}
|
|
8220
8518
|
function saveCredential(storeRoot, apiUrl, apiKey, id) {
|
|
8221
|
-
const
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
const file =
|
|
8519
|
+
const dir2 = path18.join(homeDir(), ".config/bunshin/credentials");
|
|
8520
|
+
fs23.mkdirSync(dir2, { recursive: true, mode: 448 });
|
|
8521
|
+
fs23.chmodSync(dir2, 448);
|
|
8522
|
+
const file = path18.join(dir2, `${createHash6("sha256").update(`${storeRoot}
|
|
8225
8523
|
${apiUrl}
|
|
8226
8524
|
${id}`).digest("hex")}.json`);
|
|
8227
8525
|
const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
|
|
8228
|
-
|
|
8229
|
-
|
|
8526
|
+
fs23.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8527
|
+
fs23.renameSync(tmp, file);
|
|
8230
8528
|
return file;
|
|
8231
8529
|
}
|
|
8232
8530
|
async function setupRobot(opts) {
|
|
@@ -8234,10 +8532,10 @@ async function setupRobot(opts) {
|
|
|
8234
8532
|
const existing = Store.discover(opts.cwd);
|
|
8235
8533
|
let store;
|
|
8236
8534
|
const harness = opts.harness ?? "other";
|
|
8237
|
-
if (existing &&
|
|
8535
|
+
if (existing && path18.resolve(path18.dirname(existing.root)) === path18.resolve(opts.cwd) && harness === "other") store = existing;
|
|
8238
8536
|
else {
|
|
8239
8537
|
runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
|
|
8240
|
-
store = new Store(
|
|
8538
|
+
store = new Store(path18.join(opts.cwd, ".bunshin"));
|
|
8241
8539
|
}
|
|
8242
8540
|
let config = store.config();
|
|
8243
8541
|
const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
|
|
@@ -8391,16 +8689,16 @@ __export(setup_exports, {
|
|
|
8391
8689
|
setupProject: () => setupProject,
|
|
8392
8690
|
shellQuote: () => shellQuote
|
|
8393
8691
|
});
|
|
8394
|
-
import
|
|
8395
|
-
import
|
|
8396
|
-
import { createHash as
|
|
8692
|
+
import fs24 from "node:fs";
|
|
8693
|
+
import path19 from "node:path";
|
|
8694
|
+
import { createHash as createHash7, randomBytes as randomBytes5 } from "node:crypto";
|
|
8397
8695
|
function executable(name) {
|
|
8398
|
-
const directories = [...(process.env.PATH ?? "").split(
|
|
8696
|
+
const directories = [...(process.env.PATH ?? "").split(path19.delimiter), path19.join(homeDir(), ".local/bin")];
|
|
8399
8697
|
for (const directory of directories) {
|
|
8400
|
-
const file =
|
|
8698
|
+
const file = path19.resolve(directory, name);
|
|
8401
8699
|
try {
|
|
8402
|
-
|
|
8403
|
-
if (
|
|
8700
|
+
fs24.accessSync(file, fs24.constants.X_OK);
|
|
8701
|
+
if (fs24.statSync(file).isFile()) return file;
|
|
8404
8702
|
} catch {
|
|
8405
8703
|
}
|
|
8406
8704
|
}
|
|
@@ -8408,8 +8706,8 @@ function executable(name) {
|
|
|
8408
8706
|
}
|
|
8409
8707
|
function hookBinary(harness) {
|
|
8410
8708
|
const agent = executable(harness === "claude-code" ? "claude" : harness);
|
|
8411
|
-
const directories = [.../* @__PURE__ */ new Set([
|
|
8412
|
-
return `PATH=${shellQuote(directories.join(
|
|
8709
|
+
const directories = [.../* @__PURE__ */ new Set([path19.dirname(process.execPath), path19.join(homeDir(), ".local/bin"), ...agent ? [path19.dirname(agent)] : []])];
|
|
8710
|
+
return `PATH=${shellQuote(directories.join(path19.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs24.realpathSync(process.argv[1]))}`;
|
|
8413
8711
|
}
|
|
8414
8712
|
function apiAddress(raw) {
|
|
8415
8713
|
const url = new URL(raw);
|
|
@@ -8478,23 +8776,23 @@ async function setupProject(opts) {
|
|
|
8478
8776
|
const apiKey = opts.apiKey.trim();
|
|
8479
8777
|
if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
|
|
8480
8778
|
const profile = await connectionProfile(apiUrl, apiKey);
|
|
8481
|
-
const root =
|
|
8779
|
+
const root = path19.join(opts.cwd, ".bunshin");
|
|
8482
8780
|
const store = new Store(root);
|
|
8483
8781
|
const previous = store.config();
|
|
8484
8782
|
if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
|
|
8485
8783
|
throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
|
|
8486
8784
|
}
|
|
8487
|
-
const credentialDirectory =
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
const name =
|
|
8785
|
+
const credentialDirectory = path19.join(homeDir(), ".config/bunshin/credentials");
|
|
8786
|
+
fs24.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
|
|
8787
|
+
fs24.chmodSync(credentialDirectory, 448);
|
|
8788
|
+
const name = createHash7("sha256").update(`${root}
|
|
8491
8789
|
${apiUrl}
|
|
8492
8790
|
${profile.orgId}
|
|
8493
8791
|
${profile.userId}`).digest("hex");
|
|
8494
|
-
const credentialFile =
|
|
8792
|
+
const credentialFile = path19.join(credentialDirectory, `${name}.json`);
|
|
8495
8793
|
const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
|
|
8496
|
-
|
|
8497
|
-
|
|
8794
|
+
fs24.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8795
|
+
fs24.renameSync(temporary, credentialFile);
|
|
8498
8796
|
const result = runInit({
|
|
8499
8797
|
cwd: opts.cwd,
|
|
8500
8798
|
harness: opts.harness,
|
|
@@ -8846,9 +9144,9 @@ __export(sync_exports2, {
|
|
|
8846
9144
|
resolveConflict: () => resolveConflict,
|
|
8847
9145
|
runSync: () => runSync
|
|
8848
9146
|
});
|
|
8849
|
-
import { createHash as
|
|
8850
|
-
import
|
|
8851
|
-
import
|
|
9147
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
9148
|
+
import path20 from "node:path";
|
|
9149
|
+
import fs25 from "node:fs";
|
|
8852
9150
|
async function api(config, method, route, body, timeoutMs = 1e4) {
|
|
8853
9151
|
const ctrl = new AbortController();
|
|
8854
9152
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
@@ -8886,22 +9184,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
|
|
|
8886
9184
|
}
|
|
8887
9185
|
}
|
|
8888
9186
|
async function runSync(store) {
|
|
8889
|
-
const lock =
|
|
9187
|
+
const lock = path20.join(store.root, "sync.lock");
|
|
8890
9188
|
const deadline = Date.now() + 12e4;
|
|
8891
9189
|
for (; ; ) {
|
|
8892
9190
|
try {
|
|
8893
|
-
|
|
9191
|
+
fs25.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
8894
9192
|
break;
|
|
8895
9193
|
} catch (error) {
|
|
8896
9194
|
if (error.code !== "EEXIST") throw error;
|
|
8897
9195
|
try {
|
|
8898
|
-
const owner = Number(
|
|
9196
|
+
const owner = Number(fs25.readFileSync(lock, "utf8"));
|
|
8899
9197
|
if (Number.isInteger(owner) && owner > 0) {
|
|
8900
9198
|
try {
|
|
8901
9199
|
process.kill(owner, 0);
|
|
8902
9200
|
} catch (error2) {
|
|
8903
9201
|
if (error2.code === "ESRCH") {
|
|
8904
|
-
|
|
9202
|
+
fs25.unlinkSync(lock);
|
|
8905
9203
|
continue;
|
|
8906
9204
|
}
|
|
8907
9205
|
}
|
|
@@ -8920,7 +9218,7 @@ async function runSync(store) {
|
|
|
8920
9218
|
return await syncStore(store);
|
|
8921
9219
|
} finally {
|
|
8922
9220
|
try {
|
|
8923
|
-
|
|
9221
|
+
fs25.unlinkSync(lock);
|
|
8924
9222
|
} catch {
|
|
8925
9223
|
}
|
|
8926
9224
|
}
|
|
@@ -8948,9 +9246,9 @@ async function syncStore(store) {
|
|
|
8948
9246
|
result.errors.push(String(e instanceof Error ? e.message : e));
|
|
8949
9247
|
}
|
|
8950
9248
|
}
|
|
8951
|
-
const acknowledgementsFile =
|
|
9249
|
+
const acknowledgementsFile = path20.join(store.root, "synced-traces.json");
|
|
8952
9250
|
const acknowledgements = readJson(acknowledgementsFile, {});
|
|
8953
|
-
const fingerprint2 = (trace) =>
|
|
9251
|
+
const fingerprint2 = (trace) => createHash8("sha256").update(JSON.stringify(trace)).digest("hex");
|
|
8954
9252
|
const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
|
|
8955
9253
|
for (const t of traces) {
|
|
8956
9254
|
try {
|
|
@@ -9203,11 +9501,11 @@ __export(demo_exports, {
|
|
|
9203
9501
|
runDemo: () => runDemo
|
|
9204
9502
|
});
|
|
9205
9503
|
import assert from "node:assert/strict";
|
|
9206
|
-
import
|
|
9504
|
+
import fs26 from "node:fs";
|
|
9207
9505
|
import os4 from "node:os";
|
|
9208
|
-
import
|
|
9506
|
+
import path21 from "node:path";
|
|
9209
9507
|
async function runDemo(write) {
|
|
9210
|
-
const root =
|
|
9508
|
+
const root = fs26.mkdtempSync(path21.join(os4.tmpdir(), "bunshin-demo-"));
|
|
9211
9509
|
try {
|
|
9212
9510
|
const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
|
|
9213
9511
|
const { lessons, constraint, workflow } = aprScenario();
|
|
@@ -9224,7 +9522,7 @@ Task: ${APR_TASK}
|
|
|
9224
9522
|
write(result.text);
|
|
9225
9523
|
write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
|
|
9226
9524
|
} finally {
|
|
9227
|
-
|
|
9525
|
+
fs26.rmSync(root, { recursive: true, force: true });
|
|
9228
9526
|
}
|
|
9229
9527
|
}
|
|
9230
9528
|
var init_demo = __esm({
|
|
@@ -9237,21 +9535,21 @@ var init_demo = __esm({
|
|
|
9237
9535
|
});
|
|
9238
9536
|
|
|
9239
9537
|
// src/v2/gitremote.ts
|
|
9240
|
-
import
|
|
9241
|
-
import
|
|
9242
|
-
import { createHash as
|
|
9538
|
+
import fs27 from "node:fs";
|
|
9539
|
+
import path22 from "node:path";
|
|
9540
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
9243
9541
|
function saveGitCredential(apiUrl, apiKey) {
|
|
9244
9542
|
const file = gitCredentialFile();
|
|
9245
|
-
|
|
9543
|
+
fs27.mkdirSync(path22.dirname(file), { recursive: true, mode: 448 });
|
|
9246
9544
|
let all = {};
|
|
9247
9545
|
try {
|
|
9248
|
-
all = JSON.parse(
|
|
9546
|
+
all = JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
9249
9547
|
} catch {
|
|
9250
9548
|
}
|
|
9251
9549
|
all[apiUrl.replace(/\/$/, "")] = apiKey;
|
|
9252
9550
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
9253
|
-
|
|
9254
|
-
|
|
9551
|
+
fs27.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
9552
|
+
fs27.renameSync(tmp, file);
|
|
9255
9553
|
}
|
|
9256
9554
|
function resolveGitKey(apiUrl) {
|
|
9257
9555
|
const base = apiUrl.replace(/\/$/, "");
|
|
@@ -9259,20 +9557,20 @@ function resolveGitKey(apiUrl) {
|
|
|
9259
9557
|
const origin = urlOrigin(base);
|
|
9260
9558
|
if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
|
|
9261
9559
|
try {
|
|
9262
|
-
const all = JSON.parse(
|
|
9560
|
+
const all = JSON.parse(fs27.readFileSync(gitCredentialFile(), "utf8"));
|
|
9263
9561
|
if (all[base]) return all[base];
|
|
9264
9562
|
} catch {
|
|
9265
9563
|
}
|
|
9266
|
-
const
|
|
9564
|
+
const dir2 = path22.join(homeDir(), ".config/bunshin/credentials");
|
|
9267
9565
|
let files = [];
|
|
9268
9566
|
try {
|
|
9269
|
-
files =
|
|
9567
|
+
files = fs27.readdirSync(dir2).filter((f) => f.endsWith(".json"));
|
|
9270
9568
|
} catch {
|
|
9271
9569
|
return void 0;
|
|
9272
9570
|
}
|
|
9273
9571
|
for (const f of files.sort()) {
|
|
9274
9572
|
try {
|
|
9275
|
-
const c = JSON.parse(
|
|
9573
|
+
const c = JSON.parse(fs27.readFileSync(path22.join(dir2, f), "utf8"));
|
|
9276
9574
|
if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
|
|
9277
9575
|
} catch {
|
|
9278
9576
|
}
|
|
@@ -9280,7 +9578,7 @@ function resolveGitKey(apiUrl) {
|
|
|
9280
9578
|
return void 0;
|
|
9281
9579
|
}
|
|
9282
9580
|
function refsDigest(refs) {
|
|
9283
|
-
return
|
|
9581
|
+
return createHash9("sha256").update(
|
|
9284
9582
|
[...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
|
|
9285
9583
|
).digest("hex").slice(0, 12);
|
|
9286
9584
|
}
|
|
@@ -9293,7 +9591,7 @@ var init_gitremote = __esm({
|
|
|
9293
9591
|
ZERO_SHA = "0".repeat(40);
|
|
9294
9592
|
REMOTE_PATH = "/v1/git/repos/";
|
|
9295
9593
|
remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
|
|
9296
|
-
gitCredentialFile = () =>
|
|
9594
|
+
gitCredentialFile = () => path22.join(homeDir(), ".config/bunshin/git-credentials.json");
|
|
9297
9595
|
GitApiError = class extends Error {
|
|
9298
9596
|
constructor(status, message, body = null) {
|
|
9299
9597
|
super(message);
|
|
@@ -9385,8 +9683,8 @@ __export(gitrepo_exports, {
|
|
|
9385
9683
|
githubReferences: () => githubReferences,
|
|
9386
9684
|
repoFromUrl: () => repoFromUrl
|
|
9387
9685
|
});
|
|
9388
|
-
import
|
|
9389
|
-
import
|
|
9686
|
+
import fs28 from "node:fs";
|
|
9687
|
+
import path23 from "node:path";
|
|
9390
9688
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9391
9689
|
function git3(cwd, args, opts = {}) {
|
|
9392
9690
|
const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
|
|
@@ -9395,9 +9693,9 @@ function git3(cwd, args, opts = {}) {
|
|
|
9395
9693
|
return res;
|
|
9396
9694
|
}
|
|
9397
9695
|
function onPath(name) {
|
|
9398
|
-
for (const
|
|
9696
|
+
for (const dir2 of (process.env.PATH ?? "").split(path23.delimiter)) {
|
|
9399
9697
|
try {
|
|
9400
|
-
|
|
9698
|
+
fs28.accessSync(path23.join(dir2, name), fs28.constants.X_OK);
|
|
9401
9699
|
return true;
|
|
9402
9700
|
} catch {
|
|
9403
9701
|
}
|
|
@@ -9448,9 +9746,9 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
|
|
|
9448
9746
|
if (!reuseKey || !process.env.BUNSHIN_API_KEY || key !== process.env.BUNSHIN_API_KEY) saveGitCredential(apiUrl, key);
|
|
9449
9747
|
return client;
|
|
9450
9748
|
}
|
|
9451
|
-
function toplevel(
|
|
9452
|
-
const r = git3(
|
|
9453
|
-
if (!r.ok) throw new Error(`${
|
|
9749
|
+
function toplevel(dir2) {
|
|
9750
|
+
const r = git3(dir2, ["rev-parse", "--show-toplevel"], { allowFail: true });
|
|
9751
|
+
if (!r.ok) throw new Error(`${dir2} is not inside a git repository`);
|
|
9454
9752
|
return r.out.trim();
|
|
9455
9753
|
}
|
|
9456
9754
|
function githubReferences(root, repo) {
|
|
@@ -9481,11 +9779,11 @@ function switchOrigin(root, url, out) {
|
|
|
9481
9779
|
}
|
|
9482
9780
|
async function cmdImport(args, cwd, out, err) {
|
|
9483
9781
|
requireHelper();
|
|
9484
|
-
const root = toplevel(
|
|
9782
|
+
const root = toplevel(path23.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
|
|
9485
9783
|
if (git3(root, ["rev-parse", "--is-shallow-repository"]).out.trim() === "true") throw new Error("this clone is shallow; fetch the full history first: git fetch --unshallow");
|
|
9486
9784
|
const rs = remotes(root);
|
|
9487
9785
|
const originUrl = rs.get("origin");
|
|
9488
|
-
const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${
|
|
9786
|
+
const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path23.basename(path23.dirname(root))}/${path23.basename(root)}`;
|
|
9489
9787
|
const client = await connect(args, cwd);
|
|
9490
9788
|
const url = remoteUrl(client.apiUrl, name);
|
|
9491
9789
|
const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
|
|
@@ -9561,7 +9859,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
|
|
|
9561
9859
|
if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
|
|
9562
9860
|
`);
|
|
9563
9861
|
}
|
|
9564
|
-
if (
|
|
9862
|
+
if (fs28.existsSync(path23.join(root, ".github"))) out(`
|
|
9565
9863
|
Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
|
|
9566
9864
|
`);
|
|
9567
9865
|
out(`
|
|
@@ -9572,7 +9870,7 @@ New machines clone with: bunshin repo clone ${name}
|
|
|
9572
9870
|
}
|
|
9573
9871
|
async function cmdAttach(args, cwd, out, err) {
|
|
9574
9872
|
requireHelper();
|
|
9575
|
-
const root = toplevel(
|
|
9873
|
+
const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
|
|
9576
9874
|
const rs = remotes(root);
|
|
9577
9875
|
const originUrl = rs.get("origin");
|
|
9578
9876
|
const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
|
|
@@ -9617,15 +9915,15 @@ async function cmdClone(args, cwd, out) {
|
|
|
9617
9915
|
const name = args._[1];
|
|
9618
9916
|
if (!name) throw new Error("usage: bunshin repo clone owner/name [directory]");
|
|
9619
9917
|
const client = await connect(args, cwd);
|
|
9620
|
-
const
|
|
9621
|
-
const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name),
|
|
9918
|
+
const dir2 = args._[2] ?? name.split("/")[1];
|
|
9919
|
+
const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir2], { cwd, stdio: "inherit" });
|
|
9622
9920
|
if (r.status !== 0) return r.status ?? 1;
|
|
9623
|
-
out(`Cloned ${name} into ${
|
|
9921
|
+
out(`Cloned ${name} into ${path23.resolve(cwd, dir2)}
|
|
9624
9922
|
`);
|
|
9625
9923
|
return 0;
|
|
9626
9924
|
}
|
|
9627
9925
|
async function cmdVerify(args, cwd, out, err) {
|
|
9628
|
-
const root = toplevel(
|
|
9926
|
+
const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
|
|
9629
9927
|
const rs = remotes(root);
|
|
9630
9928
|
const originUrl = rs.get("origin") ?? "";
|
|
9631
9929
|
if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
|
|
@@ -9788,22 +10086,22 @@ __export(runner_exports, {
|
|
|
9788
10086
|
setupText: () => setupText
|
|
9789
10087
|
});
|
|
9790
10088
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9791
|
-
import
|
|
10089
|
+
import fs29 from "node:fs";
|
|
9792
10090
|
import os5 from "node:os";
|
|
9793
|
-
import
|
|
10091
|
+
import path24 from "node:path";
|
|
9794
10092
|
function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
|
|
9795
|
-
return
|
|
10093
|
+
return path24.join(home, ".bunshin", "runner");
|
|
9796
10094
|
}
|
|
9797
10095
|
function readInstalled(home) {
|
|
9798
10096
|
try {
|
|
9799
|
-
return JSON.parse(
|
|
10097
|
+
return JSON.parse(fs29.readFileSync(path24.join(runnerHome(home), "config.json"), "utf8"));
|
|
9800
10098
|
} catch {
|
|
9801
10099
|
return null;
|
|
9802
10100
|
}
|
|
9803
10101
|
}
|
|
9804
10102
|
function connection(args, cwd) {
|
|
9805
10103
|
const saved = readInstalled();
|
|
9806
|
-
const store = saved &&
|
|
10104
|
+
const store = saved && path24.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
|
|
9807
10105
|
const config = store?.config();
|
|
9808
10106
|
const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
|
|
9809
10107
|
const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
|
|
@@ -9863,9 +10161,9 @@ async function runOnce(c, args, out, err) {
|
|
|
9863
10161
|
const [job] = await lease(c, args);
|
|
9864
10162
|
if (!job) break;
|
|
9865
10163
|
const started = Date.now();
|
|
9866
|
-
const scratch =
|
|
10164
|
+
const scratch = fs29.mkdtempSync(path24.join(os5.tmpdir(), "bunshin-job-"));
|
|
9867
10165
|
const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
|
|
9868
|
-
() =>
|
|
10166
|
+
() => fs29.rmSync(scratch, { recursive: true, force: true })
|
|
9869
10167
|
);
|
|
9870
10168
|
const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
|
|
9871
10169
|
if (!res.ok) {
|
|
@@ -9955,24 +10253,24 @@ function renderCheck(id, c) {
|
|
|
9955
10253
|
}
|
|
9956
10254
|
function logAnswer(entry) {
|
|
9957
10255
|
try {
|
|
9958
|
-
|
|
9959
|
-
|
|
10256
|
+
fs29.mkdirSync(runnerHome(), { recursive: true });
|
|
10257
|
+
fs29.appendFileSync(path24.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
9960
10258
|
`);
|
|
9961
10259
|
} catch {
|
|
9962
10260
|
}
|
|
9963
10261
|
}
|
|
9964
10262
|
function which(name) {
|
|
9965
|
-
for (const d of [...(process.env.PATH || "").split(
|
|
9966
|
-
if (d &&
|
|
10263
|
+
for (const d of [...(process.env.PATH || "").split(path24.delimiter), path24.join(os5.homedir(), ".local", "bin")]) {
|
|
10264
|
+
if (d && fs29.existsSync(path24.join(d, name))) return path24.join(d, name);
|
|
9967
10265
|
}
|
|
9968
10266
|
return null;
|
|
9969
10267
|
}
|
|
9970
10268
|
function scheduledCommand(backend) {
|
|
9971
|
-
const node =
|
|
9972
|
-
const npx =
|
|
10269
|
+
const node = path24.dirname(process.execPath);
|
|
10270
|
+
const npx = fs29.existsSync(path24.join(node, "npx")) ? path24.join(node, "npx") : which("npx") ?? "npx";
|
|
9973
10271
|
const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
|
|
9974
|
-
const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [
|
|
9975
|
-
return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(
|
|
10272
|
+
const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path24.dirname(bin)] : [], path24.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
|
|
10273
|
+
return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path24.delimiter) };
|
|
9976
10274
|
}
|
|
9977
10275
|
function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
9978
10276
|
const cmd = scheduledCommand(backend);
|
|
@@ -9987,8 +10285,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
|
9987
10285
|
<dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
|
|
9988
10286
|
<key>WorkingDirectory</key><string>${xml(home)}</string>
|
|
9989
10287
|
<key>StartInterval</key><integer>${everyMinutes * 60}</integer>
|
|
9990
|
-
<key>StandardOutPath</key><string>${xml(
|
|
9991
|
-
<key>StandardErrorPath</key><string>${xml(
|
|
10288
|
+
<key>StandardOutPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
|
|
10289
|
+
<key>StandardErrorPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
|
|
9992
10290
|
<key>ProcessType</key><string>Background</string>
|
|
9993
10291
|
<key>LowPriorityIO</key><true/>
|
|
9994
10292
|
</dict>
|
|
@@ -9999,19 +10297,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
|
|
|
9999
10297
|
const cmd = scheduledCommand(backend);
|
|
10000
10298
|
const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
|
|
10001
10299
|
const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
|
|
10002
|
-
return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(
|
|
10300
|
+
return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path24.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
|
|
10003
10301
|
}
|
|
10004
10302
|
function launchAgentsDir(opts) {
|
|
10005
|
-
return opts.launchAgents ??
|
|
10303
|
+
return opts.launchAgents ?? path24.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
|
|
10006
10304
|
}
|
|
10007
10305
|
function installSchedule(backend, everyMinutes, opts = {}) {
|
|
10008
10306
|
const platform = opts.platform ?? process.platform;
|
|
10009
10307
|
const run2 = opts.run ?? realRun;
|
|
10010
10308
|
if (platform === "darwin") {
|
|
10011
|
-
const
|
|
10012
|
-
const plist =
|
|
10013
|
-
|
|
10014
|
-
|
|
10309
|
+
const dir2 = launchAgentsDir(opts);
|
|
10310
|
+
const plist = path24.join(dir2, `${LAUNCHD_LABEL}.plist`);
|
|
10311
|
+
fs29.mkdirSync(dir2, { recursive: true });
|
|
10312
|
+
fs29.writeFileSync(plist, launchdPlist(backend, everyMinutes));
|
|
10015
10313
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
10016
10314
|
run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
|
|
10017
10315
|
const r = run2("launchctl", ["bootstrap", domain, plist]);
|
|
@@ -10031,10 +10329,10 @@ function removeSchedule(opts = {}) {
|
|
|
10031
10329
|
const platform = opts.platform ?? process.platform;
|
|
10032
10330
|
const run2 = opts.run ?? realRun;
|
|
10033
10331
|
if (platform === "darwin") {
|
|
10034
|
-
const plist =
|
|
10332
|
+
const plist = path24.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
|
|
10035
10333
|
run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
|
|
10036
|
-
if (!
|
|
10037
|
-
|
|
10334
|
+
if (!fs29.existsSync(plist)) return "no schedule";
|
|
10335
|
+
fs29.rmSync(plist, { force: true });
|
|
10038
10336
|
return `removed ${plist}`;
|
|
10039
10337
|
}
|
|
10040
10338
|
const current = run2("crontab", ["-l"]);
|
|
@@ -10061,10 +10359,10 @@ async function install(args, cwd, out, err, opts = {}) {
|
|
|
10061
10359
|
}
|
|
10062
10360
|
const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
|
|
10063
10361
|
const home = runnerHome();
|
|
10064
|
-
|
|
10362
|
+
fs29.mkdirSync(home, { recursive: true, mode: 448 });
|
|
10065
10363
|
const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
|
|
10066
|
-
|
|
10067
|
-
|
|
10364
|
+
fs29.writeFileSync(path24.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
|
|
10365
|
+
fs29.chmodSync(path24.join(home, "config.json"), 384);
|
|
10068
10366
|
let schedule;
|
|
10069
10367
|
try {
|
|
10070
10368
|
schedule = installSchedule(backend, every, opts);
|
|
@@ -10077,7 +10375,7 @@ Run it yourself: npx -y ${PKG2} runner --once
|
|
|
10077
10375
|
out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
|
|
10078
10376
|
`);
|
|
10079
10377
|
out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
|
|
10080
|
-
` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${
|
|
10378
|
+
` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path24.join(home, "runner.log")}
|
|
10081
10379
|
`);
|
|
10082
10380
|
out(` Stop: npx -y ${PKG2} runner uninstall
|
|
10083
10381
|
`);
|
|
@@ -10094,9 +10392,9 @@ Answering what waits now\u2026
|
|
|
10094
10392
|
function uninstall(out, opts = {}) {
|
|
10095
10393
|
out(`${removeSchedule(opts)}
|
|
10096
10394
|
`);
|
|
10097
|
-
const file =
|
|
10098
|
-
if (
|
|
10099
|
-
|
|
10395
|
+
const file = path24.join(runnerHome(), "config.json");
|
|
10396
|
+
if (fs29.existsSync(file)) {
|
|
10397
|
+
fs29.rmSync(file, { force: true });
|
|
10100
10398
|
out(`removed ${file} (revoke the key in the console: Agent keys)
|
|
10101
10399
|
`);
|
|
10102
10400
|
}
|
|
@@ -10276,7 +10574,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10276
10574
|
out(text2);
|
|
10277
10575
|
return 0;
|
|
10278
10576
|
}
|
|
10279
|
-
|
|
10577
|
+
fs29.writeFileSync(file, text2, { mode: 384 });
|
|
10280
10578
|
const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
|
|
10281
10579
|
out(
|
|
10282
10580
|
[
|
|
@@ -10294,7 +10592,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10294
10592
|
const id = args._[2];
|
|
10295
10593
|
const file = args._[3] ?? "-";
|
|
10296
10594
|
if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
|
|
10297
|
-
const text2 = file === "-" ?
|
|
10595
|
+
const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
|
|
10298
10596
|
const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
|
|
10299
10597
|
out(renderCheck(id, r.body));
|
|
10300
10598
|
return r.body.ok ? 0 : 1;
|
|
@@ -10303,7 +10601,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10303
10601
|
const id = args._[2];
|
|
10304
10602
|
const file = args._[3] ?? "-";
|
|
10305
10603
|
if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
|
|
10306
|
-
const text2 = file === "-" ?
|
|
10604
|
+
const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
|
|
10307
10605
|
const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
|
|
10308
10606
|
if (check?.body && (check.status < 400 || check.body.problems)) {
|
|
10309
10607
|
out(renderCheck(id, check.body));
|
|
@@ -10327,7 +10625,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10327
10625
|
const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
|
|
10328
10626
|
const entries = (() => {
|
|
10329
10627
|
try {
|
|
10330
|
-
return
|
|
10628
|
+
return fs29.readFileSync(path24.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
|
|
10331
10629
|
} catch {
|
|
10332
10630
|
return [];
|
|
10333
10631
|
}
|
|
@@ -10397,8 +10695,8 @@ var init_runner = __esm({
|
|
|
10397
10695
|
|
|
10398
10696
|
// src/v2/importer.ts
|
|
10399
10697
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
10400
|
-
import
|
|
10401
|
-
import
|
|
10698
|
+
import fs30 from "node:fs";
|
|
10699
|
+
import path25 from "node:path";
|
|
10402
10700
|
function rewriteRepoPaths(text2, repoName2, pack) {
|
|
10403
10701
|
const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
|
|
10404
10702
|
return text2.replace(re, `~/.bunshin/library/${pack}`);
|
|
@@ -10433,17 +10731,17 @@ function buildDocs(files, opts) {
|
|
|
10433
10731
|
const claimed = /* @__PURE__ */ new Set();
|
|
10434
10732
|
const all = [...files.keys()].sort();
|
|
10435
10733
|
const skillDirs = all.map(skillDirOf).filter((d) => !!d);
|
|
10436
|
-
for (const
|
|
10437
|
-
const main2 = `${
|
|
10734
|
+
for (const dir2 of skillDirs) {
|
|
10735
|
+
const main2 = `${dir2}/SKILL.md`;
|
|
10438
10736
|
const text2 = rw(files.get(main2).content);
|
|
10439
10737
|
const fm = frontmatterDescription(text2);
|
|
10440
|
-
const name =
|
|
10738
|
+
const name = path25.posix.basename(dir2);
|
|
10441
10739
|
const bundle = {};
|
|
10442
|
-
for (const p of all.filter((x) => x.startsWith(`${
|
|
10740
|
+
for (const p of all.filter((x) => x.startsWith(`${dir2}/`))) {
|
|
10443
10741
|
claimed.add(p);
|
|
10444
10742
|
if (p === main2) continue;
|
|
10445
10743
|
const f = files.get(p);
|
|
10446
|
-
bundle[p.slice(
|
|
10744
|
+
bundle[p.slice(dir2.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
|
|
10447
10745
|
}
|
|
10448
10746
|
const description = fm.description.replace(/\s+/g, " ").trim() || firstSentence(text2) || name;
|
|
10449
10747
|
const { name: _n, description: _d, ...rest } = fm.data;
|
|
@@ -10470,9 +10768,9 @@ function buildDocs(files, opts) {
|
|
|
10470
10768
|
if (/\.md$/i.test(p) && looksGenerated(files.get(p).content)) claimed.add(p);
|
|
10471
10769
|
}
|
|
10472
10770
|
const dirs = new Set(all.filter((x) => x.includes("/") && !claimed.has(x)).map((x) => x.split("/")[0]));
|
|
10473
|
-
for (const
|
|
10474
|
-
if (skillParents.has(
|
|
10475
|
-
const members = all.filter((x) => x.startsWith(`${
|
|
10771
|
+
for (const dir2 of [...dirs].sort()) {
|
|
10772
|
+
if (skillParents.has(dir2) && all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x)).length === 0) continue;
|
|
10773
|
+
const members = all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x));
|
|
10476
10774
|
if (!members.length) continue;
|
|
10477
10775
|
const readme = members.find((x) => /^[^/]+\/README(\.md)?$/i.test(x));
|
|
10478
10776
|
const bundle = {};
|
|
@@ -10483,15 +10781,15 @@ function buildDocs(files, opts) {
|
|
|
10483
10781
|
bundle[p] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
|
|
10484
10782
|
}
|
|
10485
10783
|
const tool = members.some((p) => files.get(p).mode === "x" || SCRIPT.test(p));
|
|
10486
|
-
const body = readme ? rw(files.get(readme).content) : `# ${
|
|
10784
|
+
const body = readme ? rw(files.get(readme).content) : `# ${dir2}
|
|
10487
10785
|
|
|
10488
10786
|
${members.map((p) => `- \`${p}\``).join("\n")}
|
|
10489
10787
|
`;
|
|
10490
10788
|
docs.push({
|
|
10491
|
-
path: readme ?? `${
|
|
10789
|
+
path: readme ?? `${dir2}/#index`,
|
|
10492
10790
|
kind: tool ? "tool" : "reference",
|
|
10493
|
-
title:
|
|
10494
|
-
when: readme ? firstSentence(body) || `The ${
|
|
10791
|
+
title: dir2,
|
|
10792
|
+
when: readme ? firstSentence(body) || `The ${dir2} ${tool ? "tool" : "folder"}` : `The ${dir2} ${tool ? "tool" : "folder"} of ${opts.pack}`,
|
|
10495
10793
|
body,
|
|
10496
10794
|
files: bundle
|
|
10497
10795
|
});
|
|
@@ -10534,23 +10832,23 @@ function isGitRepo(root) {
|
|
|
10534
10832
|
}
|
|
10535
10833
|
function workingTreeFiles(root) {
|
|
10536
10834
|
const out = /* @__PURE__ */ new Map();
|
|
10537
|
-
const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(
|
|
10538
|
-
return
|
|
10835
|
+
const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir2, base = "") {
|
|
10836
|
+
return fs30.readdirSync(dir2, { withFileTypes: true }).flatMap((e) => {
|
|
10539
10837
|
if (e.name === ".git" || e.name === "node_modules") return [];
|
|
10540
10838
|
const rel = base ? `${base}/${e.name}` : e.name;
|
|
10541
|
-
return e.isDirectory() ? walk(
|
|
10839
|
+
return e.isDirectory() ? walk(path25.join(dir2, e.name), rel) : e.isFile() ? [rel] : [];
|
|
10542
10840
|
});
|
|
10543
10841
|
})(root);
|
|
10544
10842
|
for (const rel of list2) {
|
|
10545
|
-
const abs =
|
|
10843
|
+
const abs = path25.join(root, rel);
|
|
10546
10844
|
let buf;
|
|
10547
10845
|
try {
|
|
10548
|
-
buf =
|
|
10846
|
+
buf = fs30.readFileSync(abs);
|
|
10549
10847
|
} catch {
|
|
10550
10848
|
continue;
|
|
10551
10849
|
}
|
|
10552
10850
|
if (!isText(buf)) continue;
|
|
10553
|
-
const exec = (
|
|
10851
|
+
const exec = (fs30.statSync(abs).mode & 73) !== 0;
|
|
10554
10852
|
out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
|
|
10555
10853
|
}
|
|
10556
10854
|
return out;
|
|
@@ -10584,7 +10882,7 @@ function diffDocs(prev, next) {
|
|
|
10584
10882
|
return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
|
|
10585
10883
|
}
|
|
10586
10884
|
function readSkillSystem(root, opts) {
|
|
10587
|
-
const repoName2 =
|
|
10885
|
+
const repoName2 = path25.basename(path25.resolve(root));
|
|
10588
10886
|
const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
|
|
10589
10887
|
const tree = build(workingTreeFiles(root));
|
|
10590
10888
|
if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
|
|
@@ -10661,7 +10959,7 @@ var init_importer = __esm({
|
|
|
10661
10959
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10662
10960
|
looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
|
|
10663
10961
|
isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
|
|
10664
|
-
isUpdateHook = (p, text2) => /auto-?update/i.test(
|
|
10962
|
+
isUpdateHook = (p, text2) => /auto-?update/i.test(path25.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
|
|
10665
10963
|
fingerprint = (d) => JSON.stringify(d);
|
|
10666
10964
|
REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
|
|
10667
10965
|
|
|
@@ -10697,8 +10995,8 @@ __export(skills_exports, {
|
|
|
10697
10995
|
cmdSkill: () => cmdSkill,
|
|
10698
10996
|
parsePin: () => parsePin
|
|
10699
10997
|
});
|
|
10700
|
-
import
|
|
10701
|
-
import
|
|
10998
|
+
import fs31 from "node:fs";
|
|
10999
|
+
import path26 from "node:path";
|
|
10702
11000
|
function requireOnline(store, what) {
|
|
10703
11001
|
if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
|
|
10704
11002
|
}
|
|
@@ -10707,9 +11005,9 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10707
11005
|
err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
|
|
10708
11006
|
return 1;
|
|
10709
11007
|
}
|
|
10710
|
-
const root =
|
|
10711
|
-
if (!
|
|
10712
|
-
const pack = str6(args.flags.pack) ??
|
|
11008
|
+
const root = path26.resolve(cwd, args._[2]);
|
|
11009
|
+
if (!fs31.existsSync(root)) throw new Error(`${root} does not exist`);
|
|
11010
|
+
const pack = str6(args.flags.pack) ?? path26.basename(root);
|
|
10713
11011
|
const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
|
|
10714
11012
|
const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
|
|
10715
11013
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10744,7 +11042,7 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10744
11042
|
const propose = args.flags["propose-rewrites"];
|
|
10745
11043
|
if (propose) {
|
|
10746
11044
|
const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
|
|
10747
|
-
await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack,
|
|
11045
|
+
await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path26.basename(root), out, err);
|
|
10748
11046
|
}
|
|
10749
11047
|
out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
|
|
10750
11048
|
return 0;
|
|
@@ -10884,7 +11182,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
|
|
|
10884
11182
|
}
|
|
10885
11183
|
const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
|
|
10886
11184
|
if (graph) {
|
|
10887
|
-
|
|
11185
|
+
fs31.writeFileSync(path26.resolve(cwd, graph), renderSkillGraph(report));
|
|
10888
11186
|
out(`graph written to ${graph}
|
|
10889
11187
|
`);
|
|
10890
11188
|
}
|
|
@@ -10974,9 +11272,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10974
11272
|
`;
|
|
10975
11273
|
const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
|
|
10976
11274
|
if (file) {
|
|
10977
|
-
const abs =
|
|
10978
|
-
|
|
10979
|
-
|
|
11275
|
+
const abs = path26.resolve(cwd, file);
|
|
11276
|
+
fs31.mkdirSync(path26.dirname(abs), { recursive: true });
|
|
11277
|
+
fs31.writeFileSync(abs, line);
|
|
10980
11278
|
out(`${file}: ${line}`);
|
|
10981
11279
|
} else out(line);
|
|
10982
11280
|
return 0;
|
|
@@ -10986,16 +11284,16 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10986
11284
|
const to = str6(args.flags.to);
|
|
10987
11285
|
if (!to) throw new Error("--to <dir> is required");
|
|
10988
11286
|
const pinFile = str6(args.flags.pin);
|
|
10989
|
-
const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(
|
|
11287
|
+
const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs31.readFileSync(path26.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
|
|
10990
11288
|
const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
|
|
10991
11289
|
if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
|
|
10992
11290
|
const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
|
|
10993
|
-
const
|
|
10994
|
-
if (
|
|
11291
|
+
const dir2 = path26.resolve(cwd, to);
|
|
11292
|
+
if (fs31.existsSync(dir2) && fs31.readdirSync(dir2).length && !bool4(args.flags.force)) throw new Error(`${dir2} is not empty (use --force)`);
|
|
10995
11293
|
const pack = str6(args.flags.pack);
|
|
10996
11294
|
const docs = snap.sections.filter((s) => !pack || s.pack === pack);
|
|
10997
|
-
const res = materialize(docs,
|
|
10998
|
-
out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${
|
|
11295
|
+
const res = materialize(docs, dir2);
|
|
11296
|
+
out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir2}
|
|
10999
11297
|
`);
|
|
11000
11298
|
return 0;
|
|
11001
11299
|
}
|
|
@@ -11030,27 +11328,27 @@ __export(reflect_exports, {
|
|
|
11030
11328
|
buildLesson: () => buildLesson,
|
|
11031
11329
|
runReflection: () => runReflection
|
|
11032
11330
|
});
|
|
11033
|
-
import
|
|
11034
|
-
import
|
|
11035
|
-
import { createHash as
|
|
11331
|
+
import fs32 from "node:fs";
|
|
11332
|
+
import path27 from "node:path";
|
|
11333
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
11036
11334
|
async function runReflection(store, opts) {
|
|
11037
11335
|
const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
|
|
11038
|
-
const lock =
|
|
11336
|
+
const lock = path27.join(store.root, `reflect-${createHash10("sha256").update(opts.sessionId).digest("hex")}.lock`);
|
|
11039
11337
|
const deadline = Date.now() + 66e4;
|
|
11040
11338
|
for (; ; ) {
|
|
11041
11339
|
try {
|
|
11042
|
-
|
|
11340
|
+
fs32.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
11043
11341
|
break;
|
|
11044
11342
|
} catch (error) {
|
|
11045
11343
|
if (error.code !== "EEXIST") throw error;
|
|
11046
11344
|
try {
|
|
11047
|
-
const owner = Number(
|
|
11345
|
+
const owner = Number(fs32.readFileSync(lock, "utf8"));
|
|
11048
11346
|
if (Number.isInteger(owner) && owner > 0) {
|
|
11049
11347
|
try {
|
|
11050
11348
|
process.kill(owner, 0);
|
|
11051
11349
|
} catch (error2) {
|
|
11052
11350
|
if (error2.code === "ESRCH") {
|
|
11053
|
-
|
|
11351
|
+
fs32.unlinkSync(lock);
|
|
11054
11352
|
continue;
|
|
11055
11353
|
}
|
|
11056
11354
|
}
|
|
@@ -11065,7 +11363,7 @@ async function runReflection(store, opts) {
|
|
|
11065
11363
|
return await reflectSession(store, opts);
|
|
11066
11364
|
} finally {
|
|
11067
11365
|
try {
|
|
11068
|
-
|
|
11366
|
+
fs32.unlinkSync(lock);
|
|
11069
11367
|
} catch {
|
|
11070
11368
|
}
|
|
11071
11369
|
}
|
|
@@ -11407,8 +11705,8 @@ __export(cli_exports, {
|
|
|
11407
11705
|
parseArgs: () => parseArgs,
|
|
11408
11706
|
version: () => version
|
|
11409
11707
|
});
|
|
11410
|
-
import
|
|
11411
|
-
import
|
|
11708
|
+
import fs33 from "node:fs";
|
|
11709
|
+
import path28 from "node:path";
|
|
11412
11710
|
function parseArgs(argv) {
|
|
11413
11711
|
const out = { _: [], flags: {} };
|
|
11414
11712
|
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", "allow-codex-shell", "retrace", "across"]);
|
|
@@ -11508,7 +11806,7 @@ async function main(argv, opts = {}) {
|
|
|
11508
11806
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
11509
11807
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
11510
11808
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
11511
|
-
const prior = new Store(
|
|
11809
|
+
const prior = new Store(path28.join(cwd, ".bunshin"));
|
|
11512
11810
|
const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
|
|
11513
11811
|
const apiUrl = str7(args.flags["api-url"]);
|
|
11514
11812
|
const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
@@ -11537,7 +11835,7 @@ async function main(argv, opts = {}) {
|
|
|
11537
11835
|
});
|
|
11538
11836
|
if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
|
|
11539
11837
|
const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
11540
|
-
await askSources2(new Store(
|
|
11838
|
+
await askSources2(new Store(path28.join(cwd, ".bunshin")), out);
|
|
11541
11839
|
}
|
|
11542
11840
|
out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
|
|
11543
11841
|
`);
|
|
@@ -11574,7 +11872,7 @@ Key saved privately outside the project.
|
|
|
11574
11872
|
if (cmd === "mcp") {
|
|
11575
11873
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
11576
11874
|
const root = str7(args.flags.root);
|
|
11577
|
-
await runMcpServer2({ cwd, ...root ? { store: new Store(
|
|
11875
|
+
await runMcpServer2({ cwd, ...root ? { store: new Store(path28.resolve(root)) } : {}, version: version() });
|
|
11578
11876
|
return 0;
|
|
11579
11877
|
}
|
|
11580
11878
|
if (cmd === "demo") {
|
|
@@ -11637,6 +11935,14 @@ Key saved privately outside the project.
|
|
|
11637
11935
|
case "meta-reflect":
|
|
11638
11936
|
case "meta":
|
|
11639
11937
|
return await cmdMetaReflect(store, args, cwd, out);
|
|
11938
|
+
case "route": {
|
|
11939
|
+
const { routeJob: routeJob2, sweepRouter: sweepRouter2 } = await Promise.resolve().then(() => (init_route(), route_exports));
|
|
11940
|
+
const answer = await routeJob2(store, str7(args.flags.session) ?? "", Number(args.flags.seq));
|
|
11941
|
+
sweepRouter2(new (await Promise.resolve().then(() => (init_store2(), store_exports))).V2Store(store));
|
|
11942
|
+
if (!bool5(args.flags.quiet)) out(`${JSON.stringify(answer)}
|
|
11943
|
+
`);
|
|
11944
|
+
return answer && !answer.error ? 0 : 1;
|
|
11945
|
+
}
|
|
11640
11946
|
case "update-check": {
|
|
11641
11947
|
const latest = await (await Promise.resolve().then(() => (init_update(), update_exports))).checkForUpdate();
|
|
11642
11948
|
if (!bool5(args.flags.quiet)) out(latest ? `latest ${latest} (this is ${version()})
|
|
@@ -11678,7 +11984,7 @@ ${HELP2}`);
|
|
|
11678
11984
|
function openStore(args, cwd) {
|
|
11679
11985
|
const root = str7(args.flags.root);
|
|
11680
11986
|
if (root) {
|
|
11681
|
-
const store = new Store(
|
|
11987
|
+
const store = new Store(path28.resolve(root));
|
|
11682
11988
|
store.ensureLayout();
|
|
11683
11989
|
return store;
|
|
11684
11990
|
}
|
|
@@ -11694,7 +12000,7 @@ async function cmdInit(args, cwd, out, err) {
|
|
|
11694
12000
|
orgId: str7(args.flags["org-id"]),
|
|
11695
12001
|
userId: str7(args.flags["user-id"]),
|
|
11696
12002
|
apiUrl: str7(args.flags["api-url"]),
|
|
11697
|
-
apiKey: bool5(args.flags["key-stdin"]) ?
|
|
12003
|
+
apiKey: bool5(args.flags["key-stdin"]) ? fs33.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
|
|
11698
12004
|
scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
|
|
11699
12005
|
harness,
|
|
11700
12006
|
noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
|
|
@@ -11737,10 +12043,10 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11737
12043
|
const conflicts = store.listConflicts().filter((c) => c.status === "open");
|
|
11738
12044
|
const metrics = store.readMetrics(void 0);
|
|
11739
12045
|
const last50 = metrics?.windows?.find((w) => w.name === "last50");
|
|
11740
|
-
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([
|
|
12046
|
+
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path28.join(process.env.CODEX_HOME ?? path28.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path28.dirname(store.root)).file])] : config.harness === "claude-code" ? [path28.join(homeDir(), ".claude", "settings.json"), path28.join(path28.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path28.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path28.join(path28.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
|
|
11741
12047
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
11742
12048
|
try {
|
|
11743
|
-
return
|
|
12049
|
+
return fs33.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
11744
12050
|
} catch {
|
|
11745
12051
|
return false;
|
|
11746
12052
|
}
|
|
@@ -11758,7 +12064,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11758
12064
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
11759
12065
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
11760
12066
|
hooks: { [config.harness]: hooks },
|
|
11761
|
-
hookFiles: hookFiles.filter((file) =>
|
|
12067
|
+
hookFiles: hookFiles.filter((file) => fs33.existsSync(file)),
|
|
11762
12068
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
|
|
11763
12069
|
v2
|
|
11764
12070
|
};
|
|
@@ -11801,7 +12107,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11801
12107
|
}
|
|
11802
12108
|
function hookCount(file) {
|
|
11803
12109
|
try {
|
|
11804
|
-
const raw = JSON.parse(
|
|
12110
|
+
const raw = JSON.parse(fs33.readFileSync(file, "utf8"));
|
|
11805
12111
|
let n = 0;
|
|
11806
12112
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
11807
12113
|
for (const g of groups ?? []) {
|
|
@@ -11921,7 +12227,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
11921
12227
|
let content = str7(args.flags.content) ?? "";
|
|
11922
12228
|
if (!content || content === "-" || args._.includes("-")) {
|
|
11923
12229
|
try {
|
|
11924
|
-
content =
|
|
12230
|
+
content = fs33.readFileSync(0, "utf8");
|
|
11925
12231
|
} catch {
|
|
11926
12232
|
}
|
|
11927
12233
|
}
|
|
@@ -11987,7 +12293,7 @@ async function cmdReflect(store, args, cwd, out, err) {
|
|
|
11987
12293
|
}
|
|
11988
12294
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
|
|
11989
12295
|
const result = await runReflection2(store, {
|
|
11990
|
-
sessionId: str7(args.flags.session) ??
|
|
12296
|
+
sessionId: str7(args.flags.session) ?? path28.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
11991
12297
|
transcriptPath: transcript,
|
|
11992
12298
|
cwd,
|
|
11993
12299
|
force: bool5(args.flags.force)
|
|
@@ -12190,7 +12496,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
|
|
|
12190
12496
|
Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
|
|
12191
12497
|
CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
|
|
12192
12498
|
`;
|
|
12193
|
-
untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${
|
|
12499
|
+
untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${path28.dirname(root)} to trust it.`;
|
|
12194
12500
|
}
|
|
12195
12501
|
});
|
|
12196
12502
|
|