@bunshin-cc/bunshin 0.3.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/bin.js +83 -8
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -2489,19 +2489,49 @@ import { createHash } from "node:crypto";
2489
2489
  import fs5 from "node:fs";
2490
2490
  import os from "node:os";
2491
2491
  import path4 from "node:path";
2492
+ function deployWorkspace(env = process.env) {
2493
+ if (env.BUNSHIN_DEPLOY_WORKSPACE) return path4.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
2494
+ try {
2495
+ const ws = JSON.parse(fs5.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os.homedir()), "utf8")).workspace;
2496
+ return typeof ws === "string" && ws ? ws : null;
2497
+ } catch {
2498
+ return null;
2499
+ }
2500
+ }
2492
2501
  function deployPaths(env = process.env) {
2493
2502
  const home = env.BUNSHIN_DEPLOY_HOME || os.homedir();
2503
+ const ws = deployWorkspace(env);
2494
2504
  return {
2495
2505
  home,
2496
- claudeSkills: path4.join(home, ".claude", "skills"),
2497
- codexSkills: path4.join(home, ".codex", "skills"),
2498
- claudeMd: path4.join(home, ".claude", "CLAUDE.md"),
2499
- agentsMd: path4.join(home, ".codex", "AGENTS.md"),
2506
+ claudeSkills: path4.join(ws ?? home, ".claude", "skills"),
2507
+ codexSkills: path4.join(ws ?? home, ".codex", "skills"),
2508
+ claudeMd: ws ? path4.join(ws, "CLAUDE.md") : path4.join(home, ".claude", "CLAUDE.md"),
2509
+ agentsMd: ws ? path4.join(ws, "AGENTS.md") : path4.join(home, ".codex", "AGENTS.md"),
2500
2510
  library: path4.join(home, ".bunshin", "library"),
2501
2511
  bin: path4.join(home, ".bunshin", "bin"),
2502
2512
  state: path4.join(home, ".bunshin", "deploy")
2503
2513
  };
2504
2514
  }
2515
+ function setDeployWorkspace(dir, env = process.env) {
2516
+ const home = env.BUNSHIN_DEPLOY_HOME || os.homedir();
2517
+ const ws = dir ? path4.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
2518
+ if (ws && !fs5.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
2519
+ const stateDir = path4.join(home, ".bunshin", "deploy");
2520
+ const deployed = fs5.existsSync(stateDir) && fs5.readdirSync(stateDir).some((f) => {
2521
+ if (f === "settings.json" || !f.endsWith(".json")) return false;
2522
+ try {
2523
+ return Object.keys(JSON.parse(fs5.readFileSync(path4.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
2524
+ } catch {
2525
+ return false;
2526
+ }
2527
+ });
2528
+ if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
2529
+ throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
2530
+ }
2531
+ fs5.mkdirSync(path4.dirname(settingsFile(home)), { recursive: true });
2532
+ fs5.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
2533
+ return ws;
2534
+ }
2505
2535
  function statePath(p, orgId) {
2506
2536
  return path4.join(p.state, `${orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org"}.json`);
2507
2537
  }
@@ -3037,13 +3067,14 @@ function materialize(sections, dir) {
3037
3067
  }
3038
3068
  return { files };
3039
3069
  }
3040
- var sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, LAYER_RANK, docMarker;
3070
+ var settingsFile, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, LAYER_RANK, docMarker;
3041
3071
  var init_deploy = __esm({
3042
3072
  "src/v2/deploy.ts"() {
3043
3073
  "use strict";
3044
3074
  init_v22();
3045
3075
  init_util();
3046
3076
  init_store2();
3077
+ settingsFile = (home) => path4.join(home, ".bunshin", "deploy", "settings.json");
3047
3078
  sha = (text2) => createHash("sha256").update(text2).digest("hex");
3048
3079
  BLOCK_BEGIN = "<!-- bunshin:begin -->";
3049
3080
  BLOCK_END = "<!-- bunshin:end -->";
@@ -4144,11 +4175,13 @@ var init_exports = {};
4144
4175
  __export(init_exports, {
4145
4176
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
4146
4177
  HOOK_EVENTS: () => HOOK_EVENTS,
4178
+ LEARN_COMMAND: () => LEARN_COMMAND,
4147
4179
  PRE_TOOL_MATCHER: () => PRE_TOOL_MATCHER,
4148
4180
  bunshinHookEntry: () => bunshinHookEntry,
4149
4181
  claudeSettingsPath: () => claudeSettingsPath,
4150
4182
  countBunshinHooks: () => countBunshinHooks,
4151
4183
  installClaudeHooks: () => installClaudeHooks,
4184
+ installLearnCommand: () => installLearnCommand,
4152
4185
  installMcpConfig: () => installMcpConfig,
4153
4186
  installPiExtension: () => installPiExtension,
4154
4187
  isBunshinEntry: () => isBunshinEntry,
@@ -4259,6 +4292,24 @@ function mcpLauncher(bin) {
4259
4292
  }
4260
4293
  return { command: process.execPath, args: [script, "mcp"] };
4261
4294
  }
4295
+ function installLearnCommand({ file, remove = false }) {
4296
+ try {
4297
+ const current = fileExists(file) ? fs13.readFileSync(file, "utf8") : null;
4298
+ const ours = current === null || current === LEARN_COMMAND;
4299
+ if (remove) {
4300
+ if (current !== null && ours) fs13.unlinkSync(file);
4301
+ return { file, installed: 0, changed: current !== null && ours };
4302
+ }
4303
+ if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
4304
+ if (current === null) {
4305
+ fs13.mkdirSync(path10.dirname(file), { recursive: true });
4306
+ fs13.writeFileSync(file, LEARN_COMMAND);
4307
+ }
4308
+ return { file, installed: 1, changed: current === null };
4309
+ } catch (err) {
4310
+ return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
4311
+ }
4312
+ }
4262
4313
  function installMcpConfig({ file, launcher, remove = false }) {
4263
4314
  try {
4264
4315
  const before = fileExists(file) ? JSON.parse(fs13.readFileSync(file, "utf8")) : {};
@@ -4331,15 +4382,18 @@ function runInit(opts = {}) {
4331
4382
  if (hooks.error) warnings.push(`could not update ${hooks.file}: ${hooks.error}`);
4332
4383
  }
4333
4384
  let mcp;
4385
+ let learnCommand;
4334
4386
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code" && scope === "repo") {
4335
4387
  mcp = installMcpConfig({ file: path10.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
4336
4388
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
4389
+ learnCommand = installLearnCommand({ file: path10.join(cwd, ".claude", "commands", "learn.md"), remove: opts.removeHooks });
4390
+ if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
4337
4391
  }
4338
4392
  if (!resolveBackend(config)) {
4339
4393
  warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
4340
4394
  }
4341
4395
  store.log("init", { root, scope, role: config.role, orgUnit: config.orgUnit, hooks: hooks?.installed ?? 0 });
4342
- return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, warnings };
4396
+ return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, warnings };
4343
4397
  }
4344
4398
  function otherHarnessInstructions() {
4345
4399
  return [
@@ -4355,7 +4409,7 @@ function otherHarnessInstructions() {
4355
4409
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
4356
4410
  ].join("\n");
4357
4411
  }
4358
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS;
4412
+ var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND;
4359
4413
  var init_init = __esm({
4360
4414
  "src/init.ts"() {
4361
4415
  "use strict";
@@ -4379,6 +4433,7 @@ var init_init = __esm({
4379
4433
  { event: "PreCompact", arg: "pre-compact", timeout: 10 },
4380
4434
  { event: "SessionEnd", arg: "session-end", timeout: 3 }
4381
4435
  ];
4436
+ LEARN_COMMAND = '---\ndescription: Capture what this session learned into the Bunshin library now (applied locally as a trial, submitted for the nightly review)\nargument-hint: "[what to capture, e.g. \\"the ledger must be reconciled before closing\\"]"\nallowed-tools: Bash(bunshin learn:*)\n---\n\nCapture knowledge from this session into Bunshin.\n\nRun exactly this with the Bash tool (it takes up to two minutes; use a timeout of 180000 ms):\n\n```bash\nbunshin learn "$ARGUMENTS"\n```\n\n`bunshin learn` forks this conversation (`claude -p --resume <this session> --fork-session`),\nso the reflection sees everything you saw without re-sending the transcript. It applies what it\nfinds locally as a trial and queues it for the nightly review:\n\n- edits to this repo\'s SPEC.md / TESTING.md / SKILL.md land in the working tree (they show up\n in `git diff`);\n- knowledge with no repo home becomes a local overlay on the Bunshin library;\n- per-machine values stay private.\n\nRelay its output to the user as it is. Each line reads\n`captured: <summary> \u2192 <section or file#anchor>`. If it prints `nothing new to capture.`, say so\nin one line; that is the normal outcome. If it reports an error, show it and stop; do not retry\nin a loop, and do not edit the library files by hand.\n\nIf the user named nothing specific, run it without arguments. Do not continue with other work in\nthe same reply unless the user asked you to.\n';
4382
4437
  }
4383
4438
  });
4384
4439
 
@@ -7270,6 +7325,19 @@ async function cmdLibrary(store, args, cwd, out, err) {
7270
7325
  files: ${p.library}
7271
7326
  `);
7272
7327
  for (const x of pending) out(` edited here, awaiting the nightly: ${x.name}
7328
+ `);
7329
+ return 0;
7330
+ }
7331
+ case "workspace": {
7332
+ const target = args._[2];
7333
+ if (bool3(args.flags.clear)) setDeployWorkspace(null);
7334
+ else if (target) setDeployWorkspace(target);
7335
+ const ws = deployWorkspace();
7336
+ const p = deployPaths();
7337
+ out(ws ? `workspace: ${ws}
7338
+ ` : "workspace: none (skills and instructions are user-wide)\n");
7339
+ out(` skills: ${p.claudeSkills}, ${p.codexSkills}
7340
+ instructions: ${p.claudeMd}, ${p.agentsMd}
7273
7341
  `);
7274
7342
  return 0;
7275
7343
  }
@@ -8202,7 +8270,7 @@ init_util();
8202
8270
  init_project_hooks();
8203
8271
  function parseArgs(argv) {
8204
8272
  const out = { _: [], flags: {} };
8205
- const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites"]);
8273
+ const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear"]);
8206
8274
  for (let i = 0; i < argv.length; i++) {
8207
8275
  const a = argv[i];
8208
8276
  if (a === "--") {
@@ -8290,6 +8358,9 @@ Usage: bunshin <command> [options]
8290
8358
  the deployed skills; push proposes local edits / a new skill now
8291
8359
  library version|log|status|pin [--write [file]]|checkout <lib@N> --to <dir> [--git <sha>] [--pin <file>]
8292
8360
  library versions (lib@N), history, and exact reproductions
8361
+ library workspace [<dir>|--clear]
8362
+ deploy skills and instructions into a workspace folder (e.g.
8363
+ ~/Documents/dev) instead of ~/.claude and ~/.codex; set it before the first sync
8293
8364
  questions [list|answer <id> "<choice>"] open questions for your team
8294
8365
  suggest --code <harness path> [--file <local file>] [--rationale "<text>"]
8295
8366
  queue a robot harness code change (save_skill) for the release pipeline
@@ -8393,6 +8464,10 @@ Key saved privately outside the project.
8393
8464
  `);
8394
8465
  if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
8395
8466
  if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
8467
+ `);
8468
+ if (result.learnCommand?.installed) out(`/learn installed in ${result.learnCommand.file}
8469
+ `);
8470
+ else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
8396
8471
  `);
8397
8472
  if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
8398
8473
  if (harness === "codex" && result.hooks) out("Run codex interactively in this project. Review and accept project trust, then run /hooks and review and trust the five Bunshin hooks.\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.3.0",
3
+ "version": "0.3.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": {