@lesscap/baton-cli 0.1.3 → 0.2.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.
Files changed (2) hide show
  1. package/dist/baton.mjs +116 -178
  2. package/package.json +5 -2
package/dist/baton.mjs CHANGED
@@ -1637,34 +1637,15 @@ function getBaseURL() {
1637
1637
  }
1638
1638
 
1639
1639
  // src/session/runner.ts
1640
- import { spawn, spawnSync } from "node:child_process";
1640
+ import { spawnSync } from "node:child_process";
1641
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1641
1642
 
1642
1643
  // src/session/runner/log.ts
1643
- var claudeBin = () => process.env.BATON_CLAUDE_BIN ?? "claude";
1644
- var buildClaudeArgs = (claudeSessionId, text, resuming) => [
1645
- "--print",
1646
- resuming ? "--resume" : "--session-id",
1647
- claudeSessionId,
1648
- "--output-format",
1649
- "stream-json",
1650
- "--verbose",
1651
- "--dangerously-skip-permissions",
1652
- text
1653
- ];
1654
- var STDERR_TAIL_BYTES = 2048;
1655
- var tailBuffer = (cap = STDERR_TAIL_BYTES) => {
1656
- let buf = "";
1657
- return {
1658
- append(chunk) {
1659
- buf = (buf + chunk).slice(-cap);
1660
- },
1661
- toString() {
1662
- return buf;
1663
- }
1664
- };
1665
- };
1666
1644
  var maskedEnvKeys = (env) => env ? Object.keys(env).sort().join(", ") : "(none)";
1667
- var previewText = (text, max = 80) => text.length > max ? `${text.slice(0, max)}\u2026` : text;
1645
+
1646
+ // src/session/runner/sdk-env.ts
1647
+ var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
1648
+ var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
1668
1649
 
1669
1650
  // src/session/runner/transcript.ts
1670
1651
  import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1788,90 +1769,73 @@ var augmentPrompt = (text, relPaths, labels) => {
1788
1769
  ${text}` : header;
1789
1770
  };
1790
1771
 
1791
- // src/session/runner/spawn.ts
1792
- import { createInterface as createInterface2 } from "node:readline";
1793
- var spawnClaude = async (config, worker2, text, resuming, spawnImpl, log, envOverlay) => {
1794
- const childEnv = envOverlay ? { ...process.env, ...envOverlay } : process.env;
1795
- const bin = claudeBin();
1796
- const args = buildClaudeArgs(config.agentSessionId, text, resuming);
1797
- log(`[spawn] ${bin} ${args.slice(0, -1).join(" ")} -- "${previewText(text)}"`);
1798
- log(`[spawn] cwd: ${config.worktreePath}`);
1799
- log(`[spawn] runtime env keys: ${maskedEnvKeys(envOverlay)}`);
1800
- let child;
1801
- try {
1802
- child = spawnImpl(bin, args, {
1803
- cwd: config.worktreePath,
1804
- stdio: ["ignore", "pipe", "pipe"],
1805
- env: childEnv
1806
- });
1807
- } catch (err) {
1808
- const message = err instanceof Error ? err.message : String(err);
1809
- log(`[spawn] failed: ${message}`);
1810
- await worker2.emitEvent("turn_error", { message: `spawn failed: ${message}` });
1811
- return null;
1812
- }
1813
- if (!child.stdout) {
1814
- log("[spawn] no stdout from claude \u2014 aborting turn");
1815
- await worker2.emitEvent("turn_error", { message: "no stdout from claude" });
1816
- return null;
1817
- }
1818
- const tail = tailBuffer();
1819
- if (child.stderr) {
1820
- const errLines = createInterface2({ input: child.stderr });
1821
- void (async () => {
1822
- for await (const line of errLines) {
1823
- tail.append(`${line}
1824
- `);
1825
- log(`[claude] ${line}`);
1826
- }
1827
- })();
1828
- }
1829
- return { child, tail };
1772
+ // src/session/runner/query.ts
1773
+ import "@anthropic-ai/claude-agent-sdk";
1774
+ var startQuery = (config, text, resuming, queryFn, abortController, log, envOverlay) => {
1775
+ const env = buildSdkEnv(envOverlay);
1776
+ const exe = claudeExecutable();
1777
+ const options = {
1778
+ cwd: config.worktreePath,
1779
+ permissionMode: "bypassPermissions",
1780
+ includePartialMessages: false,
1781
+ abortController,
1782
+ stderr: (line) => log(`[claude] ${line.trimEnd()}`),
1783
+ ...resuming ? { resume: config.agentSessionId } : { sessionId: config.agentSessionId },
1784
+ ...env ? { env } : {},
1785
+ ...exe ? { pathToClaudeCodeExecutable: exe } : {}
1786
+ };
1787
+ return queryFn({ prompt: text, options });
1830
1788
  };
1831
1789
 
1832
1790
  // src/session/runner/stream.ts
1833
- import { createInterface as createInterface3 } from "node:readline";
1834
- var streamSdkEvents = async (stdout, worker2) => {
1835
- const rl = createInterface3({ input: stdout });
1836
- for await (const line of rl) {
1837
- const trimmed = line.trim();
1838
- if (!trimmed) continue;
1839
- let payload;
1840
- try {
1841
- payload = JSON.parse(trimmed);
1842
- } catch {
1843
- payload = { raw: trimmed };
1844
- }
1845
- await worker2.emitEvent("sdk_event", payload);
1791
+ var streamSdkEvents = async (messages, worker2) => {
1792
+ let result = null;
1793
+ for await (const message of messages) {
1794
+ await worker2.emitEvent("sdk_event", message);
1795
+ if (message.type === "result") result = toTurnResult(message);
1846
1796
  }
1797
+ return result;
1798
+ };
1799
+ var toTurnResult = (message) => {
1800
+ const rec = message;
1801
+ const subtype = typeof rec.subtype === "string" ? rec.subtype : "unknown";
1802
+ const isError = rec.is_error === true || subtype !== "success";
1803
+ const resultText = typeof rec.result === "string" ? rec.result : "";
1804
+ return { subtype, isError, resultText };
1847
1805
  };
1848
1806
 
1849
1807
  // src/session/runner/turn.ts
1850
- var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1851
1808
  var turnTimeoutMs = () => Number(process.env.BATON_TURN_TIMEOUT_MS) || 30 * 6e4;
1852
- var DRAIN_GRACE_MS = 3e3;
1853
- var waitExit = (child) => {
1854
- if (typeof child.exitCode === "number") return Promise.resolve(child.exitCode);
1855
- if (typeof child.signalCode === "string") return Promise.resolve(-1);
1856
- return new Promise((resolve) => child.once("exit", (code) => resolve(code ?? -1)));
1857
- };
1858
- var finalizeTurn = async (exitCode, tail, worker2, log) => {
1859
- log(`[exit] code=${exitCode}`);
1860
- const stderrTail = tail.toString();
1861
- if (exitCode !== 0) {
1809
+ var TIMEOUT = /* @__PURE__ */ Symbol("turn-timeout");
1810
+ var finalizeTurn = async (result, worker2, log) => {
1811
+ if (!result) {
1812
+ log("[exit] stream ended with no result");
1813
+ await worker2.emitEvent("turn_error", { message: "claude produced no result" });
1814
+ await worker2.emitEvent("turn_complete", {});
1815
+ return -1;
1816
+ }
1817
+ log(`[exit] subtype=${result.subtype} isError=${result.isError}`);
1818
+ if (result.isError) {
1862
1819
  await worker2.emitEvent("turn_error", {
1863
- message: `claude exited with code ${exitCode}`,
1864
- exitCode,
1865
- stderrTail
1820
+ message: result.resultText || `claude result: ${result.subtype}`,
1821
+ subtype: result.subtype
1866
1822
  });
1867
1823
  }
1868
- await worker2.emitEvent("turn_complete", {
1869
- exitCode,
1870
- ...stderrTail ? { stderrTail } : {}
1824
+ await worker2.emitEvent("turn_complete", { subtype: result.subtype });
1825
+ return result.isError ? 1 : 0;
1826
+ };
1827
+ var buildPrompt = async (config, rawText, attachments, log, fetchImpl) => {
1828
+ if (attachments.length === 0) return rawText;
1829
+ const relPaths = await materializeAttachments({
1830
+ worktreePath: config.worktreePath,
1831
+ serverBase: config.server,
1832
+ attachments,
1833
+ fetchImpl
1871
1834
  });
1872
- return exitCode;
1835
+ log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
1836
+ return augmentPrompt(rawText, relPaths, labelAttachments(attachments));
1873
1837
  };
1874
- var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => console.log(m), envOverlay, fetchImpl) => {
1838
+ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => console.log(m), envOverlay, fetchImpl) => {
1875
1839
  await worker2.emitEvent("turn_start", { messageId: msg.id });
1876
1840
  const payload = msg.payload;
1877
1841
  const rawText = typeof payload?.text === "string" ? payload.text : "";
@@ -1880,56 +1844,42 @@ var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => con
1880
1844
  await worker2.emitEvent("turn_error", { message: "user_message missing text and attachments" });
1881
1845
  return -1;
1882
1846
  }
1883
- let text = rawText;
1884
- if (attachments.length > 0) {
1885
- try {
1886
- const relPaths = await materializeAttachments({
1887
- worktreePath: config.worktreePath,
1888
- serverBase: config.server,
1889
- attachments,
1890
- fetchImpl
1891
- });
1892
- log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
1893
- text = augmentPrompt(rawText, relPaths, labelAttachments(attachments));
1894
- } catch (err) {
1895
- const message = err instanceof Error ? err.message : String(err);
1896
- await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
1897
- return -1;
1898
- }
1847
+ let text;
1848
+ try {
1849
+ text = await buildPrompt(config, rawText, attachments, log, fetchImpl);
1850
+ } catch (err) {
1851
+ const message = err instanceof Error ? err.message : String(err);
1852
+ await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
1853
+ return -1;
1899
1854
  }
1900
- const result = await spawnClaude(config, worker2, text, resuming, spawnImpl, log, envOverlay);
1901
- if (!result) return -1;
1902
- const { child, tail } = result;
1903
- const stdout = child.stdout;
1904
- const exited = waitExit(child);
1905
- let timedOut = false;
1855
+ const abort = new AbortController();
1906
1856
  const ceiling = turnTimeoutMs();
1907
- const watchdog = setTimeout(() => {
1908
- timedOut = true;
1909
- log(`[watchdog] turn exceeded ${ceiling}ms \u2014 killing claude`);
1910
- child.kill("SIGKILL");
1911
- }, ceiling);
1857
+ let timer;
1858
+ const timeout = new Promise((resolve) => {
1859
+ timer = setTimeout(() => resolve(TIMEOUT), ceiling);
1860
+ });
1912
1861
  try {
1913
- const streaming = streamSdkEvents(stdout, worker2);
1914
- void streaming.catch(() => {
1862
+ const messages = startQuery(config, text, resuming, queryFn, abort, log, envOverlay);
1863
+ const consume = streamSdkEvents(messages, worker2);
1864
+ void consume.catch(() => {
1915
1865
  });
1916
- await Promise.race([streaming, exited.then(() => delay(DRAIN_GRACE_MS))]);
1917
- stdout.destroy();
1918
- const exitCode = await exited;
1919
- if (timedOut) {
1920
- await worker2.emitEvent("turn_error", {
1921
- message: `turn exceeded ${ceiling}ms \u2014 claude killed`,
1922
- stderrTail: tail.toString()
1923
- });
1866
+ const outcome = await Promise.race([consume, timeout]);
1867
+ if (outcome === TIMEOUT) {
1868
+ log(`[watchdog] turn exceeded ${ceiling}ms \u2014 aborting claude`);
1869
+ abort.abort();
1870
+ await worker2.emitEvent("turn_error", { message: `turn exceeded ${ceiling}ms \u2014 claude aborted` });
1871
+ await worker2.emitEvent("turn_complete", {});
1872
+ return -1;
1924
1873
  }
1925
- return await finalizeTurn(exitCode, tail, worker2, log);
1874
+ return await finalizeTurn(outcome, worker2, log);
1926
1875
  } catch (err) {
1927
1876
  const message = err instanceof Error ? err.message : String(err);
1928
1877
  log(`[run] ${message}`);
1929
- await worker2.emitEvent("turn_error", { message, stderrTail: tail.toString() });
1878
+ await worker2.emitEvent("turn_error", { message });
1879
+ await worker2.emitEvent("turn_complete", {});
1930
1880
  return -1;
1931
1881
  } finally {
1932
- clearTimeout(watchdog);
1882
+ clearTimeout(timer);
1933
1883
  }
1934
1884
  };
1935
1885
 
@@ -1964,9 +1914,9 @@ var restoreTty = () => {
1964
1914
  };
1965
1915
  var runDaemon = async (config, deps, signal) => {
1966
1916
  const log = deps.log ?? ((m) => console.log(`[#${config.sessionId} ${config.name}] ${m}`));
1967
- const sp = deps.spawnImpl ?? spawn;
1917
+ const qf = deps.queryFn ?? query2;
1968
1918
  const ESCtor = deps.eventSourceImpl ?? EventSource;
1969
- log(`bin: ${claudeBin()} cwd: ${config.worktreePath}`);
1919
+ log(`bin: ${claudeExecutable() ?? "(sdk bundled)"} cwd: ${config.worktreePath}`);
1970
1920
  log(`runtime env keys: ${maskedEnvKeys(deps.env)}`);
1971
1921
  const state = {
1972
1922
  firstSpawnDone: findTranscriptPath(config.agentSessionId) !== null,
@@ -1989,7 +1939,7 @@ var runDaemon = async (config, deps, signal) => {
1989
1939
  deps.worker,
1990
1940
  msg,
1991
1941
  resuming,
1992
- sp,
1942
+ qf,
1993
1943
  log,
1994
1944
  deps.env,
1995
1945
  deps.fetchImpl
@@ -2345,11 +2295,12 @@ var task = defineCommand({
2345
2295
  import { hostname as osHostname } from "node:os";
2346
2296
 
2347
2297
  // src/worker/daemon.ts
2348
- import { spawn as spawn2 } from "node:child_process";
2298
+ import { spawn } from "node:child_process";
2349
2299
  import { randomUUID } from "node:crypto";
2350
2300
  import { existsSync as existsSync4 } from "node:fs";
2351
2301
  import { dirname as dirname2, join as join5 } from "node:path";
2352
2302
  import { fileURLToPath } from "node:url";
2303
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2353
2304
 
2354
2305
  // src/session/runner/title.ts
2355
2306
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
@@ -2357,7 +2308,7 @@ var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NON
2357
2308
  var sanitizeTitle = (raw) => raw.replace(/[\r\n]+/g, " ").replace(/["'`]/g, "").replace(/^\s*(title|session)\s*[:-]\s*/i, "").replace(/\s+/g, " ").trim().slice(0, 40).trim();
2358
2309
  var TITLE_TIMEOUT_MS = 3e4;
2359
2310
  var generateTitle = async (input) => {
2360
- const { worktreePath, userText, assistantText, spawnImpl } = input;
2311
+ const { worktreePath, userText, assistantText, queryFn } = input;
2361
2312
  if (`${userText}${assistantText}`.trim().length < 4) return null;
2362
2313
  const exchange = [
2363
2314
  `User: ${clip(userText, 800)}`,
@@ -2365,44 +2316,31 @@ var generateTitle = async (input) => {
2365
2316
  ].filter(Boolean).join("\n");
2366
2317
  const prompt = `Reply with ONLY a 2-5 word title (no quotes, no punctuation) summarizing this coding session. If it is too short or generic to title meaningfully, reply with exactly NONE:
2367
2318
  ${exchange}`;
2368
- const out = await new Promise((resolve) => {
2369
- let buf = "";
2370
- let done = false;
2371
- const finish = (s) => {
2372
- if (!done) {
2373
- done = true;
2374
- resolve(s);
2375
- }
2376
- };
2377
- let child;
2378
- try {
2379
- child = spawnImpl(claudeBin(), ["--print", "--dangerously-skip-permissions", prompt], {
2319
+ const exe = claudeExecutable();
2320
+ const abort = new AbortController();
2321
+ const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
2322
+ let out = "";
2323
+ try {
2324
+ const messages = queryFn({
2325
+ prompt,
2326
+ options: {
2380
2327
  cwd: worktreePath,
2381
- stdio: ["ignore", "pipe", "pipe"],
2382
- env: process.env
2383
- });
2384
- } catch {
2385
- return finish("");
2386
- }
2387
- const timer = setTimeout(() => {
2388
- try {
2389
- child.kill();
2390
- } catch {
2328
+ permissionMode: "bypassPermissions",
2329
+ allowedTools: [],
2330
+ includePartialMessages: false,
2331
+ abortController: abort,
2332
+ ...exe ? { pathToClaudeCodeExecutable: exe } : {}
2391
2333
  }
2392
- finish("");
2393
- }, TITLE_TIMEOUT_MS);
2394
- child.stdout?.on("data", (d) => {
2395
- buf += String(d);
2396
- });
2397
- child.on("exit", () => {
2398
- clearTimeout(timer);
2399
- finish(buf);
2400
- });
2401
- child.on("error", () => {
2402
- clearTimeout(timer);
2403
- finish("");
2404
2334
  });
2405
- });
2335
+ for await (const m of messages) {
2336
+ if (m.type === "result" && typeof m.result === "string")
2337
+ out = m.result;
2338
+ }
2339
+ } catch {
2340
+ return null;
2341
+ } finally {
2342
+ clearTimeout(timer);
2343
+ }
2406
2344
  const title = sanitizeTitle(out);
2407
2345
  return !title || isDeclined(title) ? null : title;
2408
2346
  };
@@ -2468,7 +2406,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2468
2406
  };
2469
2407
  const spawnChild = (sessionId, worktreePath) => {
2470
2408
  if (children.has(sessionId)) return;
2471
- const child = spawn2(process.execPath, [binPath(), "session", "run", String(sessionId)], {
2409
+ const child = spawn(process.execPath, [binPath(), "session", "run", String(sessionId)], {
2472
2410
  detached: true,
2473
2411
  stdio: "inherit",
2474
2412
  env: {
@@ -2528,7 +2466,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2528
2466
  worktreePath,
2529
2467
  userText: exchange.userText,
2530
2468
  assistantText: exchange.assistantText,
2531
- spawnImpl: spawn2
2469
+ queryFn: query3
2532
2470
  });
2533
2471
  if (!title) return log(`title #${sessionId}: not enough to title yet, skipping`);
2534
2472
  await client.sessions.setName(sessionId, title, cfg.apiToken);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "baton": "tsx src/index.ts",
14
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --packages=bundle --banner:js=\"#!/usr/bin/env node\" --outfile=dist/baton.mjs && chmod +x dist/baton.mjs",
14
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --packages=bundle --external:@anthropic-ai/claude-agent-sdk --banner:js=\"#!/usr/bin/env node\" --outfile=dist/baton.mjs && chmod +x dist/baton.mjs",
15
15
  "prepublishOnly": "pnpm build",
16
16
  "typecheck": "tsc --noEmit -p tsconfig.json",
17
17
  "test": "tsx --test 'src/**/*.test.ts'"
@@ -24,5 +24,8 @@
24
24
  "eventsource": "^4.1.0",
25
25
  "tsx": "^4.22.4",
26
26
  "typescript": "^6.0.3"
27
+ },
28
+ "dependencies": {
29
+ "@anthropic-ai/claude-agent-sdk": "^0.3.161"
27
30
  }
28
31
  }