@bunshin-cc/bunshin 0.6.5 → 0.8.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 +1390 -761
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1074,6 +1074,95 @@ var init_permissions = __esm({
|
|
|
1074
1074
|
}
|
|
1075
1075
|
});
|
|
1076
1076
|
|
|
1077
|
+
// ../core/src/v2/sources.ts
|
|
1078
|
+
function sourceId(kind, name) {
|
|
1079
|
+
return `${kind}:${name.trim()}`;
|
|
1080
|
+
}
|
|
1081
|
+
function parseSource(id) {
|
|
1082
|
+
if (!id) return null;
|
|
1083
|
+
const i = id.indexOf(":");
|
|
1084
|
+
if (i < 0) return null;
|
|
1085
|
+
const kind = id.slice(0, i);
|
|
1086
|
+
const name = id.slice(i + 1);
|
|
1087
|
+
if (!SOURCE_KINDS.includes(kind) || !NAME.test(name) || name.trim() !== name) return null;
|
|
1088
|
+
return { kind, name };
|
|
1089
|
+
}
|
|
1090
|
+
function sourceName(id) {
|
|
1091
|
+
return parseSource(id)?.name ?? id;
|
|
1092
|
+
}
|
|
1093
|
+
var SOURCE_KINDS, NAME;
|
|
1094
|
+
var init_sources = __esm({
|
|
1095
|
+
"../core/src/v2/sources.ts"() {
|
|
1096
|
+
"use strict";
|
|
1097
|
+
SOURCE_KINDS = ["repo", "folder", "robot"];
|
|
1098
|
+
NAME = /^[^\0-\x1f\x7f]{1,280}$/;
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// ../core/src/v2/scopes.ts
|
|
1103
|
+
function emptyTree() {
|
|
1104
|
+
return { version: 0, groups: [], sources: {} };
|
|
1105
|
+
}
|
|
1106
|
+
function isScopeLayer(layer) {
|
|
1107
|
+
const k = layerKind(layer);
|
|
1108
|
+
return k === "group" || k === "source";
|
|
1109
|
+
}
|
|
1110
|
+
function groupChain(tree, groupId) {
|
|
1111
|
+
const byId = new Map(tree.groups.map((g) => [g.id, g]));
|
|
1112
|
+
const out = [];
|
|
1113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1114
|
+
let cur = groupId ? byId.get(groupId) : void 0;
|
|
1115
|
+
while (cur && !seen.has(cur.id)) {
|
|
1116
|
+
seen.add(cur.id);
|
|
1117
|
+
out.unshift(cur.id);
|
|
1118
|
+
cur = cur.parent ? byId.get(cur.parent) : void 0;
|
|
1119
|
+
}
|
|
1120
|
+
return out;
|
|
1121
|
+
}
|
|
1122
|
+
function scopePath(tree, source) {
|
|
1123
|
+
if (!source || !parseSource(source)) return [];
|
|
1124
|
+
const placed = tree?.sources[source]?.group ?? null;
|
|
1125
|
+
return [...(tree ? groupChain(tree, placed) : []).map(groupLayer), sourceLayer(source)];
|
|
1126
|
+
}
|
|
1127
|
+
function inBoundary(section, reader) {
|
|
1128
|
+
return !isScopeLayer(section.layer) || reader.layers.includes(section.layer);
|
|
1129
|
+
}
|
|
1130
|
+
function sourcesIn(tree, layer) {
|
|
1131
|
+
if (layer === "org") return Object.keys(tree.sources);
|
|
1132
|
+
if (layerKind(layer) === "source") return [layerValue(layer)];
|
|
1133
|
+
if (layerKind(layer) !== "group") return [];
|
|
1134
|
+
const gid = layerValue(layer);
|
|
1135
|
+
return Object.keys(tree.sources).filter((sid) => groupChain(tree, tree.sources[sid].group).includes(gid));
|
|
1136
|
+
}
|
|
1137
|
+
function scopeLabel(layer, tree) {
|
|
1138
|
+
const k = layerKind(layer);
|
|
1139
|
+
if (layer === "org") return "everywhere";
|
|
1140
|
+
if (k === "source") {
|
|
1141
|
+
const sid = layerValue(layer);
|
|
1142
|
+
return `${parseSource(sid)?.kind === "robot" ? "robot" : "project"} ${sourceName(sid)}`;
|
|
1143
|
+
}
|
|
1144
|
+
if (k === "group") return `group ${tree?.groups.find((g) => g.id === layerValue(layer))?.name ?? layerValue(layer)}`;
|
|
1145
|
+
if (k === "user") return "personal";
|
|
1146
|
+
return layer;
|
|
1147
|
+
}
|
|
1148
|
+
function scopeChoices(tree, source) {
|
|
1149
|
+
const t = tree ?? emptyTree();
|
|
1150
|
+
return scopePath(t, source).map((layer) => {
|
|
1151
|
+
const n = sourcesIn(t, layer).length;
|
|
1152
|
+
return { layer, label: layerKind(layer) === "source" ? `this ${scopeLabel(layer, t)} only` : `${scopeLabel(layer, t)} (${n} project${n === 1 ? "" : "s"})` };
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
var groupLayer, sourceLayer;
|
|
1156
|
+
var init_scopes = __esm({
|
|
1157
|
+
"../core/src/v2/scopes.ts"() {
|
|
1158
|
+
"use strict";
|
|
1159
|
+
init_layers();
|
|
1160
|
+
init_sources();
|
|
1161
|
+
groupLayer = (id) => `group:${id}`;
|
|
1162
|
+
sourceLayer = (id) => `source:${id}`;
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1077
1166
|
// ../core/src/v2/layers.ts
|
|
1078
1167
|
function layerKind(layer) {
|
|
1079
1168
|
const kind = layer.split(":")[0];
|
|
@@ -1091,11 +1180,22 @@ function layersFor(ctx) {
|
|
|
1091
1180
|
const dept = department(team);
|
|
1092
1181
|
return ["org", ...dept ? [`dept:${dept}`] : [], ...team ? [`team:${team}`] : [], `user:${ctx.userId}`];
|
|
1093
1182
|
}
|
|
1094
|
-
function
|
|
1183
|
+
function isLocalLayer(layer) {
|
|
1184
|
+
const kind = layerKind(layer);
|
|
1185
|
+
return kind === "user" || kind === "robot";
|
|
1186
|
+
}
|
|
1187
|
+
function withScope(layers, scope) {
|
|
1188
|
+
const path28 = scopePath(scope?.tree, scope?.source);
|
|
1189
|
+
if (!path28.length) return layers;
|
|
1190
|
+
const i = layers.findIndex((l) => isLocalLayer(l));
|
|
1191
|
+
return i < 0 ? [...layers, ...path28] : [...layers.slice(0, i), ...path28, ...layers.slice(i)];
|
|
1192
|
+
}
|
|
1193
|
+
function readerFor(ctx, ownerClearance, scope) {
|
|
1194
|
+
const source = scope?.source ? { source: scope.source } : {};
|
|
1095
1195
|
if (ctx.kind === "robot") {
|
|
1096
1196
|
const rank = (c) => c === "private" ? 0 : CLEARANCE_RANK[c];
|
|
1097
1197
|
const clearance = ownerClearance && rank(ownerClearance) < CLEARANCE_RANK.org ? ownerClearance : "org";
|
|
1098
|
-
return { userId: ctx.userId, orgId: ctx.orgId, role: "robot", team: `fleet/${ctx.robot?.site ?? "site"}`, clearance, contextId: ctx.id, layers: layersFor(ctx), ...ctx.robot?.domain ? { domain: ctx.robot.domain } : {} };
|
|
1198
|
+
return { userId: ctx.userId, orgId: ctx.orgId, role: "robot", team: `fleet/${ctx.robot?.site ?? "site"}`, clearance, contextId: ctx.id, layers: withScope(layersFor(ctx), scope), ...ctx.robot?.domain ? { domain: ctx.robot.domain } : {}, ...source };
|
|
1099
1199
|
}
|
|
1100
1200
|
return {
|
|
1101
1201
|
userId: ctx.userId,
|
|
@@ -1104,7 +1204,8 @@ function readerFor(ctx, ownerClearance) {
|
|
|
1104
1204
|
team: ctx.person?.team ?? "",
|
|
1105
1205
|
clearance: ctx.person?.clearance ?? "team",
|
|
1106
1206
|
contextId: ctx.id,
|
|
1107
|
-
layers: layersFor(ctx)
|
|
1207
|
+
layers: withScope(layersFor(ctx), scope),
|
|
1208
|
+
...source
|
|
1108
1209
|
};
|
|
1109
1210
|
}
|
|
1110
1211
|
function explainSectionAccess(section, reader) {
|
|
@@ -1134,6 +1235,15 @@ function scopeAffinity(section, reader) {
|
|
|
1134
1235
|
}
|
|
1135
1236
|
function baseAffinity(section, reader) {
|
|
1136
1237
|
const kind = layerKind(section.layer);
|
|
1238
|
+
if (kind === "group" || kind === "source") {
|
|
1239
|
+
const i = reader.layers.indexOf(section.layer);
|
|
1240
|
+
if (i < 0) return 0.05;
|
|
1241
|
+
const path28 = reader.layers.filter((l) => {
|
|
1242
|
+
const k = layerKind(l);
|
|
1243
|
+
return k === "group" || k === "source";
|
|
1244
|
+
});
|
|
1245
|
+
return 0.6 + 0.4 * ((path28.indexOf(section.layer) + 1) / path28.length);
|
|
1246
|
+
}
|
|
1137
1247
|
if (reader.layers.includes(section.layer)) {
|
|
1138
1248
|
const order = reader.role === "robot" ? FLEET_LAYER_ORDER : COMPANY_LAYER_ORDER;
|
|
1139
1249
|
return 0.5 + 0.5 * (order.indexOf(kind) / Math.max(1, order.length - 1));
|
|
@@ -1159,6 +1269,7 @@ var init_layers = __esm({
|
|
|
1159
1269
|
"use strict";
|
|
1160
1270
|
init_permissions();
|
|
1161
1271
|
init_text();
|
|
1272
|
+
init_scopes();
|
|
1162
1273
|
COMPANY_LAYER_ORDER = ["org", "dept", "team", "user"];
|
|
1163
1274
|
FLEET_LAYER_ORDER = ["fleet", "arm", "mount", "robot"];
|
|
1164
1275
|
}
|
|
@@ -1344,17 +1455,17 @@ var init_document = __esm({
|
|
|
1344
1455
|
});
|
|
1345
1456
|
|
|
1346
1457
|
// ../core/src/v2/sections/markdown.ts
|
|
1347
|
-
function repoSectionId(repo,
|
|
1348
|
-
return anchor ? `repo:${repo}/${
|
|
1458
|
+
function repoSectionId(repo, path28, anchor = "") {
|
|
1459
|
+
return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
|
|
1349
1460
|
}
|
|
1350
|
-
function docKind(
|
|
1351
|
-
const base =
|
|
1461
|
+
function docKind(path28) {
|
|
1462
|
+
const base = path28.split("/").pop()?.toLowerCase() ?? "";
|
|
1352
1463
|
if (base === "testing.md") return "verification";
|
|
1353
1464
|
if (base === "skill.md") return "skill";
|
|
1354
1465
|
return "spec";
|
|
1355
1466
|
}
|
|
1356
|
-
function defaultPaths(
|
|
1357
|
-
const dir =
|
|
1467
|
+
function defaultPaths(path28) {
|
|
1468
|
+
const dir = path28.split("/").slice(0, -1).join("/");
|
|
1358
1469
|
if (!dir || dir.startsWith(".claude/") || dir.startsWith(".codex/")) return [];
|
|
1359
1470
|
return [`${dir}/**`];
|
|
1360
1471
|
}
|
|
@@ -1499,11 +1610,11 @@ function lintSkills(sections) {
|
|
|
1499
1610
|
}
|
|
1500
1611
|
const state = /* @__PURE__ */ new Map();
|
|
1501
1612
|
const cycles = /* @__PURE__ */ new Set();
|
|
1502
|
-
const dfs = (n,
|
|
1613
|
+
const dfs = (n, path28) => {
|
|
1503
1614
|
state.set(n, 1);
|
|
1504
1615
|
for (const d of edges[n] ?? []) {
|
|
1505
|
-
if (state.get(d) === 1) cycles.add([...
|
|
1506
|
-
else if (!state.get(d)) dfs(d, [...
|
|
1616
|
+
if (state.get(d) === 1) cycles.add([...path28.slice(path28.indexOf(d)), d].join(" -> "));
|
|
1617
|
+
else if (!state.get(d)) dfs(d, [...path28, d]);
|
|
1507
1618
|
}
|
|
1508
1619
|
state.set(n, 2);
|
|
1509
1620
|
};
|
|
@@ -1590,8 +1701,8 @@ function parsePythonSkills(text2) {
|
|
|
1590
1701
|
}
|
|
1591
1702
|
return { skills, constants };
|
|
1592
1703
|
}
|
|
1593
|
-
function harnessSectionId(
|
|
1594
|
-
return `harness:${
|
|
1704
|
+
function harnessSectionId(path28, symbol) {
|
|
1705
|
+
return `harness:${path28}#${symbol}`;
|
|
1595
1706
|
}
|
|
1596
1707
|
var DEF_RE;
|
|
1597
1708
|
var init_python = __esm({
|
|
@@ -1717,8 +1828,8 @@ function globToRegExp(glob) {
|
|
|
1717
1828
|
globCache.set(glob, out);
|
|
1718
1829
|
return out;
|
|
1719
1830
|
}
|
|
1720
|
-
function matchesGlob(
|
|
1721
|
-
const p =
|
|
1831
|
+
function matchesGlob(path28, glob) {
|
|
1832
|
+
const p = path28.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
1722
1833
|
return globToRegExp(glob.replace(/^\.\//, "")).test(p);
|
|
1723
1834
|
}
|
|
1724
1835
|
var DEFAULT_EMBEDDERS, globCache;
|
|
@@ -1748,8 +1859,8 @@ function servable(sections, reader) {
|
|
|
1748
1859
|
}
|
|
1749
1860
|
return [...out.values()];
|
|
1750
1861
|
}
|
|
1751
|
-
function recallable(sections, reader) {
|
|
1752
|
-
return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect");
|
|
1862
|
+
function recallable(sections, reader, opts = {}) {
|
|
1863
|
+
return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect" && (opts.across || inBoundary(s, reader)));
|
|
1753
1864
|
}
|
|
1754
1865
|
function passagesOfSection(s) {
|
|
1755
1866
|
const hit = passageCache.get(s);
|
|
@@ -1797,6 +1908,7 @@ function passageDense(embedder, pool, q) {
|
|
|
1797
1908
|
function sourceLabel(s) {
|
|
1798
1909
|
if (s.storage.type === "repo") return `repo:${s.storage.repo}/${s.storage.path}`;
|
|
1799
1910
|
if (s.storage.type === "harness") return `harness:${s.storage.path}`;
|
|
1911
|
+
if (isScopeLayer(s.layer)) return s.layer;
|
|
1800
1912
|
return s.owner.team || s.layer;
|
|
1801
1913
|
}
|
|
1802
1914
|
function push(sections, reader, query) {
|
|
@@ -1809,10 +1921,10 @@ function push(sections, reader, query) {
|
|
|
1809
1921
|
for (const s of recallable(sections, reader).filter((x) => reachesReader(x, reader))) {
|
|
1810
1922
|
const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
|
|
1811
1923
|
for (const t of triggers) {
|
|
1812
|
-
const
|
|
1813
|
-
const entity =
|
|
1814
|
-
const skill =
|
|
1815
|
-
const trigger =
|
|
1924
|
+
const path28 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
|
|
1925
|
+
const entity = path28 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
|
|
1926
|
+
const skill = path28 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
|
|
1927
|
+
const trigger = path28 ? `path:${path28}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
|
|
1816
1928
|
if (trigger) {
|
|
1817
1929
|
hits.set(s.id, { section: s, trigger, anchor: t.anchor });
|
|
1818
1930
|
break;
|
|
@@ -1837,7 +1949,7 @@ function maxSim(embedder, q, lists) {
|
|
|
1837
1949
|
function pull(sections, reader, query, opts = {}) {
|
|
1838
1950
|
const embedders = opts.embedders ?? DEFAULT_EMBEDDERS;
|
|
1839
1951
|
const exclude = new Set(opts.exclude ?? []);
|
|
1840
|
-
const pool = recallable(sections, reader).filter((s) => !exclude.has(s.id));
|
|
1952
|
+
const pool = recallable(sections, reader, opts).filter((s) => !exclude.has(s.id));
|
|
1841
1953
|
if (!pool.length) return [];
|
|
1842
1954
|
const limit = Math.max(1, Math.min(20, query.limit ?? 10));
|
|
1843
1955
|
const signals = [];
|
|
@@ -1878,6 +1990,7 @@ function pull(sections, reader, query, opts = {}) {
|
|
|
1878
1990
|
kind: section.kind,
|
|
1879
1991
|
layer: section.layer,
|
|
1880
1992
|
source: sourceLabel(section),
|
|
1993
|
+
...isScopeLayer(section.layer) && !reader.layers.includes(section.layer) ? { outside: true } : {},
|
|
1881
1994
|
score: Math.round(score2 * 1e3) / 1e3,
|
|
1882
1995
|
reasons,
|
|
1883
1996
|
...part.get(section.id) ? { part: part.get(section.id) } : {},
|
|
@@ -1946,9 +2059,9 @@ function renderSection(s, trigger, opts = {}) {
|
|
|
1946
2059
|
`_Load the whole document, or one part, with bunshin_load {"section_id": "${s.id}"${part ? `, "anchor": "${opts.anchor}"` : ""}} (or \`bunshin load ${s.id}${part ? ` --anchor ${opts.anchor}` : ""}\`)._`
|
|
1947
2060
|
].join("\n");
|
|
1948
2061
|
}
|
|
1949
|
-
function renderCandidates(c) {
|
|
2062
|
+
function renderCandidates(c, tree) {
|
|
1950
2063
|
if (!c.length) return "No sections matched. Proceed with general knowledge; nothing here is required.";
|
|
1951
|
-
return c.map((x, i) => `${i + 1}. [${x.source}] ${x.title} (id: ${x.id}${x.part ? `, best part: \xA7${x.part}` : ""}${x.size ? `, ${x.size} words` : ""})
|
|
2064
|
+
return c.map((x, i) => `${i + 1}. [${isScopeLayer(x.source) ? scopeLabel(x.source, tree) : x.source}] ${x.title} (id: ${x.id}${x.part ? `, best part: \xA7${x.part}` : ""}${x.size ? `, ${x.size} words` : ""}${x.outside ? "; borrowed from outside this project's scope" : ""})
|
|
1952
2065
|
when: ${x.when}`).join("\n");
|
|
1953
2066
|
}
|
|
1954
2067
|
var PASSAGE_CHARS, passageCache, words;
|
|
@@ -1957,6 +2070,7 @@ var init_engine = __esm({
|
|
|
1957
2070
|
"use strict";
|
|
1958
2071
|
init_text();
|
|
1959
2072
|
init_layers();
|
|
2073
|
+
init_scopes();
|
|
1960
2074
|
init_document();
|
|
1961
2075
|
init_text_index();
|
|
1962
2076
|
PASSAGE_CHARS = 4e3;
|
|
@@ -1968,7 +2082,7 @@ var init_engine = __esm({
|
|
|
1968
2082
|
// ../core/src/v2/guidelines.ts
|
|
1969
2083
|
function reflectionGuidelines(sections, reader, opts = {}) {
|
|
1970
2084
|
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 }));
|
|
2085
|
+
return pool.filter((s) => s.delivery === "reflect" && s.status === "active" && (!reader || inBoundary(s, reader))).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
2086
|
}
|
|
1973
2087
|
function guidelineRules(body) {
|
|
1974
2088
|
const at = body.search(/^## /m);
|
|
@@ -2004,8 +2118,9 @@ var init_guidelines = __esm({
|
|
|
2004
2118
|
init_types();
|
|
2005
2119
|
init_layers();
|
|
2006
2120
|
init_engine();
|
|
2121
|
+
init_scopes();
|
|
2007
2122
|
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;
|
|
2123
|
+
generality = (l) => ({ org: 0, dept: 1, fleet: 1, group: 2, team: 2, arm: 3, mount: 3, source: 3, robot: 4, user: 4 })[layerKind(l)] ?? 5;
|
|
2009
2124
|
}
|
|
2010
2125
|
});
|
|
2011
2126
|
|
|
@@ -2060,17 +2175,6 @@ function identLike(rest) {
|
|
|
2060
2175
|
return humps.length > 0 && m[1].length / humps.length >= 3;
|
|
2061
2176
|
});
|
|
2062
2177
|
}
|
|
2063
|
-
function luhnOk(digits) {
|
|
2064
|
-
let total = 0;
|
|
2065
|
-
let alt = false;
|
|
2066
|
-
for (let i = digits.length - 1; i >= 0; i--) {
|
|
2067
|
-
let d = digits.charCodeAt(i) - 48;
|
|
2068
|
-
if (alt && (d *= 2) > 9) d -= 9;
|
|
2069
|
-
total += d;
|
|
2070
|
-
alt = !alt;
|
|
2071
|
-
}
|
|
2072
|
-
return total % 10 === 0;
|
|
2073
|
-
}
|
|
2074
2178
|
function issuerOk(d) {
|
|
2075
2179
|
const n = d.length;
|
|
2076
2180
|
if (d[0] === "4") return n === 13 || n === 16 || n === 19;
|
|
@@ -2082,23 +2186,44 @@ function issuerOk(d) {
|
|
|
2082
2186
|
if (/^(?:50|5[6-8]|6\d)/.test(d)) return n >= 12 && n <= 19;
|
|
2083
2187
|
return false;
|
|
2084
2188
|
}
|
|
2189
|
+
function luhnDigits(d, n) {
|
|
2190
|
+
let total = 0;
|
|
2191
|
+
let alt = false;
|
|
2192
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
2193
|
+
let x = d[i];
|
|
2194
|
+
if (alt && (x *= 2) > 9) x -= 9;
|
|
2195
|
+
total += x;
|
|
2196
|
+
alt = !alt;
|
|
2197
|
+
}
|
|
2198
|
+
return total % 10 === 0;
|
|
2199
|
+
}
|
|
2085
2200
|
function cardPrefix(s) {
|
|
2086
|
-
const
|
|
2087
|
-
if (/^3[1-3]|^2[019]/.test(digits)) return 0;
|
|
2088
|
-
const sep = s.search(/[ -]/);
|
|
2089
|
-
if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
|
|
2201
|
+
const d = CARD_DIGITS;
|
|
2090
2202
|
const ends = [];
|
|
2091
|
-
|
|
2203
|
+
const counts = [];
|
|
2204
|
+
let digits = 0;
|
|
2205
|
+
let sep = -1;
|
|
2092
2206
|
for (let i = 0, start = 0; i <= s.length; i++) {
|
|
2093
|
-
|
|
2207
|
+
const c = i < s.length ? s.charCodeAt(i) : -1;
|
|
2208
|
+
if (c >= 48 && c <= 57) {
|
|
2209
|
+
if (digits < d.length) d[digits] = c - 48;
|
|
2210
|
+
digits++;
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
if (sep < 0 && c !== -1) sep = i;
|
|
2094
2214
|
if (ends.length && i - start < 3) break;
|
|
2095
|
-
|
|
2096
|
-
|
|
2215
|
+
ends.push(i);
|
|
2216
|
+
counts.push(digits);
|
|
2097
2217
|
start = i + 1;
|
|
2098
2218
|
}
|
|
2219
|
+
if (d[0] === 3 && d[1] >= 1 && d[1] <= 3 || d[0] === 2 && (d[1] === 0 || d[1] === 1 || d[1] === 9)) return 0;
|
|
2220
|
+
if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
|
|
2099
2221
|
for (let k = ends.length - 1; k >= 0; k--) {
|
|
2100
|
-
const
|
|
2101
|
-
if (count
|
|
2222
|
+
const count = counts[k];
|
|
2223
|
+
if (count < 13 || count > 19 || !luhnDigits(d, count)) continue;
|
|
2224
|
+
let str8 = "";
|
|
2225
|
+
for (let j = 0; j < count; j++) str8 += String.fromCharCode(48 + d[j]);
|
|
2226
|
+
if (issuerOk(str8)) return ends[k];
|
|
2102
2227
|
}
|
|
2103
2228
|
return 0;
|
|
2104
2229
|
}
|
|
@@ -2181,7 +2306,7 @@ function redactDeep(value, opts = {}) {
|
|
|
2181
2306
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
|
|
2182
2307
|
return value;
|
|
2183
2308
|
}
|
|
2184
|
-
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,
|
|
2309
|
+
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, NAME2, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
|
|
2185
2310
|
var init_redact = __esm({
|
|
2186
2311
|
"../core/src/redact.ts"() {
|
|
2187
2312
|
"use strict";
|
|
@@ -2257,9 +2382,10 @@ var init_redact = __esm({
|
|
|
2257
2382
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2258
2383
|
PATH_RE = /^(?:~|\.{1,2})?\/[\w.@%+~/-]*$|^[A-Za-z]:\\/;
|
|
2259
2384
|
credentialish = (v) => /[\d\W_]/.test(v) || v.length >= 16 && /[a-z]/.test(v) && /[A-Z]/.test(v);
|
|
2385
|
+
CARD_DIGITS = new Uint8Array(64);
|
|
2260
2386
|
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/;
|
|
2261
2387
|
NOREPLY_RE = /^(?:no[._-]?reply|donot[._-]?reply|do[._-]?not[._-]?reply|noreply\+?.*|git|hg)$/i;
|
|
2262
|
-
|
|
2388
|
+
NAME2 = String.raw`(?=[A-Za-z_$])(?=[\w.$-]{0,64}?(?:key|token|secret|pass|pwd|credential|auth|cookie|session|creds))[A-Za-z_$][\w.$-]{0,127}`;
|
|
2263
2389
|
Q = String.raw`(?:\\\\){0,8}\\?["'\x60]`;
|
|
2264
2390
|
VALUE = String.raw`(?:(${Q})(?!\s)(.{0,4096}?)\4|(${Q})?([^\s"'\x60\\,]{1,4096}))`;
|
|
2265
2391
|
ARGS = String.raw`[^\n|;&]{0,256}?`;
|
|
@@ -2345,7 +2471,7 @@ var init_redact = __esm({
|
|
|
2345
2471
|
},
|
|
2346
2472
|
{
|
|
2347
2473
|
kind: "assigned-secret",
|
|
2348
|
-
re: new RegExp(String.raw`(?:(?<![\w.$-])|(?=["'\x60\\]))((?:\\\\){0,8}\\?["'\x60]?)(${
|
|
2474
|
+
re: new RegExp(String.raw`(?:(?<![\w.$-])|(?=["'\x60\\]))((?:\\\\){0,8}\\?["'\x60]?)(${NAME2})\1(\??\s{0,64}(?::=|=>|==?|:(?=[\s"'\x60\\]|$))[ \t]{0,64})${VALUE}`, "gim"),
|
|
2349
2475
|
retryFrom: (m) => m[1].length * 2 + m[2].length + m[3].length,
|
|
2350
2476
|
replace: (m, R, at, all) => {
|
|
2351
2477
|
const [whole, , name, , q1, inQuotes, q2, bare] = m;
|
|
@@ -2385,7 +2511,7 @@ var init_redact = __esm({
|
|
|
2385
2511
|
// YAML block scalar: `TOKEN: |` and the indented lines under it.
|
|
2386
2512
|
{
|
|
2387
2513
|
kind: "assigned-secret",
|
|
2388
|
-
re: new RegExp(String.raw`^([ \t]{0,64})((?:-[ \t]{1,16})?(["']?)(${
|
|
2514
|
+
re: new RegExp(String.raw`^([ \t]{0,64})((?:-[ \t]{1,16})?(["']?)(${NAME2})\3[ \t]{0,16}:[ \t]{0,16}[|>][+-]?\d?[ \t]{0,16})((?:\n(?:[ \t]{0,64}\n){0,8}\1[ \t]{1,64}\S[^\n]{0,4096}){1,500})`, "gim"),
|
|
2389
2515
|
replace: (m, R) => {
|
|
2390
2516
|
const [, indent, head, , name, body] = m;
|
|
2391
2517
|
if (!secretName(name)) return null;
|
|
@@ -2610,7 +2736,7 @@ ${input.digestNote ? `${input.digestNote}
|
|
|
2610
2736
|
|
|
2611
2737
|
` : ""}${input.transcriptDigest}
|
|
2612
2738
|
` : ""}
|
|
2613
|
-
${input.focus ? `## What to capture
|
|
2739
|
+
${input.feedback ? feedbackSection(input.feedback) : ""}${input.focus ? `## What to capture
|
|
2614
2740
|
|
|
2615
2741
|
${input.focus}
|
|
2616
2742
|
` : ""}${guidelinesSection(input.guidelines, { forReflection: true })}
|
|
@@ -2657,7 +2783,15 @@ ${input.pendingNew.map((p) => `- ${p}`).join("\n")}
|
|
|
2657
2783
|
Already captured this session (do not repeat): ${input.captured.length ? input.captured.join(", ") : "none"}
|
|
2658
2784
|
|
|
2659
2785
|
Layers you may target: ${input.layers.join(", ")}.
|
|
2660
|
-
${input.
|
|
2786
|
+
${input.scopes?.length ? `
|
|
2787
|
+
Where a lesson applies (put each at the narrowest scope it holds for; when unsure, the narrowest):
|
|
2788
|
+
${[...input.scopes].reverse().map((x) => `- ${x.layer}: ${x.label}`).join("\n")}
|
|
2789
|
+
- org: everywhere in this workspace
|
|
2790
|
+
A fact, command or habit of this project \u2192 this project. True of every project in a group alike \u2192
|
|
2791
|
+
that group. True anywhere this workspace works \u2192 org. Lessons that come up again in other scopes
|
|
2792
|
+
are widened by the nightly generalization pass, so a narrow scope is never a loss. Name it as
|
|
2793
|
+
"layer" in the target of a new document (an edit keeps the document's own scope).
|
|
2794
|
+
` : ""}${input.profileId ? `- How THIS person works, prefers or wants things done (a steering or approach lesson about them,
|
|
2661
2795
|
not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
|
|
2662
2796
|
A lesson for anyone doing the task belongs in the task's document instead.
|
|
2663
2797
|
` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
|
|
@@ -2690,6 +2824,34 @@ At most ${max} suggestions. Schema:
|
|
|
2690
2824
|
"rationale": string, "urgency": "normal"|"urgent",
|
|
2691
2825
|
"signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
|
|
2692
2826
|
}
|
|
2827
|
+
function feedbackSection(f) {
|
|
2828
|
+
return `
|
|
2829
|
+
## Feedback from the person (human steering)
|
|
2830
|
+
|
|
2831
|
+
${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:
|
|
2832
|
+
|
|
2833
|
+
<feedback>
|
|
2834
|
+
${f.text.trim()}
|
|
2835
|
+
</feedback>
|
|
2836
|
+
${f.lesson ? `
|
|
2837
|
+
The lesson it is about:
|
|
2838
|
+
|
|
2839
|
+
<lesson>
|
|
2840
|
+
${f.lesson.trim()}
|
|
2841
|
+
</lesson>
|
|
2842
|
+
` : ""}
|
|
2843
|
+
Treat it as a correction from a person (signal "steering"): the strongest evidence there is. Act on it:
|
|
2844
|
+
- If it says the session taught something that was missed, capture that lesson in the right
|
|
2845
|
+
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."}
|
|
2846
|
+
- If it corrects a lesson (too specific, wrong, in the wrong place), propose the corrected lesson
|
|
2847
|
+
(op "replace" if it already landed in a document, otherwise a new one) and name the lesson it
|
|
2848
|
+
corrects in the rationale. Lessons listed as already taken may be corrected this way.
|
|
2849
|
+
- If it says how lessons should be captured or written in general, propose a rule for the
|
|
2850
|
+
reflection guidelines, merged with the rules already there: never repeat one; refine an existing
|
|
2851
|
+
rule with op "replace" when the feedback sharpens it.
|
|
2852
|
+
Answer "nothing_new" only if everything it asks for is already in the library or proposed.
|
|
2853
|
+
`;
|
|
2854
|
+
}
|
|
2693
2855
|
function documentsSection(docs) {
|
|
2694
2856
|
if (!docs?.length) return "";
|
|
2695
2857
|
const body = docs.map((d) => `### ${d.id}: ${d.title} (${d.why})
|
|
@@ -2913,6 +3075,8 @@ var init_reconcile = __esm({
|
|
|
2913
3075
|
init_document();
|
|
2914
3076
|
init_markdown();
|
|
2915
3077
|
init_text_index();
|
|
3078
|
+
init_scopes();
|
|
3079
|
+
init_sources();
|
|
2916
3080
|
init_types();
|
|
2917
3081
|
}
|
|
2918
3082
|
});
|
|
@@ -2954,6 +3118,17 @@ var init_profile = __esm({
|
|
|
2954
3118
|
}
|
|
2955
3119
|
});
|
|
2956
3120
|
|
|
3121
|
+
// ../core/src/v2/generalize.ts
|
|
3122
|
+
var init_generalize = __esm({
|
|
3123
|
+
"../core/src/v2/generalize.ts"() {
|
|
3124
|
+
"use strict";
|
|
3125
|
+
init_layers();
|
|
3126
|
+
init_guidelines();
|
|
3127
|
+
init_reflect();
|
|
3128
|
+
init_scopes();
|
|
3129
|
+
}
|
|
3130
|
+
});
|
|
3131
|
+
|
|
2957
3132
|
// ../core/src/v2/index.ts
|
|
2958
3133
|
var init_v2 = __esm({
|
|
2959
3134
|
"../core/src/v2/index.ts"() {
|
|
@@ -2975,6 +3150,9 @@ var init_v2 = __esm({
|
|
|
2975
3150
|
init_research();
|
|
2976
3151
|
init_profile();
|
|
2977
3152
|
init_guidelines();
|
|
3153
|
+
init_sources();
|
|
3154
|
+
init_scopes();
|
|
3155
|
+
init_generalize();
|
|
2978
3156
|
}
|
|
2979
3157
|
});
|
|
2980
3158
|
|
|
@@ -3073,6 +3251,284 @@ var init_api = __esm({
|
|
|
3073
3251
|
}
|
|
3074
3252
|
});
|
|
3075
3253
|
|
|
3254
|
+
// src/v2/repo.ts
|
|
3255
|
+
import fs6 from "node:fs";
|
|
3256
|
+
import path5 from "node:path";
|
|
3257
|
+
import { execFileSync } from "node:child_process";
|
|
3258
|
+
function git(cwd, args, timeout = 1500) {
|
|
3259
|
+
try {
|
|
3260
|
+
return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
3261
|
+
} catch {
|
|
3262
|
+
return null;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
function repoRoot(cwd, storeRoot) {
|
|
3266
|
+
const key = `${cwd}
|
|
3267
|
+
${storeRoot ?? ""}`;
|
|
3268
|
+
const hit = rootCache.get(key);
|
|
3269
|
+
if (hit) return hit;
|
|
3270
|
+
const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
3271
|
+
let root = top || "";
|
|
3272
|
+
if (!root && storeRoot && path5.basename(storeRoot) === ".bunshin") {
|
|
3273
|
+
const parent = path5.dirname(storeRoot);
|
|
3274
|
+
if (path5.resolve(cwd).startsWith(path5.resolve(parent))) root = parent;
|
|
3275
|
+
}
|
|
3276
|
+
root = root || path5.resolve(cwd);
|
|
3277
|
+
rootCache.set(key, root);
|
|
3278
|
+
return root;
|
|
3279
|
+
}
|
|
3280
|
+
function parseRemote(url) {
|
|
3281
|
+
const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
3282
|
+
if (!clean2) return null;
|
|
3283
|
+
const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
|
|
3284
|
+
const pathPart = scp ? scp[1] : (() => {
|
|
3285
|
+
try {
|
|
3286
|
+
return new URL(clean2).pathname;
|
|
3287
|
+
} catch {
|
|
3288
|
+
return clean2;
|
|
3289
|
+
}
|
|
3290
|
+
})();
|
|
3291
|
+
const segs = pathPart.split("/").filter(Boolean);
|
|
3292
|
+
if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
|
|
3293
|
+
return segs[0] ?? null;
|
|
3294
|
+
}
|
|
3295
|
+
function repoName(root) {
|
|
3296
|
+
const url = git(root, ["remote", "get-url", "origin"]);
|
|
3297
|
+
return url && parseRemote(url) || path5.basename(root);
|
|
3298
|
+
}
|
|
3299
|
+
function repoCommit(root) {
|
|
3300
|
+
return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
|
|
3301
|
+
}
|
|
3302
|
+
function listDocs(root) {
|
|
3303
|
+
const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
|
|
3304
|
+
if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs6.existsSync(path5.join(root, rel)) && !deployedSkill(root, rel)).sort();
|
|
3305
|
+
const out = [];
|
|
3306
|
+
let visited = 0;
|
|
3307
|
+
const walk = (dir, rel, depth) => {
|
|
3308
|
+
if (depth > 8 || visited++ > 2e4) return;
|
|
3309
|
+
let entries;
|
|
3310
|
+
try {
|
|
3311
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
3312
|
+
} catch {
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
for (const e of entries) {
|
|
3316
|
+
if (e.isDirectory()) {
|
|
3317
|
+
if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(dir, e.name, ".git"))) walk(path5.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
|
|
3318
|
+
} else if (e.isFile()) {
|
|
3319
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
3320
|
+
if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
walk(root, "", 0);
|
|
3325
|
+
return out.sort();
|
|
3326
|
+
}
|
|
3327
|
+
function indexWorkingTree(v2, config, cwd, opts = {}) {
|
|
3328
|
+
const root = repoRoot(cwd, v2.store.root);
|
|
3329
|
+
const now = opts.now ?? Date.now();
|
|
3330
|
+
const cached = v2.readRepoIndex(root);
|
|
3331
|
+
const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
|
|
3332
|
+
const sameRepo = cached && cached.ownerTeam === ownerTeam;
|
|
3333
|
+
const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
|
|
3334
|
+
const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
|
|
3335
|
+
const paths = fresh ? cached.paths : listDocs(root);
|
|
3336
|
+
let changed = !fresh || !sameRepo || cached.repo !== repo;
|
|
3337
|
+
const files = {};
|
|
3338
|
+
for (const rel of paths) {
|
|
3339
|
+
const abs = path5.join(root, rel);
|
|
3340
|
+
let stat;
|
|
3341
|
+
try {
|
|
3342
|
+
stat = fs6.statSync(abs);
|
|
3343
|
+
} catch {
|
|
3344
|
+
changed = true;
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
|
|
3348
|
+
if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
|
|
3349
|
+
files[rel] = prev;
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
changed = true;
|
|
3353
|
+
try {
|
|
3354
|
+
const text2 = fs6.readFileSync(abs, "utf8");
|
|
3355
|
+
files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
|
|
3356
|
+
} catch {
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const index = {
|
|
3360
|
+
repo,
|
|
3361
|
+
ownerTeam,
|
|
3362
|
+
listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
|
|
3363
|
+
paths,
|
|
3364
|
+
files
|
|
3365
|
+
};
|
|
3366
|
+
if (changed) {
|
|
3367
|
+
try {
|
|
3368
|
+
v2.writeRepoIndex(root, index);
|
|
3369
|
+
} catch {
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
|
|
3373
|
+
}
|
|
3374
|
+
function relativeToRoot(root, file, cwd = root) {
|
|
3375
|
+
if (!file) return null;
|
|
3376
|
+
const abs = path5.isAbsolute(file) ? file : path5.resolve(cwd, file);
|
|
3377
|
+
let real = abs;
|
|
3378
|
+
let realRoot = root;
|
|
3379
|
+
try {
|
|
3380
|
+
realRoot = fs6.realpathSync(root);
|
|
3381
|
+
} catch {
|
|
3382
|
+
}
|
|
3383
|
+
try {
|
|
3384
|
+
real = fs6.realpathSync(abs);
|
|
3385
|
+
} catch {
|
|
3386
|
+
try {
|
|
3387
|
+
real = path5.join(fs6.realpathSync(path5.dirname(abs)), path5.basename(abs));
|
|
3388
|
+
} catch {
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
for (const [r, f] of [
|
|
3392
|
+
[realRoot, real],
|
|
3393
|
+
[root, abs]
|
|
3394
|
+
]) {
|
|
3395
|
+
const rel = path5.relative(r, f);
|
|
3396
|
+
if (rel && !rel.startsWith("..") && !path5.isAbsolute(rel)) return rel.split(path5.sep).join("/");
|
|
3397
|
+
}
|
|
3398
|
+
return null;
|
|
3399
|
+
}
|
|
3400
|
+
var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
|
|
3401
|
+
var init_repo = __esm({
|
|
3402
|
+
"src/v2/repo.ts"() {
|
|
3403
|
+
"use strict";
|
|
3404
|
+
init_v22();
|
|
3405
|
+
DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
|
|
3406
|
+
SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
|
|
3407
|
+
LIST_TTL_MS = 6e4;
|
|
3408
|
+
rootCache = /* @__PURE__ */ new Map();
|
|
3409
|
+
isDoc = (rel) => {
|
|
3410
|
+
const parts = rel.split("/");
|
|
3411
|
+
if (parts.some((p) => SKIP_DIRS.has(p))) return false;
|
|
3412
|
+
return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
|
|
3413
|
+
};
|
|
3414
|
+
deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs6.existsSync(path5.join(root, path5.posix.dirname(rel), ".bunshin.json"));
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
|
|
3418
|
+
// src/v2/source.ts
|
|
3419
|
+
var source_exports = {};
|
|
3420
|
+
__export(source_exports, {
|
|
3421
|
+
cwdInPool: () => cwdInPool,
|
|
3422
|
+
inPool: () => inPool,
|
|
3423
|
+
listFolders: () => listFolders,
|
|
3424
|
+
projectRootFor: () => projectRootFor,
|
|
3425
|
+
select: () => select,
|
|
3426
|
+
sourceFor: () => sourceFor,
|
|
3427
|
+
storeHome: () => storeHome,
|
|
3428
|
+
topFolder: () => topFolder
|
|
3429
|
+
});
|
|
3430
|
+
import fs7 from "node:fs";
|
|
3431
|
+
import path6 from "node:path";
|
|
3432
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3433
|
+
function storeHome(storeRoot) {
|
|
3434
|
+
return path6.dirname(path6.resolve(storeRoot));
|
|
3435
|
+
}
|
|
3436
|
+
function topFolder(cwd, storeRoot) {
|
|
3437
|
+
const rel = path6.relative(storeHome(storeRoot), path6.resolve(cwd));
|
|
3438
|
+
if (!rel || rel.startsWith("..") || path6.isAbsolute(rel)) return "";
|
|
3439
|
+
return rel.split(path6.sep)[0] ?? "";
|
|
3440
|
+
}
|
|
3441
|
+
function inPool(selection, folder) {
|
|
3442
|
+
if (!folder || !selection) return true;
|
|
3443
|
+
if (selection.exclude?.includes(folder)) return false;
|
|
3444
|
+
return selection.only ? selection.only.includes(folder) : true;
|
|
3445
|
+
}
|
|
3446
|
+
function cwdInPool(config, cwd, storeRoot) {
|
|
3447
|
+
return inPool(config.sources, topFolder(cwd, storeRoot));
|
|
3448
|
+
}
|
|
3449
|
+
function git2(cwd, args) {
|
|
3450
|
+
try {
|
|
3451
|
+
return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
|
|
3452
|
+
} catch {
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function sourceFor(cwd, storeRoot, ctx) {
|
|
3457
|
+
if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
|
|
3458
|
+
const key = `${path6.resolve(cwd)}
|
|
3459
|
+
${storeRoot}`;
|
|
3460
|
+
if (sourceCache.has(key)) return sourceCache.get(key);
|
|
3461
|
+
const found = findSource(cwd, storeRoot);
|
|
3462
|
+
sourceCache.set(key, found);
|
|
3463
|
+
return found;
|
|
3464
|
+
}
|
|
3465
|
+
function findSource(cwd, storeRoot) {
|
|
3466
|
+
const home = storeHome(storeRoot);
|
|
3467
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
3468
|
+
if (top) {
|
|
3469
|
+
const remote = git2(top, ["remote", "get-url", "origin"]);
|
|
3470
|
+
const named = remote ? parseRemote(remote) : null;
|
|
3471
|
+
if (named) return sourceId("repo", named);
|
|
3472
|
+
const rel = path6.relative(home, top);
|
|
3473
|
+
return sourceId("folder", rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel.split(path6.sep).join("/") : path6.basename(top));
|
|
3474
|
+
}
|
|
3475
|
+
const folder = topFolder(cwd, storeRoot);
|
|
3476
|
+
return folder ? sourceId("folder", folder) : void 0;
|
|
3477
|
+
}
|
|
3478
|
+
function projectRootFor(cwd, storeRoot) {
|
|
3479
|
+
const home = storeHome(storeRoot);
|
|
3480
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
3481
|
+
if (top) {
|
|
3482
|
+
const rel = path6.relative(home, top);
|
|
3483
|
+
if (!rel.startsWith("..") && !path6.isAbsolute(rel)) return top;
|
|
3484
|
+
}
|
|
3485
|
+
const folder = topFolder(cwd, storeRoot);
|
|
3486
|
+
return folder ? path6.join(home, folder) : home;
|
|
3487
|
+
}
|
|
3488
|
+
function listFolders(storeRoot, selection, opts = {}) {
|
|
3489
|
+
const home = storeHome(storeRoot);
|
|
3490
|
+
let names = [];
|
|
3491
|
+
try {
|
|
3492
|
+
names = fs7.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
3493
|
+
} catch {
|
|
3494
|
+
}
|
|
3495
|
+
return names.map((folder) => ({
|
|
3496
|
+
folder,
|
|
3497
|
+
included: inPool(selection, folder),
|
|
3498
|
+
...opts.sources ? { source: sourceFor(path6.join(home, folder), storeRoot) } : {}
|
|
3499
|
+
}));
|
|
3500
|
+
}
|
|
3501
|
+
function select(current, action, folders) {
|
|
3502
|
+
const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
|
|
3503
|
+
const only = current?.only;
|
|
3504
|
+
const exclude = current?.exclude ?? [];
|
|
3505
|
+
let next;
|
|
3506
|
+
switch (action) {
|
|
3507
|
+
case "all":
|
|
3508
|
+
return void 0;
|
|
3509
|
+
case "only":
|
|
3510
|
+
next = { only: uniq(folders) };
|
|
3511
|
+
break;
|
|
3512
|
+
case "include":
|
|
3513
|
+
next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
|
|
3514
|
+
break;
|
|
3515
|
+
case "exclude":
|
|
3516
|
+
next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
|
|
3517
|
+
break;
|
|
3518
|
+
}
|
|
3519
|
+
if (!next.exclude?.length) delete next.exclude;
|
|
3520
|
+
return next.only || next.exclude ? next : void 0;
|
|
3521
|
+
}
|
|
3522
|
+
var sourceCache;
|
|
3523
|
+
var init_source = __esm({
|
|
3524
|
+
"src/v2/source.ts"() {
|
|
3525
|
+
"use strict";
|
|
3526
|
+
init_v22();
|
|
3527
|
+
init_repo();
|
|
3528
|
+
sourceCache = /* @__PURE__ */ new Map();
|
|
3529
|
+
}
|
|
3530
|
+
});
|
|
3531
|
+
|
|
3076
3532
|
// ../core/src/types.ts
|
|
3077
3533
|
var init_types2 = __esm({
|
|
3078
3534
|
"../core/src/types.ts"() {
|
|
@@ -3593,9 +4049,9 @@ var init_src = __esm({
|
|
|
3593
4049
|
});
|
|
3594
4050
|
|
|
3595
4051
|
// src/v2/store.ts
|
|
3596
|
-
import
|
|
3597
|
-
import
|
|
3598
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4052
|
+
import fs8 from "node:fs";
|
|
4053
|
+
import path7 from "node:path";
|
|
4054
|
+
import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
|
|
3599
4055
|
function hexId(prefix) {
|
|
3600
4056
|
return `${prefix}_${randomBytes3(6).toString("hex")}`;
|
|
3601
4057
|
}
|
|
@@ -3617,7 +4073,7 @@ function normalizeSession(id, raw) {
|
|
|
3617
4073
|
hinted: list2(raw.hinted)
|
|
3618
4074
|
};
|
|
3619
4075
|
}
|
|
3620
|
-
var SAFE_ID, fileSafe, V2Store, addUnique;
|
|
4076
|
+
var SAFE_ID, fileSafe, V2Store, rootKey, addUnique;
|
|
3621
4077
|
var init_store2 = __esm({
|
|
3622
4078
|
"src/v2/store.ts"() {
|
|
3623
4079
|
"use strict";
|
|
@@ -3628,12 +4084,12 @@ var init_store2 = __esm({
|
|
|
3628
4084
|
V2Store = class {
|
|
3629
4085
|
constructor(store) {
|
|
3630
4086
|
this.store = store;
|
|
3631
|
-
this.dir =
|
|
4087
|
+
this.dir = path7.join(store.root, "v2");
|
|
3632
4088
|
}
|
|
3633
4089
|
store;
|
|
3634
4090
|
dir;
|
|
3635
4091
|
p(...parts) {
|
|
3636
|
-
return
|
|
4092
|
+
return path7.join(this.dir, ...parts);
|
|
3637
4093
|
}
|
|
3638
4094
|
// ── context / bundle / params ──
|
|
3639
4095
|
readContext() {
|
|
@@ -3714,9 +4170,9 @@ var init_store2 = __esm({
|
|
|
3714
4170
|
const dir = this.p("sessions");
|
|
3715
4171
|
if (!fileExists(dir)) return [];
|
|
3716
4172
|
const out = [];
|
|
3717
|
-
for (const f of
|
|
4173
|
+
for (const f of fs8.readdirSync(dir)) {
|
|
3718
4174
|
if (!f.endsWith(".json") || f === "current.json") continue;
|
|
3719
|
-
const raw = readJson(
|
|
4175
|
+
const raw = readJson(path7.join(dir, f), null);
|
|
3720
4176
|
if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
|
|
3721
4177
|
}
|
|
3722
4178
|
return out;
|
|
@@ -3744,7 +4200,7 @@ var init_store2 = __esm({
|
|
|
3744
4200
|
let at = raw.at;
|
|
3745
4201
|
if (!at) {
|
|
3746
4202
|
try {
|
|
3747
|
-
at =
|
|
4203
|
+
at = fs8.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
|
|
3748
4204
|
} catch {
|
|
3749
4205
|
at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3750
4206
|
}
|
|
@@ -3752,11 +4208,12 @@ var init_store2 = __esm({
|
|
|
3752
4208
|
return { status: raw.status, source: raw.source || "robot", ...raw.metrics ? { metrics: raw.metrics } : {}, at };
|
|
3753
4209
|
}
|
|
3754
4210
|
// ── repo index / sync state ──
|
|
3755
|
-
|
|
3756
|
-
|
|
4211
|
+
/** One index per working tree: a store covering a folder of repositories keeps each one's. */
|
|
4212
|
+
readRepoIndex(root) {
|
|
4213
|
+
return readJson(this.p("repo-index", `${rootKey(root)}.json`), null);
|
|
3757
4214
|
}
|
|
3758
|
-
writeRepoIndex(index) {
|
|
3759
|
-
writeJsonAtomic(this.p("repo-index.json
|
|
4215
|
+
writeRepoIndex(root, index) {
|
|
4216
|
+
writeJsonAtomic(this.p("repo-index", `${rootKey(root)}.json`), index);
|
|
3760
4217
|
}
|
|
3761
4218
|
readSyncState() {
|
|
3762
4219
|
return readJson(this.p("sync.json"), {});
|
|
@@ -3770,13 +4227,13 @@ var init_store2 = __esm({
|
|
|
3770
4227
|
/** Exclusive lock file; returns a release function, or null when another process holds it. */
|
|
3771
4228
|
tryLock(name, staleMs = 10 * 6e4) {
|
|
3772
4229
|
const file = this.p("locks", `${fileSafe(name)}.lock`);
|
|
3773
|
-
ensureDir(
|
|
4230
|
+
ensureDir(path7.dirname(file));
|
|
3774
4231
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3775
4232
|
try {
|
|
3776
|
-
|
|
4233
|
+
fs8.writeFileSync(file, String(process.pid), { flag: "wx" });
|
|
3777
4234
|
return () => {
|
|
3778
4235
|
try {
|
|
3779
|
-
|
|
4236
|
+
fs8.unlinkSync(file);
|
|
3780
4237
|
} catch {
|
|
3781
4238
|
}
|
|
3782
4239
|
};
|
|
@@ -3784,8 +4241,8 @@ var init_store2 = __esm({
|
|
|
3784
4241
|
if (err.code !== "EEXIST") return null;
|
|
3785
4242
|
let stale = false;
|
|
3786
4243
|
try {
|
|
3787
|
-
const owner = Number(
|
|
3788
|
-
const age = Date.now() -
|
|
4244
|
+
const owner = Number(fs8.readFileSync(file, "utf8"));
|
|
4245
|
+
const age = Date.now() - fs8.statSync(file).mtimeMs;
|
|
3789
4246
|
if (age > staleMs) stale = true;
|
|
3790
4247
|
else if (Number.isInteger(owner) && owner > 0) {
|
|
3791
4248
|
try {
|
|
@@ -3799,7 +4256,7 @@ var init_store2 = __esm({
|
|
|
3799
4256
|
}
|
|
3800
4257
|
if (!stale) return null;
|
|
3801
4258
|
try {
|
|
3802
|
-
|
|
4259
|
+
fs8.unlinkSync(file);
|
|
3803
4260
|
} catch {
|
|
3804
4261
|
}
|
|
3805
4262
|
}
|
|
@@ -3816,14 +4273,15 @@ var init_store2 = __esm({
|
|
|
3816
4273
|
const dir = this.p(name);
|
|
3817
4274
|
if (!fileExists(dir)) return [];
|
|
3818
4275
|
const out = [];
|
|
3819
|
-
for (const f of
|
|
4276
|
+
for (const f of fs8.readdirSync(dir)) {
|
|
3820
4277
|
if (!f.endsWith(".json")) continue;
|
|
3821
|
-
const v = readJson(
|
|
4278
|
+
const v = readJson(path7.join(dir, f), null);
|
|
3822
4279
|
if (v) out.push(v);
|
|
3823
4280
|
}
|
|
3824
4281
|
return out;
|
|
3825
4282
|
}
|
|
3826
4283
|
};
|
|
4284
|
+
rootKey = (root) => createHash2("sha256").update(path7.resolve(root)).digest("hex").slice(0, 16);
|
|
3827
4285
|
addUnique = (list2, ...ids) => {
|
|
3828
4286
|
for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
|
|
3829
4287
|
};
|
|
@@ -3831,63 +4289,49 @@ var init_store2 = __esm({
|
|
|
3831
4289
|
});
|
|
3832
4290
|
|
|
3833
4291
|
// src/v2/deploy.ts
|
|
3834
|
-
import {
|
|
3835
|
-
import
|
|
4292
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4293
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4294
|
+
import fs9 from "node:fs";
|
|
3836
4295
|
import os2 from "node:os";
|
|
3837
|
-
import
|
|
4296
|
+
import path8 from "node:path";
|
|
3838
4297
|
function deployWorkspace(env = process.env) {
|
|
3839
|
-
if (env.BUNSHIN_DEPLOY_WORKSPACE) return
|
|
4298
|
+
if (env.BUNSHIN_DEPLOY_WORKSPACE) return path8.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
|
|
3840
4299
|
try {
|
|
3841
|
-
const ws = JSON.parse(
|
|
4300
|
+
const ws = JSON.parse(fs9.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
|
|
3842
4301
|
return typeof ws === "string" && ws ? ws : null;
|
|
3843
4302
|
} catch {
|
|
3844
4303
|
return null;
|
|
3845
4304
|
}
|
|
3846
4305
|
}
|
|
3847
|
-
function deployPaths(env = process.env) {
|
|
4306
|
+
function deployPaths(env = process.env, orgId) {
|
|
3848
4307
|
const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
|
|
3849
4308
|
const ws = deployWorkspace(env);
|
|
4309
|
+
const state = path8.join(home, ".bunshin", "deploy");
|
|
4310
|
+
const shared = { claude: path8.join(ws ?? home, ".claude", "skills"), codex: path8.join(ws ?? home, ".codex", "skills") };
|
|
4311
|
+
const org = orgId ? path8.join(state, orgKey(orgId)) : null;
|
|
3850
4312
|
return {
|
|
3851
4313
|
home,
|
|
3852
|
-
claudeSkills:
|
|
3853
|
-
codexSkills:
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4314
|
+
claudeSkills: org ? path8.join(org, "claude-skills") : shared.claude,
|
|
4315
|
+
codexSkills: org ? path8.join(org, "codex-skills") : shared.codex,
|
|
4316
|
+
...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path8.join(home, ".claude", "skills"), path8.join(home, ".codex", "skills"), path8.join(home, ".bunshin", "library")])] } : {},
|
|
4317
|
+
claudeMd: ws ? path8.join(ws, "CLAUDE.md") : path8.join(home, ".claude", "CLAUDE.md"),
|
|
4318
|
+
agentsMd: ws ? path8.join(ws, "AGENTS.md") : path8.join(home, ".codex", "AGENTS.md"),
|
|
4319
|
+
library: org ? path8.join(org, "library") : path8.join(home, ".bunshin", "library"),
|
|
4320
|
+
// Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
|
|
4321
|
+
bin: path8.join(home, ".bunshin", "bin"),
|
|
4322
|
+
state
|
|
3859
4323
|
};
|
|
3860
4324
|
}
|
|
3861
|
-
function setDeployWorkspace(dir, env = process.env) {
|
|
3862
|
-
const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
|
|
3863
|
-
const ws = dir ? path6.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
|
|
3864
|
-
if (ws && !fs7.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
|
|
3865
|
-
const stateDir = path6.join(home, ".bunshin", "deploy");
|
|
3866
|
-
const deployed = fs7.existsSync(stateDir) && fs7.readdirSync(stateDir).some((f) => {
|
|
3867
|
-
if (f === "settings.json" || !f.endsWith(".json")) return false;
|
|
3868
|
-
try {
|
|
3869
|
-
return Object.keys(JSON.parse(fs7.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
|
|
3870
|
-
} catch {
|
|
3871
|
-
return false;
|
|
3872
|
-
}
|
|
3873
|
-
});
|
|
3874
|
-
if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
|
|
3875
|
-
throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
|
|
3876
|
-
}
|
|
3877
|
-
fs7.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
|
|
3878
|
-
fs7.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
|
|
3879
|
-
return ws;
|
|
3880
|
-
}
|
|
3881
4325
|
function statePath(p, orgId) {
|
|
3882
|
-
return
|
|
4326
|
+
return path8.join(p.state, `${orgKey(orgId)}.json`);
|
|
3883
4327
|
}
|
|
3884
4328
|
function readState(p, orgId) {
|
|
3885
4329
|
const s = readJson(statePath(p, orgId), null);
|
|
3886
|
-
return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
|
|
4330
|
+
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 } : {} };
|
|
3887
4331
|
}
|
|
3888
4332
|
function readText(file) {
|
|
3889
4333
|
try {
|
|
3890
|
-
return
|
|
4334
|
+
return fs9.readFileSync(file, "utf8");
|
|
3891
4335
|
} catch {
|
|
3892
4336
|
return null;
|
|
3893
4337
|
}
|
|
@@ -3895,13 +4339,13 @@ function readText(file) {
|
|
|
3895
4339
|
function writeFile(file, content, mode) {
|
|
3896
4340
|
writeTextAtomic(file, content);
|
|
3897
4341
|
try {
|
|
3898
|
-
|
|
4342
|
+
fs9.chmodSync(file, mode === "x" ? 493 : 420);
|
|
3899
4343
|
} catch {
|
|
3900
4344
|
}
|
|
3901
4345
|
}
|
|
3902
4346
|
function removeFile(file) {
|
|
3903
4347
|
try {
|
|
3904
|
-
|
|
4348
|
+
fs9.unlinkSync(file);
|
|
3905
4349
|
} catch {
|
|
3906
4350
|
}
|
|
3907
4351
|
}
|
|
@@ -3909,33 +4353,33 @@ function pruneDirs(dir, stop) {
|
|
|
3909
4353
|
let d = dir;
|
|
3910
4354
|
while (d.startsWith(stop) && d !== stop) {
|
|
3911
4355
|
try {
|
|
3912
|
-
if (
|
|
3913
|
-
|
|
4356
|
+
if (fs9.readdirSync(d).length) return;
|
|
4357
|
+
fs9.rmdirSync(d);
|
|
3914
4358
|
} catch {
|
|
3915
4359
|
return;
|
|
3916
4360
|
}
|
|
3917
|
-
d =
|
|
4361
|
+
d = path8.dirname(d);
|
|
3918
4362
|
}
|
|
3919
4363
|
}
|
|
3920
4364
|
function listFiles(dir, base = dir) {
|
|
3921
4365
|
let entries;
|
|
3922
4366
|
try {
|
|
3923
|
-
entries =
|
|
4367
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
3924
4368
|
} catch {
|
|
3925
4369
|
return [];
|
|
3926
4370
|
}
|
|
3927
4371
|
const out = [];
|
|
3928
4372
|
for (const e of entries) {
|
|
3929
|
-
const abs =
|
|
4373
|
+
const abs = path8.join(dir, e.name);
|
|
3930
4374
|
if (e.isDirectory()) out.push(...listFiles(abs, base));
|
|
3931
|
-
else if (e.isFile()) out.push(
|
|
4375
|
+
else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
|
|
3932
4376
|
}
|
|
3933
4377
|
return out.sort();
|
|
3934
4378
|
}
|
|
3935
4379
|
function childDir(root, name) {
|
|
3936
4380
|
if (!name || !SAFE_NAME.test(name)) return null;
|
|
3937
|
-
const dir =
|
|
3938
|
-
return
|
|
4381
|
+
const dir = path8.join(root, name);
|
|
4382
|
+
return path8.dirname(path8.resolve(dir)) === path8.resolve(root) && insideRoot(root, dir) ? dir : null;
|
|
3939
4383
|
}
|
|
3940
4384
|
function planDeploy(bundle, p, harnesses) {
|
|
3941
4385
|
const active = bundle.sections.filter((s) => s.status === "active" || s.status === "stale");
|
|
@@ -4042,7 +4486,7 @@ function removeBlock(file) {
|
|
|
4042
4486
|
if (start < 0 || end < start) return false;
|
|
4043
4487
|
const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
|
|
4044
4488
|
if (rest) writeTextAtomic(file, rest + "\n");
|
|
4045
|
-
else
|
|
4489
|
+
else fs9.unlinkSync(file);
|
|
4046
4490
|
return true;
|
|
4047
4491
|
}
|
|
4048
4492
|
function writeBlock(file, block2) {
|
|
@@ -4063,7 +4507,7 @@ function skillIndex(skills, p) {
|
|
|
4063
4507
|
"",
|
|
4064
4508
|
"When a task matches a skill below, read its SKILL.md and follow it before doing the work by hand.",
|
|
4065
4509
|
"",
|
|
4066
|
-
...skills.map((s) => `- **${s.skill.name}** (\`${
|
|
4510
|
+
...skills.map((s) => `- **${s.skill.name}** (\`${path8.join(p.codexSkills, s.skill.name, "SKILL.md")}\`): ${neutralizeMarkers(s.skill.description).replace(/\s+/g, " ").trim()}`)
|
|
4067
4511
|
];
|
|
4068
4512
|
return lines.join("\n");
|
|
4069
4513
|
}
|
|
@@ -4112,7 +4556,7 @@ function captureDoc(c, doc, out) {
|
|
|
4112
4556
|
for (const dir of doc.dirs ?? []) {
|
|
4113
4557
|
for (const rel of listFiles(dir)) {
|
|
4114
4558
|
if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
|
|
4115
|
-
const abs =
|
|
4559
|
+
const abs = path8.join(dir, rel);
|
|
4116
4560
|
const content = readText(abs);
|
|
4117
4561
|
if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
|
|
4118
4562
|
byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
|
|
@@ -4151,18 +4595,18 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
4151
4595
|
for (const root of roots) {
|
|
4152
4596
|
let names = [];
|
|
4153
4597
|
try {
|
|
4154
|
-
names =
|
|
4598
|
+
names = fs9.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4155
4599
|
} catch {
|
|
4156
4600
|
continue;
|
|
4157
4601
|
}
|
|
4158
4602
|
for (const name of names) {
|
|
4159
|
-
const dir =
|
|
4603
|
+
const dir = path8.join(root, name);
|
|
4160
4604
|
if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
|
|
4161
|
-
const skillMd = readText(
|
|
4605
|
+
const skillMd = readText(path8.join(dir, "SKILL.md"));
|
|
4162
4606
|
if (skillMd === null) continue;
|
|
4163
4607
|
let born = 0;
|
|
4164
4608
|
try {
|
|
4165
|
-
const st =
|
|
4609
|
+
const st = fs9.statSync(path8.join(dir, "SKILL.md"));
|
|
4166
4610
|
born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
|
|
4167
4611
|
} catch {
|
|
4168
4612
|
continue;
|
|
@@ -4171,8 +4615,8 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
4171
4615
|
const files = {};
|
|
4172
4616
|
for (const rel of listFiles(dir).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
|
|
4173
4617
|
if (rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
4174
|
-
const content = readText(
|
|
4175
|
-
if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(
|
|
4618
|
+
const content = readText(path8.join(dir, rel));
|
|
4619
|
+
if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir, rel)) ? { mode: "x" } : {} };
|
|
4176
4620
|
}
|
|
4177
4621
|
const hash = sha(JSON.stringify([skillMd, files]));
|
|
4178
4622
|
if (state.created[name]?.hash === hash) continue;
|
|
@@ -4205,7 +4649,7 @@ function captureInto(v2, config, ctx, state, p, harnesses, now, incoming, captur
|
|
|
4205
4649
|
return captured.map((s) => ({ suggestionId: s.id, op: s.op.op, ...s.target.sectionId ? { sectionId: s.target.sectionId } : {}, name: s.summary ?? s.op.op }));
|
|
4206
4650
|
}
|
|
4207
4651
|
function captureEdits(v2, config, ctx, opts = {}) {
|
|
4208
|
-
const p = opts.paths ?? deployPaths();
|
|
4652
|
+
const p = opts.paths ?? deployPaths(process.env, config.orgId);
|
|
4209
4653
|
const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
|
|
4210
4654
|
const release = lockDeploy(p);
|
|
4211
4655
|
if (!release) return [];
|
|
@@ -4220,14 +4664,14 @@ function captureEdits(v2, config, ctx, opts = {}) {
|
|
|
4220
4664
|
}
|
|
4221
4665
|
function lockDeploy(p) {
|
|
4222
4666
|
ensureDir(p.state);
|
|
4223
|
-
const file =
|
|
4667
|
+
const file = path8.join(p.state, ".lock");
|
|
4224
4668
|
for (let i = 0; i < 2; i++) {
|
|
4225
4669
|
try {
|
|
4226
|
-
|
|
4670
|
+
fs9.writeFileSync(file, String(process.pid), { flag: "wx" });
|
|
4227
4671
|
return () => removeFile(file);
|
|
4228
4672
|
} catch {
|
|
4229
4673
|
try {
|
|
4230
|
-
const age = Date.now() -
|
|
4674
|
+
const age = Date.now() - fs9.statSync(file).mtimeMs;
|
|
4231
4675
|
const pid = Number(readText(file));
|
|
4232
4676
|
let alive = age < 12e4;
|
|
4233
4677
|
if (alive && pid > 0) {
|
|
@@ -4245,11 +4689,14 @@ function lockDeploy(p) {
|
|
|
4245
4689
|
}
|
|
4246
4690
|
return null;
|
|
4247
4691
|
}
|
|
4692
|
+
function linkHarnesses(config) {
|
|
4693
|
+
return [.../* @__PURE__ */ new Set([config.harness, ...config.deploy?.harnesses ?? []])].filter((h) => h === "claude-code" || h === "codex");
|
|
4694
|
+
}
|
|
4248
4695
|
function deployEnabled(config, ctx) {
|
|
4249
4696
|
return ctx.kind === "person" && config.deploy?.enabled !== false;
|
|
4250
4697
|
}
|
|
4251
4698
|
function deployLibrary(v2, config, ctx, bundle, opts = {}) {
|
|
4252
|
-
const p = opts.paths ?? deployPaths();
|
|
4699
|
+
const p = opts.paths ?? deployPaths(process.env, config.orgId);
|
|
4253
4700
|
const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
|
|
4254
4701
|
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4255
4702
|
const release = lockDeploy(p);
|
|
@@ -4266,6 +4713,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4266
4713
|
const plan = planDeploy(bundle, p, harnesses);
|
|
4267
4714
|
report.warnings.push(...plan.warnings);
|
|
4268
4715
|
const owned = (abs) => [p.claudeSkills, p.codexSkills, p.library].some((r) => insideRoot(r, abs));
|
|
4716
|
+
const removable = (abs) => owned(abs) || (p.legacy ?? []).some((r) => insideRoot(r, abs));
|
|
4269
4717
|
if (opts.capture !== false) report.captured = captureInto(v2, config, ctx, state, p, harnesses, now, plan.skills.map((x) => x.skill.name), opts.captureNewSkills);
|
|
4270
4718
|
const decided = new Map(bundle.decisions.map((d) => [d.suggestionId, d.decision.status]));
|
|
4271
4719
|
const localStatus = (id) => decided.get(id) ?? v2.readSuggestion(id)?.status;
|
|
@@ -4290,7 +4738,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4290
4738
|
const foreign = /* @__PURE__ */ new Set();
|
|
4291
4739
|
if (d.kind === "skill" && !prev) {
|
|
4292
4740
|
for (const dir of d.dirs ?? []) {
|
|
4293
|
-
const cur = readText(
|
|
4741
|
+
const cur = readText(path8.join(dir, "SKILL.md"));
|
|
4294
4742
|
if (cur === null || cur === d.section.body || state.created[d.name]) continue;
|
|
4295
4743
|
foreign.add(dir);
|
|
4296
4744
|
report.warnings.push(`${dir} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
|
|
@@ -4302,7 +4750,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4302
4750
|
targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
|
|
4303
4751
|
continue;
|
|
4304
4752
|
}
|
|
4305
|
-
if ([...foreign].some((dir) => t.abs.startsWith(dir +
|
|
4753
|
+
if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
|
|
4306
4754
|
if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
|
|
4307
4755
|
report.warnings.push(`${t.abs} not written: it leads outside its folder`);
|
|
4308
4756
|
continue;
|
|
@@ -4311,10 +4759,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4311
4759
|
const existing = readText(t.abs);
|
|
4312
4760
|
try {
|
|
4313
4761
|
if (existing !== content) {
|
|
4314
|
-
ensureDir(
|
|
4762
|
+
ensureDir(path8.dirname(t.abs));
|
|
4315
4763
|
writeFile(t.abs, content, t.mode);
|
|
4316
4764
|
report.written++;
|
|
4317
|
-
} else if (t.mode === "x" && !isExecutable(t.abs))
|
|
4765
|
+
} else if (t.mode === "x" && !isExecutable(t.abs)) fs9.chmodSync(t.abs, 493);
|
|
4318
4766
|
} catch (err) {
|
|
4319
4767
|
report.warnings.push(`${t.abs} not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
4320
4768
|
continue;
|
|
@@ -4323,18 +4771,23 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4323
4771
|
prevByAbs.delete(`${t.abs}#${t.role}`);
|
|
4324
4772
|
}
|
|
4325
4773
|
for (const old of prevByAbs.values()) {
|
|
4326
|
-
if (old.role === "block" || d.contents.has(old.abs) || !
|
|
4774
|
+
if (old.role === "block" || d.contents.has(old.abs) || !removable(old.abs)) continue;
|
|
4327
4775
|
const cur = readText(old.abs);
|
|
4328
4776
|
if (cur !== null && sha(cur) === old.sha) {
|
|
4329
4777
|
removeFile(old.abs);
|
|
4330
|
-
pruneDirs(
|
|
4778
|
+
pruneDirs(path8.dirname(old.abs), d.kind === "skill" ? path8.dirname(path8.dirname(old.abs)) : p.library);
|
|
4331
4779
|
report.removed++;
|
|
4332
4780
|
}
|
|
4333
4781
|
}
|
|
4334
4782
|
if (d.kind === "skill") {
|
|
4783
|
+
for (const dir of prev?.dirs ?? []) {
|
|
4784
|
+
if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
|
|
4785
|
+
removeFile(path8.join(dir, MANIFEST));
|
|
4786
|
+
pruneDirs(dir, path8.dirname(dir));
|
|
4787
|
+
}
|
|
4335
4788
|
for (const dir of d.dirs ?? []) {
|
|
4336
4789
|
const manifest = safeJoin(dir, MANIFEST);
|
|
4337
|
-
if (foreign.has(dir) || !manifest || !
|
|
4790
|
+
if (foreign.has(dir) || !manifest || !fs9.existsSync(path8.join(dir, "SKILL.md"))) continue;
|
|
4338
4791
|
writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
|
|
4339
4792
|
}
|
|
4340
4793
|
report.skills++;
|
|
@@ -4350,7 +4803,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4350
4803
|
continue;
|
|
4351
4804
|
}
|
|
4352
4805
|
for (const t of doc.targets) {
|
|
4353
|
-
if (t.role === "block" || !
|
|
4806
|
+
if (t.role === "block" || !removable(t.abs)) continue;
|
|
4354
4807
|
const cur = readText(t.abs);
|
|
4355
4808
|
if (cur !== null && sha(cur) === t.sha) {
|
|
4356
4809
|
removeFile(t.abs);
|
|
@@ -4358,13 +4811,13 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4358
4811
|
}
|
|
4359
4812
|
}
|
|
4360
4813
|
for (const dir of doc.dirs ?? []) {
|
|
4361
|
-
if (!
|
|
4362
|
-
removeFile(
|
|
4363
|
-
pruneDirs(dir,
|
|
4364
|
-
if (
|
|
4814
|
+
if (!removable(dir)) continue;
|
|
4815
|
+
removeFile(path8.join(dir, MANIFEST));
|
|
4816
|
+
pruneDirs(dir, path8.dirname(dir));
|
|
4817
|
+
if (fs9.existsSync(dir)) {
|
|
4365
4818
|
const left = listFiles(dir);
|
|
4366
4819
|
if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
|
|
4367
|
-
else pruneDirs(dir,
|
|
4820
|
+
else pruneDirs(dir, path8.dirname(dir));
|
|
4368
4821
|
}
|
|
4369
4822
|
}
|
|
4370
4823
|
}
|
|
@@ -4380,17 +4833,17 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4380
4833
|
if (d.kind === "skill") continue;
|
|
4381
4834
|
for (const t of d.targets) {
|
|
4382
4835
|
if (t.role === "block" || t.mode !== "x" || !/(^|\/)bin\/[^/]+$/.test(t.rel) || !owned(t.abs)) continue;
|
|
4383
|
-
const link =
|
|
4836
|
+
const link = path8.join(p.bin, path8.basename(t.abs));
|
|
4384
4837
|
ensureDir(p.bin);
|
|
4385
4838
|
try {
|
|
4386
|
-
const cur =
|
|
4839
|
+
const cur = fs9.readlinkSync(link);
|
|
4387
4840
|
if (cur !== t.abs) {
|
|
4388
|
-
|
|
4389
|
-
|
|
4841
|
+
fs9.unlinkSync(link);
|
|
4842
|
+
fs9.symlinkSync(t.abs, link);
|
|
4390
4843
|
}
|
|
4391
4844
|
} catch {
|
|
4392
4845
|
try {
|
|
4393
|
-
if (!
|
|
4846
|
+
if (!fs9.existsSync(link)) fs9.symlinkSync(t.abs, link);
|
|
4394
4847
|
} catch {
|
|
4395
4848
|
}
|
|
4396
4849
|
}
|
|
@@ -4401,15 +4854,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4401
4854
|
if (harnesses.has("claude-code")) {
|
|
4402
4855
|
for (const pack of packs) {
|
|
4403
4856
|
const dir = childDir(p.library, pack);
|
|
4404
|
-
if (!dir || !insideRoot(dir,
|
|
4405
|
-
const link =
|
|
4857
|
+
if (!dir || !insideRoot(dir, path8.join(dir, ".claude", "x"))) continue;
|
|
4858
|
+
const link = path8.join(dir, ".claude", "skills");
|
|
4406
4859
|
try {
|
|
4407
|
-
|
|
4860
|
+
fs9.lstatSync(link);
|
|
4861
|
+
if (fs9.readlinkSync(link) !== p.claudeSkills) {
|
|
4862
|
+
fs9.unlinkSync(link);
|
|
4863
|
+
fs9.symlinkSync(p.claudeSkills, link);
|
|
4864
|
+
}
|
|
4408
4865
|
} catch {
|
|
4409
|
-
ensureDir(
|
|
4866
|
+
ensureDir(path8.dirname(link));
|
|
4410
4867
|
ensureDir(p.claudeSkills);
|
|
4411
4868
|
try {
|
|
4412
|
-
|
|
4869
|
+
fs9.symlinkSync(p.claudeSkills, link);
|
|
4413
4870
|
} catch {
|
|
4414
4871
|
}
|
|
4415
4872
|
}
|
|
@@ -4418,32 +4875,53 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4418
4875
|
for (const old of state.links) {
|
|
4419
4876
|
if (links.has(old)) continue;
|
|
4420
4877
|
try {
|
|
4421
|
-
const target =
|
|
4422
|
-
if (!
|
|
4878
|
+
const target = fs9.readlinkSync(old);
|
|
4879
|
+
if (!fs9.existsSync(target)) fs9.unlinkSync(old);
|
|
4423
4880
|
} catch {
|
|
4424
4881
|
}
|
|
4425
4882
|
}
|
|
4426
|
-
|
|
4883
|
+
let linked = state.linked ?? [];
|
|
4884
|
+
if (p.legacy) {
|
|
4885
|
+
const deployed = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length);
|
|
4886
|
+
const layerOf = new Map(plan.skills.map((x) => [x.skill.name, x.layer]));
|
|
4887
|
+
const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path8.resolve(opts.linkRoot)] : []])];
|
|
4888
|
+
linked = [];
|
|
4889
|
+
for (const root of roots) {
|
|
4890
|
+
let source;
|
|
4891
|
+
try {
|
|
4892
|
+
source = sourceFor(root, v2.store.root, ctx);
|
|
4893
|
+
} catch {
|
|
4894
|
+
source = void 0;
|
|
4895
|
+
}
|
|
4896
|
+
const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle.scopes ? { tree: bundle.scopes } : {} });
|
|
4897
|
+
const names = deployed.filter((d) => inBoundary({ layer: layerOf.get(d.name) ?? "org" }, reader)).map((d) => d.name);
|
|
4898
|
+
const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
|
|
4899
|
+
report.warnings.push(...res.warnings);
|
|
4900
|
+
if (res.kept) linked.push(root);
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
const saved = { ...state, docs: next, links: [...links], ...linked.length ? { linked } : {}, ...bundle.libraryVersion ? { libraryVersion: bundle.libraryVersion } : {} };
|
|
4904
|
+
if (!linked.length) delete saved.linked;
|
|
4427
4905
|
writeJsonAtomic(statePath(p, config.orgId), saved);
|
|
4428
4906
|
report.libraryVersion = bundle.libraryVersion;
|
|
4429
4907
|
return report;
|
|
4430
4908
|
}
|
|
4431
|
-
function deployedState(config, paths = deployPaths()) {
|
|
4909
|
+
function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
|
|
4432
4910
|
const s = readState(paths, config.orgId);
|
|
4433
|
-
return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, docs: Object.values(s.docs) };
|
|
4911
|
+
return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
|
|
4434
4912
|
}
|
|
4435
4913
|
function materialize(sections, dir) {
|
|
4436
4914
|
let files = 0;
|
|
4437
4915
|
const put = (rel, content, mode) => {
|
|
4438
4916
|
const abs = safeJoin(dir, rel);
|
|
4439
4917
|
if (!abs) return;
|
|
4440
|
-
ensureDir(
|
|
4918
|
+
ensureDir(path8.dirname(abs));
|
|
4441
4919
|
writeFile(abs, content, mode);
|
|
4442
4920
|
files++;
|
|
4443
4921
|
};
|
|
4444
4922
|
for (const s of sections.filter((x) => x.status === "active" || x.status === "stale")) {
|
|
4445
4923
|
if (s.skill) {
|
|
4446
|
-
const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ?
|
|
4924
|
+
const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ? path8.posix.dirname(s.packPath) : `.claude/skills/${s.skill.name}`;
|
|
4447
4925
|
put(`${base}/SKILL.md`, s.body);
|
|
4448
4926
|
for (const [rel, f] of Object.entries(s.skill.files ?? {})) put(`${base}/${rel}`, f.content, f.mode);
|
|
4449
4927
|
continue;
|
|
@@ -4453,22 +4931,106 @@ function materialize(sections, dir) {
|
|
|
4453
4931
|
}
|
|
4454
4932
|
return { files };
|
|
4455
4933
|
}
|
|
4456
|
-
|
|
4934
|
+
function linkSkills(p, root, harnesses, names) {
|
|
4935
|
+
const out = { linked: 0, removed: 0, kept: false, warnings: [] };
|
|
4936
|
+
const abs = path8.resolve(root);
|
|
4937
|
+
if (abs === path8.resolve(p.home) || abs === path8.resolve(os2.homedir()) || !fs9.existsSync(abs)) return out;
|
|
4938
|
+
const managed = (target) => target.startsWith(path8.resolve(p.state) + path8.sep);
|
|
4939
|
+
const excluded = [];
|
|
4940
|
+
for (const h of harnesses) {
|
|
4941
|
+
const spec = LINK_DIRS[h];
|
|
4942
|
+
if (!spec) continue;
|
|
4943
|
+
const dir = path8.join(abs, spec.rel);
|
|
4944
|
+
const canonical = path8.resolve(p[spec.key]);
|
|
4945
|
+
const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
|
|
4946
|
+
let entries = [];
|
|
4947
|
+
try {
|
|
4948
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
4949
|
+
} catch {
|
|
4950
|
+
}
|
|
4951
|
+
for (const e of entries) {
|
|
4952
|
+
if (!e.isSymbolicLink()) continue;
|
|
4953
|
+
const link = path8.join(dir, e.name);
|
|
4954
|
+
let target = "";
|
|
4955
|
+
try {
|
|
4956
|
+
target = path8.resolve(dir, fs9.readlinkSync(link));
|
|
4957
|
+
} catch {
|
|
4958
|
+
continue;
|
|
4959
|
+
}
|
|
4960
|
+
if (!managed(target)) continue;
|
|
4961
|
+
if (want.has(e.name) && target === path8.join(canonical, e.name)) {
|
|
4962
|
+
want.delete(e.name);
|
|
4963
|
+
excluded.push(path8.join(spec.rel, e.name));
|
|
4964
|
+
continue;
|
|
4965
|
+
}
|
|
4966
|
+
removeFile(link);
|
|
4967
|
+
out.removed++;
|
|
4968
|
+
}
|
|
4969
|
+
for (const name of want) {
|
|
4970
|
+
const link = path8.join(dir, name);
|
|
4971
|
+
try {
|
|
4972
|
+
fs9.lstatSync(link);
|
|
4973
|
+
out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
|
|
4974
|
+
continue;
|
|
4975
|
+
} catch {
|
|
4976
|
+
}
|
|
4977
|
+
try {
|
|
4978
|
+
ensureDir(dir);
|
|
4979
|
+
fs9.symlinkSync(path8.join(canonical, name), link, "dir");
|
|
4980
|
+
excluded.push(path8.join(spec.rel, name));
|
|
4981
|
+
out.linked++;
|
|
4982
|
+
} catch (err) {
|
|
4983
|
+
out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
pruneDirs(dir, abs);
|
|
4987
|
+
}
|
|
4988
|
+
out.kept = excluded.length > 0;
|
|
4989
|
+
gitExclude(abs, excluded);
|
|
4990
|
+
return out;
|
|
4991
|
+
}
|
|
4992
|
+
function gitExclude(root, rels) {
|
|
4993
|
+
if (!fs9.existsSync(path8.join(root, ".git"))) return;
|
|
4994
|
+
let file;
|
|
4995
|
+
try {
|
|
4996
|
+
file = path8.resolve(root, execFileSync3("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
|
|
4997
|
+
} catch {
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
const cur = readText(file) ?? "";
|
|
5001
|
+
const start = cur.indexOf(EXCLUDE_BEGIN);
|
|
5002
|
+
const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
|
|
5003
|
+
const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
|
|
5004
|
+
const block2 = rels.length ? `${EXCLUDE_BEGIN}
|
|
5005
|
+
${[...new Set(rels)].sort().map((r) => `/${r.split(path8.sep).join("/")}`).join("\n")}
|
|
5006
|
+
${EXCLUDE_END}
|
|
5007
|
+
` : "";
|
|
5008
|
+
const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
|
|
5009
|
+
if (next === cur) return;
|
|
5010
|
+
try {
|
|
5011
|
+
ensureDir(path8.dirname(file));
|
|
5012
|
+
fs9.writeFileSync(file, next);
|
|
5013
|
+
} catch {
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
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;
|
|
4457
5017
|
var init_deploy = __esm({
|
|
4458
5018
|
"src/v2/deploy.ts"() {
|
|
4459
5019
|
"use strict";
|
|
4460
5020
|
init_v22();
|
|
5021
|
+
init_source();
|
|
4461
5022
|
init_util();
|
|
4462
5023
|
init_store2();
|
|
4463
|
-
settingsFile = (home) =>
|
|
4464
|
-
|
|
5024
|
+
settingsFile = (home) => path8.join(home, ".bunshin", "deploy", "settings.json");
|
|
5025
|
+
orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
|
|
5026
|
+
sha = (text2) => createHash3("sha256").update(text2).digest("hex");
|
|
4465
5027
|
BLOCK_BEGIN = "<!-- bunshin:begin -->";
|
|
4466
5028
|
BLOCK_END = "<!-- bunshin:end -->";
|
|
4467
5029
|
MANIFEST = ".bunshin.json";
|
|
4468
5030
|
MAX_FILE = 2 * 1024 * 1024;
|
|
4469
5031
|
isExecutable = (file) => {
|
|
4470
5032
|
try {
|
|
4471
|
-
return (
|
|
5033
|
+
return (fs9.statSync(file).mode & 73) !== 0;
|
|
4472
5034
|
} catch {
|
|
4473
5035
|
return false;
|
|
4474
5036
|
}
|
|
@@ -4476,9 +5038,15 @@ var init_deploy = __esm({
|
|
|
4476
5038
|
safeJoin = containedPath;
|
|
4477
5039
|
SAFE_NAME = /^(?!\.\.?$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4478
5040
|
neutralizeMarkers = (text2) => text2.replace(/<!--(?=\s*bunshin:)/gi, "<!-\u200B-");
|
|
4479
|
-
LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
|
|
5041
|
+
LAYER_RANK = { org: 0, dept: 1, group: 2, team: 2, source: 3, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
|
|
4480
5042
|
docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
|
|
4481
5043
|
BORN_SLACK_MS = 2e3;
|
|
5044
|
+
LINK_DIRS = {
|
|
5045
|
+
"claude-code": { rel: path8.join(".claude", "skills"), key: "claudeSkills" },
|
|
5046
|
+
codex: { rel: path8.join(".codex", "skills"), key: "codexSkills" }
|
|
5047
|
+
};
|
|
5048
|
+
EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
|
|
5049
|
+
EXCLUDE_END = "# <<< bunshin";
|
|
4482
5050
|
}
|
|
4483
5051
|
});
|
|
4484
5052
|
|
|
@@ -4587,11 +5155,11 @@ var init_live = __esm({
|
|
|
4587
5155
|
});
|
|
4588
5156
|
|
|
4589
5157
|
// src/transcript.ts
|
|
4590
|
-
import
|
|
5158
|
+
import fs10 from "node:fs";
|
|
4591
5159
|
function parseTranscript(file) {
|
|
4592
5160
|
let raw = "";
|
|
4593
5161
|
try {
|
|
4594
|
-
raw =
|
|
5162
|
+
raw = fs10.readFileSync(file, "utf8");
|
|
4595
5163
|
} catch {
|
|
4596
5164
|
return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
|
|
4597
5165
|
}
|
|
@@ -4729,245 +5297,81 @@ function summarizeInput(input) {
|
|
|
4729
5297
|
function bounded(text2, maxChars) {
|
|
4730
5298
|
if (text2.length <= maxChars) return text2;
|
|
4731
5299
|
const marker = "\n[... middle omitted ...]\n";
|
|
4732
|
-
if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
|
|
4733
|
-
const head = Math.floor((maxChars - marker.length) / 2);
|
|
4734
|
-
return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
|
|
4735
|
-
}
|
|
4736
|
-
function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
|
|
4737
|
-
const parts = [];
|
|
4738
|
-
for (const t of facts.turns.slice(from)) {
|
|
4739
|
-
const text2 = redact2(t.text);
|
|
4740
|
-
if (t.role === "user") parts.push(`[USER]
|
|
4741
|
-
${bounded(text2, maxChars)}`);
|
|
4742
|
-
else if (t.role === "assistant") parts.push(`[ASSISTANT]
|
|
4743
|
-
${bounded(text2, maxChars)}`);
|
|
4744
|
-
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
4745
|
-
${bounded(text2, toolResultChars)}`);
|
|
4746
|
-
else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
|
|
4747
|
-
}
|
|
4748
|
-
return bounded(parts.join("\n\n"), maxChars);
|
|
4749
|
-
}
|
|
4750
|
-
function fullTrace(facts, { from = 0, to } = {}) {
|
|
4751
|
-
return facts.turns.slice(from, to).map((t) => {
|
|
4752
|
-
if (t.role === "user") return `[USER]
|
|
4753
|
-
${redact2(t.text)}`;
|
|
4754
|
-
if (t.role === "assistant") return `[ASSISTANT]
|
|
4755
|
-
${redact2(t.text)}`;
|
|
4756
|
-
if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
4757
|
-
${redact2(t.text)}`;
|
|
4758
|
-
const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
|
|
4759
|
-
return `[TOOL ${t.tool}]
|
|
4760
|
-
${redact2(input)}`;
|
|
4761
|
-
});
|
|
4762
|
-
}
|
|
4763
|
-
function traceChunks(turns, maxBytes = 1e6) {
|
|
4764
|
-
const out = [];
|
|
4765
|
-
let cur = "";
|
|
4766
|
-
let size = 0;
|
|
4767
|
-
turns.forEach((turn, i) => {
|
|
4768
|
-
let piece = (i ? "\n\n" : "") + turn;
|
|
4769
|
-
let bytes = jsonBytes(piece);
|
|
4770
|
-
if (size + bytes <= maxBytes) {
|
|
4771
|
-
cur += piece;
|
|
4772
|
-
size += bytes;
|
|
4773
|
-
return;
|
|
4774
|
-
}
|
|
4775
|
-
if (cur) out.push(cur);
|
|
4776
|
-
while (bytes > maxBytes) {
|
|
4777
|
-
let n = Math.min(piece.length, maxBytes);
|
|
4778
|
-
for (let b = jsonBytes(piece.slice(0, n)); b > maxBytes; b = jsonBytes(piece.slice(0, n))) n = Math.max(1, Math.floor(n * maxBytes / b) - 1);
|
|
4779
|
-
const code2 = piece.charCodeAt(n - 1);
|
|
4780
|
-
if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
|
|
4781
|
-
const head = piece.slice(0, n);
|
|
4782
|
-
out.push(head);
|
|
4783
|
-
piece = piece.slice(n);
|
|
4784
|
-
bytes -= jsonBytes(head);
|
|
4785
|
-
}
|
|
4786
|
-
cur = piece;
|
|
4787
|
-
size = bytes;
|
|
4788
|
-
});
|
|
4789
|
-
if (cur) out.push(cur);
|
|
4790
|
-
return out;
|
|
4791
|
-
}
|
|
4792
|
-
function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
|
|
4793
|
-
const facts = parseTranscript(file);
|
|
4794
|
-
const visible = facts.turns.filter((t) => t.role !== "tool");
|
|
4795
|
-
const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
|
|
4796
|
-
const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
|
|
4797
|
-
return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
|
|
4798
|
-
}
|
|
4799
|
-
var BANNER, jsonBytes;
|
|
4800
|
-
var init_transcript = __esm({
|
|
4801
|
-
"src/transcript.ts"() {
|
|
4802
|
-
"use strict";
|
|
4803
|
-
init_redact2();
|
|
4804
|
-
init_util();
|
|
4805
|
-
BANNER = "## Recalled lessons (Bunshin)";
|
|
4806
|
-
jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
|
|
4807
|
-
}
|
|
4808
|
-
});
|
|
4809
|
-
|
|
4810
|
-
// src/v2/repo.ts
|
|
4811
|
-
import fs9 from "node:fs";
|
|
4812
|
-
import path7 from "node:path";
|
|
4813
|
-
import { execFileSync } from "node:child_process";
|
|
4814
|
-
function git(cwd, args, timeout = 1500) {
|
|
4815
|
-
try {
|
|
4816
|
-
return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
4817
|
-
} catch {
|
|
4818
|
-
return null;
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
function repoRoot(cwd, storeRoot) {
|
|
4822
|
-
const key = `${cwd}
|
|
4823
|
-
${storeRoot ?? ""}`;
|
|
4824
|
-
const hit = rootCache.get(key);
|
|
4825
|
-
if (hit) return hit;
|
|
4826
|
-
const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
4827
|
-
let root = top || "";
|
|
4828
|
-
if (!root && storeRoot && path7.basename(storeRoot) === ".bunshin") {
|
|
4829
|
-
const parent = path7.dirname(storeRoot);
|
|
4830
|
-
if (path7.resolve(cwd).startsWith(path7.resolve(parent))) root = parent;
|
|
4831
|
-
}
|
|
4832
|
-
root = root || path7.resolve(cwd);
|
|
4833
|
-
rootCache.set(key, root);
|
|
4834
|
-
return root;
|
|
4835
|
-
}
|
|
4836
|
-
function parseRemote(url) {
|
|
4837
|
-
const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
4838
|
-
if (!clean2) return null;
|
|
4839
|
-
const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
|
|
4840
|
-
const pathPart = scp ? scp[1] : (() => {
|
|
4841
|
-
try {
|
|
4842
|
-
return new URL(clean2).pathname;
|
|
4843
|
-
} catch {
|
|
4844
|
-
return clean2;
|
|
4845
|
-
}
|
|
4846
|
-
})();
|
|
4847
|
-
const segs = pathPart.split("/").filter(Boolean);
|
|
4848
|
-
if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
|
|
4849
|
-
return segs[0] ?? null;
|
|
4850
|
-
}
|
|
4851
|
-
function repoName(root) {
|
|
4852
|
-
const url = git(root, ["remote", "get-url", "origin"]);
|
|
4853
|
-
return url && parseRemote(url) || path7.basename(root);
|
|
4854
|
-
}
|
|
4855
|
-
function repoCommit(root) {
|
|
4856
|
-
return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
|
|
4857
|
-
}
|
|
4858
|
-
function listDocs(root) {
|
|
4859
|
-
const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
|
|
4860
|
-
if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs9.existsSync(path7.join(root, rel)) && !deployedSkill(root, rel)).sort();
|
|
4861
|
-
const out = [];
|
|
4862
|
-
let visited = 0;
|
|
4863
|
-
const walk = (dir, rel, depth) => {
|
|
4864
|
-
if (depth > 8 || visited++ > 2e4) return;
|
|
4865
|
-
let entries;
|
|
4866
|
-
try {
|
|
4867
|
-
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
4868
|
-
} catch {
|
|
4869
|
-
return;
|
|
4870
|
-
}
|
|
4871
|
-
for (const e of entries) {
|
|
4872
|
-
if (e.isDirectory()) {
|
|
4873
|
-
if (!SKIP_DIRS.has(e.name) && !fs9.existsSync(path7.join(dir, e.name, ".git"))) walk(path7.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
|
|
4874
|
-
} else if (e.isFile()) {
|
|
4875
|
-
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
4876
|
-
if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
|
|
4877
|
-
}
|
|
4878
|
-
}
|
|
4879
|
-
};
|
|
4880
|
-
walk(root, "", 0);
|
|
4881
|
-
return out.sort();
|
|
4882
|
-
}
|
|
4883
|
-
function indexWorkingTree(v2, config, cwd, opts = {}) {
|
|
4884
|
-
const root = repoRoot(cwd, v2.store.root);
|
|
4885
|
-
const now = opts.now ?? Date.now();
|
|
4886
|
-
const cached = v2.readRepoIndex();
|
|
4887
|
-
const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
|
|
4888
|
-
const sameRepo = cached && cached.ownerTeam === ownerTeam;
|
|
4889
|
-
const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
|
|
4890
|
-
const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
|
|
4891
|
-
const paths = fresh ? cached.paths : listDocs(root);
|
|
4892
|
-
let changed = !fresh || !sameRepo || cached.repo !== repo;
|
|
4893
|
-
const files = {};
|
|
4894
|
-
for (const rel of paths) {
|
|
4895
|
-
const abs = path7.join(root, rel);
|
|
4896
|
-
let stat;
|
|
4897
|
-
try {
|
|
4898
|
-
stat = fs9.statSync(abs);
|
|
4899
|
-
} catch {
|
|
4900
|
-
changed = true;
|
|
4901
|
-
continue;
|
|
4902
|
-
}
|
|
4903
|
-
const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
|
|
4904
|
-
if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
|
|
4905
|
-
files[rel] = prev;
|
|
4906
|
-
continue;
|
|
4907
|
-
}
|
|
4908
|
-
changed = true;
|
|
4909
|
-
try {
|
|
4910
|
-
const text2 = fs9.readFileSync(abs, "utf8");
|
|
4911
|
-
files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
|
|
4912
|
-
} catch {
|
|
4913
|
-
}
|
|
4914
|
-
}
|
|
4915
|
-
const index = {
|
|
4916
|
-
repo,
|
|
4917
|
-
ownerTeam,
|
|
4918
|
-
listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
|
|
4919
|
-
paths,
|
|
4920
|
-
files
|
|
4921
|
-
};
|
|
4922
|
-
if (changed) {
|
|
4923
|
-
try {
|
|
4924
|
-
v2.writeRepoIndex(index);
|
|
4925
|
-
} catch {
|
|
4926
|
-
}
|
|
4927
|
-
}
|
|
4928
|
-
return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
|
|
5300
|
+
if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
|
|
5301
|
+
const head = Math.floor((maxChars - marker.length) / 2);
|
|
5302
|
+
return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
|
|
4929
5303
|
}
|
|
4930
|
-
function
|
|
4931
|
-
|
|
4932
|
-
const
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
5304
|
+
function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
|
|
5305
|
+
const parts = [];
|
|
5306
|
+
for (const t of facts.turns.slice(from)) {
|
|
5307
|
+
const text2 = redact2(t.text);
|
|
5308
|
+
if (t.role === "user") parts.push(`[USER]
|
|
5309
|
+
${bounded(text2, maxChars)}`);
|
|
5310
|
+
else if (t.role === "assistant") parts.push(`[ASSISTANT]
|
|
5311
|
+
${bounded(text2, maxChars)}`);
|
|
5312
|
+
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
5313
|
+
${bounded(text2, toolResultChars)}`);
|
|
5314
|
+
else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
|
|
4938
5315
|
}
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
5316
|
+
return bounded(parts.join("\n\n"), maxChars);
|
|
5317
|
+
}
|
|
5318
|
+
function fullTrace(facts, { from = 0, to } = {}) {
|
|
5319
|
+
return facts.turns.slice(from, to).map((t) => {
|
|
5320
|
+
if (t.role === "user") return `[USER]
|
|
5321
|
+
${redact2(t.text)}`;
|
|
5322
|
+
if (t.role === "assistant") return `[ASSISTANT]
|
|
5323
|
+
${redact2(t.text)}`;
|
|
5324
|
+
if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
5325
|
+
${redact2(t.text)}`;
|
|
5326
|
+
const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
|
|
5327
|
+
return `[TOOL ${t.tool}]
|
|
5328
|
+
${redact2(input)}`;
|
|
5329
|
+
});
|
|
5330
|
+
}
|
|
5331
|
+
function traceChunks(turns, maxBytes = 1e6) {
|
|
5332
|
+
const out = [];
|
|
5333
|
+
let cur = "";
|
|
5334
|
+
let size = 0;
|
|
5335
|
+
turns.forEach((turn, i) => {
|
|
5336
|
+
let piece = (i ? "\n\n" : "") + turn;
|
|
5337
|
+
let bytes = jsonBytes(piece);
|
|
5338
|
+
if (size + bytes <= maxBytes) {
|
|
5339
|
+
cur += piece;
|
|
5340
|
+
size += bytes;
|
|
5341
|
+
return;
|
|
4945
5342
|
}
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
5343
|
+
if (cur) out.push(cur);
|
|
5344
|
+
while (bytes > maxBytes) {
|
|
5345
|
+
let n = Math.min(piece.length, maxBytes);
|
|
5346
|
+
for (let b = jsonBytes(piece.slice(0, n)); b > maxBytes; b = jsonBytes(piece.slice(0, n))) n = Math.max(1, Math.floor(n * maxBytes / b) - 1);
|
|
5347
|
+
const code2 = piece.charCodeAt(n - 1);
|
|
5348
|
+
if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
|
|
5349
|
+
const head = piece.slice(0, n);
|
|
5350
|
+
out.push(head);
|
|
5351
|
+
piece = piece.slice(n);
|
|
5352
|
+
bytes -= jsonBytes(head);
|
|
5353
|
+
}
|
|
5354
|
+
cur = piece;
|
|
5355
|
+
size = bytes;
|
|
5356
|
+
});
|
|
5357
|
+
if (cur) out.push(cur);
|
|
5358
|
+
return out;
|
|
4955
5359
|
}
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
5360
|
+
function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
|
|
5361
|
+
const facts = parseTranscript(file);
|
|
5362
|
+
const visible = facts.turns.filter((t) => t.role !== "tool");
|
|
5363
|
+
const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
|
|
5364
|
+
const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
|
|
5365
|
+
return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
|
|
5366
|
+
}
|
|
5367
|
+
var BANNER, jsonBytes;
|
|
5368
|
+
var init_transcript = __esm({
|
|
5369
|
+
"src/transcript.ts"() {
|
|
4959
5370
|
"use strict";
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
rootCache = /* @__PURE__ */ new Map();
|
|
4965
|
-
isDoc = (rel) => {
|
|
4966
|
-
const parts = rel.split("/");
|
|
4967
|
-
if (parts.some((p) => SKIP_DIRS.has(p))) return false;
|
|
4968
|
-
return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
|
|
4969
|
-
};
|
|
4970
|
-
deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs9.existsSync(path7.join(root, path7.posix.dirname(rel), ".bunshin.json"));
|
|
5371
|
+
init_redact2();
|
|
5372
|
+
init_util();
|
|
5373
|
+
BANNER = "## Recalled lessons (Bunshin)";
|
|
5374
|
+
jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
|
|
4971
5375
|
}
|
|
4972
5376
|
});
|
|
4973
5377
|
|
|
@@ -5036,8 +5440,14 @@ function applyOverlays(sections, overlays, ctx) {
|
|
|
5036
5440
|
}
|
|
5037
5441
|
function localView(v2, config, cwd, opts = {}) {
|
|
5038
5442
|
const ctx = contextFor(v2, config);
|
|
5039
|
-
const reader = readerFor(ctx);
|
|
5040
5443
|
const bundle = v2.readBundle();
|
|
5444
|
+
let source;
|
|
5445
|
+
try {
|
|
5446
|
+
source = opts.source !== void 0 ? opts.source || void 0 : sourceFor(cwd, v2.store.root, ctx);
|
|
5447
|
+
} catch {
|
|
5448
|
+
source = void 0;
|
|
5449
|
+
}
|
|
5450
|
+
const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle?.scopes ? { tree: bundle.scopes } : {} });
|
|
5041
5451
|
const byId = /* @__PURE__ */ new Map();
|
|
5042
5452
|
for (const s of bundle?.sections ?? []) byId.set(s.id, s);
|
|
5043
5453
|
let root = cwd;
|
|
@@ -5055,7 +5465,7 @@ function localView(v2, config, cwd, opts = {}) {
|
|
|
5055
5465
|
}
|
|
5056
5466
|
}
|
|
5057
5467
|
applyOverlays(byId, v2.readOverlays(), ctx);
|
|
5058
|
-
return { ctx, reader, layers:
|
|
5468
|
+
return { ctx, reader, ...source ? { source } : {}, layers: reader.layers, bundle, sections: [...byId.values()], byId, root, repo };
|
|
5059
5469
|
}
|
|
5060
5470
|
var ACTIVE_OVERLAY;
|
|
5061
5471
|
var init_view = __esm({
|
|
@@ -5063,12 +5473,13 @@ var init_view = __esm({
|
|
|
5063
5473
|
"use strict";
|
|
5064
5474
|
init_v22();
|
|
5065
5475
|
init_repo();
|
|
5476
|
+
init_source();
|
|
5066
5477
|
ACTIVE_OVERLAY = /* @__PURE__ */ new Set(["proposed", "trial", "submitted", "kept-local", "needs-review"]);
|
|
5067
5478
|
}
|
|
5068
5479
|
});
|
|
5069
5480
|
|
|
5070
5481
|
// src/v2/apply.ts
|
|
5071
|
-
import
|
|
5482
|
+
import fs11 from "node:fs";
|
|
5072
5483
|
function appendedText(op) {
|
|
5073
5484
|
return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
|
|
5074
5485
|
}
|
|
@@ -5123,12 +5534,12 @@ function applyLocally(v2, view, s) {
|
|
|
5123
5534
|
if (section.storage.type === "repo") {
|
|
5124
5535
|
const file = containedPath(view.root, section.storage.path);
|
|
5125
5536
|
const ours = !view.repo || section.storage.repo === view.repo;
|
|
5126
|
-
if (ours && file &&
|
|
5127
|
-
const before =
|
|
5537
|
+
if (ours && file && fs11.existsSync(file)) {
|
|
5538
|
+
const before = fs11.readFileSync(file, "utf8");
|
|
5128
5539
|
const res2 = applyOpToMarkdown(before, section.storage.anchor, op);
|
|
5129
5540
|
if (res2.ok) {
|
|
5130
5541
|
const changed = res2.text !== before;
|
|
5131
|
-
if (changed)
|
|
5542
|
+
if (changed) fs11.writeFileSync(file, res2.text);
|
|
5132
5543
|
const overlay2 = { ...base, kind: "working-tree", sectionId: section.id, file: section.storage.path, anchor: section.storage.anchor, changed };
|
|
5133
5544
|
upsertOverlay(v2, overlay2);
|
|
5134
5545
|
return { applied: true, target: describeTarget(s, view), overlay: overlay2 };
|
|
@@ -5149,7 +5560,7 @@ function revertWorkingTree(root, o) {
|
|
|
5149
5560
|
if (!file) return { reverted: false, reason: "path outside the repository" };
|
|
5150
5561
|
let text2;
|
|
5151
5562
|
try {
|
|
5152
|
-
text2 =
|
|
5563
|
+
text2 = fs11.readFileSync(file, "utf8");
|
|
5153
5564
|
} catch {
|
|
5154
5565
|
return { reverted: false, reason: "file missing" };
|
|
5155
5566
|
}
|
|
@@ -5169,7 +5580,7 @@ ${appendedText(o.op)}`;
|
|
|
5169
5580
|
if (once(text2, o.op.text) >= 0 && !text2.includes(o.op.find)) next = text2.replace(o.op.text, o.op.find);
|
|
5170
5581
|
}
|
|
5171
5582
|
if (next === null) return { reverted: false, reason: "file changed since the trial edit" };
|
|
5172
|
-
|
|
5583
|
+
fs11.writeFileSync(file, next);
|
|
5173
5584
|
return { reverted: true };
|
|
5174
5585
|
}
|
|
5175
5586
|
function revertParam(v2, o) {
|
|
@@ -5242,7 +5653,7 @@ var init_apply = __esm({
|
|
|
5242
5653
|
});
|
|
5243
5654
|
|
|
5244
5655
|
// src/v2/signals.ts
|
|
5245
|
-
import
|
|
5656
|
+
import fs12 from "node:fs";
|
|
5246
5657
|
function isCorrection(text2) {
|
|
5247
5658
|
const t = text2.trim().slice(0, 600);
|
|
5248
5659
|
return !!t && CORRECTION_RE.test(t);
|
|
@@ -5264,7 +5675,7 @@ function scanTranscript(file) {
|
|
|
5264
5675
|
if (!file) return empty;
|
|
5265
5676
|
let raw;
|
|
5266
5677
|
try {
|
|
5267
|
-
raw =
|
|
5678
|
+
raw = fs12.readFileSync(file, "utf8");
|
|
5268
5679
|
} catch {
|
|
5269
5680
|
return empty;
|
|
5270
5681
|
}
|
|
@@ -5348,8 +5759,8 @@ var init_signals = __esm({
|
|
|
5348
5759
|
});
|
|
5349
5760
|
|
|
5350
5761
|
// src/v2/ui.ts
|
|
5351
|
-
import
|
|
5352
|
-
import
|
|
5762
|
+
import fs13 from "node:fs";
|
|
5763
|
+
import path9 from "node:path";
|
|
5353
5764
|
function feedPath(v2, sessionId) {
|
|
5354
5765
|
return v2.p("ui", `${safe(sessionId)}.json`);
|
|
5355
5766
|
}
|
|
@@ -5365,7 +5776,7 @@ function emptyFeed(v2, sessionId, now) {
|
|
|
5365
5776
|
}
|
|
5366
5777
|
function cliArgv() {
|
|
5367
5778
|
const entry = process.argv[1];
|
|
5368
|
-
return entry ? [process.execPath,
|
|
5779
|
+
return entry ? [process.execPath, path9.resolve(entry)] : ["bunshin"];
|
|
5369
5780
|
}
|
|
5370
5781
|
function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5371
5782
|
if (!sessionId || sessionId === "no-session") return null;
|
|
@@ -5380,7 +5791,7 @@ function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toI
|
|
|
5380
5791
|
f.cli = cliArgv();
|
|
5381
5792
|
f.served = f.served.slice(0, MAX_SERVED);
|
|
5382
5793
|
f.events = f.events.slice(-MAX_EVENTS);
|
|
5383
|
-
|
|
5794
|
+
fs13.mkdirSync(path9.dirname(feedPath(v2, sessionId)), { recursive: true });
|
|
5384
5795
|
writeJsonAtomic(feedPath(v2, sessionId), f);
|
|
5385
5796
|
return f;
|
|
5386
5797
|
} catch {
|
|
@@ -5481,16 +5892,16 @@ function latestFeed(v2) {
|
|
|
5481
5892
|
const dir = v2.p("ui");
|
|
5482
5893
|
let best = null;
|
|
5483
5894
|
try {
|
|
5484
|
-
for (const f2 of
|
|
5895
|
+
for (const f2 of fs13.readdirSync(dir)) {
|
|
5485
5896
|
if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
|
|
5486
|
-
const m =
|
|
5897
|
+
const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
|
|
5487
5898
|
if (!best || m > best.m) best = { f: f2, m };
|
|
5488
5899
|
}
|
|
5489
5900
|
} catch {
|
|
5490
5901
|
return null;
|
|
5491
5902
|
}
|
|
5492
5903
|
if (!best) return null;
|
|
5493
|
-
const f = readJson(
|
|
5904
|
+
const f = readJson(path9.join(dir, best.f), null);
|
|
5494
5905
|
return f && f.v === UI_FEED_VERSION ? f : null;
|
|
5495
5906
|
}
|
|
5496
5907
|
var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
|
|
@@ -5515,8 +5926,8 @@ __export(sync_exports, {
|
|
|
5515
5926
|
syncV2: () => syncV2,
|
|
5516
5927
|
uploadIndex: () => uploadIndex
|
|
5517
5928
|
});
|
|
5518
|
-
import
|
|
5519
|
-
import
|
|
5929
|
+
import fs14 from "node:fs";
|
|
5930
|
+
import path10 from "node:path";
|
|
5520
5931
|
function episodePayload(e) {
|
|
5521
5932
|
const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, transcriptPath: _tp, ...rest } = e;
|
|
5522
5933
|
void _o, _u, _s, _rs, _rl, _tp;
|
|
@@ -5550,7 +5961,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
|
|
|
5550
5961
|
if (e.upload === "header-only" || e.cliVersion || !e.transcript) continue;
|
|
5551
5962
|
if (now - Date.parse(e.endedAt || e.startedAt) > days * 864e5) continue;
|
|
5552
5963
|
const file = e.transcriptPath ?? v2.readSession(e.sessionId)?.transcriptPath;
|
|
5553
|
-
if (!file || !
|
|
5964
|
+
if (!file || !fs14.existsSync(file)) {
|
|
5554
5965
|
missing++;
|
|
5555
5966
|
continue;
|
|
5556
5967
|
}
|
|
@@ -5565,7 +5976,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
|
|
|
5565
5976
|
async function syncV2(store, opts = {}) {
|
|
5566
5977
|
const v2 = new V2Store(store);
|
|
5567
5978
|
const config = store.config();
|
|
5568
|
-
const cwd = opts.cwd ??
|
|
5979
|
+
const cwd = opts.cwd ?? path10.dirname(store.root);
|
|
5569
5980
|
const report = { offline: !canCall(config), uploadedEpisodes: 0, deferredTraces: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
|
|
5570
5981
|
const deadline = Date.now() + (opts.budgetMs ?? 24e4);
|
|
5571
5982
|
if (report.offline) {
|
|
@@ -5681,7 +6092,8 @@ async function syncV2(store, opts = {}) {
|
|
|
5681
6092
|
const bundle = v2.readBundle();
|
|
5682
6093
|
if (bundle) {
|
|
5683
6094
|
try {
|
|
5684
|
-
|
|
6095
|
+
const linkRoot = cwd && cwdInPool(config, cwd, store.root) ? projectRootFor(cwd, store.root) : void 0;
|
|
6096
|
+
report.deploy = deployLibrary(v2, config, ctx, bundle, { capture: false, ...linkRoot ? { linkRoot } : {}, linkHarnesses: linkHarnesses(config) });
|
|
5685
6097
|
} catch (err) {
|
|
5686
6098
|
report.errors.push(`deploy: ${err instanceof Error ? err.message : String(err)}`);
|
|
5687
6099
|
}
|
|
@@ -5711,7 +6123,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
|
|
|
5711
6123
|
const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
|
|
5712
6124
|
const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
|
|
5713
6125
|
try {
|
|
5714
|
-
return [{ path: rel, text:
|
|
6126
|
+
return [{ path: rel, text: fs14.readFileSync(path10.join(root, rel), "utf8") }];
|
|
5715
6127
|
} catch {
|
|
5716
6128
|
return [];
|
|
5717
6129
|
}
|
|
@@ -5729,6 +6141,7 @@ var init_sync = __esm({
|
|
|
5729
6141
|
init_api();
|
|
5730
6142
|
init_apply();
|
|
5731
6143
|
init_deploy();
|
|
6144
|
+
init_source();
|
|
5732
6145
|
init_repo();
|
|
5733
6146
|
init_store2();
|
|
5734
6147
|
init_view();
|
|
@@ -5739,10 +6152,10 @@ var init_sync = __esm({
|
|
|
5739
6152
|
|
|
5740
6153
|
// src/v2/reflect.ts
|
|
5741
6154
|
import { spawn as spawn3 } from "node:child_process";
|
|
5742
|
-
import { createHash as
|
|
5743
|
-
import
|
|
6155
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6156
|
+
import fs15 from "node:fs";
|
|
5744
6157
|
function episodeIdFor(contextId, sessionId) {
|
|
5745
|
-
return `e_${
|
|
6158
|
+
return `e_${createHash4("sha256").update(`${contextId}
|
|
5746
6159
|
${sessionId}`).digest("hex").slice(0, 16)}`;
|
|
5747
6160
|
}
|
|
5748
6161
|
function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT_MS } = {}) {
|
|
@@ -5775,7 +6188,7 @@ function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT
|
|
|
5775
6188
|
};
|
|
5776
6189
|
let child;
|
|
5777
6190
|
try {
|
|
5778
|
-
child = spawn3(bin, args, { cwd: cwd &&
|
|
6191
|
+
child = spawn3(bin, args, { cwd: cwd && fs15.existsSync(cwd) ? cwd : void 0, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
|
|
5779
6192
|
} catch (e) {
|
|
5780
6193
|
resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
5781
6194
|
return;
|
|
@@ -5820,7 +6233,7 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
|
|
|
5820
6233
|
return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
|
|
5821
6234
|
}
|
|
5822
6235
|
function humanTurnsOf(transcriptPath) {
|
|
5823
|
-
if (!transcriptPath || !
|
|
6236
|
+
if (!transcriptPath || !fs15.existsSync(transcriptPath)) return [];
|
|
5824
6237
|
try {
|
|
5825
6238
|
return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
|
|
5826
6239
|
} catch {
|
|
@@ -5828,7 +6241,7 @@ function humanTurnsOf(transcriptPath) {
|
|
|
5828
6241
|
}
|
|
5829
6242
|
}
|
|
5830
6243
|
function readFacts(transcriptPath) {
|
|
5831
|
-
if (!transcriptPath || !
|
|
6244
|
+
if (!transcriptPath || !fs15.existsSync(transcriptPath)) return null;
|
|
5832
6245
|
try {
|
|
5833
6246
|
return parseTranscript(transcriptPath);
|
|
5834
6247
|
} catch {
|
|
@@ -5855,7 +6268,7 @@ async function runReflectionV2(store, opts) {
|
|
|
5855
6268
|
if (!release) return { ...result, skipped: "a reflection of this session is already running" };
|
|
5856
6269
|
try {
|
|
5857
6270
|
const session = v2.session(opts.sessionId, opts.cwd ?? process.cwd());
|
|
5858
|
-
const view = localView(v2, config, session.cwd || opts.cwd || process.cwd());
|
|
6271
|
+
const view = localView(v2, config, session.cwd || opts.cwd || process.cwd(), session.source !== void 0 ? { source: session.source } : {});
|
|
5859
6272
|
const origin = opts.origin ?? (opts.focus !== void 0 ? "learn" : "session-end");
|
|
5860
6273
|
const episodeId = episodeIdFor(view.ctx.id, opts.sessionId);
|
|
5861
6274
|
const prior = session.suggestionIds.map((id) => v2.readSuggestion(id)).filter((s) => !!s);
|
|
@@ -5872,7 +6285,7 @@ async function runReflectionV2(store, opts) {
|
|
|
5872
6285
|
const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
|
|
5873
6286
|
const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
|
|
5874
6287
|
const guidelines = reflectionGuidelines(view.sections, view.reader);
|
|
5875
|
-
const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
|
|
6288
|
+
const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, scopes: scopeChoices(view.bundle?.scopes, view.source), ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
|
|
5876
6289
|
let raw = null;
|
|
5877
6290
|
if (harness === "claude-code") {
|
|
5878
6291
|
const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
|
|
@@ -5891,7 +6304,7 @@ async function runReflectionV2(store, opts) {
|
|
|
5891
6304
|
result.method = "fork";
|
|
5892
6305
|
} else result.error = fork.error;
|
|
5893
6306
|
}
|
|
5894
|
-
if (result.method === "none" && session.transcriptPath &&
|
|
6307
|
+
if (result.method === "none" && session.transcriptPath && fs15.existsSync(session.transcriptPath)) {
|
|
5895
6308
|
const transcriptDigest = digest(facts ?? parseTranscript(session.transcriptPath), { from, maxChars: 4e4 });
|
|
5896
6309
|
const res = await askJson(config, {
|
|
5897
6310
|
system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
|
|
@@ -5912,8 +6325,10 @@ async function runReflectionV2(store, opts) {
|
|
|
5912
6325
|
result.nothingNew = result.method !== "none" && parsed.length === 0;
|
|
5913
6326
|
const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
|
|
5914
6327
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6328
|
+
const source = view.source;
|
|
5915
6329
|
for (const p of parsed) {
|
|
5916
6330
|
const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
|
|
6331
|
+
if (source) s.source = source;
|
|
5917
6332
|
s.summary = summarize(s.op);
|
|
5918
6333
|
if (apply) {
|
|
5919
6334
|
const outcome = applyLocally(v2, view, s);
|
|
@@ -6014,51 +6429,51 @@ __export(uicmd_exports, {
|
|
|
6014
6429
|
modSource: () => modSource
|
|
6015
6430
|
});
|
|
6016
6431
|
import { spawnSync } from "node:child_process";
|
|
6017
|
-
import
|
|
6432
|
+
import fs16 from "node:fs";
|
|
6018
6433
|
import os3 from "node:os";
|
|
6019
|
-
import
|
|
6434
|
+
import path11 from "node:path";
|
|
6020
6435
|
import { fileURLToPath } from "node:url";
|
|
6021
6436
|
function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
|
|
6022
|
-
return
|
|
6437
|
+
return path11.join(home, ".bunshin", "claude-code-ui");
|
|
6023
6438
|
}
|
|
6024
6439
|
function modSource() {
|
|
6025
|
-
const here =
|
|
6026
|
-
for (const dir of [
|
|
6027
|
-
if (
|
|
6440
|
+
const here = path11.dirname(fileURLToPath(import.meta.url));
|
|
6441
|
+
for (const dir of [path11.join(here, "claude-code-ui"), path11.join(here, "..", "claude-code-ui"), path11.resolve(here, "../../../../adapters/claude-code-ui")]) {
|
|
6442
|
+
if (fs16.existsSync(path11.join(dir, ".claude-plugin", "plugin.json"))) return dir;
|
|
6028
6443
|
}
|
|
6029
6444
|
return null;
|
|
6030
6445
|
}
|
|
6031
6446
|
function installedModVersion(home) {
|
|
6032
6447
|
try {
|
|
6033
|
-
return JSON.parse(
|
|
6448
|
+
return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
|
|
6034
6449
|
} catch {
|
|
6035
6450
|
return null;
|
|
6036
6451
|
}
|
|
6037
6452
|
}
|
|
6038
6453
|
function copyDir(from, to) {
|
|
6039
|
-
|
|
6040
|
-
for (const e of
|
|
6454
|
+
fs16.mkdirSync(to, { recursive: true });
|
|
6455
|
+
for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
|
|
6041
6456
|
if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
|
|
6042
6457
|
if (e.name === "node_modules") continue;
|
|
6043
|
-
const a =
|
|
6044
|
-
const b =
|
|
6458
|
+
const a = path11.join(from, e.name);
|
|
6459
|
+
const b = path11.join(to, e.name);
|
|
6045
6460
|
if (e.isDirectory()) copyDir(a, b);
|
|
6046
|
-
else
|
|
6461
|
+
else fs16.copyFileSync(a, b);
|
|
6047
6462
|
}
|
|
6048
6463
|
}
|
|
6049
6464
|
function installMod(version2, opts = {}) {
|
|
6050
6465
|
const src = modSource();
|
|
6051
6466
|
if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
|
|
6052
6467
|
const home = modHome(opts.home);
|
|
6053
|
-
const dir =
|
|
6054
|
-
|
|
6468
|
+
const dir = path11.join(home, MOD_PLUGIN);
|
|
6469
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
6055
6470
|
copyDir(src, dir);
|
|
6056
|
-
const manifest =
|
|
6057
|
-
const m = JSON.parse(
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6471
|
+
const manifest = path11.join(dir, ".claude-plugin", "plugin.json");
|
|
6472
|
+
const m = JSON.parse(fs16.readFileSync(manifest, "utf8"));
|
|
6473
|
+
fs16.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
|
|
6474
|
+
fs16.mkdirSync(path11.join(home, ".claude-plugin"), { recursive: true });
|
|
6475
|
+
fs16.writeFileSync(
|
|
6476
|
+
path11.join(home, ".claude-plugin", "marketplace.json"),
|
|
6062
6477
|
JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
|
|
6063
6478
|
);
|
|
6064
6479
|
const run2 = opts.run ?? spawnSync;
|
|
@@ -6093,7 +6508,7 @@ async function cmdUi(store, args, out, err, version2) {
|
|
|
6093
6508
|
`);
|
|
6094
6509
|
for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
|
|
6095
6510
|
`);
|
|
6096
|
-
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 " +
|
|
6511
|
+
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");
|
|
6097
6512
|
}
|
|
6098
6513
|
return installed || !failed.length ? 0 : 1;
|
|
6099
6514
|
}
|
|
@@ -6174,19 +6589,19 @@ __export(update_exports, {
|
|
|
6174
6589
|
updateCheckDue: () => updateCheckDue,
|
|
6175
6590
|
updateNotice: () => updateNotice
|
|
6176
6591
|
});
|
|
6177
|
-
import
|
|
6178
|
-
import
|
|
6592
|
+
import fs17 from "node:fs";
|
|
6593
|
+
import path12 from "node:path";
|
|
6179
6594
|
function readState2() {
|
|
6180
6595
|
try {
|
|
6181
|
-
return JSON.parse(
|
|
6596
|
+
return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
|
|
6182
6597
|
} catch {
|
|
6183
6598
|
return {};
|
|
6184
6599
|
}
|
|
6185
6600
|
}
|
|
6186
6601
|
function writeState(s) {
|
|
6187
6602
|
try {
|
|
6188
|
-
|
|
6189
|
-
|
|
6603
|
+
fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
|
|
6604
|
+
fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
|
|
6190
6605
|
} catch {
|
|
6191
6606
|
}
|
|
6192
6607
|
}
|
|
@@ -6236,14 +6651,14 @@ var init_update = __esm({
|
|
|
6236
6651
|
PKG = "@bunshin-cc/bunshin";
|
|
6237
6652
|
LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
|
|
6238
6653
|
DAY = 864e5;
|
|
6239
|
-
stateFile = () =>
|
|
6654
|
+
stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
|
|
6240
6655
|
disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
|
|
6241
6656
|
}
|
|
6242
6657
|
});
|
|
6243
6658
|
|
|
6244
6659
|
// src/v2/hooks.ts
|
|
6245
|
-
import
|
|
6246
|
-
import
|
|
6660
|
+
import fs18 from "node:fs";
|
|
6661
|
+
import path13 from "node:path";
|
|
6247
6662
|
function inContext(s) {
|
|
6248
6663
|
return s.inContext ?? [...s.loaded, ...s.pushed];
|
|
6249
6664
|
}
|
|
@@ -6258,6 +6673,12 @@ function touch(v2, ctx, patch = () => {
|
|
|
6258
6673
|
const s = v2.updateSession(id, ctx.cwd, (s2) => {
|
|
6259
6674
|
s2.cwd = ctx.cwd || s2.cwd;
|
|
6260
6675
|
s2.harness = ctx.store.config().harness;
|
|
6676
|
+
if (s2.source === void 0) {
|
|
6677
|
+
try {
|
|
6678
|
+
s2.source = sourceFor(s2.cwd, ctx.store.root, contextFor(v2, ctx.store.config())) ?? "";
|
|
6679
|
+
} catch {
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6261
6682
|
if (ctx.input.transcript_path) s2.transcriptPath = ctx.input.transcript_path;
|
|
6262
6683
|
patch(s2);
|
|
6263
6684
|
});
|
|
@@ -6304,10 +6725,10 @@ async function keepModCurrent(ctx, config) {
|
|
|
6304
6725
|
const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
6305
6726
|
const current = version2();
|
|
6306
6727
|
if (!modSource2() || installedModVersion2() === current) return;
|
|
6307
|
-
const marker =
|
|
6308
|
-
if (
|
|
6309
|
-
|
|
6310
|
-
|
|
6728
|
+
const marker = path13.join(modHome2(), `.attempted-${current}`);
|
|
6729
|
+
if (fs18.existsSync(marker)) return;
|
|
6730
|
+
fs18.mkdirSync(modHome2(), { recursive: true });
|
|
6731
|
+
fs18.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
|
|
6311
6732
|
ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
|
|
6312
6733
|
} catch {
|
|
6313
6734
|
}
|
|
@@ -6410,8 +6831,8 @@ function liveUpdate(v2, ctx, session) {
|
|
|
6410
6831
|
return "";
|
|
6411
6832
|
}
|
|
6412
6833
|
const config = ctx.store.config();
|
|
6413
|
-
const deployed = deployEnabled(config,
|
|
6414
|
-
const paths = deployed ? deployPaths() : null;
|
|
6834
|
+
const deployed = deployEnabled(config, contextFor(v2, config));
|
|
6835
|
+
const paths = deployed ? deployPaths(process.env, config.orgId) : null;
|
|
6415
6836
|
const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
|
|
6416
6837
|
const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
|
|
6417
6838
|
const { text: text2, next } = changesSince(session.live, bundle, skillPath);
|
|
@@ -6436,13 +6857,13 @@ function skillHint(sec, others) {
|
|
|
6436
6857
|
const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
|
|
6437
6858
|
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}`;
|
|
6438
6859
|
}
|
|
6439
|
-
async function remoteAuto(ctx, prompt, exclude) {
|
|
6860
|
+
async function remoteAuto(ctx, prompt, exclude, source) {
|
|
6440
6861
|
const config = ctx.store.config();
|
|
6441
6862
|
if (!canCall(config) || !config.recall.useMothership) return null;
|
|
6442
6863
|
const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
|
|
6443
6864
|
if (budget < 300) return null;
|
|
6444
6865
|
try {
|
|
6445
|
-
const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude }, { timeoutMs: budget });
|
|
6866
|
+
const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
|
|
6446
6867
|
return res.available ? res : null;
|
|
6447
6868
|
} catch {
|
|
6448
6869
|
return null;
|
|
@@ -6477,7 +6898,7 @@ async function onPrompt(ctx) {
|
|
|
6477
6898
|
};
|
|
6478
6899
|
if (!prompt.trim()) return withLive({});
|
|
6479
6900
|
const config = ctx.store.config();
|
|
6480
|
-
const view = localView(v2, config, ctx.cwd);
|
|
6901
|
+
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
6481
6902
|
const injected = inContext(session);
|
|
6482
6903
|
const named = invokedSkill(prompt, view.sections);
|
|
6483
6904
|
if (named) {
|
|
@@ -6497,7 +6918,7 @@ async function onPrompt(ctx) {
|
|
|
6497
6918
|
return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
|
|
6498
6919
|
}
|
|
6499
6920
|
const res = auto(view.sections, view.reader, prompt, { exclude: injected });
|
|
6500
|
-
const remote = await remoteAuto(ctx, prompt, injected);
|
|
6921
|
+
const remote = await remoteAuto(ctx, prompt, injected, session.source);
|
|
6501
6922
|
if (remote) {
|
|
6502
6923
|
const local = (id) => view.sections.find((s) => s.id === id);
|
|
6503
6924
|
if (remote.section) {
|
|
@@ -6565,7 +6986,7 @@ function onPreToolUse(ctx) {
|
|
|
6565
6986
|
const raw = toolPaths(ctx.input.tool_input);
|
|
6566
6987
|
if (!raw.length) return withLive({});
|
|
6567
6988
|
const config = ctx.store.config();
|
|
6568
|
-
const view = localView(v2, config, ctx.cwd);
|
|
6989
|
+
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
6569
6990
|
const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
|
|
6570
6991
|
const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
|
|
6571
6992
|
if (!paths.length && !text2) return withLive({});
|
|
@@ -6649,6 +7070,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6649
7070
|
const v2 = new V2Store(store);
|
|
6650
7071
|
const config = store.config();
|
|
6651
7072
|
const view = localView(v2, config, session.cwd, { index: false });
|
|
7073
|
+
const source = sessionSource(store, session, view.ctx);
|
|
6652
7074
|
const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
|
|
6653
7075
|
const outcome = v2.readOutcome(session.sessionId);
|
|
6654
7076
|
const turns = scan.facts.turns.length;
|
|
@@ -6688,6 +7110,8 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6688
7110
|
...outcome ? { outcome } : {},
|
|
6689
7111
|
...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
|
|
6690
7112
|
cliVersion: version(),
|
|
7113
|
+
...source ? { source } : {},
|
|
7114
|
+
...session.borrowed?.length ? { borrowed: session.borrowed } : {},
|
|
6691
7115
|
uploaded: false,
|
|
6692
7116
|
...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
|
|
6693
7117
|
...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
|
|
@@ -6695,6 +7119,14 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6695
7119
|
};
|
|
6696
7120
|
return { episode, gate, scan };
|
|
6697
7121
|
}
|
|
7122
|
+
function sessionSource(store, session, ctx) {
|
|
7123
|
+
if (session.source !== void 0) return session.source || void 0;
|
|
7124
|
+
try {
|
|
7125
|
+
return sourceFor(session.cwd, store.root, ctx);
|
|
7126
|
+
} catch {
|
|
7127
|
+
return void 0;
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
6698
7130
|
function checkpointDue(session, scan, now) {
|
|
6699
7131
|
const since = Date.parse(session.checkpointAt ?? session.startedAt);
|
|
6700
7132
|
if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
|
|
@@ -6809,6 +7241,7 @@ var init_hooks = __esm({
|
|
|
6809
7241
|
init_signals();
|
|
6810
7242
|
init_store2();
|
|
6811
7243
|
init_ui();
|
|
7244
|
+
init_source();
|
|
6812
7245
|
init_view();
|
|
6813
7246
|
NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
|
|
6814
7247
|
NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
@@ -6823,6 +7256,8 @@ var init_hooks = __esm({
|
|
|
6823
7256
|
});
|
|
6824
7257
|
|
|
6825
7258
|
// src/hooks.ts
|
|
7259
|
+
import fs19 from "node:fs";
|
|
7260
|
+
import path14 from "node:path";
|
|
6826
7261
|
function normalizeEvent(raw) {
|
|
6827
7262
|
return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
|
|
6828
7263
|
}
|
|
@@ -6866,6 +7301,8 @@ async function handleHook(rawEvent, input, opts = {}) {
|
|
|
6866
7301
|
const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
|
|
6867
7302
|
if (!store) return {};
|
|
6868
7303
|
const config = store.config();
|
|
7304
|
+
if (!cwdInPool(config, cwd, store.root)) return {};
|
|
7305
|
+
if (deferToProject(cwd)) return {};
|
|
6869
7306
|
const budgetMs = Math.min(opts.budgetMs ?? config.recall.hookBudgetMs ?? HARD_BUDGET_MS, HARD_BUDGET_MS);
|
|
6870
7307
|
const deadline = Date.now() + budgetMs;
|
|
6871
7308
|
try {
|
|
@@ -6886,6 +7323,24 @@ async function handleHook(rawEvent, input, opts = {}) {
|
|
|
6886
7323
|
return {};
|
|
6887
7324
|
}
|
|
6888
7325
|
}
|
|
7326
|
+
function deferToProject(cwd, env = process.env) {
|
|
7327
|
+
if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
|
|
7328
|
+
let dir = path14.resolve(cwd);
|
|
7329
|
+
for (let i = 0; i < 40; i++) {
|
|
7330
|
+
for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
|
|
7331
|
+
try {
|
|
7332
|
+
const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
|
|
7333
|
+
if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
|
|
7334
|
+
} catch {
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
if (fs19.existsSync(path14.join(dir, ".git"))) return false;
|
|
7338
|
+
const up = path14.dirname(dir);
|
|
7339
|
+
if (up === dir) return false;
|
|
7340
|
+
dir = up;
|
|
7341
|
+
}
|
|
7342
|
+
return false;
|
|
7343
|
+
}
|
|
6889
7344
|
async function hookCommand(rawEvent, opts = {}) {
|
|
6890
7345
|
const write = opts.write ?? ((s) => process.stdout.write(s));
|
|
6891
7346
|
let output = {};
|
|
@@ -6908,6 +7363,7 @@ var init_hooks2 = __esm({
|
|
|
6908
7363
|
init_detach();
|
|
6909
7364
|
init_store();
|
|
6910
7365
|
init_hooks();
|
|
7366
|
+
init_source();
|
|
6911
7367
|
HARD_BUDGET_MS = 4e3;
|
|
6912
7368
|
EVENT_NAMES = {
|
|
6913
7369
|
"session-start": "session-start",
|
|
@@ -6927,32 +7383,32 @@ var init_hooks2 = __esm({
|
|
|
6927
7383
|
});
|
|
6928
7384
|
|
|
6929
7385
|
// src/project-hooks.ts
|
|
6930
|
-
import
|
|
6931
|
-
import
|
|
6932
|
-
import { execFileSync as
|
|
7386
|
+
import fs20 from "node:fs";
|
|
7387
|
+
import path15 from "node:path";
|
|
7388
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
6933
7389
|
function codexProjectHooks(cwd) {
|
|
6934
|
-
const legacyFile =
|
|
6935
|
-
const
|
|
7390
|
+
const legacyFile = path15.join(cwd, ".codex", "hooks.json");
|
|
7391
|
+
const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
|
|
6936
7392
|
encoding: "utf8",
|
|
6937
7393
|
stdio: ["ignore", "pipe", "ignore"],
|
|
6938
7394
|
timeout: 2e3
|
|
6939
7395
|
}).trim();
|
|
6940
7396
|
let root, common, directory;
|
|
6941
7397
|
try {
|
|
6942
|
-
root =
|
|
6943
|
-
common =
|
|
6944
|
-
directory =
|
|
7398
|
+
root = git5("--show-toplevel");
|
|
7399
|
+
common = git5("--path-format=absolute", "--git-common-dir");
|
|
7400
|
+
directory = git5("--absolute-git-dir");
|
|
6945
7401
|
} catch {
|
|
6946
7402
|
return { file: legacyFile, shared: false, legacyFile };
|
|
6947
7403
|
}
|
|
6948
|
-
const linked =
|
|
7404
|
+
const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
|
|
6949
7405
|
if (linked) {
|
|
6950
|
-
if (
|
|
7406
|
+
if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
|
|
6951
7407
|
throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
|
|
6952
7408
|
}
|
|
6953
|
-
root =
|
|
7409
|
+
root = path15.dirname(common);
|
|
6954
7410
|
}
|
|
6955
|
-
const file =
|
|
7411
|
+
const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
|
|
6956
7412
|
return { file, shared: linked, legacyFile };
|
|
6957
7413
|
}
|
|
6958
7414
|
var init_project_hooks = __esm({
|
|
@@ -6964,6 +7420,7 @@ var init_project_hooks = __esm({
|
|
|
6964
7420
|
// src/init.ts
|
|
6965
7421
|
var init_exports = {};
|
|
6966
7422
|
__export(init_exports, {
|
|
7423
|
+
BORROW_COMMAND: () => BORROW_COMMAND,
|
|
6967
7424
|
CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
|
|
6968
7425
|
HOOK_EVENTS: () => HOOK_EVENTS,
|
|
6969
7426
|
LEARN_COMMAND: () => LEARN_COMMAND,
|
|
@@ -6975,6 +7432,7 @@ __export(init_exports, {
|
|
|
6975
7432
|
installLearnCommand: () => installLearnCommand,
|
|
6976
7433
|
installMcpConfig: () => installMcpConfig,
|
|
6977
7434
|
installPiExtension: () => installPiExtension,
|
|
7435
|
+
installUserMcp: () => installUserMcp,
|
|
6978
7436
|
isBunshinEntry: () => isBunshinEntry,
|
|
6979
7437
|
mcpLauncher: () => mcpLauncher,
|
|
6980
7438
|
mergeClaudeSettings: () => mergeClaudeSettings,
|
|
@@ -6982,8 +7440,9 @@ __export(init_exports, {
|
|
|
6982
7440
|
removeBunshinHooks: () => removeBunshinHooks,
|
|
6983
7441
|
runInit: () => runInit
|
|
6984
7442
|
});
|
|
6985
|
-
import
|
|
6986
|
-
import
|
|
7443
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7444
|
+
import fs21 from "node:fs";
|
|
7445
|
+
import path16 from "node:path";
|
|
6987
7446
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
6988
7447
|
function isBunshinEntry(entry) {
|
|
6989
7448
|
return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
|
|
@@ -7008,13 +7467,13 @@ function removeBunshinHooks(settings) {
|
|
|
7008
7467
|
else delete out.hooks;
|
|
7009
7468
|
return out;
|
|
7010
7469
|
}
|
|
7011
|
-
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code" } = {}) {
|
|
7470
|
+
function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
|
|
7012
7471
|
const base = removeBunshinHooks(settings);
|
|
7013
7472
|
const hooks = { ...base.hooks ?? {} };
|
|
7014
7473
|
for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
|
|
7015
7474
|
const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
7016
7475
|
const hook = bunshinHookEntry(arg, timeout, bin);
|
|
7017
|
-
hook.command =
|
|
7476
|
+
hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
|
|
7018
7477
|
if (harness === "codex") delete hook._bunshin;
|
|
7019
7478
|
groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
|
|
7020
7479
|
hooks[event] = groups;
|
|
@@ -7028,16 +7487,16 @@ function countBunshinHooks(settings) {
|
|
|
7028
7487
|
);
|
|
7029
7488
|
}
|
|
7030
7489
|
function claudeSettingsPath() {
|
|
7031
|
-
return
|
|
7490
|
+
return path16.join(homeDir(), ".claude", "settings.json");
|
|
7032
7491
|
}
|
|
7033
7492
|
function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
7034
|
-
const file = scope === "global" ?
|
|
7493
|
+
const file = scope === "global" ? path16.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path16.join(cwd, ".pi/extensions/bunshin.js");
|
|
7035
7494
|
const marker = "// Managed by Bunshin setup.";
|
|
7036
7495
|
try {
|
|
7037
|
-
const previous = fileExists(file) ?
|
|
7496
|
+
const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
|
|
7038
7497
|
if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
|
|
7039
7498
|
if (remove) {
|
|
7040
|
-
if (previous)
|
|
7499
|
+
if (previous) fs21.unlinkSync(file);
|
|
7041
7500
|
return { file, installed: 0, changed: !!previous };
|
|
7042
7501
|
}
|
|
7043
7502
|
const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
|
|
@@ -7046,24 +7505,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
|
7046
7505
|
export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
7047
7506
|
`;
|
|
7048
7507
|
if (text2 !== previous) {
|
|
7049
|
-
ensureDir(
|
|
7050
|
-
|
|
7508
|
+
ensureDir(path16.dirname(file));
|
|
7509
|
+
fs21.writeFileSync(file, text2);
|
|
7051
7510
|
}
|
|
7052
7511
|
return { file, installed: 1, changed: text2 !== previous };
|
|
7053
7512
|
} catch (error) {
|
|
7054
7513
|
return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
7055
7514
|
}
|
|
7056
7515
|
}
|
|
7057
|
-
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code" } = {}) {
|
|
7516
|
+
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
|
|
7058
7517
|
try {
|
|
7059
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7060
|
-
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness });
|
|
7518
|
+
const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
|
|
7519
|
+
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
|
|
7061
7520
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7062
7521
|
if (changed) {
|
|
7063
|
-
ensureDir(
|
|
7522
|
+
ensureDir(path16.dirname(file));
|
|
7064
7523
|
if (fileExists(file)) {
|
|
7065
7524
|
try {
|
|
7066
|
-
|
|
7525
|
+
fs21.copyFileSync(file, `${file}.bunshin-backup`);
|
|
7067
7526
|
} catch {
|
|
7068
7527
|
}
|
|
7069
7528
|
}
|
|
@@ -7078,23 +7537,23 @@ function mcpLauncher(bin) {
|
|
|
7078
7537
|
if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
|
|
7079
7538
|
let script = process.argv[1] ?? "";
|
|
7080
7539
|
try {
|
|
7081
|
-
script =
|
|
7540
|
+
script = fs21.realpathSync(script);
|
|
7082
7541
|
} catch {
|
|
7083
7542
|
}
|
|
7084
7543
|
return { command: process.execPath, args: [script, "mcp"] };
|
|
7085
7544
|
}
|
|
7086
|
-
function installLearnCommand({ file, remove = false }) {
|
|
7545
|
+
function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
|
|
7087
7546
|
try {
|
|
7088
|
-
const current = fileExists(file) ?
|
|
7089
|
-
const ours = current === null || current ===
|
|
7547
|
+
const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
|
|
7548
|
+
const ours = current === null || current === content;
|
|
7090
7549
|
if (remove) {
|
|
7091
|
-
if (current !== null && ours)
|
|
7550
|
+
if (current !== null && ours) fs21.unlinkSync(file);
|
|
7092
7551
|
return { file, installed: 0, changed: current !== null && ours };
|
|
7093
7552
|
}
|
|
7094
|
-
if (!ours) return { file, installed: 0, changed: false, error:
|
|
7553
|
+
if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
|
|
7095
7554
|
if (current === null) {
|
|
7096
|
-
|
|
7097
|
-
|
|
7555
|
+
fs21.mkdirSync(path16.dirname(file), { recursive: true });
|
|
7556
|
+
fs21.writeFileSync(file, content);
|
|
7098
7557
|
}
|
|
7099
7558
|
return { file, installed: 1, changed: current === null };
|
|
7100
7559
|
} catch (err) {
|
|
@@ -7103,7 +7562,7 @@ function installLearnCommand({ file, remove = false }) {
|
|
|
7103
7562
|
}
|
|
7104
7563
|
function installMcpConfig({ file, launcher, remove = false }) {
|
|
7105
7564
|
try {
|
|
7106
|
-
const before = fileExists(file) ? JSON.parse(
|
|
7565
|
+
const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
|
|
7107
7566
|
const servers = { ...before.mcpServers ?? {} };
|
|
7108
7567
|
if (remove) delete servers.bunshin;
|
|
7109
7568
|
else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
|
|
@@ -7111,7 +7570,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7111
7570
|
if (!Object.keys(servers).length) delete after.mcpServers;
|
|
7112
7571
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
7113
7572
|
if (changed) {
|
|
7114
|
-
if (!Object.keys(after).length && fileExists(file))
|
|
7573
|
+
if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
|
|
7115
7574
|
else if (Object.keys(after).length) writeJsonAtomic(file, after);
|
|
7116
7575
|
}
|
|
7117
7576
|
return { file, installed: servers.bunshin ? 1 : 0, changed };
|
|
@@ -7119,25 +7578,49 @@ function installMcpConfig({ file, launcher, remove = false }) {
|
|
|
7119
7578
|
return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
|
|
7120
7579
|
}
|
|
7121
7580
|
}
|
|
7581
|
+
function installUserMcp({ launcher, remove = false, claude = "claude" }) {
|
|
7582
|
+
const file = path16.join(homeDir(), ".claude.json");
|
|
7583
|
+
const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
|
|
7584
|
+
try {
|
|
7585
|
+
let had = false;
|
|
7586
|
+
try {
|
|
7587
|
+
run2(["mcp", "get", "bunshin"]);
|
|
7588
|
+
had = true;
|
|
7589
|
+
} catch {
|
|
7590
|
+
}
|
|
7591
|
+
if (had) {
|
|
7592
|
+
try {
|
|
7593
|
+
run2(["mcp", "remove", "bunshin", "-s", "user"]);
|
|
7594
|
+
} catch {
|
|
7595
|
+
}
|
|
7596
|
+
}
|
|
7597
|
+
if (remove) return { file, installed: 0, changed: had };
|
|
7598
|
+
run2(["mcp", "add-json", "bunshin", JSON.stringify({ type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } }), "-s", "user"]);
|
|
7599
|
+
return { file, installed: 1, changed: true };
|
|
7600
|
+
} catch (err) {
|
|
7601
|
+
return { file, installed: 0, changed: false, error: err instanceof Error ? err.message.split("\n")[0] : String(err) };
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7122
7604
|
function runInit(opts = {}) {
|
|
7123
7605
|
const cwd = opts.cwd ?? process.cwd();
|
|
7124
7606
|
const warnings = [];
|
|
7125
7607
|
const scope = opts.scope ?? "repo";
|
|
7126
|
-
const root = scope === "global" ?
|
|
7127
|
-
if (
|
|
7608
|
+
const root = scope === "global" ? path16.join(homeDir(), ".bunshin") : path16.join(cwd, ".bunshin");
|
|
7609
|
+
if (fs21.existsSync(root) && !new Store(root).trusted && fs21.readdirSync(root).length) {
|
|
7128
7610
|
const aside = `${root}.untrusted-${Date.now()}`;
|
|
7129
|
-
|
|
7611
|
+
fs21.renameSync(root, aside);
|
|
7130
7612
|
warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
|
|
7131
7613
|
}
|
|
7132
|
-
const existed = fileExists(
|
|
7614
|
+
const existed = fileExists(path16.join(root, "config.json"));
|
|
7133
7615
|
const store = new Store(root);
|
|
7134
7616
|
store.ensureLayout();
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
const
|
|
7617
|
+
fs21.chmodSync(root, 448);
|
|
7618
|
+
const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
|
|
7619
|
+
if (scope === "repo" && (hookScope === "project" || fileExists(path16.join(cwd, ".git")))) {
|
|
7620
|
+
const ignore = path16.join(cwd, ".gitignore");
|
|
7621
|
+
const current = fileExists(ignore) ? fs21.readFileSync(ignore, "utf8") : "";
|
|
7139
7622
|
if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
|
|
7140
|
-
|
|
7623
|
+
fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
|
|
7141
7624
|
`);
|
|
7142
7625
|
}
|
|
7143
7626
|
}
|
|
@@ -7145,6 +7628,7 @@ function runInit(opts = {}) {
|
|
|
7145
7628
|
const config = {
|
|
7146
7629
|
...previous,
|
|
7147
7630
|
scope,
|
|
7631
|
+
hooks: hookScope,
|
|
7148
7632
|
orgId: opts.orgId ?? previous.orgId,
|
|
7149
7633
|
userId: opts.userId ?? previous.userId,
|
|
7150
7634
|
role: opts.role ?? previous.role,
|
|
@@ -7161,18 +7645,22 @@ function runInit(opts = {}) {
|
|
|
7161
7645
|
if (!fileExists(store.indexFile)) store.rebuildIndex();
|
|
7162
7646
|
let hooks;
|
|
7163
7647
|
if ((opts.removeHooks || !opts.noHooks) && config.harness === "pi") {
|
|
7164
|
-
hooks = installPiExtension(cwd, opts.removeHooks,
|
|
7648
|
+
hooks = installPiExtension(cwd, opts.removeHooks, hookScope === "user" ? "global" : "repo");
|
|
7165
7649
|
if (hooks.error) warnings.push(hooks.error);
|
|
7166
7650
|
}
|
|
7167
7651
|
if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
|
|
7168
|
-
const projectHooks = config.harness === "codex" &&
|
|
7169
|
-
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ??
|
|
7170
|
-
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness });
|
|
7652
|
+
const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
|
|
7653
|
+
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());
|
|
7654
|
+
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
|
|
7655
|
+
if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
|
|
7656
|
+
const local = path16.join(cwd, ".claude", "settings.local.json");
|
|
7657
|
+
if (fileExists(local) && path16.resolve(local) !== path16.resolve(file)) installClaudeHooks({ file: local, remove: true });
|
|
7658
|
+
}
|
|
7171
7659
|
if (projectHooks?.shared) {
|
|
7172
7660
|
hooks.sharedWithWorktrees = true;
|
|
7173
|
-
if (!hooks.error && !opts.removeHooks) ensureDir(
|
|
7661
|
+
if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
|
|
7174
7662
|
}
|
|
7175
|
-
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) &&
|
|
7663
|
+
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
|
|
7176
7664
|
const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
|
|
7177
7665
|
if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
|
|
7178
7666
|
}
|
|
@@ -7180,17 +7668,27 @@ function runInit(opts = {}) {
|
|
|
7180
7668
|
}
|
|
7181
7669
|
let mcp;
|
|
7182
7670
|
let learnCommand;
|
|
7183
|
-
if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code"
|
|
7184
|
-
|
|
7671
|
+
if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
|
|
7672
|
+
const user = hookScope === "user";
|
|
7673
|
+
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 });
|
|
7185
7674
|
if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
|
|
7186
|
-
learnCommand = installLearnCommand({ file:
|
|
7675
|
+
learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
|
|
7187
7676
|
if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
|
|
7188
7677
|
}
|
|
7678
|
+
let borrowCommand;
|
|
7679
|
+
if (opts.removeHooks || !opts.noHooks) {
|
|
7680
|
+
const user = hookScope === "user";
|
|
7681
|
+
const file = config.harness === "claude-code" ? path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path16.join(process.env.CODEX_HOME ?? path16.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path16.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path16.join(cwd, ".pi", "prompts", "borrow.md") : null;
|
|
7682
|
+
if (file) {
|
|
7683
|
+
borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
|
|
7684
|
+
if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7189
7687
|
if (!resolveBackend(config)) {
|
|
7190
7688
|
warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
|
|
7191
7689
|
}
|
|
7192
7690
|
store.log("init", { root, scope, role: config.role, orgUnit: config.orgUnit, hooks: hooks?.installed ?? 0 });
|
|
7193
|
-
return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, warnings };
|
|
7691
|
+
return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, ...borrowCommand ? { borrowCommand } : {}, warnings };
|
|
7194
7692
|
}
|
|
7195
7693
|
function otherHarnessInstructions() {
|
|
7196
7694
|
return [
|
|
@@ -7206,7 +7704,7 @@ function otherHarnessInstructions() {
|
|
|
7206
7704
|
" Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
|
|
7207
7705
|
].join("\n");
|
|
7208
7706
|
}
|
|
7209
|
-
var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND;
|
|
7707
|
+
var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
|
|
7210
7708
|
var init_init = __esm({
|
|
7211
7709
|
"src/init.ts"() {
|
|
7212
7710
|
"use strict";
|
|
@@ -7232,6 +7730,7 @@ var init_init = __esm({
|
|
|
7232
7730
|
{ event: "SessionEnd", arg: "session-end", timeout: 3 }
|
|
7233
7731
|
];
|
|
7234
7732
|
LEARN_COMMAND = '---\ndescription: Capture what this session learned into the Bunshin library now (applied locally as a trial, submitted for the nightly review)\nargument-hint: "[what to capture, e.g. \\"the ledger must be reconciled before closing\\"]"\nallowed-tools: Bash(bunshin learn:*)\n---\n\nCapture knowledge from this session into Bunshin.\n\nRun exactly this with the Bash tool (it takes up to two minutes; use a timeout of 180000 ms):\n\n```bash\nbunshin learn "$ARGUMENTS"\n```\n\n`bunshin learn` forks this conversation (`claude -p --resume <this session> --fork-session`),\nso the reflection sees everything you saw without re-sending the transcript. It applies what it\nfinds locally as a trial and queues it for the nightly review:\n\n- edits to this repo\'s SPEC.md / TESTING.md / SKILL.md land in the working tree (they show up\n in `git diff`);\n- knowledge with no repo home becomes a local overlay on the Bunshin library;\n- per-machine values stay private.\n\nRelay its output to the user as it is. Each line reads\n`captured: <summary> \u2192 <section or file#anchor>`. If it prints `nothing new to capture.`, say so\nin one line; that is the normal outcome. If it reports an error, show it and stop; do not retry\nin a loop, and do not edit the library files by hand.\n\nIf the user named nothing specific, run it without arguments. Do not continue with other work in\nthe same reply unless the user asked you to.\n';
|
|
7733
|
+
BORROW_COMMAND = '---\ndescription: Bring in a lesson or skill learned elsewhere in this workspace (another project or group) when you know it will help here\nargument-hint: "[what to look for, e.g. \\"the release checklist from the api repo\\"]"\nallowed-tools: Bash(bunshin recall:*), Bash(bunshin load:*)\n---\n\nThe person wants something learned outside this project\'s scope. Search the whole workspace for it\n(run with the Bash tool):\n\n```bash\nbunshin recall "$ARGUMENTS" --hypothetical "$ARGUMENTS" --across\n```\n\nShow the person the numbered results as printed (each says where it was learned). Load the one\nthey pick, or the one that plainly matches, with `bunshin load <id>`, then use it as prior\nknowledge for the task. Loading it records that it was borrowed into this project: the nightly\ngeneralization reads that as evidence the lesson applies beyond where it was learned.\n\nIf nothing matches, say so in one line and carry on.\n';
|
|
7235
7734
|
}
|
|
7236
7735
|
});
|
|
7237
7736
|
|
|
@@ -7253,19 +7752,19 @@ function mark(v2, sessionId, cwd, field, ids) {
|
|
|
7253
7752
|
} catch {
|
|
7254
7753
|
}
|
|
7255
7754
|
}
|
|
7256
|
-
async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3 }) {
|
|
7755
|
+
async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3, across = false }) {
|
|
7257
7756
|
const v2 = new V2Store(store);
|
|
7258
7757
|
const config = store.config();
|
|
7259
7758
|
const sid = sessionId ?? sessionIdFromEnv(v2);
|
|
7260
7759
|
const session = sid ? v2.readSession(sid) : null;
|
|
7261
7760
|
const exclude = session ? [...session.loaded] : [];
|
|
7262
7761
|
const limit = Math.max(1, Math.min(10, query.limit ?? 5));
|
|
7263
|
-
const view = localView(v2, config, cwd);
|
|
7264
|
-
const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude });
|
|
7762
|
+
const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
|
|
7763
|
+
const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude, across });
|
|
7265
7764
|
let out;
|
|
7266
7765
|
if (canCall(config)) {
|
|
7267
7766
|
try {
|
|
7268
|
-
const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude }, { timeoutMs });
|
|
7767
|
+
const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude, ...view.source ? { source: view.source } : {}, ...across ? { across: true } : {} }, { timeoutMs });
|
|
7269
7768
|
const remote = Array.isArray(res.candidates) ? res.candidates : [];
|
|
7270
7769
|
const extra = local().filter((c) => c.id.startsWith("local:") && !remote.some((r) => r.id === c.id));
|
|
7271
7770
|
out = { candidates: [...remote, ...extra].slice(0, limit), source: "mothership" };
|
|
@@ -7280,7 +7779,8 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
|
|
|
7280
7779
|
const v2 = new V2Store(store);
|
|
7281
7780
|
const config = store.config();
|
|
7282
7781
|
const sid = sessionId ?? sessionIdFromEnv(v2);
|
|
7283
|
-
const
|
|
7782
|
+
const session = sid ? v2.readSession(sid) : null;
|
|
7783
|
+
const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
|
|
7284
7784
|
let section = view.byId.get(id);
|
|
7285
7785
|
let source = "local";
|
|
7286
7786
|
if (!section && canCall(config)) {
|
|
@@ -7293,8 +7793,17 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
|
|
|
7293
7793
|
}
|
|
7294
7794
|
if (!section) return null;
|
|
7295
7795
|
mark(v2, sid, cwd, "loaded", [section.id]);
|
|
7796
|
+
const borrowed = !inBoundary(section, view.reader);
|
|
7797
|
+
if (borrowed && sid) {
|
|
7798
|
+
try {
|
|
7799
|
+
v2.updateSession(sid, cwd, (s) => {
|
|
7800
|
+
s.borrowed = [...(s.borrowed ?? []).filter((b) => b.sectionId !== section.id), { sectionId: section.id, layer: section.layer, at: (/* @__PURE__ */ new Date()).toISOString() }].slice(-50);
|
|
7801
|
+
});
|
|
7802
|
+
} catch {
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7296
7805
|
const part = anchor?.replace(/^[§#]/, "");
|
|
7297
|
-
return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source };
|
|
7806
|
+
return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source, ...borrowed ? { borrowed: true } : {} };
|
|
7298
7807
|
}
|
|
7299
7808
|
var init_library = __esm({
|
|
7300
7809
|
"src/v2/library.ts"() {
|
|
@@ -7318,6 +7827,7 @@ __export(commands_exports, {
|
|
|
7318
7827
|
cmdReflectFork: () => cmdReflectFork,
|
|
7319
7828
|
cmdReview: () => cmdReview,
|
|
7320
7829
|
cmdReviewDecide: () => cmdReviewDecide,
|
|
7830
|
+
cmdSources: () => cmdSources,
|
|
7321
7831
|
cmdSuggest: () => cmdSuggest,
|
|
7322
7832
|
cmdSync: () => cmdSync,
|
|
7323
7833
|
robotAttrsFromFlags: () => robotAttrsFromFlags,
|
|
@@ -7325,9 +7835,9 @@ __export(commands_exports, {
|
|
|
7325
7835
|
setupRobot: () => setupRobot,
|
|
7326
7836
|
v2Status: () => v2Status
|
|
7327
7837
|
});
|
|
7328
|
-
import
|
|
7329
|
-
import
|
|
7330
|
-
import { createHash as
|
|
7838
|
+
import fs22 from "node:fs";
|
|
7839
|
+
import path17 from "node:path";
|
|
7840
|
+
import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
|
|
7331
7841
|
function cmdReview(store, args, cwd, out, err) {
|
|
7332
7842
|
const v2 = new V2Store(store);
|
|
7333
7843
|
const sub = args._[1] ?? "list";
|
|
@@ -7427,18 +7937,19 @@ async function cmdReviewDecide(store, args, out, err) {
|
|
|
7427
7937
|
async function cmdRecall(store, args, cwd, out, err) {
|
|
7428
7938
|
const abstract = args._.slice(1).join(" ").trim() || str3(args.flags.abstract) || "";
|
|
7429
7939
|
if (!abstract) {
|
|
7430
|
-
err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--json]\n');
|
|
7940
|
+
err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--across] [--json]\n');
|
|
7431
7941
|
return 1;
|
|
7432
7942
|
}
|
|
7433
7943
|
const limit = Number(str3(args.flags.limit)) || void 0;
|
|
7434
|
-
const
|
|
7944
|
+
const across = bool2(args.flags.across);
|
|
7945
|
+
const res = await recallPull(store, { taskAbstract: abstract, hypotheticalLesson: str3(args.flags.hypothetical) ?? abstract, identifiers: list(args.flags.id ?? args.flags.ids), ...limit ? { limit } : {} }, { cwd, sessionId: str3(args.flags.session), across });
|
|
7435
7946
|
if (bool2(args.flags.json)) {
|
|
7436
7947
|
out(JSON.stringify(res, null, 2) + "\n");
|
|
7437
7948
|
return 0;
|
|
7438
7949
|
}
|
|
7439
|
-
out(renderCandidates(res.candidates) + "\n");
|
|
7950
|
+
out(renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes) + "\n");
|
|
7440
7951
|
if (res.candidates.length) out(`
|
|
7441
|
-
(${res.source}) Load one with: bunshin load <id
|
|
7952
|
+
(${res.source}) Load one with: bunshin load <id>${across && res.candidates.some((c) => c.outside) ? " (a lesson from outside this project's scope is recorded as borrowed: evidence it applies here too)" : ""}
|
|
7442
7953
|
`);
|
|
7443
7954
|
return 0;
|
|
7444
7955
|
}
|
|
@@ -7455,7 +7966,7 @@ async function cmdLoad(store, args, cwd, out, err) {
|
|
|
7455
7966
|
`);
|
|
7456
7967
|
return 1;
|
|
7457
7968
|
}
|
|
7458
|
-
out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source }, null, 2) + "\n" : res.text + "\n");
|
|
7969
|
+
out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source, ...res.borrowed ? { borrowed: true } : {} }, null, 2) + "\n" : res.text + "\n");
|
|
7459
7970
|
return 0;
|
|
7460
7971
|
}
|
|
7461
7972
|
async function cmdLearn(store, args, cwd, out, err) {
|
|
@@ -7496,11 +8007,11 @@ function cmdSuggest(store, args, cwd, out, err) {
|
|
|
7496
8007
|
err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
|
|
7497
8008
|
return 1;
|
|
7498
8009
|
}
|
|
7499
|
-
const harnessDir =
|
|
7500
|
-
const file = str3(args.flags.file) ?
|
|
8010
|
+
const harnessDir = path17.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
|
|
8011
|
+
const file = str3(args.flags.file) ? path17.resolve(cwd, str3(args.flags.file)) : path17.join(harnessDir, rel);
|
|
7501
8012
|
let content;
|
|
7502
8013
|
try {
|
|
7503
|
-
content =
|
|
8014
|
+
content = fs22.readFileSync(file, "utf8");
|
|
7504
8015
|
} catch {
|
|
7505
8016
|
err(`bunshin suggest: cannot read ${file}
|
|
7506
8017
|
`);
|
|
@@ -7621,15 +8132,15 @@ async function cmdQuestions(store, args, out, err) {
|
|
|
7621
8132
|
return 0;
|
|
7622
8133
|
}
|
|
7623
8134
|
function saveCredential(storeRoot, apiUrl, apiKey, id) {
|
|
7624
|
-
const dir =
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
const file =
|
|
8135
|
+
const dir = path17.join(homeDir(), ".config/bunshin/credentials");
|
|
8136
|
+
fs22.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
8137
|
+
fs22.chmodSync(dir, 448);
|
|
8138
|
+
const file = path17.join(dir, `${createHash5("sha256").update(`${storeRoot}
|
|
7628
8139
|
${apiUrl}
|
|
7629
8140
|
${id}`).digest("hex")}.json`);
|
|
7630
8141
|
const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
|
|
7631
|
-
|
|
7632
|
-
|
|
8142
|
+
fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8143
|
+
fs22.renameSync(tmp, file);
|
|
7633
8144
|
return file;
|
|
7634
8145
|
}
|
|
7635
8146
|
async function setupRobot(opts) {
|
|
@@ -7637,10 +8148,10 @@ async function setupRobot(opts) {
|
|
|
7637
8148
|
const existing = Store.discover(opts.cwd);
|
|
7638
8149
|
let store;
|
|
7639
8150
|
const harness = opts.harness ?? "other";
|
|
7640
|
-
if (existing &&
|
|
8151
|
+
if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
|
|
7641
8152
|
else {
|
|
7642
8153
|
runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
|
|
7643
|
-
store = new Store(
|
|
8154
|
+
store = new Store(path17.join(opts.cwd, ".bunshin"));
|
|
7644
8155
|
}
|
|
7645
8156
|
let config = store.config();
|
|
7646
8157
|
const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
|
|
@@ -7703,8 +8214,10 @@ function v2Status(store, cwd) {
|
|
|
7703
8214
|
}
|
|
7704
8215
|
const view = localView(v2, config, cwd, { index: false });
|
|
7705
8216
|
const sync = v2.readSyncState();
|
|
8217
|
+
const pooled = cwdInPool(config, cwd, store.root);
|
|
7706
8218
|
return {
|
|
7707
8219
|
context: { id: view.ctx.id, kind: view.ctx.kind, layers: view.layers },
|
|
8220
|
+
source: { id: pooled ? sourceFor(cwd, store.root, view.ctx) ?? null : null, inPool: pooled, folder: topFolder(cwd, store.root) || null },
|
|
7708
8221
|
bundle: bundle ? { sections: bundle.sections.length, generatedAt: bundle.generatedAt, release: bundle.release?.version ?? null } : null,
|
|
7709
8222
|
repo: { name: repo, sections: repoSections },
|
|
7710
8223
|
overlays: v2.readOverlays().length,
|
|
@@ -7715,6 +8228,46 @@ function v2Status(store, cwd) {
|
|
|
7715
8228
|
session: v2.readCurrent()?.sessionId ?? null
|
|
7716
8229
|
};
|
|
7717
8230
|
}
|
|
8231
|
+
function cmdSources(store, args, cwd, out, err) {
|
|
8232
|
+
const config = store.config();
|
|
8233
|
+
const action = args._[1];
|
|
8234
|
+
const names = args._.slice(2).flatMap((x) => x.split(",")).map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
|
|
8235
|
+
if (action) {
|
|
8236
|
+
if (action !== "all" && action !== "only" && action !== "include" && action !== "exclude") {
|
|
8237
|
+
err("usage: bunshin sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>]\n");
|
|
8238
|
+
return 1;
|
|
8239
|
+
}
|
|
8240
|
+
if (action !== "all" && !names.length) {
|
|
8241
|
+
err(`bunshin sources ${action}: name at least one folder
|
|
8242
|
+
`);
|
|
8243
|
+
return 1;
|
|
8244
|
+
}
|
|
8245
|
+
const known = new Set(listFolders(store.root, void 0).map((f) => f.folder));
|
|
8246
|
+
const unknown = names.filter((n) => !known.has(n));
|
|
8247
|
+
if (unknown.length) {
|
|
8248
|
+
err(`bunshin sources: not a folder in ${storeHome(store.root)}: ${unknown.join(", ")}
|
|
8249
|
+
`);
|
|
8250
|
+
return 1;
|
|
8251
|
+
}
|
|
8252
|
+
const next = select(config.sources, action, names);
|
|
8253
|
+
const { sources: _drop, ...rest } = config;
|
|
8254
|
+
void _drop;
|
|
8255
|
+
store.saveConfig(next ? { ...rest, sources: next } : rest);
|
|
8256
|
+
}
|
|
8257
|
+
const selection = store.config().sources;
|
|
8258
|
+
const folders = listFolders(store.root, selection, { sources: true });
|
|
8259
|
+
if (bool2(args.flags.json)) {
|
|
8260
|
+
out(JSON.stringify({ home: storeHome(store.root), selection: selection ?? null, folders }, null, 2) + "\n");
|
|
8261
|
+
return 0;
|
|
8262
|
+
}
|
|
8263
|
+
const here = topFolder(cwd, store.root);
|
|
8264
|
+
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)"}
|
|
8265
|
+
`);
|
|
8266
|
+
if (!folders.length) out(" (no folders here yet)\n");
|
|
8267
|
+
for (const f of folders) out(` ${f.included ? "in " : "out"} ${f.folder}${f.source ? ` (${f.source})` : ""}${f.folder === here ? " \u2190 here" : ""}
|
|
8268
|
+
`);
|
|
8269
|
+
return 0;
|
|
8270
|
+
}
|
|
7718
8271
|
var str3, bool2, list;
|
|
7719
8272
|
var init_commands = __esm({
|
|
7720
8273
|
"src/v2/commands.ts"() {
|
|
@@ -7731,6 +8284,7 @@ var init_commands = __esm({
|
|
|
7731
8284
|
init_repo();
|
|
7732
8285
|
init_store2();
|
|
7733
8286
|
init_sync();
|
|
8287
|
+
init_source();
|
|
7734
8288
|
init_view();
|
|
7735
8289
|
str3 = (v) => typeof v === "string" ? v : void 0;
|
|
7736
8290
|
bool2 = (v) => v === true || v === "true";
|
|
@@ -7743,6 +8297,7 @@ var setup_exports = {};
|
|
|
7743
8297
|
__export(setup_exports, {
|
|
7744
8298
|
HOSTED_API: () => HOSTED_API,
|
|
7745
8299
|
apiAddress: () => apiAddress,
|
|
8300
|
+
askSources: () => askSources,
|
|
7746
8301
|
connectionProfile: () => connectionProfile,
|
|
7747
8302
|
executable: () => executable,
|
|
7748
8303
|
hookBinary: () => hookBinary,
|
|
@@ -7750,16 +8305,16 @@ __export(setup_exports, {
|
|
|
7750
8305
|
setupProject: () => setupProject,
|
|
7751
8306
|
shellQuote: () => shellQuote
|
|
7752
8307
|
});
|
|
7753
|
-
import
|
|
7754
|
-
import
|
|
7755
|
-
import { createHash as
|
|
8308
|
+
import fs23 from "node:fs";
|
|
8309
|
+
import path18 from "node:path";
|
|
8310
|
+
import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
|
|
7756
8311
|
function executable(name) {
|
|
7757
|
-
const directories = [...(process.env.PATH ?? "").split(
|
|
8312
|
+
const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
|
|
7758
8313
|
for (const directory of directories) {
|
|
7759
|
-
const file =
|
|
8314
|
+
const file = path18.resolve(directory, name);
|
|
7760
8315
|
try {
|
|
7761
|
-
|
|
7762
|
-
if (
|
|
8316
|
+
fs23.accessSync(file, fs23.constants.X_OK);
|
|
8317
|
+
if (fs23.statSync(file).isFile()) return file;
|
|
7763
8318
|
} catch {
|
|
7764
8319
|
}
|
|
7765
8320
|
}
|
|
@@ -7767,8 +8322,8 @@ function executable(name) {
|
|
|
7767
8322
|
}
|
|
7768
8323
|
function hookBinary(harness) {
|
|
7769
8324
|
const agent = executable(harness === "claude-code" ? "claude" : harness);
|
|
7770
|
-
const directories = [.../* @__PURE__ */ new Set([
|
|
7771
|
-
return `PATH=${shellQuote(directories.join(
|
|
8325
|
+
const directories = [.../* @__PURE__ */ new Set([path18.dirname(process.execPath), path18.join(homeDir(), ".local/bin"), ...agent ? [path18.dirname(agent)] : []])];
|
|
8326
|
+
return `PATH=${shellQuote(directories.join(path18.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs23.realpathSync(process.argv[1]))}`;
|
|
7772
8327
|
}
|
|
7773
8328
|
function apiAddress(raw) {
|
|
7774
8329
|
const url = new URL(raw);
|
|
@@ -7837,33 +8392,37 @@ async function setupProject(opts) {
|
|
|
7837
8392
|
const apiKey = opts.apiKey.trim();
|
|
7838
8393
|
if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
|
|
7839
8394
|
const profile = await connectionProfile(apiUrl, apiKey);
|
|
7840
|
-
const root =
|
|
8395
|
+
const root = path18.join(opts.cwd, ".bunshin");
|
|
7841
8396
|
const store = new Store(root);
|
|
7842
8397
|
const previous = store.config();
|
|
7843
8398
|
if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
|
|
7844
8399
|
throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
|
|
7845
8400
|
}
|
|
7846
|
-
const credentialDirectory =
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
const name =
|
|
8401
|
+
const credentialDirectory = path18.join(homeDir(), ".config/bunshin/credentials");
|
|
8402
|
+
fs23.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
|
|
8403
|
+
fs23.chmodSync(credentialDirectory, 448);
|
|
8404
|
+
const name = createHash6("sha256").update(`${root}
|
|
7850
8405
|
${apiUrl}
|
|
7851
8406
|
${profile.orgId}
|
|
7852
8407
|
${profile.userId}`).digest("hex");
|
|
7853
|
-
const credentialFile =
|
|
8408
|
+
const credentialFile = path18.join(credentialDirectory, `${name}.json`);
|
|
7854
8409
|
const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
|
|
7855
|
-
|
|
7856
|
-
|
|
8410
|
+
fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
8411
|
+
fs23.renameSync(temporary, credentialFile);
|
|
7857
8412
|
const result = runInit({
|
|
7858
8413
|
cwd: opts.cwd,
|
|
7859
8414
|
harness: opts.harness,
|
|
7860
8415
|
apiUrl,
|
|
7861
8416
|
...profile,
|
|
7862
8417
|
noHooks: opts.noHooks,
|
|
7863
|
-
bin: opts.bin ?? hookBinary(opts.harness)
|
|
8418
|
+
bin: opts.bin ?? hookBinary(opts.harness),
|
|
8419
|
+
...opts.hooks ? { hooks: opts.hooks } : {}
|
|
7864
8420
|
});
|
|
8421
|
+
const { sources: kept, ...base } = result.config;
|
|
8422
|
+
const sources = opts.sources === void 0 ? kept : opts.sources ?? void 0;
|
|
7865
8423
|
store.saveConfig({
|
|
7866
|
-
...
|
|
8424
|
+
...base,
|
|
8425
|
+
...sources ? { sources } : {},
|
|
7867
8426
|
...profile,
|
|
7868
8427
|
apiKey: void 0,
|
|
7869
8428
|
credentialFile,
|
|
@@ -7874,6 +8433,39 @@ ${profile.userId}`).digest("hex");
|
|
|
7874
8433
|
trustStore(root);
|
|
7875
8434
|
return { ...result, profile, credentialFile };
|
|
7876
8435
|
}
|
|
8436
|
+
async function askSources(store, out) {
|
|
8437
|
+
const { listFolders: listFolders2, select: select2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
8438
|
+
const config = store.config();
|
|
8439
|
+
const folders = listFolders2(store.root, void 0);
|
|
8440
|
+
if (folders.length < 2) return;
|
|
8441
|
+
const readline2 = await import("node:readline/promises");
|
|
8442
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
8443
|
+
try {
|
|
8444
|
+
out(`This folder holds ${folders.length} folders:
|
|
8445
|
+
`);
|
|
8446
|
+
folders.forEach((f, i) => out(` ${String(i + 1).padStart(2)}. ${f.folder}
|
|
8447
|
+
`));
|
|
8448
|
+
const answer = (await rl.question("Learn from [a]ll of them, all [e]xcept some, or [o]nly some? [a] ")).trim().toLowerCase();
|
|
8449
|
+
const mode = answer.startsWith("e") || answer.startsWith("x") ? "exclude" : answer.startsWith("o") || answer.startsWith("p") ? "only" : "all";
|
|
8450
|
+
const { sources: _drop, ...rest } = config;
|
|
8451
|
+
void _drop;
|
|
8452
|
+
if (mode === "all") {
|
|
8453
|
+
store.saveConfig(rest);
|
|
8454
|
+
out("Learning from every folder here; new folders join automatically.\n");
|
|
8455
|
+
return;
|
|
8456
|
+
}
|
|
8457
|
+
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): ");
|
|
8458
|
+
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))];
|
|
8459
|
+
const next = select2(void 0, mode, picked);
|
|
8460
|
+
store.saveConfig(next ? { ...rest, sources: next } : rest);
|
|
8461
|
+
if (mode === "exclude") out(picked.length ? `Learning from every folder but ${picked.join(", ")}; new folders join automatically.
|
|
8462
|
+
` : "Learning from every folder here; new folders join automatically.\n");
|
|
8463
|
+
else out(picked.length ? `Learning from ${picked.join(", ")} only; new folders stay out until you add them (bunshin sources include <folder>).
|
|
8464
|
+
` : "Nothing here takes part until you add a folder (bunshin sources include <folder>).\n");
|
|
8465
|
+
} finally {
|
|
8466
|
+
rl.close();
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
7877
8469
|
var shellQuote;
|
|
7878
8470
|
var init_setup = __esm({
|
|
7879
8471
|
"src/setup.ts"() {
|
|
@@ -7924,6 +8516,7 @@ async function handleMcp(msg, opts) {
|
|
|
7924
8516
|
const cwd = opts.cwd ?? process.cwd();
|
|
7925
8517
|
const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
|
|
7926
8518
|
if (!store) return reply(text("Bunshin is not set up in this project (run `bunshin setup`). Proceed with general knowledge.", true));
|
|
8519
|
+
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));
|
|
7927
8520
|
try {
|
|
7928
8521
|
if (name === "bunshin_recall") {
|
|
7929
8522
|
const taskAbstract = typeof args.task_abstract === "string" ? args.task_abstract : "";
|
|
@@ -7931,8 +8524,8 @@ async function handleMcp(msg, opts) {
|
|
|
7931
8524
|
if (!taskAbstract.trim()) return reply(text("task_abstract is required: describe the task abstractly.", true));
|
|
7932
8525
|
const identifiers = Array.isArray(args.identifiers) ? args.identifiers.filter((x) => typeof x === "string") : [];
|
|
7933
8526
|
const limit = typeof args.limit === "number" ? args.limit : void 0;
|
|
7934
|
-
const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd });
|
|
7935
|
-
const body = renderCandidates(res.candidates);
|
|
8527
|
+
const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd, across: args.across === true });
|
|
8528
|
+
const body = renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes);
|
|
7936
8529
|
return reply(text(res.candidates.length ? `${body}
|
|
7937
8530
|
|
|
7938
8531
|
Load the ones that would change what you do with bunshin_load (0-3).` : body));
|
|
@@ -7987,7 +8580,9 @@ var init_mcp = __esm({
|
|
|
7987
8580
|
"src/v2/mcp.ts"() {
|
|
7988
8581
|
"use strict";
|
|
7989
8582
|
init_v22();
|
|
8583
|
+
init_source();
|
|
7990
8584
|
init_store();
|
|
8585
|
+
init_store2();
|
|
7991
8586
|
init_library();
|
|
7992
8587
|
DEFAULT_PROTOCOL = "2025-06-18";
|
|
7993
8588
|
MCP_TOOLS = [
|
|
@@ -8001,7 +8596,8 @@ var init_mcp = __esm({
|
|
|
8001
8596
|
task_abstract: { type: "string", description: "The task in abstract, structural terms (what kind of problem this is), not a restatement of its topic." },
|
|
8002
8597
|
hypothetical_lesson: { type: "string", description: "The lesson you would want to find, written as if it existed." },
|
|
8003
8598
|
identifiers: { type: "array", items: { type: "string" }, description: "Concrete identifiers involved: function names, error codes, file paths, product or system names. May be empty." },
|
|
8004
|
-
limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." }
|
|
8599
|
+
limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." },
|
|
8600
|
+
across: { type: "boolean", description: "Only when the person asks for something learned elsewhere (another project or group of this workspace): search across the project's boundary. Default false." }
|
|
8005
8601
|
},
|
|
8006
8602
|
required: ["task_abstract", "hypothetical_lesson", "identifiers"],
|
|
8007
8603
|
additionalProperties: false
|
|
@@ -8164,9 +8760,9 @@ __export(sync_exports2, {
|
|
|
8164
8760
|
resolveConflict: () => resolveConflict,
|
|
8165
8761
|
runSync: () => runSync
|
|
8166
8762
|
});
|
|
8167
|
-
import { createHash as
|
|
8168
|
-
import
|
|
8169
|
-
import
|
|
8763
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8764
|
+
import path19 from "node:path";
|
|
8765
|
+
import fs24 from "node:fs";
|
|
8170
8766
|
async function api(config, method, route, body, timeoutMs = 1e4) {
|
|
8171
8767
|
const ctrl = new AbortController();
|
|
8172
8768
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
@@ -8204,22 +8800,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
|
|
|
8204
8800
|
}
|
|
8205
8801
|
}
|
|
8206
8802
|
async function runSync(store) {
|
|
8207
|
-
const lock =
|
|
8803
|
+
const lock = path19.join(store.root, "sync.lock");
|
|
8208
8804
|
const deadline = Date.now() + 12e4;
|
|
8209
8805
|
for (; ; ) {
|
|
8210
8806
|
try {
|
|
8211
|
-
|
|
8807
|
+
fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
8212
8808
|
break;
|
|
8213
8809
|
} catch (error) {
|
|
8214
8810
|
if (error.code !== "EEXIST") throw error;
|
|
8215
8811
|
try {
|
|
8216
|
-
const owner = Number(
|
|
8812
|
+
const owner = Number(fs24.readFileSync(lock, "utf8"));
|
|
8217
8813
|
if (Number.isInteger(owner) && owner > 0) {
|
|
8218
8814
|
try {
|
|
8219
8815
|
process.kill(owner, 0);
|
|
8220
8816
|
} catch (error2) {
|
|
8221
8817
|
if (error2.code === "ESRCH") {
|
|
8222
|
-
|
|
8818
|
+
fs24.unlinkSync(lock);
|
|
8223
8819
|
continue;
|
|
8224
8820
|
}
|
|
8225
8821
|
}
|
|
@@ -8238,7 +8834,7 @@ async function runSync(store) {
|
|
|
8238
8834
|
return await syncStore(store);
|
|
8239
8835
|
} finally {
|
|
8240
8836
|
try {
|
|
8241
|
-
|
|
8837
|
+
fs24.unlinkSync(lock);
|
|
8242
8838
|
} catch {
|
|
8243
8839
|
}
|
|
8244
8840
|
}
|
|
@@ -8266,9 +8862,9 @@ async function syncStore(store) {
|
|
|
8266
8862
|
result.errors.push(String(e instanceof Error ? e.message : e));
|
|
8267
8863
|
}
|
|
8268
8864
|
}
|
|
8269
|
-
const acknowledgementsFile =
|
|
8865
|
+
const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
|
|
8270
8866
|
const acknowledgements = readJson(acknowledgementsFile, {});
|
|
8271
|
-
const fingerprint2 = (trace) =>
|
|
8867
|
+
const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
|
|
8272
8868
|
const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
|
|
8273
8869
|
for (const t of traces) {
|
|
8274
8870
|
try {
|
|
@@ -8521,11 +9117,11 @@ __export(demo_exports, {
|
|
|
8521
9117
|
runDemo: () => runDemo
|
|
8522
9118
|
});
|
|
8523
9119
|
import assert from "node:assert/strict";
|
|
8524
|
-
import
|
|
9120
|
+
import fs25 from "node:fs";
|
|
8525
9121
|
import os4 from "node:os";
|
|
8526
|
-
import
|
|
9122
|
+
import path20 from "node:path";
|
|
8527
9123
|
async function runDemo(write) {
|
|
8528
|
-
const root =
|
|
9124
|
+
const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
|
|
8529
9125
|
try {
|
|
8530
9126
|
const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
|
|
8531
9127
|
const { lessons, constraint, workflow } = aprScenario();
|
|
@@ -8542,7 +9138,7 @@ Task: ${APR_TASK}
|
|
|
8542
9138
|
write(result.text);
|
|
8543
9139
|
write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
|
|
8544
9140
|
} finally {
|
|
8545
|
-
|
|
9141
|
+
fs25.rmSync(root, { recursive: true, force: true });
|
|
8546
9142
|
}
|
|
8547
9143
|
}
|
|
8548
9144
|
var init_demo = __esm({
|
|
@@ -8555,21 +9151,21 @@ var init_demo = __esm({
|
|
|
8555
9151
|
});
|
|
8556
9152
|
|
|
8557
9153
|
// src/v2/gitremote.ts
|
|
8558
|
-
import
|
|
8559
|
-
import
|
|
8560
|
-
import { createHash as
|
|
9154
|
+
import fs26 from "node:fs";
|
|
9155
|
+
import path21 from "node:path";
|
|
9156
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8561
9157
|
function saveGitCredential(apiUrl, apiKey) {
|
|
8562
9158
|
const file = gitCredentialFile();
|
|
8563
|
-
|
|
9159
|
+
fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
|
|
8564
9160
|
let all = {};
|
|
8565
9161
|
try {
|
|
8566
|
-
all = JSON.parse(
|
|
9162
|
+
all = JSON.parse(fs26.readFileSync(file, "utf8"));
|
|
8567
9163
|
} catch {
|
|
8568
9164
|
}
|
|
8569
9165
|
all[apiUrl.replace(/\/$/, "")] = apiKey;
|
|
8570
9166
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8571
|
-
|
|
8572
|
-
|
|
9167
|
+
fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
9168
|
+
fs26.renameSync(tmp, file);
|
|
8573
9169
|
}
|
|
8574
9170
|
function resolveGitKey(apiUrl) {
|
|
8575
9171
|
const base = apiUrl.replace(/\/$/, "");
|
|
@@ -8577,20 +9173,20 @@ function resolveGitKey(apiUrl) {
|
|
|
8577
9173
|
const origin = urlOrigin(base);
|
|
8578
9174
|
if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
|
|
8579
9175
|
try {
|
|
8580
|
-
const all = JSON.parse(
|
|
9176
|
+
const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
|
|
8581
9177
|
if (all[base]) return all[base];
|
|
8582
9178
|
} catch {
|
|
8583
9179
|
}
|
|
8584
|
-
const dir =
|
|
9180
|
+
const dir = path21.join(homeDir(), ".config/bunshin/credentials");
|
|
8585
9181
|
let files = [];
|
|
8586
9182
|
try {
|
|
8587
|
-
files =
|
|
9183
|
+
files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
8588
9184
|
} catch {
|
|
8589
9185
|
return void 0;
|
|
8590
9186
|
}
|
|
8591
9187
|
for (const f of files.sort()) {
|
|
8592
9188
|
try {
|
|
8593
|
-
const c = JSON.parse(
|
|
9189
|
+
const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
|
|
8594
9190
|
if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
|
|
8595
9191
|
} catch {
|
|
8596
9192
|
}
|
|
@@ -8598,7 +9194,7 @@ function resolveGitKey(apiUrl) {
|
|
|
8598
9194
|
return void 0;
|
|
8599
9195
|
}
|
|
8600
9196
|
function refsDigest(refs) {
|
|
8601
|
-
return
|
|
9197
|
+
return createHash8("sha256").update(
|
|
8602
9198
|
[...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
|
|
8603
9199
|
).digest("hex").slice(0, 12);
|
|
8604
9200
|
}
|
|
@@ -8611,7 +9207,7 @@ var init_gitremote = __esm({
|
|
|
8611
9207
|
ZERO_SHA = "0".repeat(40);
|
|
8612
9208
|
REMOTE_PATH = "/v1/git/repos/";
|
|
8613
9209
|
remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
|
|
8614
|
-
gitCredentialFile = () =>
|
|
9210
|
+
gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
|
|
8615
9211
|
GitApiError = class extends Error {
|
|
8616
9212
|
constructor(status, message, body = null) {
|
|
8617
9213
|
super(message);
|
|
@@ -8703,19 +9299,19 @@ __export(gitrepo_exports, {
|
|
|
8703
9299
|
githubReferences: () => githubReferences,
|
|
8704
9300
|
repoFromUrl: () => repoFromUrl
|
|
8705
9301
|
});
|
|
8706
|
-
import
|
|
8707
|
-
import
|
|
9302
|
+
import fs27 from "node:fs";
|
|
9303
|
+
import path22 from "node:path";
|
|
8708
9304
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8709
|
-
function
|
|
9305
|
+
function git3(cwd, args, opts = {}) {
|
|
8710
9306
|
const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
|
|
8711
9307
|
const res = { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
|
|
8712
9308
|
if (!res.ok && !opts.allowFail) throw new Error(`git ${args.join(" ")} failed: ${(res.err || res.out).trim()}`);
|
|
8713
9309
|
return res;
|
|
8714
9310
|
}
|
|
8715
9311
|
function onPath(name) {
|
|
8716
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
9312
|
+
for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
|
|
8717
9313
|
try {
|
|
8718
|
-
|
|
9314
|
+
fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
|
|
8719
9315
|
return true;
|
|
8720
9316
|
} catch {
|
|
8721
9317
|
}
|
|
@@ -8767,7 +9363,7 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
|
|
|
8767
9363
|
return client;
|
|
8768
9364
|
}
|
|
8769
9365
|
function toplevel(dir) {
|
|
8770
|
-
const r =
|
|
9366
|
+
const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
|
|
8771
9367
|
if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
|
|
8772
9368
|
return r.out.trim();
|
|
8773
9369
|
}
|
|
@@ -8775,7 +9371,7 @@ function githubReferences(root, repo) {
|
|
|
8775
9371
|
const [owner, name] = repo.split("/");
|
|
8776
9372
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8777
9373
|
const pattern = `github\\.com[:/]${esc(owner)}/${esc(name)}|--repo[ =]${esc(owner)}/${esc(name)}|gh repo clone ${esc(owner)}/${esc(name)}`;
|
|
8778
|
-
const r =
|
|
9374
|
+
const r = git3(root, ["grep", "-n", "-I", "-E", pattern, "HEAD", "--"], { allowFail: true });
|
|
8779
9375
|
return r.out.split("\n").filter(Boolean).map((l) => l.replace(/^HEAD:/, ""));
|
|
8780
9376
|
}
|
|
8781
9377
|
function switchOrigin(root, url, out) {
|
|
@@ -8784,41 +9380,41 @@ function switchOrigin(root, url, out) {
|
|
|
8784
9380
|
let keptAs = null;
|
|
8785
9381
|
if (old && old !== url) {
|
|
8786
9382
|
keptAs = rs.has("github") ? `github-${Date.now()}` : "github";
|
|
8787
|
-
|
|
9383
|
+
git3(root, ["remote", "rename", "origin", keptAs]);
|
|
8788
9384
|
}
|
|
8789
|
-
if (rs.has("bunshin") && rs.get("bunshin") === url)
|
|
8790
|
-
else if (old !== url)
|
|
8791
|
-
|
|
8792
|
-
|
|
9385
|
+
if (rs.has("bunshin") && rs.get("bunshin") === url) git3(root, ["remote", "rename", "bunshin", "origin"]);
|
|
9386
|
+
else if (old !== url) git3(root, ["remote", "add", "origin", url]);
|
|
9387
|
+
git3(root, ["fetch", "--quiet", "--tags", "origin"]);
|
|
9388
|
+
git3(root, ["remote", "set-head", "origin", "--auto"], { allowFail: true });
|
|
8793
9389
|
const originRefs = refMap(root, "refs/remotes/origin");
|
|
8794
|
-
for (const b of
|
|
8795
|
-
if (originRefs.has(`refs/remotes/origin/${b}`))
|
|
9390
|
+
for (const b of git3(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
|
|
9391
|
+
if (originRefs.has(`refs/remotes/origin/${b}`)) git3(root, ["branch", `--set-upstream-to=origin/${b}`, b], { allowFail: true });
|
|
8796
9392
|
}
|
|
8797
9393
|
out(`origin \u2192 ${url}${keptAs ? ` (the old origin is now the remote "${keptAs}")` : ""}
|
|
8798
9394
|
`);
|
|
8799
9395
|
}
|
|
8800
9396
|
async function cmdImport(args, cwd, out, err) {
|
|
8801
9397
|
requireHelper();
|
|
8802
|
-
const root = toplevel(
|
|
8803
|
-
if (
|
|
9398
|
+
const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
|
|
9399
|
+
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");
|
|
8804
9400
|
const rs = remotes(root);
|
|
8805
9401
|
const originUrl = rs.get("origin");
|
|
8806
|
-
const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${
|
|
9402
|
+
const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path22.basename(path22.dirname(root))}/${path22.basename(root)}`;
|
|
8807
9403
|
const client = await connect(args, cwd);
|
|
8808
9404
|
const url = remoteUrl(client.apiUrl, name);
|
|
8809
9405
|
const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
|
|
8810
9406
|
if (fromOrigin) {
|
|
8811
9407
|
out(`Fetching ${originUrl} \u2026
|
|
8812
9408
|
`);
|
|
8813
|
-
const f =
|
|
9409
|
+
const f = git3(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
|
|
8814
9410
|
if (!f.ok) err(`! could not fetch origin (${f.err.trim().split("\n")[0]}); importing what this clone already has from it
|
|
8815
9411
|
`);
|
|
8816
9412
|
}
|
|
8817
9413
|
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 }));
|
|
8818
9414
|
const tags = [...refMap(root, "refs/tags").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
|
|
8819
9415
|
if (!branches.length) throw new Error("nothing to import: no branches found");
|
|
8820
|
-
const originHead =
|
|
8821
|
-
const current =
|
|
9416
|
+
const originHead = git3(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
|
|
9417
|
+
const current = git3(root, ["symbolic-ref", "--quiet", "--short", "HEAD"], { allowFail: true }).out.trim();
|
|
8822
9418
|
const defaultBranch = str4(args.flags["default-branch"]) ?? (fromOrigin && originHead ? originHead : current || "main");
|
|
8823
9419
|
if (!branches.some((b) => b.dst === `refs/heads/${defaultBranch}`)) throw new Error(`default branch ${defaultBranch} is not among the imported branches`);
|
|
8824
9420
|
if (fromOrigin) {
|
|
@@ -8828,7 +9424,7 @@ async function cmdImport(args, cwd, out, err) {
|
|
|
8828
9424
|
if (!remote) err(`! local branch ${b} is not on the origin and was not imported (push it to Bunshin afterwards if you need it)
|
|
8829
9425
|
`);
|
|
8830
9426
|
else if (remote !== sha2) {
|
|
8831
|
-
const ahead =
|
|
9427
|
+
const ahead = git3(root, ["rev-list", "--count", `${remote}..${sha2}`], { allowFail: true }).out.trim();
|
|
8832
9428
|
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
|
|
8833
9429
|
`);
|
|
8834
9430
|
}
|
|
@@ -8851,13 +9447,13 @@ async function cmdImport(args, cwd, out, err) {
|
|
|
8851
9447
|
out(`${name} exists and is empty; importing into it
|
|
8852
9448
|
`);
|
|
8853
9449
|
}
|
|
8854
|
-
if (rs.has("bunshin"))
|
|
8855
|
-
else
|
|
9450
|
+
if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
|
|
9451
|
+
else git3(root, ["remote", "add", "bunshin", url]);
|
|
8856
9452
|
const specs = [...branches, ...tags].map((r) => `${r.src}:${r.dst}`);
|
|
8857
|
-
const commits =
|
|
9453
|
+
const commits = git3(root, ["rev-list", "--count", "--all"]).out.trim();
|
|
8858
9454
|
out(`Pushing ${branches.length} branch${branches.length === 1 ? "" : "es"}, ${tags.length} tag${tags.length === 1 ? "" : "s"} (${commits} commits) \u2026
|
|
8859
9455
|
`);
|
|
8860
|
-
const push2 =
|
|
9456
|
+
const push2 = git3(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
|
|
8861
9457
|
if (!push2.ok) throw new Error(`push to Bunshin failed: ${push2.err.trim()}`);
|
|
8862
9458
|
const info = await client.info(name);
|
|
8863
9459
|
const want = [...branches, ...tags].map((r) => ({ ref: r.dst, sha: r.sha }));
|
|
@@ -8879,7 +9475,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
|
|
|
8879
9475
|
if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
|
|
8880
9476
|
`);
|
|
8881
9477
|
}
|
|
8882
|
-
if (
|
|
9478
|
+
if (fs27.existsSync(path22.join(root, ".github"))) out(`
|
|
8883
9479
|
Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
|
|
8884
9480
|
`);
|
|
8885
9481
|
out(`
|
|
@@ -8890,7 +9486,7 @@ New machines clone with: bunshin repo clone ${name}
|
|
|
8890
9486
|
}
|
|
8891
9487
|
async function cmdAttach(args, cwd, out, err) {
|
|
8892
9488
|
requireHelper();
|
|
8893
|
-
const root = toplevel(
|
|
9489
|
+
const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
|
|
8894
9490
|
const rs = remotes(root);
|
|
8895
9491
|
const originUrl = rs.get("origin");
|
|
8896
9492
|
const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
|
|
@@ -8904,9 +9500,9 @@ async function cmdAttach(args, cwd, out, err) {
|
|
|
8904
9500
|
}
|
|
8905
9501
|
const info = await client.info(name);
|
|
8906
9502
|
if (!info.refs.length) throw new Error(`${name} in Bunshin is empty; import it first (bunshin repo import)`);
|
|
8907
|
-
if (rs.has("bunshin"))
|
|
8908
|
-
else
|
|
8909
|
-
|
|
9503
|
+
if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
|
|
9504
|
+
else git3(root, ["remote", "add", "bunshin", url]);
|
|
9505
|
+
git3(root, ["fetch", "--quiet", "--tags", "bunshin"]);
|
|
8910
9506
|
if (originUrl) {
|
|
8911
9507
|
const behind = [];
|
|
8912
9508
|
for (const [ref, sha2] of refMap(root, "refs/remotes/origin")) {
|
|
@@ -8914,7 +9510,7 @@ async function cmdAttach(args, cwd, out, err) {
|
|
|
8914
9510
|
const b = ref.replace("refs/remotes/origin/", "");
|
|
8915
9511
|
const theirs = info.refs.find((r) => r.ref === `refs/heads/${b}`)?.sha;
|
|
8916
9512
|
if (!theirs) behind.push(`${b} (missing in Bunshin)`);
|
|
8917
|
-
else if (theirs !== sha2 && !
|
|
9513
|
+
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)`);
|
|
8918
9514
|
}
|
|
8919
9515
|
if (behind.length && !bool3(args.flags.force)) {
|
|
8920
9516
|
err(`The old origin has work that is not in Bunshin:
|
|
@@ -8925,7 +9521,7 @@ Push it to Bunshin first (git push bunshin <branch>), or re-run with --force.
|
|
|
8925
9521
|
}
|
|
8926
9522
|
}
|
|
8927
9523
|
switchOrigin(root, url, out);
|
|
8928
|
-
const status =
|
|
9524
|
+
const status = git3(root, ["status", "-sb"]).out.split("\n")[0];
|
|
8929
9525
|
out(`${status}
|
|
8930
9526
|
`);
|
|
8931
9527
|
return 0;
|
|
@@ -8938,12 +9534,12 @@ async function cmdClone(args, cwd, out) {
|
|
|
8938
9534
|
const dir = args._[2] ?? name.split("/")[1];
|
|
8939
9535
|
const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
|
|
8940
9536
|
if (r.status !== 0) return r.status ?? 1;
|
|
8941
|
-
out(`Cloned ${name} into ${
|
|
9537
|
+
out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
|
|
8942
9538
|
`);
|
|
8943
9539
|
return 0;
|
|
8944
9540
|
}
|
|
8945
9541
|
async function cmdVerify(args, cwd, out, err) {
|
|
8946
|
-
const root = toplevel(
|
|
9542
|
+
const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
|
|
8947
9543
|
const rs = remotes(root);
|
|
8948
9544
|
const originUrl = rs.get("origin") ?? "";
|
|
8949
9545
|
if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
|
|
@@ -8964,7 +9560,7 @@ async function cmdVerify(args, cwd, out, err) {
|
|
|
8964
9560
|
`);
|
|
8965
9561
|
}
|
|
8966
9562
|
}
|
|
8967
|
-
const fsck =
|
|
9563
|
+
const fsck = git3(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
|
|
8968
9564
|
if (!fsck.ok) {
|
|
8969
9565
|
problems++;
|
|
8970
9566
|
err(`! git fsck: ${fsck.err.trim().split("\n")[0]}
|
|
@@ -9054,12 +9650,12 @@ var init_gitrepo = __esm({
|
|
|
9054
9650
|
bool3 = (v) => v === true || v === "true";
|
|
9055
9651
|
remotes = (root) => {
|
|
9056
9652
|
const m = /* @__PURE__ */ new Map();
|
|
9057
|
-
for (const name of
|
|
9653
|
+
for (const name of git3(root, ["remote"]).out.split("\n").filter(Boolean)) m.set(name, git3(root, ["remote", "get-url", name]).out.trim());
|
|
9058
9654
|
return m;
|
|
9059
9655
|
};
|
|
9060
9656
|
refMap = (root, prefix) => {
|
|
9061
9657
|
const m = /* @__PURE__ */ new Map();
|
|
9062
|
-
for (const line of
|
|
9658
|
+
for (const line of git3(root, ["for-each-ref", "--format=%(objectname) %(refname)", prefix]).out.split("\n").filter(Boolean)) {
|
|
9063
9659
|
const [sha2, ref] = line.split(" ");
|
|
9064
9660
|
m.set(ref, sha2);
|
|
9065
9661
|
}
|
|
@@ -9106,22 +9702,22 @@ __export(runner_exports, {
|
|
|
9106
9702
|
setupText: () => setupText
|
|
9107
9703
|
});
|
|
9108
9704
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9109
|
-
import
|
|
9705
|
+
import fs28 from "node:fs";
|
|
9110
9706
|
import os5 from "node:os";
|
|
9111
|
-
import
|
|
9707
|
+
import path23 from "node:path";
|
|
9112
9708
|
function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
|
|
9113
|
-
return
|
|
9709
|
+
return path23.join(home, ".bunshin", "runner");
|
|
9114
9710
|
}
|
|
9115
9711
|
function readInstalled(home) {
|
|
9116
9712
|
try {
|
|
9117
|
-
return JSON.parse(
|
|
9713
|
+
return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
|
|
9118
9714
|
} catch {
|
|
9119
9715
|
return null;
|
|
9120
9716
|
}
|
|
9121
9717
|
}
|
|
9122
9718
|
function connection(args, cwd) {
|
|
9123
9719
|
const saved = readInstalled();
|
|
9124
|
-
const store = saved &&
|
|
9720
|
+
const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
|
|
9125
9721
|
const config = store?.config();
|
|
9126
9722
|
const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
|
|
9127
9723
|
const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
|
|
@@ -9181,9 +9777,9 @@ async function runOnce(c, args, out, err) {
|
|
|
9181
9777
|
const [job] = await lease(c, args);
|
|
9182
9778
|
if (!job) break;
|
|
9183
9779
|
const started = Date.now();
|
|
9184
|
-
const scratch =
|
|
9780
|
+
const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
|
|
9185
9781
|
const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
|
|
9186
|
-
() =>
|
|
9782
|
+
() => fs28.rmSync(scratch, { recursive: true, force: true })
|
|
9187
9783
|
);
|
|
9188
9784
|
const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
|
|
9189
9785
|
if (!res.ok) {
|
|
@@ -9210,7 +9806,7 @@ function effortOf(job) {
|
|
|
9210
9806
|
const tokens = Math.round(chars / 4);
|
|
9211
9807
|
const minutes = Math.max(2, Math.round(2 + tokens / 15e3));
|
|
9212
9808
|
const proposals = (job.prompt.match(/^### g_\w+/gm) ?? []).length;
|
|
9213
|
-
const what = job.kind === "reflect" ? "read every [USER] message and each failed or denied result first; most sessions teach nothing new, and at most 4 lessons are kept" : job.kind === "merge" ? `${proposals} proposal${proposals === 1 ? "" : "s"} against one document: decide each, edit only what the applied ones need` : `${proposals} proposed new document${proposals === 1 ? "" : "s"}: combine, fold into existing ones, or reject`;
|
|
9809
|
+
const what = job.kind === "reflect" ? "read every [USER] message and each failed or denied result first; most sessions teach nothing new, and at most 4 lessons are kept" : job.kind === "merge" ? `${proposals} proposal${proposals === 1 ? "" : "s"} against one document: decide each, edit only what the applied ones need` : job.kind === "generalize" ? `${(job.prompt.match(/^<document /gm) ?? []).length} documents from sibling scopes: write once at the parent what at least two of them share, and trim it out of them; "nothing_shared" is a normal answer` : `${proposals} proposed new document${proposals === 1 ? "" : "s"}: combine, fold into existing ones, or reject`;
|
|
9214
9810
|
return `${chars.toLocaleString("en")} chars (~${tokens.toLocaleString("en")} tokens); about ${minutes} min. ${what}.`;
|
|
9215
9811
|
}
|
|
9216
9812
|
function jobIndex(text2) {
|
|
@@ -9273,24 +9869,24 @@ function renderCheck(id, c) {
|
|
|
9273
9869
|
}
|
|
9274
9870
|
function logAnswer(entry) {
|
|
9275
9871
|
try {
|
|
9276
|
-
|
|
9277
|
-
|
|
9872
|
+
fs28.mkdirSync(runnerHome(), { recursive: true });
|
|
9873
|
+
fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
9278
9874
|
`);
|
|
9279
9875
|
} catch {
|
|
9280
9876
|
}
|
|
9281
9877
|
}
|
|
9282
9878
|
function which(name) {
|
|
9283
|
-
for (const d of [...(process.env.PATH || "").split(
|
|
9284
|
-
if (d &&
|
|
9879
|
+
for (const d of [...(process.env.PATH || "").split(path23.delimiter), path23.join(os5.homedir(), ".local", "bin")]) {
|
|
9880
|
+
if (d && fs28.existsSync(path23.join(d, name))) return path23.join(d, name);
|
|
9285
9881
|
}
|
|
9286
9882
|
return null;
|
|
9287
9883
|
}
|
|
9288
9884
|
function scheduledCommand(backend) {
|
|
9289
|
-
const node =
|
|
9290
|
-
const npx =
|
|
9885
|
+
const node = path23.dirname(process.execPath);
|
|
9886
|
+
const npx = fs28.existsSync(path23.join(node, "npx")) ? path23.join(node, "npx") : which("npx") ?? "npx";
|
|
9291
9887
|
const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
|
|
9292
|
-
const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [
|
|
9293
|
-
return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(
|
|
9888
|
+
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"])];
|
|
9889
|
+
return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path23.delimiter) };
|
|
9294
9890
|
}
|
|
9295
9891
|
function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
9296
9892
|
const cmd = scheduledCommand(backend);
|
|
@@ -9305,8 +9901,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
|
|
|
9305
9901
|
<dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
|
|
9306
9902
|
<key>WorkingDirectory</key><string>${xml(home)}</string>
|
|
9307
9903
|
<key>StartInterval</key><integer>${everyMinutes * 60}</integer>
|
|
9308
|
-
<key>StandardOutPath</key><string>${xml(
|
|
9309
|
-
<key>StandardErrorPath</key><string>${xml(
|
|
9904
|
+
<key>StandardOutPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
|
|
9905
|
+
<key>StandardErrorPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
|
|
9310
9906
|
<key>ProcessType</key><string>Background</string>
|
|
9311
9907
|
<key>LowPriorityIO</key><true/>
|
|
9312
9908
|
</dict>
|
|
@@ -9317,19 +9913,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
|
|
|
9317
9913
|
const cmd = scheduledCommand(backend);
|
|
9318
9914
|
const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
|
|
9319
9915
|
const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
|
|
9320
|
-
return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(
|
|
9916
|
+
return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path23.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
|
|
9321
9917
|
}
|
|
9322
9918
|
function launchAgentsDir(opts) {
|
|
9323
|
-
return opts.launchAgents ??
|
|
9919
|
+
return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
|
|
9324
9920
|
}
|
|
9325
9921
|
function installSchedule(backend, everyMinutes, opts = {}) {
|
|
9326
9922
|
const platform = opts.platform ?? process.platform;
|
|
9327
9923
|
const run2 = opts.run ?? realRun;
|
|
9328
9924
|
if (platform === "darwin") {
|
|
9329
9925
|
const dir = launchAgentsDir(opts);
|
|
9330
|
-
const plist =
|
|
9331
|
-
|
|
9332
|
-
|
|
9926
|
+
const plist = path23.join(dir, `${LAUNCHD_LABEL}.plist`);
|
|
9927
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
9928
|
+
fs28.writeFileSync(plist, launchdPlist(backend, everyMinutes));
|
|
9333
9929
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
9334
9930
|
run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
|
|
9335
9931
|
const r = run2("launchctl", ["bootstrap", domain, plist]);
|
|
@@ -9349,10 +9945,10 @@ function removeSchedule(opts = {}) {
|
|
|
9349
9945
|
const platform = opts.platform ?? process.platform;
|
|
9350
9946
|
const run2 = opts.run ?? realRun;
|
|
9351
9947
|
if (platform === "darwin") {
|
|
9352
|
-
const plist =
|
|
9948
|
+
const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
|
|
9353
9949
|
run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
|
|
9354
|
-
if (!
|
|
9355
|
-
|
|
9950
|
+
if (!fs28.existsSync(plist)) return "no schedule";
|
|
9951
|
+
fs28.rmSync(plist, { force: true });
|
|
9356
9952
|
return `removed ${plist}`;
|
|
9357
9953
|
}
|
|
9358
9954
|
const current = run2("crontab", ["-l"]);
|
|
@@ -9379,10 +9975,10 @@ async function install(args, cwd, out, err, opts = {}) {
|
|
|
9379
9975
|
}
|
|
9380
9976
|
const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
|
|
9381
9977
|
const home = runnerHome();
|
|
9382
|
-
|
|
9978
|
+
fs28.mkdirSync(home, { recursive: true, mode: 448 });
|
|
9383
9979
|
const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
|
|
9384
|
-
|
|
9385
|
-
|
|
9980
|
+
fs28.writeFileSync(path23.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
|
|
9981
|
+
fs28.chmodSync(path23.join(home, "config.json"), 384);
|
|
9386
9982
|
let schedule;
|
|
9387
9983
|
try {
|
|
9388
9984
|
schedule = installSchedule(backend, every, opts);
|
|
@@ -9395,7 +9991,7 @@ Run it yourself: npx -y ${PKG2} runner --once
|
|
|
9395
9991
|
out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
|
|
9396
9992
|
`);
|
|
9397
9993
|
out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
|
|
9398
|
-
` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${
|
|
9994
|
+
` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path23.join(home, "runner.log")}
|
|
9399
9995
|
`);
|
|
9400
9996
|
out(` Stop: npx -y ${PKG2} runner uninstall
|
|
9401
9997
|
`);
|
|
@@ -9412,9 +10008,9 @@ Answering what waits now\u2026
|
|
|
9412
10008
|
function uninstall(out, opts = {}) {
|
|
9413
10009
|
out(`${removeSchedule(opts)}
|
|
9414
10010
|
`);
|
|
9415
|
-
const file =
|
|
9416
|
-
if (
|
|
9417
|
-
|
|
10011
|
+
const file = path23.join(runnerHome(), "config.json");
|
|
10012
|
+
if (fs28.existsSync(file)) {
|
|
10013
|
+
fs28.rmSync(file, { force: true });
|
|
9418
10014
|
out(`removed ${file} (revoke the key in the console: Agent keys)
|
|
9419
10015
|
`);
|
|
9420
10016
|
}
|
|
@@ -9594,7 +10190,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9594
10190
|
out(text2);
|
|
9595
10191
|
return 0;
|
|
9596
10192
|
}
|
|
9597
|
-
|
|
10193
|
+
fs28.writeFileSync(file, text2, { mode: 384 });
|
|
9598
10194
|
const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
|
|
9599
10195
|
out(
|
|
9600
10196
|
[
|
|
@@ -9612,7 +10208,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9612
10208
|
const id = args._[2];
|
|
9613
10209
|
const file = args._[3] ?? "-";
|
|
9614
10210
|
if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
|
|
9615
|
-
const text2 = file === "-" ?
|
|
10211
|
+
const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
|
|
9616
10212
|
const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
|
|
9617
10213
|
out(renderCheck(id, r.body));
|
|
9618
10214
|
return r.body.ok ? 0 : 1;
|
|
@@ -9621,7 +10217,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9621
10217
|
const id = args._[2];
|
|
9622
10218
|
const file = args._[3] ?? "-";
|
|
9623
10219
|
if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
|
|
9624
|
-
const text2 = file === "-" ?
|
|
10220
|
+
const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
|
|
9625
10221
|
const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
|
|
9626
10222
|
if (check?.body && (check.status < 400 || check.body.problems)) {
|
|
9627
10223
|
out(renderCheck(id, check.body));
|
|
@@ -9645,7 +10241,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
|
|
|
9645
10241
|
const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
|
|
9646
10242
|
const entries = (() => {
|
|
9647
10243
|
try {
|
|
9648
|
-
return
|
|
10244
|
+
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);
|
|
9649
10245
|
} catch {
|
|
9650
10246
|
return [];
|
|
9651
10247
|
}
|
|
@@ -9698,7 +10294,7 @@ var init_runner = __esm({
|
|
|
9698
10294
|
init_trust();
|
|
9699
10295
|
init_version();
|
|
9700
10296
|
DEFAULT_API = "https://api.bunshin.cc";
|
|
9701
|
-
KINDS2 = ["reflect", "merge", "create"];
|
|
10297
|
+
KINDS2 = ["reflect", "merge", "create", "generalize"];
|
|
9702
10298
|
str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
9703
10299
|
LAUNCHD_LABEL = "cc.bunshin.runner";
|
|
9704
10300
|
CRON_MARK = "# bunshin-runner";
|
|
@@ -9714,9 +10310,9 @@ var init_runner = __esm({
|
|
|
9714
10310
|
});
|
|
9715
10311
|
|
|
9716
10312
|
// src/v2/importer.ts
|
|
9717
|
-
import { execFileSync as
|
|
9718
|
-
import
|
|
9719
|
-
import
|
|
10313
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
10314
|
+
import fs29 from "node:fs";
|
|
10315
|
+
import path24 from "node:path";
|
|
9720
10316
|
function rewriteRepoPaths(text2, repoName2, pack) {
|
|
9721
10317
|
const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
|
|
9722
10318
|
return text2.replace(re, `~/.bunshin/library/${pack}`);
|
|
@@ -9755,7 +10351,7 @@ function buildDocs(files, opts) {
|
|
|
9755
10351
|
const main2 = `${dir}/SKILL.md`;
|
|
9756
10352
|
const text2 = rw(files.get(main2).content);
|
|
9757
10353
|
const fm = frontmatterDescription(text2);
|
|
9758
|
-
const name =
|
|
10354
|
+
const name = path24.posix.basename(dir);
|
|
9759
10355
|
const bundle = {};
|
|
9760
10356
|
for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
|
|
9761
10357
|
claimed.add(p);
|
|
@@ -9837,45 +10433,45 @@ ${Object.keys(loose).map((p) => `- \`${p}\``).join("\n")}
|
|
|
9837
10433
|
}
|
|
9838
10434
|
return docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
9839
10435
|
}
|
|
9840
|
-
function
|
|
9841
|
-
return
|
|
10436
|
+
function git4(root, args) {
|
|
10437
|
+
return execFileSync6("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
9842
10438
|
}
|
|
9843
10439
|
function gitBuf(root, args) {
|
|
9844
|
-
return
|
|
10440
|
+
return execFileSync6("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
|
|
9845
10441
|
}
|
|
9846
10442
|
function isGitRepo(root) {
|
|
9847
10443
|
try {
|
|
9848
|
-
return
|
|
10444
|
+
return git4(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
9849
10445
|
} catch {
|
|
9850
10446
|
return false;
|
|
9851
10447
|
}
|
|
9852
10448
|
}
|
|
9853
10449
|
function workingTreeFiles(root) {
|
|
9854
10450
|
const out = /* @__PURE__ */ new Map();
|
|
9855
|
-
const list2 = isGitRepo(root) ?
|
|
9856
|
-
return
|
|
10451
|
+
const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
|
|
10452
|
+
return fs29.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
9857
10453
|
if (e.name === ".git" || e.name === "node_modules") return [];
|
|
9858
10454
|
const rel = base ? `${base}/${e.name}` : e.name;
|
|
9859
|
-
return e.isDirectory() ? walk(
|
|
10455
|
+
return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
|
|
9860
10456
|
});
|
|
9861
10457
|
})(root);
|
|
9862
10458
|
for (const rel of list2) {
|
|
9863
|
-
const abs =
|
|
10459
|
+
const abs = path24.join(root, rel);
|
|
9864
10460
|
let buf;
|
|
9865
10461
|
try {
|
|
9866
|
-
buf =
|
|
10462
|
+
buf = fs29.readFileSync(abs);
|
|
9867
10463
|
} catch {
|
|
9868
10464
|
continue;
|
|
9869
10465
|
}
|
|
9870
10466
|
if (!isText(buf)) continue;
|
|
9871
|
-
const exec = (
|
|
10467
|
+
const exec = (fs29.statSync(abs).mode & 73) !== 0;
|
|
9872
10468
|
out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
|
|
9873
10469
|
}
|
|
9874
10470
|
return out;
|
|
9875
10471
|
}
|
|
9876
10472
|
function commitsOf(root, since) {
|
|
9877
10473
|
const range = since ? [`${since}..HEAD`] : ["HEAD"];
|
|
9878
|
-
return
|
|
10474
|
+
return git4(root, ["log", "--reverse", "--topo-order", "--format=%H%x1f%an%x1f%aI%x1f%s", ...range]).split("\n").filter(Boolean).map((l) => {
|
|
9879
10475
|
const [sha2, author, at, subject] = l.split("");
|
|
9880
10476
|
return { sha: sha2, author, at, subject: subject ?? "" };
|
|
9881
10477
|
});
|
|
@@ -9888,7 +10484,7 @@ function readAt(root, sha2, rel, mode) {
|
|
|
9888
10484
|
}
|
|
9889
10485
|
function treeAt(root, sha2) {
|
|
9890
10486
|
const out = /* @__PURE__ */ new Map();
|
|
9891
|
-
for (const line of
|
|
10487
|
+
for (const line of git4(root, ["ls-tree", "-r", "-z", sha2]).split("\0").filter(Boolean)) {
|
|
9892
10488
|
const m = /^(\d+) blob [0-9a-f]+\t(.+)$/.exec(line);
|
|
9893
10489
|
if (!m) continue;
|
|
9894
10490
|
const f = readAt(root, sha2, m[2], m[1]);
|
|
@@ -9902,7 +10498,7 @@ function diffDocs(prev, next) {
|
|
|
9902
10498
|
return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
|
|
9903
10499
|
}
|
|
9904
10500
|
function readSkillSystem(root, opts) {
|
|
9905
|
-
const repoName2 =
|
|
10501
|
+
const repoName2 = path24.basename(path24.resolve(root));
|
|
9906
10502
|
const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
|
|
9907
10503
|
const tree = build(workingTreeFiles(root));
|
|
9908
10504
|
if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
|
|
@@ -9914,7 +10510,7 @@ function readSkillSystem(root, opts) {
|
|
|
9914
10510
|
commits.forEach((c, i) => {
|
|
9915
10511
|
if (!prevSha) files = treeAt(root, c.sha);
|
|
9916
10512
|
else {
|
|
9917
|
-
const changes =
|
|
10513
|
+
const changes = git4(root, ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id", prevSha, c.sha]).split("\0").filter(Boolean);
|
|
9918
10514
|
for (let k = 0; k + 1 < changes.length; k += 2) {
|
|
9919
10515
|
const m = /^:\d+ (\d+) [0-9a-f]+ [0-9a-f]+ ([A-Z])/.exec(changes[k]);
|
|
9920
10516
|
const rel = changes[k + 1];
|
|
@@ -9979,7 +10575,7 @@ var init_importer = __esm({
|
|
|
9979
10575
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9980
10576
|
looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
|
|
9981
10577
|
isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
|
|
9982
|
-
isUpdateHook = (p, text2) => /auto-?update/i.test(
|
|
10578
|
+
isUpdateHook = (p, text2) => /auto-?update/i.test(path24.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
|
|
9983
10579
|
fingerprint = (d) => JSON.stringify(d);
|
|
9984
10580
|
REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
|
|
9985
10581
|
|
|
@@ -10015,8 +10611,8 @@ __export(skills_exports, {
|
|
|
10015
10611
|
cmdSkill: () => cmdSkill,
|
|
10016
10612
|
parsePin: () => parsePin
|
|
10017
10613
|
});
|
|
10018
|
-
import
|
|
10019
|
-
import
|
|
10614
|
+
import fs30 from "node:fs";
|
|
10615
|
+
import path25 from "node:path";
|
|
10020
10616
|
function requireOnline(store, what) {
|
|
10021
10617
|
if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
|
|
10022
10618
|
}
|
|
@@ -10025,9 +10621,9 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10025
10621
|
err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
|
|
10026
10622
|
return 1;
|
|
10027
10623
|
}
|
|
10028
|
-
const root =
|
|
10029
|
-
if (!
|
|
10030
|
-
const pack = str6(args.flags.pack) ??
|
|
10624
|
+
const root = path25.resolve(cwd, args._[2]);
|
|
10625
|
+
if (!fs30.existsSync(root)) throw new Error(`${root} does not exist`);
|
|
10626
|
+
const pack = str6(args.flags.pack) ?? path25.basename(root);
|
|
10031
10627
|
const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
|
|
10032
10628
|
const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
|
|
10033
10629
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10062,7 +10658,7 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
10062
10658
|
const propose = args.flags["propose-rewrites"];
|
|
10063
10659
|
if (propose) {
|
|
10064
10660
|
const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
|
|
10065
|
-
await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack,
|
|
10661
|
+
await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path25.basename(root), out, err);
|
|
10066
10662
|
}
|
|
10067
10663
|
out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
|
|
10068
10664
|
return 0;
|
|
@@ -10202,7 +10798,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
|
|
|
10202
10798
|
}
|
|
10203
10799
|
const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
|
|
10204
10800
|
if (graph) {
|
|
10205
|
-
|
|
10801
|
+
fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
|
|
10206
10802
|
out(`graph written to ${graph}
|
|
10207
10803
|
`);
|
|
10208
10804
|
}
|
|
@@ -10269,12 +10865,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10269
10865
|
}
|
|
10270
10866
|
case "status": {
|
|
10271
10867
|
const d = deployedState(config);
|
|
10272
|
-
const p = deployPaths();
|
|
10868
|
+
const p = deployPaths(process.env, config.orgId);
|
|
10273
10869
|
const pending = d.docs.filter((x) => x.pending);
|
|
10274
10870
|
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
|
|
10275
10871
|
`);
|
|
10276
|
-
out(` skills: ${p.claudeSkills}, ${p.codexSkills}
|
|
10277
|
-
|
|
10872
|
+
out(` skills: ${p.claudeSkills}, ${p.codexSkills} (this workspace's own copy)
|
|
10873
|
+
linked into: ${d.linked.length ? d.linked.join(", ") : "no project yet (each project gets links when a session runs there)"}
|
|
10278
10874
|
files: ${p.library}
|
|
10279
10875
|
`);
|
|
10280
10876
|
for (const x of pending) out(` edited here, awaiting the nightly: ${x.name}
|
|
@@ -10282,16 +10878,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10282
10878
|
return 0;
|
|
10283
10879
|
}
|
|
10284
10880
|
case "workspace": {
|
|
10285
|
-
|
|
10286
|
-
if (bool4(args.flags.clear)) setDeployWorkspace(null);
|
|
10287
|
-
else if (target) setDeployWorkspace(target);
|
|
10288
|
-
const ws = deployWorkspace();
|
|
10289
|
-
const p = deployPaths();
|
|
10290
|
-
out(ws ? `workspace: ${ws}
|
|
10291
|
-
` : "workspace: none (skills and instructions are user-wide)\n");
|
|
10292
|
-
out(` skills: ${p.claudeSkills}, ${p.codexSkills}
|
|
10293
|
-
instructions: ${p.claudeMd}, ${p.agentsMd}
|
|
10294
|
-
`);
|
|
10881
|
+
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");
|
|
10295
10882
|
return 0;
|
|
10296
10883
|
}
|
|
10297
10884
|
case "pin": {
|
|
@@ -10301,9 +10888,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10301
10888
|
`;
|
|
10302
10889
|
const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
|
|
10303
10890
|
if (file) {
|
|
10304
|
-
const abs =
|
|
10305
|
-
|
|
10306
|
-
|
|
10891
|
+
const abs = path25.resolve(cwd, file);
|
|
10892
|
+
fs30.mkdirSync(path25.dirname(abs), { recursive: true });
|
|
10893
|
+
fs30.writeFileSync(abs, line);
|
|
10307
10894
|
out(`${file}: ${line}`);
|
|
10308
10895
|
} else out(line);
|
|
10309
10896
|
return 0;
|
|
@@ -10313,12 +10900,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
10313
10900
|
const to = str6(args.flags.to);
|
|
10314
10901
|
if (!to) throw new Error("--to <dir> is required");
|
|
10315
10902
|
const pinFile = str6(args.flags.pin);
|
|
10316
|
-
const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(
|
|
10903
|
+
const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs30.readFileSync(path25.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
|
|
10317
10904
|
const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
|
|
10318
10905
|
if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
|
|
10319
10906
|
const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
|
|
10320
|
-
const dir =
|
|
10321
|
-
if (
|
|
10907
|
+
const dir = path25.resolve(cwd, to);
|
|
10908
|
+
if (fs30.existsSync(dir) && fs30.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
|
|
10322
10909
|
const pack = str6(args.flags.pack);
|
|
10323
10910
|
const docs = snap.sections.filter((s) => !pack || s.pack === pack);
|
|
10324
10911
|
const res = materialize(docs, dir);
|
|
@@ -10357,27 +10944,27 @@ __export(reflect_exports, {
|
|
|
10357
10944
|
buildLesson: () => buildLesson,
|
|
10358
10945
|
runReflection: () => runReflection
|
|
10359
10946
|
});
|
|
10360
|
-
import
|
|
10361
|
-
import
|
|
10362
|
-
import { createHash as
|
|
10947
|
+
import fs31 from "node:fs";
|
|
10948
|
+
import path26 from "node:path";
|
|
10949
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
10363
10950
|
async function runReflection(store, opts) {
|
|
10364
10951
|
const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
|
|
10365
|
-
const lock =
|
|
10952
|
+
const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
|
|
10366
10953
|
const deadline = Date.now() + 66e4;
|
|
10367
10954
|
for (; ; ) {
|
|
10368
10955
|
try {
|
|
10369
|
-
|
|
10956
|
+
fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
10370
10957
|
break;
|
|
10371
10958
|
} catch (error) {
|
|
10372
10959
|
if (error.code !== "EEXIST") throw error;
|
|
10373
10960
|
try {
|
|
10374
|
-
const owner = Number(
|
|
10961
|
+
const owner = Number(fs31.readFileSync(lock, "utf8"));
|
|
10375
10962
|
if (Number.isInteger(owner) && owner > 0) {
|
|
10376
10963
|
try {
|
|
10377
10964
|
process.kill(owner, 0);
|
|
10378
10965
|
} catch (error2) {
|
|
10379
10966
|
if (error2.code === "ESRCH") {
|
|
10380
|
-
|
|
10967
|
+
fs31.unlinkSync(lock);
|
|
10381
10968
|
continue;
|
|
10382
10969
|
}
|
|
10383
10970
|
}
|
|
@@ -10392,7 +10979,7 @@ async function runReflection(store, opts) {
|
|
|
10392
10979
|
return await reflectSession(store, opts);
|
|
10393
10980
|
} finally {
|
|
10394
10981
|
try {
|
|
10395
|
-
|
|
10982
|
+
fs31.unlinkSync(lock);
|
|
10396
10983
|
} catch {
|
|
10397
10984
|
}
|
|
10398
10985
|
}
|
|
@@ -10734,11 +11321,11 @@ __export(cli_exports, {
|
|
|
10734
11321
|
parseArgs: () => parseArgs,
|
|
10735
11322
|
version: () => version
|
|
10736
11323
|
});
|
|
10737
|
-
import
|
|
10738
|
-
import
|
|
11324
|
+
import fs32 from "node:fs";
|
|
11325
|
+
import path27 from "node:path";
|
|
10739
11326
|
function parseArgs(argv) {
|
|
10740
11327
|
const out = { _: [], flags: {} };
|
|
10741
|
-
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"]);
|
|
11328
|
+
const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace", "across"]);
|
|
10742
11329
|
for (let i = 0; i < argv.length; i++) {
|
|
10743
11330
|
const a = argv[i];
|
|
10744
11331
|
if (a === "--") {
|
|
@@ -10835,7 +11422,7 @@ async function main(argv, opts = {}) {
|
|
|
10835
11422
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
10836
11423
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
10837
11424
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
10838
|
-
const prior = new Store(
|
|
11425
|
+
const prior = new Store(path27.join(cwd, ".bunshin"));
|
|
10839
11426
|
const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
|
|
10840
11427
|
const apiUrl = str7(args.flags["api-url"]);
|
|
10841
11428
|
const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
@@ -10847,23 +11434,45 @@ async function main(argv, opts = {}) {
|
|
|
10847
11434
|
process.stdin.on("end", () => resolve(value.trim()));
|
|
10848
11435
|
process.stdin.on("error", reject);
|
|
10849
11436
|
}) : process.env.BUNSHIN_API_KEY || (!apiUrl || apiUrl === existing.apiUrl) && existing.apiKey || await readKey2();
|
|
11437
|
+
const hooksFlag = str7(args.flags.hooks);
|
|
11438
|
+
if (hooksFlag && hooksFlag !== "user" && hooksFlag !== "project") throw new Error("--hooks must be user (every session under this folder) or project (this folder only)");
|
|
11439
|
+
const only = str7(args.flags.only);
|
|
11440
|
+
const exclude = str7(args.flags.exclude);
|
|
11441
|
+
const listed = (v) => v.split(",").map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
|
|
10850
11442
|
out("Connecting to Bunshin\u2026\n");
|
|
10851
|
-
const result = await setupProject2({
|
|
11443
|
+
const result = await setupProject2({
|
|
11444
|
+
cwd,
|
|
11445
|
+
harness,
|
|
11446
|
+
apiKey: key,
|
|
11447
|
+
apiUrl,
|
|
11448
|
+
noHooks: bool5(args.flags["no-hooks"]),
|
|
11449
|
+
...hooksFlag ? { hooks: hooksFlag } : {},
|
|
11450
|
+
...bool5(args.flags.all) ? { sources: null } : only || exclude ? { sources: { ...only ? { only: listed(only) } : {}, ...exclude ? { exclude: listed(exclude) } : {} } } : {}
|
|
11451
|
+
});
|
|
11452
|
+
if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
|
|
11453
|
+
const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
11454
|
+
await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
|
|
11455
|
+
}
|
|
10852
11456
|
out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
|
|
10853
11457
|
`);
|
|
10854
|
-
out(`
|
|
11458
|
+
out(result.config.hooks === "user" ? `Folder: ${cwd} (every session in it or below it uses this workspace)
|
|
11459
|
+
Key saved privately outside the folder.
|
|
11460
|
+
` : `Project: ${cwd}
|
|
10855
11461
|
Key saved privately outside the project.
|
|
10856
11462
|
`);
|
|
10857
11463
|
if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
|
|
10858
11464
|
if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
|
|
10859
11465
|
`);
|
|
10860
11466
|
if (result.learnCommand?.installed) out(`/learn installed in ${result.learnCommand.file}
|
|
11467
|
+
`);
|
|
11468
|
+
if (result.borrowCommand?.installed) out(`/borrow installed in ${result.borrowCommand.file} (bring in a lesson learned in another project or group)
|
|
10861
11469
|
`);
|
|
10862
11470
|
else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
|
|
10863
11471
|
`);
|
|
10864
11472
|
if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
|
|
10865
11473
|
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");
|
|
10866
11474
|
else if (harness === "pi" && result.hooks) out("Open Pi in this project and approve its project extension. Check it with /bunshin status.\n");
|
|
11475
|
+
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");
|
|
10867
11476
|
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)" : ""}.
|
|
10868
11477
|
`);
|
|
10869
11478
|
else out("Hooks are disabled. Run setup again without --no-hooks to configure them.\n");
|
|
@@ -10879,7 +11488,7 @@ Key saved privately outside the project.
|
|
|
10879
11488
|
if (cmd === "mcp") {
|
|
10880
11489
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
10881
11490
|
const root = str7(args.flags.root);
|
|
10882
|
-
await runMcpServer2({ cwd, ...root ? { store: new Store(
|
|
11491
|
+
await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
|
|
10883
11492
|
return 0;
|
|
10884
11493
|
}
|
|
10885
11494
|
if (cmd === "demo") {
|
|
@@ -10906,6 +11515,14 @@ Key saved privately outside the project.
|
|
|
10906
11515
|
}
|
|
10907
11516
|
if (skipped.length && cmd === "status") err(`! ${untrustedNote(skipped[0])}
|
|
10908
11517
|
`);
|
|
11518
|
+
if (["learn", "reflect", "recall", "load", "suggest"].includes(cmd) && !str7(args.flags.root)) {
|
|
11519
|
+
const { cwdInPool: cwdInPool2, topFolder: topFolder2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
11520
|
+
if (!cwdInPool2(store.config(), cwd, store.root)) {
|
|
11521
|
+
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)}\`.
|
|
11522
|
+
`);
|
|
11523
|
+
return 1;
|
|
11524
|
+
}
|
|
11525
|
+
}
|
|
10909
11526
|
try {
|
|
10910
11527
|
switch (cmd) {
|
|
10911
11528
|
case "status":
|
|
@@ -10957,6 +11574,9 @@ Key saved privately outside the project.
|
|
|
10957
11574
|
return await (await Promise.resolve().then(() => (init_skills(), skills_exports))).cmdLibrary(store, args, cwd, out, err);
|
|
10958
11575
|
case "ui":
|
|
10959
11576
|
return await (await Promise.resolve().then(() => (init_uicmd(), uicmd_exports))).cmdUi(store, args, out, err, version());
|
|
11577
|
+
case "sources":
|
|
11578
|
+
case "source":
|
|
11579
|
+
return (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdSources(store, args, cwd, out, err);
|
|
10960
11580
|
default:
|
|
10961
11581
|
err(`bunshin: unknown command "${cmd}"
|
|
10962
11582
|
|
|
@@ -10972,7 +11592,7 @@ ${HELP2}`);
|
|
|
10972
11592
|
function openStore(args, cwd) {
|
|
10973
11593
|
const root = str7(args.flags.root);
|
|
10974
11594
|
if (root) {
|
|
10975
|
-
const store = new Store(
|
|
11595
|
+
const store = new Store(path27.resolve(root));
|
|
10976
11596
|
store.ensureLayout();
|
|
10977
11597
|
return store;
|
|
10978
11598
|
}
|
|
@@ -10988,7 +11608,7 @@ async function cmdInit(args, cwd, out, err) {
|
|
|
10988
11608
|
orgId: str7(args.flags["org-id"]),
|
|
10989
11609
|
userId: str7(args.flags["user-id"]),
|
|
10990
11610
|
apiUrl: str7(args.flags["api-url"]),
|
|
10991
|
-
apiKey: bool5(args.flags["key-stdin"]) ?
|
|
11611
|
+
apiKey: bool5(args.flags["key-stdin"]) ? fs32.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
|
|
10992
11612
|
scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
|
|
10993
11613
|
harness,
|
|
10994
11614
|
noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
|
|
@@ -11031,10 +11651,10 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11031
11651
|
const conflicts = store.listConflicts().filter((c) => c.status === "open");
|
|
11032
11652
|
const metrics = store.readMetrics(void 0);
|
|
11033
11653
|
const last50 = metrics?.windows?.find((w) => w.name === "last50");
|
|
11034
|
-
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([
|
|
11654
|
+
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")] : [];
|
|
11035
11655
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
11036
11656
|
try {
|
|
11037
|
-
return
|
|
11657
|
+
return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
11038
11658
|
} catch {
|
|
11039
11659
|
return false;
|
|
11040
11660
|
}
|
|
@@ -11052,7 +11672,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11052
11672
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
11053
11673
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
11054
11674
|
hooks: { [config.harness]: hooks },
|
|
11055
|
-
hookFiles: hookFiles.filter((file) =>
|
|
11675
|
+
hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
|
|
11056
11676
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
|
|
11057
11677
|
v2
|
|
11058
11678
|
};
|
|
@@ -11068,6 +11688,8 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11068
11688
|
out(`user ${config.userId} \u2014 ${config.role} @ ${config.orgUnit} (org ${config.orgId})
|
|
11069
11689
|
`);
|
|
11070
11690
|
out(`context ${v2.context.id} (${v2.context.kind}; layers ${v2.context.layers.join(", ")})
|
|
11691
|
+
`);
|
|
11692
|
+
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)"}
|
|
11071
11693
|
`);
|
|
11072
11694
|
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}` : ""}
|
|
11073
11695
|
`);
|
|
@@ -11093,7 +11715,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
11093
11715
|
}
|
|
11094
11716
|
function hookCount(file) {
|
|
11095
11717
|
try {
|
|
11096
|
-
const raw = JSON.parse(
|
|
11718
|
+
const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
|
|
11097
11719
|
let n = 0;
|
|
11098
11720
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
11099
11721
|
for (const g of groups ?? []) {
|
|
@@ -11213,7 +11835,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
11213
11835
|
let content = str7(args.flags.content) ?? "";
|
|
11214
11836
|
if (!content || content === "-" || args._.includes("-")) {
|
|
11215
11837
|
try {
|
|
11216
|
-
content =
|
|
11838
|
+
content = fs32.readFileSync(0, "utf8");
|
|
11217
11839
|
} catch {
|
|
11218
11840
|
}
|
|
11219
11841
|
}
|
|
@@ -11279,7 +11901,7 @@ async function cmdReflect(store, args, cwd, out, err) {
|
|
|
11279
11901
|
}
|
|
11280
11902
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
|
|
11281
11903
|
const result = await runReflection2(store, {
|
|
11282
|
-
sessionId: str7(args.flags.session) ??
|
|
11904
|
+
sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
11283
11905
|
transcriptPath: transcript,
|
|
11284
11906
|
cwd,
|
|
11285
11907
|
force: bool5(args.flags.force)
|
|
@@ -11404,6 +12026,10 @@ Usage: bunshin <command> [options]
|
|
|
11404
12026
|
setup --harness codex|claude-code|pi connect this project to your hosted workspace
|
|
11405
12027
|
--api-url <url> --no-hooks paste the key at the hidden prompt; profile is read from Bunshin
|
|
11406
12028
|
--key-stdin read the key from stdin (for automated setup)
|
|
12029
|
+
--hooks user a folder of projects: hooks in your user settings, so every
|
|
12030
|
+
session under this folder uses its workspace (default: project)
|
|
12031
|
+
--only a,b | --exclude c | --all
|
|
12032
|
+
which of the folder's projects take part (asked when omitted)
|
|
11407
12033
|
setup --robot --robot-id <id> --arm so101 --gripper <g> --mount <m> --cameras scene,wrist
|
|
11408
12034
|
--domain <d> --site <s> --cell <c> [--depth] [--api-url <url>] [--harness claude-code]
|
|
11409
12035
|
register this machine as a robot context (x-bunshin-context)
|
|
@@ -11413,10 +12039,14 @@ Usage: bunshin <command> [options]
|
|
|
11413
12039
|
--harness claude-code|codex|pi|other --no-hooks --remove-hooks
|
|
11414
12040
|
|
|
11415
12041
|
status [--json] store, context, library, outbox, hooks and sync health
|
|
11416
|
-
|
|
11417
|
-
|
|
12042
|
+
sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
|
|
12043
|
+
which folders under this store's folder take part (learning pool)
|
|
12044
|
+
recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--across] [--json]
|
|
12045
|
+
pull candidates from this project's scopes (offline: local view);
|
|
12046
|
+
--across borrows from anywhere in the workspace (/borrow)
|
|
11418
12047
|
load <id> [--anchor <heading>] [--json]
|
|
11419
|
-
print one document (or one part of it) and mark it loaded
|
|
12048
|
+
print one document (or one part of it) and mark it loaded; a
|
|
12049
|
+
lesson from outside this project's scopes is recorded as borrowed
|
|
11420
12050
|
learn [focus...] capture what this session learned now (fork reflection),
|
|
11421
12051
|
apply it locally as a trial and submit it
|
|
11422
12052
|
review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all|approve <id>|reject <id> [--note <text>]]
|
|
@@ -11436,9 +12066,8 @@ Usage: bunshin <command> [options]
|
|
|
11436
12066
|
the deployed skills; push proposes local edits / a new skill now
|
|
11437
12067
|
library version|log|status|pin [--write [file]]|checkout <lib@N> --to <dir> [--git <sha>] [--pin <file>]
|
|
11438
12068
|
library versions (lib@N), history, and exact reproductions
|
|
11439
|
-
library workspace
|
|
11440
|
-
|
|
11441
|
-
~/Documents/dev) instead of ~/.claude and ~/.codex; set it before the first sync
|
|
12069
|
+
library workspace (retired: each workspace keeps its own skills and links them into
|
|
12070
|
+
the projects its sessions run in; see library status)
|
|
11442
12071
|
ui [--session <id>] [--json] [--watch] what Bunshin did this session, as the person sees it
|
|
11443
12072
|
ui status | install | uninstall the Claude Code mod (band above the prompt, /bunshin, toasts)
|
|
11444
12073
|
ui enable|disable --harness <h> [--mode auto|mod|widget|lines|off] how a harness shows it
|
|
@@ -11449,9 +12078,9 @@ Usage: bunshin <command> [options]
|
|
|
11449
12078
|
repo import|attach|clone|verify|list|url|login
|
|
11450
12079
|
host a git repository in Bunshin (full history; git push/pull
|
|
11451
12080
|
keep working through git-remote-bunshin). bunshin repo help
|
|
11452
|
-
runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create]
|
|
12081
|
+
runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create,generalize]
|
|
11453
12082
|
answer the company's queued model work (cloud reflection,
|
|
11454
|
-
reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
|
|
12083
|
+
reconciliation, generalization) with this machine's model. BUNSHIN_RUNNER_KEY
|
|
11455
12084
|
runner next | submit <job> <file|-> | fail <job> --error <why>
|
|
11456
12085
|
agent mode, for a Claude Code routine or Codex cloud task
|
|
11457
12086
|
runner install --key <runner key> [--every <minutes>] [--no-run] | uninstall
|
|
@@ -11475,7 +12104,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
|
|
|
11475
12104
|
Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
|
|
11476
12105
|
CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
|
|
11477
12106
|
`;
|
|
11478
|
-
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 ${
|
|
12107
|
+
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.`;
|
|
11479
12108
|
}
|
|
11480
12109
|
});
|
|
11481
12110
|
|