@bunshin-cc/bunshin 0.6.4 → 0.7.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/README.md +2 -0
- package/dist/bin.js +899 -394
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1344,17 +1344,17 @@ var init_document = __esm({
|
|
|
1344
1344
|
});
|
|
1345
1345
|
|
|
1346
1346
|
// ../core/src/v2/sections/markdown.ts
|
|
1347
|
-
function repoSectionId(repo,
|
|
1348
|
-
return anchor ? `repo:${repo}/${
|
|
1347
|
+
function repoSectionId(repo, path28, anchor = "") {
|
|
1348
|
+
return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
|
|
1349
1349
|
}
|
|
1350
|
-
function docKind(
|
|
1351
|
-
const base =
|
|
1350
|
+
function docKind(path28) {
|
|
1351
|
+
const base = path28.split("/").pop()?.toLowerCase() ?? "";
|
|
1352
1352
|
if (base === "testing.md") return "verification";
|
|
1353
1353
|
if (base === "skill.md") return "skill";
|
|
1354
1354
|
return "spec";
|
|
1355
1355
|
}
|
|
1356
|
-
function defaultPaths(
|
|
1357
|
-
const dir =
|
|
1356
|
+
function defaultPaths(path28) {
|
|
1357
|
+
const dir = path28.split("/").slice(0, -1).join("/");
|
|
1358
1358
|
if (!dir || dir.startsWith(".claude/") || dir.startsWith(".codex/")) return [];
|
|
1359
1359
|
return [`${dir}/**`];
|
|
1360
1360
|
}
|
|
@@ -1499,11 +1499,11 @@ function lintSkills(sections) {
|
|
|
1499
1499
|
}
|
|
1500
1500
|
const state = /* @__PURE__ */ new Map();
|
|
1501
1501
|
const cycles = /* @__PURE__ */ new Set();
|
|
1502
|
-
const dfs = (n,
|
|
1502
|
+
const dfs = (n, path28) => {
|
|
1503
1503
|
state.set(n, 1);
|
|
1504
1504
|
for (const d of edges[n] ?? []) {
|
|
1505
|
-
if (state.get(d) === 1) cycles.add([...
|
|
1506
|
-
else if (!state.get(d)) dfs(d, [...
|
|
1505
|
+
if (state.get(d) === 1) cycles.add([...path28.slice(path28.indexOf(d)), d].join(" -> "));
|
|
1506
|
+
else if (!state.get(d)) dfs(d, [...path28, d]);
|
|
1507
1507
|
}
|
|
1508
1508
|
state.set(n, 2);
|
|
1509
1509
|
};
|
|
@@ -1590,8 +1590,8 @@ function parsePythonSkills(text2) {
|
|
|
1590
1590
|
}
|
|
1591
1591
|
return { skills, constants };
|
|
1592
1592
|
}
|
|
1593
|
-
function harnessSectionId(
|
|
1594
|
-
return `harness:${
|
|
1593
|
+
function harnessSectionId(path28, symbol) {
|
|
1594
|
+
return `harness:${path28}#${symbol}`;
|
|
1595
1595
|
}
|
|
1596
1596
|
var DEF_RE;
|
|
1597
1597
|
var init_python = __esm({
|
|
@@ -1717,8 +1717,8 @@ function globToRegExp(glob) {
|
|
|
1717
1717
|
globCache.set(glob, out);
|
|
1718
1718
|
return out;
|
|
1719
1719
|
}
|
|
1720
|
-
function matchesGlob(
|
|
1721
|
-
const p =
|
|
1720
|
+
function matchesGlob(path28, glob) {
|
|
1721
|
+
const p = path28.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
1722
1722
|
return globToRegExp(glob.replace(/^\.\//, "")).test(p);
|
|
1723
1723
|
}
|
|
1724
1724
|
var DEFAULT_EMBEDDERS, globCache;
|
|
@@ -1749,7 +1749,7 @@ function servable(sections, reader) {
|
|
|
1749
1749
|
return [...out.values()];
|
|
1750
1750
|
}
|
|
1751
1751
|
function recallable(sections, reader) {
|
|
1752
|
-
return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop");
|
|
1752
|
+
return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect");
|
|
1753
1753
|
}
|
|
1754
1754
|
function passagesOfSection(s) {
|
|
1755
1755
|
const hit = passageCache.get(s);
|
|
@@ -1809,10 +1809,10 @@ function push(sections, reader, query) {
|
|
|
1809
1809
|
for (const s of recallable(sections, reader).filter((x) => reachesReader(x, reader))) {
|
|
1810
1810
|
const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
|
|
1811
1811
|
for (const t of triggers) {
|
|
1812
|
-
const
|
|
1813
|
-
const entity =
|
|
1814
|
-
const skill =
|
|
1815
|
-
const trigger =
|
|
1812
|
+
const path28 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
|
|
1813
|
+
const entity = path28 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
|
|
1814
|
+
const skill = path28 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
|
|
1815
|
+
const trigger = path28 ? `path:${path28}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
|
|
1816
1816
|
if (trigger) {
|
|
1817
1817
|
hits.set(s.id, { section: s, trigger, anchor: t.anchor });
|
|
1818
1818
|
break;
|
|
@@ -1965,6 +1965,50 @@ var init_engine = __esm({
|
|
|
1965
1965
|
}
|
|
1966
1966
|
});
|
|
1967
1967
|
|
|
1968
|
+
// ../core/src/v2/guidelines.ts
|
|
1969
|
+
function reflectionGuidelines(sections, reader, opts = {}) {
|
|
1970
|
+
const pool = reader ? servable(sections, reader) : sections;
|
|
1971
|
+
return pool.filter((s) => s.delivery === "reflect" && s.status === "active").filter((s) => !opts.layer || s.layer === "org" || opts.layer === s.layer || opts.layer.startsWith(`${s.layer}/`)).sort((a, b) => generality(a.layer) - generality(b.layer) || a.id.localeCompare(b.id)).map((s) => ({ id: s.id, title: s.title, layer: s.layer, body: s.body }));
|
|
1972
|
+
}
|
|
1973
|
+
function guidelineRules(body) {
|
|
1974
|
+
const at = body.search(/^## /m);
|
|
1975
|
+
const rules = (at < 0 ? body : body.slice(at)).replace(/^## Rules[^\n]*\n/m, "").trim();
|
|
1976
|
+
return rules;
|
|
1977
|
+
}
|
|
1978
|
+
function guidelinesSection(gs, opts = {}) {
|
|
1979
|
+
const list2 = gs ?? [];
|
|
1980
|
+
const withRules = list2.map((g) => ({ g, rules: guidelineRules(g.body) })).filter((x) => x.rules);
|
|
1981
|
+
const rules = withRules.length ? `These rules come from reviewers' feedback on earlier lessons. Follow them; where they conflict
|
|
1982
|
+
with the defaults below, they win.
|
|
1983
|
+
|
|
1984
|
+
${withRules.map(({ g, rules: rules2 }) => `### ${g.title}${list2.length > 1 ? ` (${g.layer})` : ""}
|
|
1985
|
+
|
|
1986
|
+
${rules2}`).join("\n\n")}
|
|
1987
|
+
` : "";
|
|
1988
|
+
const target = list2[0];
|
|
1989
|
+
const route = opts.forReflection && target ? `${rules ? "\n" : ""}When the person in this session gives feedback about lessons themselves (how they should be
|
|
1990
|
+
captured or written, what to leave out: for example "/learn do not write ticket-specific details"),
|
|
1991
|
+
that is a rule for these guidelines, not for a task document: op "append" to ${target.id},
|
|
1992
|
+
anchor "${GUIDELINES_ANCHOR}", one general rule in the imperative, with the reason.
|
|
1993
|
+
` : "";
|
|
1994
|
+
if (!rules && !route) return "";
|
|
1995
|
+
return `
|
|
1996
|
+
## How this company wants lessons written (reflection guidelines)
|
|
1997
|
+
|
|
1998
|
+
${rules}${route}`;
|
|
1999
|
+
}
|
|
2000
|
+
var GUIDELINES_ANCHOR, generality;
|
|
2001
|
+
var init_guidelines = __esm({
|
|
2002
|
+
"../core/src/v2/guidelines.ts"() {
|
|
2003
|
+
"use strict";
|
|
2004
|
+
init_types();
|
|
2005
|
+
init_layers();
|
|
2006
|
+
init_engine();
|
|
2007
|
+
GUIDELINES_ANCHOR = "rules";
|
|
2008
|
+
generality = (l) => ({ org: 0, dept: 1, fleet: 1, team: 2, arm: 3, mount: 3, robot: 4, user: 4 })[layerKind(l)] ?? 5;
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
|
|
1968
2012
|
// ../core/src/redact.ts
|
|
1969
2013
|
function secretWord(w) {
|
|
1970
2014
|
if (/tokeniz/.test(w)) return false;
|
|
@@ -2016,17 +2060,6 @@ function identLike(rest) {
|
|
|
2016
2060
|
return humps.length > 0 && m[1].length / humps.length >= 3;
|
|
2017
2061
|
});
|
|
2018
2062
|
}
|
|
2019
|
-
function luhnOk(digits) {
|
|
2020
|
-
let total = 0;
|
|
2021
|
-
let alt = false;
|
|
2022
|
-
for (let i = digits.length - 1; i >= 0; i--) {
|
|
2023
|
-
let d = digits.charCodeAt(i) - 48;
|
|
2024
|
-
if (alt && (d *= 2) > 9) d -= 9;
|
|
2025
|
-
total += d;
|
|
2026
|
-
alt = !alt;
|
|
2027
|
-
}
|
|
2028
|
-
return total % 10 === 0;
|
|
2029
|
-
}
|
|
2030
2063
|
function issuerOk(d) {
|
|
2031
2064
|
const n = d.length;
|
|
2032
2065
|
if (d[0] === "4") return n === 13 || n === 16 || n === 19;
|
|
@@ -2038,23 +2071,44 @@ function issuerOk(d) {
|
|
|
2038
2071
|
if (/^(?:50|5[6-8]|6\d)/.test(d)) return n >= 12 && n <= 19;
|
|
2039
2072
|
return false;
|
|
2040
2073
|
}
|
|
2074
|
+
function luhnDigits(d, n) {
|
|
2075
|
+
let total = 0;
|
|
2076
|
+
let alt = false;
|
|
2077
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
2078
|
+
let x = d[i];
|
|
2079
|
+
if (alt && (x *= 2) > 9) x -= 9;
|
|
2080
|
+
total += x;
|
|
2081
|
+
alt = !alt;
|
|
2082
|
+
}
|
|
2083
|
+
return total % 10 === 0;
|
|
2084
|
+
}
|
|
2041
2085
|
function cardPrefix(s) {
|
|
2042
|
-
const
|
|
2043
|
-
if (/^3[1-3]|^2[019]/.test(digits)) return 0;
|
|
2044
|
-
const sep = s.search(/[ -]/);
|
|
2045
|
-
if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
|
|
2086
|
+
const d = CARD_DIGITS;
|
|
2046
2087
|
const ends = [];
|
|
2047
|
-
|
|
2088
|
+
const counts = [];
|
|
2089
|
+
let digits = 0;
|
|
2090
|
+
let sep = -1;
|
|
2048
2091
|
for (let i = 0, start = 0; i <= s.length; i++) {
|
|
2049
|
-
|
|
2092
|
+
const c = i < s.length ? s.charCodeAt(i) : -1;
|
|
2093
|
+
if (c >= 48 && c <= 57) {
|
|
2094
|
+
if (digits < d.length) d[digits] = c - 48;
|
|
2095
|
+
digits++;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (sep < 0 && c !== -1) sep = i;
|
|
2050
2099
|
if (ends.length && i - start < 3) break;
|
|
2051
|
-
|
|
2052
|
-
|
|
2100
|
+
ends.push(i);
|
|
2101
|
+
counts.push(digits);
|
|
2053
2102
|
start = i + 1;
|
|
2054
2103
|
}
|
|
2104
|
+
if (d[0] === 3 && d[1] >= 1 && d[1] <= 3 || d[0] === 2 && (d[1] === 0 || d[1] === 1 || d[1] === 9)) return 0;
|
|
2105
|
+
if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
|
|
2055
2106
|
for (let k = ends.length - 1; k >= 0; k--) {
|
|
2056
|
-
const
|
|
2057
|
-
if (count
|
|
2107
|
+
const count = counts[k];
|
|
2108
|
+
if (count < 13 || count > 19 || !luhnDigits(d, count)) continue;
|
|
2109
|
+
let str8 = "";
|
|
2110
|
+
for (let j = 0; j < count; j++) str8 += String.fromCharCode(48 + d[j]);
|
|
2111
|
+
if (issuerOk(str8)) return ends[k];
|
|
2058
2112
|
}
|
|
2059
2113
|
return 0;
|
|
2060
2114
|
}
|
|
@@ -2137,7 +2191,7 @@ function redactDeep(value, opts = {}) {
|
|
|
2137
2191
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
|
|
2138
2192
|
return value;
|
|
2139
2193
|
}
|
|
2140
|
-
var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, ASSET_EXT, NOREPLY_RE, NAME, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
|
|
2194
|
+
var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, CARD_DIGITS, ASSET_EXT, NOREPLY_RE, NAME, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
|
|
2141
2195
|
var init_redact = __esm({
|
|
2142
2196
|
"../core/src/redact.ts"() {
|
|
2143
2197
|
"use strict";
|
|
@@ -2213,6 +2267,7 @@ var init_redact = __esm({
|
|
|
2213
2267
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2214
2268
|
PATH_RE = /^(?:~|\.{1,2})?\/[\w.@%+~/-]*$|^[A-Za-z]:\\/;
|
|
2215
2269
|
credentialish = (v) => /[\d\W_]/.test(v) || v.length >= 16 && /[a-z]/.test(v) && /[A-Z]/.test(v);
|
|
2270
|
+
CARD_DIGITS = new Uint8Array(64);
|
|
2216
2271
|
ASSET_EXT = /^\.(?:m?js|cjs|jsx|tsx?|css|scss|map|json|html?|wasm|woff2?|ttf|otf|eot|png|jpe?g|gif|svg|webp|avif|ico|bmp|mp[34]|webm|wav|ogg|pdf|txt|md|zip|gz|br|tgz|tar|whl|jar|bin|dat|lock|log|ya?ml|xml|csv)\b/;
|
|
2217
2272
|
NOREPLY_RE = /^(?:no[._-]?reply|donot[._-]?reply|do[._-]?not[._-]?reply|noreply\+?.*|git|hg)$/i;
|
|
2218
2273
|
NAME = String.raw`(?=[A-Za-z_$])(?=[\w.$-]{0,64}?(?:key|token|secret|pass|pwd|credential|auth|cookie|session|creds))[A-Za-z_$][\w.$-]{0,127}`;
|
|
@@ -2566,10 +2621,10 @@ ${input.digestNote ? `${input.digestNote}
|
|
|
2566
2621
|
|
|
2567
2622
|
` : ""}${input.transcriptDigest}
|
|
2568
2623
|
` : ""}
|
|
2569
|
-
${input.focus ? `## What to capture
|
|
2624
|
+
${input.feedback ? feedbackSection(input.feedback) : ""}${input.focus ? `## What to capture
|
|
2570
2625
|
|
|
2571
2626
|
${input.focus}
|
|
2572
|
-
` : ""}
|
|
2627
|
+
` : ""}${guidelinesSection(input.guidelines, { forReflection: true })}
|
|
2573
2628
|
## When something is worth capturing
|
|
2574
2629
|
|
|
2575
2630
|
Capture only what would change how a future agent acts, and is recorded nowhere durable:
|
|
@@ -2586,6 +2641,16 @@ Capture only what would change how a future agent acts, and is recorded nowhere
|
|
|
2586
2641
|
Label each suggestion with its "signal": a \u2192 "steering", b \u2192 "environment", c \u2192 "knowledge",
|
|
2587
2642
|
d \u2192 "capability", e \u2192 "approach".
|
|
2588
2643
|
|
|
2644
|
+
## How to write it
|
|
2645
|
+
|
|
2646
|
+
A lesson is read by agents on other tasks, months from now. Write the rule, not the story:
|
|
2647
|
+
- Say when it applies, what to do, and why, so it holds beyond this session.
|
|
2648
|
+
- Leave out what only this session had: dates, how many tickets or items there were, ticket ids,
|
|
2649
|
+
the particular cases it happened on, who asked. Name a specific repo, service, file, command or
|
|
2650
|
+
spec only when the lesson is about that thing. One short generic example is fine when it makes
|
|
2651
|
+
the rule clearer.
|
|
2652
|
+
- The date and source are added automatically; never write them yourself.
|
|
2653
|
+
|
|
2589
2654
|
## Where it goes
|
|
2590
2655
|
|
|
2591
2656
|
Every entry is a whole document (a skill, a spec, a policy). Prefer editing an existing document
|
|
@@ -2636,6 +2701,34 @@ At most ${max} suggestions. Schema:
|
|
|
2636
2701
|
"rationale": string, "urgency": "normal"|"urgent",
|
|
2637
2702
|
"signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
|
|
2638
2703
|
}
|
|
2704
|
+
function feedbackSection(f) {
|
|
2705
|
+
return `
|
|
2706
|
+
## Feedback from the person (human steering)
|
|
2707
|
+
|
|
2708
|
+
${f.lesson ? "A person reviewed a lesson reflection took from this session" : "The person who worked in this session read what reflection took from it"} and wrote:
|
|
2709
|
+
|
|
2710
|
+
<feedback>
|
|
2711
|
+
${f.text.trim()}
|
|
2712
|
+
</feedback>
|
|
2713
|
+
${f.lesson ? `
|
|
2714
|
+
The lesson it is about:
|
|
2715
|
+
|
|
2716
|
+
<lesson>
|
|
2717
|
+
${f.lesson.trim()}
|
|
2718
|
+
</lesson>
|
|
2719
|
+
` : ""}
|
|
2720
|
+
Treat it as a correction from a person (signal "steering"): the strongest evidence there is. Act on it:
|
|
2721
|
+
- If it says the session taught something that was missed, capture that lesson in the right
|
|
2722
|
+
document, written as the guidelines below ask. ${f.transcript ? "Find it in the session below." : "The session's trace is no longer available: work from the feedback itself (the person's own account), the lessons already taken and the documents."}
|
|
2723
|
+
- If it corrects a lesson (too specific, wrong, in the wrong place), propose the corrected lesson
|
|
2724
|
+
(op "replace" if it already landed in a document, otherwise a new one) and name the lesson it
|
|
2725
|
+
corrects in the rationale. Lessons listed as already taken may be corrected this way.
|
|
2726
|
+
- If it says how lessons should be captured or written in general, propose a rule for the
|
|
2727
|
+
reflection guidelines, merged with the rules already there: never repeat one; refine an existing
|
|
2728
|
+
rule with op "replace" when the feedback sharpens it.
|
|
2729
|
+
Answer "nothing_new" only if everything it asks for is already in the library or proposed.
|
|
2730
|
+
`;
|
|
2731
|
+
}
|
|
2639
2732
|
function documentsSection(docs) {
|
|
2640
2733
|
if (!docs?.length) return "";
|
|
2641
2734
|
const body = docs.map((d) => `### ${d.id}: ${d.title} (${d.why})
|
|
@@ -2772,6 +2865,7 @@ var REFLECT_MARKER, ENVIRONMENT_RECHECK_DAYS, SIGNAL_WORDS, REFLECT_SCHEMA, KIND
|
|
|
2772
2865
|
var init_reflect = __esm({
|
|
2773
2866
|
"../core/src/v2/reflect.ts"() {
|
|
2774
2867
|
"use strict";
|
|
2868
|
+
init_guidelines();
|
|
2775
2869
|
init_redact();
|
|
2776
2870
|
init_types();
|
|
2777
2871
|
REFLECT_MARKER = "BUNSHIN:reflect";
|
|
@@ -2866,6 +2960,7 @@ var init_reconcile = __esm({
|
|
|
2866
2960
|
var init_merge = __esm({
|
|
2867
2961
|
"../core/src/v2/merge.ts"() {
|
|
2868
2962
|
"use strict";
|
|
2963
|
+
init_guidelines();
|
|
2869
2964
|
init_text();
|
|
2870
2965
|
init_reconcile();
|
|
2871
2966
|
init_markdown();
|
|
@@ -2898,6 +2993,16 @@ var init_profile = __esm({
|
|
|
2898
2993
|
}
|
|
2899
2994
|
});
|
|
2900
2995
|
|
|
2996
|
+
// ../core/src/v2/sources.ts
|
|
2997
|
+
function sourceId(kind, name) {
|
|
2998
|
+
return `${kind}:${name.trim()}`;
|
|
2999
|
+
}
|
|
3000
|
+
var init_sources = __esm({
|
|
3001
|
+
"../core/src/v2/sources.ts"() {
|
|
3002
|
+
"use strict";
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
|
|
2901
3006
|
// ../core/src/v2/index.ts
|
|
2902
3007
|
var init_v2 = __esm({
|
|
2903
3008
|
"../core/src/v2/index.ts"() {
|
|
@@ -2918,6 +3023,8 @@ var init_v2 = __esm({
|
|
|
2918
3023
|
init_release();
|
|
2919
3024
|
init_research();
|
|
2920
3025
|
init_profile();
|
|
3026
|
+
init_guidelines();
|
|
3027
|
+
init_sources();
|
|
2921
3028
|
}
|
|
2922
3029
|
});
|
|
2923
3030
|
|
|
@@ -3538,7 +3645,7 @@ var init_src = __esm({
|
|
|
3538
3645
|
// src/v2/store.ts
|
|
3539
3646
|
import fs6 from "node:fs";
|
|
3540
3647
|
import path5 from "node:path";
|
|
3541
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
3648
|
+
import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
|
|
3542
3649
|
function hexId(prefix) {
|
|
3543
3650
|
return `${prefix}_${randomBytes3(6).toString("hex")}`;
|
|
3544
3651
|
}
|
|
@@ -3560,7 +3667,7 @@ function normalizeSession(id, raw) {
|
|
|
3560
3667
|
hinted: list2(raw.hinted)
|
|
3561
3668
|
};
|
|
3562
3669
|
}
|
|
3563
|
-
var SAFE_ID, fileSafe, V2Store, addUnique;
|
|
3670
|
+
var SAFE_ID, fileSafe, V2Store, rootKey, addUnique;
|
|
3564
3671
|
var init_store2 = __esm({
|
|
3565
3672
|
"src/v2/store.ts"() {
|
|
3566
3673
|
"use strict";
|
|
@@ -3695,11 +3802,12 @@ var init_store2 = __esm({
|
|
|
3695
3802
|
return { status: raw.status, source: raw.source || "robot", ...raw.metrics ? { metrics: raw.metrics } : {}, at };
|
|
3696
3803
|
}
|
|
3697
3804
|
// ── repo index / sync state ──
|
|
3698
|
-
|
|
3699
|
-
|
|
3805
|
+
/** One index per working tree: a store covering a folder of repositories keeps each one's. */
|
|
3806
|
+
readRepoIndex(root) {
|
|
3807
|
+
return readJson(this.p("repo-index", `${rootKey(root)}.json`), null);
|
|
3700
3808
|
}
|
|
3701
|
-
writeRepoIndex(index) {
|
|
3702
|
-
writeJsonAtomic(this.p("repo-index.json
|
|
3809
|
+
writeRepoIndex(root, index) {
|
|
3810
|
+
writeJsonAtomic(this.p("repo-index", `${rootKey(root)}.json`), index);
|
|
3703
3811
|
}
|
|
3704
3812
|
readSyncState() {
|
|
3705
3813
|
return readJson(this.p("sync.json"), {});
|
|
@@ -3767,6 +3875,7 @@ var init_store2 = __esm({
|
|
|
3767
3875
|
return out;
|
|
3768
3876
|
}
|
|
3769
3877
|
};
|
|
3878
|
+
rootKey = (root) => createHash2("sha256").update(path5.resolve(root)).digest("hex").slice(0, 16);
|
|
3770
3879
|
addUnique = (list2, ...ids) => {
|
|
3771
3880
|
for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
|
|
3772
3881
|
};
|
|
@@ -3774,7 +3883,8 @@ var init_store2 = __esm({
|
|
|
3774
3883
|
});
|
|
3775
3884
|
|
|
3776
3885
|
// src/v2/deploy.ts
|
|
3777
|
-
import {
|
|
3886
|
+
import { execFileSync } from "node:child_process";
|
|
3887
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3778
3888
|
import fs7 from "node:fs";
|
|
3779
3889
|
import os2 from "node:os";
|
|
3780
3890
|
import path6 from "node:path";
|
|
@@ -3787,46 +3897,31 @@ function deployWorkspace(env = process.env) {
|
|
|
3787
3897
|
return null;
|
|
3788
3898
|
}
|
|
3789
3899
|
}
|
|
3790
|
-
function deployPaths(env = process.env) {
|
|
3900
|
+
function deployPaths(env = process.env, orgId) {
|
|
3791
3901
|
const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
|
|
3792
3902
|
const ws = deployWorkspace(env);
|
|
3903
|
+
const state = path6.join(home, ".bunshin", "deploy");
|
|
3904
|
+
const shared = { claude: path6.join(ws ?? home, ".claude", "skills"), codex: path6.join(ws ?? home, ".codex", "skills") };
|
|
3905
|
+
const org = orgId ? path6.join(state, orgKey(orgId)) : null;
|
|
3793
3906
|
return {
|
|
3794
3907
|
home,
|
|
3795
|
-
claudeSkills: path6.join(
|
|
3796
|
-
codexSkills: path6.join(
|
|
3908
|
+
claudeSkills: org ? path6.join(org, "claude-skills") : shared.claude,
|
|
3909
|
+
codexSkills: org ? path6.join(org, "codex-skills") : shared.codex,
|
|
3910
|
+
...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path6.join(home, ".claude", "skills"), path6.join(home, ".codex", "skills"), path6.join(home, ".bunshin", "library")])] } : {},
|
|
3797
3911
|
claudeMd: ws ? path6.join(ws, "CLAUDE.md") : path6.join(home, ".claude", "CLAUDE.md"),
|
|
3798
3912
|
agentsMd: ws ? path6.join(ws, "AGENTS.md") : path6.join(home, ".codex", "AGENTS.md"),
|
|
3799
|
-
library: path6.join(home, ".bunshin", "library"),
|
|
3913
|
+
library: org ? path6.join(org, "library") : path6.join(home, ".bunshin", "library"),
|
|
3914
|
+
// Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
|
|
3800
3915
|
bin: path6.join(home, ".bunshin", "bin"),
|
|
3801
|
-
state
|
|
3916
|
+
state
|
|
3802
3917
|
};
|
|
3803
3918
|
}
|
|
3804
|
-
function setDeployWorkspace(dir, env = process.env) {
|
|
3805
|
-
const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
|
|
3806
|
-
const ws = dir ? path6.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
|
|
3807
|
-
if (ws && !fs7.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
|
|
3808
|
-
const stateDir = path6.join(home, ".bunshin", "deploy");
|
|
3809
|
-
const deployed = fs7.existsSync(stateDir) && fs7.readdirSync(stateDir).some((f) => {
|
|
3810
|
-
if (f === "settings.json" || !f.endsWith(".json")) return false;
|
|
3811
|
-
try {
|
|
3812
|
-
return Object.keys(JSON.parse(fs7.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
|
|
3813
|
-
} catch {
|
|
3814
|
-
return false;
|
|
3815
|
-
}
|
|
3816
|
-
});
|
|
3817
|
-
if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
|
|
3818
|
-
throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
|
|
3819
|
-
}
|
|
3820
|
-
fs7.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
|
|
3821
|
-
fs7.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
|
|
3822
|
-
return ws;
|
|
3823
|
-
}
|
|
3824
3919
|
function statePath(p, orgId) {
|
|
3825
|
-
return path6.join(p.state, `${orgId
|
|
3920
|
+
return path6.join(p.state, `${orgKey(orgId)}.json`);
|
|
3826
3921
|
}
|
|
3827
3922
|
function readState(p, orgId) {
|
|
3828
3923
|
const s = readJson(statePath(p, orgId), null);
|
|
3829
|
-
return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
|
|
3924
|
+
return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.linked?.length ? { linked: s.linked } : {}, ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
|
|
3830
3925
|
}
|
|
3831
3926
|
function readText(file) {
|
|
3832
3927
|
try {
|
|
@@ -4148,7 +4243,7 @@ function captureInto(v2, config, ctx, state, p, harnesses, now, incoming, captur
|
|
|
4148
4243
|
return captured.map((s) => ({ suggestionId: s.id, op: s.op.op, ...s.target.sectionId ? { sectionId: s.target.sectionId } : {}, name: s.summary ?? s.op.op }));
|
|
4149
4244
|
}
|
|
4150
4245
|
function captureEdits(v2, config, ctx, opts = {}) {
|
|
4151
|
-
const p = opts.paths ?? deployPaths();
|
|
4246
|
+
const p = opts.paths ?? deployPaths(process.env, config.orgId);
|
|
4152
4247
|
const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
|
|
4153
4248
|
const release = lockDeploy(p);
|
|
4154
4249
|
if (!release) return [];
|
|
@@ -4188,11 +4283,14 @@ function lockDeploy(p) {
|
|
|
4188
4283
|
}
|
|
4189
4284
|
return null;
|
|
4190
4285
|
}
|
|
4286
|
+
function linkHarnesses(config) {
|
|
4287
|
+
return [.../* @__PURE__ */ new Set([config.harness, ...config.deploy?.harnesses ?? []])].filter((h) => h === "claude-code" || h === "codex");
|
|
4288
|
+
}
|
|
4191
4289
|
function deployEnabled(config, ctx) {
|
|
4192
4290
|
return ctx.kind === "person" && config.deploy?.enabled !== false;
|
|
4193
4291
|
}
|
|
4194
4292
|
function deployLibrary(v2, config, ctx, bundle, opts = {}) {
|
|
4195
|
-
const p = opts.paths ?? deployPaths();
|
|
4293
|
+
const p = opts.paths ?? deployPaths(process.env, config.orgId);
|
|
4196
4294
|
const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
|
|
4197
4295
|
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4198
4296
|
const release = lockDeploy(p);
|
|
@@ -4209,6 +4307,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4209
4307
|
const plan = planDeploy(bundle, p, harnesses);
|
|
4210
4308
|
report.warnings.push(...plan.warnings);
|
|
4211
4309
|
const owned = (abs) => [p.claudeSkills, p.codexSkills, p.library].some((r) => insideRoot(r, abs));
|
|
4310
|
+
const removable = (abs) => owned(abs) || (p.legacy ?? []).some((r) => insideRoot(r, abs));
|
|
4212
4311
|
if (opts.capture !== false) report.captured = captureInto(v2, config, ctx, state, p, harnesses, now, plan.skills.map((x) => x.skill.name), opts.captureNewSkills);
|
|
4213
4312
|
const decided = new Map(bundle.decisions.map((d) => [d.suggestionId, d.decision.status]));
|
|
4214
4313
|
const localStatus = (id) => decided.get(id) ?? v2.readSuggestion(id)?.status;
|
|
@@ -4266,7 +4365,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4266
4365
|
prevByAbs.delete(`${t.abs}#${t.role}`);
|
|
4267
4366
|
}
|
|
4268
4367
|
for (const old of prevByAbs.values()) {
|
|
4269
|
-
if (old.role === "block" || d.contents.has(old.abs) || !
|
|
4368
|
+
if (old.role === "block" || d.contents.has(old.abs) || !removable(old.abs)) continue;
|
|
4270
4369
|
const cur = readText(old.abs);
|
|
4271
4370
|
if (cur !== null && sha(cur) === old.sha) {
|
|
4272
4371
|
removeFile(old.abs);
|
|
@@ -4275,6 +4374,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4275
4374
|
}
|
|
4276
4375
|
}
|
|
4277
4376
|
if (d.kind === "skill") {
|
|
4377
|
+
for (const dir of prev?.dirs ?? []) {
|
|
4378
|
+
if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
|
|
4379
|
+
removeFile(path6.join(dir, MANIFEST));
|
|
4380
|
+
pruneDirs(dir, path6.dirname(dir));
|
|
4381
|
+
}
|
|
4278
4382
|
for (const dir of d.dirs ?? []) {
|
|
4279
4383
|
const manifest = safeJoin(dir, MANIFEST);
|
|
4280
4384
|
if (foreign.has(dir) || !manifest || !fs7.existsSync(path6.join(dir, "SKILL.md"))) continue;
|
|
@@ -4293,7 +4397,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4293
4397
|
continue;
|
|
4294
4398
|
}
|
|
4295
4399
|
for (const t of doc.targets) {
|
|
4296
|
-
if (t.role === "block" || !
|
|
4400
|
+
if (t.role === "block" || !removable(t.abs)) continue;
|
|
4297
4401
|
const cur = readText(t.abs);
|
|
4298
4402
|
if (cur !== null && sha(cur) === t.sha) {
|
|
4299
4403
|
removeFile(t.abs);
|
|
@@ -4301,7 +4405,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4301
4405
|
}
|
|
4302
4406
|
}
|
|
4303
4407
|
for (const dir of doc.dirs ?? []) {
|
|
4304
|
-
if (!
|
|
4408
|
+
if (!removable(dir)) continue;
|
|
4305
4409
|
removeFile(path6.join(dir, MANIFEST));
|
|
4306
4410
|
pruneDirs(dir, path6.dirname(dir));
|
|
4307
4411
|
if (fs7.existsSync(dir)) {
|
|
@@ -4348,6 +4452,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4348
4452
|
const link = path6.join(dir, ".claude", "skills");
|
|
4349
4453
|
try {
|
|
4350
4454
|
fs7.lstatSync(link);
|
|
4455
|
+
if (fs7.readlinkSync(link) !== p.claudeSkills) {
|
|
4456
|
+
fs7.unlinkSync(link);
|
|
4457
|
+
fs7.symlinkSync(p.claudeSkills, link);
|
|
4458
|
+
}
|
|
4351
4459
|
} catch {
|
|
4352
4460
|
ensureDir(path6.dirname(link));
|
|
4353
4461
|
ensureDir(p.claudeSkills);
|
|
@@ -4366,14 +4474,26 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4366
4474
|
} catch {
|
|
4367
4475
|
}
|
|
4368
4476
|
}
|
|
4369
|
-
|
|
4477
|
+
let linked = state.linked ?? [];
|
|
4478
|
+
if (p.legacy) {
|
|
4479
|
+
const names = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length).map((d) => d.name);
|
|
4480
|
+
const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path6.resolve(opts.linkRoot)] : []])];
|
|
4481
|
+
linked = [];
|
|
4482
|
+
for (const root of roots) {
|
|
4483
|
+
const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
|
|
4484
|
+
report.warnings.push(...res.warnings);
|
|
4485
|
+
if (res.kept) linked.push(root);
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
const saved = { ...state, docs: next, links: [...links], ...linked.length ? { linked } : {}, ...bundle.libraryVersion ? { libraryVersion: bundle.libraryVersion } : {} };
|
|
4489
|
+
if (!linked.length) delete saved.linked;
|
|
4370
4490
|
writeJsonAtomic(statePath(p, config.orgId), saved);
|
|
4371
4491
|
report.libraryVersion = bundle.libraryVersion;
|
|
4372
4492
|
return report;
|
|
4373
4493
|
}
|
|
4374
|
-
function deployedState(config, paths = deployPaths()) {
|
|
4494
|
+
function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
|
|
4375
4495
|
const s = readState(paths, config.orgId);
|
|
4376
|
-
return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, docs: Object.values(s.docs) };
|
|
4496
|
+
return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
|
|
4377
4497
|
}
|
|
4378
4498
|
function materialize(sections, dir) {
|
|
4379
4499
|
let files = 0;
|
|
@@ -4396,7 +4516,89 @@ function materialize(sections, dir) {
|
|
|
4396
4516
|
}
|
|
4397
4517
|
return { files };
|
|
4398
4518
|
}
|
|
4399
|
-
|
|
4519
|
+
function linkSkills(p, root, harnesses, names) {
|
|
4520
|
+
const out = { linked: 0, removed: 0, kept: false, warnings: [] };
|
|
4521
|
+
const abs = path6.resolve(root);
|
|
4522
|
+
if (abs === path6.resolve(p.home) || abs === path6.resolve(os2.homedir()) || !fs7.existsSync(abs)) return out;
|
|
4523
|
+
const managed = (target) => target.startsWith(path6.resolve(p.state) + path6.sep);
|
|
4524
|
+
const excluded = [];
|
|
4525
|
+
for (const h of harnesses) {
|
|
4526
|
+
const spec = LINK_DIRS[h];
|
|
4527
|
+
if (!spec) continue;
|
|
4528
|
+
const dir = path6.join(abs, spec.rel);
|
|
4529
|
+
const canonical = path6.resolve(p[spec.key]);
|
|
4530
|
+
const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs7.existsSync(path6.join(canonical, n, "SKILL.md"))));
|
|
4531
|
+
let entries = [];
|
|
4532
|
+
try {
|
|
4533
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
4534
|
+
} catch {
|
|
4535
|
+
}
|
|
4536
|
+
for (const e of entries) {
|
|
4537
|
+
if (!e.isSymbolicLink()) continue;
|
|
4538
|
+
const link = path6.join(dir, e.name);
|
|
4539
|
+
let target = "";
|
|
4540
|
+
try {
|
|
4541
|
+
target = path6.resolve(dir, fs7.readlinkSync(link));
|
|
4542
|
+
} catch {
|
|
4543
|
+
continue;
|
|
4544
|
+
}
|
|
4545
|
+
if (!managed(target)) continue;
|
|
4546
|
+
if (want.has(e.name) && target === path6.join(canonical, e.name)) {
|
|
4547
|
+
want.delete(e.name);
|
|
4548
|
+
excluded.push(path6.join(spec.rel, e.name));
|
|
4549
|
+
continue;
|
|
4550
|
+
}
|
|
4551
|
+
removeFile(link);
|
|
4552
|
+
out.removed++;
|
|
4553
|
+
}
|
|
4554
|
+
for (const name of want) {
|
|
4555
|
+
const link = path6.join(dir, name);
|
|
4556
|
+
try {
|
|
4557
|
+
fs7.lstatSync(link);
|
|
4558
|
+
out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
|
|
4559
|
+
continue;
|
|
4560
|
+
} catch {
|
|
4561
|
+
}
|
|
4562
|
+
try {
|
|
4563
|
+
ensureDir(dir);
|
|
4564
|
+
fs7.symlinkSync(path6.join(canonical, name), link, "dir");
|
|
4565
|
+
excluded.push(path6.join(spec.rel, name));
|
|
4566
|
+
out.linked++;
|
|
4567
|
+
} catch (err) {
|
|
4568
|
+
out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
pruneDirs(dir, abs);
|
|
4572
|
+
}
|
|
4573
|
+
out.kept = excluded.length > 0;
|
|
4574
|
+
gitExclude(abs, excluded);
|
|
4575
|
+
return out;
|
|
4576
|
+
}
|
|
4577
|
+
function gitExclude(root, rels) {
|
|
4578
|
+
if (!fs7.existsSync(path6.join(root, ".git"))) return;
|
|
4579
|
+
let file;
|
|
4580
|
+
try {
|
|
4581
|
+
file = path6.resolve(root, execFileSync("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
|
|
4582
|
+
} catch {
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
const cur = readText(file) ?? "";
|
|
4586
|
+
const start = cur.indexOf(EXCLUDE_BEGIN);
|
|
4587
|
+
const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
|
|
4588
|
+
const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
|
|
4589
|
+
const block2 = rels.length ? `${EXCLUDE_BEGIN}
|
|
4590
|
+
${[...new Set(rels)].sort().map((r) => `/${r.split(path6.sep).join("/")}`).join("\n")}
|
|
4591
|
+
${EXCLUDE_END}
|
|
4592
|
+
` : "";
|
|
4593
|
+
const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
|
|
4594
|
+
if (next === cur) return;
|
|
4595
|
+
try {
|
|
4596
|
+
ensureDir(path6.dirname(file));
|
|
4597
|
+
fs7.writeFileSync(file, next);
|
|
4598
|
+
} catch {
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
|
|
4400
4602
|
var init_deploy = __esm({
|
|
4401
4603
|
"src/v2/deploy.ts"() {
|
|
4402
4604
|
"use strict";
|
|
@@ -4404,7 +4606,8 @@ var init_deploy = __esm({
|
|
|
4404
4606
|
init_util();
|
|
4405
4607
|
init_store2();
|
|
4406
4608
|
settingsFile = (home) => path6.join(home, ".bunshin", "deploy", "settings.json");
|
|
4407
|
-
|
|
4609
|
+
orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
|
|
4610
|
+
sha = (text2) => createHash3("sha256").update(text2).digest("hex");
|
|
4408
4611
|
BLOCK_BEGIN = "<!-- bunshin:begin -->";
|
|
4409
4612
|
BLOCK_END = "<!-- bunshin:end -->";
|
|
4410
4613
|
MANIFEST = ".bunshin.json";
|
|
@@ -4422,6 +4625,12 @@ var init_deploy = __esm({
|
|
|
4422
4625
|
LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
|
|
4423
4626
|
docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
|
|
4424
4627
|
BORN_SLACK_MS = 2e3;
|
|
4628
|
+
LINK_DIRS = {
|
|
4629
|
+
"claude-code": { rel: path6.join(".claude", "skills"), key: "claudeSkills" },
|
|
4630
|
+
codex: { rel: path6.join(".codex", "skills"), key: "codexSkills" }
|
|
4631
|
+
};
|
|
4632
|
+
EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
|
|
4633
|
+
EXCLUDE_END = "# <<< bunshin";
|
|
4425
4634
|
}
|
|
4426
4635
|
});
|
|
4427
4636
|
|
|
@@ -4753,10 +4962,10 @@ var init_transcript = __esm({
|
|
|
4753
4962
|
// src/v2/repo.ts
|
|
4754
4963
|
import fs9 from "node:fs";
|
|
4755
4964
|
import path7 from "node:path";
|
|
4756
|
-
import { execFileSync } from "node:child_process";
|
|
4965
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4757
4966
|
function git(cwd, args, timeout = 1500) {
|
|
4758
4967
|
try {
|
|
4759
|
-
return
|
|
4968
|
+
return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
4760
4969
|
} catch {
|
|
4761
4970
|
return null;
|
|
4762
4971
|
}
|
|
@@ -4826,7 +5035,7 @@ function listDocs(root) {
|
|
|
4826
5035
|
function indexWorkingTree(v2, config, cwd, opts = {}) {
|
|
4827
5036
|
const root = repoRoot(cwd, v2.store.root);
|
|
4828
5037
|
const now = opts.now ?? Date.now();
|
|
4829
|
-
const cached = v2.readRepoIndex();
|
|
5038
|
+
const cached = v2.readRepoIndex(root);
|
|
4830
5039
|
const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
|
|
4831
5040
|
const sameRepo = cached && cached.ownerTeam === ownerTeam;
|
|
4832
5041
|
const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
|
|
@@ -4864,7 +5073,7 @@ function indexWorkingTree(v2, config, cwd, opts = {}) {
|
|
|
4864
5073
|
};
|
|
4865
5074
|
if (changed) {
|
|
4866
5075
|
try {
|
|
4867
|
-
v2.writeRepoIndex(index);
|
|
5076
|
+
v2.writeRepoIndex(root, index);
|
|
4868
5077
|
} catch {
|
|
4869
5078
|
}
|
|
4870
5079
|
}
|
|
@@ -5449,6 +5658,110 @@ var init_ui = __esm({
|
|
|
5449
5658
|
}
|
|
5450
5659
|
});
|
|
5451
5660
|
|
|
5661
|
+
// src/v2/source.ts
|
|
5662
|
+
var source_exports = {};
|
|
5663
|
+
__export(source_exports, {
|
|
5664
|
+
cwdInPool: () => cwdInPool,
|
|
5665
|
+
inPool: () => inPool,
|
|
5666
|
+
listFolders: () => listFolders,
|
|
5667
|
+
projectRootFor: () => projectRootFor,
|
|
5668
|
+
select: () => select,
|
|
5669
|
+
sourceFor: () => sourceFor,
|
|
5670
|
+
storeHome: () => storeHome,
|
|
5671
|
+
topFolder: () => topFolder
|
|
5672
|
+
});
|
|
5673
|
+
import fs13 from "node:fs";
|
|
5674
|
+
import path9 from "node:path";
|
|
5675
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
5676
|
+
function storeHome(storeRoot) {
|
|
5677
|
+
return path9.dirname(path9.resolve(storeRoot));
|
|
5678
|
+
}
|
|
5679
|
+
function topFolder(cwd, storeRoot) {
|
|
5680
|
+
const rel = path9.relative(storeHome(storeRoot), path9.resolve(cwd));
|
|
5681
|
+
if (!rel || rel.startsWith("..") || path9.isAbsolute(rel)) return "";
|
|
5682
|
+
return rel.split(path9.sep)[0] ?? "";
|
|
5683
|
+
}
|
|
5684
|
+
function inPool(selection, folder) {
|
|
5685
|
+
if (!folder || !selection) return true;
|
|
5686
|
+
if (selection.exclude?.includes(folder)) return false;
|
|
5687
|
+
return selection.only ? selection.only.includes(folder) : true;
|
|
5688
|
+
}
|
|
5689
|
+
function cwdInPool(config, cwd, storeRoot) {
|
|
5690
|
+
return inPool(config.sources, topFolder(cwd, storeRoot));
|
|
5691
|
+
}
|
|
5692
|
+
function git2(cwd, args) {
|
|
5693
|
+
try {
|
|
5694
|
+
return execFileSync3("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
|
|
5695
|
+
} catch {
|
|
5696
|
+
return null;
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
function sourceFor(cwd, storeRoot, ctx) {
|
|
5700
|
+
if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
|
|
5701
|
+
const home = storeHome(storeRoot);
|
|
5702
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
5703
|
+
if (top) {
|
|
5704
|
+
const remote = git2(top, ["remote", "get-url", "origin"]);
|
|
5705
|
+
const named = remote ? parseRemote(remote) : null;
|
|
5706
|
+
if (named) return sourceId("repo", named);
|
|
5707
|
+
const rel = path9.relative(home, top);
|
|
5708
|
+
return sourceId("folder", rel && !rel.startsWith("..") && !path9.isAbsolute(rel) ? rel.split(path9.sep).join("/") : path9.basename(top));
|
|
5709
|
+
}
|
|
5710
|
+
const folder = topFolder(cwd, storeRoot);
|
|
5711
|
+
return folder ? sourceId("folder", folder) : void 0;
|
|
5712
|
+
}
|
|
5713
|
+
function projectRootFor(cwd, storeRoot) {
|
|
5714
|
+
const home = storeHome(storeRoot);
|
|
5715
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
5716
|
+
if (top) {
|
|
5717
|
+
const rel = path9.relative(home, top);
|
|
5718
|
+
if (!rel.startsWith("..") && !path9.isAbsolute(rel)) return top;
|
|
5719
|
+
}
|
|
5720
|
+
const folder = topFolder(cwd, storeRoot);
|
|
5721
|
+
return folder ? path9.join(home, folder) : home;
|
|
5722
|
+
}
|
|
5723
|
+
function listFolders(storeRoot, selection, opts = {}) {
|
|
5724
|
+
const home = storeHome(storeRoot);
|
|
5725
|
+
let names = [];
|
|
5726
|
+
try {
|
|
5727
|
+
names = fs13.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
5728
|
+
} catch {
|
|
5729
|
+
}
|
|
5730
|
+
return names.map((folder) => ({
|
|
5731
|
+
folder,
|
|
5732
|
+
included: inPool(selection, folder),
|
|
5733
|
+
...opts.sources ? { source: sourceFor(path9.join(home, folder), storeRoot) } : {}
|
|
5734
|
+
}));
|
|
5735
|
+
}
|
|
5736
|
+
function select(current, action, folders) {
|
|
5737
|
+
const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
|
|
5738
|
+
const only = current?.only;
|
|
5739
|
+
const exclude = current?.exclude ?? [];
|
|
5740
|
+
let next;
|
|
5741
|
+
switch (action) {
|
|
5742
|
+
case "all":
|
|
5743
|
+
return void 0;
|
|
5744
|
+
case "only":
|
|
5745
|
+
next = { only: uniq(folders) };
|
|
5746
|
+
break;
|
|
5747
|
+
case "include":
|
|
5748
|
+
next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
|
|
5749
|
+
break;
|
|
5750
|
+
case "exclude":
|
|
5751
|
+
next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
|
|
5752
|
+
break;
|
|
5753
|
+
}
|
|
5754
|
+
if (!next.exclude?.length) delete next.exclude;
|
|
5755
|
+
return next.only || next.exclude ? next : void 0;
|
|
5756
|
+
}
|
|
5757
|
+
var init_source = __esm({
|
|
5758
|
+
"src/v2/source.ts"() {
|
|
5759
|
+
"use strict";
|
|
5760
|
+
init_v22();
|
|
5761
|
+
init_repo();
|
|
5762
|
+
}
|
|
5763
|
+
});
|
|
5764
|
+
|
|
5452
5765
|
// src/v2/sync.ts
|
|
5453
5766
|
var sync_exports = {};
|
|
5454
5767
|
__export(sync_exports, {
|
|
@@ -5458,8 +5771,8 @@ __export(sync_exports, {
|
|
|
5458
5771
|
syncV2: () => syncV2,
|
|
5459
5772
|
uploadIndex: () => uploadIndex
|
|
5460
5773
|
});
|
|
5461
|
-
import
|
|
5462
|
-
import
|
|
5774
|
+
import fs14 from "node:fs";
|
|
5775
|
+
import path10 from "node:path";
|
|
5463
5776
|
function episodePayload(e) {
|
|
5464
5777
|
const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, transcriptPath: _tp, ...rest } = e;
|
|
5465
5778
|
void _o, _u, _s, _rs, _rl, _tp;
|
|
@@ -5493,7 +5806,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
|
|
|
5493
5806
|
if (e.upload === "header-only" || e.cliVersion || !e.transcript) continue;
|
|
5494
5807
|
if (now - Date.parse(e.endedAt || e.startedAt) > days * 864e5) continue;
|
|
5495
5808
|
const file = e.transcriptPath ?? v2.readSession(e.sessionId)?.transcriptPath;
|
|
5496
|
-
if (!file || !
|
|
5809
|
+
if (!file || !fs14.existsSync(file)) {
|
|
5497
5810
|
missing++;
|
|
5498
5811
|
continue;
|
|
5499
5812
|
}
|
|
@@ -5508,7 +5821,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
|
|
|
5508
5821
|
async function syncV2(store, opts = {}) {
|
|
5509
5822
|
const v2 = new V2Store(store);
|
|
5510
5823
|
const config = store.config();
|
|
5511
|
-
const cwd = opts.cwd ??
|
|
5824
|
+
const cwd = opts.cwd ?? path10.dirname(store.root);
|
|
5512
5825
|
const report = { offline: !canCall(config), uploadedEpisodes: 0, deferredTraces: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
|
|
5513
5826
|
const deadline = Date.now() + (opts.budgetMs ?? 24e4);
|
|
5514
5827
|
if (report.offline) {
|
|
@@ -5624,7 +5937,8 @@ async function syncV2(store, opts = {}) {
|
|
|
5624
5937
|
const bundle = v2.readBundle();
|
|
5625
5938
|
if (bundle) {
|
|
5626
5939
|
try {
|
|
5627
|
-
|
|
5940
|
+
const linkRoot = cwd && cwdInPool(config, cwd, store.root) ? projectRootFor(cwd, store.root) : void 0;
|
|
5941
|
+
report.deploy = deployLibrary(v2, config, ctx, bundle, { capture: false, ...linkRoot ? { linkRoot } : {}, linkHarnesses: linkHarnesses(config) });
|
|
5628
5942
|
} catch (err) {
|
|
5629
5943
|
report.errors.push(`deploy: ${err instanceof Error ? err.message : String(err)}`);
|
|
5630
5944
|
}
|
|
@@ -5654,7 +5968,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
|
|
|
5654
5968
|
const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
|
|
5655
5969
|
const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
|
|
5656
5970
|
try {
|
|
5657
|
-
return [{ path: rel, text:
|
|
5971
|
+
return [{ path: rel, text: fs14.readFileSync(path10.join(root, rel), "utf8") }];
|
|
5658
5972
|
} catch {
|
|
5659
5973
|
return [];
|
|
5660
5974
|
}
|
|
@@ -5672,6 +5986,7 @@ var init_sync = __esm({
|
|
|
5672
5986
|
init_api();
|
|
5673
5987
|
init_apply();
|
|
5674
5988
|
init_deploy();
|
|
5989
|
+
init_source();
|
|
5675
5990
|
init_repo();
|
|
5676
5991
|
init_store2();
|
|
5677
5992
|
init_view();
|
|
@@ -5682,10 +5997,10 @@ var init_sync = __esm({
|
|
|
5682
5997
|
|
|
5683
5998
|
// src/v2/reflect.ts
|
|
5684
5999
|
import { spawn as spawn3 } from "node:child_process";
|
|
5685
|
-
import { createHash as
|
|
5686
|
-
import
|
|
6000
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6001
|
+
import fs15 from "node:fs";
|
|
5687
6002
|
function episodeIdFor(contextId, sessionId) {
|
|
5688
|
-
return `e_${
|
|
6003
|
+
return `e_${createHash4("sha256").update(`${contextId}
|
|
5689
6004
|
${sessionId}`).digest("hex").slice(0, 16)}`;
|
|
5690
6005
|
}
|
|
5691
6006
|
function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT_MS } = {}) {
|
|
@@ -5718,7 +6033,7 @@ function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT
|
|
|
5718
6033
|
};
|
|
5719
6034
|
let child;
|
|
5720
6035
|
try {
|
|
5721
|
-
child = spawn3(bin, args, { cwd: cwd &&
|
|
6036
|
+
child = spawn3(bin, args, { cwd: cwd && fs15.existsSync(cwd) ? cwd : void 0, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
|
|
5722
6037
|
} catch (e) {
|
|
5723
6038
|
resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
5724
6039
|
return;
|
|
@@ -5763,7 +6078,7 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
|
|
|
5763
6078
|
return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
|
|
5764
6079
|
}
|
|
5765
6080
|
function humanTurnsOf(transcriptPath) {
|
|
5766
|
-
if (!transcriptPath || !
|
|
6081
|
+
if (!transcriptPath || !fs15.existsSync(transcriptPath)) return [];
|
|
5767
6082
|
try {
|
|
5768
6083
|
return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
|
|
5769
6084
|
} catch {
|
|
@@ -5771,7 +6086,7 @@ function humanTurnsOf(transcriptPath) {
|
|
|
5771
6086
|
}
|
|
5772
6087
|
}
|
|
5773
6088
|
function readFacts(transcriptPath) {
|
|
5774
|
-
if (!transcriptPath || !
|
|
6089
|
+
if (!transcriptPath || !fs15.existsSync(transcriptPath)) return null;
|
|
5775
6090
|
try {
|
|
5776
6091
|
return parseTranscript(transcriptPath);
|
|
5777
6092
|
} catch {
|
|
@@ -5814,7 +6129,8 @@ async function runReflectionV2(store, opts) {
|
|
|
5814
6129
|
const from = reflectCursor(session, turns);
|
|
5815
6130
|
const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
|
|
5816
6131
|
const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
|
|
5817
|
-
const
|
|
6132
|
+
const guidelines = reflectionGuidelines(view.sections, view.reader);
|
|
6133
|
+
const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
|
|
5818
6134
|
let raw = null;
|
|
5819
6135
|
if (harness === "claude-code") {
|
|
5820
6136
|
const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
|
|
@@ -5833,7 +6149,7 @@ async function runReflectionV2(store, opts) {
|
|
|
5833
6149
|
result.method = "fork";
|
|
5834
6150
|
} else result.error = fork.error;
|
|
5835
6151
|
}
|
|
5836
|
-
if (result.method === "none" && session.transcriptPath &&
|
|
6152
|
+
if (result.method === "none" && session.transcriptPath && fs15.existsSync(session.transcriptPath)) {
|
|
5837
6153
|
const transcriptDigest = digest(facts ?? parseTranscript(session.transcriptPath), { from, maxChars: 4e4 });
|
|
5838
6154
|
const res = await askJson(config, {
|
|
5839
6155
|
system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
|
|
@@ -5854,8 +6170,10 @@ async function runReflectionV2(store, opts) {
|
|
|
5854
6170
|
result.nothingNew = result.method !== "none" && parsed.length === 0;
|
|
5855
6171
|
const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
|
|
5856
6172
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6173
|
+
const source = session.source !== void 0 ? session.source || void 0 : sourceFor(session.cwd || opts.cwd || process.cwd(), store.root, view.ctx);
|
|
5857
6174
|
for (const p of parsed) {
|
|
5858
6175
|
const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
|
|
6176
|
+
if (source) s.source = source;
|
|
5859
6177
|
s.summary = summarize(s.op);
|
|
5860
6178
|
if (apply) {
|
|
5861
6179
|
const outcome = applyLocally(v2, view, s);
|
|
@@ -5939,6 +6257,7 @@ var init_reflect3 = __esm({
|
|
|
5939
6257
|
init_signals();
|
|
5940
6258
|
init_ui();
|
|
5941
6259
|
init_view();
|
|
6260
|
+
init_source();
|
|
5942
6261
|
FORK_TIMEOUT_MS = 12e4;
|
|
5943
6262
|
FORK_DENIED_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task,Agent,Skill,SlashCommand";
|
|
5944
6263
|
}
|
|
@@ -5956,51 +6275,51 @@ __export(uicmd_exports, {
|
|
|
5956
6275
|
modSource: () => modSource
|
|
5957
6276
|
});
|
|
5958
6277
|
import { spawnSync } from "node:child_process";
|
|
5959
|
-
import
|
|
6278
|
+
import fs16 from "node:fs";
|
|
5960
6279
|
import os3 from "node:os";
|
|
5961
|
-
import
|
|
6280
|
+
import path11 from "node:path";
|
|
5962
6281
|
import { fileURLToPath } from "node:url";
|
|
5963
6282
|
function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
|
|
5964
|
-
return
|
|
6283
|
+
return path11.join(home, ".bunshin", "claude-code-ui");
|
|
5965
6284
|
}
|
|
5966
6285
|
function modSource() {
|
|
5967
|
-
const here =
|
|
5968
|
-
for (const dir of [
|
|
5969
|
-
if (
|
|
6286
|
+
const here = path11.dirname(fileURLToPath(import.meta.url));
|
|
6287
|
+
for (const dir of [path11.join(here, "claude-code-ui"), path11.join(here, "..", "claude-code-ui"), path11.resolve(here, "../../../../adapters/claude-code-ui")]) {
|
|
6288
|
+
if (fs16.existsSync(path11.join(dir, ".claude-plugin", "plugin.json"))) return dir;
|
|
5970
6289
|
}
|
|
5971
6290
|
return null;
|
|
5972
6291
|
}
|
|
5973
6292
|
function installedModVersion(home) {
|
|
5974
6293
|
try {
|
|
5975
|
-
return JSON.parse(
|
|
6294
|
+
return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
|
|
5976
6295
|
} catch {
|
|
5977
6296
|
return null;
|
|
5978
6297
|
}
|
|
5979
6298
|
}
|
|
5980
6299
|
function copyDir(from, to) {
|
|
5981
|
-
|
|
5982
|
-
for (const e of
|
|
6300
|
+
fs16.mkdirSync(to, { recursive: true });
|
|
6301
|
+
for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
|
|
5983
6302
|
if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
|
|
5984
6303
|
if (e.name === "node_modules") continue;
|
|
5985
|
-
const a =
|
|
5986
|
-
const b =
|
|
6304
|
+
const a = path11.join(from, e.name);
|
|
6305
|
+
const b = path11.join(to, e.name);
|
|
5987
6306
|
if (e.isDirectory()) copyDir(a, b);
|
|
5988
|
-
else
|
|
6307
|
+
else fs16.copyFileSync(a, b);
|
|
5989
6308
|
}
|
|
5990
6309
|
}
|
|
5991
6310
|
function installMod(version2, opts = {}) {
|
|
5992
6311
|
const src = modSource();
|
|
5993
6312
|
if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
|
|
5994
6313
|
const home = modHome(opts.home);
|
|
5995
|
-
const dir =
|
|
5996
|
-
|
|
6314
|
+
const dir = path11.join(home, MOD_PLUGIN);
|
|
6315
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
5997
6316
|
copyDir(src, dir);
|
|
5998
|
-
const manifest =
|
|
5999
|
-
const m = JSON.parse(
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6317
|
+
const manifest = path11.join(dir, ".claude-plugin", "plugin.json");
|
|
6318
|
+
const m = JSON.parse(fs16.readFileSync(manifest, "utf8"));
|
|
6319
|
+
fs16.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
|
|
6320
|
+
fs16.mkdirSync(path11.join(home, ".claude-plugin"), { recursive: true });
|
|
6321
|
+
fs16.writeFileSync(
|
|
6322
|
+
path11.join(home, ".claude-plugin", "marketplace.json"),
|
|
6004
6323
|
JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
|
|
6005
6324
|
);
|
|
6006
6325
|
const run2 = opts.run ?? spawnSync;
|
|
@@ -6035,7 +6354,7 @@ async function cmdUi(store, args, out, err, version2) {
|
|
|
6035
6354
|
`);
|
|
6036
6355
|
for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
|
|
6037
6356
|
`);
|
|
6038
|
-
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 " +
|
|
6357
|
+
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 " + path11.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
|
|
6039
6358
|
}
|
|
6040
6359
|
return installed || !failed.length ? 0 : 1;
|
|
6041
6360
|
}
|
|
@@ -6116,19 +6435,19 @@ __export(update_exports, {
|
|
|
6116
6435
|
updateCheckDue: () => updateCheckDue,
|
|
6117
6436
|
updateNotice: () => updateNotice
|
|
6118
6437
|
});
|
|
6119
|
-
import
|
|
6120
|
-
import
|
|
6438
|
+
import fs17 from "node:fs";
|
|
6439
|
+
import path12 from "node:path";
|
|
6121
6440
|
function readState2() {
|
|
6122
6441
|
try {
|
|
6123
|
-
return JSON.parse(
|
|
6442
|
+
return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
|
|
6124
6443
|
} catch {
|
|
6125
6444
|
return {};
|
|
6126
6445
|
}
|
|
6127
6446
|
}
|
|
6128
6447
|
function writeState(s) {
|
|
6129
6448
|
try {
|
|
6130
|
-
|
|
6131
|
-
|
|
6449
|
+
fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
|
|
6450
|
+
fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
|
|
6132
6451
|
} catch {
|
|
6133
6452
|
}
|
|
6134
6453
|
}
|
|
@@ -6178,14 +6497,14 @@ var init_update = __esm({
|
|
|
6178
6497
|
PKG = "@bunshin-cc/bunshin";
|
|
6179
6498
|
LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
|
|
6180
6499
|
DAY = 864e5;
|
|
6181
|
-
stateFile = () =>
|
|
6500
|
+
stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
|
|
6182
6501
|
disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
|
|
6183
6502
|
}
|
|
6184
6503
|
});
|
|
6185
6504
|
|
|
6186
6505
|
// src/v2/hooks.ts
|
|
6187
|
-
import
|
|
6188
|
-
import
|
|
6506
|
+
import fs18 from "node:fs";
|
|
6507
|
+
import path13 from "node:path";
|
|
6189
6508
|
function inContext(s) {
|
|
6190
6509
|
return s.inContext ?? [...s.loaded, ...s.pushed];
|
|
6191
6510
|
}
|
|
@@ -6200,6 +6519,12 @@ function touch(v2, ctx, patch = () => {
|
|
|
6200
6519
|
const s = v2.updateSession(id, ctx.cwd, (s2) => {
|
|
6201
6520
|
s2.cwd = ctx.cwd || s2.cwd;
|
|
6202
6521
|
s2.harness = ctx.store.config().harness;
|
|
6522
|
+
if (s2.source === void 0) {
|
|
6523
|
+
try {
|
|
6524
|
+
s2.source = sourceFor(s2.cwd, ctx.store.root, contextFor(v2, ctx.store.config())) ?? "";
|
|
6525
|
+
} catch {
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6203
6528
|
if (ctx.input.transcript_path) s2.transcriptPath = ctx.input.transcript_path;
|
|
6204
6529
|
patch(s2);
|
|
6205
6530
|
});
|
|
@@ -6246,10 +6571,10 @@ async function keepModCurrent(ctx, config) {
|
|
|
6246
6571
|
const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
6247
6572
|
const current = version2();
|
|
6248
6573
|
if (!modSource2() || installedModVersion2() === current) return;
|
|
6249
|
-
const marker =
|
|
6250
|
-
if (
|
|
6251
|
-
|
|
6252
|
-
|
|
6574
|
+
const marker = path13.join(modHome2(), `.attempted-${current}`);
|
|
6575
|
+
if (fs18.existsSync(marker)) return;
|
|
6576
|
+
fs18.mkdirSync(modHome2(), { recursive: true });
|
|
6577
|
+
fs18.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
|
|
6253
6578
|
ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
|
|
6254
6579
|
} catch {
|
|
6255
6580
|
}
|
|
@@ -6353,7 +6678,7 @@ function liveUpdate(v2, ctx, session) {
|
|
|
6353
6678
|
}
|
|
6354
6679
|
const config = ctx.store.config();
|
|
6355
6680
|
const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
|
|
6356
|
-
const paths = deployed ? deployPaths() : null;
|
|
6681
|
+
const paths = deployed ? deployPaths(process.env, config.orgId) : null;
|
|
6357
6682
|
const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
|
|
6358
6683
|
const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
|
|
6359
6684
|
const { text: text2, next } = changesSince(session.live, bundle, skillPath);
|
|
@@ -6378,13 +6703,13 @@ function skillHint(sec, others) {
|
|
|
6378
6703
|
const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
|
|
6379
6704
|
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}`;
|
|
6380
6705
|
}
|
|
6381
|
-
async function remoteAuto(ctx, prompt, exclude) {
|
|
6706
|
+
async function remoteAuto(ctx, prompt, exclude, source) {
|
|
6382
6707
|
const config = ctx.store.config();
|
|
6383
6708
|
if (!canCall(config) || !config.recall.useMothership) return null;
|
|
6384
6709
|
const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
|
|
6385
6710
|
if (budget < 300) return null;
|
|
6386
6711
|
try {
|
|
6387
|
-
const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude }, { timeoutMs: budget });
|
|
6712
|
+
const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
|
|
6388
6713
|
return res.available ? res : null;
|
|
6389
6714
|
} catch {
|
|
6390
6715
|
return null;
|
|
@@ -6439,7 +6764,7 @@ async function onPrompt(ctx) {
|
|
|
6439
6764
|
return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
|
|
6440
6765
|
}
|
|
6441
6766
|
const res = auto(view.sections, view.reader, prompt, { exclude: injected });
|
|
6442
|
-
const remote = await remoteAuto(ctx, prompt, injected);
|
|
6767
|
+
const remote = await remoteAuto(ctx, prompt, injected, session.source);
|
|
6443
6768
|
if (remote) {
|
|
6444
6769
|
const local = (id) => view.sections.find((s) => s.id === id);
|
|
6445
6770
|
if (remote.section) {
|
|
@@ -6591,6 +6916,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6591
6916
|
const v2 = new V2Store(store);
|
|
6592
6917
|
const config = store.config();
|
|
6593
6918
|
const view = localView(v2, config, session.cwd, { index: false });
|
|
6919
|
+
const source = sessionSource(store, session, view.ctx);
|
|
6594
6920
|
const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
|
|
6595
6921
|
const outcome = v2.readOutcome(session.sessionId);
|
|
6596
6922
|
const turns = scan.facts.turns.length;
|
|
@@ -6630,6 +6956,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6630
6956
|
...outcome ? { outcome } : {},
|
|
6631
6957
|
...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
|
|
6632
6958
|
cliVersion: version(),
|
|
6959
|
+
...source ? { source } : {},
|
|
6633
6960
|
uploaded: false,
|
|
6634
6961
|
...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
|
|
6635
6962
|
...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
|
|
@@ -6637,6 +6964,14 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6637
6964
|
};
|
|
6638
6965
|
return { episode, gate, scan };
|
|
6639
6966
|
}
|
|
6967
|
+
function sessionSource(store, session, ctx) {
|
|
6968
|
+
if (session.source !== void 0) return session.source || void 0;
|
|
6969
|
+
try {
|
|
6970
|
+
return sourceFor(session.cwd, store.root, ctx);
|
|
6971
|
+
} catch {
|
|
6972
|
+
return void 0;
|
|
6973
|
+
}
|
|
6974
|
+
}
|
|
6640
6975
|
function checkpointDue(session, scan, now) {
|
|
6641
6976
|
const since = Date.parse(session.checkpointAt ?? session.startedAt);
|
|
6642
6977
|
if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
|
|
@@ -6751,6 +7086,7 @@ var init_hooks = __esm({
|
|
|
6751
7086
|
init_signals();
|
|
6752
7087
|
init_store2();
|
|
6753
7088
|
init_ui();
|
|
7089
|
+
init_source();
|
|
6754
7090
|
init_view();
|
|
6755
7091
|
NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
|
|
6756
7092
|
NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
@@ -6765,6 +7101,8 @@ var init_hooks = __esm({
|
|
|
6765
7101
|
});
|
|
6766
7102
|
|
|
6767
7103
|
// src/hooks.ts
|
|
7104
|
+
import fs19 from "node:fs";
|
|
7105
|
+
import path14 from "node:path";
|
|
6768
7106
|
function normalizeEvent(raw) {
|
|
6769
7107
|
return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
|
|
6770
7108
|
}
|
|
@@ -6808,6 +7146,8 @@ async function handleHook(rawEvent, input, opts = {}) {
|
|
|
6808
7146
|
const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
|
|
6809
7147
|
if (!store) return {};
|
|
6810
7148
|
const config = store.config();
|
|
7149
|
+
if (!cwdInPool(config, cwd, store.root)) return {};
|
|
7150
|
+
if (deferToProject(cwd)) return {};
|
|
6811
7151
|
const budgetMs = Math.min(opts.budgetMs ?? config.recall.hookBudgetMs ?? HARD_BUDGET_MS, HARD_BUDGET_MS);
|
|
6812
7152
|
const deadline = Date.now() + budgetMs;
|
|
6813
7153
|
try {
|
|
@@ -6828,6 +7168,24 @@ async function handleHook(rawEvent, input, opts = {}) {
|
|
|
6828
7168
|
return {};
|
|
6829
7169
|
}
|
|
6830
7170
|
}
|
|
7171
|
+
function deferToProject(cwd, env = process.env) {
|
|
7172
|
+
if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
|
|
7173
|
+
let dir = path14.resolve(cwd);
|
|
7174
|
+
for (let i = 0; i < 40; i++) {
|
|
7175
|
+
for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
|
|
7176
|
+
try {
|
|
7177
|
+
const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
|
|
7178
|
+
if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
|
|
7179
|
+
} catch {
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
if (fs19.existsSync(path14.join(dir, ".git"))) return false;
|
|
7183
|
+
const up = path14.dirname(dir);
|
|
7184
|
+
if (up === dir) return false;
|
|
7185
|
+
dir = up;
|
|
7186
|
+
}
|
|
7187
|
+
return false;
|
|
7188
|
+
}
|
|
6831
7189
|
async function hookCommand(rawEvent, opts = {}) {
|
|
6832
7190
|
const write = opts.write ?? ((s) => process.stdout.write(s));
|
|
6833
7191
|
let output = {};
|
|
@@ -6850,6 +7208,7 @@ var init_hooks2 = __esm({
|
|
|
6850
7208
|
init_detach();
|
|
6851
7209
|
init_store();
|
|
6852
7210
|
init_hooks();
|
|
7211
|
+
init_source();
|
|
6853
7212
|
HARD_BUDGET_MS = 4e3;
|
|
6854
7213
|
EVENT_NAMES = {
|
|
6855
7214
|
"session-start": "session-start",
|
|
@@ -6869,32 +7228,32 @@ var init_hooks2 = __esm({
|
|
|
6869
7228
|
});
|
|
6870
7229
|
|
|
6871
7230
|
// src/project-hooks.ts
|
|
6872
|
-
import
|
|
6873
|
-
import
|
|
6874
|
-
import { execFileSync as
|
|
7231
|
+
import fs20 from "node:fs";
|
|
7232
|
+
import path15 from "node:path";
|
|
7233
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
6875
7234
|
function codexProjectHooks(cwd) {
|
|
6876
|
-
const legacyFile =
|
|
6877
|
-
const
|
|
7235
|
+
const legacyFile = path15.join(cwd, ".codex", "hooks.json");
|
|
7236
|
+
const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
|
|
6878
7237
|
encoding: "utf8",
|
|
6879
7238
|
stdio: ["ignore", "pipe", "ignore"],
|
|
6880
7239
|
timeout: 2e3
|
|
6881
7240
|
}).trim();
|
|
6882
7241
|
let root, common, directory;
|
|
6883
7242
|
try {
|
|
6884
|
-
root =
|
|
6885
|
-
common =
|
|
6886
|
-
directory =
|
|
7243
|
+
root = git5("--show-toplevel");
|
|
7244
|
+
common = git5("--path-format=absolute", "--git-common-dir");
|
|
7245
|
+
directory = git5("--absolute-git-dir");
|
|
6887
7246
|
} catch {
|
|
6888
7247
|
return { file: legacyFile, shared: false, legacyFile };
|
|
6889
7248
|
}
|
|
6890
|
-
const linked =
|
|
7249
|
+
const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
|
|
6891
7250
|
if (linked) {
|
|
6892
|
-
if (
|
|
7251
|
+
if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
|
|
6893
7252
|
throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
|
|
6894
7253
|
}
|
|
6895
|
-
root =
|
|
7254
|
+
root = path15.dirname(common);
|
|
6896
7255
|
}
|
|
6897
|
-
const file =
|
|
7256
|
+
const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
|
|
6898
7257
|
return { file, shared: linked, legacyFile };
|
|
6899
7258
|
}
|
|
6900
7259
|
var init_project_hooks = __esm({
|
|
@@ -6917,6 +7276,7 @@ __export(init_exports, {
|
|
|
6917
7276
|
installLearnCommand: () => installLearnCommand,
|
|
6918
7277
|
installMcpConfig: () => installMcpConfig,
|
|
6919
7278
|
installPiExtension: () => installPiExtension,
|
|
7279
|
+
installUserMcp: () => installUserMcp,
|
|
6920
7280
|
isBunshinEntry: () => isBunshinEntry,
|
|
6921
7281
|
mcpLauncher: () => mcpLauncher,
|
|
6922
7282
|
mergeClaudeSettings: () => mergeClaudeSettings,
|
|
@@ -6924,8 +7284,9 @@ __export(init_exports, {
|
|
|
6924
7284
|
removeBunshinHooks: () => removeBunshinHooks,
|
|
6925
7285
|
runInit: () => runInit
|
|
6926
7286
|
});
|
|
6927
|
-
import
|
|
6928
|
-
import
|
|
7287
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7288
|
+
import fs21 from "node:fs";
|
|
7289
|
+
import path16 from "node:path";
|
|
6929
7290
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
6930
7291
|
function isBunshinEntry(entry) {
|
|
6931
7292
|
return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
|
|
@@ -6950,13 +7311,13 @@ function removeBunshinHooks(settings) {
|
|
|
6950
7311
|
else delete out.hooks;
|
|
6951
7312
|
return out;
|
|
6952
7313
|
}
|
|
6953
|
-
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code" } = {}) {
|
|
7314
|
+
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
|
|
6954
7315
|
const base = removeBunshinHooks(settings);
|
|
6955
7316
|
const hooks = { ...base.hooks ?? {} };
|
|
6956
7317
|
for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
|
|
6957
7318
|
const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
6958
7319
|
const hook = bunshinHookEntry(arg, timeout, bin);
|
|
6959
|
-
hook.command =
|
|
7320
|
+
hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
|
|
6960
7321
|
if (harness === "codex") delete hook._bunshin;
|
|
6961
7322
|
groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
|
|
6962
7323
|
hooks[event] = groups;
|
|
@@ -6970,16 +7331,16 @@ function countBunshinHooks(settings) {
|
|
|
6970
7331
|
);
|
|
6971
7332
|
}
|
|
6972
7333
|
function claudeSettingsPath() {
|
|
6973
|
-
return
|
|
7334
|
+
return path16.join(homeDir(), ".claude", "settings.json");
|
|
6974
7335
|
}
|
|
6975
7336
|
function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
6976
|
-
const file = scope === "global" ?
|
|
7337
|
+
const file = scope === "global" ? path16.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path16.join(cwd, ".pi/extensions/bunshin.js");
|
|
6977
7338
|
const marker = "// Managed by Bunshin setup.";
|
|
6978
7339
|
try {
|
|
6979
|
-
const previous = fileExists(file) ?
|
|
7340
|
+
const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
|
|
6980
7341
|
if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
|
|
6981
7342
|
if (remove) {
|
|
6982
|
-
if (previous)
|
|
7343
|
+
if (previous) fs21.unlinkSync(file);
|
|
6983
7344
|
return { file, installed: 0, changed: !!previous };
|
|
6984
7345
|
}
|
|
6985
7346
|
const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
|
|
@@ -6988,24 +7349,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
|
6988
7349
|
export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
6989
7350
|
`;
|
|
6990
7351
|
if (text2 !== previous) {
|
|
6991
|
-
ensureDir(
|
|
6992
|
-
|
|
7352
|
+
ensureDir(path16.dirname(file));
|
|
7353
|
+
fs21.writeFileSync(file, text2);
|
|
6993
7354
|
}
|
|
6994
7355
|
return { file, installed: 1, changed: text2 !== previous };
|
|
6995
7356
|
} catch (error) {
|
|
6996
7357
|
return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
6997
7358
|
}
|
|
6998
7359
|
}
|
|
6999
|
-
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code" } = {}) {
|
|
7360
|
+
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
|
|
7000
7361
|
try {
|
|
7001
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7002
|
-
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness });
|
|
7362
|
+
const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
|
|
7363
|
+
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
|
|
7003
7364
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7004
7365
|
if (changed) {
|
|
7005
|
-
ensureDir(
|
|
7366
|
+
ensureDir(path16.dirname(file));
|
|
7006
7367
|
if (fileExists(file)) {
|
|
7007
7368
|
try {
|
|
7008
|
-
|
|
7369
|
+
fs21.copyFileSync(file, `${file}.bunshin-backup`);
|
|
7009
7370
|
} catch {
|
|
7010
7371
|
}
|
|
7011
7372
|
}
|
|
@@ -7020,23 +7381,23 @@ function mcpLauncher(bin) {
|
|
|
7020
7381
|
if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
|
|
7021
7382
|
let script = process.argv[1] ?? "";
|
|
7022
7383
|
try {
|
|
7023
|
-
script =
|
|
7384
|
+
script = fs21.realpathSync(script);
|
|
7024
7385
|
} catch {
|
|
7025
7386
|
}
|
|
7026
7387
|
return { command: process.execPath, args: [script, "mcp"] };
|
|
7027
7388
|
}
|
|
7028
7389
|
function installLearnCommand({ file, remove = false }) {
|
|
7029
7390
|
try {
|
|
7030
|
-
const current = fileExists(file) ?
|
|
7391
|
+
const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
|
|
7031
7392
|
const ours = current === null || current === LEARN_COMMAND;
|
|
7032
7393
|
if (remove) {
|
|
7033
|
-
if (current !== null && ours)
|
|
7394
|
+
if (current !== null && ours) fs21.unlinkSync(file);
|
|
7034
7395
|
return { file, installed: 0, changed: current !== null && ours };
|
|
7035
7396
|
}
|
|
7036
7397
|
if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
|
|
7037
7398
|
if (current === null) {
|
|
7038
|
-
|
|
7039
|
-
|
|
7399
|
+
fs21.mkdirSync(path16.dirname(file), { recursive: true });
|
|
7400
|
+
fs21.writeFileSync(file, LEARN_COMMAND);
|
|
7040
7401
|
}
|
|
7041
7402
|
return { file, installed: 1, changed: current === null };
|
|
7042
7403
|
} catch (err) {
|
|
@@ -7045,7 +7406,7 @@ function installLearnCommand({ file, remove = false }) {
|
|
|
7045
7406
|
}
|
|
7046
7407
|
function installMcpConfig({ file, launcher, remove = false }) {
|
|
7047
7408
|
try {
|
|
7048
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7409
|
+
const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
|
|
7049
7410
|
const servers = { ...before.mcpServers ?? {} };
|
|
7050
7411
|
if (remove) delete servers.bunshin;
|
|
7051
7412
|
else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
|
|
@@ -7053,7 +7414,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7053
7414
|
if (!Object.keys(servers).length) delete after.mcpServers;
|
|
7054
7415
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7055
7416
|
if (changed) {
|
|
7056
|
-
if (!Object.keys(after).length && fileExists(file))
|
|
7417
|
+
if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
|
|
7057
7418
|
else if (Object.keys(after).length) writeJsonAtomic(file, after);
|
|
7058
7419
|
}
|
|
7059
7420
|
return { file, installed: servers.bunshin ? 1 : 0, changed };
|
|
@@ -7061,25 +7422,49 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7061
7422
|
return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
|
|
7062
7423
|
}
|
|
7063
7424
|
}
|
|
7425
|
+
function installUserMcp({ launcher, remove = false, claude = "claude" }) {
|
|
7426
|
+
const file = path16.join(homeDir(), ".claude.json");
|
|
7427
|
+
const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
|
|
7428
|
+
try {
|
|
7429
|
+
let had = false;
|
|
7430
|
+
try {
|
|
7431
|
+
run2(["mcp", "get", "bunshin"]);
|
|
7432
|
+
had = true;
|
|
7433
|
+
} catch {
|
|
7434
|
+
}
|
|
7435
|
+
if (had) {
|
|
7436
|
+
try {
|
|
7437
|
+
run2(["mcp", "remove", "bunshin", "-s", "user"]);
|
|
7438
|
+
} catch {
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
if (remove) return { file, installed: 0, changed: had };
|
|
7442
|
+
run2(["mcp", "add-json", "bunshin", JSON.stringify({ type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } }), "-s", "user"]);
|
|
7443
|
+
return { file, installed: 1, changed: true };
|
|
7444
|
+
} catch (err) {
|
|
7445
|
+
return { file, installed: 0, changed: false, error: err instanceof Error ? err.message.split("\n")[0] : String(err) };
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7064
7448
|
function runInit(opts = {}) {
|
|
7065
7449
|
const cwd = opts.cwd ?? process.cwd();
|
|
7066
7450
|
const warnings = [];
|
|
7067
7451
|
const scope = opts.scope ?? "repo";
|
|
7068
|
-
const root = scope === "global" ?
|
|
7069
|
-
if (
|
|
7452
|
+
const root = scope === "global" ? path16.join(homeDir(), ".bunshin") : path16.join(cwd, ".bunshin");
|
|
7453
|
+
if (fs21.existsSync(root) && !new Store(root).trusted && fs21.readdirSync(root).length) {
|
|
7070
7454
|
const aside = `${root}.untrusted-${Date.now()}`;
|
|
7071
|
-
|
|
7455
|
+
fs21.renameSync(root, aside);
|
|
7072
7456
|
warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
|
|
7073
7457
|
}
|
|
7074
|
-
const existed = fileExists(
|
|
7458
|
+
const existed = fileExists(path16.join(root, "config.json"));
|
|
7075
7459
|
const store = new Store(root);
|
|
7076
7460
|
store.ensureLayout();
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
const
|
|
7461
|
+
fs21.chmodSync(root, 448);
|
|
7462
|
+
const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
|
|
7463
|
+
if (scope === "repo" && (hookScope === "project" || fileExists(path16.join(cwd, ".git")))) {
|
|
7464
|
+
const ignore = path16.join(cwd, ".gitignore");
|
|
7465
|
+
const current = fileExists(ignore) ? fs21.readFileSync(ignore, "utf8") : "";
|
|
7081
7466
|
if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
|
|
7082
|
-
|
|
7467
|
+
fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
|
|
7083
7468
|
`);
|
|
7084
7469
|
}
|
|
7085
7470
|
}
|
|
@@ -7087,6 +7472,7 @@ function runInit(opts = {}) {
|
|
|
7087
7472
|
const config = {
|
|
7088
7473
|
...previous,
|
|
7089
7474
|
scope,
|
|
7475
|
+
hooks: hookScope,
|
|
7090
7476
|
orgId: opts.orgId ?? previous.orgId,
|
|
7091
7477
|
userId: opts.userId ?? previous.userId,
|
|
7092
7478
|
role: opts.role ?? previous.role,
|
|
@@ -7103,18 +7489,22 @@ function runInit(opts = {}) {
|
|
|
7103
7489
|
if (!fileExists(store.indexFile)) store.rebuildIndex();
|
|
7104
7490
|
let hooks;
|
|
7105
7491
|
if ((opts.removeHooks || !opts.noHooks) && config.harness === "pi") {
|
|
7106
|
-
hooks = installPiExtension(cwd, opts.removeHooks,
|
|
7492
|
+
hooks = installPiExtension(cwd, opts.removeHooks, hookScope === "user" ? "global" : "repo");
|
|
7107
7493
|
if (hooks.error) warnings.push(hooks.error);
|
|
7108
7494
|
}
|
|
7109
7495
|
if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
|
|
7110
|
-
const projectHooks = config.harness === "codex" &&
|
|
7111
|
-
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ??
|
|
7112
|
-
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness });
|
|
7496
|
+
const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
|
|
7497
|
+
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path16.join(process.env.CODEX_HOME ?? path16.join(homeDir(), ".codex"), "hooks.json") : hookScope === "project" ? path16.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
|
|
7498
|
+
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
|
|
7499
|
+
if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
|
|
7500
|
+
const local = path16.join(cwd, ".claude", "settings.local.json");
|
|
7501
|
+
if (fileExists(local) && path16.resolve(local) !== path16.resolve(file)) installClaudeHooks({ file: local, remove: true });
|
|
7502
|
+
}
|
|
7113
7503
|
if (projectHooks?.shared) {
|
|
7114
7504
|
hooks.sharedWithWorktrees = true;
|
|
7115
|
-
if (!hooks.error && !opts.removeHooks) ensureDir(
|
|
7505
|
+
if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
|
|
7116
7506
|
}
|
|
7117
|
-
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) &&
|
|
7507
|
+
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
|
|
7118
7508
|
const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
|
|
7119
7509
|
if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
|
|
7120
7510
|
}
|
|
@@ -7122,10 +7512,11 @@ function runInit(opts = {}) {
|
|
|
7122
7512
|
}
|
|
7123
7513
|
let mcp;
|
|
7124
7514
|
let learnCommand;
|
|
7125
|
-
if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code"
|
|
7126
|
-
|
|
7515
|
+
if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
|
|
7516
|
+
const user = hookScope === "user";
|
|
7517
|
+
mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file: path16.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
|
|
7127
7518
|
if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
|
|
7128
|
-
learnCommand = installLearnCommand({ file:
|
|
7519
|
+
learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
|
|
7129
7520
|
if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
|
|
7130
7521
|
}
|
|
7131
7522
|
if (!resolveBackend(config)) {
|
|
@@ -7260,6 +7651,7 @@ __export(commands_exports, {
|
|
|
7260
7651
|
cmdReflectFork: () => cmdReflectFork,
|
|
7261
7652
|
cmdReview: () => cmdReview,
|
|
7262
7653
|
cmdReviewDecide: () => cmdReviewDecide,
|
|
7654
|
+
cmdSources: () => cmdSources,
|
|
7263
7655
|
cmdSuggest: () => cmdSuggest,
|
|
7264
7656
|
cmdSync: () => cmdSync,
|
|
7265
7657
|
robotAttrsFromFlags: () => robotAttrsFromFlags,
|
|
@@ -7267,9 +7659,9 @@ __export(commands_exports, {
|
|
|
7267
7659
|
setupRobot: () => setupRobot,
|
|
7268
7660
|
v2Status: () => v2Status
|
|
7269
7661
|
});
|
|
7270
|
-
import
|
|
7271
|
-
import
|
|
7272
|
-
import { createHash as
|
|
7662
|
+
import fs22 from "node:fs";
|
|
7663
|
+
import path17 from "node:path";
|
|
7664
|
+
import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
|
|
7273
7665
|
function cmdReview(store, args, cwd, out, err) {
|
|
7274
7666
|
const v2 = new V2Store(store);
|
|
7275
7667
|
const sub = args._[1] ?? "list";
|
|
@@ -7438,11 +7830,11 @@ function cmdSuggest(store, args, cwd, out, err) {
|
|
|
7438
7830
|
err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
|
|
7439
7831
|
return 1;
|
|
7440
7832
|
}
|
|
7441
|
-
const harnessDir =
|
|
7442
|
-
const file = str3(args.flags.file) ?
|
|
7833
|
+
const harnessDir = path17.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
|
|
7834
|
+
const file = str3(args.flags.file) ? path17.resolve(cwd, str3(args.flags.file)) : path17.join(harnessDir, rel);
|
|
7443
7835
|
let content;
|
|
7444
7836
|
try {
|
|
7445
|
-
content =
|
|
7837
|
+
content = fs22.readFileSync(file, "utf8");
|
|
7446
7838
|
} catch {
|
|
7447
7839
|
err(`bunshin suggest: cannot read ${file}
|
|
7448
7840
|
`);
|
|
@@ -7563,15 +7955,15 @@ async function cmdQuestions(store, args, out, err) {
|
|
|
7563
7955
|
return 0;
|
|
7564
7956
|
}
|
|
7565
7957
|
function saveCredential(storeRoot, apiUrl, apiKey, id) {
|
|
7566
|
-
const dir =
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
const file =
|
|
7958
|
+
const dir = path17.join(homeDir(), ".config/bunshin/credentials");
|
|
7959
|
+
fs22.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
7960
|
+
fs22.chmodSync(dir, 448);
|
|
7961
|
+
const file = path17.join(dir, `${createHash5("sha256").update(`${storeRoot}
|
|
7570
7962
|
${apiUrl}
|
|
7571
7963
|
${id}`).digest("hex")}.json`);
|
|
7572
7964
|
const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
|
|
7573
|
-
|
|
7574
|
-
|
|
7965
|
+
fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
7966
|
+
fs22.renameSync(tmp, file);
|
|
7575
7967
|
return file;
|
|
7576
7968
|
}
|
|
7577
7969
|
async function setupRobot(opts) {
|
|
@@ -7579,10 +7971,10 @@ async function setupRobot(opts) {
|
|
|
7579
7971
|
const existing = Store.discover(opts.cwd);
|
|
7580
7972
|
let store;
|
|
7581
7973
|
const harness = opts.harness ?? "other";
|
|
7582
|
-
if (existing &&
|
|
7974
|
+
if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
|
|
7583
7975
|
else {
|
|
7584
7976
|
runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
|
|
7585
|
-
store = new Store(
|
|
7977
|
+
store = new Store(path17.join(opts.cwd, ".bunshin"));
|
|
7586
7978
|
}
|
|
7587
7979
|
let config = store.config();
|
|
7588
7980
|
const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
|
|
@@ -7645,8 +8037,10 @@ function v2Status(store, cwd) {
|
|
|
7645
8037
|
}
|
|
7646
8038
|
const view = localView(v2, config, cwd, { index: false });
|
|
7647
8039
|
const sync = v2.readSyncState();
|
|
8040
|
+
const pooled = cwdInPool(config, cwd, store.root);
|
|
7648
8041
|
return {
|
|
7649
8042
|
context: { id: view.ctx.id, kind: view.ctx.kind, layers: view.layers },
|
|
8043
|
+
source: { id: pooled ? sourceFor(cwd, store.root, view.ctx) ?? null : null, inPool: pooled, folder: topFolder(cwd, store.root) || null },
|
|
7650
8044
|
bundle: bundle ? { sections: bundle.sections.length, generatedAt: bundle.generatedAt, release: bundle.release?.version ?? null } : null,
|
|
7651
8045
|
repo: { name: repo, sections: repoSections },
|
|
7652
8046
|
overlays: v2.readOverlays().length,
|
|
@@ -7657,6 +8051,46 @@ function v2Status(store, cwd) {
|
|
|
7657
8051
|
session: v2.readCurrent()?.sessionId ?? null
|
|
7658
8052
|
};
|
|
7659
8053
|
}
|
|
8054
|
+
function cmdSources(store, args, cwd, out, err) {
|
|
8055
|
+
const config = store.config();
|
|
8056
|
+
const action = args._[1];
|
|
8057
|
+
const names = args._.slice(2).flatMap((x) => x.split(",")).map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
|
|
8058
|
+
if (action) {
|
|
8059
|
+
if (action !== "all" && action !== "only" && action !== "include" && action !== "exclude") {
|
|
8060
|
+
err("usage: bunshin sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>]\n");
|
|
8061
|
+
return 1;
|
|
8062
|
+
}
|
|
8063
|
+
if (action !== "all" && !names.length) {
|
|
8064
|
+
err(`bunshin sources ${action}: name at least one folder
|
|
8065
|
+
`);
|
|
8066
|
+
return 1;
|
|
8067
|
+
}
|
|
8068
|
+
const known = new Set(listFolders(store.root, void 0).map((f) => f.folder));
|
|
8069
|
+
const unknown = names.filter((n) => !known.has(n));
|
|
8070
|
+
if (unknown.length) {
|
|
8071
|
+
err(`bunshin sources: not a folder in ${storeHome(store.root)}: ${unknown.join(", ")}
|
|
8072
|
+
`);
|
|
8073
|
+
return 1;
|
|
8074
|
+
}
|
|
8075
|
+
const next = select(config.sources, action, names);
|
|
8076
|
+
const { sources: _drop, ...rest } = config;
|
|
8077
|
+
void _drop;
|
|
8078
|
+
store.saveConfig(next ? { ...rest, sources: next } : rest);
|
|
8079
|
+
}
|
|
8080
|
+
const selection = store.config().sources;
|
|
8081
|
+
const folders = listFolders(store.root, selection, { sources: true });
|
|
8082
|
+
if (bool2(args.flags.json)) {
|
|
8083
|
+
out(JSON.stringify({ home: storeHome(store.root), selection: selection ?? null, folders }, null, 2) + "\n");
|
|
8084
|
+
return 0;
|
|
8085
|
+
}
|
|
8086
|
+
const here = topFolder(cwd, store.root);
|
|
8087
|
+
out(`Learning pool of ${storeHome(store.root)}: ${!selection ? "every folder (new ones join)" : selection.only ? "only the folders marked in (new ones stay out)" : "every folder but those marked out (new ones join)"}
|
|
8088
|
+
`);
|
|
8089
|
+
if (!folders.length) out(" (no folders here yet)\n");
|
|
8090
|
+
for (const f of folders) out(` ${f.included ? "in " : "out"} ${f.folder}${f.source ? ` (${f.source})` : ""}${f.folder === here ? " \u2190 here" : ""}
|
|
8091
|
+
`);
|
|
8092
|
+
return 0;
|
|
8093
|
+
}
|
|
7660
8094
|
var str3, bool2, list;
|
|
7661
8095
|
var init_commands = __esm({
|
|
7662
8096
|
"src/v2/commands.ts"() {
|
|
@@ -7673,6 +8107,7 @@ var init_commands = __esm({
|
|
|
7673
8107
|
init_repo();
|
|
7674
8108
|
init_store2();
|
|
7675
8109
|
init_sync();
|
|
8110
|
+
init_source();
|
|
7676
8111
|
init_view();
|
|
7677
8112
|
str3 = (v) => typeof v === "string" ? v : void 0;
|
|
7678
8113
|
bool2 = (v) => v === true || v === "true";
|
|
@@ -7685,6 +8120,7 @@ var setup_exports = {};
|
|
|
7685
8120
|
__export(setup_exports, {
|
|
7686
8121
|
HOSTED_API: () => HOSTED_API,
|
|
7687
8122
|
apiAddress: () => apiAddress,
|
|
8123
|
+
askSources: () => askSources,
|
|
7688
8124
|
connectionProfile: () => connectionProfile,
|
|
7689
8125
|
executable: () => executable,
|
|
7690
8126
|
hookBinary: () => hookBinary,
|
|
@@ -7692,16 +8128,16 @@ __export(setup_exports, {
|
|
|
7692
8128
|
setupProject: () => setupProject,
|
|
7693
8129
|
shellQuote: () => shellQuote
|
|
7694
8130
|
});
|
|
7695
|
-
import
|
|
7696
|
-
import
|
|
7697
|
-
import { createHash as
|
|
8131
|
+
import fs23 from "node:fs";
|
|
8132
|
+
import path18 from "node:path";
|
|
8133
|
+
import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
|
|
7698
8134
|
function executable(name) {
|
|
7699
|
-
const directories = [...(process.env.PATH ?? "").split(
|
|
8135
|
+
const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
|
|
7700
8136
|
for (const directory of directories) {
|
|
7701
|
-
const file =
|
|
8137
|
+
const file = path18.resolve(directory, name);
|
|
7702
8138
|
try {
|
|
7703
|
-
|
|
7704
|
-
if (
|
|
8139
|
+
fs23.accessSync(file, fs23.constants.X_OK);
|
|
8140
|
+
if (fs23.statSync(file).isFile()) return file;
|
|
7705
8141
|
} catch {
|
|
7706
8142
|
}
|
|
7707
8143
|
}
|
|
@@ -7709,8 +8145,8 @@ function executable(name) {
|
|
|
7709
8145
|
}
|
|
7710
8146
|
function hookBinary(harness) {
|
|
7711
8147
|
const agent = executable(harness === "claude-code" ? "claude" : harness);
|
|
7712
|
-
const directories = [.../* @__PURE__ */ new Set([
|
|
7713
|
-
return `PATH=${shellQuote(directories.join(
|
|
8148
|
+
const directories = [.../* @__PURE__ */ new Set([path18.dirname(process.execPath), path18.join(homeDir(), ".local/bin"), ...agent ? [path18.dirname(agent)] : []])];
|
|
8149
|
+
return `PATH=${shellQuote(directories.join(path18.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs23.realpathSync(process.argv[1]))}`;
|
|
7714
8150
|
}
|
|
7715
8151
|
function apiAddress(raw) {
|
|
7716
8152
|
const url = new URL(raw);
|
|
@@ -7779,33 +8215,37 @@ async function setupProject(opts) {
|
|
|
7779
8215
|
const apiKey = opts.apiKey.trim();
|
|
7780
8216
|
if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
|
|
7781
8217
|
const profile = await connectionProfile(apiUrl, apiKey);
|
|
7782
|
-
const root =
|
|
8218
|
+
const root = path18.join(opts.cwd, ".bunshin");
|
|
7783
8219
|
const store = new Store(root);
|
|
7784
8220
|
const previous = store.config();
|
|
7785
8221
|
if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
|
|
7786
8222
|
throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
|
|
7787
8223
|
}
|
|
7788
|
-
const credentialDirectory =
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
const name =
|
|
8224
|
+
const credentialDirectory = path18.join(homeDir(), ".config/bunshin/credentials");
|
|
8225
|
+
fs23.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
|
|
8226
|
+
fs23.chmodSync(credentialDirectory, 448);
|
|
8227
|
+
const name = createHash6("sha256").update(`${root}
|
|
7792
8228
|
${apiUrl}
|
|
7793
8229
|
${profile.orgId}
|
|
7794
8230
|
${profile.userId}`).digest("hex");
|
|
7795
|
-
const credentialFile =
|
|
8231
|
+
const credentialFile = path18.join(credentialDirectory, `${name}.json`);
|
|
7796
8232
|
const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
|
|
7797
|
-
|
|
7798
|
-
|
|
8233
|
+
fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8234
|
+
fs23.renameSync(temporary, credentialFile);
|
|
7799
8235
|
const result = runInit({
|
|
7800
8236
|
cwd: opts.cwd,
|
|
7801
8237
|
harness: opts.harness,
|
|
7802
8238
|
apiUrl,
|
|
7803
8239
|
...profile,
|
|
7804
8240
|
noHooks: opts.noHooks,
|
|
7805
|
-
bin: opts.bin ?? hookBinary(opts.harness)
|
|
8241
|
+
bin: opts.bin ?? hookBinary(opts.harness),
|
|
8242
|
+
...opts.hooks ? { hooks: opts.hooks } : {}
|
|
7806
8243
|
});
|
|
8244
|
+
const { sources: kept, ...base } = result.config;
|
|
8245
|
+
const sources = opts.sources === void 0 ? kept : opts.sources ?? void 0;
|
|
7807
8246
|
store.saveConfig({
|
|
7808
|
-
...
|
|
8247
|
+
...base,
|
|
8248
|
+
...sources ? { sources } : {},
|
|
7809
8249
|
...profile,
|
|
7810
8250
|
apiKey: void 0,
|
|
7811
8251
|
credentialFile,
|
|
@@ -7816,6 +8256,39 @@ ${profile.userId}`).digest("hex");
|
|
|
7816
8256
|
trustStore(root);
|
|
7817
8257
|
return { ...result, profile, credentialFile };
|
|
7818
8258
|
}
|
|
8259
|
+
async function askSources(store, out) {
|
|
8260
|
+
const { listFolders: listFolders2, select: select2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
8261
|
+
const config = store.config();
|
|
8262
|
+
const folders = listFolders2(store.root, void 0);
|
|
8263
|
+
if (folders.length < 2) return;
|
|
8264
|
+
const readline2 = await import("node:readline/promises");
|
|
8265
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
8266
|
+
try {
|
|
8267
|
+
out(`This folder holds ${folders.length} folders:
|
|
8268
|
+
`);
|
|
8269
|
+
folders.forEach((f, i) => out(` ${String(i + 1).padStart(2)}. ${f.folder}
|
|
8270
|
+
`));
|
|
8271
|
+
const answer = (await rl.question("Learn from [a]ll of them, all [e]xcept some, or [o]nly some? [a] ")).trim().toLowerCase();
|
|
8272
|
+
const mode = answer.startsWith("e") || answer.startsWith("x") ? "exclude" : answer.startsWith("o") || answer.startsWith("p") ? "only" : "all";
|
|
8273
|
+
const { sources: _drop, ...rest } = config;
|
|
8274
|
+
void _drop;
|
|
8275
|
+
if (mode === "all") {
|
|
8276
|
+
store.saveConfig(rest);
|
|
8277
|
+
out("Learning from every folder here; new folders join automatically.\n");
|
|
8278
|
+
return;
|
|
8279
|
+
}
|
|
8280
|
+
const raw = await rl.question(mode === "exclude" ? "Folders to leave out (numbers or names, comma-separated): " : "Folders to learn from (numbers or names, comma-separated): ");
|
|
8281
|
+
const picked = [...new Set(raw.split(/[,\s]+/).map((x) => x.trim()).filter(Boolean).map((x) => /^\d+$/.test(x) ? folders[Number(x) - 1]?.folder : folders.find((f) => f.folder === x)?.folder).filter((x) => !!x))];
|
|
8282
|
+
const next = select2(void 0, mode, picked);
|
|
8283
|
+
store.saveConfig(next ? { ...rest, sources: next } : rest);
|
|
8284
|
+
if (mode === "exclude") out(picked.length ? `Learning from every folder but ${picked.join(", ")}; new folders join automatically.
|
|
8285
|
+
` : "Learning from every folder here; new folders join automatically.\n");
|
|
8286
|
+
else out(picked.length ? `Learning from ${picked.join(", ")} only; new folders stay out until you add them (bunshin sources include <folder>).
|
|
8287
|
+
` : "Nothing here takes part until you add a folder (bunshin sources include <folder>).\n");
|
|
8288
|
+
} finally {
|
|
8289
|
+
rl.close();
|
|
8290
|
+
}
|
|
8291
|
+
}
|
|
7819
8292
|
var shellQuote;
|
|
7820
8293
|
var init_setup = __esm({
|
|
7821
8294
|
"src/setup.ts"() {
|
|
@@ -7866,6 +8339,7 @@ async function handleMcp(msg, opts) {
|
|
|
7866
8339
|
const cwd = opts.cwd ?? process.cwd();
|
|
7867
8340
|
const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
|
|
7868
8341
|
if (!store) return reply(text("Bunshin is not set up in this project (run `bunshin setup`). Proceed with general knowledge.", true));
|
|
8342
|
+
if (!cwdInPool(store.config(), cwd, store.root)) return reply(text("This folder is not part of Bunshin's learning pool (`bunshin sources`). Proceed with general knowledge.", true));
|
|
7869
8343
|
try {
|
|
7870
8344
|
if (name === "bunshin_recall") {
|
|
7871
8345
|
const taskAbstract = typeof args.task_abstract === "string" ? args.task_abstract : "";
|
|
@@ -7929,6 +8403,7 @@ var init_mcp = __esm({
|
|
|
7929
8403
|
"src/v2/mcp.ts"() {
|
|
7930
8404
|
"use strict";
|
|
7931
8405
|
init_v22();
|
|
8406
|
+
init_source();
|
|
7932
8407
|
init_store();
|
|
7933
8408
|
init_library();
|
|
7934
8409
|
DEFAULT_PROTOCOL = "2025-06-18";
|
|
@@ -8106,9 +8581,9 @@ __export(sync_exports2, {
|
|
|
8106
8581
|
resolveConflict: () => resolveConflict,
|
|
8107
8582
|
runSync: () => runSync
|
|
8108
8583
|
});
|
|
8109
|
-
import { createHash as
|
|
8110
|
-
import
|
|
8111
|
-
import
|
|
8584
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8585
|
+
import path19 from "node:path";
|
|
8586
|
+
import fs24 from "node:fs";
|
|
8112
8587
|
async function api(config, method, route, body, timeoutMs = 1e4) {
|
|
8113
8588
|
const ctrl = new AbortController();
|
|
8114
8589
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
@@ -8146,22 +8621,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
|
|
|
8146
8621
|
}
|
|
8147
8622
|
}
|
|
8148
8623
|
async function runSync(store) {
|
|
8149
|
-
const lock =
|
|
8624
|
+
const lock = path19.join(store.root, "sync.lock");
|
|
8150
8625
|
const deadline = Date.now() + 12e4;
|
|
8151
8626
|
for (; ; ) {
|
|
8152
8627
|
try {
|
|
8153
|
-
|
|
8628
|
+
fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
8154
8629
|
break;
|
|
8155
8630
|
} catch (error) {
|
|
8156
8631
|
if (error.code !== "EEXIST") throw error;
|
|
8157
8632
|
try {
|
|
8158
|
-
const owner = Number(
|
|
8633
|
+
const owner = Number(fs24.readFileSync(lock, "utf8"));
|
|
8159
8634
|
if (Number.isInteger(owner) && owner > 0) {
|
|
8160
8635
|
try {
|
|
8161
8636
|
process.kill(owner, 0);
|
|
8162
8637
|
} catch (error2) {
|
|
8163
8638
|
if (error2.code === "ESRCH") {
|
|
8164
|
-
|
|
8639
|
+
fs24.unlinkSync(lock);
|
|
8165
8640
|
continue;
|
|
8166
8641
|
}
|
|
8167
8642
|
}
|
|
@@ -8180,7 +8655,7 @@ async function runSync(store) {
|
|
|
8180
8655
|
return await syncStore(store);
|
|
8181
8656
|
} finally {
|
|
8182
8657
|
try {
|
|
8183
|
-
|
|
8658
|
+
fs24.unlinkSync(lock);
|
|
8184
8659
|
} catch {
|
|
8185
8660
|
}
|
|
8186
8661
|
}
|
|
@@ -8208,9 +8683,9 @@ async function syncStore(store) {
|
|
|
8208
8683
|
result.errors.push(String(e instanceof Error ? e.message : e));
|
|
8209
8684
|
}
|
|
8210
8685
|
}
|
|
8211
|
-
const acknowledgementsFile =
|
|
8686
|
+
const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
|
|
8212
8687
|
const acknowledgements = readJson(acknowledgementsFile, {});
|
|
8213
|
-
const fingerprint2 = (trace) =>
|
|
8688
|
+
const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
|
|
8214
8689
|
const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
|
|
8215
8690
|
for (const t of traces) {
|
|
8216
8691
|
try {
|
|
@@ -8463,11 +8938,11 @@ __export(demo_exports, {
|
|
|
8463
8938
|
runDemo: () => runDemo
|
|
8464
8939
|
});
|
|
8465
8940
|
import assert from "node:assert/strict";
|
|
8466
|
-
import
|
|
8941
|
+
import fs25 from "node:fs";
|
|
8467
8942
|
import os4 from "node:os";
|
|
8468
|
-
import
|
|
8943
|
+
import path20 from "node:path";
|
|
8469
8944
|
async function runDemo(write) {
|
|
8470
|
-
const root =
|
|
8945
|
+
const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
|
|
8471
8946
|
try {
|
|
8472
8947
|
const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
|
|
8473
8948
|
const { lessons, constraint, workflow } = aprScenario();
|
|
@@ -8484,7 +8959,7 @@ Task: ${APR_TASK}
|
|
|
8484
8959
|
write(result.text);
|
|
8485
8960
|
write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
|
|
8486
8961
|
} finally {
|
|
8487
|
-
|
|
8962
|
+
fs25.rmSync(root, { recursive: true, force: true });
|
|
8488
8963
|
}
|
|
8489
8964
|
}
|
|
8490
8965
|
var init_demo = __esm({
|
|
@@ -8497,21 +8972,21 @@ var init_demo = __esm({
|
|
|
8497
8972
|
});
|
|
8498
8973
|
|
|
8499
8974
|
// src/v2/gitremote.ts
|
|
8500
|
-
import
|
|
8501
|
-
import
|
|
8502
|
-
import { createHash as
|
|
8975
|
+
import fs26 from "node:fs";
|
|
8976
|
+
import path21 from "node:path";
|
|
8977
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8503
8978
|
function saveGitCredential(apiUrl, apiKey) {
|
|
8504
8979
|
const file = gitCredentialFile();
|
|
8505
|
-
|
|
8980
|
+
fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
|
|
8506
8981
|
let all = {};
|
|
8507
8982
|
try {
|
|
8508
|
-
all = JSON.parse(
|
|
8983
|
+
all = JSON.parse(fs26.readFileSync(file, "utf8"));
|
|
8509
8984
|
} catch {
|
|
8510
8985
|
}
|
|
8511
8986
|
all[apiUrl.replace(/\/$/, "")] = apiKey;
|
|
8512
8987
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8513
|
-
|
|
8514
|
-
|
|
8988
|
+
fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
8989
|
+
fs26.renameSync(tmp, file);
|
|
8515
8990
|
}
|
|
8516
8991
|
function resolveGitKey(apiUrl) {
|
|
8517
8992
|
const base = apiUrl.replace(/\/$/, "");
|
|
@@ -8519,20 +8994,20 @@ function resolveGitKey(apiUrl) {
|
|
|
8519
8994
|
const origin = urlOrigin(base);
|
|
8520
8995
|
if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
|
|
8521
8996
|
try {
|
|
8522
|
-
const all = JSON.parse(
|
|
8997
|
+
const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
|
|
8523
8998
|
if (all[base]) return all[base];
|
|
8524
8999
|
} catch {
|
|
8525
9000
|
}
|
|
8526
|
-
const dir =
|
|
9001
|
+
const dir = path21.join(homeDir(), ".config/bunshin/credentials");
|
|
8527
9002
|
let files = [];
|
|
8528
9003
|
try {
|
|
8529
|
-
files =
|
|
9004
|
+
files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
8530
9005
|
} catch {
|
|
8531
9006
|
return void 0;
|
|
8532
9007
|
}
|
|
8533
9008
|
for (const f of files.sort()) {
|
|
8534
9009
|
try {
|
|
8535
|
-
const c = JSON.parse(
|
|
9010
|
+
const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
|
|
8536
9011
|
if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
|
|
8537
9012
|
} catch {
|
|
8538
9013
|
}
|
|
@@ -8540,7 +9015,7 @@ function resolveGitKey(apiUrl) {
|
|
|
8540
9015
|
return void 0;
|
|
8541
9016
|
}
|
|
8542
9017
|
function refsDigest(refs) {
|
|
8543
|
-
return
|
|
9018
|
+
return createHash8("sha256").update(
|
|
8544
9019
|
[...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
|
|
8545
9020
|
).digest("hex").slice(0, 12);
|
|
8546
9021
|
}
|
|
@@ -8553,7 +9028,7 @@ var init_gitremote = __esm({
|
|
|
8553
9028
|
ZERO_SHA = "0".repeat(40);
|
|
8554
9029
|
REMOTE_PATH = "/v1/git/repos/";
|
|
8555
9030
|
remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
|
|
8556
|
-
gitCredentialFile = () =>
|
|
9031
|
+
gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
|
|
8557
9032
|
GitApiError = class extends Error {
|
|
8558
9033
|
constructor(status, message, body = null) {
|
|
8559
9034
|
super(message);
|
|
@@ -8645,19 +9120,19 @@ __export(gitrepo_exports, {
|
|
|
8645
9120
|
githubReferences: () => githubReferences,
|
|
8646
9121
|
repoFromUrl: () => repoFromUrl
|
|
8647
9122
|
});
|
|
8648
|
-
import
|
|
8649
|
-
import
|
|
9123
|
+
import fs27 from "node:fs";
|
|
9124
|
+
import path22 from "node:path";
|
|
8650
9125
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8651
|
-
function
|
|
9126
|
+
function git3(cwd, args, opts = {}) {
|
|
8652
9127
|
const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
|
|
8653
9128
|
const res = { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
|
|
8654
9129
|
if (!res.ok && !opts.allowFail) throw new Error(`git ${args.join(" ")} failed: ${(res.err || res.out).trim()}`);
|
|
8655
9130
|
return res;
|
|
8656
9131
|
}
|
|
8657
9132
|
function onPath(name) {
|
|
8658
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
9133
|
+
for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
|
|
8659
9134
|
try {
|
|
8660
|
-
|
|
9135
|
+
fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
|
|
8661
9136
|
return true;
|
|
8662
9137
|
} catch {
|
|
8663
9138
|
}
|
|
@@ -8709,7 +9184,7 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
|
|
|
8709
9184
|
return client;
|
|
8710
9185
|
}
|
|
8711
9186
|
function toplevel(dir) {
|
|
8712
|
-
const r =
|
|
9187
|
+
const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
|
|
8713
9188
|
if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
|
|
8714
9189
|
return r.out.trim();
|
|
8715
9190
|
}
|
|
@@ -8717,7 +9192,7 @@ function githubReferences(root, repo) {
|
|
|
8717
9192
|
const [owner, name] = repo.split("/");
|
|
8718
9193
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8719
9194
|
const pattern = `github\\.com[:/]${esc(owner)}/${esc(name)}|--repo[ =]${esc(owner)}/${esc(name)}|gh repo clone ${esc(owner)}/${esc(name)}`;
|
|
8720
|
-
const r =
|
|
9195
|
+
const r = git3(root, ["grep", "-n", "-I", "-E", pattern, "HEAD", "--"], { allowFail: true });
|
|
8721
9196
|
return r.out.split("\n").filter(Boolean).map((l) => l.replace(/^HEAD:/, ""));
|
|
8722
9197
|
}
|
|
8723
9198
|
function switchOrigin(root, url, out) {
|
|
@@ -8726,41 +9201,41 @@ function switchOrigin(root, url, out) {
|
|
|
8726
9201
|
let keptAs = null;
|
|
8727
9202
|
if (old && old !== url) {
|
|
8728
9203
|
keptAs = rs.has("github") ? `github-${Date.now()}` : "github";
|
|
8729
|
-
|
|
9204
|
+
git3(root, ["remote", "rename", "origin", keptAs]);
|
|
8730
9205
|
}
|
|
8731
|
-
if (rs.has("bunshin") && rs.get("bunshin") === url)
|
|
8732
|
-
else if (old !== url)
|
|
8733
|
-
|
|
8734
|
-
|
|
9206
|
+
if (rs.has("bunshin") && rs.get("bunshin") === url) git3(root, ["remote", "rename", "bunshin", "origin"]);
|
|
9207
|
+
else if (old !== url) git3(root, ["remote", "add", "origin", url]);
|
|
9208
|
+
git3(root, ["fetch", "--quiet", "--tags", "origin"]);
|
|
9209
|
+
git3(root, ["remote", "set-head", "origin", "--auto"], { allowFail: true });
|
|
8735
9210
|
const originRefs = refMap(root, "refs/remotes/origin");
|
|
8736
|
-
for (const b of
|
|
8737
|
-
if (originRefs.has(`refs/remotes/origin/${b}`))
|
|
9211
|
+
for (const b of git3(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
|
|
9212
|
+
if (originRefs.has(`refs/remotes/origin/${b}`)) git3(root, ["branch", `--set-upstream-to=origin/${b}`, b], { allowFail: true });
|
|
8738
9213
|
}
|
|
8739
9214
|
out(`origin \u2192 ${url}${keptAs ? ` (the old origin is now the remote "${keptAs}")` : ""}
|
|
8740
9215
|
`);
|
|
8741
9216
|
}
|
|
8742
9217
|
async function cmdImport(args, cwd, out, err) {
|
|
8743
9218
|
requireHelper();
|
|
8744
|
-
const root = toplevel(
|
|
8745
|
-
if (
|
|
9219
|
+
const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
|
|
9220
|
+
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");
|
|
8746
9221
|
const rs = remotes(root);
|
|
8747
9222
|
const originUrl = rs.get("origin");
|
|
8748
|
-
const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${
|
|
9223
|
+
const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path22.basename(path22.dirname(root))}/${path22.basename(root)}`;
|
|
8749
9224
|
const client = await connect(args, cwd);
|
|
8750
9225
|
const url = remoteUrl(client.apiUrl, name);
|
|
8751
9226
|
const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
|
|
8752
9227
|
if (fromOrigin) {
|
|
8753
9228
|
out(`Fetching ${originUrl} \u2026
|
|
8754
9229
|
`);
|
|
8755
|
-
const f =
|
|
9230
|
+
const f = git3(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
|
|
8756
9231
|
if (!f.ok) err(`! could not fetch origin (${f.err.trim().split("\n")[0]}); importing what this clone already has from it
|
|
8757
9232
|
`);
|
|
8758
9233
|
}
|
|
8759
9234
|
const branches = fromOrigin ? [...refMap(root, "refs/remotes/origin").entries()].filter(([ref]) => ref !== "refs/remotes/origin/HEAD").map(([ref, sha2]) => ({ src: ref, dst: ref.replace("refs/remotes/origin/", "refs/heads/"), sha: sha2 })) : [...refMap(root, "refs/heads").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
|
|
8760
9235
|
const tags = [...refMap(root, "refs/tags").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
|
|
8761
9236
|
if (!branches.length) throw new Error("nothing to import: no branches found");
|
|
8762
|
-
const originHead =
|
|
8763
|
-
const current =
|
|
9237
|
+
const originHead = git3(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
|
|
9238
|
+
const current = git3(root, ["symbolic-ref", "--quiet", "--short", "HEAD"], { allowFail: true }).out.trim();
|
|
8764
9239
|
const defaultBranch = str4(args.flags["default-branch"]) ?? (fromOrigin && originHead ? originHead : current || "main");
|
|
8765
9240
|
if (!branches.some((b) => b.dst === `refs/heads/${defaultBranch}`)) throw new Error(`default branch ${defaultBranch} is not among the imported branches`);
|
|
8766
9241
|
if (fromOrigin) {
|
|
@@ -8770,7 +9245,7 @@ async function cmdImport(args, cwd, out, err) {
|
|
|
8770
9245
|
if (!remote) err(`! local branch ${b} is not on the origin and was not imported (push it to Bunshin afterwards if you need it)
|
|
8771
9246
|
`);
|
|
8772
9247
|
else if (remote !== sha2) {
|
|
8773
|
-
const ahead =
|
|
9248
|
+
const ahead = git3(root, ["rev-list", "--count", `${remote}..${sha2}`], { allowFail: true }).out.trim();
|
|
8774
9249
|
if (ahead && ahead !== "0") err(`! ${b} has ${ahead} local commit${ahead === "1" ? "" : "s"} not on the origin; after the switch, \`git push\` sends them to Bunshin
|
|
8775
9250
|
`);
|
|
8776
9251
|
}
|
|
@@ -8793,13 +9268,13 @@ async function cmdImport(args, cwd, out, err) {
|
|
|
8793
9268
|
out(`${name} exists and is empty; importing into it
|
|
8794
9269
|
`);
|
|
8795
9270
|
}
|
|
8796
|
-
if (rs.has("bunshin"))
|
|
8797
|
-
else
|
|
9271
|
+
if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
|
|
9272
|
+
else git3(root, ["remote", "add", "bunshin", url]);
|
|
8798
9273
|
const specs = [...branches, ...tags].map((r) => `${r.src}:${r.dst}`);
|
|
8799
|
-
const commits =
|
|
9274
|
+
const commits = git3(root, ["rev-list", "--count", "--all"]).out.trim();
|
|
8800
9275
|
out(`Pushing ${branches.length} branch${branches.length === 1 ? "" : "es"}, ${tags.length} tag${tags.length === 1 ? "" : "s"} (${commits} commits) \u2026
|
|
8801
9276
|
`);
|
|
8802
|
-
const push2 =
|
|
9277
|
+
const push2 = git3(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
|
|
8803
9278
|
if (!push2.ok) throw new Error(`push to Bunshin failed: ${push2.err.trim()}`);
|
|
8804
9279
|
const info = await client.info(name);
|
|
8805
9280
|
const want = [...branches, ...tags].map((r) => ({ ref: r.dst, sha: r.sha }));
|
|
@@ -8821,7 +9296,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
|
|
|
8821
9296
|
if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
|
|
8822
9297
|
`);
|
|
8823
9298
|
}
|
|
8824
|
-
if (
|
|
9299
|
+
if (fs27.existsSync(path22.join(root, ".github"))) out(`
|
|
8825
9300
|
Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
|
|
8826
9301
|
`);
|
|
8827
9302
|
out(`
|
|
@@ -8832,7 +9307,7 @@ New machines clone with: bunshin repo clone ${name}
|
|
|
8832
9307
|
}
|
|
8833
9308
|
async function cmdAttach(args, cwd, out, err) {
|
|
8834
9309
|
requireHelper();
|
|
8835
|
-
const root = toplevel(
|
|
9310
|
+
const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
|
|
8836
9311
|
const rs = remotes(root);
|
|
8837
9312
|
const originUrl = rs.get("origin");
|
|
8838
9313
|
const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
|
|
@@ -8846,9 +9321,9 @@ async function cmdAttach(args, cwd, out, err) {
|
|
|
8846
9321
|
}
|
|
8847
9322
|
const info = await client.info(name);
|
|
8848
9323
|
if (!info.refs.length) throw new Error(`${name} in Bunshin is empty; import it first (bunshin repo import)`);
|
|
8849
|
-
if (rs.has("bunshin"))
|
|
8850
|
-
else
|
|
8851
|
-
|
|
9324
|
+
if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
|
|
9325
|
+
else git3(root, ["remote", "add", "bunshin", url]);
|
|
9326
|
+
git3(root, ["fetch", "--quiet", "--tags", "bunshin"]);
|
|
8852
9327
|
if (originUrl) {
|
|
8853
9328
|
const behind = [];
|
|
8854
9329
|
for (const [ref, sha2] of refMap(root, "refs/remotes/origin")) {
|
|
@@ -8856,7 +9331,7 @@ async function cmdAttach(args, cwd, out, err) {
|
|
|
8856
9331
|
const b = ref.replace("refs/remotes/origin/", "");
|
|
8857
9332
|
const theirs = info.refs.find((r) => r.ref === `refs/heads/${b}`)?.sha;
|
|
8858
9333
|
if (!theirs) behind.push(`${b} (missing in Bunshin)`);
|
|
8859
|
-
else if (theirs !== sha2 && !
|
|
9334
|
+
else if (theirs !== sha2 && !git3(root, ["merge-base", "--is-ancestor", sha2, theirs], { allowFail: true }).ok) behind.push(`${b} (the old origin has commits Bunshin lacks)`);
|
|
8860
9335
|
}
|
|
8861
9336
|
if (behind.length && !bool3(args.flags.force)) {
|
|
8862
9337
|
err(`The old origin has work that is not in Bunshin:
|
|
@@ -8867,7 +9342,7 @@ Push it to Bunshin first (git push bunshin <branch>), or re-run with --force.
|
|
|
8867
9342
|
}
|
|
8868
9343
|
}
|
|
8869
9344
|
switchOrigin(root, url, out);
|
|
8870
|
-
const status =
|
|
9345
|
+
const status = git3(root, ["status", "-sb"]).out.split("\n")[0];
|
|
8871
9346
|
out(`${status}
|
|
8872
9347
|
`);
|
|
8873
9348
|
return 0;
|
|
@@ -8880,12 +9355,12 @@ async function cmdClone(args, cwd, out) {
|
|
|
8880
9355
|
const dir = args._[2] ?? name.split("/")[1];
|
|
8881
9356
|
const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
|
|
8882
9357
|
if (r.status !== 0) return r.status ?? 1;
|
|
8883
|
-
out(`Cloned ${name} into ${
|
|
9358
|
+
out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
|
|
8884
9359
|
`);
|
|
8885
9360
|
return 0;
|
|
8886
9361
|
}
|
|
8887
9362
|
async function cmdVerify(args, cwd, out, err) {
|
|
8888
|
-
const root = toplevel(
|
|
9363
|
+
const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
|
|
8889
9364
|
const rs = remotes(root);
|
|
8890
9365
|
const originUrl = rs.get("origin") ?? "";
|
|
8891
9366
|
if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
|
|
@@ -8906,7 +9381,7 @@ async function cmdVerify(args, cwd, out, err) {
|
|
|
8906
9381
|
`);
|
|
8907
9382
|
}
|
|
8908
9383
|
}
|
|
8909
|
-
const fsck =
|
|
9384
|
+
const fsck = git3(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
|
|
8910
9385
|
if (!fsck.ok) {
|
|
8911
9386
|
problems++;
|
|
8912
9387
|
err(`! git fsck: ${fsck.err.trim().split("\n")[0]}
|
|
@@ -8996,12 +9471,12 @@ var init_gitrepo = __esm({
|
|
|
8996
9471
|
bool3 = (v) => v === true || v === "true";
|
|
8997
9472
|
remotes = (root) => {
|
|
8998
9473
|
const m = /* @__PURE__ */ new Map();
|
|
8999
|
-
for (const name of
|
|
9474
|
+
for (const name of git3(root, ["remote"]).out.split("\n").filter(Boolean)) m.set(name, git3(root, ["remote", "get-url", name]).out.trim());
|
|
9000
9475
|
return m;
|
|
9001
9476
|
};
|
|
9002
9477
|
refMap = (root, prefix) => {
|
|
9003
9478
|
const m = /* @__PURE__ */ new Map();
|
|
9004
|
-
for (const line of
|
|
9479
|
+
for (const line of git3(root, ["for-each-ref", "--format=%(objectname) %(refname)", prefix]).out.split("\n").filter(Boolean)) {
|
|
9005
9480
|
const [sha2, ref] = line.split(" ");
|
|
9006
9481
|
m.set(ref, sha2);
|
|
9007
9482
|
}
|
|
@@ -9048,22 +9523,22 @@ __export(runner_exports, {
|
|
|
9048
9523
|
setupText: () => setupText
|
|
9049
9524
|
});
|
|
9050
9525
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9051
|
-
import
|
|
9526
|
+
import fs28 from "node:fs";
|
|
9052
9527
|
import os5 from "node:os";
|
|
9053
|
-
import
|
|
9528
|
+
import path23 from "node:path";
|
|
9054
9529
|
function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
|
|
9055
|
-
return
|
|
9530
|
+
return path23.join(home, ".bunshin", "runner");
|
|
9056
9531
|
}
|
|
9057
9532
|
function readInstalled(home) {
|
|
9058
9533
|
try {
|
|
9059
|
-
return JSON.parse(
|
|
9534
|
+
return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
|
|
9060
9535
|
} catch {
|
|
9061
9536
|
return null;
|
|
9062
9537
|
}
|
|
9063
9538
|
}
|
|
9064
9539
|
function connection(args, cwd) {
|
|
9065
9540
|
const saved = readInstalled();
|
|
9066
|
-
const store = saved &&
|
|
9541
|
+
const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
|
|
9067
9542
|
const config = store?.config();
|
|
9068
9543
|
const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
|
|
9069
9544
|
const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
|
|
@@ -9123,9 +9598,9 @@ async function runOnce(c, args, out, err) {
|
|
|
9123
9598
|
const [job] = await lease(c, args);
|
|
9124
9599
|
if (!job) break;
|
|
9125
9600
|
const started = Date.now();
|
|
9126
|
-
const scratch =
|
|
9601
|
+
const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
|
|
9127
9602
|
const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
|
|
9128
|
-
() =>
|
|
9603
|
+
() => fs28.rmSync(scratch, { recursive: true, force: true })
|
|
9129
9604
|
);
|
|
9130
9605
|
const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
|
|
9131
9606
|
if (!res.ok) {
|
|
@@ -9215,24 +9690,24 @@ function renderCheck(id, c) {
|
|
|
9215
9690
|
}
|
|
9216
9691
|
function logAnswer(entry) {
|
|
9217
9692
|
try {
|
|
9218
|
-
|
|
9219
|
-
|
|
9693
|
+
fs28.mkdirSync(runnerHome(), { recursive: true });
|
|
9694
|
+
fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
9220
9695
|
`);
|
|
9221
9696
|
} catch {
|
|
9222
9697
|
}
|
|
9223
9698
|
}
|
|
9224
9699
|
function which(name) {
|
|
9225
|
-
for (const d of [...(process.env.PATH || "").split(
|
|
9226
|
-
if (d &&
|
|
9700
|
+
for (const d of [...(process.env.PATH || "").split(path23.delimiter), path23.join(os5.homedir(), ".local", "bin")]) {
|
|
9701
|
+
if (d && fs28.existsSync(path23.join(d, name))) return path23.join(d, name);
|
|
9227
9702
|
}
|
|
9228
9703
|
return null;
|
|
9229
9704
|
}
|
|
9230
9705
|
function scheduledCommand(backend) {
|
|
9231
|
-
const node =
|
|
9232
|
-
const npx =
|
|
9706
|
+
const node = path23.dirname(process.execPath);
|
|
9707
|
+
const npx = fs28.existsSync(path23.join(node, "npx")) ? path23.join(node, "npx") : which("npx") ?? "npx";
|
|
9233
9708
|
const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
|
|
9234
|
-
const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [
|
|
9235
|
-
return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(
|
|
9709
|
+
const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path23.dirname(bin)] : [], path23.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
|
|
9710
|
+
return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path23.delimiter) };
|
|
9236
9711
|
}
|
|
9237
9712
|
function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
9238
9713
|
const cmd = scheduledCommand(backend);
|
|
@@ -9247,8 +9722,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
|
9247
9722
|
<dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
|
|
9248
9723
|
<key>WorkingDirectory</key><string>${xml(home)}</string>
|
|
9249
9724
|
<key>StartInterval</key><integer>${everyMinutes * 60}</integer>
|
|
9250
|
-
<key>StandardOutPath</key><string>${xml(
|
|
9251
|
-
<key>StandardErrorPath</key><string>${xml(
|
|
9725
|
+
<key>StandardOutPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
|
|
9726
|
+
<key>StandardErrorPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
|
|
9252
9727
|
<key>ProcessType</key><string>Background</string>
|
|
9253
9728
|
<key>LowPriorityIO</key><true/>
|
|
9254
9729
|
</dict>
|
|
@@ -9259,19 +9734,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
|
|
|
9259
9734
|
const cmd = scheduledCommand(backend);
|
|
9260
9735
|
const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
|
|
9261
9736
|
const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
|
|
9262
|
-
return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(
|
|
9737
|
+
return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path23.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
|
|
9263
9738
|
}
|
|
9264
9739
|
function launchAgentsDir(opts) {
|
|
9265
|
-
return opts.launchAgents ??
|
|
9740
|
+
return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
|
|
9266
9741
|
}
|
|
9267
9742
|
function installSchedule(backend, everyMinutes, opts = {}) {
|
|
9268
9743
|
const platform = opts.platform ?? process.platform;
|
|
9269
9744
|
const run2 = opts.run ?? realRun;
|
|
9270
9745
|
if (platform === "darwin") {
|
|
9271
9746
|
const dir = launchAgentsDir(opts);
|
|
9272
|
-
const plist =
|
|
9273
|
-
|
|
9274
|
-
|
|
9747
|
+
const plist = path23.join(dir, `${LAUNCHD_LABEL}.plist`);
|
|
9748
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
9749
|
+
fs28.writeFileSync(plist, launchdPlist(backend, everyMinutes));
|
|
9275
9750
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
9276
9751
|
run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
|
|
9277
9752
|
const r = run2("launchctl", ["bootstrap", domain, plist]);
|
|
@@ -9291,10 +9766,10 @@ function removeSchedule(opts = {}) {
|
|
|
9291
9766
|
const platform = opts.platform ?? process.platform;
|
|
9292
9767
|
const run2 = opts.run ?? realRun;
|
|
9293
9768
|
if (platform === "darwin") {
|
|
9294
|
-
const plist =
|
|
9769
|
+
const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
|
|
9295
9770
|
run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
|
|
9296
|
-
if (!
|
|
9297
|
-
|
|
9771
|
+
if (!fs28.existsSync(plist)) return "no schedule";
|
|
9772
|
+
fs28.rmSync(plist, { force: true });
|
|
9298
9773
|
return `removed ${plist}`;
|
|
9299
9774
|
}
|
|
9300
9775
|
const current = run2("crontab", ["-l"]);
|
|
@@ -9321,10 +9796,10 @@ async function install(args, cwd, out, err, opts = {}) {
|
|
|
9321
9796
|
}
|
|
9322
9797
|
const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
|
|
9323
9798
|
const home = runnerHome();
|
|
9324
|
-
|
|
9799
|
+
fs28.mkdirSync(home, { recursive: true, mode: 448 });
|
|
9325
9800
|
const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
|
|
9326
|
-
|
|
9327
|
-
|
|
9801
|
+
fs28.writeFileSync(path23.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
|
|
9802
|
+
fs28.chmodSync(path23.join(home, "config.json"), 384);
|
|
9328
9803
|
let schedule;
|
|
9329
9804
|
try {
|
|
9330
9805
|
schedule = installSchedule(backend, every, opts);
|
|
@@ -9337,7 +9812,7 @@ Run it yourself: npx -y ${PKG2} runner --once
|
|
|
9337
9812
|
out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
|
|
9338
9813
|
`);
|
|
9339
9814
|
out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
|
|
9340
|
-
` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${
|
|
9815
|
+
` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path23.join(home, "runner.log")}
|
|
9341
9816
|
`);
|
|
9342
9817
|
out(` Stop: npx -y ${PKG2} runner uninstall
|
|
9343
9818
|
`);
|
|
@@ -9354,9 +9829,9 @@ Answering what waits now\u2026
|
|
|
9354
9829
|
function uninstall(out, opts = {}) {
|
|
9355
9830
|
out(`${removeSchedule(opts)}
|
|
9356
9831
|
`);
|
|
9357
|
-
const file =
|
|
9358
|
-
if (
|
|
9359
|
-
|
|
9832
|
+
const file = path23.join(runnerHome(), "config.json");
|
|
9833
|
+
if (fs28.existsSync(file)) {
|
|
9834
|
+
fs28.rmSync(file, { force: true });
|
|
9360
9835
|
out(`removed ${file} (revoke the key in the console: Agent keys)
|
|
9361
9836
|
`);
|
|
9362
9837
|
}
|
|
@@ -9536,7 +10011,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9536
10011
|
out(text2);
|
|
9537
10012
|
return 0;
|
|
9538
10013
|
}
|
|
9539
|
-
|
|
10014
|
+
fs28.writeFileSync(file, text2, { mode: 384 });
|
|
9540
10015
|
const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
|
|
9541
10016
|
out(
|
|
9542
10017
|
[
|
|
@@ -9554,7 +10029,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9554
10029
|
const id = args._[2];
|
|
9555
10030
|
const file = args._[3] ?? "-";
|
|
9556
10031
|
if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
|
|
9557
|
-
const text2 = file === "-" ?
|
|
10032
|
+
const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
|
|
9558
10033
|
const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
|
|
9559
10034
|
out(renderCheck(id, r.body));
|
|
9560
10035
|
return r.body.ok ? 0 : 1;
|
|
@@ -9563,7 +10038,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9563
10038
|
const id = args._[2];
|
|
9564
10039
|
const file = args._[3] ?? "-";
|
|
9565
10040
|
if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
|
|
9566
|
-
const text2 = file === "-" ?
|
|
10041
|
+
const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
|
|
9567
10042
|
const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
|
|
9568
10043
|
if (check?.body && (check.status < 400 || check.body.problems)) {
|
|
9569
10044
|
out(renderCheck(id, check.body));
|
|
@@ -9587,7 +10062,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9587
10062
|
const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
|
|
9588
10063
|
const entries = (() => {
|
|
9589
10064
|
try {
|
|
9590
|
-
return
|
|
10065
|
+
return fs28.readFileSync(path23.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
|
|
9591
10066
|
} catch {
|
|
9592
10067
|
return [];
|
|
9593
10068
|
}
|
|
@@ -9656,9 +10131,9 @@ var init_runner = __esm({
|
|
|
9656
10131
|
});
|
|
9657
10132
|
|
|
9658
10133
|
// src/v2/importer.ts
|
|
9659
|
-
import { execFileSync as
|
|
9660
|
-
import
|
|
9661
|
-
import
|
|
10134
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
10135
|
+
import fs29 from "node:fs";
|
|
10136
|
+
import path24 from "node:path";
|
|
9662
10137
|
function rewriteRepoPaths(text2, repoName2, pack) {
|
|
9663
10138
|
const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
|
|
9664
10139
|
return text2.replace(re, `~/.bunshin/library/${pack}`);
|
|
@@ -9697,7 +10172,7 @@ function buildDocs(files, opts) {
|
|
|
9697
10172
|
const main2 = `${dir}/SKILL.md`;
|
|
9698
10173
|
const text2 = rw(files.get(main2).content);
|
|
9699
10174
|
const fm = frontmatterDescription(text2);
|
|
9700
|
-
const name =
|
|
10175
|
+
const name = path24.posix.basename(dir);
|
|
9701
10176
|
const bundle = {};
|
|
9702
10177
|
for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
|
|
9703
10178
|
claimed.add(p);
|
|
@@ -9779,45 +10254,45 @@ ${Object.keys(loose).map((p) => `- \`${p}\``).join("\n")}
|
|
|
9779
10254
|
}
|
|
9780
10255
|
return docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
9781
10256
|
}
|
|
9782
|
-
function
|
|
9783
|
-
return
|
|
10257
|
+
function git4(root, args) {
|
|
10258
|
+
return execFileSync6("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
9784
10259
|
}
|
|
9785
10260
|
function gitBuf(root, args) {
|
|
9786
|
-
return
|
|
10261
|
+
return execFileSync6("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
|
|
9787
10262
|
}
|
|
9788
10263
|
function isGitRepo(root) {
|
|
9789
10264
|
try {
|
|
9790
|
-
return
|
|
10265
|
+
return git4(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
9791
10266
|
} catch {
|
|
9792
10267
|
return false;
|
|
9793
10268
|
}
|
|
9794
10269
|
}
|
|
9795
10270
|
function workingTreeFiles(root) {
|
|
9796
10271
|
const out = /* @__PURE__ */ new Map();
|
|
9797
|
-
const list2 = isGitRepo(root) ?
|
|
9798
|
-
return
|
|
10272
|
+
const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
|
|
10273
|
+
return fs29.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
9799
10274
|
if (e.name === ".git" || e.name === "node_modules") return [];
|
|
9800
10275
|
const rel = base ? `${base}/${e.name}` : e.name;
|
|
9801
|
-
return e.isDirectory() ? walk(
|
|
10276
|
+
return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
|
|
9802
10277
|
});
|
|
9803
10278
|
})(root);
|
|
9804
10279
|
for (const rel of list2) {
|
|
9805
|
-
const abs =
|
|
10280
|
+
const abs = path24.join(root, rel);
|
|
9806
10281
|
let buf;
|
|
9807
10282
|
try {
|
|
9808
|
-
buf =
|
|
10283
|
+
buf = fs29.readFileSync(abs);
|
|
9809
10284
|
} catch {
|
|
9810
10285
|
continue;
|
|
9811
10286
|
}
|
|
9812
10287
|
if (!isText(buf)) continue;
|
|
9813
|
-
const exec = (
|
|
10288
|
+
const exec = (fs29.statSync(abs).mode & 73) !== 0;
|
|
9814
10289
|
out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
|
|
9815
10290
|
}
|
|
9816
10291
|
return out;
|
|
9817
10292
|
}
|
|
9818
10293
|
function commitsOf(root, since) {
|
|
9819
10294
|
const range = since ? [`${since}..HEAD`] : ["HEAD"];
|
|
9820
|
-
return
|
|
10295
|
+
return git4(root, ["log", "--reverse", "--topo-order", "--format=%H%x1f%an%x1f%aI%x1f%s", ...range]).split("\n").filter(Boolean).map((l) => {
|
|
9821
10296
|
const [sha2, author, at, subject] = l.split("");
|
|
9822
10297
|
return { sha: sha2, author, at, subject: subject ?? "" };
|
|
9823
10298
|
});
|
|
@@ -9830,7 +10305,7 @@ function readAt(root, sha2, rel, mode) {
|
|
|
9830
10305
|
}
|
|
9831
10306
|
function treeAt(root, sha2) {
|
|
9832
10307
|
const out = /* @__PURE__ */ new Map();
|
|
9833
|
-
for (const line of
|
|
10308
|
+
for (const line of git4(root, ["ls-tree", "-r", "-z", sha2]).split("\0").filter(Boolean)) {
|
|
9834
10309
|
const m = /^(\d+) blob [0-9a-f]+\t(.+)$/.exec(line);
|
|
9835
10310
|
if (!m) continue;
|
|
9836
10311
|
const f = readAt(root, sha2, m[2], m[1]);
|
|
@@ -9844,7 +10319,7 @@ function diffDocs(prev, next) {
|
|
|
9844
10319
|
return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
|
|
9845
10320
|
}
|
|
9846
10321
|
function readSkillSystem(root, opts) {
|
|
9847
|
-
const repoName2 =
|
|
10322
|
+
const repoName2 = path24.basename(path24.resolve(root));
|
|
9848
10323
|
const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
|
|
9849
10324
|
const tree = build(workingTreeFiles(root));
|
|
9850
10325
|
if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
|
|
@@ -9856,7 +10331,7 @@ function readSkillSystem(root, opts) {
|
|
|
9856
10331
|
commits.forEach((c, i) => {
|
|
9857
10332
|
if (!prevSha) files = treeAt(root, c.sha);
|
|
9858
10333
|
else {
|
|
9859
|
-
const changes =
|
|
10334
|
+
const changes = git4(root, ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id", prevSha, c.sha]).split("\0").filter(Boolean);
|
|
9860
10335
|
for (let k = 0; k + 1 < changes.length; k += 2) {
|
|
9861
10336
|
const m = /^:\d+ (\d+) [0-9a-f]+ [0-9a-f]+ ([A-Z])/.exec(changes[k]);
|
|
9862
10337
|
const rel = changes[k + 1];
|
|
@@ -9921,7 +10396,7 @@ var init_importer = __esm({
|
|
|
9921
10396
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9922
10397
|
looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
|
|
9923
10398
|
isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
|
|
9924
|
-
isUpdateHook = (p, text2) => /auto-?update/i.test(
|
|
10399
|
+
isUpdateHook = (p, text2) => /auto-?update/i.test(path24.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
|
|
9925
10400
|
fingerprint = (d) => JSON.stringify(d);
|
|
9926
10401
|
REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
|
|
9927
10402
|
|
|
@@ -9957,8 +10432,8 @@ __export(skills_exports, {
|
|
|
9957
10432
|
cmdSkill: () => cmdSkill,
|
|
9958
10433
|
parsePin: () => parsePin
|
|
9959
10434
|
});
|
|
9960
|
-
import
|
|
9961
|
-
import
|
|
10435
|
+
import fs30 from "node:fs";
|
|
10436
|
+
import path25 from "node:path";
|
|
9962
10437
|
function requireOnline(store, what) {
|
|
9963
10438
|
if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
|
|
9964
10439
|
}
|
|
@@ -9967,9 +10442,9 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
9967
10442
|
err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
|
|
9968
10443
|
return 1;
|
|
9969
10444
|
}
|
|
9970
|
-
const root =
|
|
9971
|
-
if (!
|
|
9972
|
-
const pack = str6(args.flags.pack) ??
|
|
10445
|
+
const root = path25.resolve(cwd, args._[2]);
|
|
10446
|
+
if (!fs30.existsSync(root)) throw new Error(`${root} does not exist`);
|
|
10447
|
+
const pack = str6(args.flags.pack) ?? path25.basename(root);
|
|
9973
10448
|
const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
|
|
9974
10449
|
const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
|
|
9975
10450
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10004,7 +10479,7 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10004
10479
|
const propose = args.flags["propose-rewrites"];
|
|
10005
10480
|
if (propose) {
|
|
10006
10481
|
const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
|
|
10007
|
-
await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack,
|
|
10482
|
+
await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path25.basename(root), out, err);
|
|
10008
10483
|
}
|
|
10009
10484
|
out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
|
|
10010
10485
|
return 0;
|
|
@@ -10144,7 +10619,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
|
|
|
10144
10619
|
}
|
|
10145
10620
|
const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
|
|
10146
10621
|
if (graph) {
|
|
10147
|
-
|
|
10622
|
+
fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
|
|
10148
10623
|
out(`graph written to ${graph}
|
|
10149
10624
|
`);
|
|
10150
10625
|
}
|
|
@@ -10211,12 +10686,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10211
10686
|
}
|
|
10212
10687
|
case "status": {
|
|
10213
10688
|
const d = deployedState(config);
|
|
10214
|
-
const p = deployPaths();
|
|
10689
|
+
const p = deployPaths(process.env, config.orgId);
|
|
10215
10690
|
const pending = d.docs.filter((x) => x.pending);
|
|
10216
10691
|
out(`deployed: ${d.libraryVersion ? `lib@${d.libraryVersion}` : "nothing yet"}; ${d.docs.filter((x) => x.kind === "skill").length} skills, ${d.docs.filter((x) => x.kind === "instructions").length} instruction documents, ${d.docs.filter((x) => x.kind === "pack").length} pack documents
|
|
10217
10692
|
`);
|
|
10218
|
-
out(` skills: ${p.claudeSkills}, ${p.codexSkills}
|
|
10219
|
-
|
|
10693
|
+
out(` skills: ${p.claudeSkills}, ${p.codexSkills} (this workspace's own copy)
|
|
10694
|
+
linked into: ${d.linked.length ? d.linked.join(", ") : "no project yet (each project gets links when a session runs there)"}
|
|
10220
10695
|
files: ${p.library}
|
|
10221
10696
|
`);
|
|
10222
10697
|
for (const x of pending) out(` edited here, awaiting the nightly: ${x.name}
|
|
@@ -10224,16 +10699,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10224
10699
|
return 0;
|
|
10225
10700
|
}
|
|
10226
10701
|
case "workspace": {
|
|
10227
|
-
|
|
10228
|
-
if (bool4(args.flags.clear)) setDeployWorkspace(null);
|
|
10229
|
-
else if (target) setDeployWorkspace(target);
|
|
10230
|
-
const ws = deployWorkspace();
|
|
10231
|
-
const p = deployPaths();
|
|
10232
|
-
out(ws ? `workspace: ${ws}
|
|
10233
|
-
` : "workspace: none (skills and instructions are user-wide)\n");
|
|
10234
|
-
out(` skills: ${p.claudeSkills}, ${p.codexSkills}
|
|
10235
|
-
instructions: ${p.claudeMd}, ${p.agentsMd}
|
|
10236
|
-
`);
|
|
10702
|
+
out("Skills no longer go to one shared folder: each workspace keeps its own copy and links it into the projects its sessions run in, so they never reach another workspace's projects. See `bunshin library status`.\n");
|
|
10237
10703
|
return 0;
|
|
10238
10704
|
}
|
|
10239
10705
|
case "pin": {
|
|
@@ -10243,9 +10709,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10243
10709
|
`;
|
|
10244
10710
|
const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
|
|
10245
10711
|
if (file) {
|
|
10246
|
-
const abs =
|
|
10247
|
-
|
|
10248
|
-
|
|
10712
|
+
const abs = path25.resolve(cwd, file);
|
|
10713
|
+
fs30.mkdirSync(path25.dirname(abs), { recursive: true });
|
|
10714
|
+
fs30.writeFileSync(abs, line);
|
|
10249
10715
|
out(`${file}: ${line}`);
|
|
10250
10716
|
} else out(line);
|
|
10251
10717
|
return 0;
|
|
@@ -10255,12 +10721,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10255
10721
|
const to = str6(args.flags.to);
|
|
10256
10722
|
if (!to) throw new Error("--to <dir> is required");
|
|
10257
10723
|
const pinFile = str6(args.flags.pin);
|
|
10258
|
-
const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(
|
|
10724
|
+
const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs30.readFileSync(path25.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
|
|
10259
10725
|
const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
|
|
10260
10726
|
if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
|
|
10261
10727
|
const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
|
|
10262
|
-
const dir =
|
|
10263
|
-
if (
|
|
10728
|
+
const dir = path25.resolve(cwd, to);
|
|
10729
|
+
if (fs30.existsSync(dir) && fs30.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
|
|
10264
10730
|
const pack = str6(args.flags.pack);
|
|
10265
10731
|
const docs = snap.sections.filter((s) => !pack || s.pack === pack);
|
|
10266
10732
|
const res = materialize(docs, dir);
|
|
@@ -10299,27 +10765,27 @@ __export(reflect_exports, {
|
|
|
10299
10765
|
buildLesson: () => buildLesson,
|
|
10300
10766
|
runReflection: () => runReflection
|
|
10301
10767
|
});
|
|
10302
|
-
import
|
|
10303
|
-
import
|
|
10304
|
-
import { createHash as
|
|
10768
|
+
import fs31 from "node:fs";
|
|
10769
|
+
import path26 from "node:path";
|
|
10770
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
10305
10771
|
async function runReflection(store, opts) {
|
|
10306
10772
|
const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
|
|
10307
|
-
const lock =
|
|
10773
|
+
const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
|
|
10308
10774
|
const deadline = Date.now() + 66e4;
|
|
10309
10775
|
for (; ; ) {
|
|
10310
10776
|
try {
|
|
10311
|
-
|
|
10777
|
+
fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
10312
10778
|
break;
|
|
10313
10779
|
} catch (error) {
|
|
10314
10780
|
if (error.code !== "EEXIST") throw error;
|
|
10315
10781
|
try {
|
|
10316
|
-
const owner = Number(
|
|
10782
|
+
const owner = Number(fs31.readFileSync(lock, "utf8"));
|
|
10317
10783
|
if (Number.isInteger(owner) && owner > 0) {
|
|
10318
10784
|
try {
|
|
10319
10785
|
process.kill(owner, 0);
|
|
10320
10786
|
} catch (error2) {
|
|
10321
10787
|
if (error2.code === "ESRCH") {
|
|
10322
|
-
|
|
10788
|
+
fs31.unlinkSync(lock);
|
|
10323
10789
|
continue;
|
|
10324
10790
|
}
|
|
10325
10791
|
}
|
|
@@ -10334,7 +10800,7 @@ async function runReflection(store, opts) {
|
|
|
10334
10800
|
return await reflectSession(store, opts);
|
|
10335
10801
|
} finally {
|
|
10336
10802
|
try {
|
|
10337
|
-
|
|
10803
|
+
fs31.unlinkSync(lock);
|
|
10338
10804
|
} catch {
|
|
10339
10805
|
}
|
|
10340
10806
|
}
|
|
@@ -10676,8 +11142,8 @@ __export(cli_exports, {
|
|
|
10676
11142
|
parseArgs: () => parseArgs,
|
|
10677
11143
|
version: () => version
|
|
10678
11144
|
});
|
|
10679
|
-
import
|
|
10680
|
-
import
|
|
11145
|
+
import fs32 from "node:fs";
|
|
11146
|
+
import path27 from "node:path";
|
|
10681
11147
|
function parseArgs(argv) {
|
|
10682
11148
|
const out = { _: [], flags: {} };
|
|
10683
11149
|
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"]);
|
|
@@ -10777,7 +11243,7 @@ async function main(argv, opts = {}) {
|
|
|
10777
11243
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
10778
11244
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
10779
11245
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
10780
|
-
const prior = new Store(
|
|
11246
|
+
const prior = new Store(path27.join(cwd, ".bunshin"));
|
|
10781
11247
|
const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
|
|
10782
11248
|
const apiUrl = str7(args.flags["api-url"]);
|
|
10783
11249
|
const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
@@ -10789,11 +11255,30 @@ async function main(argv, opts = {}) {
|
|
|
10789
11255
|
process.stdin.on("end", () => resolve(value.trim()));
|
|
10790
11256
|
process.stdin.on("error", reject);
|
|
10791
11257
|
}) : process.env.BUNSHIN_API_KEY || (!apiUrl || apiUrl === existing.apiUrl) && existing.apiKey || await readKey2();
|
|
11258
|
+
const hooksFlag = str7(args.flags.hooks);
|
|
11259
|
+
if (hooksFlag && hooksFlag !== "user" && hooksFlag !== "project") throw new Error("--hooks must be user (every session under this folder) or project (this folder only)");
|
|
11260
|
+
const only = str7(args.flags.only);
|
|
11261
|
+
const exclude = str7(args.flags.exclude);
|
|
11262
|
+
const listed = (v) => v.split(",").map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
|
|
10792
11263
|
out("Connecting to Bunshin\u2026\n");
|
|
10793
|
-
const result = await setupProject2({
|
|
11264
|
+
const result = await setupProject2({
|
|
11265
|
+
cwd,
|
|
11266
|
+
harness,
|
|
11267
|
+
apiKey: key,
|
|
11268
|
+
apiUrl,
|
|
11269
|
+
noHooks: bool5(args.flags["no-hooks"]),
|
|
11270
|
+
...hooksFlag ? { hooks: hooksFlag } : {},
|
|
11271
|
+
...bool5(args.flags.all) ? { sources: null } : only || exclude ? { sources: { ...only ? { only: listed(only) } : {}, ...exclude ? { exclude: listed(exclude) } : {} } } : {}
|
|
11272
|
+
});
|
|
11273
|
+
if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
|
|
11274
|
+
const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
11275
|
+
await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
|
|
11276
|
+
}
|
|
10794
11277
|
out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
|
|
10795
11278
|
`);
|
|
10796
|
-
out(`
|
|
11279
|
+
out(result.config.hooks === "user" ? `Folder: ${cwd} (every session in it or below it uses this workspace)
|
|
11280
|
+
Key saved privately outside the folder.
|
|
11281
|
+
` : `Project: ${cwd}
|
|
10797
11282
|
Key saved privately outside the project.
|
|
10798
11283
|
`);
|
|
10799
11284
|
if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
|
|
@@ -10806,6 +11291,7 @@ Key saved privately outside the project.
|
|
|
10806
11291
|
if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
|
|
10807
11292
|
if (harness === "codex" && result.hooks) out("Run codex interactively in this project. Review and accept project trust, then run /hooks and review and trust the five Bunshin hooks.\n");
|
|
10808
11293
|
else if (harness === "pi" && result.hooks) out("Open Pi in this project and approve its project extension. Check it with /bunshin status.\n");
|
|
11294
|
+
else if (result.hooks && result.config.hooks === "user") out("Open a new Claude Code session in any project under this folder to load the hooks. Sessions elsewhere are untouched.\nChange which folders take part with: bunshin sources\n");
|
|
10809
11295
|
else if (result.hooks) out(`Open a new Claude Code session in this project to load the hooks${result.mcp?.installed ? " and approve the bunshin MCP server (.mcp.json)" : ""}.
|
|
10810
11296
|
`);
|
|
10811
11297
|
else out("Hooks are disabled. Run setup again without --no-hooks to configure them.\n");
|
|
@@ -10821,7 +11307,7 @@ Key saved privately outside the project.
|
|
|
10821
11307
|
if (cmd === "mcp") {
|
|
10822
11308
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
10823
11309
|
const root = str7(args.flags.root);
|
|
10824
|
-
await runMcpServer2({ cwd, ...root ? { store: new Store(
|
|
11310
|
+
await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
|
|
10825
11311
|
return 0;
|
|
10826
11312
|
}
|
|
10827
11313
|
if (cmd === "demo") {
|
|
@@ -10848,6 +11334,14 @@ Key saved privately outside the project.
|
|
|
10848
11334
|
}
|
|
10849
11335
|
if (skipped.length && cmd === "status") err(`! ${untrustedNote(skipped[0])}
|
|
10850
11336
|
`);
|
|
11337
|
+
if (["learn", "reflect", "recall", "load", "suggest"].includes(cmd) && !str7(args.flags.root)) {
|
|
11338
|
+
const { cwdInPool: cwdInPool2, topFolder: topFolder2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
11339
|
+
if (!cwdInPool2(store.config(), cwd, store.root)) {
|
|
11340
|
+
err(`bunshin: ${topFolder2(cwd, store.root)} is not part of this store's learning pool. Add it with \`bunshin sources include ${topFolder2(cwd, store.root)}\`.
|
|
11341
|
+
`);
|
|
11342
|
+
return 1;
|
|
11343
|
+
}
|
|
11344
|
+
}
|
|
10851
11345
|
try {
|
|
10852
11346
|
switch (cmd) {
|
|
10853
11347
|
case "status":
|
|
@@ -10899,6 +11393,9 @@ Key saved privately outside the project.
|
|
|
10899
11393
|
return await (await Promise.resolve().then(() => (init_skills(), skills_exports))).cmdLibrary(store, args, cwd, out, err);
|
|
10900
11394
|
case "ui":
|
|
10901
11395
|
return await (await Promise.resolve().then(() => (init_uicmd(), uicmd_exports))).cmdUi(store, args, out, err, version());
|
|
11396
|
+
case "sources":
|
|
11397
|
+
case "source":
|
|
11398
|
+
return (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdSources(store, args, cwd, out, err);
|
|
10902
11399
|
default:
|
|
10903
11400
|
err(`bunshin: unknown command "${cmd}"
|
|
10904
11401
|
|
|
@@ -10914,7 +11411,7 @@ ${HELP2}`);
|
|
|
10914
11411
|
function openStore(args, cwd) {
|
|
10915
11412
|
const root = str7(args.flags.root);
|
|
10916
11413
|
if (root) {
|
|
10917
|
-
const store = new Store(
|
|
11414
|
+
const store = new Store(path27.resolve(root));
|
|
10918
11415
|
store.ensureLayout();
|
|
10919
11416
|
return store;
|
|
10920
11417
|
}
|
|
@@ -10930,7 +11427,7 @@ async function cmdInit(args, cwd, out, err) {
|
|
|
10930
11427
|
orgId: str7(args.flags["org-id"]),
|
|
10931
11428
|
userId: str7(args.flags["user-id"]),
|
|
10932
11429
|
apiUrl: str7(args.flags["api-url"]),
|
|
10933
|
-
apiKey: bool5(args.flags["key-stdin"]) ?
|
|
11430
|
+
apiKey: bool5(args.flags["key-stdin"]) ? fs32.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
|
|
10934
11431
|
scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
|
|
10935
11432
|
harness,
|
|
10936
11433
|
noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
|
|
@@ -10973,10 +11470,10 @@ function cmdStatus(store, args, out, v2) {
|
|
|
10973
11470
|
const conflicts = store.listConflicts().filter((c) => c.status === "open");
|
|
10974
11471
|
const metrics = store.readMetrics(void 0);
|
|
10975
11472
|
const last50 = metrics?.windows?.find((w) => w.name === "last50");
|
|
10976
|
-
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([
|
|
11473
|
+
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path27.join(process.env.CODEX_HOME ?? path27.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path27.dirname(store.root)).file])] : config.harness === "claude-code" ? [path27.join(homeDir(), ".claude", "settings.json"), path27.join(path27.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path27.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path27.join(path27.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
|
|
10977
11474
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
10978
11475
|
try {
|
|
10979
|
-
return
|
|
11476
|
+
return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
10980
11477
|
} catch {
|
|
10981
11478
|
return false;
|
|
10982
11479
|
}
|
|
@@ -10994,7 +11491,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
10994
11491
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
10995
11492
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
10996
11493
|
hooks: { [config.harness]: hooks },
|
|
10997
|
-
hookFiles: hookFiles.filter((file) =>
|
|
11494
|
+
hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
|
|
10998
11495
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
|
|
10999
11496
|
v2
|
|
11000
11497
|
};
|
|
@@ -11010,6 +11507,8 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11010
11507
|
out(`user ${config.userId} \u2014 ${config.role} @ ${config.orgUnit} (org ${config.orgId})
|
|
11011
11508
|
`);
|
|
11012
11509
|
out(`context ${v2.context.id} (${v2.context.kind}; layers ${v2.context.layers.join(", ")})
|
|
11510
|
+
`);
|
|
11511
|
+
if (v2.source) out(`source ${!v2.source.inPool ? `${v2.source.folder} is outside the learning pool (\`bunshin sources\`)` : v2.source.id ?? "none (the store's own folder)"}
|
|
11013
11512
|
`);
|
|
11014
11513
|
out(`library ${v2.bundle ? `${v2.bundle.sections} bundle section(s), generated ${v2.bundle.generatedAt}` : "no bundle yet"}; ${v2.repo.sections} working-tree section(s)${v2.repo.name ? ` in ${v2.repo.name}` : ""}
|
|
11015
11514
|
`);
|
|
@@ -11035,7 +11534,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11035
11534
|
}
|
|
11036
11535
|
function hookCount(file) {
|
|
11037
11536
|
try {
|
|
11038
|
-
const raw = JSON.parse(
|
|
11537
|
+
const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
|
|
11039
11538
|
let n = 0;
|
|
11040
11539
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
11041
11540
|
for (const g of groups ?? []) {
|
|
@@ -11155,7 +11654,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
11155
11654
|
let content = str7(args.flags.content) ?? "";
|
|
11156
11655
|
if (!content || content === "-" || args._.includes("-")) {
|
|
11157
11656
|
try {
|
|
11158
|
-
content =
|
|
11657
|
+
content = fs32.readFileSync(0, "utf8");
|
|
11159
11658
|
} catch {
|
|
11160
11659
|
}
|
|
11161
11660
|
}
|
|
@@ -11221,7 +11720,7 @@ async function cmdReflect(store, args, cwd, out, err) {
|
|
|
11221
11720
|
}
|
|
11222
11721
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
|
|
11223
11722
|
const result = await runReflection2(store, {
|
|
11224
|
-
sessionId: str7(args.flags.session) ??
|
|
11723
|
+
sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
11225
11724
|
transcriptPath: transcript,
|
|
11226
11725
|
cwd,
|
|
11227
11726
|
force: bool5(args.flags.force)
|
|
@@ -11346,6 +11845,10 @@ Usage: bunshin <command> [options]
|
|
|
11346
11845
|
setup --harness codex|claude-code|pi connect this project to your hosted workspace
|
|
11347
11846
|
--api-url <url> --no-hooks paste the key at the hidden prompt; profile is read from Bunshin
|
|
11348
11847
|
--key-stdin read the key from stdin (for automated setup)
|
|
11848
|
+
--hooks user a folder of projects: hooks in your user settings, so every
|
|
11849
|
+
session under this folder uses its workspace (default: project)
|
|
11850
|
+
--only a,b | --exclude c | --all
|
|
11851
|
+
which of the folder's projects take part (asked when omitted)
|
|
11349
11852
|
setup --robot --robot-id <id> --arm so101 --gripper <g> --mount <m> --cameras scene,wrist
|
|
11350
11853
|
--domain <d> --site <s> --cell <c> [--depth] [--api-url <url>] [--harness claude-code]
|
|
11351
11854
|
register this machine as a robot context (x-bunshin-context)
|
|
@@ -11355,6 +11858,8 @@ Usage: bunshin <command> [options]
|
|
|
11355
11858
|
--harness claude-code|codex|pi|other --no-hooks --remove-hooks
|
|
11356
11859
|
|
|
11357
11860
|
status [--json] store, context, library, outbox, hooks and sync health
|
|
11861
|
+
sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
|
|
11862
|
+
which folders under this store's folder take part (learning pool)
|
|
11358
11863
|
recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--json]
|
|
11359
11864
|
pull candidates from the library (offline: local view)
|
|
11360
11865
|
load <id> [--anchor <heading>] [--json]
|
|
@@ -11417,7 +11922,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
|
|
|
11417
11922
|
Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
|
|
11418
11923
|
CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
|
|
11419
11924
|
`;
|
|
11420
|
-
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 ${
|
|
11925
|
+
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 ${path27.dirname(root)} to trust it.`;
|
|
11421
11926
|
}
|
|
11422
11927
|
});
|
|
11423
11928
|
|