@lesscap/baton-cli 0.1.2 → 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 +84 -27
  2. package/package.json +11 -10
package/dist/baton.mjs CHANGED
@@ -577,6 +577,7 @@ var sessionsClient = (baseUrl) => {
577
577
  }),
578
578
  resume: (id) => request(u(`/sessions/${id}/resume`), { method: "POST" }),
579
579
  stop: (id) => request(u(`/sessions/${id}/stop`), { method: "POST" }),
580
+ clear: (id) => request(u(`/sessions/${id}/clear`), { method: "POST" }),
580
581
  rename: (id, name) => request(u(`/sessions/${id}/rename`), { method: "POST", body: { name } }),
581
582
  listByProject: (projectId) => request(u(`/projects/${projectId}/sessions`), { method: "GET" }),
582
583
  get: (id) => request(u(`/sessions/${id}`), { method: "GET" }),
@@ -1018,6 +1019,38 @@ var requirement = defineCommand({
1018
1019
  }
1019
1020
  });
1020
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
+
1021
1054
  // src/commands/session/create.ts
1022
1055
  var sessionCreateCommand = defineCommand({
1023
1056
  meta: { name: "create", description: "create + start a session on a worker" },
@@ -1038,22 +1071,6 @@ var sessionCreateCommand = defineCommand({
1038
1071
  }
1039
1072
  });
1040
1073
 
1041
- // src/commands/session/shared.ts
1042
- import { homedir } from "node:os";
1043
- import { join as join2 } from "node:path";
1044
- var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join2(env.XDG_DATA_HOME ?? join2(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
1045
- var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1046
- var resolveSession = async (client, projectId, handle) => {
1047
- const asInt = Number(handle);
1048
- if (Number.isInteger(asInt) && asInt > 0) {
1049
- const byId = await client.sessions.get(asInt).catch(() => null);
1050
- if (byId) return { id: byId.id, name: byId.name };
1051
- }
1052
- const byName = await client.sessions.findByName(projectId, handle);
1053
- if (byName) return { id: byName.id, name: byName.name };
1054
- throw new Error(`session "${handle}" not found in project ${projectId}`);
1055
- };
1056
-
1057
1074
  // src/commands/session/get.ts
1058
1075
  var sessionGetCommand = defineCommand({
1059
1076
  meta: { name: "get", description: "get a session by int id or name" },
@@ -1709,6 +1726,17 @@ var readFirstExchange = (agentSessionId) => {
1709
1726
  }
1710
1727
  };
1711
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
+
1712
1740
  // src/session/runner/attachments.ts
1713
1741
  import { createWriteStream } from "node:fs";
1714
1742
  import { mkdir, writeFile } from "node:fs/promises";
@@ -1749,11 +1777,11 @@ var materializeAttachments = async (opts) => {
1749
1777
  }
1750
1778
  return relPaths;
1751
1779
  };
1752
- var augmentPrompt = (text, relPaths) => {
1780
+ var augmentPrompt = (text, relPaths, labels) => {
1753
1781
  if (relPaths.length === 0) return text;
1754
1782
  const header = [
1755
- "[Attached files \u2014 already saved in the working directory:]",
1756
- ...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}`)
1757
1785
  ].join("\n");
1758
1786
  return text.length > 0 ? `${header}
1759
1787
 
@@ -1819,10 +1847,15 @@ var streamSdkEvents = async (stdout, worker2) => {
1819
1847
  };
1820
1848
 
1821
1849
  // src/session/runner/turn.ts
1822
- var finalizeTurn = async (child, tail, worker2, log) => {
1823
- const exitCode = await new Promise(
1824
- (resolve) => child.once("exit", (code) => resolve(code ?? -1))
1825
- );
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) => {
1826
1859
  log(`[exit] code=${exitCode}`);
1827
1860
  const stderrTail = tail.toString();
1828
1861
  if (exitCode !== 0) {
@@ -1857,7 +1890,7 @@ var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => con
1857
1890
  fetchImpl
1858
1891
  });
1859
1892
  log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
1860
- text = augmentPrompt(rawText, relPaths);
1893
+ text = augmentPrompt(rawText, relPaths, labelAttachments(attachments));
1861
1894
  } catch (err) {
1862
1895
  const message = err instanceof Error ? err.message : String(err);
1863
1896
  await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
@@ -1866,14 +1899,37 @@ var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => con
1866
1899
  }
1867
1900
  const result = await spawnClaude(config, worker2, text, resuming, spawnImpl, log, envOverlay);
1868
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);
1869
1912
  try {
1870
- await streamSdkEvents(result.child.stdout, worker2);
1871
- 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);
1872
1926
  } catch (err) {
1873
1927
  const message = err instanceof Error ? err.message : String(err);
1874
1928
  log(`[run] ${message}`);
1875
- await worker2.emitEvent("turn_error", { message, stderrTail: result.tail.toString() });
1929
+ await worker2.emitEvent("turn_error", { message, stderrTail: tail.toString() });
1876
1930
  return -1;
1931
+ } finally {
1932
+ clearTimeout(watchdog);
1877
1933
  }
1878
1934
  };
1879
1935
 
@@ -2137,6 +2193,7 @@ var session = defineCommand({
2137
2193
  create: sessionCreateCommand,
2138
2194
  resume: sessionResumeCommand,
2139
2195
  stop: sessionStopCommand,
2196
+ clear: sessionClearCommand,
2140
2197
  rename: sessionRenameCommand,
2141
2198
  rm: sessionRmCommand,
2142
2199
  send: sessionSendCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.1.2",
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
+ }