@bunshin-cc/bunshin 0.4.4 → 0.4.6
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 +4652 -4106
- package/dist/claude-code-ui/.claude-plugin/plugin.json +7 -0
- package/dist/claude-code-ui/README.md +29 -0
- package/dist/claude-code-ui/hooks/bunshin-ui.mjs +310 -0
- package/dist/claude-code-ui/hooks/hooks.json +4 -0
- package/dist/claude-code-ui/types/index.d.ts +31 -0
- package/dist/pi-extension.js +11 -1
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bunshin-ui",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Bunshin for the person at the terminal: what the library served and why, live library changes, lessons from the session, reviews waiting. A band above the prompt, a /bunshin pane, toasts.",
|
|
5
|
+
"author": { "name": "Bunshin", "url": "https://bunshin.cc" },
|
|
6
|
+
"types": "./types/index.d.ts"
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# bunshin-ui: a Claude Code mod
|
|
2
|
+
|
|
3
|
+
Bunshin's hooks talk to the model; this mod shows the person what they did. It draws the CLI's
|
|
4
|
+
session feed (`.bunshin/v2/ui/<session>.json`, written by `packages/cli/src/v2/ui.ts`):
|
|
5
|
+
|
|
6
|
+
- **above the prompt**: `◆ bunshin show-me skill picked by Jev (98%) lib@207 · 3 served · 1 learned`
|
|
7
|
+
- **`/bunshin`**: a pane with everything served this session and why, the lessons taken from it,
|
|
8
|
+
recent events, and Learn now (`l`), Sync (`s`), Review (`r`)
|
|
9
|
+
- **toasts**: library updates that reached the session, lessons learned, reviews waiting, the
|
|
10
|
+
`/learn` nudge
|
|
11
|
+
|
|
12
|
+
While it runs it writes a heartbeat beside the feed (`<session>.mod.json`), and the CLI stops
|
|
13
|
+
printing its one-line fallback, so nothing is said twice. It draws nothing outside a project
|
|
14
|
+
connected to Bunshin.
|
|
15
|
+
|
|
16
|
+
Install (the CLI does this itself the first time a connected Claude Code session starts, and
|
|
17
|
+
again after each upgrade):
|
|
18
|
+
|
|
19
|
+
bunshin ui install # local marketplace ~/.bunshin/claude-code-ui, user scope
|
|
20
|
+
bunshin ui status
|
|
21
|
+
bunshin ui disable --harness claude-code # neither the mod's drawing nor the lines
|
|
22
|
+
|
|
23
|
+
Develop: `claude --plugin-dir adapters/claude-code-ui` hot-reloads on save;
|
|
24
|
+
`claude plugin validate adapters/claude-code-ui`; `claude plugin test adapters/claude-code-ui`.
|
|
25
|
+
Needs Claude Code 2.1.287+ with mods enabled for the account.
|
|
26
|
+
|
|
27
|
+
Other harnesses read the same feed: pi draws it as a widget above the editor (`adapters/pi`);
|
|
28
|
+
Codex gets nothing until it shows hook messages (`bunshin ui enable --harness codex` turns on the
|
|
29
|
+
plain lines); any terminal can follow it with `bunshin ui --watch`.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// bunshin-ui: Bunshin for the person at the terminal.
|
|
2
|
+
//
|
|
3
|
+
// Bunshin's hooks talk to the model (additionalContext), which the person never sees. The CLI
|
|
4
|
+
// keeps the person's side in a feed, .bunshin/v2/ui/<session>.json (packages/cli/src/v2/ui.ts):
|
|
5
|
+
// what the library served and why, library changes that reached the session, lessons taken,
|
|
6
|
+
// checkpoints, reviews waiting. This mod draws it:
|
|
7
|
+
//
|
|
8
|
+
// above the prompt one line: the latest document served and why, then the session's counts
|
|
9
|
+
// /bunshin a pane: everything served, the lessons, recent events; Learn now, Sync, Review
|
|
10
|
+
// toasts library updates, lessons learned, reviews waiting, the /learn nudge
|
|
11
|
+
//
|
|
12
|
+
// While it runs it writes a heartbeat beside the feed (<session>.mod.json); the CLI then stops
|
|
13
|
+
// printing its plain one-line fallback (systemMessage), so nothing is said twice.
|
|
14
|
+
|
|
15
|
+
const FEED = { plugin: "bunshin-ui", key: "feed" };
|
|
16
|
+
const WHERE = { plugin: "bunshin-ui", key: "where" };
|
|
17
|
+
const SEEN = { plugin: "bunshin-ui", key: "seen" };
|
|
18
|
+
const PANEL = { plugin: "bunshin-ui", key: "panel" };
|
|
19
|
+
|
|
20
|
+
const PANE_ID = "bunshin";
|
|
21
|
+
const POLL_MS = 15_000;
|
|
22
|
+
const BEAT_MS = 60_000;
|
|
23
|
+
const TOAST_KINDS = new Set(["live", "learned", "review", "nudge", "offline"]);
|
|
24
|
+
const BRAND = "magenta";
|
|
25
|
+
|
|
26
|
+
// Session-only, rebuilt by session.start after a hot reload: closures over that session's $, for
|
|
27
|
+
// timers and button presses (which run outside any hook).
|
|
28
|
+
let host = null;
|
|
29
|
+
let poll = null;
|
|
30
|
+
let lastBeat = 0;
|
|
31
|
+
let refreshing = false;
|
|
32
|
+
|
|
33
|
+
export function register(on) {
|
|
34
|
+
on("session.start", async ($, e, next) => {
|
|
35
|
+
const result = await next(e);
|
|
36
|
+
host = {
|
|
37
|
+
refresh: () => refresh($),
|
|
38
|
+
act: (what) => act($, what),
|
|
39
|
+
review: () => showReview($),
|
|
40
|
+
session: () => showSession($),
|
|
41
|
+
};
|
|
42
|
+
await locate($);
|
|
43
|
+
await refresh($, { first: true });
|
|
44
|
+
try {
|
|
45
|
+
await $.command.register({ name: "bunshin", description: "Bunshin: what the library served this session, lessons, reviews", argumentHint: "[learn|sync|review]" });
|
|
46
|
+
} catch {
|
|
47
|
+
/* a command of that name exists (the bunshin plugin's own): the pane still opens from it below */
|
|
48
|
+
}
|
|
49
|
+
poll?.cancel();
|
|
50
|
+
poll = $.clock.every(POLL_MS, () => {
|
|
51
|
+
if (host) void host.refresh().catch(() => undefined);
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// After each Bunshin hook has run (the settings hooks are below us in the chain), read the feed.
|
|
57
|
+
on("classic.SessionStart", after);
|
|
58
|
+
on("classic.UserPromptSubmit", after);
|
|
59
|
+
on("classic.PreToolUse", after);
|
|
60
|
+
on("classic.Stop", after);
|
|
61
|
+
|
|
62
|
+
// /clear and /resume change the session.
|
|
63
|
+
on("command.run", { command: ["clear", "resume"] }, async ($, e, next) => {
|
|
64
|
+
const result = await next(e);
|
|
65
|
+
await locate($);
|
|
66
|
+
void refresh($, { first: true }).catch(() => undefined);
|
|
67
|
+
return result;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
on("command.run", { command: "bunshin" }, async ($, e, next) => {
|
|
71
|
+
const { value: where } = await $.state.get(WHERE);
|
|
72
|
+
if (!where) return { text: "Bunshin is not connected here: run `bunshin setup` in the project (or a folder above it)." };
|
|
73
|
+
const arg = String(e.args ?? "").trim().split(/\s+/)[0];
|
|
74
|
+
if (arg === "learn" || arg === "sync") {
|
|
75
|
+
void act($, arg).catch(() => undefined);
|
|
76
|
+
} else if (arg === "review") {
|
|
77
|
+
await showReview($);
|
|
78
|
+
} else {
|
|
79
|
+
await $.state.set(PANEL, { view: "session", busy: "", output: "" });
|
|
80
|
+
}
|
|
81
|
+
const opened = await $.ui.open({ id: PANE_ID, title: "Bunshin", focus: true, closeOnEscape: true, holdToasts: true });
|
|
82
|
+
if (opened && opened.isPlaced === false) {
|
|
83
|
+
const { value: feed } = await $.state.get(FEED);
|
|
84
|
+
return { text: feed ? plainSummary(feed) : "Bunshin: nothing yet this session." };
|
|
85
|
+
}
|
|
86
|
+
return {};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
|
|
90
|
+
if (e.props.hasSurvey) return next(e);
|
|
91
|
+
const { value: feed } = await $.state.get(FEED);
|
|
92
|
+
if (!feed) return next(e);
|
|
93
|
+
const { Box, Text } = $.ui.resolve(e);
|
|
94
|
+
return band(Box, Text, feed, e.props.bodyColumns);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
on("ui.render", { component: "Pane" }, async ($, e, next) => {
|
|
98
|
+
if (e.requestId !== PANE_ID || e.surface === "mobile") return next(e);
|
|
99
|
+
const { value: feed } = await $.state.get(FEED);
|
|
100
|
+
const { value: panel = { view: "session", busy: "", output: "" } } = await $.state.get(PANEL);
|
|
101
|
+
const { Box, Text, Button } = $.ui.resolve(e);
|
|
102
|
+
return pane({ Box, Text, Button }, feed, panel, e.props.bodyColumns, e.props.scroll?.bodyRows ?? 30);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** After one of Bunshin's settings hooks ran (they sit below the mods in the chain): read the feed. */
|
|
107
|
+
async function after($, e, next) {
|
|
108
|
+
const result = await next(e);
|
|
109
|
+
void refresh($).catch(() => undefined);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── data ────────────────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
const safe = (id) => String(id).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "session";
|
|
116
|
+
|
|
117
|
+
/** The connected project above the session's folder: the nearest one with .bunshin/config.json. */
|
|
118
|
+
async function locate($) {
|
|
119
|
+
let where = null;
|
|
120
|
+
try {
|
|
121
|
+
const sessionId = await $.session.id();
|
|
122
|
+
let dir = await $.session.cwd();
|
|
123
|
+
for (let i = 0; i < 40 && dir; i++) {
|
|
124
|
+
if (await $.fs.exists(`${dir}/.bunshin/config.json`)) {
|
|
125
|
+
where = { store: `${dir}/.bunshin/v2`, project: dir, sessionId };
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const up = dir.replace(/\/[^/]+\/?$/, "");
|
|
129
|
+
if (!up || up === dir) break;
|
|
130
|
+
dir = up;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
where = null;
|
|
134
|
+
}
|
|
135
|
+
await $.state.set(WHERE, where);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function refresh($, { first = false } = {}) {
|
|
139
|
+
if (refreshing) return;
|
|
140
|
+
refreshing = true;
|
|
141
|
+
try {
|
|
142
|
+
const { value: where } = await $.state.get(WHERE);
|
|
143
|
+
if (!where) return;
|
|
144
|
+
const file = `${where.store}/ui/${safe(where.sessionId)}.json`;
|
|
145
|
+
let feed = null;
|
|
146
|
+
try {
|
|
147
|
+
if (await $.fs.exists(file)) feed = JSON.parse(await $.fs.read(file));
|
|
148
|
+
} catch {
|
|
149
|
+
feed = null;
|
|
150
|
+
}
|
|
151
|
+
const now = await $.clock.now();
|
|
152
|
+
if (now - lastBeat >= BEAT_MS) {
|
|
153
|
+
lastBeat = now;
|
|
154
|
+
await $.fs.write(`${where.store}/ui/${safe(where.sessionId)}.mod.json`, JSON.stringify({ at: new Date(now).toISOString(), mod: "bunshin-ui" })).catch(() => undefined);
|
|
155
|
+
}
|
|
156
|
+
if (!feed || feed.v !== 1) {
|
|
157
|
+
await $.state.set(FEED, null);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { value: prev } = await $.state.get(FEED);
|
|
161
|
+
if (!prev || prev.updatedAt !== feed.updatedAt) await $.state.set(FEED, feed);
|
|
162
|
+
const { value: seen = 0 } = await $.state.get(SEEN);
|
|
163
|
+
const last = feed.events.at(-1)?.seq ?? 0;
|
|
164
|
+
if (first) {
|
|
165
|
+
// A new or reloaded session: what happened before now is in the pane, not replayed as toasts.
|
|
166
|
+
if (last !== seen) await $.state.set(SEEN, last);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (last <= seen) return;
|
|
170
|
+
for (const ev of feed.events.filter((x) => x.seq > seen && TOAST_KINDS.has(x.kind)).slice(-3)) {
|
|
171
|
+
$.ui.toast(`◆ Bunshin · ${ev.text}`, { timeoutMs: ev.kind === "learned" || ev.kind === "live" ? 8000 : 5000 });
|
|
172
|
+
}
|
|
173
|
+
await $.state.set(SEEN, last);
|
|
174
|
+
} finally {
|
|
175
|
+
refreshing = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Run the CLI for the session (its argv is in the feed, so the runtime copy the hooks use). */
|
|
180
|
+
async function act($, what) {
|
|
181
|
+
const { value: where } = await $.state.get(WHERE);
|
|
182
|
+
const { value: feed } = await $.state.get(FEED);
|
|
183
|
+
if (!where) return;
|
|
184
|
+
const argv = [...(feed?.cli?.length ? feed.cli : ["bunshin"]), ...(what === "learn" ? ["learn"] : what === "sync" ? ["sync"] : what)];
|
|
185
|
+
const label = what === "learn" ? "Learning from this session…" : what === "sync" ? "Syncing…" : "Working…";
|
|
186
|
+
await $.state.set(PANEL, { view: "session", busy: label, output: "" });
|
|
187
|
+
const res = await $.process
|
|
188
|
+
.run(argv, { cwd: where.project, env: { CLAUDE_CODE_SESSION_ID: where.sessionId, BUNSHIN_HARNESS: "claude-code" }, timeoutMs: what === "learn" ? 300_000 : 60_000 })
|
|
189
|
+
.catch((err) => ({ exitCode: 1, stdout: "", stderr: String(err?.message ?? err) }));
|
|
190
|
+
const output = (res.exitCode === 0 ? res.stdout : res.stderr || res.stdout).trim();
|
|
191
|
+
await $.state.set(PANEL, { view: "session", busy: "", output });
|
|
192
|
+
if (output) $.ui.toast(`◆ Bunshin · ${output.split("\n")[0]}`, { timeoutMs: 6000 });
|
|
193
|
+
await refresh($);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function showSession($) {
|
|
197
|
+
return $.state.set(PANEL, { view: "session", busy: "", output: "" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function showReview($) {
|
|
201
|
+
const { value: where } = await $.state.get(WHERE);
|
|
202
|
+
const { value: feed } = await $.state.get(FEED);
|
|
203
|
+
if (!where) return;
|
|
204
|
+
await $.state.set(PANEL, { view: "review", busy: "Reading suggestions…", output: "" });
|
|
205
|
+
const res = await $.process
|
|
206
|
+
.run([...(feed?.cli?.length ? feed.cli : ["bunshin"]), "review", "list"], { cwd: where.project, env: { BUNSHIN_HARNESS: "claude-code" }, timeoutMs: 30_000 })
|
|
207
|
+
.catch((err) => ({ exitCode: 1, stdout: "", stderr: String(err?.message ?? err) }));
|
|
208
|
+
await $.state.set(PANEL, { view: "review", busy: "", output: (res.stdout || res.stderr).trim() || "Nothing waiting for review." });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── drawing ─────────────────────────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
const label = (s) => (s.skill ? `${s.skill} skill` : `${s.title}`);
|
|
214
|
+
const clip = (s, n) => (s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s);
|
|
215
|
+
const hhmm = (iso) => String(iso).slice(11, 16);
|
|
216
|
+
|
|
217
|
+
function counts(feed) {
|
|
218
|
+
const served = feed.served.filter((s) => s.how !== "hint").length;
|
|
219
|
+
return [
|
|
220
|
+
feed.library ? `lib@${feed.library}` : "",
|
|
221
|
+
served ? `${served} served` : "",
|
|
222
|
+
feed.lessons.length ? `${feed.lessons.length} learned` : "",
|
|
223
|
+
feed.review ? `${feed.review} to review` : "",
|
|
224
|
+
].filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The band above the prompt: ◆ bunshin <latest served> why · counts. One line, two when narrow. */
|
|
228
|
+
function band(Box, Text, feed, columns) {
|
|
229
|
+
const latest = feed.served.find((s) => s.how !== "hint");
|
|
230
|
+
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" })];
|
|
233
|
+
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))}` }));
|
|
236
|
+
} else if (hint.length) {
|
|
237
|
+
parts.push(Text({ dimColor: true, children: ` maybe: ${clip(hint.map(label).join(", "), Math.max(20, columns - 40))}` }));
|
|
238
|
+
} else {
|
|
239
|
+
parts.push(Text({ dimColor: true, children: " listening" }));
|
|
240
|
+
}
|
|
241
|
+
if (meta && columns >= 50) parts.push(Text({ dimColor: true, children: ` ${meta}` }));
|
|
242
|
+
return Box({ flexDirection: "row", paddingX: 1, children: parts });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function plainSummary(feed) {
|
|
246
|
+
const lines = [`◆ Bunshin — ${counts(feed).join(" · ") || "nothing yet"}`];
|
|
247
|
+
for (const s of feed.served.slice(0, 8)) lines.push(`- ${hhmm(s.at)} ${s.how} **${label(s)}** — ${s.why}`);
|
|
248
|
+
for (const l of feed.lessons.slice(-5)) lines.push(`- learned (${l.status}): ${l.target}: ${l.summary}`);
|
|
249
|
+
return lines.join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function heading(Text, text) {
|
|
253
|
+
return Text({ bold: true, color: BRAND, children: text });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pane({ Box, Text, Button }, feed, panel, columns, rows) {
|
|
257
|
+
const width = Math.max(30, columns - 2);
|
|
258
|
+
const body = [];
|
|
259
|
+
if (!feed) {
|
|
260
|
+
body.push(Text({ dimColor: true, children: "Nothing yet: Bunshin's hooks write here as the session runs." }));
|
|
261
|
+
} else if (panel.view === "review") {
|
|
262
|
+
body.push(heading(Text, "Waiting for your review"));
|
|
263
|
+
body.push(Text({ children: panel.busy || panel.output, wrap: "wrap" }));
|
|
264
|
+
body.push(Text({ dimColor: true, children: "Apply or skip each in a terminal: bunshin review apply <id> · skip <id> · apply-all" }));
|
|
265
|
+
} else {
|
|
266
|
+
body.push(Text({ dimColor: true, children: clip(counts(feed).join(" · ") || "nothing served yet", width) }));
|
|
267
|
+
body.push(Text({ children: " " }));
|
|
268
|
+
body.push(heading(Text, "Served this session"));
|
|
269
|
+
const served = feed.served.slice(0, Math.max(3, Math.floor(rows / 3)));
|
|
270
|
+
if (!served.length) body.push(Text({ dimColor: true, children: " nothing yet" }));
|
|
271
|
+
for (const s of served) {
|
|
272
|
+
body.push(
|
|
273
|
+
Box({
|
|
274
|
+
flexDirection: "row",
|
|
275
|
+
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))}` }),
|
|
279
|
+
],
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
body.push(Text({ children: " " }));
|
|
284
|
+
body.push(heading(Text, "Learned from this session"));
|
|
285
|
+
if (!feed.lessons.length) body.push(Text({ dimColor: true, children: feed.reflectedAt ? " nothing new" : " not reflected on yet (Learn now, or at session end)" }));
|
|
286
|
+
for (const l of feed.lessons.slice(-6)) body.push(Text({ children: clip(` ${l.status.padEnd(10)} ${l.target}: ${l.summary}`, width), wrap: "truncate-end" }));
|
|
287
|
+
const recent = feed.events.filter((x) => x.kind !== "served" && x.kind !== "hint" && x.kind !== "push").slice(-4);
|
|
288
|
+
if (recent.length) {
|
|
289
|
+
body.push(Text({ children: " " }));
|
|
290
|
+
body.push(heading(Text, "Recent"));
|
|
291
|
+
for (const ev of recent) body.push(Text({ dimColor: true, children: clip(` ${hhmm(ev.at)} ${ev.text}`, width) }));
|
|
292
|
+
}
|
|
293
|
+
if (panel.busy || panel.output) {
|
|
294
|
+
body.push(Text({ children: " " }));
|
|
295
|
+
body.push(Text({ color: panel.busy ? "yellow" : undefined, children: clip(panel.busy || panel.output.split("\n").slice(-3).join(" "), width) }));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const buttons = [
|
|
299
|
+
Button({ label: "Learn now", hotkey: "l", onPress: () => host && void host.act("learn").catch(() => undefined) }),
|
|
300
|
+
Button({ label: "Sync", hotkey: "s", onPress: () => host && void host.act("sync").catch(() => undefined) }),
|
|
301
|
+
panel.view === "review"
|
|
302
|
+
? Button({ label: "Session", hotkey: "b", onPress: () => host && void host.session().catch(() => undefined) })
|
|
303
|
+
: Button({ label: feed?.review ? `Review (${feed.review})` : "Review", hotkey: "r", onPress: () => host && void host.review().catch(() => undefined) }),
|
|
304
|
+
];
|
|
305
|
+
return Box({
|
|
306
|
+
flexDirection: "column",
|
|
307
|
+
paddingX: 1,
|
|
308
|
+
children: [...body, Text({ children: " " }), Box({ flexDirection: "row", gap: 2, children: buttons })],
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// State the bunshin-ui mod keeps in the session ($.state): survives hot reloads.
|
|
2
|
+
export type BunshinUiServed = { id: string; title: string; kind: string; skill?: string; how: "auto" | "push" | "hint" | "load"; why: string; at: string };
|
|
3
|
+
export type BunshinUiEvent = { seq: number; at: string; kind: string; text: string };
|
|
4
|
+
export type BunshinUiLesson = { id: string; summary: string; status: string; target: string };
|
|
5
|
+
export type BunshinUiFeed = {
|
|
6
|
+
v: number;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
library?: number;
|
|
10
|
+
served: BunshinUiServed[];
|
|
11
|
+
events: BunshinUiEvent[];
|
|
12
|
+
lessons: BunshinUiLesson[];
|
|
13
|
+
review: number;
|
|
14
|
+
checkpointAt?: string;
|
|
15
|
+
reflectedAt?: string;
|
|
16
|
+
cli: string[];
|
|
17
|
+
root: string;
|
|
18
|
+
};
|
|
19
|
+
export type BunshinUiWhere = { store: string; project: string; sessionId: string };
|
|
20
|
+
export type BunshinUiPanel = { view: "session" | "review"; busy: string; output: string };
|
|
21
|
+
|
|
22
|
+
declare module "claude-code" {
|
|
23
|
+
interface PluginState {
|
|
24
|
+
"bunshin-ui": {
|
|
25
|
+
feed: BunshinUiFeed | null;
|
|
26
|
+
where: BunshinUiWhere | null;
|
|
27
|
+
seen: number;
|
|
28
|
+
panel: BunshinUiPanel;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/pi-extension.js
CHANGED
|
@@ -78,11 +78,20 @@ async function hook(event, ctx, extra = {}) {
|
|
|
78
78
|
transcript_path: transcript(ctx),
|
|
79
79
|
...extra
|
|
80
80
|
});
|
|
81
|
+
let output = {};
|
|
81
82
|
try {
|
|
82
|
-
|
|
83
|
+
output = JSON.parse(raw);
|
|
83
84
|
} catch {
|
|
84
85
|
return {};
|
|
85
86
|
}
|
|
87
|
+
showUi(output, ctx);
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
90
|
+
function showUi(output, ctx) {
|
|
91
|
+
if (!ctx.hasUI || !output.bunshinUi) return;
|
|
92
|
+
const { lines = [], notices = [] } = output.bunshinUi;
|
|
93
|
+
ctx.ui.setWidget("bunshin", lines.length ? lines : void 0);
|
|
94
|
+
for (const n of notices) ctx.ui.notify(`Bunshin: ${n}`, "info");
|
|
86
95
|
}
|
|
87
96
|
function bunshin(pi) {
|
|
88
97
|
if (process.env.BUNSHIN_CHILD || process.env.BUNSHIN_DISABLE) return;
|
|
@@ -144,5 +153,6 @@ function bunshin(pi) {
|
|
|
144
153
|
}
|
|
145
154
|
export {
|
|
146
155
|
bunshin as default,
|
|
156
|
+
showUi,
|
|
147
157
|
transcriptLines
|
|
148
158
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunshin-cc/bunshin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"knowledge"
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
|
-
"build": "tsc -p tsconfig.json --noEmit && esbuild src/bin.ts --bundle --platform=node --target=node20 --format=esm --external:@anthropic-ai/sdk --external:yaml --outfile=dist/bin.js && esbuild src/git-remote-bunshin.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/git-remote-bunshin.js && esbuild ../../adapters/pi/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/pi-extension.js",
|
|
53
|
+
"build": "tsc -p tsconfig.json --noEmit && esbuild src/bin.ts --bundle --platform=node --target=node20 --format=esm --external:@anthropic-ai/sdk --external:yaml --outfile=dist/bin.js && esbuild src/git-remote-bunshin.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/git-remote-bunshin.js && esbuild ../../adapters/pi/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/pi-extension.js && node scripts/copy-mod.mjs",
|
|
54
54
|
"dev": "tsx src/bin.ts",
|
|
55
55
|
"test": "vitest run",
|
|
56
56
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|