@lesscap/baton-cli 0.2.1 → 0.2.3

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 +77 -34
  2. package/package.json +1 -1
package/dist/baton.mjs CHANGED
@@ -587,9 +587,13 @@ var sessionsClient = (baseUrl) => {
587
587
  const matches = all.filter((s) => s.name === name);
588
588
  return matches.length === 0 ? null : matches[matches.length - 1] ?? null;
589
589
  },
590
- sendMessage: (id, text, attachments) => request(u(`/sessions/${id}/messages`), {
590
+ sendMessage: (id, text, attachments, planMode) => request(u(`/sessions/${id}/messages`), {
591
591
  method: "POST",
592
- body: attachments && attachments.length > 0 ? { text, attachments } : { text }
592
+ body: {
593
+ text,
594
+ ...attachments && attachments.length > 0 ? { attachments } : {},
595
+ ...planMode ? { planMode: true } : {}
596
+ }
593
597
  }),
594
598
  // Raw-body upload: the file streams as the request body (no multipart), so
595
599
  // a Blob backed by a file on disk uploads without buffering. filename rides
@@ -734,6 +738,11 @@ var viewWorker = (cfg) => {
734
738
  apiToken: worker2.apiToken
735
739
  };
736
740
  };
741
+ var worktreeConfig = (cfg) => ({
742
+ server: cfg.server,
743
+ project: cfg.projectId,
744
+ worker: { id: cfg.workerId, name: cfg.name, machineId: cfg.machineId, apiToken: cfg.apiToken }
745
+ });
737
746
 
738
747
  // src/config.ts
739
748
  var resolveBaseUrl = (urlArg, env = process.env) => {
@@ -1655,14 +1664,22 @@ function getBaseURL() {
1655
1664
 
1656
1665
  // src/session/runner.ts
1657
1666
  import { spawnSync } from "node:child_process";
1658
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1667
+ import { query } from "@anthropic-ai/claude-agent-sdk";
1659
1668
 
1660
1669
  // src/session/runner/log.ts
1661
1670
  var maskedEnvKeys = (env) => env ? Object.keys(env).sort().join(", ") : "(none)";
1662
1671
 
1663
1672
  // src/session/runner/sdk-env.ts
1673
+ import { existsSync as existsSync3 } from "node:fs";
1674
+ import { isAbsolute } from "node:path";
1664
1675
  var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
1665
1676
  var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
1677
+ var additionalDirs = () => {
1678
+ const raw = process.env.BATON_ADD_DIRS;
1679
+ if (!raw) return void 0;
1680
+ const dirs = raw.split(":").map((s) => s.trim()).filter((s) => s !== "" && isAbsolute(s) && existsSync3(s));
1681
+ return dirs.length > 0 ? dirs : void 0;
1682
+ };
1666
1683
 
1667
1684
  // src/session/runner/transcript.ts
1668
1685
  import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1787,19 +1804,22 @@ ${text}` : header;
1787
1804
  };
1788
1805
 
1789
1806
  // src/session/runner/query.ts
1790
- import "@anthropic-ai/claude-agent-sdk";
1791
- var startQuery = (config, text, resuming, queryFn, abortController, log, envOverlay) => {
1807
+ var startQuery = (config, text, resuming, queryFn, abortController, log, envOverlay, planMode = false) => {
1792
1808
  const env = buildSdkEnv(envOverlay);
1793
1809
  const exe = claudeExecutable();
1810
+ const addDirs = additionalDirs();
1811
+ if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
1812
+ if (planMode) log("[plan] read-only planning turn");
1794
1813
  const options = {
1795
1814
  cwd: config.worktreePath,
1796
- permissionMode: "bypassPermissions",
1815
+ permissionMode: planMode ? "plan" : "bypassPermissions",
1797
1816
  includePartialMessages: false,
1798
1817
  abortController,
1799
1818
  stderr: (line) => log(`[claude] ${line.trimEnd()}`),
1800
1819
  ...resuming ? { resume: config.agentSessionId } : { sessionId: config.agentSessionId },
1801
1820
  ...env ? { env } : {},
1802
- ...exe ? { pathToClaudeCodeExecutable: exe } : {}
1821
+ ...exe ? { pathToClaudeCodeExecutable: exe } : {},
1822
+ ...addDirs ? { additionalDirectories: addDirs } : {}
1803
1823
  };
1804
1824
  return queryFn({ prompt: text, options });
1805
1825
  };
@@ -1858,6 +1878,7 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
1858
1878
  const payload = msg.payload;
1859
1879
  const rawText = typeof payload?.text === "string" ? payload.text : "";
1860
1880
  const attachments = Array.isArray(payload?.attachments) ? payload.attachments : [];
1881
+ const planMode = payload?.planMode === true;
1861
1882
  if (rawText.length === 0 && attachments.length === 0) {
1862
1883
  await worker2.emitEvent("turn_error", { message: "user_message missing text and attachments" });
1863
1884
  return -1;
@@ -1873,15 +1894,15 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
1873
1894
  const abort = new AbortController();
1874
1895
  const ceiling = turnTimeoutMs();
1875
1896
  let timer;
1876
- const timeout = new Promise((resolve) => {
1877
- timer = setTimeout(() => resolve(TIMEOUT), ceiling);
1897
+ const timeout = new Promise((resolve2) => {
1898
+ timer = setTimeout(() => resolve2(TIMEOUT), ceiling);
1878
1899
  });
1879
- const interrupted = new Promise((resolve) => {
1880
- if (externalSignal?.aborted) resolve(ABORTED);
1881
- else externalSignal?.addEventListener("abort", () => resolve(ABORTED), { once: true });
1900
+ const interrupted = new Promise((resolve2) => {
1901
+ if (externalSignal?.aborted) resolve2(ABORTED);
1902
+ else externalSignal?.addEventListener("abort", () => resolve2(ABORTED), { once: true });
1882
1903
  });
1883
1904
  try {
1884
- const messages = startQuery(config, text, resuming, queryFn, abort, log, envOverlay);
1905
+ const messages = startQuery(config, text, resuming, queryFn, abort, log, envOverlay, planMode);
1885
1906
  const consume = streamSdkEvents(messages, worker2);
1886
1907
  void consume.catch(() => {
1887
1908
  });
@@ -1896,7 +1917,9 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
1896
1917
  if (outcome === TIMEOUT) {
1897
1918
  log(`[watchdog] turn exceeded ${ceiling}ms \u2014 aborting claude`);
1898
1919
  abort.abort();
1899
- await worker2.emitEvent("turn_error", { message: `turn exceeded ${ceiling}ms \u2014 claude aborted` });
1920
+ await worker2.emitEvent("turn_error", {
1921
+ message: `turn exceeded ${ceiling}ms \u2014 claude aborted`
1922
+ });
1900
1923
  await worker2.emitEvent("turn_complete", {});
1901
1924
  return -1;
1902
1925
  }
@@ -1930,9 +1953,9 @@ var subscribeStream = (url, ESCtor, seen, log, onUserMessage, onInterrupt, onRea
1930
1953
  es.onerror = () => log("sse error (eventsource will retry)");
1931
1954
  return es;
1932
1955
  };
1933
- var waitForAbort = (signal) => new Promise((resolve) => {
1934
- if (signal.aborted) return resolve();
1935
- signal.addEventListener("abort", () => resolve(), { once: true });
1956
+ var waitForAbort = (signal) => new Promise((resolve2) => {
1957
+ if (signal.aborted) return resolve2();
1958
+ signal.addEventListener("abort", () => resolve2(), { once: true });
1936
1959
  });
1937
1960
  var shouldReap = (lastActivity, now, busy, queueLen, idleMs) => !busy && queueLen === 0 && now - lastActivity >= idleMs;
1938
1961
  var idleMsFromEnv = () => Number(process.env.BATON_SESSION_IDLE_MS) || 30 * 6e4;
@@ -1945,7 +1968,7 @@ var restoreTty = () => {
1945
1968
  };
1946
1969
  var runDaemon = async (config, deps, signal) => {
1947
1970
  const log = deps.log ?? ((m) => console.log(`[#${config.sessionId} ${config.name}] ${m}`));
1948
- const qf = deps.queryFn ?? query2;
1971
+ const qf = deps.queryFn ?? query;
1949
1972
  const ESCtor = deps.eventSourceImpl ?? EventSource;
1950
1973
  log(`bin: ${claudeExecutable() ?? "(sdk bundled)"} cwd: ${config.worktreePath}`);
1951
1974
  log(`runtime env keys: ${maskedEnvKeys(deps.env)}`);
@@ -2015,12 +2038,12 @@ var runDaemon = async (config, deps, signal) => {
2015
2038
  );
2016
2039
  const idleMs = idleMsFromEnv();
2017
2040
  let idleTimer;
2018
- const reaped = new Promise((resolve) => {
2041
+ const reaped = new Promise((resolve2) => {
2019
2042
  idleTimer = setInterval(
2020
2043
  () => {
2021
2044
  if (shouldReap(lastActivity, Date.now(), busy, pendingQueue.length, idleMs)) {
2022
2045
  log("idle \u2014 shutting down (resumes on next message)");
2023
- resolve();
2046
+ resolve2();
2024
2047
  }
2025
2048
  },
2026
2049
  Math.min(idleMs, 3e4)
@@ -2341,10 +2364,10 @@ import { hostname as osHostname } from "node:os";
2341
2364
  // src/worker/daemon.ts
2342
2365
  import { spawn } from "node:child_process";
2343
2366
  import { randomUUID } from "node:crypto";
2344
- import { existsSync as existsSync4 } from "node:fs";
2367
+ import { existsSync as existsSync5 } from "node:fs";
2345
2368
  import { dirname as dirname2, join as join5 } from "node:path";
2346
2369
  import { fileURLToPath } from "node:url";
2347
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2370
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2348
2371
 
2349
2372
  // src/session/runner/title.ts
2350
2373
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
@@ -2391,10 +2414,10 @@ ${exchange}`;
2391
2414
 
2392
2415
  // src/session/worktree.ts
2393
2416
  import { execFileSync, spawnSync as spawnSync2 } from "node:child_process";
2394
- import { existsSync as existsSync3, mkdirSync } from "node:fs";
2395
- import { dirname } from "node:path";
2417
+ import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3 } from "node:fs";
2418
+ import { dirname, isAbsolute as isAbsolute2, resolve } from "node:path";
2396
2419
  var createWorktree = (input) => {
2397
- if (existsSync3(input.worktreePath))
2420
+ if (existsSync4(input.worktreePath))
2398
2421
  throw new Error(`worktree path already exists: ${input.worktreePath}`);
2399
2422
  mkdirSync(dirname(input.worktreePath), { recursive: true });
2400
2423
  const branch = `baton/${input.sessionCode}`;
@@ -2415,8 +2438,26 @@ var repoHeadBranch = (repo) => {
2415
2438
  const branch = r.status === 0 ? r.stdout.trim() : "";
2416
2439
  return branch || "main";
2417
2440
  };
2441
+ var ensureExcluded = (repo, pattern) => {
2442
+ try {
2443
+ const r = spawnSync2("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
2444
+ stdio: "pipe",
2445
+ encoding: "utf8"
2446
+ });
2447
+ if (r.status !== 0) return;
2448
+ const rel = r.stdout.trim();
2449
+ const path = isAbsolute2(rel) ? rel : resolve(repo, rel);
2450
+ const current = existsSync4(path) ? readFileSync3(path, "utf8") : "";
2451
+ if (current.split("\n").includes(pattern)) return;
2452
+ mkdirSync(dirname(path), { recursive: true });
2453
+ const sep = current === "" || current.endsWith("\n") ? "" : "\n";
2454
+ appendFileSync(path, `${sep}${pattern}
2455
+ `, "utf8");
2456
+ } catch {
2457
+ }
2458
+ };
2418
2459
  var removeWorktree = (repo, worktreePath) => {
2419
- if (!existsSync3(worktreePath)) return;
2460
+ if (!existsSync4(worktreePath)) return;
2420
2461
  try {
2421
2462
  execFileSync("git", ["-C", repo, "worktree", "remove", "--force", worktreePath], {
2422
2463
  stdio: "pipe"
@@ -2429,7 +2470,7 @@ var removeWorktree = (repo, worktreePath) => {
2429
2470
  var binPath = () => {
2430
2471
  const here = dirname2(fileURLToPath(import.meta.url));
2431
2472
  const devShim = join5(here, "..", "..", "bin", "baton.mjs");
2432
- return existsSync4(devShim) ? devShim : fileURLToPath(import.meta.url);
2473
+ return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
2433
2474
  };
2434
2475
  var runWorkerDaemon = async (cfg, client, signal) => {
2435
2476
  const repo = process.cwd();
@@ -2484,6 +2525,8 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2484
2525
  await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
2485
2526
  log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
2486
2527
  }
2528
+ ensureExcluded(repo, PROJECT_CONFIG_NAME);
2529
+ saveProjectConfig(join5(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg));
2487
2530
  spawnChild(sessionId, worktreePath);
2488
2531
  };
2489
2532
  const onStop = (sessionId) => {
@@ -2510,7 +2553,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2510
2553
  worktreePath,
2511
2554
  userText: exchange.userText,
2512
2555
  assistantText: exchange.assistantText,
2513
- queryFn: query3
2556
+ queryFn: query2
2514
2557
  });
2515
2558
  if (!title) return log(`title #${sessionId}: not enough to title yet, skipping`);
2516
2559
  await client.sessions.setName(sessionId, title, cfg.apiToken);
@@ -2555,9 +2598,9 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2555
2598
  };
2556
2599
  es.onerror = () => log("command stream error (eventsource will retry)");
2557
2600
  log(`listening for commands (server ${cfg.server}, repo ${repo})`);
2558
- await new Promise((resolve) => {
2559
- if (signal.aborted) return resolve();
2560
- signal.addEventListener("abort", () => resolve(), { once: true });
2601
+ await new Promise((resolve2) => {
2602
+ if (signal.aborted) return resolve2();
2603
+ signal.addEventListener("abort", () => resolve2(), { once: true });
2561
2604
  });
2562
2605
  es.close();
2563
2606
  clearInterval(hb);
@@ -2566,13 +2609,13 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2566
2609
 
2567
2610
  // src/worker/machine-id.ts
2568
2611
  import { randomUUID as randomUUID2 } from "node:crypto";
2569
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2612
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2570
2613
  import { homedir as homedir3 } from "node:os";
2571
2614
  import { dirname as dirname3, join as join6 } from "node:path";
2572
2615
  var machineIdPath = (env = process.env) => join6(env.XDG_DATA_HOME ?? join6(env.HOME ?? homedir3(), ".local/share"), "baton", "machine-id");
2573
2616
  var readOrCreateMachineId = (path = machineIdPath()) => {
2574
- if (existsSync5(path)) {
2575
- const s = readFileSync3(path, "utf8").trim();
2617
+ if (existsSync6(path)) {
2618
+ const s = readFileSync4(path, "utf8").trim();
2576
2619
  if (s) return s;
2577
2620
  }
2578
2621
  const id = randomUUID2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
6
6
  "bin": {