@lesscap/baton-cli 0.2.0 → 0.2.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/baton.mjs +71 -17
  2. package/package.json +1 -1
package/dist/baton.mjs CHANGED
@@ -578,6 +578,7 @@ var sessionsClient = (baseUrl) => {
578
578
  resume: (id) => request(u(`/sessions/${id}/resume`), { method: "POST" }),
579
579
  stop: (id) => request(u(`/sessions/${id}/stop`), { method: "POST" }),
580
580
  clear: (id) => request(u(`/sessions/${id}/clear`), { method: "POST" }),
581
+ abort: (id) => request(u(`/sessions/${id}/abort`), { method: "POST" }),
581
582
  rename: (id, name) => request(u(`/sessions/${id}/rename`), { method: "POST", body: { name } }),
582
583
  listByProject: (projectId) => request(u(`/projects/${projectId}/sessions`), { method: "GET" }),
583
584
  get: (id) => request(u(`/sessions/${id}`), { method: "GET" }),
@@ -1035,6 +1036,22 @@ var resolveSession = async (client, projectId, handle) => {
1035
1036
  throw new Error(`session "${handle}" not found in project ${projectId}`);
1036
1037
  };
1037
1038
 
1039
+ // src/commands/session/abort.ts
1040
+ var sessionAbortCommand = defineCommand({
1041
+ meta: { name: "abort", description: "interrupt the in-flight turn (like Esc); keeps the session" },
1042
+ args: {
1043
+ session: { type: "positional", required: true, description: "session int id or name" },
1044
+ project: { type: "string", description: "project id (overrides .baton.json)" },
1045
+ ...common
1046
+ },
1047
+ run: async ({ args }) => {
1048
+ const c = clientFor(args);
1049
+ const handle = await resolveSession(c, resolveProjectId(args), args.session);
1050
+ const s = await c.sessions.abort(handle.id);
1051
+ console.log(renderOne(s, fmtSession, Boolean(args.json)));
1052
+ }
1053
+ });
1054
+
1038
1055
  // src/commands/session/clear.ts
1039
1056
  var sessionClearCommand = defineCommand({
1040
1057
  meta: { name: "clear", description: "reset the conversation context (keeps the session + worktree)" },
@@ -1638,14 +1655,22 @@ function getBaseURL() {
1638
1655
 
1639
1656
  // src/session/runner.ts
1640
1657
  import { spawnSync } from "node:child_process";
1641
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1658
+ import { query } from "@anthropic-ai/claude-agent-sdk";
1642
1659
 
1643
1660
  // src/session/runner/log.ts
1644
1661
  var maskedEnvKeys = (env) => env ? Object.keys(env).sort().join(", ") : "(none)";
1645
1662
 
1646
1663
  // src/session/runner/sdk-env.ts
1664
+ import { existsSync as existsSync3 } from "node:fs";
1665
+ import { isAbsolute } from "node:path";
1647
1666
  var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
1648
1667
  var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
1668
+ var additionalDirs = () => {
1669
+ const raw = process.env.BATON_ADD_DIRS;
1670
+ if (!raw) return void 0;
1671
+ const dirs = raw.split(":").map((s) => s.trim()).filter((s) => s !== "" && isAbsolute(s) && existsSync3(s));
1672
+ return dirs.length > 0 ? dirs : void 0;
1673
+ };
1649
1674
 
1650
1675
  // src/session/runner/transcript.ts
1651
1676
  import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1770,10 +1795,11 @@ ${text}` : header;
1770
1795
  };
1771
1796
 
1772
1797
  // src/session/runner/query.ts
1773
- import "@anthropic-ai/claude-agent-sdk";
1774
1798
  var startQuery = (config, text, resuming, queryFn, abortController, log, envOverlay) => {
1775
1799
  const env = buildSdkEnv(envOverlay);
1776
1800
  const exe = claudeExecutable();
1801
+ const addDirs = additionalDirs();
1802
+ if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
1777
1803
  const options = {
1778
1804
  cwd: config.worktreePath,
1779
1805
  permissionMode: "bypassPermissions",
@@ -1782,7 +1808,8 @@ var startQuery = (config, text, resuming, queryFn, abortController, log, envOver
1782
1808
  stderr: (line) => log(`[claude] ${line.trimEnd()}`),
1783
1809
  ...resuming ? { resume: config.agentSessionId } : { sessionId: config.agentSessionId },
1784
1810
  ...env ? { env } : {},
1785
- ...exe ? { pathToClaudeCodeExecutable: exe } : {}
1811
+ ...exe ? { pathToClaudeCodeExecutable: exe } : {},
1812
+ ...addDirs ? { additionalDirectories: addDirs } : {}
1786
1813
  };
1787
1814
  return queryFn({ prompt: text, options });
1788
1815
  };
@@ -1807,6 +1834,7 @@ var toTurnResult = (message) => {
1807
1834
  // src/session/runner/turn.ts
1808
1835
  var turnTimeoutMs = () => Number(process.env.BATON_TURN_TIMEOUT_MS) || 30 * 6e4;
1809
1836
  var TIMEOUT = /* @__PURE__ */ Symbol("turn-timeout");
1837
+ var ABORTED = /* @__PURE__ */ Symbol("turn-aborted");
1810
1838
  var finalizeTurn = async (result, worker2, log) => {
1811
1839
  if (!result) {
1812
1840
  log("[exit] stream ended with no result");
@@ -1835,7 +1863,7 @@ var buildPrompt = async (config, rawText, attachments, log, fetchImpl) => {
1835
1863
  log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
1836
1864
  return augmentPrompt(rawText, relPaths, labelAttachments(attachments));
1837
1865
  };
1838
- var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => console.log(m), envOverlay, fetchImpl) => {
1866
+ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => console.log(m), envOverlay, fetchImpl, externalSignal) => {
1839
1867
  await worker2.emitEvent("turn_start", { messageId: msg.id });
1840
1868
  const payload = msg.payload;
1841
1869
  const rawText = typeof payload?.text === "string" ? payload.text : "";
@@ -1858,12 +1886,23 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
1858
1886
  const timeout = new Promise((resolve) => {
1859
1887
  timer = setTimeout(() => resolve(TIMEOUT), ceiling);
1860
1888
  });
1889
+ const interrupted = new Promise((resolve) => {
1890
+ if (externalSignal?.aborted) resolve(ABORTED);
1891
+ else externalSignal?.addEventListener("abort", () => resolve(ABORTED), { once: true });
1892
+ });
1861
1893
  try {
1862
1894
  const messages = startQuery(config, text, resuming, queryFn, abort, log, envOverlay);
1863
1895
  const consume = streamSdkEvents(messages, worker2);
1864
1896
  void consume.catch(() => {
1865
1897
  });
1866
- const outcome = await Promise.race([consume, timeout]);
1898
+ const outcome = await Promise.race([consume, timeout, interrupted]);
1899
+ if (outcome === ABORTED) {
1900
+ log("[abort] interrupted by user");
1901
+ abort.abort();
1902
+ await worker2.emitEvent("turn_error", { message: "interrupted by user" });
1903
+ await worker2.emitEvent("turn_complete", {});
1904
+ return -1;
1905
+ }
1867
1906
  if (outcome === TIMEOUT) {
1868
1907
  log(`[watchdog] turn exceeded ${ceiling}ms \u2014 aborting claude`);
1869
1908
  abort.abort();
@@ -1884,7 +1923,7 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
1884
1923
  };
1885
1924
 
1886
1925
  // src/session/runner.ts
1887
- var subscribeStream = (url, ESCtor, seen, log, onUserMessage, onReady) => {
1926
+ var subscribeStream = (url, ESCtor, seen, log, onUserMessage, onInterrupt, onReady) => {
1888
1927
  const es = new ESCtor(url);
1889
1928
  es.onopen = onReady;
1890
1929
  es.onmessage = (e) => {
@@ -1893,6 +1932,8 @@ var subscribeStream = (url, ESCtor, seen, log, onUserMessage, onReady) => {
1893
1932
  if (seen.has(ev.sequence)) return;
1894
1933
  seen.add(ev.sequence);
1895
1934
  if (ev.type === "user_message") onUserMessage(ev);
1935
+ else if (ev.type === "system" && ev.payload?.action === "interrupt")
1936
+ onInterrupt();
1896
1937
  } catch {
1897
1938
  }
1898
1939
  };
@@ -1914,7 +1955,7 @@ var restoreTty = () => {
1914
1955
  };
1915
1956
  var runDaemon = async (config, deps, signal) => {
1916
1957
  const log = deps.log ?? ((m) => console.log(`[#${config.sessionId} ${config.name}] ${m}`));
1917
- const qf = deps.queryFn ?? query2;
1958
+ const qf = deps.queryFn ?? query;
1918
1959
  const ESCtor = deps.eventSourceImpl ?? EventSource;
1919
1960
  log(`bin: ${claudeExecutable() ?? "(sdk bundled)"} cwd: ${config.worktreePath}`);
1920
1961
  log(`runtime env keys: ${maskedEnvKeys(deps.env)}`);
@@ -1925,6 +1966,7 @@ var runDaemon = async (config, deps, signal) => {
1925
1966
  const pendingQueue = [];
1926
1967
  let busy = false;
1927
1968
  let lastActivity = Date.now();
1969
+ let currentTurn;
1928
1970
  const drain = async () => {
1929
1971
  if (busy) return;
1930
1972
  busy = true;
@@ -1934,6 +1976,8 @@ var runDaemon = async (config, deps, signal) => {
1934
1976
  if (!msg) break;
1935
1977
  const resuming = state.firstSpawnDone;
1936
1978
  log(`\u25B6 msg #${msg.sequence} (${resuming ? "resume" : "first"})`);
1979
+ const turnAbort = new AbortController();
1980
+ currentTurn = turnAbort;
1937
1981
  const code = await runTurn(
1938
1982
  config,
1939
1983
  deps.worker,
@@ -1942,8 +1986,10 @@ var runDaemon = async (config, deps, signal) => {
1942
1986
  qf,
1943
1987
  log,
1944
1988
  deps.env,
1945
- deps.fetchImpl
1989
+ deps.fetchImpl,
1990
+ turnAbort.signal
1946
1991
  );
1992
+ currentTurn = void 0;
1947
1993
  state.firstSpawnDone = true;
1948
1994
  lastActivity = Date.now();
1949
1995
  log(`\u2714 msg #${msg.sequence}${code === 0 ? "" : ` (exit ${code})`}`);
@@ -1964,6 +2010,13 @@ var runDaemon = async (config, deps, signal) => {
1964
2010
  pendingQueue.push(ev);
1965
2011
  void drain();
1966
2012
  },
2013
+ () => {
2014
+ lastActivity = Date.now();
2015
+ if (currentTurn) {
2016
+ log("interrupt \u2014 aborting current turn");
2017
+ currentTurn.abort();
2018
+ }
2019
+ },
1967
2020
  // Report active only now that we're subscribed — before this, a message
1968
2021
  // sent right after spawn would be missed (no replay on the live stream).
1969
2022
  () => {
@@ -2144,6 +2197,7 @@ var session = defineCommand({
2144
2197
  resume: sessionResumeCommand,
2145
2198
  stop: sessionStopCommand,
2146
2199
  clear: sessionClearCommand,
2200
+ abort: sessionAbortCommand,
2147
2201
  rename: sessionRenameCommand,
2148
2202
  rm: sessionRmCommand,
2149
2203
  send: sessionSendCommand,
@@ -2297,10 +2351,10 @@ import { hostname as osHostname } from "node:os";
2297
2351
  // src/worker/daemon.ts
2298
2352
  import { spawn } from "node:child_process";
2299
2353
  import { randomUUID } from "node:crypto";
2300
- import { existsSync as existsSync4 } from "node:fs";
2354
+ import { existsSync as existsSync5 } from "node:fs";
2301
2355
  import { dirname as dirname2, join as join5 } from "node:path";
2302
2356
  import { fileURLToPath } from "node:url";
2303
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2357
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2304
2358
 
2305
2359
  // src/session/runner/title.ts
2306
2360
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
@@ -2347,10 +2401,10 @@ ${exchange}`;
2347
2401
 
2348
2402
  // src/session/worktree.ts
2349
2403
  import { execFileSync, spawnSync as spawnSync2 } from "node:child_process";
2350
- import { existsSync as existsSync3, mkdirSync } from "node:fs";
2404
+ import { existsSync as existsSync4, mkdirSync } from "node:fs";
2351
2405
  import { dirname } from "node:path";
2352
2406
  var createWorktree = (input) => {
2353
- if (existsSync3(input.worktreePath))
2407
+ if (existsSync4(input.worktreePath))
2354
2408
  throw new Error(`worktree path already exists: ${input.worktreePath}`);
2355
2409
  mkdirSync(dirname(input.worktreePath), { recursive: true });
2356
2410
  const branch = `baton/${input.sessionCode}`;
@@ -2372,7 +2426,7 @@ var repoHeadBranch = (repo) => {
2372
2426
  return branch || "main";
2373
2427
  };
2374
2428
  var removeWorktree = (repo, worktreePath) => {
2375
- if (!existsSync3(worktreePath)) return;
2429
+ if (!existsSync4(worktreePath)) return;
2376
2430
  try {
2377
2431
  execFileSync("git", ["-C", repo, "worktree", "remove", "--force", worktreePath], {
2378
2432
  stdio: "pipe"
@@ -2385,7 +2439,7 @@ var removeWorktree = (repo, worktreePath) => {
2385
2439
  var binPath = () => {
2386
2440
  const here = dirname2(fileURLToPath(import.meta.url));
2387
2441
  const devShim = join5(here, "..", "..", "bin", "baton.mjs");
2388
- return existsSync4(devShim) ? devShim : fileURLToPath(import.meta.url);
2442
+ return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
2389
2443
  };
2390
2444
  var runWorkerDaemon = async (cfg, client, signal) => {
2391
2445
  const repo = process.cwd();
@@ -2466,7 +2520,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2466
2520
  worktreePath,
2467
2521
  userText: exchange.userText,
2468
2522
  assistantText: exchange.assistantText,
2469
- queryFn: query3
2523
+ queryFn: query2
2470
2524
  });
2471
2525
  if (!title) return log(`title #${sessionId}: not enough to title yet, skipping`);
2472
2526
  await client.sessions.setName(sessionId, title, cfg.apiToken);
@@ -2522,12 +2576,12 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2522
2576
 
2523
2577
  // src/worker/machine-id.ts
2524
2578
  import { randomUUID as randomUUID2 } from "node:crypto";
2525
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2579
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2526
2580
  import { homedir as homedir3 } from "node:os";
2527
2581
  import { dirname as dirname3, join as join6 } from "node:path";
2528
2582
  var machineIdPath = (env = process.env) => join6(env.XDG_DATA_HOME ?? join6(env.HOME ?? homedir3(), ".local/share"), "baton", "machine-id");
2529
2583
  var readOrCreateMachineId = (path = machineIdPath()) => {
2530
- if (existsSync5(path)) {
2584
+ if (existsSync6(path)) {
2531
2585
  const s = readFileSync3(path, "utf8").trim();
2532
2586
  if (s) return s;
2533
2587
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
6
6
  "bin": {