@bunshin-cc/bunshin 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/v2/gitremote.ts
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import readline from "node:readline";
8
+ import { spawnSync } from "node:child_process";
9
+
10
+ // src/util.ts
11
+ var STOPWORDS = new Set(
12
+ "a an the and or but if then else of to in on at for from by with without as is are was were be been being do does did done have has had can could should would will shall may might must this that these those it its i we you they he she them us our your my me not no yes so just also very really please now new use using make get set add fix run change update create write need want like about into over under out up down how what when where why which who whom there here some any all each every more most less least than too own same other".split(" ")
13
+ );
14
+ function homeDir() {
15
+ return process.env.HOME || process.env.USERPROFILE || "/";
16
+ }
17
+
18
+ // src/v2/gitremote.ts
19
+ var ZERO_SHA = "0".repeat(40);
20
+ var REMOTE_PATH = "/v1/git/repos/";
21
+ function parseRemoteUrl(url) {
22
+ const raw = url.replace(/^bunshin::/, "").replace(/\.git$/, "").replace(/\/+$/, "");
23
+ const i = raw.indexOf(REMOTE_PATH);
24
+ if (i < 0) throw new Error(`not a Bunshin repository address: ${url}`);
25
+ const apiUrl = raw.slice(0, i);
26
+ const repo = decodeURIComponent(raw.slice(i + REMOTE_PATH.length));
27
+ if (!/^https?:\/\//.test(apiUrl) || repo.split("/").length !== 2) throw new Error(`not a Bunshin repository address: ${url}`);
28
+ return { apiUrl, repo };
29
+ }
30
+ var gitCredentialFile = () => path.join(homeDir(), ".config/bunshin/git-credentials.json");
31
+ function resolveGitKey(apiUrl) {
32
+ const base = apiUrl.replace(/\/$/, "");
33
+ if (process.env.BUNSHIN_API_KEY) return process.env.BUNSHIN_API_KEY;
34
+ try {
35
+ const all = JSON.parse(fs.readFileSync(gitCredentialFile(), "utf8"));
36
+ if (all[base]) return all[base];
37
+ } catch {
38
+ }
39
+ const dir = path.join(homeDir(), ".config/bunshin/credentials");
40
+ let files = [];
41
+ try {
42
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
43
+ } catch {
44
+ return void 0;
45
+ }
46
+ for (const f of files.sort()) {
47
+ try {
48
+ const c = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
49
+ if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
50
+ } catch {
51
+ }
52
+ }
53
+ return void 0;
54
+ }
55
+ var GitApiError = class extends Error {
56
+ constructor(status, message, body = null) {
57
+ super(message);
58
+ this.status = status;
59
+ this.body = body;
60
+ }
61
+ status;
62
+ body;
63
+ };
64
+ var GitClient = class {
65
+ constructor(apiUrl, key) {
66
+ this.apiUrl = apiUrl;
67
+ this.key = key;
68
+ }
69
+ apiUrl;
70
+ key;
71
+ async call(method, route, body, contentType, timeoutMs = 6e4) {
72
+ const ctrl = new AbortController();
73
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
74
+ try {
75
+ const send = () => fetch(`${this.apiUrl.replace(/\/$/, "")}/v1/git${route}`, {
76
+ method,
77
+ headers: { authorization: `Bearer ${this.key}`, ...contentType ? { "content-type": contentType } : {} },
78
+ body,
79
+ signal: ctrl.signal,
80
+ redirect: "error"
81
+ });
82
+ let res;
83
+ const started = Date.now();
84
+ try {
85
+ res = await send();
86
+ } catch (err) {
87
+ const cause = err?.cause?.message ?? (err instanceof Error ? err.message : "");
88
+ if (Date.now() - started < 200 && /other side closed|ECONNRESET|socket hang up/i.test(cause)) res = await send();
89
+ else throw err;
90
+ }
91
+ return res;
92
+ } catch (err) {
93
+ throw new GitApiError(0, `${method} ${route}: ${err instanceof Error ? err.message : String(err)}`);
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+ async json(method, route, body, okStatuses = []) {
99
+ const res = await this.call(method, route, body === void 0 ? void 0 : JSON.stringify(body), body === void 0 ? void 0 : "application/json");
100
+ const text = await res.text();
101
+ let parsed = null;
102
+ try {
103
+ parsed = text ? JSON.parse(text) : {};
104
+ } catch {
105
+ }
106
+ if (!res.ok && !okStatuses.includes(res.status)) {
107
+ const e = parsed;
108
+ throw new GitApiError(res.status, e?.message || e?.error || `${method} ${route} \u2192 ${res.status}`, parsed);
109
+ }
110
+ return parsed;
111
+ }
112
+ repos = () => this.json("GET", "/repos");
113
+ create = (body) => this.json("POST", "/repos", body);
114
+ info = (repo) => this.json("GET", `/repos/${repo}`);
115
+ bundles = (repo, after = 0, data = false) => this.json("GET", `/repos/${repo}/bundles?after=${after}${data ? "&data=1" : ""}`);
116
+ async push(repo, updates, bundle) {
117
+ const header = Buffer.from(JSON.stringify({ updates }));
118
+ const len = Buffer.alloc(4);
119
+ len.writeUInt32BE(header.length);
120
+ const body = Buffer.concat([len, header, bundle ? Buffer.from(bundle) : Buffer.alloc(0)]);
121
+ const res = await this.call("POST", `/repos/${repo}/push`, body, "application/octet-stream", 3e5);
122
+ const text = await res.text();
123
+ let parsed = null;
124
+ try {
125
+ parsed = JSON.parse(text);
126
+ } catch {
127
+ }
128
+ if (res.status === 409 && parsed && typeof parsed === "object" && "results" in parsed) return parsed;
129
+ if (!res.ok) {
130
+ const e = parsed;
131
+ throw new GitApiError(res.status, e?.message || e?.error || `push \u2192 ${res.status}`, parsed);
132
+ }
133
+ return parsed;
134
+ }
135
+ };
136
+ function clientFor(address) {
137
+ const key = resolveGitKey(address.apiUrl);
138
+ if (!key) throw new Error(`no Bunshin key for ${address.apiUrl}. Run \`bunshin repo login --api-url ${address.apiUrl}\` (or set BUNSHIN_API_KEY).`);
139
+ return new GitClient(address.apiUrl, key);
140
+ }
141
+ function git(args, opts = {}) {
142
+ const r = spawnSync("git", args, { encoding: "utf8", input: opts.input, cwd: opts.cwd, maxBuffer: 256 * 1024 * 1024, env: process.env });
143
+ const ok = r.status === 0;
144
+ if (!ok && !opts.allowFail) throw new Error(`git ${args.join(" ")}: ${(r.stderr || r.stdout || "").trim()}`);
145
+ return { ok, out: r.stdout ?? "", err: r.stderr ?? "" };
146
+ }
147
+ function presentObjects(shas, cwd) {
148
+ if (!shas.length) return /* @__PURE__ */ new Set();
149
+ const r = git(["cat-file", "--batch-check"], { input: shas.join("\n") + "\n", allowFail: true, cwd });
150
+ const have = /* @__PURE__ */ new Set();
151
+ for (const line of r.out.split("\n")) {
152
+ const m = /^([0-9a-f]{40}) (commit|tree|blob|tag) /.exec(line);
153
+ if (m) have.add(m[1]);
154
+ }
155
+ return have;
156
+ }
157
+ var isAncestor = (a, b) => git(["merge-base", "--is-ancestor", a, b], { allowFail: true }).ok;
158
+ var objectType = (sha) => git(["cat-file", "-t", sha], { allowFail: true }).out.trim();
159
+ async function doList(client, repo, write) {
160
+ const info = await client.info(repo);
161
+ for (const r of info.refs) write(`${r.sha} ${r.ref}
162
+ `);
163
+ if (info.head) write(`@${info.head} HEAD
164
+ `);
165
+ write("\n");
166
+ }
167
+ async function fetchObjects(client, repo, wanted, log = () => {
168
+ }) {
169
+ if (wanted.every((s) => presentObjects([s]).has(s))) return;
170
+ const meta = (await client.bundles(repo)).bundles;
171
+ const have = presentObjects([...new Set(meta.flatMap((b) => b.tips))]);
172
+ const needed = meta.filter((b) => b.tips.some((t) => !have.has(t)));
173
+ if (!needed.length) throw new Error("the server has no bundle with the missing objects");
174
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-git-"));
175
+ try {
176
+ let after = needed[0].seq - 1;
177
+ const want = new Set(needed.map((b) => b.seq));
178
+ let applied = 0;
179
+ for (; ; ) {
180
+ const page = await client.bundles(repo, after, true);
181
+ for (const b of page.bundles) {
182
+ after = b.seq;
183
+ if (!want.has(b.seq) || !b.data) continue;
184
+ const file = path.join(tmp, `${b.seq}.bundle`);
185
+ fs.writeFileSync(file, Buffer.from(b.data, "base64"));
186
+ git(["bundle", "unbundle", file]);
187
+ fs.rmSync(file, { force: true });
188
+ applied++;
189
+ }
190
+ if (!page.more || !page.bundles.length) break;
191
+ }
192
+ log(`bunshin: applied ${applied} bundle${applied === 1 ? "" : "s"}
193
+ `);
194
+ } finally {
195
+ fs.rmSync(tmp, { recursive: true, force: true });
196
+ }
197
+ const still = wanted.filter((s) => !presentObjects([s]).has(s));
198
+ if (still.length) throw new Error(`objects still missing after fetch: ${still.slice(0, 3).join(", ")}`);
199
+ }
200
+ function parsePushLine(line) {
201
+ let spec = line.slice("push ".length);
202
+ const force = spec.startsWith("+");
203
+ if (force) spec = spec.slice(1);
204
+ const i = spec.lastIndexOf(":");
205
+ return { src: spec.slice(0, i), dst: spec.slice(i + 1), force };
206
+ }
207
+ async function pushRefs(client, repo, specs) {
208
+ const info = await client.info(repo);
209
+ if (!info.repo.canPush) return specs.map((s) => `error ${s.dst} you may read this repository but not push to it`);
210
+ const remote = new Map(info.refs.map((r) => [r.ref, r.sha]));
211
+ const status = /* @__PURE__ */ new Map();
212
+ const updates = [];
213
+ for (const s of specs) {
214
+ const old = remote.get(s.dst) ?? ZERO_SHA;
215
+ if (!s.src) {
216
+ if (old === ZERO_SHA) status.set(s.dst, `error ${s.dst} no such ref on the remote`);
217
+ else updates.push({ ref: s.dst, old, new: ZERO_SHA, ...s.force ? { force: true } : {} });
218
+ continue;
219
+ }
220
+ const r = git(["rev-parse", "--verify", s.src], { allowFail: true });
221
+ if (!r.ok) {
222
+ status.set(s.dst, `error ${s.dst} src refspec ${s.src} does not match any`);
223
+ continue;
224
+ }
225
+ const sha = r.out.trim();
226
+ if (!s.force && old !== ZERO_SHA && old !== sha) {
227
+ if (s.dst.startsWith("refs/tags/")) {
228
+ status.set(s.dst, `error ${s.dst} already exists`);
229
+ continue;
230
+ }
231
+ if (!presentObjects([old]).has(old)) {
232
+ status.set(s.dst, `error ${s.dst} fetch first`);
233
+ continue;
234
+ }
235
+ if (!isAncestor(old, sha)) {
236
+ status.set(s.dst, `error ${s.dst} non-fast-forward`);
237
+ continue;
238
+ }
239
+ }
240
+ if (old === sha) {
241
+ status.set(s.dst, `ok ${s.dst}`);
242
+ continue;
243
+ }
244
+ updates.push({ ref: s.dst, old, new: sha, ...s.force ? { force: true } : {} });
245
+ }
246
+ const sends = updates.filter((u) => u.new !== ZERO_SHA);
247
+ let bundle = null;
248
+ if (sends.length) {
249
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-git-"));
250
+ const tmpRefs = [];
251
+ try {
252
+ sends.forEach((u, i) => {
253
+ const ref = `refs/bunshin-push/${process.pid}-${i}`;
254
+ git(["update-ref", ref, u.new]);
255
+ tmpRefs.push(ref);
256
+ });
257
+ const known = presentObjects([...new Set(remote.values())]);
258
+ const file = path.join(tmp, "push.bundle");
259
+ const r = git(["bundle", "create", "--quiet", file, ...tmpRefs, ...[...known].map((k) => `^${k}`)], { allowFail: true });
260
+ if (r.ok) bundle = new Uint8Array(fs.readFileSync(file));
261
+ else if (!/empty bundle/i.test(r.err)) throw new Error(r.err.trim() || "git bundle create failed");
262
+ else {
263
+ for (const u of sends) {
264
+ const target = objectType(u.new) === "commit" ? u.new : git(["rev-parse", `${u.new}^{commit}`], { allowFail: true }).out.trim();
265
+ if (![...known].some((k) => k === u.new || target && isAncestor(target, k))) throw new Error(`${u.ref}: objects are missing on the remote`);
266
+ }
267
+ }
268
+ } finally {
269
+ for (const ref of tmpRefs) git(["update-ref", "-d", ref], { allowFail: true });
270
+ fs.rmSync(tmp, { recursive: true, force: true });
271
+ }
272
+ }
273
+ if (updates.length) {
274
+ const res = await client.push(repo, updates, bundle);
275
+ for (const r of res.results) status.set(r.ref, r.ok ? `ok ${r.ref}` : `error ${r.ref} ${r.error ?? "rejected"}`);
276
+ }
277
+ return specs.map((s) => status.get(s.dst) ?? `error ${s.dst} not pushed`);
278
+ }
279
+ async function runRemoteHelper(_remote, url, io = {}) {
280
+ const write = io.write ?? ((s) => void process.stdout.write(s));
281
+ const log = io.log ?? ((s) => void process.stderr.write(s));
282
+ let client;
283
+ let repo;
284
+ try {
285
+ const address = parseRemoteUrl(url);
286
+ client = clientFor(address);
287
+ repo = address.repo;
288
+ } catch (e) {
289
+ log(`fatal: ${e instanceof Error ? e.message : String(e)}
290
+ `);
291
+ return 128;
292
+ }
293
+ const rl = readline.createInterface({ input: io.input ?? process.stdin, crlfDelay: Infinity, terminal: false });
294
+ const lines = rl[Symbol.asyncIterator]();
295
+ const next = async () => {
296
+ const r = await lines.next();
297
+ return r.done ? null : String(r.value);
298
+ };
299
+ let verbose = 1;
300
+ try {
301
+ for (; ; ) {
302
+ const line = await next();
303
+ if (line === null || line === "") return 0;
304
+ if (line === "capabilities") {
305
+ write("fetch\npush\noption\n\n");
306
+ } else if (line.startsWith("option ")) {
307
+ const [, name, value] = line.split(" ");
308
+ if (name === "verbosity") {
309
+ verbose = Number(value) || 0;
310
+ write("ok\n");
311
+ } else if (name === "progress") write("ok\n");
312
+ else write("unsupported\n");
313
+ } else if (line === "list" || line === "list for-push") {
314
+ await doList(client, repo, write);
315
+ } else if (line.startsWith("fetch ")) {
316
+ const wanted = [line.split(" ")[1]];
317
+ for (let l = await next(); l && l.startsWith("fetch "); l = await next()) wanted.push(l.split(" ")[1]);
318
+ await fetchObjects(client, repo, wanted, verbose > 1 ? log : () => {
319
+ });
320
+ write("\n");
321
+ } else if (line.startsWith("push ")) {
322
+ const specs = [parsePushLine(line)];
323
+ for (let l = await next(); l && l.startsWith("push "); l = await next()) specs.push(parsePushLine(l));
324
+ for (const s of await pushRefs(client, repo, specs)) write(`${s}
325
+ `);
326
+ write("\n");
327
+ } else {
328
+ log(`fatal: git-remote-bunshin: unsupported command "${line}"
329
+ `);
330
+ return 1;
331
+ }
332
+ }
333
+ } catch (e) {
334
+ const status = e instanceof GitApiError ? e.status : 0;
335
+ const why = status === 404 ? `repository '${repo}' not found in Bunshin, or you cannot read it` : status === 401 ? "the Bunshin key was refused" : e instanceof Error ? e.message : String(e);
336
+ log(`fatal: ${why}
337
+ `);
338
+ return 128;
339
+ }
340
+ }
341
+
342
+ // src/git-remote-bunshin.ts
343
+ runRemoteHelper(process.argv[2] ?? "", process.argv[3] ?? process.argv[2] ?? "").then(
344
+ (code) => process.exit(code),
345
+ (e) => {
346
+ process.stderr.write(`fatal: ${e instanceof Error ? e.message : String(e)}
347
+ `);
348
+ process.exit(128);
349
+ }
350
+ );
@@ -1,6 +1,6 @@
1
1
  // ../../adapters/pi/index.ts
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, renameSync } from "node:fs";
3
+ import { existsSync, mkdirSync, writeFileSync, renameSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -86,59 +86,59 @@ async function hook(event, ctx, extra = {}) {
86
86
  }
87
87
  function bunshin(pi) {
88
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
- };
89
+ const connected = (ctx) => existsSync(join(storeDir(ctx.cwd), "config.json"));
90
+ let startContext = "";
91
+ pi.on("session_start", async (event, ctx) => {
92
+ if (!connected(ctx)) return;
93
+ const output = await hook("session-start", ctx, { source: event.reason });
94
+ startContext = output.hookSpecificOutput?.additionalContext?.trim() ?? "";
95
+ });
94
96
  pi.on("before_agent_start", async (event, ctx) => {
95
- if (!existsSync(join(storeDir(ctx.cwd), "config.json"))) {
97
+ if (!connected(ctx)) {
96
98
  if (ctx.hasUI) ctx.ui.notify("Run bunshin setup --harness pi in this project to connect your workspace.", "warning");
97
99
  return;
98
100
  }
99
101
  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();
102
+ const text = [startContext, output.hookSpecificOutput?.additionalContext?.trim() ?? ""].filter(Boolean).join("\n\n");
103
+ startContext = "";
104
+ if (text) return { message: { customType: "bunshin-recall", content: text, display: true } };
105
+ });
106
+ pi.on("tool_call", async (event, ctx) => {
107
+ if (!["read", "edit", "write"].includes(event.toolName) || !connected(ctx)) return;
108
+ const output = await hook("pre-tool-use", ctx, { tool_name: event.toolName, tool_input: event.input });
120
109
  const text = output.hookSpecificOutput?.additionalContext;
121
- if (text?.trim()) return { message: { customType: "bunshin-recall", content: text, display: true } };
110
+ if (text?.trim()) pi.sendMessage({ customType: "bunshin-push", content: text, display: true }, { triggerTurn: false, deliverAs: "steer" });
122
111
  });
123
112
  pi.on("agent_end", async (_event, ctx) => {
124
- if (existsSync(join(storeDir(ctx.cwd), "config.json"))) await hook("stop", ctx, { stop_hook_active: false });
113
+ if (!connected(ctx)) return;
114
+ const output = await hook("stop", ctx, { stop_hook_active: false });
115
+ if (output.systemMessage && ctx.hasUI) ctx.ui.notify(output.systemMessage.replace("/learn", "/learn (bunshin learn)"), "info");
125
116
  });
126
117
  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 });
118
+ if (connected(ctx)) await hook("pre-compact", ctx, { trigger: event.reason });
128
119
  });
129
120
  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 });
121
+ if (connected(ctx)) await hook("session-end", ctx, { reason: event.reason });
132
122
  });
123
+ const tokens = (input) => [...input.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((match) => match[1] ?? match[2] ?? match[3]);
124
+ const show = (output, ctx) => {
125
+ if (ctx.hasUI) ctx.ui.notify(output.trim() || "Done.", "info");
126
+ else process.stdout.write(output);
127
+ };
133
128
  pi.registerCommand("bunshin", {
134
- description: "Bunshin status, recall, lessons, or sync",
129
+ description: "Bunshin status, recall, load, review, questions, or sync",
135
130
  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"];
131
+ const args = tokens(input);
132
+ if (!args.length) args.push("status");
138
133
  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);
134
+ show(await run(args, ctx.cwd, void 0, 6e4), ctx);
135
+ }
136
+ });
137
+ pi.registerCommand("learn", {
138
+ description: "Capture what this session learned into the Bunshin library now",
139
+ handler: async (input, ctx) => {
140
+ transcript(ctx);
141
+ show(await run(["learn", "--session", ctx.sessionManager.getSessionId(), ...input.trim() ? [input.trim()] : []], ctx.cwd, void 0, 18e4), ctx);
142
142
  }
143
143
  });
144
144
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "bunshin": "dist/bin.js"
7
+ "bunshin": "dist/bin.js",
8
+ "git-remote-bunshin": "dist/git-remote-bunshin.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",
@@ -14,13 +15,6 @@
14
15
  "engines": {
15
16
  "node": ">=20"
16
17
  },
17
- "scripts": {
18
- "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",
19
- "dev": "tsx src/bin.ts",
20
- "test": "vitest run",
21
- "typecheck": "tsc -p tsconfig.json --noEmit",
22
- "prepack": "pnpm build"
23
- },
24
18
  "dependencies": {
25
19
  "@anthropic-ai/sdk": "^0.127.0",
26
20
  "yaml": "^2.8.0"
@@ -31,7 +25,7 @@
31
25
  "tsx": "^4.20.0",
32
26
  "typescript": "^5.8.0",
33
27
  "vitest": "^3.2.0",
34
- "@bunshin/core": "workspace:*"
28
+ "@bunshin/core": "0.1.0"
35
29
  },
36
30
  "license": "MIT",
37
31
  "homepage": "https://bunshin.cc",
@@ -54,5 +48,11 @@
54
48
  "claude-code",
55
49
  "memory",
56
50
  "knowledge"
57
- ]
58
- }
51
+ ],
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",
54
+ "dev": "tsx src/bin.ts",
55
+ "test": "vitest run",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit"
57
+ }
58
+ }