@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 +313 -226
- package/dist/claude-code-ui/README.md +8 -1
- package/dist/claude-code-ui/hooks/bunshin-ui.mjs +63 -14
- package/package.json +6 -6
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
3609
|
+
fs5.writeFileSync(file, String(process.pid), { flag: "wx" });
|
|
3788
3610
|
return () => {
|
|
3789
3611
|
try {
|
|
3790
|
-
|
|
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(
|
|
3799
|
-
const age = Date.now() -
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 && !
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
3889
|
-
|
|
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
|
|
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
|
-
|
|
3731
|
+
fs6.chmodSync(file, mode === "x" ? 493 : 420);
|
|
3910
3732
|
} catch {
|
|
3911
3733
|
}
|
|
3912
3734
|
}
|
|
3913
3735
|
function removeFile(file) {
|
|
3914
3736
|
try {
|
|
3915
|
-
|
|
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 (
|
|
3924
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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() -
|
|
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))
|
|
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 || !
|
|
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 (
|
|
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 =
|
|
4219
|
+
const cur = fs6.readlinkSync(link);
|
|
4398
4220
|
if (cur !== t.abs) {
|
|
4399
|
-
|
|
4400
|
-
|
|
4221
|
+
fs6.unlinkSync(link);
|
|
4222
|
+
fs6.symlinkSync(t.abs, link);
|
|
4401
4223
|
}
|
|
4402
4224
|
} catch {
|
|
4403
4225
|
try {
|
|
4404
|
-
if (!
|
|
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
|
-
|
|
4240
|
+
fs6.lstatSync(link);
|
|
4419
4241
|
} catch {
|
|
4420
4242
|
ensureDir(path6.dirname(link));
|
|
4421
4243
|
ensureDir(p.claudeSkills);
|
|
4422
4244
|
try {
|
|
4423
|
-
|
|
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 =
|
|
4433
|
-
if (!
|
|
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 (
|
|
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 ??
|
|
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
|
-
|
|
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**:
|
|
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
|
|
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
|
|
232
|
-
const
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
274
|
-
|
|
321
|
+
Text({
|
|
322
|
+
wrap: "truncate-end",
|
|
275
323
|
children: [
|
|
276
|
-
Text({ dimColor: true, children: ` ${hhmm(s.at)}
|
|
277
|
-
Text({
|
|
278
|
-
Text({
|
|
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.
|
|
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.
|
|
19
|
+
"@anthropic-ai/sdk": "^0.129.0",
|
|
20
20
|
"yaml": "^2.8.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
23
|
+
"@types/node": "^26.6.3",
|
|
24
24
|
"esbuild": "^0.28.1",
|
|
25
|
-
"tsx": "^4.
|
|
26
|
-
"typescript": "^
|
|
27
|
-
"vitest": "^
|
|
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",
|