@bunshin-cc/bunshin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ // ../../adapters/pi/index.ts
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, renameSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ var bundledCli = fileURLToPath(new URL("./bin.js", import.meta.url));
8
+ var cli = process.env.BUNSHIN_BIN ? { command: process.env.BUNSHIN_BIN, args: [] } : existsSync(bundledCli) ? { command: process.execPath, args: [bundledCli] } : { command: "bunshin", args: [] };
9
+ function run(args, cwd, input, timeoutMs = 1e4) {
10
+ return new Promise((resolve) => {
11
+ const child = spawn(cli.command, [...cli.args, ...args], { cwd, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, BUNSHIN_HARNESS: "pi" } });
12
+ let output = "";
13
+ let error = "";
14
+ let done = false;
15
+ const finish = (value) => {
16
+ if (!done) {
17
+ done = true;
18
+ clearTimeout(timer);
19
+ resolve(value);
20
+ }
21
+ };
22
+ const timer = setTimeout(() => {
23
+ child.kill("SIGKILL");
24
+ finish("bunshin: command timed out");
25
+ }, timeoutMs);
26
+ child.stdout.on("data", (chunk) => {
27
+ output += chunk;
28
+ });
29
+ child.stderr.on("data", (chunk) => {
30
+ error += chunk;
31
+ });
32
+ child.on("error", (err) => finish(`bunshin: ${err.message}`));
33
+ child.on("close", (code) => finish(code === 0 ? output : error || output));
34
+ child.stdin.on("error", () => {
35
+ });
36
+ child.stdin.end(input ? JSON.stringify(input) : void 0);
37
+ });
38
+ }
39
+ function storeDir(cwd) {
40
+ let current = cwd;
41
+ for (; ; ) {
42
+ if (existsSync(join(current, ".bunshin/config.json"))) return join(current, ".bunshin");
43
+ const parent = dirname(current);
44
+ if (parent === current) return join(homedir(), ".bunshin");
45
+ current = parent;
46
+ }
47
+ }
48
+ function transcriptLines(messages) {
49
+ return messages.flatMap((message) => {
50
+ if (!["user", "assistant", "toolResult"].includes(message.role)) return [];
51
+ let content = message.content;
52
+ if (message.role === "toolResult") {
53
+ content = [{ type: "tool_result", content, is_error: !!message.isError }];
54
+ } else if (Array.isArray(content)) {
55
+ content = content.flatMap((block) => {
56
+ if (block.type === "text") return [block];
57
+ if (block.type === "toolCall") return [{ type: "tool_use", name: block.name, input: block.arguments }];
58
+ return [];
59
+ });
60
+ }
61
+ return [{ type: message.role === "assistant" ? "assistant" : "user", message: { content }, timestamp: message.timestamp }];
62
+ });
63
+ }
64
+ function transcript(ctx) {
65
+ const directory = join(storeDir(ctx.cwd), "transcripts");
66
+ mkdirSync(directory, { recursive: true, mode: 448 });
67
+ const file = join(directory, `pi-${ctx.sessionManager.getSessionId()}.jsonl`);
68
+ const messages = ctx.sessionManager.getBranch().flatMap((entry) => entry.type === "message" ? [entry.message] : []);
69
+ const temporary = `${file}.${process.pid}.tmp`;
70
+ writeFileSync(temporary, transcriptLines(messages).map((line) => JSON.stringify(line)).join("\n") + "\n", { mode: 384 });
71
+ renameSync(temporary, file);
72
+ return file;
73
+ }
74
+ async function hook(event, ctx, extra = {}) {
75
+ const raw = await run(["hook", event], ctx.cwd, {
76
+ session_id: ctx.sessionManager.getSessionId(),
77
+ cwd: ctx.cwd,
78
+ transcript_path: transcript(ctx),
79
+ ...extra
80
+ });
81
+ try {
82
+ return JSON.parse(raw);
83
+ } catch {
84
+ return {};
85
+ }
86
+ }
87
+ function bunshin(pi) {
88
+ if (process.env.BUNSHIN_CHILD || process.env.BUNSHIN_DISABLE) return;
89
+ let timer;
90
+ const clear = () => {
91
+ if (timer) clearInterval(timer);
92
+ timer = void 0;
93
+ };
94
+ pi.on("before_agent_start", async (event, ctx) => {
95
+ if (!existsSync(join(storeDir(ctx.cwd), "config.json"))) {
96
+ if (ctx.hasUI) ctx.ui.notify("Run bunshin setup --harness pi in this project to connect your workspace.", "warning");
97
+ return;
98
+ }
99
+ const output = await hook("user-prompt-submit", ctx, { prompt: event.prompt });
100
+ clear();
101
+ const pending = join(storeDir(ctx.cwd), "pending-inject", `${ctx.sessionManager.getSessionId()}.md`);
102
+ const deadline = Date.now() + 3e4;
103
+ timer = setInterval(() => {
104
+ if (Date.now() >= deadline) {
105
+ clear();
106
+ return;
107
+ }
108
+ if (!existsSync(pending)) return;
109
+ let text2;
110
+ try {
111
+ text2 = readFileSync(pending, "utf8");
112
+ unlinkSync(pending);
113
+ } catch {
114
+ return;
115
+ }
116
+ clear();
117
+ if (text2.trim()) pi.sendMessage({ customType: "bunshin-recall", content: text2, display: true }, { triggerTurn: false, deliverAs: "steer" });
118
+ }, 250);
119
+ timer.unref();
120
+ const text = output.hookSpecificOutput?.additionalContext;
121
+ if (text?.trim()) return { message: { customType: "bunshin-recall", content: text, display: true } };
122
+ });
123
+ pi.on("agent_end", async (_event, ctx) => {
124
+ if (existsSync(join(storeDir(ctx.cwd), "config.json"))) await hook("stop", ctx, { stop_hook_active: false });
125
+ });
126
+ pi.on("session_before_compact", async (event, ctx) => {
127
+ if (existsSync(join(storeDir(ctx.cwd), "config.json"))) await hook("pre-compact", ctx, { trigger: event.reason });
128
+ });
129
+ pi.on("session_shutdown", async (event, ctx) => {
130
+ clear();
131
+ if (existsSync(join(storeDir(ctx.cwd), "config.json"))) await hook("session-end", ctx, { reason: event.reason });
132
+ });
133
+ pi.registerCommand("bunshin", {
134
+ description: "Bunshin status, recall, lessons, or sync",
135
+ handler: async (input, ctx) => {
136
+ const tokens = [...input.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((match) => match[1] ?? match[2] ?? match[3]);
137
+ const args = tokens.length ? tokens : ["status"];
138
+ if (args[0] === "recall" && args.length > 1) args.splice(1, args.length - 1, args.slice(1).join(" "));
139
+ const output = await run(args, ctx.cwd, void 0, 6e4);
140
+ if (ctx.hasUI) ctx.ui.notify(output.trim() || "Done.", "info");
141
+ else process.stdout.write(output);
142
+ }
143
+ });
144
+ }
145
+ export {
146
+ bunshin as default,
147
+ transcriptLines
148
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@bunshin-cc/bunshin",
3
+ "version": "0.1.0",
4
+ "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
+ "type": "module",
6
+ "bin": {
7
+ "bunshin": "dist/bin.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "dependencies": {
18
+ "@anthropic-ai/sdk": "^0.127.0",
19
+ "yaml": "^2.8.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.15.0",
23
+ "esbuild": "^0.28.1",
24
+ "tsx": "^4.20.0",
25
+ "typescript": "^5.8.0",
26
+ "vitest": "^3.2.0",
27
+ "@bunshin/core": "0.1.0"
28
+ },
29
+ "license": "MIT",
30
+ "homepage": "https://bunshin.cc",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/yiheinchai/bunshin.git",
34
+ "directory": "packages/cli"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./dist/pi-extension.js"
42
+ ]
43
+ },
44
+ "keywords": [
45
+ "pi-package",
46
+ "codex",
47
+ "claude-code",
48
+ "memory",
49
+ "knowledge"
50
+ ],
51
+ "scripts": {
52
+ "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 ../../adapters/pi/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/pi-extension.js",
53
+ "dev": "tsx src/bin.ts",
54
+ "test": "vitest run",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit"
56
+ }
57
+ }