@bunshin-cc/bunshin 0.3.3 → 0.4.1

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 +192 -19
  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
 
@@ -7938,7 +8037,7 @@ async function handleV2(event, ctx) {
7938
8037
  case "session-start":
7939
8038
  return onSessionStart(ctx);
7940
8039
  case "user-prompt-submit":
7941
- return onPrompt(ctx);
8040
+ return await onPrompt(ctx);
7942
8041
  case "pre-tool-use":
7943
8042
  return onPreToolUse(ctx);
7944
8043
  case "stop":
@@ -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,16 +8085,60 @@ 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
+ }
8119
+ }
8120
+ function skillHint(sec, others) {
8121
+ const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
8122
+ 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}`;
8123
+ }
8124
+ async function remoteAuto(ctx, prompt, exclude) {
8125
+ const config = ctx.store.config();
8126
+ if (!canCall(config) || !config.recall.useMothership) return null;
8127
+ const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
8128
+ if (budget < 300) return null;
8129
+ try {
8130
+ const res = await v2api(config, "POST", "/recall/auto", { prompt: prompt.slice(0, 8e3), exclude }, { timeoutMs: budget });
8131
+ return res.available ? res : null;
8132
+ } catch {
8133
+ return null;
8134
+ }
7988
8135
  }
7989
8136
  function hintLine(titles, harness) {
7990
8137
  const quoted = titles.map((t) => `"${t}"`).join(", ");
7991
8138
  const how = harness === "claude-code" ? "If one could change your approach, call the bunshin_recall tool (describe the task abstractly) and load only what you need." : 'If one could change your approach, run `bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>"` and `bunshin load <id>`.';
7992
8139
  return `Bunshin: possibly relevant library sections: ${quoted}. ${how}`;
7993
8140
  }
7994
- function onPrompt(ctx) {
8141
+ async function onPrompt(ctx) {
7995
8142
  const prompt = ctx.input.prompt ?? "";
7996
8143
  if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
7997
8144
  const v2 = new V2Store(ctx.store);
@@ -8001,11 +8148,30 @@ function onPrompt(ctx) {
8001
8148
  s.lastPrompt = prompt.slice(0, 4e3);
8002
8149
  s.lastActivityAt = now;
8003
8150
  });
8004
- if (!prompt.trim()) return {};
8151
+ const live = liveUpdate(v2, ctx, session);
8152
+ const withLive = (out) => {
8153
+ if (!live) return out;
8154
+ const text2 = [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
8155
+ return additional("UserPromptSubmit", text2);
8156
+ };
8157
+ if (!prompt.trim()) return withLive({});
8005
8158
  const config = ctx.store.config();
8006
8159
  const view = localView(v2, config, ctx.cwd);
8007
8160
  const injected = [...session.loaded, ...session.pushed];
8008
8161
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
8162
+ const remote = await remoteAuto(ctx, prompt, injected);
8163
+ if (remote) {
8164
+ const local = (id) => view.sections.find((s) => s.id === id);
8165
+ if (remote.section) {
8166
+ res.section = local(remote.section.id) ?? remote.section;
8167
+ res.anchor = void 0;
8168
+ res.reason = `dense ${remote.reason ?? ""}`.trim();
8169
+ } else {
8170
+ res.section = void 0;
8171
+ res.hint = (remote.hint ?? []).map((h) => ({ id: h.id, title: local(h.id)?.title ?? h.title }));
8172
+ res.reason = `dense ${remote.reason ?? ""}`.trim();
8173
+ }
8174
+ }
8009
8175
  if (res.section) {
8010
8176
  const sec = res.section;
8011
8177
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
@@ -8013,16 +8179,18 @@ function onPrompt(ctx) {
8013
8179
  addUnique(s.loaded, sec.id);
8014
8180
  });
8015
8181
  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) }));
8182
+ return withLive(additional("UserPromptSubmit", renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) })));
8017
8183
  }
8018
8184
  const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id)).slice(0, 3);
8019
- if (!fresh.length) return {};
8185
+ if (!fresh.length) return withLive({});
8020
8186
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
8021
8187
  s.hinted = s.hinted ?? [];
8022
8188
  addUnique(s.hinted, ...fresh.map((h) => h.id));
8023
8189
  addUnique(s.served, ...fresh.map((h) => h.id));
8024
8190
  });
8025
- return additional("UserPromptSubmit", hintLine(fresh.map((h) => h.title), config.harness));
8191
+ const topSkill = view.sections.find((s) => s.id === fresh[0].id && s.skill);
8192
+ if (topSkill && deployEnabled(config, view.ctx)) return withLive(additional("UserPromptSubmit", skillHint(topSkill, fresh.slice(1).map((h) => h.title))));
8193
+ return withLive(additional("UserPromptSubmit", hintLine(fresh.map((h) => h.title), config.harness)));
8026
8194
  }
8027
8195
  function toolPaths(input) {
8028
8196
  if (!input || typeof input !== "object") return [];
@@ -8038,16 +8206,21 @@ function onPreToolUse(ctx) {
8038
8206
  const session = touch(v2, ctx, (s) => {
8039
8207
  s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
8040
8208
  });
8209
+ const live = liveUpdate(v2, ctx, session);
8210
+ const withLive = (out) => {
8211
+ if (!live) return out;
8212
+ return additional("PreToolUse", [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n"));
8213
+ };
8041
8214
  const raw = toolPaths(ctx.input.tool_input);
8042
- if (!raw.length) return {};
8215
+ if (!raw.length) return withLive({});
8043
8216
  const config = ctx.store.config();
8044
8217
  const view = localView(v2, config, ctx.cwd);
8045
8218
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
8046
8219
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
8047
- if (!paths.length && !text2) return {};
8220
+ if (!paths.length && !text2) return withLive({});
8048
8221
  const seen = /* @__PURE__ */ new Set([...session.pushed, ...session.loaded]);
8049
8222
  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 {};
8223
+ if (!hits.length) return withLive({});
8051
8224
  const ids = hits.map((h) => h.section.id);
8052
8225
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
8053
8226
  addUnique(s.pushed, ...ids);
@@ -8056,7 +8229,7 @@ function onPreToolUse(ctx) {
8056
8229
  });
8057
8230
  v2.log("v2-push", { session: session.sessionId, tool: ctx.input.tool_name, hits: hits.map((h) => `${h.section.id} ${h.trigger}`) });
8058
8231
  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"));
8232
+ 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
8233
  }
8061
8234
  function onStop(ctx) {
8062
8235
  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.1",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {