@lesscap/baton-cli 0.1.1 → 0.1.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 +103 -30
  2. package/package.json +11 -10
package/dist/baton.mjs CHANGED
@@ -498,6 +498,11 @@ var primed = false;
498
498
  var primeLogin = (baseUrl) => {
499
499
  if (primed) return;
500
500
  primed = true;
501
+ const token = process.env.BATON_TOKEN;
502
+ if (token) {
503
+ setAuthHeaders({ authorization: `Bearer ${token}` });
504
+ return;
505
+ }
501
506
  const username = process.env.BATON_USER;
502
507
  const password = process.env.BATON_PASS;
503
508
  if (!username || !password) return;
@@ -508,7 +513,7 @@ var primeLogin = (baseUrl) => {
508
513
  headers: { "content-type": "application/json" },
509
514
  body: JSON.stringify({ username, password })
510
515
  });
511
- const cookie = (res.headers.get("set-cookie") ?? "").split(";")[0];
516
+ const cookie = (res.headers.getSetCookie()[0] ?? "").split(";")[0];
512
517
  if (res.ok && cookie) setAuthHeaders({ cookie });
513
518
  })().catch(() => {
514
519
  })
@@ -572,6 +577,7 @@ var sessionsClient = (baseUrl) => {
572
577
  }),
573
578
  resume: (id) => request(u(`/sessions/${id}/resume`), { method: "POST" }),
574
579
  stop: (id) => request(u(`/sessions/${id}/stop`), { method: "POST" }),
580
+ clear: (id) => request(u(`/sessions/${id}/clear`), { method: "POST" }),
575
581
  rename: (id, name) => request(u(`/sessions/${id}/rename`), { method: "POST", body: { name } }),
576
582
  listByProject: (projectId) => request(u(`/projects/${projectId}/sessions`), { method: "GET" }),
577
583
  get: (id) => request(u(`/sessions/${id}`), { method: "GET" }),
@@ -735,6 +741,12 @@ var resolveBaseUrl = (urlArg, env = process.env) => {
735
741
  if (cfg?.server) return cfg.server;
736
742
  return env.BATON_URL ?? "http://localhost:3280";
737
743
  };
744
+ var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
745
+ if (urlArg) return urlArg;
746
+ const cfg = loadProjectConfigOrNull(projectConfigPath());
747
+ if (cfg?.server) return cfg.server;
748
+ return env.BATON_URL ?? null;
749
+ };
738
750
 
739
751
  // src/util.ts
740
752
  var common = {
@@ -1007,6 +1019,38 @@ var requirement = defineCommand({
1007
1019
  }
1008
1020
  });
1009
1021
 
1022
+ // src/commands/session/shared.ts
1023
+ import { homedir } from "node:os";
1024
+ import { join as join2 } from "node:path";
1025
+ var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join2(env.XDG_DATA_HOME ?? join2(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
1026
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1027
+ var resolveSession = async (client, projectId, handle) => {
1028
+ const asInt = Number(handle);
1029
+ if (Number.isInteger(asInt) && asInt > 0) {
1030
+ const byId = await client.sessions.get(asInt).catch(() => null);
1031
+ if (byId) return { id: byId.id, name: byId.name };
1032
+ }
1033
+ const byName = await client.sessions.findByName(projectId, handle);
1034
+ if (byName) return { id: byName.id, name: byName.name };
1035
+ throw new Error(`session "${handle}" not found in project ${projectId}`);
1036
+ };
1037
+
1038
+ // src/commands/session/clear.ts
1039
+ var sessionClearCommand = defineCommand({
1040
+ meta: { name: "clear", description: "reset the conversation context (keeps the session + worktree)" },
1041
+ args: {
1042
+ session: { type: "positional", required: true, description: "session int id or name" },
1043
+ project: { type: "string", description: "project id (overrides .baton.json)" },
1044
+ ...common
1045
+ },
1046
+ run: async ({ args }) => {
1047
+ const c = clientFor(args);
1048
+ const handle = await resolveSession(c, resolveProjectId(args), args.session);
1049
+ const s = await c.sessions.clear(handle.id);
1050
+ console.log(renderOne(s, fmtSession, Boolean(args.json)));
1051
+ }
1052
+ });
1053
+
1010
1054
  // src/commands/session/create.ts
1011
1055
  var sessionCreateCommand = defineCommand({
1012
1056
  meta: { name: "create", description: "create + start a session on a worker" },
@@ -1027,22 +1071,6 @@ var sessionCreateCommand = defineCommand({
1027
1071
  }
1028
1072
  });
1029
1073
 
1030
- // src/commands/session/shared.ts
1031
- import { homedir } from "node:os";
1032
- import { join as join2 } from "node:path";
1033
- var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join2(env.XDG_DATA_HOME ?? join2(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
1034
- var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1035
- var resolveSession = async (client, projectId, handle) => {
1036
- const asInt = Number(handle);
1037
- if (Number.isInteger(asInt) && asInt > 0) {
1038
- const byId = await client.sessions.get(asInt).catch(() => null);
1039
- if (byId) return { id: byId.id, name: byId.name };
1040
- }
1041
- const byName = await client.sessions.findByName(projectId, handle);
1042
- if (byName) return { id: byName.id, name: byName.name };
1043
- throw new Error(`session "${handle}" not found in project ${projectId}`);
1044
- };
1045
-
1046
1074
  // src/commands/session/get.ts
1047
1075
  var sessionGetCommand = defineCommand({
1048
1076
  meta: { name: "get", description: "get a session by int id or name" },
@@ -1698,6 +1726,17 @@ var readFirstExchange = (agentSessionId) => {
1698
1726
  }
1699
1727
  };
1700
1728
 
1729
+ // ../shared/src/domain/attachment.ts
1730
+ var isImageAttachment = (att) => att.contentType.startsWith("image/");
1731
+ var labelAttachments = (attachments) => {
1732
+ const counters = { image: 0, file: 0 };
1733
+ return attachments.map((att) => {
1734
+ const kind = isImageAttachment(att) ? "image" : "file";
1735
+ counters[kind] += 1;
1736
+ return `${kind}-${counters[kind]}`;
1737
+ });
1738
+ };
1739
+
1701
1740
  // src/session/runner/attachments.ts
1702
1741
  import { createWriteStream } from "node:fs";
1703
1742
  import { mkdir, writeFile } from "node:fs/promises";
@@ -1738,11 +1777,11 @@ var materializeAttachments = async (opts) => {
1738
1777
  }
1739
1778
  return relPaths;
1740
1779
  };
1741
- var augmentPrompt = (text, relPaths) => {
1780
+ var augmentPrompt = (text, relPaths, labels) => {
1742
1781
  if (relPaths.length === 0) return text;
1743
1782
  const header = [
1744
- "[Attached files \u2014 already saved in the working directory:]",
1745
- ...relPaths.map((p) => `- ${p}`)
1783
+ "[Attached files \u2014 already saved in the working directory. Reference them by label:]",
1784
+ ...relPaths.map((p, i) => `- {${labels[i]}} ${p}`)
1746
1785
  ].join("\n");
1747
1786
  return text.length > 0 ? `${header}
1748
1787
 
@@ -1808,10 +1847,15 @@ var streamSdkEvents = async (stdout, worker2) => {
1808
1847
  };
1809
1848
 
1810
1849
  // src/session/runner/turn.ts
1811
- var finalizeTurn = async (child, tail, worker2, log) => {
1812
- const exitCode = await new Promise(
1813
- (resolve) => child.once("exit", (code) => resolve(code ?? -1))
1814
- );
1850
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1851
+ 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) => {
1815
1859
  log(`[exit] code=${exitCode}`);
1816
1860
  const stderrTail = tail.toString();
1817
1861
  if (exitCode !== 0) {
@@ -1846,7 +1890,7 @@ var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => con
1846
1890
  fetchImpl
1847
1891
  });
1848
1892
  log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
1849
- text = augmentPrompt(rawText, relPaths);
1893
+ text = augmentPrompt(rawText, relPaths, labelAttachments(attachments));
1850
1894
  } catch (err) {
1851
1895
  const message = err instanceof Error ? err.message : String(err);
1852
1896
  await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
@@ -1855,14 +1899,37 @@ var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => con
1855
1899
  }
1856
1900
  const result = await spawnClaude(config, worker2, text, resuming, spawnImpl, log, envOverlay);
1857
1901
  if (!result) return -1;
1902
+ const { child, tail } = result;
1903
+ const stdout = child.stdout;
1904
+ const exited = waitExit(child);
1905
+ let timedOut = false;
1906
+ 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);
1858
1912
  try {
1859
- await streamSdkEvents(result.child.stdout, worker2);
1860
- return await finalizeTurn(result.child, result.tail, worker2, log);
1913
+ const streaming = streamSdkEvents(stdout, worker2);
1914
+ void streaming.catch(() => {
1915
+ });
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
+ });
1924
+ }
1925
+ return await finalizeTurn(exitCode, tail, worker2, log);
1861
1926
  } catch (err) {
1862
1927
  const message = err instanceof Error ? err.message : String(err);
1863
1928
  log(`[run] ${message}`);
1864
- await worker2.emitEvent("turn_error", { message, stderrTail: result.tail.toString() });
1929
+ await worker2.emitEvent("turn_error", { message, stderrTail: tail.toString() });
1865
1930
  return -1;
1931
+ } finally {
1932
+ clearTimeout(watchdog);
1866
1933
  }
1867
1934
  };
1868
1935
 
@@ -2126,6 +2193,7 @@ var session = defineCommand({
2126
2193
  create: sessionCreateCommand,
2127
2194
  resume: sessionResumeCommand,
2128
2195
  stop: sessionStopCommand,
2196
+ clear: sessionClearCommand,
2129
2197
  rename: sessionRenameCommand,
2130
2198
  rm: sessionRmCommand,
2131
2199
  send: sessionSendCommand,
@@ -2577,8 +2645,13 @@ var worker = defineCommand({
2577
2645
  ...common
2578
2646
  },
2579
2647
  run: async ({ args }) => {
2580
- const server = resolveBaseUrl(args.url);
2581
- const c = clientFor(args);
2648
+ const server = resolveBaseUrlOrNull(args.url);
2649
+ if (!server)
2650
+ throw new Error(
2651
+ "no baton server \u2014 pass --url <url> (e.g. https://baton.fmap.dev/api), set BATON_URL, or run `baton init`"
2652
+ );
2653
+ if (!args.json) console.log(`registering against ${server}`);
2654
+ const c = createClient(server);
2582
2655
  const hostname = osHostname();
2583
2656
  const machineId = readOrCreateMachineId();
2584
2657
  const name = args.name ?? hostname;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
6
6
  "bin": {
@@ -9,19 +9,20 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
+ "scripts": {
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",
15
+ "prepublishOnly": "pnpm build",
16
+ "typecheck": "tsc --noEmit -p tsconfig.json",
17
+ "test": "tsx --test 'src/**/*.test.ts'"
18
+ },
12
19
  "devDependencies": {
20
+ "@baton/shared": "workspace:*",
13
21
  "@types/node": "^25.9.1",
14
22
  "citty": "^0.2.2",
15
23
  "esbuild": "^0.28.0",
16
24
  "eventsource": "^4.1.0",
17
25
  "tsx": "^4.22.4",
18
- "typescript": "^6.0.3",
19
- "@baton/shared": "0.0.0"
20
- },
21
- "scripts": {
22
- "baton": "tsx src/index.ts",
23
- "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",
24
- "typecheck": "tsc --noEmit -p tsconfig.json",
25
- "test": "tsx --test 'src/**/*.test.ts'"
26
+ "typescript": "^6.0.3"
26
27
  }
27
- }
28
+ }