@bunshin-cc/bunshin 0.8.1 → 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 +761 -390
- 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;
|
|
@@ -2770,8 +2817,17 @@ A lesson is read by agents on other tasks, months from now. Write the rule, not
|
|
|
2770
2817
|
|
|
2771
2818
|
Every entry is a whole document (a skill, a spec, a policy). Prefer editing an existing document
|
|
2772
2819
|
over creating one, and aim the edit: "anchor" names the heading (from its headings list) the text
|
|
2773
|
-
belongs under, so it lands in the right part of the document.
|
|
2774
|
-
|
|
2820
|
+
belongs under, so it lands in the right part of the document.
|
|
2821
|
+
|
|
2822
|
+
Steering and approach lessons (a, e) are the exception: each is its own short lesson. Use op
|
|
2823
|
+
"create" with kind "rule": a title naming the behaviour, a "when" naming the situations it applies
|
|
2824
|
+
in (as the user would phrase a request there, even one that never mentions it), and a body of at
|
|
2825
|
+
most about 120 words: the rule, why, and when it was learned. Do not append such a lesson inside a
|
|
2826
|
+
long skill or procedure: a rule buried in a long document goes unfollowed even when the document is
|
|
2827
|
+
loaded, and the user ends up repeating it. If a short rule in the list below already says it, append
|
|
2828
|
+
to that rule instead. Traps, procedures and facts (b, c, d) still go into the document they belong to.
|
|
2829
|
+
|
|
2830
|
+
Library documents relevant to this session:
|
|
2775
2831
|
${index}
|
|
2776
2832
|
${input.skills?.length ? `
|
|
2777
2833
|
Skills in the library (target any of them by id, listed here or not):
|
|
@@ -2791,9 +2847,9 @@ A fact, command or habit of this project \u2192 this project. True of every proj
|
|
|
2791
2847
|
that group. True anywhere this workspace works \u2192 org. Lessons that come up again in other scopes
|
|
2792
2848
|
are widened by the nightly generalization pass, so a narrow scope is never a loss. Name it as
|
|
2793
2849
|
"layer" in the target of a new document (an edit keeps the document's own scope).
|
|
2794
|
-
` : ""}${input.profileId ? `- How THIS person works
|
|
2795
|
-
|
|
2796
|
-
|
|
2850
|
+
` : ""}${input.profileId ? `- How THIS person works in every task alike (tone, length, how they like to be asked) \u2192
|
|
2851
|
+
op "append" to their profile ${input.profileId}, anchor "how-i-work". A preference that applies in
|
|
2852
|
+
one kind of situation (how they want PRs, reports, deploys) is a short rule lesson in their layer.
|
|
2797
2853
|
` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
|
|
2798
2854
|
- Cross-repo facts, another team's rules, non-code processes \u2192 a Bunshin section at the right layer.
|
|
2799
2855
|
- If it only holds in some contexts, use op "append" with a "condition" (e.g. "objects over 50 g").
|
|
@@ -3152,6 +3208,7 @@ var init_v2 = __esm({
|
|
|
3152
3208
|
init_python();
|
|
3153
3209
|
init_text_index();
|
|
3154
3210
|
init_engine();
|
|
3211
|
+
init_router();
|
|
3155
3212
|
init_reflect();
|
|
3156
3213
|
init_digest();
|
|
3157
3214
|
init_reflect_context();
|
|
@@ -3316,17 +3373,17 @@ function listDocs(root) {
|
|
|
3316
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();
|
|
3317
3374
|
const out = [];
|
|
3318
3375
|
let visited = 0;
|
|
3319
|
-
const walk = (
|
|
3376
|
+
const walk = (dir2, rel, depth) => {
|
|
3320
3377
|
if (depth > 8 || visited++ > 2e4) return;
|
|
3321
3378
|
let entries;
|
|
3322
3379
|
try {
|
|
3323
|
-
entries = fs6.readdirSync(
|
|
3380
|
+
entries = fs6.readdirSync(dir2, { withFileTypes: true });
|
|
3324
3381
|
} catch {
|
|
3325
3382
|
return;
|
|
3326
3383
|
}
|
|
3327
3384
|
for (const e of entries) {
|
|
3328
3385
|
if (e.isDirectory()) {
|
|
3329
|
-
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);
|
|
3330
3387
|
} else if (e.isFile()) {
|
|
3331
3388
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
3332
3389
|
if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
|
|
@@ -4065,6 +4122,14 @@ var init_src = __esm({
|
|
|
4065
4122
|
});
|
|
4066
4123
|
|
|
4067
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
|
+
});
|
|
4068
4133
|
import fs8 from "node:fs";
|
|
4069
4134
|
import path7 from "node:path";
|
|
4070
4135
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
|
|
@@ -4183,12 +4248,12 @@ var init_store2 = __esm({
|
|
|
4183
4248
|
return s;
|
|
4184
4249
|
}
|
|
4185
4250
|
listSessions() {
|
|
4186
|
-
const
|
|
4187
|
-
if (!fileExists(
|
|
4251
|
+
const dir2 = this.p("sessions");
|
|
4252
|
+
if (!fileExists(dir2)) return [];
|
|
4188
4253
|
const out = [];
|
|
4189
|
-
for (const f of fs8.readdirSync(
|
|
4254
|
+
for (const f of fs8.readdirSync(dir2)) {
|
|
4190
4255
|
if (!f.endsWith(".json") || f === "current.json") continue;
|
|
4191
|
-
const raw = readJson(path7.join(
|
|
4256
|
+
const raw = readJson(path7.join(dir2, f), null);
|
|
4192
4257
|
if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
|
|
4193
4258
|
}
|
|
4194
4259
|
return out;
|
|
@@ -4286,12 +4351,12 @@ var init_store2 = __esm({
|
|
|
4286
4351
|
}
|
|
4287
4352
|
}
|
|
4288
4353
|
listDir(name) {
|
|
4289
|
-
const
|
|
4290
|
-
if (!fileExists(
|
|
4354
|
+
const dir2 = this.p(name);
|
|
4355
|
+
if (!fileExists(dir2)) return [];
|
|
4291
4356
|
const out = [];
|
|
4292
|
-
for (const f of fs8.readdirSync(
|
|
4357
|
+
for (const f of fs8.readdirSync(dir2)) {
|
|
4293
4358
|
if (!f.endsWith(".json")) continue;
|
|
4294
|
-
const v = readJson(path7.join(
|
|
4359
|
+
const v = readJson(path7.join(dir2, f), null);
|
|
4295
4360
|
if (v) out.push(v);
|
|
4296
4361
|
}
|
|
4297
4362
|
return out;
|
|
@@ -4365,8 +4430,8 @@ function removeFile(file) {
|
|
|
4365
4430
|
} catch {
|
|
4366
4431
|
}
|
|
4367
4432
|
}
|
|
4368
|
-
function pruneDirs(
|
|
4369
|
-
let d =
|
|
4433
|
+
function pruneDirs(dir2, stop) {
|
|
4434
|
+
let d = dir2;
|
|
4370
4435
|
while (d.startsWith(stop) && d !== stop) {
|
|
4371
4436
|
try {
|
|
4372
4437
|
if (fs9.readdirSync(d).length) return;
|
|
@@ -4377,16 +4442,16 @@ function pruneDirs(dir, stop) {
|
|
|
4377
4442
|
d = path8.dirname(d);
|
|
4378
4443
|
}
|
|
4379
4444
|
}
|
|
4380
|
-
function listFiles(
|
|
4445
|
+
function listFiles(dir2, base = dir2) {
|
|
4381
4446
|
let entries;
|
|
4382
4447
|
try {
|
|
4383
|
-
entries = fs9.readdirSync(
|
|
4448
|
+
entries = fs9.readdirSync(dir2, { withFileTypes: true });
|
|
4384
4449
|
} catch {
|
|
4385
4450
|
return [];
|
|
4386
4451
|
}
|
|
4387
4452
|
const out = [];
|
|
4388
4453
|
for (const e of entries) {
|
|
4389
|
-
const abs = path8.join(
|
|
4454
|
+
const abs = path8.join(dir2, e.name);
|
|
4390
4455
|
if (e.isDirectory()) out.push(...listFiles(abs, base));
|
|
4391
4456
|
else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
|
|
4392
4457
|
}
|
|
@@ -4394,8 +4459,8 @@ function listFiles(dir, base = dir) {
|
|
|
4394
4459
|
}
|
|
4395
4460
|
function childDir(root, name) {
|
|
4396
4461
|
if (!name || !SAFE_NAME.test(name)) return null;
|
|
4397
|
-
const
|
|
4398
|
-
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;
|
|
4399
4464
|
}
|
|
4400
4465
|
function layerRank(layer, tree) {
|
|
4401
4466
|
const kind = layerKind(layer);
|
|
@@ -4424,17 +4489,17 @@ function planDeploy(bundle, p, harnesses) {
|
|
|
4424
4489
|
const contents = /* @__PURE__ */ new Map();
|
|
4425
4490
|
const dirs = [];
|
|
4426
4491
|
for (const root of roots) {
|
|
4427
|
-
const
|
|
4428
|
-
const body =
|
|
4429
|
-
if (!
|
|
4492
|
+
const dir2 = childDir(root, name);
|
|
4493
|
+
const body = dir2 && safeJoin(dir2, "SKILL.md");
|
|
4494
|
+
if (!dir2 || !body) {
|
|
4430
4495
|
warnings.push(`skipped skill ${name} in ${root}: its folder leads outside ${root}`);
|
|
4431
4496
|
continue;
|
|
4432
4497
|
}
|
|
4433
|
-
dirs.push(
|
|
4498
|
+
dirs.push(dir2);
|
|
4434
4499
|
targets.push({ abs: body, rel: "SKILL.md", role: "body" });
|
|
4435
4500
|
contents.set(body, s.body);
|
|
4436
4501
|
for (const [rel, f] of Object.entries(s.skill.files ?? {})) {
|
|
4437
|
-
const abs = safeJoin(
|
|
4502
|
+
const abs = safeJoin(dir2, rel);
|
|
4438
4503
|
if (!abs || rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
4439
4504
|
targets.push({ abs, rel, role: "file", ...f.mode ? { mode: f.mode } : {} });
|
|
4440
4505
|
contents.set(abs, f.content);
|
|
@@ -4574,10 +4639,10 @@ function captureDoc(c, doc, out) {
|
|
|
4574
4639
|
if (chosen) byRel.set(key, chosen);
|
|
4575
4640
|
}
|
|
4576
4641
|
if (doc.kind === "skill") {
|
|
4577
|
-
for (const
|
|
4578
|
-
for (const rel of listFiles(
|
|
4642
|
+
for (const dir2 of doc.dirs ?? []) {
|
|
4643
|
+
for (const rel of listFiles(dir2)) {
|
|
4579
4644
|
if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
|
|
4580
|
-
const abs = path8.join(
|
|
4645
|
+
const abs = path8.join(dir2, rel);
|
|
4581
4646
|
const content = readText(abs);
|
|
4582
4647
|
if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
|
|
4583
4648
|
byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
|
|
@@ -4621,23 +4686,23 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
4621
4686
|
continue;
|
|
4622
4687
|
}
|
|
4623
4688
|
for (const name of names) {
|
|
4624
|
-
const
|
|
4625
|
-
if (owned.has(
|
|
4626
|
-
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"));
|
|
4627
4692
|
if (skillMd === null) continue;
|
|
4628
4693
|
let born = 0;
|
|
4629
4694
|
try {
|
|
4630
|
-
const st = fs9.statSync(path8.join(
|
|
4695
|
+
const st = fs9.statSync(path8.join(dir2, "SKILL.md"));
|
|
4631
4696
|
born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
|
|
4632
4697
|
} catch {
|
|
4633
4698
|
continue;
|
|
4634
4699
|
}
|
|
4635
4700
|
if (!(born >= since - BORN_SLACK_MS) && !only) continue;
|
|
4636
4701
|
const files = {};
|
|
4637
|
-
for (const rel of listFiles(
|
|
4702
|
+
for (const rel of listFiles(dir2).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
|
|
4638
4703
|
if (rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
4639
|
-
const content = readText(path8.join(
|
|
4640
|
-
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" } : {} };
|
|
4641
4706
|
}
|
|
4642
4707
|
const hash = sha(JSON.stringify([skillMd, files]));
|
|
4643
4708
|
if (state.created[name]?.hash === hash) continue;
|
|
@@ -4758,11 +4823,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4758
4823
|
const prevByAbs = new Map((prev?.targets ?? []).map((t) => [`${t.abs}#${t.role}`, t]));
|
|
4759
4824
|
const foreign = /* @__PURE__ */ new Set();
|
|
4760
4825
|
if (d.kind === "skill" && !prev) {
|
|
4761
|
-
for (const
|
|
4762
|
-
const cur = readText(path8.join(
|
|
4826
|
+
for (const dir2 of d.dirs ?? []) {
|
|
4827
|
+
const cur = readText(path8.join(dir2, "SKILL.md"));
|
|
4763
4828
|
if (cur === null || cur === d.section.body || state.created[d.name]) continue;
|
|
4764
|
-
foreign.add(
|
|
4765
|
-
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`);
|
|
4766
4831
|
}
|
|
4767
4832
|
delete state.created[d.name];
|
|
4768
4833
|
}
|
|
@@ -4771,7 +4836,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4771
4836
|
targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
|
|
4772
4837
|
continue;
|
|
4773
4838
|
}
|
|
4774
|
-
if ([...foreign].some((
|
|
4839
|
+
if ([...foreign].some((dir2) => t.abs.startsWith(dir2 + path8.sep))) continue;
|
|
4775
4840
|
if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
|
|
4776
4841
|
report.warnings.push(`${t.abs} not written: it leads outside its folder`);
|
|
4777
4842
|
continue;
|
|
@@ -4801,20 +4866,20 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4801
4866
|
}
|
|
4802
4867
|
}
|
|
4803
4868
|
if (d.kind === "skill") {
|
|
4804
|
-
for (const
|
|
4805
|
-
if ((d.dirs ?? []).includes(
|
|
4806
|
-
removeFile(path8.join(
|
|
4807
|
-
pruneDirs(
|
|
4808
|
-
}
|
|
4809
|
-
for (const
|
|
4810
|
-
const manifest = safeJoin(
|
|
4811
|
-
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;
|
|
4812
4877
|
writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
|
|
4813
4878
|
}
|
|
4814
4879
|
report.skills++;
|
|
4815
4880
|
} else if (d.kind === "pack") report.packFiles += targets.length;
|
|
4816
4881
|
if (d.kind === "instructions") report.instructions++;
|
|
4817
|
-
const dirs = d.dirs?.filter((
|
|
4882
|
+
const dirs = d.dirs?.filter((dir2) => !foreign.has(dir2));
|
|
4818
4883
|
next[id] = { sectionId: id, kind: d.kind, name: d.name, version: d.section.version, targets, ...dirs ? { dirs } : {} };
|
|
4819
4884
|
}
|
|
4820
4885
|
for (const [id, doc] of Object.entries(state.docs)) {
|
|
@@ -4831,14 +4896,14 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4831
4896
|
report.removed++;
|
|
4832
4897
|
}
|
|
4833
4898
|
}
|
|
4834
|
-
for (const
|
|
4835
|
-
if (!removable(
|
|
4836
|
-
removeFile(path8.join(
|
|
4837
|
-
pruneDirs(
|
|
4838
|
-
if (fs9.existsSync(
|
|
4839
|
-
const left = listFiles(
|
|
4840
|
-
if (left.length) report.warnings.push(`${
|
|
4841
|
-
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));
|
|
4842
4907
|
}
|
|
4843
4908
|
}
|
|
4844
4909
|
}
|
|
@@ -4874,9 +4939,9 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4874
4939
|
const packs = new Set(plan.skills.concat([...plan.docs.values()].map((d) => d.section)).map((s) => s.pack).filter((x) => !!x));
|
|
4875
4940
|
if (harnesses.has("claude-code")) {
|
|
4876
4941
|
for (const pack of packs) {
|
|
4877
|
-
const
|
|
4878
|
-
if (!
|
|
4879
|
-
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");
|
|
4880
4945
|
try {
|
|
4881
4946
|
fs9.lstatSync(link);
|
|
4882
4947
|
if (fs9.readlinkSync(link) !== p.claudeSkills) {
|
|
@@ -4931,10 +4996,10 @@ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
|
|
|
4931
4996
|
const s = readState(paths, config.orgId);
|
|
4932
4997
|
return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
|
|
4933
4998
|
}
|
|
4934
|
-
function materialize(sections,
|
|
4999
|
+
function materialize(sections, dir2) {
|
|
4935
5000
|
let files = 0;
|
|
4936
5001
|
const put = (rel, content, mode) => {
|
|
4937
|
-
const abs = safeJoin(
|
|
5002
|
+
const abs = safeJoin(dir2, rel);
|
|
4938
5003
|
if (!abs) return;
|
|
4939
5004
|
ensureDir(path8.dirname(abs));
|
|
4940
5005
|
writeFile(abs, content, mode);
|
|
@@ -4961,20 +5026,20 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
4961
5026
|
for (const h of harnesses) {
|
|
4962
5027
|
const spec = LINK_DIRS[h];
|
|
4963
5028
|
if (!spec) continue;
|
|
4964
|
-
const
|
|
5029
|
+
const dir2 = path8.join(abs, spec.rel);
|
|
4965
5030
|
const canonical = path8.resolve(p[spec.key]);
|
|
4966
5031
|
const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
|
|
4967
5032
|
let entries = [];
|
|
4968
5033
|
try {
|
|
4969
|
-
entries = fs9.readdirSync(
|
|
5034
|
+
entries = fs9.readdirSync(dir2, { withFileTypes: true });
|
|
4970
5035
|
} catch {
|
|
4971
5036
|
}
|
|
4972
5037
|
for (const e of entries) {
|
|
4973
5038
|
if (!e.isSymbolicLink()) continue;
|
|
4974
|
-
const link = path8.join(
|
|
5039
|
+
const link = path8.join(dir2, e.name);
|
|
4975
5040
|
let target = "";
|
|
4976
5041
|
try {
|
|
4977
|
-
target = path8.resolve(
|
|
5042
|
+
target = path8.resolve(dir2, fs9.readlinkSync(link));
|
|
4978
5043
|
} catch {
|
|
4979
5044
|
continue;
|
|
4980
5045
|
}
|
|
@@ -4988,7 +5053,7 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
4988
5053
|
out.removed++;
|
|
4989
5054
|
}
|
|
4990
5055
|
for (const name of want) {
|
|
4991
|
-
const link = path8.join(
|
|
5056
|
+
const link = path8.join(dir2, name);
|
|
4992
5057
|
try {
|
|
4993
5058
|
fs9.lstatSync(link);
|
|
4994
5059
|
out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
|
|
@@ -4996,7 +5061,7 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
4996
5061
|
} catch {
|
|
4997
5062
|
}
|
|
4998
5063
|
try {
|
|
4999
|
-
ensureDir(
|
|
5064
|
+
ensureDir(dir2);
|
|
5000
5065
|
fs9.symlinkSync(path8.join(canonical, name), link, "dir");
|
|
5001
5066
|
excluded.push(path8.join(spec.rel, name));
|
|
5002
5067
|
out.linked++;
|
|
@@ -5004,7 +5069,7 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
5004
5069
|
out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
|
|
5005
5070
|
}
|
|
5006
5071
|
}
|
|
5007
|
-
pruneDirs(
|
|
5072
|
+
pruneDirs(dir2, abs);
|
|
5008
5073
|
}
|
|
5009
5074
|
out.kept = excluded.length > 0;
|
|
5010
5075
|
gitExclude(abs, excluded);
|
|
@@ -5909,19 +5974,19 @@ function renderFeed(f) {
|
|
|
5909
5974
|
return out.join("\n").trimEnd() + "\n";
|
|
5910
5975
|
}
|
|
5911
5976
|
function latestFeed(v2) {
|
|
5912
|
-
const
|
|
5977
|
+
const dir2 = v2.p("ui");
|
|
5913
5978
|
let best = null;
|
|
5914
5979
|
try {
|
|
5915
|
-
for (const f2 of fs13.readdirSync(
|
|
5980
|
+
for (const f2 of fs13.readdirSync(dir2)) {
|
|
5916
5981
|
if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
|
|
5917
|
-
const m = fs13.statSync(path9.join(
|
|
5982
|
+
const m = fs13.statSync(path9.join(dir2, f2)).mtimeMs;
|
|
5918
5983
|
if (!best || m > best.m) best = { f: f2, m };
|
|
5919
5984
|
}
|
|
5920
5985
|
} catch {
|
|
5921
5986
|
return null;
|
|
5922
5987
|
}
|
|
5923
5988
|
if (!best) return null;
|
|
5924
|
-
const f = readJson(path9.join(
|
|
5989
|
+
const f = readJson(path9.join(dir2, best.f), null);
|
|
5925
5990
|
return f && f.v === UI_FEED_VERSION ? f : null;
|
|
5926
5991
|
}
|
|
5927
5992
|
var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
|
|
@@ -6438,6 +6503,202 @@ var init_reflect3 = __esm({
|
|
|
6438
6503
|
}
|
|
6439
6504
|
});
|
|
6440
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
|
+
|
|
6441
6702
|
// src/v2/uicmd.ts
|
|
6442
6703
|
var uicmd_exports = {};
|
|
6443
6704
|
__export(uicmd_exports, {
|
|
@@ -6450,51 +6711,51 @@ __export(uicmd_exports, {
|
|
|
6450
6711
|
modSource: () => modSource
|
|
6451
6712
|
});
|
|
6452
6713
|
import { spawnSync } from "node:child_process";
|
|
6453
|
-
import
|
|
6714
|
+
import fs17 from "node:fs";
|
|
6454
6715
|
import os3 from "node:os";
|
|
6455
|
-
import
|
|
6716
|
+
import path12 from "node:path";
|
|
6456
6717
|
import { fileURLToPath } from "node:url";
|
|
6457
6718
|
function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
|
|
6458
|
-
return
|
|
6719
|
+
return path12.join(home, ".bunshin", "claude-code-ui");
|
|
6459
6720
|
}
|
|
6460
6721
|
function modSource() {
|
|
6461
|
-
const here =
|
|
6462
|
-
for (const
|
|
6463
|
-
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;
|
|
6464
6725
|
}
|
|
6465
6726
|
return null;
|
|
6466
6727
|
}
|
|
6467
6728
|
function installedModVersion(home) {
|
|
6468
6729
|
try {
|
|
6469
|
-
return JSON.parse(
|
|
6730
|
+
return JSON.parse(fs17.readFileSync(path12.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
|
|
6470
6731
|
} catch {
|
|
6471
6732
|
return null;
|
|
6472
6733
|
}
|
|
6473
6734
|
}
|
|
6474
6735
|
function copyDir(from, to) {
|
|
6475
|
-
|
|
6476
|
-
for (const e of
|
|
6736
|
+
fs17.mkdirSync(to, { recursive: true });
|
|
6737
|
+
for (const e of fs17.readdirSync(from, { withFileTypes: true })) {
|
|
6477
6738
|
if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
|
|
6478
6739
|
if (e.name === "node_modules") continue;
|
|
6479
|
-
const a =
|
|
6480
|
-
const b =
|
|
6740
|
+
const a = path12.join(from, e.name);
|
|
6741
|
+
const b = path12.join(to, e.name);
|
|
6481
6742
|
if (e.isDirectory()) copyDir(a, b);
|
|
6482
|
-
else
|
|
6743
|
+
else fs17.copyFileSync(a, b);
|
|
6483
6744
|
}
|
|
6484
6745
|
}
|
|
6485
6746
|
function installMod(version2, opts = {}) {
|
|
6486
6747
|
const src = modSource();
|
|
6487
6748
|
if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
|
|
6488
6749
|
const home = modHome(opts.home);
|
|
6489
|
-
const
|
|
6490
|
-
|
|
6491
|
-
copyDir(src,
|
|
6492
|
-
const manifest =
|
|
6493
|
-
const m = JSON.parse(
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
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"),
|
|
6498
6759
|
JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
|
|
6499
6760
|
);
|
|
6500
6761
|
const run2 = opts.run ?? spawnSync;
|
|
@@ -6508,7 +6769,7 @@ function installMod(version2, opts = {}) {
|
|
|
6508
6769
|
};
|
|
6509
6770
|
if (!step(["plugin", "marketplace", "add", home])) step(["plugin", "marketplace", "update", MOD_MARKETPLACE]);
|
|
6510
6771
|
if (!step(["plugin", "install", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`, "--scope", "user"])) step(["plugin", "update", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`]);
|
|
6511
|
-
return { dir, version: version2, steps };
|
|
6772
|
+
return { dir: dir2, version: version2, steps };
|
|
6512
6773
|
}
|
|
6513
6774
|
async function cmdUi(store, args, out, err, version2) {
|
|
6514
6775
|
const sub = args._[1];
|
|
@@ -6529,7 +6790,7 @@ async function cmdUi(store, args, out, err, version2) {
|
|
|
6529
6790
|
`);
|
|
6530
6791
|
for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
|
|
6531
6792
|
`);
|
|
6532
|
-
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");
|
|
6533
6794
|
}
|
|
6534
6795
|
return installed || !failed.length ? 0 : 1;
|
|
6535
6796
|
}
|
|
@@ -6610,19 +6871,19 @@ __export(update_exports, {
|
|
|
6610
6871
|
updateCheckDue: () => updateCheckDue,
|
|
6611
6872
|
updateNotice: () => updateNotice
|
|
6612
6873
|
});
|
|
6613
|
-
import
|
|
6614
|
-
import
|
|
6874
|
+
import fs18 from "node:fs";
|
|
6875
|
+
import path13 from "node:path";
|
|
6615
6876
|
function readState2() {
|
|
6616
6877
|
try {
|
|
6617
|
-
return JSON.parse(
|
|
6878
|
+
return JSON.parse(fs18.readFileSync(stateFile(), "utf8"));
|
|
6618
6879
|
} catch {
|
|
6619
6880
|
return {};
|
|
6620
6881
|
}
|
|
6621
6882
|
}
|
|
6622
6883
|
function writeState(s) {
|
|
6623
6884
|
try {
|
|
6624
|
-
|
|
6625
|
-
|
|
6885
|
+
fs18.mkdirSync(path13.dirname(stateFile()), { recursive: true });
|
|
6886
|
+
fs18.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
|
|
6626
6887
|
} catch {
|
|
6627
6888
|
}
|
|
6628
6889
|
}
|
|
@@ -6672,14 +6933,14 @@ var init_update = __esm({
|
|
|
6672
6933
|
PKG = "@bunshin-cc/bunshin";
|
|
6673
6934
|
LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
|
|
6674
6935
|
DAY = 864e5;
|
|
6675
|
-
stateFile = () =>
|
|
6936
|
+
stateFile = () => path13.join(homeDir(), ".bunshin", "update.json");
|
|
6676
6937
|
disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
|
|
6677
6938
|
}
|
|
6678
6939
|
});
|
|
6679
6940
|
|
|
6680
6941
|
// src/v2/hooks.ts
|
|
6681
|
-
import
|
|
6682
|
-
import
|
|
6942
|
+
import fs19 from "node:fs";
|
|
6943
|
+
import path14 from "node:path";
|
|
6683
6944
|
function inContext(s) {
|
|
6684
6945
|
return s.inContext ?? [...s.loaded, ...s.pushed];
|
|
6685
6946
|
}
|
|
@@ -6746,10 +7007,10 @@ async function keepModCurrent(ctx, config) {
|
|
|
6746
7007
|
const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
6747
7008
|
const current = version2();
|
|
6748
7009
|
if (!modSource2() || installedModVersion2() === current) return;
|
|
6749
|
-
const marker =
|
|
6750
|
-
if (
|
|
6751
|
-
|
|
6752
|
-
|
|
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());
|
|
6753
7014
|
ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
|
|
6754
7015
|
} catch {
|
|
6755
7016
|
}
|
|
@@ -6878,13 +7139,13 @@ function skillHint(sec, others) {
|
|
|
6878
7139
|
const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
|
|
6879
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}`;
|
|
6880
7141
|
}
|
|
6881
|
-
async function remoteAuto(ctx, prompt, exclude, source) {
|
|
7142
|
+
async function remoteAuto(ctx, prompt, exclude, source, context) {
|
|
6882
7143
|
const config = ctx.store.config();
|
|
6883
7144
|
if (!canCall(config) || !config.recall.useMothership) return null;
|
|
6884
7145
|
const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
|
|
6885
7146
|
if (budget < 300) return null;
|
|
6886
7147
|
try {
|
|
6887
|
-
const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
|
|
7148
|
+
const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {}, ...context ? { context } : {} }, { timeoutMs: budget });
|
|
6888
7149
|
return res.available ? res : null;
|
|
6889
7150
|
} catch {
|
|
6890
7151
|
return null;
|
|
@@ -6901,6 +7162,14 @@ function invokedSkill(prompt, sections) {
|
|
|
6901
7162
|
const name = m[1].split(":").pop().toLowerCase();
|
|
6902
7163
|
return sections.find((s) => s.skill && s.skill.name.toLowerCase() === name);
|
|
6903
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
|
+
}
|
|
6904
7173
|
async function onPrompt(ctx) {
|
|
6905
7174
|
const prompt = ctx.input.prompt ?? "";
|
|
6906
7175
|
if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
|
|
@@ -6908,6 +7177,7 @@ async function onPrompt(ctx) {
|
|
|
6908
7177
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6909
7178
|
const session = touch(v2, ctx, (s) => {
|
|
6910
7179
|
if (!s.task && prompt.trim()) s.task = redact2(prompt.trim()).slice(0, 2e3);
|
|
7180
|
+
if (s.lastPrompt && prompt.trim()) s.recentPrompts = [...s.recentPrompts ?? [], s.lastPrompt.slice(0, 600)].slice(-3);
|
|
6911
7181
|
s.lastPrompt = redact2(prompt).slice(0, 4e3);
|
|
6912
7182
|
s.lastActivityAt = now;
|
|
6913
7183
|
});
|
|
@@ -6921,7 +7191,9 @@ async function onPrompt(ctx) {
|
|
|
6921
7191
|
const config = ctx.store.config();
|
|
6922
7192
|
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
6923
7193
|
const injected = inContext(session);
|
|
7194
|
+
const job = config.recall.router === "claude-cli" && canCall(config) ? startRoute(v2, ctx, session.sessionId, now) : void 0;
|
|
6924
7195
|
const named = invokedSkill(prompt, view.sections);
|
|
7196
|
+
if (named && job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
|
|
6925
7197
|
if (named) {
|
|
6926
7198
|
const why = `you ran /${named.skill.name}`;
|
|
6927
7199
|
const deployed = deployEnabled(config, view.ctx);
|
|
@@ -6939,19 +7211,51 @@ async function onPrompt(ctx) {
|
|
|
6939
7211
|
return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
|
|
6940
7212
|
}
|
|
6941
7213
|
const res = auto(view.sections, view.reader, prompt, { exclude: injected });
|
|
6942
|
-
const
|
|
7214
|
+
const recent = session.recentPrompts ?? [];
|
|
7215
|
+
const context = {
|
|
7216
|
+
...recent.length && session.task ? { first: session.task.slice(0, 1e3) } : {},
|
|
7217
|
+
...recent.length ? { recent } : {}
|
|
7218
|
+
};
|
|
7219
|
+
const reply = recent.length ? lastReply(ctx.input.transcript_path ?? session.transcriptPath) : void 0;
|
|
7220
|
+
if (reply) context.reply = reply;
|
|
7221
|
+
const remote = await remoteAuto(ctx, prompt, injected, session.source, Object.keys(context).length ? context : void 0);
|
|
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 });
|
|
6943
7229
|
if (remote) {
|
|
6944
|
-
const
|
|
7230
|
+
const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
|
|
6945
7231
|
if (remote.section) {
|
|
6946
7232
|
res.section = local(remote.section.id) ?? remote.section;
|
|
6947
7233
|
res.anchor = void 0;
|
|
6948
|
-
res.reason =
|
|
7234
|
+
res.reason = why;
|
|
6949
7235
|
} else {
|
|
6950
7236
|
res.section = void 0;
|
|
6951
7237
|
res.hint = (remote.hint ?? []).map((h) => ({ id: h.id, title: local(h.id)?.title ?? h.title }));
|
|
6952
|
-
res.reason =
|
|
7238
|
+
res.reason = why;
|
|
6953
7239
|
}
|
|
6954
7240
|
}
|
|
7241
|
+
const rules = (remote?.rules ?? []).filter((r) => !injected.includes(r.id) && r.id !== res.section?.id).map((r) => local(r.id) ?? r);
|
|
7242
|
+
let lessons = "";
|
|
7243
|
+
if (rules.length) {
|
|
7244
|
+
v2.updateSession(session.sessionId, ctx.cwd, (s) => {
|
|
7245
|
+
for (const r of rules) {
|
|
7246
|
+
addUnique(s.served, r.id);
|
|
7247
|
+
addUnique(s.loaded, r.id);
|
|
7248
|
+
markInContext(s, r.id);
|
|
7249
|
+
}
|
|
7250
|
+
});
|
|
7251
|
+
v2.log("v2-auto-rules", { session: session.sessionId, sections: rules.map((r) => r.id), reason: res.reason });
|
|
7252
|
+
updateFeed(v2, session.sessionId, (f) => {
|
|
7253
|
+
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" });
|
|
7254
|
+
addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
|
|
7255
|
+
});
|
|
7256
|
+
lessons = rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })).join("\n\n");
|
|
7257
|
+
}
|
|
7258
|
+
const withLessons = (text2) => withLive(additional("UserPromptSubmit", [text2, lessons].filter(Boolean).join("\n\n")));
|
|
6955
7259
|
if (res.section) {
|
|
6956
7260
|
const sec = res.section;
|
|
6957
7261
|
v2.updateSession(session.sessionId, ctx.cwd, (s) => {
|
|
@@ -6965,10 +7269,10 @@ async function onPrompt(ctx) {
|
|
|
6965
7269
|
addServed(f, { id: sec.id, title: sec.title, kind: sec.kind, ...sec.skill ? { skill: sec.skill.name } : {}, how: "auto", why });
|
|
6966
7270
|
addEvent(f, "served", `served ${label({ ...sec.skill ? { skill: sec.skill.name } : {}, title: sec.title, kind: sec.kind })} \u2014 ${why}`);
|
|
6967
7271
|
});
|
|
6968
|
-
return
|
|
7272
|
+
return withLessons(renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) }));
|
|
6969
7273
|
}
|
|
6970
|
-
const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id)).slice(0, 3);
|
|
6971
|
-
if (!fresh.length) return
|
|
7274
|
+
const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id) && !rules.some((r) => r.id === h.id)).slice(0, 3);
|
|
7275
|
+
if (!fresh.length) return withLessons("");
|
|
6972
7276
|
v2.updateSession(session.sessionId, ctx.cwd, (s) => {
|
|
6973
7277
|
s.hinted = s.hinted ?? [];
|
|
6974
7278
|
addUnique(s.hinted, ...fresh.map((h) => h.id));
|
|
@@ -6982,8 +7286,8 @@ async function onPrompt(ctx) {
|
|
|
6982
7286
|
addEvent(f, "hint", `possibly relevant: ${fresh.map((h) => h.title).join(", ")}`);
|
|
6983
7287
|
});
|
|
6984
7288
|
const topSkill = view.sections.find((s) => s.id === fresh[0].id && s.skill);
|
|
6985
|
-
if (topSkill && deployEnabled(config, view.ctx)) return
|
|
6986
|
-
return
|
|
7289
|
+
if (topSkill && deployEnabled(config, view.ctx)) return withLessons(skillHint(topSkill, fresh.slice(1).map((h) => h.title)));
|
|
7290
|
+
return withLessons(hintLine(fresh.map((h) => h.title), config.harness));
|
|
6987
7291
|
}
|
|
6988
7292
|
function toolPaths(input) {
|
|
6989
7293
|
if (!input || typeof input !== "object") return [];
|
|
@@ -6994,15 +7298,69 @@ function toolPaths(input) {
|
|
|
6994
7298
|
}
|
|
6995
7299
|
return [...new Set(out)];
|
|
6996
7300
|
}
|
|
6997
|
-
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) {
|
|
6998
7351
|
const v2 = new V2Store(ctx.store);
|
|
6999
7352
|
const session = touch(v2, ctx, (s) => {
|
|
7000
7353
|
s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7001
7354
|
});
|
|
7002
7355
|
const live = liveUpdate(v2, ctx, session);
|
|
7356
|
+
let routed = "";
|
|
7357
|
+
try {
|
|
7358
|
+
routed = await routedText(v2, ctx, session);
|
|
7359
|
+
} catch {
|
|
7360
|
+
}
|
|
7003
7361
|
const withLive = (out) => {
|
|
7004
|
-
|
|
7005
|
-
return
|
|
7362
|
+
const text3 = [live, routed, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
|
|
7363
|
+
return text3 === (out.hookSpecificOutput?.additionalContext ?? "") ? out : additional("PreToolUse", text3);
|
|
7006
7364
|
};
|
|
7007
7365
|
const raw = toolPaths(ctx.input.tool_input);
|
|
7008
7366
|
if (!raw.length) return withLive({});
|
|
@@ -7011,7 +7369,7 @@ function onPreToolUse(ctx) {
|
|
|
7011
7369
|
const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
|
|
7012
7370
|
const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
|
|
7013
7371
|
if (!paths.length && !text2) return withLive({});
|
|
7014
|
-
const seen = new Set(inContext(session));
|
|
7372
|
+
const seen = new Set(inContext(v2.readSession(session.sessionId) ?? session));
|
|
7015
7373
|
const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
|
|
7016
7374
|
if (!hits.length) return withLive({});
|
|
7017
7375
|
const ids = hits.map((h) => h.section.id);
|
|
@@ -7262,8 +7620,10 @@ var init_hooks = __esm({
|
|
|
7262
7620
|
init_signals();
|
|
7263
7621
|
init_store2();
|
|
7264
7622
|
init_ui();
|
|
7623
|
+
init_route();
|
|
7265
7624
|
init_source();
|
|
7266
7625
|
init_view();
|
|
7626
|
+
init_route();
|
|
7267
7627
|
NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
|
|
7268
7628
|
NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
7269
7629
|
CHECKPOINT_MS = 30 * 6e4;
|
|
@@ -7277,8 +7637,8 @@ var init_hooks = __esm({
|
|
|
7277
7637
|
});
|
|
7278
7638
|
|
|
7279
7639
|
// src/hooks.ts
|
|
7280
|
-
import
|
|
7281
|
-
import
|
|
7640
|
+
import fs20 from "node:fs";
|
|
7641
|
+
import path15 from "node:path";
|
|
7282
7642
|
function normalizeEvent(raw) {
|
|
7283
7643
|
return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
|
|
7284
7644
|
}
|
|
@@ -7346,19 +7706,19 @@ async function handleHook(rawEvent, input, opts = {}) {
|
|
|
7346
7706
|
}
|
|
7347
7707
|
function deferToProject(cwd, env = process.env) {
|
|
7348
7708
|
if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
|
|
7349
|
-
let
|
|
7709
|
+
let dir2 = path15.resolve(cwd);
|
|
7350
7710
|
for (let i = 0; i < 40; i++) {
|
|
7351
7711
|
for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
|
|
7352
7712
|
try {
|
|
7353
|
-
const text2 =
|
|
7713
|
+
const text2 = fs20.readFileSync(path15.join(dir2, f), "utf8");
|
|
7354
7714
|
if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
|
|
7355
7715
|
} catch {
|
|
7356
7716
|
}
|
|
7357
7717
|
}
|
|
7358
|
-
if (
|
|
7359
|
-
const up =
|
|
7360
|
-
if (up ===
|
|
7361
|
-
|
|
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;
|
|
7362
7722
|
}
|
|
7363
7723
|
return false;
|
|
7364
7724
|
}
|
|
@@ -7404,11 +7764,11 @@ var init_hooks2 = __esm({
|
|
|
7404
7764
|
});
|
|
7405
7765
|
|
|
7406
7766
|
// src/project-hooks.ts
|
|
7407
|
-
import
|
|
7408
|
-
import
|
|
7767
|
+
import fs21 from "node:fs";
|
|
7768
|
+
import path16 from "node:path";
|
|
7409
7769
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
7410
7770
|
function codexProjectHooks(cwd) {
|
|
7411
|
-
const legacyFile =
|
|
7771
|
+
const legacyFile = path16.join(cwd, ".codex", "hooks.json");
|
|
7412
7772
|
const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
|
|
7413
7773
|
encoding: "utf8",
|
|
7414
7774
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -7422,14 +7782,14 @@ function codexProjectHooks(cwd) {
|
|
|
7422
7782
|
} catch {
|
|
7423
7783
|
return { file: legacyFile, shared: false, legacyFile };
|
|
7424
7784
|
}
|
|
7425
|
-
const linked =
|
|
7785
|
+
const linked = fs21.realpathSync(common) !== fs21.realpathSync(directory);
|
|
7426
7786
|
if (linked) {
|
|
7427
|
-
if (
|
|
7787
|
+
if (path16.basename(common) !== ".git" || !fs21.statSync(common).isDirectory()) {
|
|
7428
7788
|
throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
|
|
7429
7789
|
}
|
|
7430
|
-
root =
|
|
7790
|
+
root = path16.dirname(common);
|
|
7431
7791
|
}
|
|
7432
|
-
const file =
|
|
7792
|
+
const file = fs21.realpathSync(root) === fs21.realpathSync(cwd) ? legacyFile : path16.join(root, ".codex", "hooks.json");
|
|
7433
7793
|
return { file, shared: linked, legacyFile };
|
|
7434
7794
|
}
|
|
7435
7795
|
var init_project_hooks = __esm({
|
|
@@ -7441,6 +7801,7 @@ var init_project_hooks = __esm({
|
|
|
7441
7801
|
// src/init.ts
|
|
7442
7802
|
var init_exports = {};
|
|
7443
7803
|
__export(init_exports, {
|
|
7804
|
+
ALL_TOOLS_MATCHER: () => ALL_TOOLS_MATCHER,
|
|
7444
7805
|
BORROW_COMMAND: () => BORROW_COMMAND,
|
|
7445
7806
|
CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
|
|
7446
7807
|
HOOK_EVENTS: () => HOOK_EVENTS,
|
|
@@ -7462,8 +7823,8 @@ __export(init_exports, {
|
|
|
7462
7823
|
runInit: () => runInit
|
|
7463
7824
|
});
|
|
7464
7825
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7465
|
-
import
|
|
7466
|
-
import
|
|
7826
|
+
import fs22 from "node:fs";
|
|
7827
|
+
import path17 from "node:path";
|
|
7467
7828
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
7468
7829
|
function isBunshinEntry(entry) {
|
|
7469
7830
|
return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
|
|
@@ -7488,7 +7849,7 @@ function removeBunshinHooks(settings) {
|
|
|
7488
7849
|
else delete out.hooks;
|
|
7489
7850
|
return out;
|
|
7490
7851
|
}
|
|
7491
|
-
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
|
|
7852
|
+
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false, allTools = false } = {}) {
|
|
7492
7853
|
const base = removeBunshinHooks(settings);
|
|
7493
7854
|
const hooks = { ...base.hooks ?? {} };
|
|
7494
7855
|
for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
|
|
@@ -7496,7 +7857,8 @@ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code
|
|
|
7496
7857
|
const hook = bunshinHookEntry(arg, timeout, bin);
|
|
7497
7858
|
hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
|
|
7498
7859
|
if (harness === "codex") delete hook._bunshin;
|
|
7499
|
-
|
|
7860
|
+
const m = matcher && allTools && event === "PreToolUse" ? ALL_TOOLS_MATCHER : matcher;
|
|
7861
|
+
groups.push(m ? { matcher: m, hooks: [hook] } : { hooks: [hook] });
|
|
7500
7862
|
hooks[event] = groups;
|
|
7501
7863
|
}
|
|
7502
7864
|
return { ...base, hooks };
|
|
@@ -7508,16 +7870,16 @@ function countBunshinHooks(settings) {
|
|
|
7508
7870
|
);
|
|
7509
7871
|
}
|
|
7510
7872
|
function claudeSettingsPath() {
|
|
7511
|
-
return
|
|
7873
|
+
return path17.join(homeDir(), ".claude", "settings.json");
|
|
7512
7874
|
}
|
|
7513
7875
|
function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
7514
|
-
const file = scope === "global" ?
|
|
7876
|
+
const file = scope === "global" ? path17.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path17.join(cwd, ".pi/extensions/bunshin.js");
|
|
7515
7877
|
const marker = "// Managed by Bunshin setup.";
|
|
7516
7878
|
try {
|
|
7517
|
-
const previous = fileExists(file) ?
|
|
7879
|
+
const previous = fileExists(file) ? fs22.readFileSync(file, "utf8") : "";
|
|
7518
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.");
|
|
7519
7881
|
if (remove) {
|
|
7520
|
-
if (previous)
|
|
7882
|
+
if (previous) fs22.unlinkSync(file);
|
|
7521
7883
|
return { file, installed: 0, changed: !!previous };
|
|
7522
7884
|
}
|
|
7523
7885
|
const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
|
|
@@ -7526,24 +7888,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
|
7526
7888
|
export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
7527
7889
|
`;
|
|
7528
7890
|
if (text2 !== previous) {
|
|
7529
|
-
ensureDir(
|
|
7530
|
-
|
|
7891
|
+
ensureDir(path17.dirname(file));
|
|
7892
|
+
fs22.writeFileSync(file, text2);
|
|
7531
7893
|
}
|
|
7532
7894
|
return { file, installed: 1, changed: text2 !== previous };
|
|
7533
7895
|
} catch (error) {
|
|
7534
7896
|
return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
7535
7897
|
}
|
|
7536
7898
|
}
|
|
7537
|
-
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 } = {}) {
|
|
7538
7900
|
try {
|
|
7539
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7540
|
-
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 });
|
|
7541
7903
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7542
7904
|
if (changed) {
|
|
7543
|
-
ensureDir(
|
|
7905
|
+
ensureDir(path17.dirname(file));
|
|
7544
7906
|
if (fileExists(file)) {
|
|
7545
7907
|
try {
|
|
7546
|
-
|
|
7908
|
+
fs22.copyFileSync(file, `${file}.bunshin-backup`);
|
|
7547
7909
|
} catch {
|
|
7548
7910
|
}
|
|
7549
7911
|
}
|
|
@@ -7558,23 +7920,23 @@ function mcpLauncher(bin) {
|
|
|
7558
7920
|
if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
|
|
7559
7921
|
let script = process.argv[1] ?? "";
|
|
7560
7922
|
try {
|
|
7561
|
-
script =
|
|
7923
|
+
script = fs22.realpathSync(script);
|
|
7562
7924
|
} catch {
|
|
7563
7925
|
}
|
|
7564
7926
|
return { command: process.execPath, args: [script, "mcp"] };
|
|
7565
7927
|
}
|
|
7566
7928
|
function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
|
|
7567
7929
|
try {
|
|
7568
|
-
const current = fileExists(file) ?
|
|
7930
|
+
const current = fileExists(file) ? fs22.readFileSync(file, "utf8") : null;
|
|
7569
7931
|
const ours = current === null || current === content;
|
|
7570
7932
|
if (remove) {
|
|
7571
|
-
if (current !== null && ours)
|
|
7933
|
+
if (current !== null && ours) fs22.unlinkSync(file);
|
|
7572
7934
|
return { file, installed: 0, changed: current !== null && ours };
|
|
7573
7935
|
}
|
|
7574
|
-
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` };
|
|
7575
7937
|
if (current === null) {
|
|
7576
|
-
|
|
7577
|
-
|
|
7938
|
+
fs22.mkdirSync(path17.dirname(file), { recursive: true });
|
|
7939
|
+
fs22.writeFileSync(file, content);
|
|
7578
7940
|
}
|
|
7579
7941
|
return { file, installed: 1, changed: current === null };
|
|
7580
7942
|
} catch (err) {
|
|
@@ -7583,7 +7945,7 @@ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND })
|
|
|
7583
7945
|
}
|
|
7584
7946
|
function installMcpConfig({ file, launcher, remove = false }) {
|
|
7585
7947
|
try {
|
|
7586
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7948
|
+
const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
|
|
7587
7949
|
const servers = { ...before.mcpServers ?? {} };
|
|
7588
7950
|
if (remove) delete servers.bunshin;
|
|
7589
7951
|
else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
|
|
@@ -7591,7 +7953,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7591
7953
|
if (!Object.keys(servers).length) delete after.mcpServers;
|
|
7592
7954
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7593
7955
|
if (changed) {
|
|
7594
|
-
if (!Object.keys(after).length && fileExists(file))
|
|
7956
|
+
if (!Object.keys(after).length && fileExists(file)) fs22.unlinkSync(file);
|
|
7595
7957
|
else if (Object.keys(after).length) writeJsonAtomic(file, after);
|
|
7596
7958
|
}
|
|
7597
7959
|
return { file, installed: servers.bunshin ? 1 : 0, changed };
|
|
@@ -7600,7 +7962,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7600
7962
|
}
|
|
7601
7963
|
}
|
|
7602
7964
|
function installUserMcp({ launcher, remove = false, claude = "claude" }) {
|
|
7603
|
-
const file =
|
|
7965
|
+
const file = path17.join(homeDir(), ".claude.json");
|
|
7604
7966
|
const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
|
|
7605
7967
|
try {
|
|
7606
7968
|
let had = false;
|
|
@@ -7626,22 +7988,22 @@ function runInit(opts = {}) {
|
|
|
7626
7988
|
const cwd = opts.cwd ?? process.cwd();
|
|
7627
7989
|
const warnings = [];
|
|
7628
7990
|
const scope = opts.scope ?? "repo";
|
|
7629
|
-
const root = scope === "global" ?
|
|
7630
|
-
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) {
|
|
7631
7993
|
const aside = `${root}.untrusted-${Date.now()}`;
|
|
7632
|
-
|
|
7994
|
+
fs22.renameSync(root, aside);
|
|
7633
7995
|
warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
|
|
7634
7996
|
}
|
|
7635
|
-
const existed = fileExists(
|
|
7997
|
+
const existed = fileExists(path17.join(root, "config.json"));
|
|
7636
7998
|
const store = new Store(root);
|
|
7637
7999
|
store.ensureLayout();
|
|
7638
|
-
|
|
8000
|
+
fs22.chmodSync(root, 448);
|
|
7639
8001
|
const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
|
|
7640
|
-
if (scope === "repo" && (hookScope === "project" || fileExists(
|
|
7641
|
-
const ignore =
|
|
7642
|
-
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") : "";
|
|
7643
8005
|
if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
|
|
7644
|
-
|
|
8006
|
+
fs22.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
|
|
7645
8007
|
`);
|
|
7646
8008
|
}
|
|
7647
8009
|
}
|
|
@@ -7671,17 +8033,17 @@ function runInit(opts = {}) {
|
|
|
7671
8033
|
}
|
|
7672
8034
|
if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
|
|
7673
8035
|
const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
|
|
7674
|
-
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ??
|
|
7675
|
-
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" });
|
|
7676
8038
|
if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
|
|
7677
|
-
const local =
|
|
7678
|
-
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 });
|
|
7679
8041
|
}
|
|
7680
8042
|
if (projectHooks?.shared) {
|
|
7681
8043
|
hooks.sharedWithWorktrees = true;
|
|
7682
|
-
if (!hooks.error && !opts.removeHooks) ensureDir(
|
|
8044
|
+
if (!hooks.error && !opts.removeHooks) ensureDir(path17.dirname(projectHooks.legacyFile));
|
|
7683
8045
|
}
|
|
7684
|
-
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)) {
|
|
7685
8047
|
const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
|
|
7686
8048
|
if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
|
|
7687
8049
|
}
|
|
@@ -7691,15 +8053,15 @@ function runInit(opts = {}) {
|
|
|
7691
8053
|
let learnCommand;
|
|
7692
8054
|
if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
|
|
7693
8055
|
const user = hookScope === "user";
|
|
7694
|
-
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 });
|
|
7695
8057
|
if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
|
|
7696
|
-
learnCommand = installLearnCommand({ file:
|
|
8058
|
+
learnCommand = installLearnCommand({ file: path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
|
|
7697
8059
|
if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
|
|
7698
8060
|
}
|
|
7699
8061
|
let borrowCommand;
|
|
7700
8062
|
if (opts.removeHooks || !opts.noHooks) {
|
|
7701
8063
|
const user = hookScope === "user";
|
|
7702
|
-
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;
|
|
7703
8065
|
if (file) {
|
|
7704
8066
|
borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
|
|
7705
8067
|
if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
|
|
@@ -7725,7 +8087,7 @@ function otherHarnessInstructions() {
|
|
|
7725
8087
|
" Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
|
|
7726
8088
|
].join("\n");
|
|
7727
8089
|
}
|
|
7728
|
-
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;
|
|
7729
8091
|
var init_init = __esm({
|
|
7730
8092
|
"src/init.ts"() {
|
|
7731
8093
|
"use strict";
|
|
@@ -7735,6 +8097,7 @@ var init_init = __esm({
|
|
|
7735
8097
|
init_project_hooks();
|
|
7736
8098
|
init_trust();
|
|
7737
8099
|
PRE_TOOL_MATCHER = "Read|Edit|Write|MultiEdit|NotebookEdit";
|
|
8100
|
+
ALL_TOOLS_MATCHER = "*";
|
|
7738
8101
|
HOOK_EVENTS = [
|
|
7739
8102
|
{ event: "SessionStart", arg: "session-start", timeout: 10 },
|
|
7740
8103
|
{ event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
|
|
@@ -7856,9 +8219,9 @@ __export(commands_exports, {
|
|
|
7856
8219
|
setupRobot: () => setupRobot,
|
|
7857
8220
|
v2Status: () => v2Status
|
|
7858
8221
|
});
|
|
7859
|
-
import
|
|
7860
|
-
import
|
|
7861
|
-
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";
|
|
7862
8225
|
function cmdReview(store, args, cwd, out, err) {
|
|
7863
8226
|
const v2 = new V2Store(store);
|
|
7864
8227
|
const sub = args._[1] ?? "list";
|
|
@@ -8028,11 +8391,11 @@ function cmdSuggest(store, args, cwd, out, err) {
|
|
|
8028
8391
|
err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
|
|
8029
8392
|
return 1;
|
|
8030
8393
|
}
|
|
8031
|
-
const harnessDir =
|
|
8032
|
-
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);
|
|
8033
8396
|
let content;
|
|
8034
8397
|
try {
|
|
8035
|
-
content =
|
|
8398
|
+
content = fs23.readFileSync(file, "utf8");
|
|
8036
8399
|
} catch {
|
|
8037
8400
|
err(`bunshin suggest: cannot read ${file}
|
|
8038
8401
|
`);
|
|
@@ -8153,15 +8516,15 @@ async function cmdQuestions(store, args, out, err) {
|
|
|
8153
8516
|
return 0;
|
|
8154
8517
|
}
|
|
8155
8518
|
function saveCredential(storeRoot, apiUrl, apiKey, id) {
|
|
8156
|
-
const
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
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}
|
|
8160
8523
|
${apiUrl}
|
|
8161
8524
|
${id}`).digest("hex")}.json`);
|
|
8162
8525
|
const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
|
|
8163
|
-
|
|
8164
|
-
|
|
8526
|
+
fs23.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8527
|
+
fs23.renameSync(tmp, file);
|
|
8165
8528
|
return file;
|
|
8166
8529
|
}
|
|
8167
8530
|
async function setupRobot(opts) {
|
|
@@ -8169,10 +8532,10 @@ async function setupRobot(opts) {
|
|
|
8169
8532
|
const existing = Store.discover(opts.cwd);
|
|
8170
8533
|
let store;
|
|
8171
8534
|
const harness = opts.harness ?? "other";
|
|
8172
|
-
if (existing &&
|
|
8535
|
+
if (existing && path18.resolve(path18.dirname(existing.root)) === path18.resolve(opts.cwd) && harness === "other") store = existing;
|
|
8173
8536
|
else {
|
|
8174
8537
|
runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
|
|
8175
|
-
store = new Store(
|
|
8538
|
+
store = new Store(path18.join(opts.cwd, ".bunshin"));
|
|
8176
8539
|
}
|
|
8177
8540
|
let config = store.config();
|
|
8178
8541
|
const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
|
|
@@ -8326,16 +8689,16 @@ __export(setup_exports, {
|
|
|
8326
8689
|
setupProject: () => setupProject,
|
|
8327
8690
|
shellQuote: () => shellQuote
|
|
8328
8691
|
});
|
|
8329
|
-
import
|
|
8330
|
-
import
|
|
8331
|
-
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";
|
|
8332
8695
|
function executable(name) {
|
|
8333
|
-
const directories = [...(process.env.PATH ?? "").split(
|
|
8696
|
+
const directories = [...(process.env.PATH ?? "").split(path19.delimiter), path19.join(homeDir(), ".local/bin")];
|
|
8334
8697
|
for (const directory of directories) {
|
|
8335
|
-
const file =
|
|
8698
|
+
const file = path19.resolve(directory, name);
|
|
8336
8699
|
try {
|
|
8337
|
-
|
|
8338
|
-
if (
|
|
8700
|
+
fs24.accessSync(file, fs24.constants.X_OK);
|
|
8701
|
+
if (fs24.statSync(file).isFile()) return file;
|
|
8339
8702
|
} catch {
|
|
8340
8703
|
}
|
|
8341
8704
|
}
|
|
@@ -8343,8 +8706,8 @@ function executable(name) {
|
|
|
8343
8706
|
}
|
|
8344
8707
|
function hookBinary(harness) {
|
|
8345
8708
|
const agent = executable(harness === "claude-code" ? "claude" : harness);
|
|
8346
|
-
const directories = [.../* @__PURE__ */ new Set([
|
|
8347
|
-
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]))}`;
|
|
8348
8711
|
}
|
|
8349
8712
|
function apiAddress(raw) {
|
|
8350
8713
|
const url = new URL(raw);
|
|
@@ -8413,23 +8776,23 @@ async function setupProject(opts) {
|
|
|
8413
8776
|
const apiKey = opts.apiKey.trim();
|
|
8414
8777
|
if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
|
|
8415
8778
|
const profile = await connectionProfile(apiUrl, apiKey);
|
|
8416
|
-
const root =
|
|
8779
|
+
const root = path19.join(opts.cwd, ".bunshin");
|
|
8417
8780
|
const store = new Store(root);
|
|
8418
8781
|
const previous = store.config();
|
|
8419
8782
|
if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
|
|
8420
8783
|
throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
|
|
8421
8784
|
}
|
|
8422
|
-
const credentialDirectory =
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
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}
|
|
8426
8789
|
${apiUrl}
|
|
8427
8790
|
${profile.orgId}
|
|
8428
8791
|
${profile.userId}`).digest("hex");
|
|
8429
|
-
const credentialFile =
|
|
8792
|
+
const credentialFile = path19.join(credentialDirectory, `${name}.json`);
|
|
8430
8793
|
const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
|
|
8431
|
-
|
|
8432
|
-
|
|
8794
|
+
fs24.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8795
|
+
fs24.renameSync(temporary, credentialFile);
|
|
8433
8796
|
const result = runInit({
|
|
8434
8797
|
cwd: opts.cwd,
|
|
8435
8798
|
harness: opts.harness,
|
|
@@ -8781,9 +9144,9 @@ __export(sync_exports2, {
|
|
|
8781
9144
|
resolveConflict: () => resolveConflict,
|
|
8782
9145
|
runSync: () => runSync
|
|
8783
9146
|
});
|
|
8784
|
-
import { createHash as
|
|
8785
|
-
import
|
|
8786
|
-
import
|
|
9147
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
9148
|
+
import path20 from "node:path";
|
|
9149
|
+
import fs25 from "node:fs";
|
|
8787
9150
|
async function api(config, method, route, body, timeoutMs = 1e4) {
|
|
8788
9151
|
const ctrl = new AbortController();
|
|
8789
9152
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
@@ -8821,22 +9184,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
|
|
|
8821
9184
|
}
|
|
8822
9185
|
}
|
|
8823
9186
|
async function runSync(store) {
|
|
8824
|
-
const lock =
|
|
9187
|
+
const lock = path20.join(store.root, "sync.lock");
|
|
8825
9188
|
const deadline = Date.now() + 12e4;
|
|
8826
9189
|
for (; ; ) {
|
|
8827
9190
|
try {
|
|
8828
|
-
|
|
9191
|
+
fs25.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
8829
9192
|
break;
|
|
8830
9193
|
} catch (error) {
|
|
8831
9194
|
if (error.code !== "EEXIST") throw error;
|
|
8832
9195
|
try {
|
|
8833
|
-
const owner = Number(
|
|
9196
|
+
const owner = Number(fs25.readFileSync(lock, "utf8"));
|
|
8834
9197
|
if (Number.isInteger(owner) && owner > 0) {
|
|
8835
9198
|
try {
|
|
8836
9199
|
process.kill(owner, 0);
|
|
8837
9200
|
} catch (error2) {
|
|
8838
9201
|
if (error2.code === "ESRCH") {
|
|
8839
|
-
|
|
9202
|
+
fs25.unlinkSync(lock);
|
|
8840
9203
|
continue;
|
|
8841
9204
|
}
|
|
8842
9205
|
}
|
|
@@ -8855,7 +9218,7 @@ async function runSync(store) {
|
|
|
8855
9218
|
return await syncStore(store);
|
|
8856
9219
|
} finally {
|
|
8857
9220
|
try {
|
|
8858
|
-
|
|
9221
|
+
fs25.unlinkSync(lock);
|
|
8859
9222
|
} catch {
|
|
8860
9223
|
}
|
|
8861
9224
|
}
|
|
@@ -8883,9 +9246,9 @@ async function syncStore(store) {
|
|
|
8883
9246
|
result.errors.push(String(e instanceof Error ? e.message : e));
|
|
8884
9247
|
}
|
|
8885
9248
|
}
|
|
8886
|
-
const acknowledgementsFile =
|
|
9249
|
+
const acknowledgementsFile = path20.join(store.root, "synced-traces.json");
|
|
8887
9250
|
const acknowledgements = readJson(acknowledgementsFile, {});
|
|
8888
|
-
const fingerprint2 = (trace) =>
|
|
9251
|
+
const fingerprint2 = (trace) => createHash8("sha256").update(JSON.stringify(trace)).digest("hex");
|
|
8889
9252
|
const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
|
|
8890
9253
|
for (const t of traces) {
|
|
8891
9254
|
try {
|
|
@@ -9138,11 +9501,11 @@ __export(demo_exports, {
|
|
|
9138
9501
|
runDemo: () => runDemo
|
|
9139
9502
|
});
|
|
9140
9503
|
import assert from "node:assert/strict";
|
|
9141
|
-
import
|
|
9504
|
+
import fs26 from "node:fs";
|
|
9142
9505
|
import os4 from "node:os";
|
|
9143
|
-
import
|
|
9506
|
+
import path21 from "node:path";
|
|
9144
9507
|
async function runDemo(write) {
|
|
9145
|
-
const root =
|
|
9508
|
+
const root = fs26.mkdtempSync(path21.join(os4.tmpdir(), "bunshin-demo-"));
|
|
9146
9509
|
try {
|
|
9147
9510
|
const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
|
|
9148
9511
|
const { lessons, constraint, workflow } = aprScenario();
|
|
@@ -9159,7 +9522,7 @@ Task: ${APR_TASK}
|
|
|
9159
9522
|
write(result.text);
|
|
9160
9523
|
write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
|
|
9161
9524
|
} finally {
|
|
9162
|
-
|
|
9525
|
+
fs26.rmSync(root, { recursive: true, force: true });
|
|
9163
9526
|
}
|
|
9164
9527
|
}
|
|
9165
9528
|
var init_demo = __esm({
|
|
@@ -9172,21 +9535,21 @@ var init_demo = __esm({
|
|
|
9172
9535
|
});
|
|
9173
9536
|
|
|
9174
9537
|
// src/v2/gitremote.ts
|
|
9175
|
-
import
|
|
9176
|
-
import
|
|
9177
|
-
import { createHash as
|
|
9538
|
+
import fs27 from "node:fs";
|
|
9539
|
+
import path22 from "node:path";
|
|
9540
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
9178
9541
|
function saveGitCredential(apiUrl, apiKey) {
|
|
9179
9542
|
const file = gitCredentialFile();
|
|
9180
|
-
|
|
9543
|
+
fs27.mkdirSync(path22.dirname(file), { recursive: true, mode: 448 });
|
|
9181
9544
|
let all = {};
|
|
9182
9545
|
try {
|
|
9183
|
-
all = JSON.parse(
|
|
9546
|
+
all = JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
9184
9547
|
} catch {
|
|
9185
9548
|
}
|
|
9186
9549
|
all[apiUrl.replace(/\/$/, "")] = apiKey;
|
|
9187
9550
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
9188
|
-
|
|
9189
|
-
|
|
9551
|
+
fs27.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
9552
|
+
fs27.renameSync(tmp, file);
|
|
9190
9553
|
}
|
|
9191
9554
|
function resolveGitKey(apiUrl) {
|
|
9192
9555
|
const base = apiUrl.replace(/\/$/, "");
|
|
@@ -9194,20 +9557,20 @@ function resolveGitKey(apiUrl) {
|
|
|
9194
9557
|
const origin = urlOrigin(base);
|
|
9195
9558
|
if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
|
|
9196
9559
|
try {
|
|
9197
|
-
const all = JSON.parse(
|
|
9560
|
+
const all = JSON.parse(fs27.readFileSync(gitCredentialFile(), "utf8"));
|
|
9198
9561
|
if (all[base]) return all[base];
|
|
9199
9562
|
} catch {
|
|
9200
9563
|
}
|
|
9201
|
-
const
|
|
9564
|
+
const dir2 = path22.join(homeDir(), ".config/bunshin/credentials");
|
|
9202
9565
|
let files = [];
|
|
9203
9566
|
try {
|
|
9204
|
-
files =
|
|
9567
|
+
files = fs27.readdirSync(dir2).filter((f) => f.endsWith(".json"));
|
|
9205
9568
|
} catch {
|
|
9206
9569
|
return void 0;
|
|
9207
9570
|
}
|
|
9208
9571
|
for (const f of files.sort()) {
|
|
9209
9572
|
try {
|
|
9210
|
-
const c = JSON.parse(
|
|
9573
|
+
const c = JSON.parse(fs27.readFileSync(path22.join(dir2, f), "utf8"));
|
|
9211
9574
|
if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
|
|
9212
9575
|
} catch {
|
|
9213
9576
|
}
|
|
@@ -9215,7 +9578,7 @@ function resolveGitKey(apiUrl) {
|
|
|
9215
9578
|
return void 0;
|
|
9216
9579
|
}
|
|
9217
9580
|
function refsDigest(refs) {
|
|
9218
|
-
return
|
|
9581
|
+
return createHash9("sha256").update(
|
|
9219
9582
|
[...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
|
|
9220
9583
|
).digest("hex").slice(0, 12);
|
|
9221
9584
|
}
|
|
@@ -9228,7 +9591,7 @@ var init_gitremote = __esm({
|
|
|
9228
9591
|
ZERO_SHA = "0".repeat(40);
|
|
9229
9592
|
REMOTE_PATH = "/v1/git/repos/";
|
|
9230
9593
|
remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
|
|
9231
|
-
gitCredentialFile = () =>
|
|
9594
|
+
gitCredentialFile = () => path22.join(homeDir(), ".config/bunshin/git-credentials.json");
|
|
9232
9595
|
GitApiError = class extends Error {
|
|
9233
9596
|
constructor(status, message, body = null) {
|
|
9234
9597
|
super(message);
|
|
@@ -9320,8 +9683,8 @@ __export(gitrepo_exports, {
|
|
|
9320
9683
|
githubReferences: () => githubReferences,
|
|
9321
9684
|
repoFromUrl: () => repoFromUrl
|
|
9322
9685
|
});
|
|
9323
|
-
import
|
|
9324
|
-
import
|
|
9686
|
+
import fs28 from "node:fs";
|
|
9687
|
+
import path23 from "node:path";
|
|
9325
9688
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9326
9689
|
function git3(cwd, args, opts = {}) {
|
|
9327
9690
|
const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
|
|
@@ -9330,9 +9693,9 @@ function git3(cwd, args, opts = {}) {
|
|
|
9330
9693
|
return res;
|
|
9331
9694
|
}
|
|
9332
9695
|
function onPath(name) {
|
|
9333
|
-
for (const
|
|
9696
|
+
for (const dir2 of (process.env.PATH ?? "").split(path23.delimiter)) {
|
|
9334
9697
|
try {
|
|
9335
|
-
|
|
9698
|
+
fs28.accessSync(path23.join(dir2, name), fs28.constants.X_OK);
|
|
9336
9699
|
return true;
|
|
9337
9700
|
} catch {
|
|
9338
9701
|
}
|
|
@@ -9383,9 +9746,9 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
|
|
|
9383
9746
|
if (!reuseKey || !process.env.BUNSHIN_API_KEY || key !== process.env.BUNSHIN_API_KEY) saveGitCredential(apiUrl, key);
|
|
9384
9747
|
return client;
|
|
9385
9748
|
}
|
|
9386
|
-
function toplevel(
|
|
9387
|
-
const r = git3(
|
|
9388
|
-
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`);
|
|
9389
9752
|
return r.out.trim();
|
|
9390
9753
|
}
|
|
9391
9754
|
function githubReferences(root, repo) {
|
|
@@ -9416,11 +9779,11 @@ function switchOrigin(root, url, out) {
|
|
|
9416
9779
|
}
|
|
9417
9780
|
async function cmdImport(args, cwd, out, err) {
|
|
9418
9781
|
requireHelper();
|
|
9419
|
-
const root = toplevel(
|
|
9782
|
+
const root = toplevel(path23.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
|
|
9420
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");
|
|
9421
9784
|
const rs = remotes(root);
|
|
9422
9785
|
const originUrl = rs.get("origin");
|
|
9423
|
-
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)}`;
|
|
9424
9787
|
const client = await connect(args, cwd);
|
|
9425
9788
|
const url = remoteUrl(client.apiUrl, name);
|
|
9426
9789
|
const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
|
|
@@ -9496,7 +9859,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
|
|
|
9496
9859
|
if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
|
|
9497
9860
|
`);
|
|
9498
9861
|
}
|
|
9499
|
-
if (
|
|
9862
|
+
if (fs28.existsSync(path23.join(root, ".github"))) out(`
|
|
9500
9863
|
Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
|
|
9501
9864
|
`);
|
|
9502
9865
|
out(`
|
|
@@ -9507,7 +9870,7 @@ New machines clone with: bunshin repo clone ${name}
|
|
|
9507
9870
|
}
|
|
9508
9871
|
async function cmdAttach(args, cwd, out, err) {
|
|
9509
9872
|
requireHelper();
|
|
9510
|
-
const root = toplevel(
|
|
9873
|
+
const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
|
|
9511
9874
|
const rs = remotes(root);
|
|
9512
9875
|
const originUrl = rs.get("origin");
|
|
9513
9876
|
const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
|
|
@@ -9552,15 +9915,15 @@ async function cmdClone(args, cwd, out) {
|
|
|
9552
9915
|
const name = args._[1];
|
|
9553
9916
|
if (!name) throw new Error("usage: bunshin repo clone owner/name [directory]");
|
|
9554
9917
|
const client = await connect(args, cwd);
|
|
9555
|
-
const
|
|
9556
|
-
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" });
|
|
9557
9920
|
if (r.status !== 0) return r.status ?? 1;
|
|
9558
|
-
out(`Cloned ${name} into ${
|
|
9921
|
+
out(`Cloned ${name} into ${path23.resolve(cwd, dir2)}
|
|
9559
9922
|
`);
|
|
9560
9923
|
return 0;
|
|
9561
9924
|
}
|
|
9562
9925
|
async function cmdVerify(args, cwd, out, err) {
|
|
9563
|
-
const root = toplevel(
|
|
9926
|
+
const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
|
|
9564
9927
|
const rs = remotes(root);
|
|
9565
9928
|
const originUrl = rs.get("origin") ?? "";
|
|
9566
9929
|
if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
|
|
@@ -9723,22 +10086,22 @@ __export(runner_exports, {
|
|
|
9723
10086
|
setupText: () => setupText
|
|
9724
10087
|
});
|
|
9725
10088
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9726
|
-
import
|
|
10089
|
+
import fs29 from "node:fs";
|
|
9727
10090
|
import os5 from "node:os";
|
|
9728
|
-
import
|
|
10091
|
+
import path24 from "node:path";
|
|
9729
10092
|
function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
|
|
9730
|
-
return
|
|
10093
|
+
return path24.join(home, ".bunshin", "runner");
|
|
9731
10094
|
}
|
|
9732
10095
|
function readInstalled(home) {
|
|
9733
10096
|
try {
|
|
9734
|
-
return JSON.parse(
|
|
10097
|
+
return JSON.parse(fs29.readFileSync(path24.join(runnerHome(home), "config.json"), "utf8"));
|
|
9735
10098
|
} catch {
|
|
9736
10099
|
return null;
|
|
9737
10100
|
}
|
|
9738
10101
|
}
|
|
9739
10102
|
function connection(args, cwd) {
|
|
9740
10103
|
const saved = readInstalled();
|
|
9741
|
-
const store = saved &&
|
|
10104
|
+
const store = saved && path24.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
|
|
9742
10105
|
const config = store?.config();
|
|
9743
10106
|
const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
|
|
9744
10107
|
const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
|
|
@@ -9798,9 +10161,9 @@ async function runOnce(c, args, out, err) {
|
|
|
9798
10161
|
const [job] = await lease(c, args);
|
|
9799
10162
|
if (!job) break;
|
|
9800
10163
|
const started = Date.now();
|
|
9801
|
-
const scratch =
|
|
10164
|
+
const scratch = fs29.mkdtempSync(path24.join(os5.tmpdir(), "bunshin-job-"));
|
|
9802
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(
|
|
9803
|
-
() =>
|
|
10166
|
+
() => fs29.rmSync(scratch, { recursive: true, force: true })
|
|
9804
10167
|
);
|
|
9805
10168
|
const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
|
|
9806
10169
|
if (!res.ok) {
|
|
@@ -9890,24 +10253,24 @@ function renderCheck(id, c) {
|
|
|
9890
10253
|
}
|
|
9891
10254
|
function logAnswer(entry) {
|
|
9892
10255
|
try {
|
|
9893
|
-
|
|
9894
|
-
|
|
10256
|
+
fs29.mkdirSync(runnerHome(), { recursive: true });
|
|
10257
|
+
fs29.appendFileSync(path24.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
9895
10258
|
`);
|
|
9896
10259
|
} catch {
|
|
9897
10260
|
}
|
|
9898
10261
|
}
|
|
9899
10262
|
function which(name) {
|
|
9900
|
-
for (const d of [...(process.env.PATH || "").split(
|
|
9901
|
-
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);
|
|
9902
10265
|
}
|
|
9903
10266
|
return null;
|
|
9904
10267
|
}
|
|
9905
10268
|
function scheduledCommand(backend) {
|
|
9906
|
-
const node =
|
|
9907
|
-
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";
|
|
9908
10271
|
const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
|
|
9909
|
-
const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [
|
|
9910
|
-
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) };
|
|
9911
10274
|
}
|
|
9912
10275
|
function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
9913
10276
|
const cmd = scheduledCommand(backend);
|
|
@@ -9922,8 +10285,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
|
9922
10285
|
<dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
|
|
9923
10286
|
<key>WorkingDirectory</key><string>${xml(home)}</string>
|
|
9924
10287
|
<key>StartInterval</key><integer>${everyMinutes * 60}</integer>
|
|
9925
|
-
<key>StandardOutPath</key><string>${xml(
|
|
9926
|
-
<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>
|
|
9927
10290
|
<key>ProcessType</key><string>Background</string>
|
|
9928
10291
|
<key>LowPriorityIO</key><true/>
|
|
9929
10292
|
</dict>
|
|
@@ -9934,19 +10297,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
|
|
|
9934
10297
|
const cmd = scheduledCommand(backend);
|
|
9935
10298
|
const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
|
|
9936
10299
|
const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
|
|
9937
|
-
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}`;
|
|
9938
10301
|
}
|
|
9939
10302
|
function launchAgentsDir(opts) {
|
|
9940
|
-
return opts.launchAgents ??
|
|
10303
|
+
return opts.launchAgents ?? path24.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
|
|
9941
10304
|
}
|
|
9942
10305
|
function installSchedule(backend, everyMinutes, opts = {}) {
|
|
9943
10306
|
const platform = opts.platform ?? process.platform;
|
|
9944
10307
|
const run2 = opts.run ?? realRun;
|
|
9945
10308
|
if (platform === "darwin") {
|
|
9946
|
-
const
|
|
9947
|
-
const plist =
|
|
9948
|
-
|
|
9949
|
-
|
|
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));
|
|
9950
10313
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
9951
10314
|
run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
|
|
9952
10315
|
const r = run2("launchctl", ["bootstrap", domain, plist]);
|
|
@@ -9966,10 +10329,10 @@ function removeSchedule(opts = {}) {
|
|
|
9966
10329
|
const platform = opts.platform ?? process.platform;
|
|
9967
10330
|
const run2 = opts.run ?? realRun;
|
|
9968
10331
|
if (platform === "darwin") {
|
|
9969
|
-
const plist =
|
|
10332
|
+
const plist = path24.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
|
|
9970
10333
|
run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
|
|
9971
|
-
if (!
|
|
9972
|
-
|
|
10334
|
+
if (!fs29.existsSync(plist)) return "no schedule";
|
|
10335
|
+
fs29.rmSync(plist, { force: true });
|
|
9973
10336
|
return `removed ${plist}`;
|
|
9974
10337
|
}
|
|
9975
10338
|
const current = run2("crontab", ["-l"]);
|
|
@@ -9996,10 +10359,10 @@ async function install(args, cwd, out, err, opts = {}) {
|
|
|
9996
10359
|
}
|
|
9997
10360
|
const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
|
|
9998
10361
|
const home = runnerHome();
|
|
9999
|
-
|
|
10362
|
+
fs29.mkdirSync(home, { recursive: true, mode: 448 });
|
|
10000
10363
|
const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
|
|
10001
|
-
|
|
10002
|
-
|
|
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);
|
|
10003
10366
|
let schedule;
|
|
10004
10367
|
try {
|
|
10005
10368
|
schedule = installSchedule(backend, every, opts);
|
|
@@ -10012,7 +10375,7 @@ Run it yourself: npx -y ${PKG2} runner --once
|
|
|
10012
10375
|
out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
|
|
10013
10376
|
`);
|
|
10014
10377
|
out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
|
|
10015
|
-
` : ` 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")}
|
|
10016
10379
|
`);
|
|
10017
10380
|
out(` Stop: npx -y ${PKG2} runner uninstall
|
|
10018
10381
|
`);
|
|
@@ -10029,9 +10392,9 @@ Answering what waits now\u2026
|
|
|
10029
10392
|
function uninstall(out, opts = {}) {
|
|
10030
10393
|
out(`${removeSchedule(opts)}
|
|
10031
10394
|
`);
|
|
10032
|
-
const file =
|
|
10033
|
-
if (
|
|
10034
|
-
|
|
10395
|
+
const file = path24.join(runnerHome(), "config.json");
|
|
10396
|
+
if (fs29.existsSync(file)) {
|
|
10397
|
+
fs29.rmSync(file, { force: true });
|
|
10035
10398
|
out(`removed ${file} (revoke the key in the console: Agent keys)
|
|
10036
10399
|
`);
|
|
10037
10400
|
}
|
|
@@ -10211,7 +10574,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10211
10574
|
out(text2);
|
|
10212
10575
|
return 0;
|
|
10213
10576
|
}
|
|
10214
|
-
|
|
10577
|
+
fs29.writeFileSync(file, text2, { mode: 384 });
|
|
10215
10578
|
const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
|
|
10216
10579
|
out(
|
|
10217
10580
|
[
|
|
@@ -10229,7 +10592,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10229
10592
|
const id = args._[2];
|
|
10230
10593
|
const file = args._[3] ?? "-";
|
|
10231
10594
|
if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
|
|
10232
|
-
const text2 = file === "-" ?
|
|
10595
|
+
const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
|
|
10233
10596
|
const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
|
|
10234
10597
|
out(renderCheck(id, r.body));
|
|
10235
10598
|
return r.body.ok ? 0 : 1;
|
|
@@ -10238,7 +10601,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10238
10601
|
const id = args._[2];
|
|
10239
10602
|
const file = args._[3] ?? "-";
|
|
10240
10603
|
if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
|
|
10241
|
-
const text2 = file === "-" ?
|
|
10604
|
+
const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
|
|
10242
10605
|
const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
|
|
10243
10606
|
if (check?.body && (check.status < 400 || check.body.problems)) {
|
|
10244
10607
|
out(renderCheck(id, check.body));
|
|
@@ -10262,7 +10625,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
10262
10625
|
const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
|
|
10263
10626
|
const entries = (() => {
|
|
10264
10627
|
try {
|
|
10265
|
-
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);
|
|
10266
10629
|
} catch {
|
|
10267
10630
|
return [];
|
|
10268
10631
|
}
|
|
@@ -10332,8 +10695,8 @@ var init_runner = __esm({
|
|
|
10332
10695
|
|
|
10333
10696
|
// src/v2/importer.ts
|
|
10334
10697
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
10335
|
-
import
|
|
10336
|
-
import
|
|
10698
|
+
import fs30 from "node:fs";
|
|
10699
|
+
import path25 from "node:path";
|
|
10337
10700
|
function rewriteRepoPaths(text2, repoName2, pack) {
|
|
10338
10701
|
const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
|
|
10339
10702
|
return text2.replace(re, `~/.bunshin/library/${pack}`);
|
|
@@ -10368,17 +10731,17 @@ function buildDocs(files, opts) {
|
|
|
10368
10731
|
const claimed = /* @__PURE__ */ new Set();
|
|
10369
10732
|
const all = [...files.keys()].sort();
|
|
10370
10733
|
const skillDirs = all.map(skillDirOf).filter((d) => !!d);
|
|
10371
|
-
for (const
|
|
10372
|
-
const main2 = `${
|
|
10734
|
+
for (const dir2 of skillDirs) {
|
|
10735
|
+
const main2 = `${dir2}/SKILL.md`;
|
|
10373
10736
|
const text2 = rw(files.get(main2).content);
|
|
10374
10737
|
const fm = frontmatterDescription(text2);
|
|
10375
|
-
const name =
|
|
10738
|
+
const name = path25.posix.basename(dir2);
|
|
10376
10739
|
const bundle = {};
|
|
10377
|
-
for (const p of all.filter((x) => x.startsWith(`${
|
|
10740
|
+
for (const p of all.filter((x) => x.startsWith(`${dir2}/`))) {
|
|
10378
10741
|
claimed.add(p);
|
|
10379
10742
|
if (p === main2) continue;
|
|
10380
10743
|
const f = files.get(p);
|
|
10381
|
-
bundle[p.slice(
|
|
10744
|
+
bundle[p.slice(dir2.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
|
|
10382
10745
|
}
|
|
10383
10746
|
const description = fm.description.replace(/\s+/g, " ").trim() || firstSentence(text2) || name;
|
|
10384
10747
|
const { name: _n, description: _d, ...rest } = fm.data;
|
|
@@ -10405,9 +10768,9 @@ function buildDocs(files, opts) {
|
|
|
10405
10768
|
if (/\.md$/i.test(p) && looksGenerated(files.get(p).content)) claimed.add(p);
|
|
10406
10769
|
}
|
|
10407
10770
|
const dirs = new Set(all.filter((x) => x.includes("/") && !claimed.has(x)).map((x) => x.split("/")[0]));
|
|
10408
|
-
for (const
|
|
10409
|
-
if (skillParents.has(
|
|
10410
|
-
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));
|
|
10411
10774
|
if (!members.length) continue;
|
|
10412
10775
|
const readme = members.find((x) => /^[^/]+\/README(\.md)?$/i.test(x));
|
|
10413
10776
|
const bundle = {};
|
|
@@ -10418,15 +10781,15 @@ function buildDocs(files, opts) {
|
|
|
10418
10781
|
bundle[p] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
|
|
10419
10782
|
}
|
|
10420
10783
|
const tool = members.some((p) => files.get(p).mode === "x" || SCRIPT.test(p));
|
|
10421
|
-
const body = readme ? rw(files.get(readme).content) : `# ${
|
|
10784
|
+
const body = readme ? rw(files.get(readme).content) : `# ${dir2}
|
|
10422
10785
|
|
|
10423
10786
|
${members.map((p) => `- \`${p}\``).join("\n")}
|
|
10424
10787
|
`;
|
|
10425
10788
|
docs.push({
|
|
10426
|
-
path: readme ?? `${
|
|
10789
|
+
path: readme ?? `${dir2}/#index`,
|
|
10427
10790
|
kind: tool ? "tool" : "reference",
|
|
10428
|
-
title:
|
|
10429
|
-
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}`,
|
|
10430
10793
|
body,
|
|
10431
10794
|
files: bundle
|
|
10432
10795
|
});
|
|
@@ -10469,23 +10832,23 @@ function isGitRepo(root) {
|
|
|
10469
10832
|
}
|
|
10470
10833
|
function workingTreeFiles(root) {
|
|
10471
10834
|
const out = /* @__PURE__ */ new Map();
|
|
10472
|
-
const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(
|
|
10473
|
-
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) => {
|
|
10474
10837
|
if (e.name === ".git" || e.name === "node_modules") return [];
|
|
10475
10838
|
const rel = base ? `${base}/${e.name}` : e.name;
|
|
10476
|
-
return e.isDirectory() ? walk(
|
|
10839
|
+
return e.isDirectory() ? walk(path25.join(dir2, e.name), rel) : e.isFile() ? [rel] : [];
|
|
10477
10840
|
});
|
|
10478
10841
|
})(root);
|
|
10479
10842
|
for (const rel of list2) {
|
|
10480
|
-
const abs =
|
|
10843
|
+
const abs = path25.join(root, rel);
|
|
10481
10844
|
let buf;
|
|
10482
10845
|
try {
|
|
10483
|
-
buf =
|
|
10846
|
+
buf = fs30.readFileSync(abs);
|
|
10484
10847
|
} catch {
|
|
10485
10848
|
continue;
|
|
10486
10849
|
}
|
|
10487
10850
|
if (!isText(buf)) continue;
|
|
10488
|
-
const exec = (
|
|
10851
|
+
const exec = (fs30.statSync(abs).mode & 73) !== 0;
|
|
10489
10852
|
out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
|
|
10490
10853
|
}
|
|
10491
10854
|
return out;
|
|
@@ -10519,7 +10882,7 @@ function diffDocs(prev, next) {
|
|
|
10519
10882
|
return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
|
|
10520
10883
|
}
|
|
10521
10884
|
function readSkillSystem(root, opts) {
|
|
10522
|
-
const repoName2 =
|
|
10885
|
+
const repoName2 = path25.basename(path25.resolve(root));
|
|
10523
10886
|
const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
|
|
10524
10887
|
const tree = build(workingTreeFiles(root));
|
|
10525
10888
|
if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
|
|
@@ -10596,7 +10959,7 @@ var init_importer = __esm({
|
|
|
10596
10959
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10597
10960
|
looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
|
|
10598
10961
|
isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
|
|
10599
|
-
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);
|
|
10600
10963
|
fingerprint = (d) => JSON.stringify(d);
|
|
10601
10964
|
REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
|
|
10602
10965
|
|
|
@@ -10632,8 +10995,8 @@ __export(skills_exports, {
|
|
|
10632
10995
|
cmdSkill: () => cmdSkill,
|
|
10633
10996
|
parsePin: () => parsePin
|
|
10634
10997
|
});
|
|
10635
|
-
import
|
|
10636
|
-
import
|
|
10998
|
+
import fs31 from "node:fs";
|
|
10999
|
+
import path26 from "node:path";
|
|
10637
11000
|
function requireOnline(store, what) {
|
|
10638
11001
|
if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
|
|
10639
11002
|
}
|
|
@@ -10642,9 +11005,9 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10642
11005
|
err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
|
|
10643
11006
|
return 1;
|
|
10644
11007
|
}
|
|
10645
|
-
const root =
|
|
10646
|
-
if (!
|
|
10647
|
-
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);
|
|
10648
11011
|
const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
|
|
10649
11012
|
const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
|
|
10650
11013
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10679,7 +11042,7 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10679
11042
|
const propose = args.flags["propose-rewrites"];
|
|
10680
11043
|
if (propose) {
|
|
10681
11044
|
const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
|
|
10682
|
-
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);
|
|
10683
11046
|
}
|
|
10684
11047
|
out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
|
|
10685
11048
|
return 0;
|
|
@@ -10819,7 +11182,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
|
|
|
10819
11182
|
}
|
|
10820
11183
|
const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
|
|
10821
11184
|
if (graph) {
|
|
10822
|
-
|
|
11185
|
+
fs31.writeFileSync(path26.resolve(cwd, graph), renderSkillGraph(report));
|
|
10823
11186
|
out(`graph written to ${graph}
|
|
10824
11187
|
`);
|
|
10825
11188
|
}
|
|
@@ -10909,9 +11272,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10909
11272
|
`;
|
|
10910
11273
|
const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
|
|
10911
11274
|
if (file) {
|
|
10912
|
-
const abs =
|
|
10913
|
-
|
|
10914
|
-
|
|
11275
|
+
const abs = path26.resolve(cwd, file);
|
|
11276
|
+
fs31.mkdirSync(path26.dirname(abs), { recursive: true });
|
|
11277
|
+
fs31.writeFileSync(abs, line);
|
|
10915
11278
|
out(`${file}: ${line}`);
|
|
10916
11279
|
} else out(line);
|
|
10917
11280
|
return 0;
|
|
@@ -10921,16 +11284,16 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10921
11284
|
const to = str6(args.flags.to);
|
|
10922
11285
|
if (!to) throw new Error("--to <dir> is required");
|
|
10923
11286
|
const pinFile = str6(args.flags.pin);
|
|
10924
|
-
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] ?? "");
|
|
10925
11288
|
const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
|
|
10926
11289
|
if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
|
|
10927
11290
|
const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
|
|
10928
|
-
const
|
|
10929
|
-
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)`);
|
|
10930
11293
|
const pack = str6(args.flags.pack);
|
|
10931
11294
|
const docs = snap.sections.filter((s) => !pack || s.pack === pack);
|
|
10932
|
-
const res = materialize(docs,
|
|
10933
|
-
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}
|
|
10934
11297
|
`);
|
|
10935
11298
|
return 0;
|
|
10936
11299
|
}
|
|
@@ -10965,27 +11328,27 @@ __export(reflect_exports, {
|
|
|
10965
11328
|
buildLesson: () => buildLesson,
|
|
10966
11329
|
runReflection: () => runReflection
|
|
10967
11330
|
});
|
|
10968
|
-
import
|
|
10969
|
-
import
|
|
10970
|
-
import { createHash as
|
|
11331
|
+
import fs32 from "node:fs";
|
|
11332
|
+
import path27 from "node:path";
|
|
11333
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
10971
11334
|
async function runReflection(store, opts) {
|
|
10972
11335
|
const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
|
|
10973
|
-
const lock =
|
|
11336
|
+
const lock = path27.join(store.root, `reflect-${createHash10("sha256").update(opts.sessionId).digest("hex")}.lock`);
|
|
10974
11337
|
const deadline = Date.now() + 66e4;
|
|
10975
11338
|
for (; ; ) {
|
|
10976
11339
|
try {
|
|
10977
|
-
|
|
11340
|
+
fs32.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
10978
11341
|
break;
|
|
10979
11342
|
} catch (error) {
|
|
10980
11343
|
if (error.code !== "EEXIST") throw error;
|
|
10981
11344
|
try {
|
|
10982
|
-
const owner = Number(
|
|
11345
|
+
const owner = Number(fs32.readFileSync(lock, "utf8"));
|
|
10983
11346
|
if (Number.isInteger(owner) && owner > 0) {
|
|
10984
11347
|
try {
|
|
10985
11348
|
process.kill(owner, 0);
|
|
10986
11349
|
} catch (error2) {
|
|
10987
11350
|
if (error2.code === "ESRCH") {
|
|
10988
|
-
|
|
11351
|
+
fs32.unlinkSync(lock);
|
|
10989
11352
|
continue;
|
|
10990
11353
|
}
|
|
10991
11354
|
}
|
|
@@ -11000,7 +11363,7 @@ async function runReflection(store, opts) {
|
|
|
11000
11363
|
return await reflectSession(store, opts);
|
|
11001
11364
|
} finally {
|
|
11002
11365
|
try {
|
|
11003
|
-
|
|
11366
|
+
fs32.unlinkSync(lock);
|
|
11004
11367
|
} catch {
|
|
11005
11368
|
}
|
|
11006
11369
|
}
|
|
@@ -11342,8 +11705,8 @@ __export(cli_exports, {
|
|
|
11342
11705
|
parseArgs: () => parseArgs,
|
|
11343
11706
|
version: () => version
|
|
11344
11707
|
});
|
|
11345
|
-
import
|
|
11346
|
-
import
|
|
11708
|
+
import fs33 from "node:fs";
|
|
11709
|
+
import path28 from "node:path";
|
|
11347
11710
|
function parseArgs(argv) {
|
|
11348
11711
|
const out = { _: [], flags: {} };
|
|
11349
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"]);
|
|
@@ -11443,7 +11806,7 @@ async function main(argv, opts = {}) {
|
|
|
11443
11806
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
11444
11807
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
11445
11808
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
11446
|
-
const prior = new Store(
|
|
11809
|
+
const prior = new Store(path28.join(cwd, ".bunshin"));
|
|
11447
11810
|
const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
|
|
11448
11811
|
const apiUrl = str7(args.flags["api-url"]);
|
|
11449
11812
|
const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
@@ -11472,7 +11835,7 @@ async function main(argv, opts = {}) {
|
|
|
11472
11835
|
});
|
|
11473
11836
|
if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
|
|
11474
11837
|
const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
11475
|
-
await askSources2(new Store(
|
|
11838
|
+
await askSources2(new Store(path28.join(cwd, ".bunshin")), out);
|
|
11476
11839
|
}
|
|
11477
11840
|
out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
|
|
11478
11841
|
`);
|
|
@@ -11509,7 +11872,7 @@ Key saved privately outside the project.
|
|
|
11509
11872
|
if (cmd === "mcp") {
|
|
11510
11873
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
11511
11874
|
const root = str7(args.flags.root);
|
|
11512
|
-
await runMcpServer2({ cwd, ...root ? { store: new Store(
|
|
11875
|
+
await runMcpServer2({ cwd, ...root ? { store: new Store(path28.resolve(root)) } : {}, version: version() });
|
|
11513
11876
|
return 0;
|
|
11514
11877
|
}
|
|
11515
11878
|
if (cmd === "demo") {
|
|
@@ -11572,6 +11935,14 @@ Key saved privately outside the project.
|
|
|
11572
11935
|
case "meta-reflect":
|
|
11573
11936
|
case "meta":
|
|
11574
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
|
+
}
|
|
11575
11946
|
case "update-check": {
|
|
11576
11947
|
const latest = await (await Promise.resolve().then(() => (init_update(), update_exports))).checkForUpdate();
|
|
11577
11948
|
if (!bool5(args.flags.quiet)) out(latest ? `latest ${latest} (this is ${version()})
|
|
@@ -11613,7 +11984,7 @@ ${HELP2}`);
|
|
|
11613
11984
|
function openStore(args, cwd) {
|
|
11614
11985
|
const root = str7(args.flags.root);
|
|
11615
11986
|
if (root) {
|
|
11616
|
-
const store = new Store(
|
|
11987
|
+
const store = new Store(path28.resolve(root));
|
|
11617
11988
|
store.ensureLayout();
|
|
11618
11989
|
return store;
|
|
11619
11990
|
}
|
|
@@ -11629,7 +12000,7 @@ async function cmdInit(args, cwd, out, err) {
|
|
|
11629
12000
|
orgId: str7(args.flags["org-id"]),
|
|
11630
12001
|
userId: str7(args.flags["user-id"]),
|
|
11631
12002
|
apiUrl: str7(args.flags["api-url"]),
|
|
11632
|
-
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"]),
|
|
11633
12004
|
scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
|
|
11634
12005
|
harness,
|
|
11635
12006
|
noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
|
|
@@ -11672,10 +12043,10 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11672
12043
|
const conflicts = store.listConflicts().filter((c) => c.status === "open");
|
|
11673
12044
|
const metrics = store.readMetrics(void 0);
|
|
11674
12045
|
const last50 = metrics?.windows?.find((w) => w.name === "last50");
|
|
11675
|
-
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")] : [];
|
|
11676
12047
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
11677
12048
|
try {
|
|
11678
|
-
return
|
|
12049
|
+
return fs33.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
11679
12050
|
} catch {
|
|
11680
12051
|
return false;
|
|
11681
12052
|
}
|
|
@@ -11693,7 +12064,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11693
12064
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
11694
12065
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
11695
12066
|
hooks: { [config.harness]: hooks },
|
|
11696
|
-
hookFiles: hookFiles.filter((file) =>
|
|
12067
|
+
hookFiles: hookFiles.filter((file) => fs33.existsSync(file)),
|
|
11697
12068
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
|
|
11698
12069
|
v2
|
|
11699
12070
|
};
|
|
@@ -11736,7 +12107,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11736
12107
|
}
|
|
11737
12108
|
function hookCount(file) {
|
|
11738
12109
|
try {
|
|
11739
|
-
const raw = JSON.parse(
|
|
12110
|
+
const raw = JSON.parse(fs33.readFileSync(file, "utf8"));
|
|
11740
12111
|
let n = 0;
|
|
11741
12112
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
11742
12113
|
for (const g of groups ?? []) {
|
|
@@ -11856,7 +12227,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
11856
12227
|
let content = str7(args.flags.content) ?? "";
|
|
11857
12228
|
if (!content || content === "-" || args._.includes("-")) {
|
|
11858
12229
|
try {
|
|
11859
|
-
content =
|
|
12230
|
+
content = fs33.readFileSync(0, "utf8");
|
|
11860
12231
|
} catch {
|
|
11861
12232
|
}
|
|
11862
12233
|
}
|
|
@@ -11922,7 +12293,7 @@ async function cmdReflect(store, args, cwd, out, err) {
|
|
|
11922
12293
|
}
|
|
11923
12294
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
|
|
11924
12295
|
const result = await runReflection2(store, {
|
|
11925
|
-
sessionId: str7(args.flags.session) ??
|
|
12296
|
+
sessionId: str7(args.flags.session) ?? path28.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
11926
12297
|
transcriptPath: transcript,
|
|
11927
12298
|
cwd,
|
|
11928
12299
|
force: bool5(args.flags.force)
|
|
@@ -12125,7 +12496,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
|
|
|
12125
12496
|
Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
|
|
12126
12497
|
CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
|
|
12127
12498
|
`;
|
|
12128
|
-
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.`;
|
|
12129
12500
|
}
|
|
12130
12501
|
});
|
|
12131
12502
|
|