buddy-builder 1.4.3 → 1.4.5

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/main.cjs CHANGED
@@ -30,7 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  var import_electron6 = require("electron");
31
31
  var path5 = __toESM(require("path"), 1);
32
32
  var fs3 = __toESM(require("fs"), 1);
33
- var import_child_process4 = require("child_process");
33
+ var import_child_process5 = require("child_process");
34
34
 
35
35
  // node_modules/zod/v4/classic/external.js
36
36
  var external_exports = {};
@@ -13818,7 +13818,9 @@ var ContentBlockSchema = external_exports.union([
13818
13818
  ]);
13819
13819
  var UsageSchema = external_exports.object({
13820
13820
  input_tokens: external_exports.number(),
13821
- output_tokens: external_exports.number()
13821
+ output_tokens: external_exports.number(),
13822
+ cache_read_input_tokens: external_exports.number().optional(),
13823
+ cache_creation_input_tokens: external_exports.number().optional()
13822
13824
  }).catchall(external_exports.unknown());
13823
13825
  var InitMessageSchema = external_exports.object({
13824
13826
  type: external_exports.literal("system"),
@@ -13911,6 +13913,11 @@ var SessionConfigSchema = external_exports.object({
13911
13913
  permissionMode: external_exports.enum(PermissionModes).optional(),
13912
13914
  resumeSessionId: external_exports.string().optional(),
13913
13915
  maxTurns: external_exports.number().positive().optional(),
13916
+ maxBudgetUsd: external_exports.number().positive().optional(),
13917
+ fallbackModel: external_exports.string().optional(),
13918
+ addDirs: external_exports.array(external_exports.string()).optional(),
13919
+ effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
13920
+ worktree: external_exports.union([external_exports.boolean(), external_exports.string()]).optional(),
13914
13921
  noSessionPersistence: external_exports.boolean().optional(),
13915
13922
  env: external_exports.record(external_exports.string(), external_exports.string()).optional()
13916
13923
  });
@@ -13966,7 +13973,7 @@ function openInTerminal(cwd, command) {
13966
13973
 
13967
13974
  // src/main/manager.ts
13968
13975
  var import_crypto2 = require("crypto");
13969
- var import_child_process3 = require("child_process");
13976
+ var import_child_process4 = require("child_process");
13970
13977
  var os2 = __toESM(require("os"), 1);
13971
13978
 
13972
13979
  // src/emitter.ts
@@ -14106,7 +14113,15 @@ var import_fs = require("fs");
14106
14113
  var import_path = require("path");
14107
14114
  var import_os = require("os");
14108
14115
  var import_crypto = require("crypto");
14116
+ var import_child_process3 = require("child_process");
14109
14117
  var DEFAULT_CLAUDE_PATH = process.platform === "win32" ? "claude.exe" : "claude";
14118
+ var resolvedClaudePath = null;
14119
+ if (process.platform === "win32") {
14120
+ try {
14121
+ resolvedClaudePath = (0, import_child_process3.execSync)("where claude", { encoding: "utf-8", timeout: 3e3 }).trim().split(/\r?\n/)[0].replace(/\//g, "\\") || null;
14122
+ } catch {
14123
+ }
14124
+ }
14110
14125
  function buildArgs(config2, settingsFilePath) {
14111
14126
  const args = [
14112
14127
  "--print",
@@ -14115,15 +14130,23 @@ function buildArgs(config2, settingsFilePath) {
14115
14130
  "--output-format",
14116
14131
  "stream-json",
14117
14132
  "--verbose",
14118
- "--include-partial-messages",
14119
14133
  "--settings",
14120
14134
  settingsFilePath
14121
14135
  ];
14122
14136
  if (config2.model) args.push("--model", config2.model);
14123
14137
  args.push("--dangerously-skip-permissions");
14138
+ if (config2.systemPrompt) args.push("--append-system-prompt", config2.systemPrompt);
14124
14139
  if (config2.resumeSessionId) args.push("--resume", config2.resumeSessionId);
14125
14140
  if (config2.maxTurns != null) args.push("--max-turns", String(config2.maxTurns));
14141
+ if (config2.maxBudgetUsd != null) args.push("--max-budget-usd", String(config2.maxBudgetUsd));
14142
+ if (config2.fallbackModel) args.push("--fallback-model", config2.fallbackModel);
14143
+ if (config2.effort) args.push("--effort", config2.effort);
14144
+ if (config2.worktree === true) args.push("--worktree");
14145
+ else if (typeof config2.worktree === "string" && config2.worktree) args.push("--worktree", config2.worktree);
14126
14146
  if (config2.noSessionPersistence) args.push("--no-session-persistence");
14147
+ for (const dir of config2.addDirs ?? []) {
14148
+ args.push("--add-dir", dir);
14149
+ }
14127
14150
  for (const tool of config2.allowedTools ?? []) {
14128
14151
  args.push("--allowedTools", tool);
14129
14152
  }
@@ -14133,7 +14156,10 @@ function buildArgs(config2, settingsFilePath) {
14133
14156
  return args;
14134
14157
  }
14135
14158
  function spawnClaude(config2, hookPort, settingsJson) {
14136
- const claudePath = config2.claudePath ?? DEFAULT_CLAUDE_PATH;
14159
+ let claudePath = resolvedClaudePath ?? config2.claudePath ?? DEFAULT_CLAUDE_PATH;
14160
+ if (process.platform === "win32") {
14161
+ claudePath = claudePath.replace(/\//g, "\\");
14162
+ }
14137
14163
  const settingsDir = (0, import_path.join)((0, import_os.tmpdir)(), "buddy-builder");
14138
14164
  (0, import_fs.mkdirSync)(settingsDir, { recursive: true });
14139
14165
  const settingsFile = (0, import_path.join)(settingsDir, `settings-${(0, import_crypto.randomUUID)()}.json`);
@@ -14676,6 +14702,7 @@ function applyEvent(entries, event) {
14676
14702
  case "stateChange":
14677
14703
  case "warn":
14678
14704
  case "nameChanged":
14705
+ case "effortChanged":
14679
14706
  case "popoutChanged":
14680
14707
  return false;
14681
14708
  }
@@ -14964,9 +14991,11 @@ function wireSession(managed, session, sink, onAssistantTurn) {
14964
14991
  });
14965
14992
  session.on("message", (msg) => {
14966
14993
  if (msg.message.usage) {
14967
- managed.totalInputTokens += msg.message.usage.input_tokens;
14968
- managed.totalOutputTokens += msg.message.usage.output_tokens;
14969
- sink({ kind: "usage", sessionId: id, inputTokens: msg.message.usage.input_tokens, outputTokens: msg.message.usage.output_tokens });
14994
+ const u = msg.message.usage;
14995
+ const totalIn = u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
14996
+ managed.totalInputTokens += totalIn;
14997
+ managed.totalOutputTokens += u.output_tokens;
14998
+ sink({ kind: "usage", sessionId: id, inputTokens: totalIn, outputTokens: u.output_tokens });
14970
14999
  }
14971
15000
  });
14972
15001
  session.on("stop", (ev) => {
@@ -15024,7 +15053,7 @@ async function requestAutoName(managed, claudePath, sink) {
15024
15053
  const env = { ...process.env };
15025
15054
  delete env.CLAUDECODE;
15026
15055
  const rawTitle = await new Promise((resolve, reject) => {
15027
- const child = (0, import_child_process3.spawn)(
15056
+ const child = (0, import_child_process4.spawn)(
15028
15057
  claudePath,
15029
15058
  ["-p", prompt, "--no-session-persistence", "--max-turns", "1"],
15030
15059
  { stdio: ["ignore", "pipe", "pipe"], env, cwd: os2.tmpdir() }
@@ -15089,7 +15118,7 @@ function createSessionManager(sink, claudePath) {
15089
15118
  const id = stub.claudeSessionId;
15090
15119
  const managed = {
15091
15120
  id,
15092
- name: m.name || stub.slug || "Untitled Session",
15121
+ name: m.name || stub.slug || "untitled session",
15093
15122
  projectName: stub.projectName,
15094
15123
  session: null,
15095
15124
  claudeSessionId: stub.claudeSessionId,
@@ -15112,6 +15141,7 @@ function createSessionManager(sink, claudePath) {
15112
15141
  totalInputTokens: 0,
15113
15142
  totalOutputTokens: 0,
15114
15143
  totalCost: 0,
15144
+ effort: null,
15115
15145
  autoNamed: true,
15116
15146
  // don't auto-name until resumed
15117
15147
  userNamed: !!m.name,
@@ -15142,6 +15172,40 @@ function createSessionManager(sink, claudePath) {
15142
15172
  });
15143
15173
  }
15144
15174
  }
15175
+ async function killAndResume(managed, configOverrides, successMsg, errorPrefix) {
15176
+ if (!managed.session) throw new Error("Session is dead");
15177
+ if (managed.session.state !== "idle") throw new Error("Session must be idle");
15178
+ if (!managed.claudeSessionId) throw new Error("No Claude session ID");
15179
+ const exitPromise = new Promise((resolve) => {
15180
+ const check2 = () => {
15181
+ if (!managed.session) resolve();
15182
+ else setTimeout(check2, 50);
15183
+ };
15184
+ check2();
15185
+ });
15186
+ managed.session.kill();
15187
+ await Promise.race([exitPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out waiting for session exit")), 3e3))]);
15188
+ try {
15189
+ const config2 = {
15190
+ claudePath,
15191
+ permissionMode: managed.permissionMode,
15192
+ resumeSessionId: managed.claudeSessionId,
15193
+ cwd: managed.cwd ?? void 0,
15194
+ ...configOverrides
15195
+ };
15196
+ const session = await createSession(config2);
15197
+ managed.session = session;
15198
+ session.setToolPolicy(buildToolPolicy(managed.policy, managed.permissionMode));
15199
+ ensureEntries(managed).push({ kind: "system", text: successMsg, ts: Date.now() });
15200
+ managed.lastActiveAt = Date.now();
15201
+ wireSession(managed, session, sink, () => onAssistantTurn(managed));
15202
+ sink({ kind: "stateChange", sessionId: managed.id, from: "dead", to: session.state });
15203
+ } catch (err) {
15204
+ const msg = `${errorPrefix}: ${err instanceof Error ? err.message : String(err)}`;
15205
+ ensureEntries(managed).push({ kind: "system", text: msg, ts: Date.now() });
15206
+ sink({ kind: "error", sessionId: managed.id, message: msg });
15207
+ }
15208
+ }
15145
15209
  return {
15146
15210
  async create(options) {
15147
15211
  const id = (0, import_crypto2.randomUUID)();
@@ -15155,7 +15219,12 @@ function createSessionManager(sink, claudePath) {
15155
15219
  cwd: cwd ?? void 0,
15156
15220
  model: options?.model,
15157
15221
  systemPrompt: options?.systemPrompt,
15158
- maxTurns: options?.maxTurns
15222
+ maxTurns: options?.maxTurns,
15223
+ maxBudgetUsd: options?.maxBudgetUsd,
15224
+ fallbackModel: options?.fallbackModel,
15225
+ addDirs: options?.addDirs ? [...options.addDirs] : void 0,
15226
+ effort: options?.effort,
15227
+ worktree: options?.worktree
15159
15228
  };
15160
15229
  const session = await createSession(config2);
15161
15230
  const initialPreset = PERMISSION_MODE_PRESETS[permMode];
@@ -15184,6 +15253,7 @@ function createSessionManager(sink, claudePath) {
15184
15253
  totalInputTokens: 0,
15185
15254
  totalOutputTokens: 0,
15186
15255
  totalCost: 0,
15256
+ effort: options?.effort ?? null,
15187
15257
  autoNamed: !!options?.name,
15188
15258
  userNamed: !!options?.name,
15189
15259
  turnCount: 0
@@ -15269,6 +15339,66 @@ function createSessionManager(sink, claudePath) {
15269
15339
  wireSession(managed, session, sink, () => onAssistantTurn(managed));
15270
15340
  sink({ kind: "stateChange", sessionId: id, from: "dead", to: session.state });
15271
15341
  },
15342
+ async changeModel(id, model) {
15343
+ const managed = getManaged(id);
15344
+ await killAndResume(managed, { model }, `Model changed to ${model}.`, `Failed to change model`);
15345
+ },
15346
+ async changeEffort(id, effort) {
15347
+ const managed = getManaged(id);
15348
+ await killAndResume(managed, { effort, model: managed.model ?? void 0 }, `Effort level changed to ${effort}.`, `Failed to change effort`);
15349
+ managed.effort = effort;
15350
+ sink({ kind: "effortChanged", sessionId: id, effort });
15351
+ },
15352
+ async fork(id) {
15353
+ const source = getManaged(id);
15354
+ if (!source.claudeSessionId) throw new Error("No Claude session ID \u2014 cannot fork");
15355
+ const forkId = (0, import_crypto2.randomUUID)();
15356
+ counter++;
15357
+ const now = Date.now();
15358
+ const config2 = {
15359
+ claudePath,
15360
+ permissionMode: source.permissionMode,
15361
+ resumeSessionId: source.claudeSessionId,
15362
+ cwd: source.cwd ?? void 0,
15363
+ model: source.model ?? void 0
15364
+ };
15365
+ const session = await createSession(config2);
15366
+ const policy = { ...source.policy };
15367
+ const managed = {
15368
+ id: forkId,
15369
+ name: `${source.name} (fork)`,
15370
+ projectName: source.projectName,
15371
+ session,
15372
+ claudeSessionId: null,
15373
+ // will be set on ready event
15374
+ cwd: source.cwd,
15375
+ transcriptPath: null,
15376
+ policy,
15377
+ permissionMode: source.permissionMode,
15378
+ entries: [{ kind: "system", text: `Forked from "${source.name}".`, ts: now }],
15379
+ favorite: false,
15380
+ createdAt: now,
15381
+ lastActiveAt: now,
15382
+ model: source.model,
15383
+ claudeCodeVersion: null,
15384
+ tools: [],
15385
+ mcpServers: [],
15386
+ skills: [],
15387
+ agents: [],
15388
+ slashCommands: [],
15389
+ totalInputTokens: 0,
15390
+ totalOutputTokens: 0,
15391
+ totalCost: 0,
15392
+ effort: source.effort,
15393
+ autoNamed: false,
15394
+ userNamed: false,
15395
+ turnCount: 0
15396
+ };
15397
+ sessions.set(forkId, managed);
15398
+ session.setToolPolicy(buildToolPolicy(policy, source.permissionMode));
15399
+ wireSession(managed, session, sink, () => onAssistantTurn(managed));
15400
+ return forkId;
15401
+ },
15272
15402
  getResumeInfo(id) {
15273
15403
  const managed = getManaged(id);
15274
15404
  if (!managed.claudeSessionId) throw new Error("No Claude session ID to resume");
@@ -15289,6 +15419,71 @@ function createSessionManager(sink, claudePath) {
15289
15419
  getEntries(id) {
15290
15420
  return ensureEntries(getManaged(id));
15291
15421
  },
15422
+ exportSession(id, format) {
15423
+ const managed = getManaged(id);
15424
+ const entries = ensureEntries(managed);
15425
+ if (format === "json") {
15426
+ return JSON.stringify({
15427
+ sessionId: managed.id,
15428
+ name: managed.name,
15429
+ projectName: managed.projectName,
15430
+ cwd: managed.cwd,
15431
+ model: managed.model,
15432
+ createdAt: managed.createdAt,
15433
+ entries: entries.map((e) => ({ ...e }))
15434
+ }, null, 2);
15435
+ }
15436
+ const lines = [];
15437
+ lines.push(`# ${managed.name}`);
15438
+ lines.push("");
15439
+ if (managed.cwd) lines.push(`**Project:** ${managed.cwd}`);
15440
+ if (managed.model) lines.push(`**Model:** ${managed.model}`);
15441
+ lines.push(`**Created:** ${new Date(managed.createdAt).toISOString()}`);
15442
+ lines.push("");
15443
+ lines.push("---");
15444
+ lines.push("");
15445
+ for (const e of entries) {
15446
+ switch (e.kind) {
15447
+ case "user":
15448
+ lines.push(`## User`);
15449
+ lines.push("");
15450
+ lines.push(e.text);
15451
+ lines.push("");
15452
+ break;
15453
+ case "text":
15454
+ lines.push(`## Assistant`);
15455
+ lines.push("");
15456
+ lines.push(e.text);
15457
+ lines.push("");
15458
+ break;
15459
+ case "tool":
15460
+ lines.push(`### Tool: ${e.toolName}`);
15461
+ lines.push("");
15462
+ if (e.detail) lines.push(`> ${e.detail}`);
15463
+ if (e.toolResult) {
15464
+ lines.push("```");
15465
+ lines.push(e.toolResult.slice(0, 2e3));
15466
+ lines.push("```");
15467
+ }
15468
+ lines.push("");
15469
+ break;
15470
+ case "system":
15471
+ lines.push(`*${e.text}*`);
15472
+ lines.push("");
15473
+ break;
15474
+ case "result":
15475
+ lines.push(`---`);
15476
+ lines.push(`*Cost: $${e.cost.toFixed(4)} \xB7 ${e.turns} turns \xB7 ${(e.durationMs / 1e3).toFixed(1)}s*`);
15477
+ lines.push("");
15478
+ break;
15479
+ case "compact":
15480
+ lines.push(`*Context compacted (${e.trigger})*`);
15481
+ lines.push("");
15482
+ break;
15483
+ }
15484
+ }
15485
+ return lines.join("\n");
15486
+ },
15292
15487
  getMeta(id) {
15293
15488
  const s = getManaged(id);
15294
15489
  return {
@@ -16010,6 +16205,15 @@ function createPopoutWindow(sessionId) {
16010
16205
  register({ kind: "popout", win, sessionId });
16011
16206
  dispatch({ kind: "popoutChanged", sessionId, poppedOut: true });
16012
16207
  dlog(`[popout] created id=${win.id} session=${sessionId}`);
16208
+ win.webContents.on("console-message", (_e, level, message, line, sourceId) => {
16209
+ if (level >= 2) dlog(`[popout-console][ERROR] ${message} (${sourceId}:${line})`);
16210
+ });
16211
+ win.webContents.on("did-fail-load", (_e, code, desc) => {
16212
+ dlog(`[popout] DID-FAIL-LOAD code=${code} desc=${desc}`);
16213
+ });
16214
+ win.webContents.on("preload-error", (_e, preloadPath, error48) => {
16215
+ dlog(`[popout] PRELOAD-ERROR path=${preloadPath} error=${error48}`);
16216
+ });
16013
16217
  win.webContents.on("render-process-gone", (_e, details) => {
16014
16218
  dlog(`[popout] RENDER-PROCESS-GONE id=${win.id} reason=${details.reason} exitCode=${details.exitCode}`);
16015
16219
  });
@@ -16093,11 +16297,15 @@ function buildHandlers(mgr) {
16093
16297
  mgr.setFavorite(sessionId, favorite);
16094
16298
  },
16095
16299
  resumeSession: ({ sessionId }) => mgr.resume(sessionId),
16300
+ changeModel: ({ sessionId, model }) => mgr.changeModel(sessionId, model),
16301
+ forkSession: ({ sessionId }) => mgr.fork(sessionId),
16302
+ changeEffort: ({ sessionId, effort }) => mgr.changeEffort(sessionId, effort),
16096
16303
  resumeInTerminal: ({ sessionId }) => {
16097
16304
  const { claudeSessionId, cwd, permissionMode } = mgr.getResumeInfo(sessionId);
16098
16305
  const skipPerms = permissionMode === "bypassPermissions" ? " --dangerously-skip-permissions" : "";
16099
16306
  openInTerminal(cwd ?? process.env.HOME ?? process.env.USERPROFILE ?? ".", `claude --resume ${claudeSessionId}${skipPerms}`);
16100
16307
  },
16308
+ exportSession: ({ sessionId, format }) => mgr.exportSession(sessionId, format),
16101
16309
  deleteSession: ({ sessionId }) => {
16102
16310
  mgr.remove(sessionId);
16103
16311
  },
@@ -16310,14 +16518,14 @@ import_electron6.app.whenReady().then(() => {
16310
16518
  let claudePath = config2.claudePath;
16311
16519
  try {
16312
16520
  const shell2 = process.env.SHELL || "/bin/zsh";
16313
- const rawPath = (0, import_child_process4.execSync)(`${shell2} -ilc "echo \\$PATH"`, { encoding: "utf-8" });
16521
+ const rawPath = (0, import_child_process5.execSync)(`${shell2} -ilc "echo \\$PATH"`, { encoding: "utf-8" });
16314
16522
  const shellPath = rawPath.trim().split("\n").filter(Boolean).pop();
16315
16523
  if (shellPath) {
16316
16524
  process.env.PATH = shellPath;
16317
16525
  console.log("[shell] inherited PATH from login shell");
16318
16526
  }
16319
16527
  if (claudePath === "claude") {
16320
- const raw = (0, import_child_process4.execSync)(`${shell2} -ilc "which claude"`, { encoding: "utf-8" });
16528
+ const raw = (0, import_child_process5.execSync)(`${shell2} -ilc "which claude"`, { encoding: "utf-8" });
16321
16529
  claudePath = raw.trim().split("\n").filter(Boolean).pop() ?? "claude";
16322
16530
  console.log("[claude] resolved path:", claudePath);
16323
16531
  }