@bunshin-cc/bunshin 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -2849,184 +2849,6 @@ var init_redact2 = __esm({
2849
2849
  }
2850
2850
  });
2851
2851
 
2852
- // src/transcript.ts
2853
- import fs5 from "node:fs";
2854
- function parseTranscript(file) {
2855
- let raw = "";
2856
- try {
2857
- raw = fs5.readFileSync(file, "utf8");
2858
- } catch {
2859
- return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
2860
- }
2861
- const turns = [];
2862
- let userMessages = 0;
2863
- let toolCalls = 0;
2864
- let toolFailures = 0;
2865
- for (const lineText of raw.split("\n")) {
2866
- if (!lineText.trim()) continue;
2867
- let line;
2868
- try {
2869
- line = JSON.parse(lineText);
2870
- } catch {
2871
- continue;
2872
- }
2873
- if (line.type === "response_item") {
2874
- const item = line.payload;
2875
- if (!item) continue;
2876
- if (item.type === "message" && (item.role === "user" || item.role === "assistant") && item.channel !== "analysis") {
2877
- const text2 = blockText(item.content).trim();
2878
- if (item.role === "user" ? isHumanText(text2) : !!text2) {
2879
- turns.push({ role: item.role, text: text2 });
2880
- if (item.role === "user") userMessages++;
2881
- }
2882
- } else if (item.type === "function_call" || item.type === "custom_tool_call") {
2883
- let input = item.input ?? item.arguments;
2884
- if (typeof input === "string") {
2885
- try {
2886
- input = JSON.parse(input);
2887
- } catch {
2888
- }
2889
- }
2890
- turns.push({ role: "tool", tool: item.name || "tool", text: summarizeInput(input) });
2891
- toolCalls++;
2892
- } else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
2893
- const outputs = Array.isArray(item.output) ? item.output.filter((part) => part && typeof part === "object" && typeof part.text === "string").map((part) => part.text) : [item.output];
2894
- for (const output of outputs) {
2895
- for (const result of codexToolResults(output)) {
2896
- if (!result.text) continue;
2897
- turns.push({ role: "tool", tool: "result", ...result });
2898
- if (result.isError) toolFailures++;
2899
- }
2900
- }
2901
- }
2902
- continue;
2903
- }
2904
- if (line.type !== "user" && line.type !== "assistant") continue;
2905
- if (line.isMeta) continue;
2906
- const content = line.message?.content;
2907
- if (typeof content === "string") {
2908
- if (line.type === "user" && isHumanText(content)) {
2909
- turns.push({ role: "user", text: content });
2910
- userMessages++;
2911
- } else if (line.type === "assistant" && content.trim()) {
2912
- turns.push({ role: "assistant", text: content });
2913
- }
2914
- continue;
2915
- }
2916
- if (!Array.isArray(content)) continue;
2917
- for (const block2 of content) {
2918
- if (block2.type === "text" && block2.text) {
2919
- if (line.type === "user") {
2920
- if (!isHumanText(block2.text)) continue;
2921
- userMessages++;
2922
- turns.push({ role: "user", text: block2.text });
2923
- } else {
2924
- turns.push({ role: "assistant", text: block2.text });
2925
- }
2926
- } else if (block2.type === "tool_use") {
2927
- toolCalls++;
2928
- turns.push({ role: "tool", tool: block2.name || "tool", text: summarizeInput(block2.input) });
2929
- } else if (block2.type === "tool_result") {
2930
- const text2 = blockText(block2.content);
2931
- if (block2.is_error) toolFailures++;
2932
- turns.push({ role: "tool", tool: "result", text: text2, isError: !!block2.is_error });
2933
- }
2934
- }
2935
- }
2936
- return { turns, userMessages, toolCalls, toolFailures };
2937
- }
2938
- function isHumanText(text2) {
2939
- const t = text2.trim();
2940
- if (!t) return false;
2941
- if (t.startsWith(BANNER)) return false;
2942
- if (/^<(?:command-name|command-message|local-command-stdout|system-reminder|bash-input|bash-stdout)/.test(t)) return false;
2943
- return true;
2944
- }
2945
- function blockText(content) {
2946
- if (typeof content === "string") return content;
2947
- if (Array.isArray(content)) {
2948
- return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
2949
- }
2950
- return content == null ? "" : JSON.stringify(content);
2951
- }
2952
- function codexToolResults(output, depth = 0) {
2953
- const raw = blockText(output);
2954
- let envelope = output;
2955
- if (typeof output === "string") {
2956
- try {
2957
- envelope = JSON.parse(output);
2958
- } catch {
2959
- }
2960
- }
2961
- if (depth > 6) return [{ text: raw, pending: true }];
2962
- if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
2963
- if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
2964
- const result = envelope;
2965
- if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
2966
- const pending = !Number.isInteger(result.exit_code);
2967
- return [{
2968
- text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
2969
- ${result.output}`,
2970
- isError: !pending && result.exit_code !== 0,
2971
- pending
2972
- }];
2973
- }
2974
- if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
2975
- if (result.status === "rejected") return [{ text: raw, isError: true }];
2976
- }
2977
- return [{
2978
- text: raw,
2979
- isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
2980
- pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
2981
- }];
2982
- }
2983
- function summarizeInput(input) {
2984
- if (input == null) return "";
2985
- if (typeof input !== "object") return String(input);
2986
- const obj = input;
2987
- for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
2988
- if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
2989
- }
2990
- return JSON.stringify(obj);
2991
- }
2992
- function bounded(text2, maxChars) {
2993
- if (text2.length <= maxChars) return text2;
2994
- const marker = "\n[... middle omitted ...]\n";
2995
- if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
2996
- const head = Math.floor((maxChars - marker.length) / 2);
2997
- return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
2998
- }
2999
- function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
3000
- const parts = [];
3001
- for (const t of facts.turns.slice(from)) {
3002
- const text2 = redact2(t.text);
3003
- if (t.role === "user") parts.push(`[USER]
3004
- ${bounded(text2, maxChars)}`);
3005
- else if (t.role === "assistant") parts.push(`[ASSISTANT]
3006
- ${bounded(text2, maxChars)}`);
3007
- else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
3008
- ${bounded(text2, toolResultChars)}`);
3009
- else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
3010
- }
3011
- return bounded(parts.join("\n\n"), maxChars);
3012
- }
3013
- function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
3014
- const facts = parseTranscript(file);
3015
- const visible = facts.turns.filter((t) => t.role !== "tool");
3016
- const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
3017
- const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
3018
- return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
3019
- }
3020
- var BANNER;
3021
- var init_transcript = __esm({
3022
- "src/transcript.ts"() {
3023
- "use strict";
3024
- init_redact2();
3025
- init_util();
3026
- BANNER = "## Recalled lessons (Bunshin)";
3027
- }
3028
- });
3029
-
3030
2852
  // src/v2/api.ts
3031
2853
  function canCall(config) {
3032
2854
  return !!config.apiUrl && !!config.apiKey;
@@ -3604,7 +3426,7 @@ var init_src = __esm({
3604
3426
  });
3605
3427
 
3606
3428
  // src/v2/store.ts
3607
- import fs6 from "node:fs";
3429
+ import fs5 from "node:fs";
3608
3430
  import path5 from "node:path";
3609
3431
  import { randomBytes as randomBytes3 } from "node:crypto";
3610
3432
  function hexId(prefix) {
@@ -3725,7 +3547,7 @@ var init_store2 = __esm({
3725
3547
  const dir = this.p("sessions");
3726
3548
  if (!fileExists(dir)) return [];
3727
3549
  const out = [];
3728
- for (const f of fs6.readdirSync(dir)) {
3550
+ for (const f of fs5.readdirSync(dir)) {
3729
3551
  if (!f.endsWith(".json") || f === "current.json") continue;
3730
3552
  const raw = readJson(path5.join(dir, f), null);
3731
3553
  if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
@@ -3755,7 +3577,7 @@ var init_store2 = __esm({
3755
3577
  let at = raw.at;
3756
3578
  if (!at) {
3757
3579
  try {
3758
- at = fs6.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
3580
+ at = fs5.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
3759
3581
  } catch {
3760
3582
  at = (/* @__PURE__ */ new Date()).toISOString();
3761
3583
  }
@@ -3784,10 +3606,10 @@ var init_store2 = __esm({
3784
3606
  ensureDir(path5.dirname(file));
3785
3607
  for (let attempt = 0; attempt < 2; attempt++) {
3786
3608
  try {
3787
- fs6.writeFileSync(file, String(process.pid), { flag: "wx" });
3609
+ fs5.writeFileSync(file, String(process.pid), { flag: "wx" });
3788
3610
  return () => {
3789
3611
  try {
3790
- fs6.unlinkSync(file);
3612
+ fs5.unlinkSync(file);
3791
3613
  } catch {
3792
3614
  }
3793
3615
  };
@@ -3795,8 +3617,8 @@ var init_store2 = __esm({
3795
3617
  if (err.code !== "EEXIST") return null;
3796
3618
  let stale = false;
3797
3619
  try {
3798
- const owner = Number(fs6.readFileSync(file, "utf8"));
3799
- const age = Date.now() - fs6.statSync(file).mtimeMs;
3620
+ const owner = Number(fs5.readFileSync(file, "utf8"));
3621
+ const age = Date.now() - fs5.statSync(file).mtimeMs;
3800
3622
  if (age > staleMs) stale = true;
3801
3623
  else if (Number.isInteger(owner) && owner > 0) {
3802
3624
  try {
@@ -3810,7 +3632,7 @@ var init_store2 = __esm({
3810
3632
  }
3811
3633
  if (!stale) return null;
3812
3634
  try {
3813
- fs6.unlinkSync(file);
3635
+ fs5.unlinkSync(file);
3814
3636
  } catch {
3815
3637
  }
3816
3638
  }
@@ -3827,7 +3649,7 @@ var init_store2 = __esm({
3827
3649
  const dir = this.p(name);
3828
3650
  if (!fileExists(dir)) return [];
3829
3651
  const out = [];
3830
- for (const f of fs6.readdirSync(dir)) {
3652
+ for (const f of fs5.readdirSync(dir)) {
3831
3653
  if (!f.endsWith(".json")) continue;
3832
3654
  const v = readJson(path5.join(dir, f), null);
3833
3655
  if (v) out.push(v);
@@ -3843,13 +3665,13 @@ var init_store2 = __esm({
3843
3665
 
3844
3666
  // src/v2/deploy.ts
3845
3667
  import { createHash as createHash2 } from "node:crypto";
3846
- import fs7 from "node:fs";
3668
+ import fs6 from "node:fs";
3847
3669
  import os2 from "node:os";
3848
3670
  import path6 from "node:path";
3849
3671
  function deployWorkspace(env = process.env) {
3850
3672
  if (env.BUNSHIN_DEPLOY_WORKSPACE) return path6.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
3851
3673
  try {
3852
- const ws = JSON.parse(fs7.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
3674
+ const ws = JSON.parse(fs6.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
3853
3675
  return typeof ws === "string" && ws ? ws : null;
3854
3676
  } catch {
3855
3677
  return null;
@@ -3872,12 +3694,12 @@ function deployPaths(env = process.env) {
3872
3694
  function setDeployWorkspace(dir, env = process.env) {
3873
3695
  const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3874
3696
  const ws = dir ? path6.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
3875
- if (ws && !fs7.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
3697
+ if (ws && !fs6.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
3876
3698
  const stateDir = path6.join(home, ".bunshin", "deploy");
3877
- const deployed = fs7.existsSync(stateDir) && fs7.readdirSync(stateDir).some((f) => {
3699
+ const deployed = fs6.existsSync(stateDir) && fs6.readdirSync(stateDir).some((f) => {
3878
3700
  if (f === "settings.json" || !f.endsWith(".json")) return false;
3879
3701
  try {
3880
- return Object.keys(JSON.parse(fs7.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
3702
+ return Object.keys(JSON.parse(fs6.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
3881
3703
  } catch {
3882
3704
  return false;
3883
3705
  }
@@ -3885,8 +3707,8 @@ function setDeployWorkspace(dir, env = process.env) {
3885
3707
  if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
3886
3708
  throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
3887
3709
  }
3888
- fs7.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
3889
- fs7.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
3710
+ fs6.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
3711
+ fs6.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
3890
3712
  return ws;
3891
3713
  }
3892
3714
  function statePath(p, orgId) {
@@ -3898,7 +3720,7 @@ function readState(p, orgId) {
3898
3720
  }
3899
3721
  function readText(file) {
3900
3722
  try {
3901
- return fs7.readFileSync(file, "utf8");
3723
+ return fs6.readFileSync(file, "utf8");
3902
3724
  } catch {
3903
3725
  return null;
3904
3726
  }
@@ -3906,13 +3728,13 @@ function readText(file) {
3906
3728
  function writeFile(file, content, mode) {
3907
3729
  writeTextAtomic(file, content);
3908
3730
  try {
3909
- fs7.chmodSync(file, mode === "x" ? 493 : 420);
3731
+ fs6.chmodSync(file, mode === "x" ? 493 : 420);
3910
3732
  } catch {
3911
3733
  }
3912
3734
  }
3913
3735
  function removeFile(file) {
3914
3736
  try {
3915
- fs7.unlinkSync(file);
3737
+ fs6.unlinkSync(file);
3916
3738
  } catch {
3917
3739
  }
3918
3740
  }
@@ -3920,8 +3742,8 @@ function pruneDirs(dir, stop) {
3920
3742
  let d = dir;
3921
3743
  while (d.startsWith(stop) && d !== stop) {
3922
3744
  try {
3923
- if (fs7.readdirSync(d).length) return;
3924
- fs7.rmdirSync(d);
3745
+ if (fs6.readdirSync(d).length) return;
3746
+ fs6.rmdirSync(d);
3925
3747
  } catch {
3926
3748
  return;
3927
3749
  }
@@ -3931,7 +3753,7 @@ function pruneDirs(dir, stop) {
3931
3753
  function listFiles(dir, base = dir) {
3932
3754
  let entries;
3933
3755
  try {
3934
- entries = fs7.readdirSync(dir, { withFileTypes: true });
3756
+ entries = fs6.readdirSync(dir, { withFileTypes: true });
3935
3757
  } catch {
3936
3758
  return [];
3937
3759
  }
@@ -4053,7 +3875,7 @@ function removeBlock(file) {
4053
3875
  if (start < 0 || end < start) return false;
4054
3876
  const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
4055
3877
  if (rest) writeTextAtomic(file, rest + "\n");
4056
- else fs7.unlinkSync(file);
3878
+ else fs6.unlinkSync(file);
4057
3879
  return true;
4058
3880
  }
4059
3881
  function writeBlock(file, block2) {
@@ -4162,7 +3984,7 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4162
3984
  for (const root of roots) {
4163
3985
  let names = [];
4164
3986
  try {
4165
- names = fs7.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
3987
+ names = fs6.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
4166
3988
  } catch {
4167
3989
  continue;
4168
3990
  }
@@ -4173,7 +3995,7 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4173
3995
  if (skillMd === null) continue;
4174
3996
  let born = 0;
4175
3997
  try {
4176
- const st = fs7.statSync(path6.join(dir, "SKILL.md"));
3998
+ const st = fs6.statSync(path6.join(dir, "SKILL.md"));
4177
3999
  born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
4178
4000
  } catch {
4179
4001
  continue;
@@ -4234,11 +4056,11 @@ function lockDeploy(p) {
4234
4056
  const file = path6.join(p.state, ".lock");
4235
4057
  for (let i = 0; i < 2; i++) {
4236
4058
  try {
4237
- fs7.writeFileSync(file, String(process.pid), { flag: "wx" });
4059
+ fs6.writeFileSync(file, String(process.pid), { flag: "wx" });
4238
4060
  return () => removeFile(file);
4239
4061
  } catch {
4240
4062
  try {
4241
- const age = Date.now() - fs7.statSync(file).mtimeMs;
4063
+ const age = Date.now() - fs6.statSync(file).mtimeMs;
4242
4064
  const pid = Number(readText(file));
4243
4065
  let alive = age < 12e4;
4244
4066
  if (alive && pid > 0) {
@@ -4325,7 +4147,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4325
4147
  ensureDir(path6.dirname(t.abs));
4326
4148
  writeFile(t.abs, content, t.mode);
4327
4149
  report.written++;
4328
- } else if (t.mode === "x" && !isExecutable(t.abs)) fs7.chmodSync(t.abs, 493);
4150
+ } else if (t.mode === "x" && !isExecutable(t.abs)) fs6.chmodSync(t.abs, 493);
4329
4151
  } catch (err) {
4330
4152
  report.warnings.push(`${t.abs} not written: ${err instanceof Error ? err.message : String(err)}`);
4331
4153
  continue;
@@ -4345,7 +4167,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4345
4167
  if (d.kind === "skill") {
4346
4168
  for (const dir of d.dirs ?? []) {
4347
4169
  const manifest = safeJoin(dir, MANIFEST);
4348
- if (foreign.has(dir) || !manifest || !fs7.existsSync(path6.join(dir, "SKILL.md"))) continue;
4170
+ if (foreign.has(dir) || !manifest || !fs6.existsSync(path6.join(dir, "SKILL.md"))) continue;
4349
4171
  writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
4350
4172
  }
4351
4173
  report.skills++;
@@ -4372,7 +4194,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4372
4194
  if (!owned(dir)) continue;
4373
4195
  removeFile(path6.join(dir, MANIFEST));
4374
4196
  pruneDirs(dir, path6.dirname(dir));
4375
- if (fs7.existsSync(dir)) {
4197
+ if (fs6.existsSync(dir)) {
4376
4198
  const left = listFiles(dir);
4377
4199
  if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
4378
4200
  else pruneDirs(dir, path6.dirname(dir));
@@ -4394,14 +4216,14 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4394
4216
  const link = path6.join(p.bin, path6.basename(t.abs));
4395
4217
  ensureDir(p.bin);
4396
4218
  try {
4397
- const cur = fs7.readlinkSync(link);
4219
+ const cur = fs6.readlinkSync(link);
4398
4220
  if (cur !== t.abs) {
4399
- fs7.unlinkSync(link);
4400
- fs7.symlinkSync(t.abs, link);
4221
+ fs6.unlinkSync(link);
4222
+ fs6.symlinkSync(t.abs, link);
4401
4223
  }
4402
4224
  } catch {
4403
4225
  try {
4404
- if (!fs7.existsSync(link)) fs7.symlinkSync(t.abs, link);
4226
+ if (!fs6.existsSync(link)) fs6.symlinkSync(t.abs, link);
4405
4227
  } catch {
4406
4228
  }
4407
4229
  }
@@ -4415,12 +4237,12 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4415
4237
  if (!dir || !insideRoot(dir, path6.join(dir, ".claude", "x"))) continue;
4416
4238
  const link = path6.join(dir, ".claude", "skills");
4417
4239
  try {
4418
- fs7.lstatSync(link);
4240
+ fs6.lstatSync(link);
4419
4241
  } catch {
4420
4242
  ensureDir(path6.dirname(link));
4421
4243
  ensureDir(p.claudeSkills);
4422
4244
  try {
4423
- fs7.symlinkSync(p.claudeSkills, link);
4245
+ fs6.symlinkSync(p.claudeSkills, link);
4424
4246
  } catch {
4425
4247
  }
4426
4248
  }
@@ -4429,8 +4251,8 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4429
4251
  for (const old of state.links) {
4430
4252
  if (links.has(old)) continue;
4431
4253
  try {
4432
- const target = fs7.readlinkSync(old);
4433
- if (!fs7.existsSync(target)) fs7.unlinkSync(old);
4254
+ const target = fs6.readlinkSync(old);
4255
+ if (!fs6.existsSync(target)) fs6.unlinkSync(old);
4434
4256
  } catch {
4435
4257
  }
4436
4258
  }
@@ -4479,7 +4301,7 @@ var init_deploy = __esm({
4479
4301
  MAX_FILE = 2 * 1024 * 1024;
4480
4302
  isExecutable = (file) => {
4481
4303
  try {
4482
- return (fs7.statSync(file).mode & 73) !== 0;
4304
+ return (fs6.statSync(file).mode & 73) !== 0;
4483
4305
  } catch {
4484
4306
  return false;
4485
4307
  }
@@ -4597,6 +4419,227 @@ var init_live = __esm({
4597
4419
  }
4598
4420
  });
4599
4421
 
4422
+ // src/transcript.ts
4423
+ import fs7 from "node:fs";
4424
+ function parseTranscript(file) {
4425
+ let raw = "";
4426
+ try {
4427
+ raw = fs7.readFileSync(file, "utf8");
4428
+ } catch {
4429
+ return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
4430
+ }
4431
+ const turns = [];
4432
+ let userMessages = 0;
4433
+ let toolCalls = 0;
4434
+ let toolFailures = 0;
4435
+ for (const lineText of raw.split("\n")) {
4436
+ if (!lineText.trim()) continue;
4437
+ let line;
4438
+ try {
4439
+ line = JSON.parse(lineText);
4440
+ } catch {
4441
+ continue;
4442
+ }
4443
+ if (line.type === "response_item") {
4444
+ const item = line.payload;
4445
+ if (!item) continue;
4446
+ if (item.type === "message" && (item.role === "user" || item.role === "assistant") && item.channel !== "analysis") {
4447
+ const text2 = blockText(item.content).trim();
4448
+ if (item.role === "user" ? isHumanText(text2) : !!text2) {
4449
+ turns.push({ role: item.role, text: text2 });
4450
+ if (item.role === "user") userMessages++;
4451
+ }
4452
+ } else if (item.type === "function_call" || item.type === "custom_tool_call") {
4453
+ let input = item.input ?? item.arguments;
4454
+ if (typeof input === "string") {
4455
+ try {
4456
+ input = JSON.parse(input);
4457
+ } catch {
4458
+ }
4459
+ }
4460
+ turns.push({ role: "tool", tool: item.name || "tool", text: summarizeInput(input), input });
4461
+ toolCalls++;
4462
+ } else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
4463
+ const outputs = Array.isArray(item.output) ? item.output.filter((part) => part && typeof part === "object" && typeof part.text === "string").map((part) => part.text) : [item.output];
4464
+ for (const output of outputs) {
4465
+ for (const result of codexToolResults(output)) {
4466
+ if (!result.text) continue;
4467
+ turns.push({ role: "tool", tool: "result", ...result });
4468
+ if (result.isError) toolFailures++;
4469
+ }
4470
+ }
4471
+ }
4472
+ continue;
4473
+ }
4474
+ if (line.type !== "user" && line.type !== "assistant") continue;
4475
+ if (line.isMeta) continue;
4476
+ const content = line.message?.content;
4477
+ if (typeof content === "string") {
4478
+ if (line.type === "user" && isHumanText(content)) {
4479
+ turns.push({ role: "user", text: content });
4480
+ userMessages++;
4481
+ } else if (line.type === "assistant" && content.trim()) {
4482
+ turns.push({ role: "assistant", text: content });
4483
+ }
4484
+ continue;
4485
+ }
4486
+ if (!Array.isArray(content)) continue;
4487
+ for (const block2 of content) {
4488
+ if (block2.type === "text" && block2.text) {
4489
+ if (line.type === "user") {
4490
+ if (!isHumanText(block2.text)) continue;
4491
+ userMessages++;
4492
+ turns.push({ role: "user", text: block2.text });
4493
+ } else {
4494
+ turns.push({ role: "assistant", text: block2.text });
4495
+ }
4496
+ } else if (block2.type === "tool_use") {
4497
+ toolCalls++;
4498
+ turns.push({ role: "tool", tool: block2.name || "tool", text: summarizeInput(block2.input), input: block2.input });
4499
+ } else if (block2.type === "tool_result") {
4500
+ const text2 = blockText(block2.content);
4501
+ if (block2.is_error) toolFailures++;
4502
+ turns.push({ role: "tool", tool: "result", text: text2, isError: !!block2.is_error });
4503
+ }
4504
+ }
4505
+ }
4506
+ return { turns, userMessages, toolCalls, toolFailures };
4507
+ }
4508
+ function isHumanText(text2) {
4509
+ const t = text2.trim();
4510
+ if (!t) return false;
4511
+ if (t.startsWith(BANNER)) return false;
4512
+ if (/^<(?:command-name|command-message|local-command-stdout|system-reminder|bash-input|bash-stdout)/.test(t)) return false;
4513
+ return true;
4514
+ }
4515
+ function blockText(content) {
4516
+ if (typeof content === "string") return content;
4517
+ if (Array.isArray(content)) {
4518
+ return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
4519
+ }
4520
+ return content == null ? "" : JSON.stringify(content);
4521
+ }
4522
+ function codexToolResults(output, depth = 0) {
4523
+ const raw = blockText(output);
4524
+ let envelope = output;
4525
+ if (typeof output === "string") {
4526
+ try {
4527
+ envelope = JSON.parse(output);
4528
+ } catch {
4529
+ }
4530
+ }
4531
+ if (depth > 6) return [{ text: raw, pending: true }];
4532
+ if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
4533
+ if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
4534
+ const result = envelope;
4535
+ if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
4536
+ const pending = !Number.isInteger(result.exit_code);
4537
+ return [{
4538
+ text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
4539
+ ${result.output}`,
4540
+ isError: !pending && result.exit_code !== 0,
4541
+ pending
4542
+ }];
4543
+ }
4544
+ if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
4545
+ if (result.status === "rejected") return [{ text: raw, isError: true }];
4546
+ }
4547
+ return [{
4548
+ text: raw,
4549
+ isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
4550
+ pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
4551
+ }];
4552
+ }
4553
+ function summarizeInput(input) {
4554
+ if (input == null) return "";
4555
+ if (typeof input !== "object") return String(input);
4556
+ const obj = input;
4557
+ for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
4558
+ if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
4559
+ }
4560
+ return JSON.stringify(obj);
4561
+ }
4562
+ function bounded(text2, maxChars) {
4563
+ if (text2.length <= maxChars) return text2;
4564
+ const marker = "\n[... middle omitted ...]\n";
4565
+ if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
4566
+ const head = Math.floor((maxChars - marker.length) / 2);
4567
+ return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
4568
+ }
4569
+ function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
4570
+ const parts = [];
4571
+ for (const t of facts.turns.slice(from)) {
4572
+ const text2 = redact2(t.text);
4573
+ if (t.role === "user") parts.push(`[USER]
4574
+ ${bounded(text2, maxChars)}`);
4575
+ else if (t.role === "assistant") parts.push(`[ASSISTANT]
4576
+ ${bounded(text2, maxChars)}`);
4577
+ else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
4578
+ ${bounded(text2, toolResultChars)}`);
4579
+ else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
4580
+ }
4581
+ return bounded(parts.join("\n\n"), maxChars);
4582
+ }
4583
+ function fullTrace(facts, { from = 0, to } = {}) {
4584
+ return facts.turns.slice(from, to).map((t) => {
4585
+ if (t.role === "user") return `[USER]
4586
+ ${redact2(t.text)}`;
4587
+ if (t.role === "assistant") return `[ASSISTANT]
4588
+ ${redact2(t.text)}`;
4589
+ if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
4590
+ ${redact2(t.text)}`;
4591
+ const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
4592
+ return `[TOOL ${t.tool}]
4593
+ ${redact2(input)}`;
4594
+ });
4595
+ }
4596
+ function traceChunks(turns, maxBytes = 1e6) {
4597
+ const out = [];
4598
+ let cur = "";
4599
+ let size = 0;
4600
+ turns.forEach((turn, i) => {
4601
+ let piece = (i ? "\n\n" : "") + turn;
4602
+ let bytes = jsonBytes(piece);
4603
+ if (size + bytes <= maxBytes) {
4604
+ cur += piece;
4605
+ size += bytes;
4606
+ return;
4607
+ }
4608
+ if (cur) out.push(cur);
4609
+ while (bytes > maxBytes) {
4610
+ let n = Math.min(piece.length, maxBytes);
4611
+ 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);
4612
+ const code2 = piece.charCodeAt(n - 1);
4613
+ if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
4614
+ const head = piece.slice(0, n);
4615
+ out.push(head);
4616
+ piece = piece.slice(n);
4617
+ bytes -= jsonBytes(head);
4618
+ }
4619
+ cur = piece;
4620
+ size = bytes;
4621
+ });
4622
+ if (cur) out.push(cur);
4623
+ return out;
4624
+ }
4625
+ function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
4626
+ const facts = parseTranscript(file);
4627
+ const visible = facts.turns.filter((t) => t.role !== "tool");
4628
+ const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
4629
+ const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
4630
+ return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
4631
+ }
4632
+ var BANNER, jsonBytes;
4633
+ var init_transcript = __esm({
4634
+ "src/transcript.ts"() {
4635
+ "use strict";
4636
+ init_redact2();
4637
+ init_util();
4638
+ BANNER = "## Recalled lessons (Bunshin)";
4639
+ jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
4640
+ }
4641
+ });
4642
+
4600
4643
  // src/v2/repo.ts
4601
4644
  import fs8 from "node:fs";
4602
4645
  import path7 from "node:path";
@@ -5307,8 +5350,8 @@ __export(sync_exports, {
5307
5350
  import fs12 from "node:fs";
5308
5351
  import path9 from "node:path";
5309
5352
  function episodePayload(e) {
5310
- const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, ...rest } = e;
5311
- void _o, _u, _s, _rs, _rl;
5353
+ const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, transcriptPath: _tp, ...rest } = e;
5354
+ void _o, _u, _s, _rs, _rl, _tp;
5312
5355
  return rest;
5313
5356
  }
5314
5357
  function suggestionPayload(s) {
@@ -5316,12 +5359,25 @@ function suggestionPayload(s) {
5316
5359
  void _o, _st, _d, _u, _t, _su;
5317
5360
  return rest;
5318
5361
  }
5362
+ async function uploadTrace(e, call2) {
5363
+ if (!carriesTrace(e)) return;
5364
+ const chunks = traceChunks(fullTrace(parseTranscript(e.transcriptPath), { from: e.transcriptFrom, to: e.turns }));
5365
+ if (!chunks.length) return;
5366
+ for (let attempt = 0; ; attempt++) {
5367
+ try {
5368
+ for (const [seq, text2] of chunks.entries()) await call2("PUT", `/episodes/${encodeURIComponent(e.id)}/transcript`, { seq, text: text2, done: seq === chunks.length - 1 });
5369
+ return;
5370
+ } catch (err) {
5371
+ if (attempt || !(err instanceof Error && err.message.includes("trace_gap"))) throw err;
5372
+ }
5373
+ }
5374
+ }
5319
5375
  async function syncV2(store, opts = {}) {
5320
5376
  const v2 = new V2Store(store);
5321
5377
  const config = store.config();
5322
5378
  const cwd = opts.cwd ?? path9.dirname(store.root);
5323
- const report = { offline: !canCall(config), uploadedEpisodes: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
5324
- const deadline = Date.now() + (opts.budgetMs ?? 6e4);
5379
+ const report = { offline: !canCall(config), uploadedEpisodes: 0, deferredTraces: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
5380
+ const deadline = Date.now() + (opts.budgetMs ?? 24e4);
5325
5381
  if (report.offline) {
5326
5382
  report.settled = settleOverlays(v2, v2.readBundle(), cwd).actions;
5327
5383
  return report;
@@ -5344,8 +5400,13 @@ async function syncV2(store, opts = {}) {
5344
5400
  }
5345
5401
  for (const e of v2.listEpisodes().filter((x) => !x.uploaded)) {
5346
5402
  if (Date.now() >= deadline) break;
5403
+ if (opts.traces === false && carriesTrace(e)) {
5404
+ report.deferredTraces++;
5405
+ continue;
5406
+ }
5347
5407
  try {
5348
5408
  const res = await call2("POST", "/episodes", { episode: episodePayload(e) });
5409
+ await uploadTrace(e, call2);
5349
5410
  if (res && (typeof res.reflectedTurns === "number" || Array.isArray(res.lessons)) && v2.readSession(e.sessionId)) {
5350
5411
  v2.updateSession(e.sessionId, "", (s) => {
5351
5412
  if (typeof res.reflectedTurns === "number") s.serverReflectedTurns = Math.max(s.serverReflectedTurns ?? 0, res.reflectedTurns);
@@ -5469,10 +5530,11 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
5469
5530
  const res = await v2api(config, "POST", "/sections/index", { repo: index.repo, ...commit ? { commit } : {}, ownerTeam: index.ownerTeam, docs }, { deadline, timeoutMs: 3e4 });
5470
5531
  return typeof res.indexed === "number" ? res.indexed : docs.length;
5471
5532
  }
5472
- var isLocalId;
5533
+ var isLocalId, carriesTrace;
5473
5534
  var init_sync = __esm({
5474
5535
  "src/v2/sync.ts"() {
5475
5536
  "use strict";
5537
+ init_transcript();
5476
5538
  init_api();
5477
5539
  init_apply();
5478
5540
  init_deploy();
@@ -5480,6 +5542,7 @@ var init_sync = __esm({
5480
5542
  init_store2();
5481
5543
  init_view();
5482
5544
  isLocalId = (id) => id.startsWith("local:");
5545
+ carriesTrace = (e) => e.upload !== "header-only" && !e.transcript && e.transcriptFrom !== void 0 && !!e.transcriptPath && e.turns !== void 0;
5483
5546
  }
5484
5547
  });
5485
5548
 
@@ -6029,9 +6092,10 @@ async function onSessionStart(ctx) {
6029
6092
  try {
6030
6093
  const { syncV2: syncV22 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
6031
6094
  const report = await Promise.race([
6032
- syncV22(ctx.store, { budgetMs: budget, cwd: ctx.cwd }),
6095
+ syncV22(ctx.store, { budgetMs: budget, cwd: ctx.cwd, traces: false }),
6033
6096
  new Promise((r) => setTimeout(() => r(null), budget + 200).unref?.())
6034
6097
  ]);
6098
+ if (report?.deferredTraces) ctx.detach(["--root", ctx.store.root, "sync", "--quiet"], { cwd: ctx.cwd });
6035
6099
  if (!report || report.errors.length) v2.log("v2-session-start-sync", { session: id, errors: report?.errors ?? ["budget exceeded"] });
6036
6100
  } catch {
6037
6101
  }
@@ -6115,6 +6179,12 @@ function hintLine(titles, harness) {
6115
6179
  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>`.';
6116
6180
  return `Bunshin: possibly relevant library sections: ${quoted}. ${how}`;
6117
6181
  }
6182
+ function invokedSkill(prompt, sections) {
6183
+ const m = /^\s*(?:<command-name>\s*)?\/([\w.:-]+)/.exec(prompt);
6184
+ if (!m) return void 0;
6185
+ const name = m[1].split(":").pop().toLowerCase();
6186
+ return sections.find((s) => s.skill && s.skill.name.toLowerCase() === name);
6187
+ }
6118
6188
  async function onPrompt(ctx) {
6119
6189
  const prompt = ctx.input.prompt ?? "";
6120
6190
  if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
@@ -6135,6 +6205,23 @@ async function onPrompt(ctx) {
6135
6205
  const config = ctx.store.config();
6136
6206
  const view = localView(v2, config, ctx.cwd);
6137
6207
  const injected = inContext(session);
6208
+ const named = invokedSkill(prompt, view.sections);
6209
+ if (named) {
6210
+ const why = `you ran /${named.skill.name}`;
6211
+ const deployed = deployEnabled(config, view.ctx);
6212
+ const fresh2 = !injected.includes(named.id);
6213
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
6214
+ addUnique(s.served, named.id);
6215
+ addUnique(s.loaded, named.id);
6216
+ markInContext(s, named.id);
6217
+ });
6218
+ v2.log("v2-invoked", { session: session.sessionId, section: named.id });
6219
+ updateFeed(v2, session.sessionId, (f) => {
6220
+ addServed(f, { id: named.id, title: named.title, kind: named.kind, skill: named.skill.name, how: "load", why });
6221
+ addEvent(f, "served", `loaded ${label({ skill: named.skill.name, title: named.title, kind: named.kind })} \u2014 ${why}`);
6222
+ });
6223
+ return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6224
+ }
6138
6225
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6139
6226
  const remote = await remoteAuto(ctx, prompt, injected);
6140
6227
  if (remote) {
@@ -6320,7 +6407,8 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6320
6407
  upload,
6321
6408
  turns,
6322
6409
  reflectedTurns: from,
6323
- ...upload !== "header-only" && turns > from ? { transcript: digest(scan.facts, { from, maxChars: upload === "full" ? 2e5 : 6e4 }), transcriptFrom: from } : {},
6410
+ // The whole trace is built (and redacted) by sync at upload time, outside the hook's budget.
6411
+ ...upload !== "header-only" && turns > from && (transcriptPath ?? session.transcriptPath) ? { transcriptFrom: from, transcriptPath: transcriptPath ?? session.transcriptPath } : {},
6324
6412
  ...calls.length ? { calls } : {},
6325
6413
  ...snippets.length ? { snippets } : {},
6326
6414
  ...outcome ? { outcome } : {},
@@ -6437,7 +6525,6 @@ var init_hooks = __esm({
6437
6525
  "use strict";
6438
6526
  init_v22();
6439
6527
  init_redact2();
6440
- init_transcript();
6441
6528
  init_api();
6442
6529
  init_deploy();
6443
6530
  init_live();
@@ -7298,7 +7385,7 @@ async function setupRobot(opts) {
7298
7385
  v2.writeContext(context);
7299
7386
  let synced = false;
7300
7387
  if (registered) {
7301
- const report = await syncV2(store, { cwd: opts.cwd, budgetMs: 15e3 });
7388
+ const report = await syncV2(store, { cwd: opts.cwd, budgetMs: 15e3, traces: false });
7302
7389
  synced = report.bundle === "fetched";
7303
7390
  errors.push(...report.errors);
7304
7391
  }
@@ -3,7 +3,14 @@
3
3
  Bunshin's hooks talk to the model; this mod shows the person what they did. It draws the CLI's
4
4
  session feed (`.bunshin/v2/ui/<session>.json`, written by `packages/cli/src/v2/ui.ts`):
5
5
 
6
- - **above the prompt**: `◆ bunshin show-me skill picked by Jev (98%) lib@207 · 3 served · 1 learned`
6
+ - **above the prompt**: always one row, never wrapped. It shows the document served and why on the left, the
7
+ session's counts on the right, and reviews waiting in yellow:
8
+
9
+ ◆ bunshin show-me skill picked by Jev (98%) lib@207 · 3 served · 1 learned
10
+ ◇ bunshin maybe agent-grid · manage-agent-fleet skill lib@209 · 7 to review
11
+
12
+ On narrow terminals it drops whole pieces, least useful first: the counts, then the reason. It never drops
13
+ the reviews waiting, and only then trims the name with "…".
7
14
  - **`/bunshin`**: a pane with everything served this session and why, the lessons taken from it,
8
15
  recent events, and Learn now (`l`), Sync (`s`), Review (`r`)
9
16
  - **toasts**: library updates that reached the session, lessons learned, reviews waiting, the
@@ -224,22 +224,69 @@ function counts(feed) {
224
224
  ].filter(Boolean);
225
225
  }
226
226
 
227
- /** The band above the prompt: ◆ bunshin <latest served> why · counts. One line, two when narrow. */
227
+ /** The reason in the band's few columns: the feed's sentence, without its filler. */
228
+ const shortWhy = (why) =>
229
+ String(why ?? "")
230
+ .replace(/^both embedders agree, and Jev$/, "embedders + Jev agree")
231
+ .replace(/^both embedders agree$/, "embedders agree")
232
+ .replace(/, over the embedders$/, "");
233
+
234
+ /**
235
+ * The band above the prompt, always exactly one row (it never wraps into fragments):
236
+ *
237
+ * ◆ bunshin complete-ticket skill embedders agree lib@209 · 1 served · 7 to review
238
+ * ◇ bunshin maybe agent-grid · manage-agent-fleet skill lib@209 · 7 to review
239
+ *
240
+ * What does not fit is dropped whole, least useful first: the quiet counts, then the reason (never
241
+ * the reviews waiting, in yellow); only then does the name end in "…".
242
+ */
228
243
  function band(Box, Text, feed, columns) {
229
244
  const latest = feed.served.find((s) => s.how !== "hint");
230
245
  const hint = feed.served[0]?.how === "hint" ? feed.served.filter((s) => s.how === "hint" && s.at === feed.served[0].at) : [];
231
- const meta = counts(feed).join(" · ");
232
- const parts = [Text({ color: BRAND, bold: true, children: "◆ bunshin" })];
246
+ const served = feed.served.filter((s) => s.how !== "hint").length;
247
+ const quiet = [feed.library ? `lib@${feed.library}` : "", served ? `${served} served` : "", feed.lessons.length ? `${feed.lessons.length} learned` : ""].filter(Boolean).join(" · ");
248
+ const review = feed.review ? `${feed.review} to review` : "";
249
+ const name = latest ? label(latest) : hint.length ? `maybe ${hint.map(label).join(" · ")}` : "listening";
250
+ const why = latest?.why ? ` ${shortWhy(latest.why)}` : "";
251
+ // The cells each piece takes: "◆ bunshin " (11), the name, the reason; the counts plus a 2-cell gap.
252
+ const room = columns - 2;
253
+ const fits = (withWhy, withQuiet) => {
254
+ const right = [withQuiet ? quiet : "", review].filter(Boolean).join(" · ");
255
+ return 11 + name.length + (withWhy ? why.length : 0) + (right ? right.length + 2 : 0) <= room;
256
+ };
257
+ // Keep, in order: the name, the reviews waiting, the reason, the quiet counts.
258
+ const showWhy = !!why && fits(true, false);
259
+ const showQuiet = !!quiet && fits(showWhy, true);
260
+
261
+ const left = [];
233
262
  if (latest) {
234
- parts.push(Text({ bold: true, children: ` ${clip(label(latest), 40)}` }));
235
- if (columns >= 70) parts.push(Text({ dimColor: true, children: ` ${clip(latest.why, Math.max(12, columns - 70))}` }));
263
+ left.push(Text({ color: BRAND, bold: true, children: "◆ bunshin " }));
264
+ left.push(Text({ bold: true, children: label(latest) }));
265
+ if (showWhy) left.push(Text({ dimColor: true, children: why }));
236
266
  } else if (hint.length) {
237
- parts.push(Text({ dimColor: true, children: ` maybe: ${clip(hint.map(label).join(", "), Math.max(20, columns - 40))}` }));
267
+ left.push(Text({ color: BRAND, children: "◇ bunshin " }));
268
+ left.push(Text({ dimColor: true, italic: true, children: "maybe " }));
269
+ left.push(Text({ dimColor: true, children: hint.map(label).join(" · ") }));
238
270
  } else {
239
- parts.push(Text({ dimColor: true, children: " listening" }));
271
+ left.push(Text({ color: BRAND, children: "◇ bunshin " }));
272
+ left.push(Text({ dimColor: true, italic: true, children: "listening" }));
273
+ }
274
+ const right = [];
275
+ if (showQuiet) right.push(Text({ dimColor: true, children: quiet }));
276
+ if (review) {
277
+ if (right.length) right.push(Text({ dimColor: true, children: " · " }));
278
+ right.push(Text({ color: "yellow", children: review }));
240
279
  }
241
- if (meta && columns >= 50) parts.push(Text({ dimColor: true, children: ` ${meta}` }));
242
- return Box({ flexDirection: "row", paddingX: 1, children: parts });
280
+ return Box({
281
+ flexDirection: "row",
282
+ justifyContent: "space-between",
283
+ width: columns,
284
+ paddingX: 1,
285
+ children: [
286
+ Box({ flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [Text({ wrap: "truncate-end", children: left })] }),
287
+ ...(right.length ? [Box({ flexShrink: 0, marginLeft: 2, children: [Text({ wrap: "truncate-end", children: right })] })] : []),
288
+ ],
289
+ });
243
290
  }
244
291
 
245
292
  function plainSummary(feed) {
@@ -268,14 +315,16 @@ function pane({ Box, Text, Button }, feed, panel, columns, rows) {
268
315
  body.push(heading(Text, "Served this session"));
269
316
  const served = feed.served.slice(0, Math.max(3, Math.floor(rows / 3)));
270
317
  if (!served.length) body.push(Text({ dimColor: true, children: " nothing yet" }));
318
+ // One line per row, never wrapped into fragments: time and how, the document, why.
271
319
  for (const s of served) {
272
320
  body.push(
273
- Box({
274
- flexDirection: "row",
321
+ Text({
322
+ wrap: "truncate-end",
275
323
  children: [
276
- Text({ dimColor: true, children: ` ${hhmm(s.at)} ${s.how.padEnd(4)} ` }),
277
- Text({ bold: s.how !== "hint", dimColor: s.how === "hint", children: clip(label(s), 36) }),
278
- Text({ dimColor: true, children: ` ${clip(s.why, Math.max(10, width - 52))}` }),
324
+ Text({ dimColor: true, children: ` ${hhmm(s.at)} ` }),
325
+ Text({ color: s.how === "hint" ? undefined : BRAND, dimColor: s.how === "hint", children: `${s.how === "hint" ? "◇" : "◆"} ` }),
326
+ Text({ bold: s.how !== "hint", dimColor: s.how === "hint", children: label(s) }),
327
+ Text({ dimColor: true, children: s.why ? ` ${shortWhy(s.why)}` : s.how === "hint" ? " maybe" : "" }),
279
328
  ],
280
329
  }),
281
330
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,15 +16,15 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "dependencies": {
19
- "@anthropic-ai/sdk": "^0.127.0",
19
+ "@anthropic-ai/sdk": "^0.129.0",
20
20
  "yaml": "^2.8.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/node": "^22.15.0",
23
+ "@types/node": "^26.6.3",
24
24
  "esbuild": "^0.28.1",
25
- "tsx": "^4.20.0",
26
- "typescript": "^5.8.0",
27
- "vitest": "^3.2.0",
25
+ "tsx": "^4.23.15",
26
+ "typescript": "^7.0.2",
27
+ "vitest": "^5.0.2",
28
28
  "@bunshin/core": "0.1.0"
29
29
  },
30
30
  "license": "MIT",