@bunshin-cc/bunshin 0.3.3 → 0.4.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.
Files changed (2) hide show
  1. package/dist/bin.js +159 -17
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -2674,6 +2674,17 @@ ${d.text.replace(/\n*$/, "\n")}`),
2674
2674
  ];
2675
2675
  return parts.join("\n");
2676
2676
  }
2677
+ function removeBlock(file) {
2678
+ const text2 = readText(file);
2679
+ if (text2 === null) return false;
2680
+ const start = text2.indexOf(BLOCK_BEGIN);
2681
+ const end = text2.indexOf(BLOCK_END);
2682
+ if (start < 0 || end < start) return false;
2683
+ const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
2684
+ if (rest) writeTextAtomic(file, rest + "\n");
2685
+ else fs5.unlinkSync(file);
2686
+ return true;
2687
+ }
2677
2688
  function writeBlock(file, block2) {
2678
2689
  const text2 = readText(file) ?? "";
2679
2690
  const start = text2.indexOf(BLOCK_BEGIN);
@@ -2984,9 +2995,9 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
2984
2995
  }
2985
2996
  }
2986
2997
  }
2987
- const blockDocs = plan.instructions.map((s) => ({ id: s.id, version: s.version, text: next[s.id]?.pending ? readBlock(p.claudeMd)?.get(s.id) ?? s.body : s.body }));
2998
+ const blockDocs = config.instructionsFile ? plan.instructions.map((s) => ({ id: s.id, version: s.version, text: next[s.id]?.pending ? readBlock(p.claudeMd)?.get(s.id) ?? s.body : s.body })) : [];
2988
2999
  if (harnesses.has("claude-code") && (blockDocs.length || readBlock(p.claudeMd))) {
2989
- if (writeBlock(p.claudeMd, renderBlock(blockDocs, ""))) report.written++;
3000
+ if (blockDocs.length ? writeBlock(p.claudeMd, renderBlock(blockDocs, "")) : removeBlock(p.claudeMd)) report.written++;
2990
3001
  }
2991
3002
  if (harnesses.has("codex") && (blockDocs.length || plan.skills.length || readBlock(p.agentsMd))) {
2992
3003
  if (writeBlock(p.agentsMd, renderBlock(blockDocs, skillIndex(plan.skills, p)))) report.written++;
@@ -7127,8 +7138,8 @@ async function cmdImport2(store, args, cwd, out, err) {
7127
7138
  if (batches.length > 1) out(` batch ${i + 1}/${batches.length}: ${last.versions.length} versions${last.skipped.length ? `, ${last.skipped.length} already imported` : ""}
7128
7139
  `);
7129
7140
  }
7130
- const lib = last?.versions.at(-1)?.version;
7131
- out(`Imported into the library: ${written} document revisions${lib ? `, now lib@${lib}` : ""}; ${last?.documents ?? 0} documents active in ${pack}.
7141
+ const lib2 = last?.versions.at(-1)?.version;
7142
+ out(`Imported into the library: ${written} document revisions${lib2 ? `, now lib@${lib2}` : ""}; ${last?.documents ?? 0} documents active in ${pack}.
7132
7143
  `);
7133
7144
  if (history) out("Old commit pins resolve with `bunshin library checkout --git <sha>`.\n");
7134
7145
  const propose = args.flags["propose-rewrites"];
@@ -7316,8 +7327,8 @@ function sinceCutoff(since) {
7316
7327
  }
7317
7328
  function parsePin(text2) {
7318
7329
  const t = text2.trim();
7319
- const lib = /lib@(\d+)/.exec(t) ?? /^(\d+)$/.exec(t);
7320
- if (lib) return { version: Number(lib[1]) };
7330
+ const lib2 = /lib@(\d+)/.exec(t) ?? /^(\d+)$/.exec(t);
7331
+ if (lib2) return { version: Number(lib2[1]) };
7321
7332
  const sha2 = /\b([0-9a-f]{7,40})\b/i.exec(t);
7322
7333
  return sha2 ? { git: sha2[1].toLowerCase() } : {};
7323
7334
  }
@@ -7826,6 +7837,94 @@ init_v22();
7826
7837
  init_transcript();
7827
7838
  init_api();
7828
7839
  init_deploy();
7840
+
7841
+ // src/v2/live.ts
7842
+ init_api();
7843
+ var LIVE_REFRESH_MS = 5 * 6e4;
7844
+ var MAX_DIFF_LINES = 80;
7845
+ var instructionDocs = (bundle) => (bundle?.sections ?? []).filter((s) => s.delivery === "always" && s.status === "active");
7846
+ var skillDocs = (bundle) => (bundle?.sections ?? []).filter((s) => !!s.skill && s.status === "active");
7847
+ function baselineOf(bundle) {
7848
+ return {
7849
+ ...bundle?.libraryVersion !== void 0 ? { library: bundle.libraryVersion } : {},
7850
+ skills: Object.fromEntries(skillDocs(bundle).map((s) => [s.id, { version: s.version, name: s.skill.name }])),
7851
+ instructions: Object.fromEntries(instructionDocs(bundle).map((s) => [s.id, s.body]))
7852
+ };
7853
+ }
7854
+ var lib = (v) => v === void 0 ? "the library" : `lib@${v}`;
7855
+ function renderInstructions(bundle) {
7856
+ const docs = instructionDocs(bundle);
7857
+ if (!docs.length) return "";
7858
+ return [
7859
+ `## Standing instructions (your organization, from Bunshin ${lib(bundle?.libraryVersion)})`,
7860
+ "Follow these as you would CLAUDE.md. Bunshin re-delivers them after /clear and compaction, and tells you when they change.",
7861
+ "",
7862
+ ...docs.map((d) => d.body.trim())
7863
+ ].join("\n");
7864
+ }
7865
+ function lineDiff(before, after, max = MAX_DIFF_LINES) {
7866
+ const a = before.split("\n");
7867
+ const b = after.split("\n");
7868
+ if (a.length * b.length > 4e6) return [`(the text changed substantially; ${b.length} lines now)`];
7869
+ const dp = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
7870
+ for (let i2 = a.length - 1; i2 >= 0; i2--) for (let j2 = b.length - 1; j2 >= 0; j2--) dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
7871
+ const out = [];
7872
+ let i = 0;
7873
+ let j = 0;
7874
+ while (i < a.length || j < b.length) {
7875
+ if (i < a.length && j < b.length && a[i] === b[j]) {
7876
+ i++;
7877
+ j++;
7878
+ } else if (j < b.length && (i === a.length || dp[i][j + 1] >= dp[i + 1][j])) {
7879
+ if (b[j].trim()) out.push(`+ ${b[j]}`);
7880
+ j++;
7881
+ } else {
7882
+ if (a[i].trim()) out.push(`- ${a[i]}`);
7883
+ i++;
7884
+ }
7885
+ }
7886
+ return out.length > max ? [...out.slice(0, max), `\u2026 ${out.length - max} more changed lines`] : out;
7887
+ }
7888
+ function changesSince(base, bundle, skillPath) {
7889
+ const next = baselineOf(bundle);
7890
+ if (!bundle || next.library === base.library) return { text: "", next: base };
7891
+ const lines = [];
7892
+ const now = instructionDocs(bundle);
7893
+ for (const d of now) {
7894
+ const old = base.instructions[d.id];
7895
+ if (old === d.body) continue;
7896
+ if (old === void 0) {
7897
+ lines.push(`New standing instructions ("${d.title}"):`, d.body.trim(), "");
7898
+ continue;
7899
+ }
7900
+ const diff = lineDiff(old, d.body);
7901
+ if (diff.length) lines.push(`Standing instructions changed ("${d.title}"); follow the new text:`, ...diff, "");
7902
+ }
7903
+ for (const id of Object.keys(base.instructions)) if (!now.some((d) => d.id === id)) lines.push(`Standing instructions withdrawn (${id}): stop following them.`, "");
7904
+ const skillLines = [];
7905
+ for (const s of skillDocs(bundle)) {
7906
+ const old = base.skills[s.id];
7907
+ const where = skillPath(s.skill.name);
7908
+ const read = where ? `re-read \`${where}\` before using it` : `load it with \`bunshin load ${s.id}\` before using it`;
7909
+ if (!old) skillLines.push(`- new skill \`${s.skill.name}\`: ${s.skill.description.replace(/\s+/g, " ").trim()} (${where ? `\`${where}\`` : `\`bunshin load ${s.id}\``})`);
7910
+ else if (old.version !== s.version) skillLines.push(`- \`${s.skill.name}\` updated (v${old.version} \u2192 v${s.version}): ${read}`);
7911
+ }
7912
+ for (const [id, old] of Object.entries(base.skills)) if (!next.skills[id]) skillLines.push(`- \`${old.name}\` was retired: don't use it`);
7913
+ if (skillLines.length) lines.push("Skills:", ...skillLines);
7914
+ if (!lines.length) return { text: "", next };
7915
+ return { text: [`## Bunshin: the library changed during this session (${lib(base.library)} \u2192 ${lib(next.library)})`, ...lines].join("\n").trim(), next };
7916
+ }
7917
+ function refreshInBackground(v2, store, detach2, cwd, now = Date.now()) {
7918
+ if (!canCall(store.config())) return false;
7919
+ const state = v2.readSyncState();
7920
+ const last = Math.max(Date.parse(state.lastAttemptAt ?? "") || 0, Date.parse(state.lastSyncAt ?? "") || 0);
7921
+ if (now - last < LIVE_REFRESH_MS) return false;
7922
+ v2.writeSyncState({ ...state, lastAttemptAt: new Date(now).toISOString() });
7923
+ detach2(["--root", store.root, "sync", "--quiet"], { cwd });
7924
+ return true;
7925
+ }
7926
+
7927
+ // src/v2/hooks.ts
7829
7928
  init_reflect2();
7830
7929
  init_repo();
7831
7930
 
@@ -7973,6 +8072,10 @@ async function onSessionStart(ctx) {
7973
8072
  }
7974
8073
  }
7975
8074
  const bundle = v2.readBundle();
8075
+ v2.updateSession(id, ctx.cwd, (s) => {
8076
+ s.live = baselineOf(bundle);
8077
+ });
8078
+ const instructions = renderInstructions(bundle);
7976
8079
  const lines = [];
7977
8080
  for (const d of (bundle?.digest ?? []).slice(0, 5)) lines.push(`- ${d}`);
7978
8081
  const proposed = v2.listSuggestions().filter((s) => s.status === "proposed").length;
@@ -7982,9 +8085,37 @@ async function onSessionStart(ctx) {
7982
8085
  lines.push(`Open question${questions.length === 1 ? "" : "s"} for your team (ask the user when it fits; answer with \`bunshin questions answer <id> "<choice>"\`):`);
7983
8086
  for (const q of questions) lines.push(`- [${q.id}] ${q.question}${q.options.length ? ` Options: ${q.options.join(" | ")}` : ""}`);
7984
8087
  }
7985
- if (!lines.length) return {};
7986
- return additional("SessionStart", `## Bunshin
7987
- ${lines.join("\n")}`);
8088
+ const parts = [instructions, lines.length ? `## Bunshin
8089
+ ${lines.join("\n")}` : ""].filter(Boolean);
8090
+ if (!parts.length) return {};
8091
+ return additional("SessionStart", parts.join("\n\n"));
8092
+ }
8093
+ function liveUpdate(v2, ctx, session) {
8094
+ try {
8095
+ refreshInBackground(v2, ctx.store, ctx.detach, ctx.cwd);
8096
+ const bundle = v2.readBundle();
8097
+ if (!session.live) {
8098
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
8099
+ s.live = baselineOf(bundle);
8100
+ });
8101
+ return "";
8102
+ }
8103
+ const config = ctx.store.config();
8104
+ const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
8105
+ const paths = deployed ? deployPaths() : null;
8106
+ const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
8107
+ const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
8108
+ const { text: text2, next } = changesSince(session.live, bundle, skillPath);
8109
+ if (next !== session.live) {
8110
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
8111
+ s.live = next;
8112
+ });
8113
+ }
8114
+ if (text2) v2.log("v2-live-update", { session: session.sessionId, from: session.live.library ?? null, to: next.library ?? null });
8115
+ return text2;
8116
+ } catch {
8117
+ return "";
8118
+ }
7988
8119
  }
7989
8120
  function hintLine(titles, harness) {
7990
8121
  const quoted = titles.map((t) => `"${t}"`).join(", ");
@@ -8001,7 +8132,13 @@ function onPrompt(ctx) {
8001
8132
  s.lastPrompt = prompt.slice(0, 4e3);
8002
8133
  s.lastActivityAt = now;
8003
8134
  });
8004
- if (!prompt.trim()) return {};
8135
+ const live = liveUpdate(v2, ctx, session);
8136
+ const withLive = (out) => {
8137
+ if (!live) return out;
8138
+ const text2 = [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
8139
+ return additional("UserPromptSubmit", text2);
8140
+ };
8141
+ if (!prompt.trim()) return withLive({});
8005
8142
  const config = ctx.store.config();
8006
8143
  const view = localView(v2, config, ctx.cwd);
8007
8144
  const injected = [...session.loaded, ...session.pushed];
@@ -8013,16 +8150,16 @@ function onPrompt(ctx) {
8013
8150
  addUnique(s.loaded, sec.id);
8014
8151
  });
8015
8152
  v2.log("v2-auto", { session: session.sessionId, section: sec.id, reason: res.reason });
8016
- return additional("UserPromptSubmit", renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) }));
8153
+ return withLive(additional("UserPromptSubmit", renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) })));
8017
8154
  }
8018
8155
  const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id)).slice(0, 3);
8019
- if (!fresh.length) return {};
8156
+ if (!fresh.length) return withLive({});
8020
8157
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
8021
8158
  s.hinted = s.hinted ?? [];
8022
8159
  addUnique(s.hinted, ...fresh.map((h) => h.id));
8023
8160
  addUnique(s.served, ...fresh.map((h) => h.id));
8024
8161
  });
8025
- return additional("UserPromptSubmit", hintLine(fresh.map((h) => h.title), config.harness));
8162
+ return withLive(additional("UserPromptSubmit", hintLine(fresh.map((h) => h.title), config.harness)));
8026
8163
  }
8027
8164
  function toolPaths(input) {
8028
8165
  if (!input || typeof input !== "object") return [];
@@ -8038,16 +8175,21 @@ function onPreToolUse(ctx) {
8038
8175
  const session = touch(v2, ctx, (s) => {
8039
8176
  s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
8040
8177
  });
8178
+ const live = liveUpdate(v2, ctx, session);
8179
+ const withLive = (out) => {
8180
+ if (!live) return out;
8181
+ return additional("PreToolUse", [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n"));
8182
+ };
8041
8183
  const raw = toolPaths(ctx.input.tool_input);
8042
- if (!raw.length) return {};
8184
+ if (!raw.length) return withLive({});
8043
8185
  const config = ctx.store.config();
8044
8186
  const view = localView(v2, config, ctx.cwd);
8045
8187
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
8046
8188
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
8047
- if (!paths.length && !text2) return {};
8189
+ if (!paths.length && !text2) return withLive({});
8048
8190
  const seen = /* @__PURE__ */ new Set([...session.pushed, ...session.loaded]);
8049
8191
  const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
8050
- if (!hits.length) return {};
8192
+ if (!hits.length) return withLive({});
8051
8193
  const ids = hits.map((h) => h.section.id);
8052
8194
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
8053
8195
  addUnique(s.pushed, ...ids);
@@ -8056,7 +8198,7 @@ function onPreToolUse(ctx) {
8056
8198
  });
8057
8199
  v2.log("v2-push", { session: session.sessionId, tool: ctx.input.tool_name, hits: hits.map((h) => `${h.section.id} ${h.trigger}`) });
8058
8200
  const deployed = deployEnabled(config, view.ctx);
8059
- return additional("PreToolUse", hits.map((h) => renderSection(h.section, h.trigger, { ...h.anchor ? { anchor: h.anchor } : {}, deployed: deployed && !!h.section.skill })).join("\n\n"));
8201
+ return withLive(additional("PreToolUse", hits.map((h) => renderSection(h.section, h.trigger, { ...h.anchor ? { anchor: h.anchor } : {}, deployed: deployed && !!h.section.skill })).join("\n\n")));
8060
8202
  }
8061
8203
  function onStop(ctx) {
8062
8204
  const v2 = new V2Store(ctx.store);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {