@lesscap/baton-cli 0.2.9 → 0.2.11

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 (3) hide show
  1. package/dist/baton.mjs +52 -18
  2. package/package.json +11 -10
  3. package/LICENSE +0 -21
package/dist/baton.mjs CHANGED
@@ -2446,16 +2446,30 @@ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2446
2446
  // src/session/runner/title.ts
2447
2447
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
2448
2448
  var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
2449
- var sanitizeTitle = (raw) => raw.replace(/[\r\n]+/g, " ").replace(/[*_`~#>]/g, "").replace(/["']/g, "").replace(/^\s*(?:title|session|标题)\s*[::-]\s*/i, "").replace(/^\s*(?:[-*•]|\d+[.)、])\s+/, "").replace(/\s+/g, " ").trim().slice(0, 30).replace(/[\s。.,,、;;::!!??.]+$/, "").trim();
2449
+ var sanitizeTitle = (raw) => raw.replace(/[\r\n]+/g, " ").replace(/[*_`~#>]/g, "").replace(/["']/g, "").replace(/^\s*(?:title|session|标题)\s*[::-]\s*/i, "").replace(/^\s*(?:[-*•]|\d+[.)、])\s+/, "").replace(/\s+/g, " ").trim().replace(/(?:[。.!?,、;;]|[.!?;](?=\s|$)).*$/s, "").trim().slice(0, 30).replace(/[\s。.,,、;;::!!??.]+$/, "").trim();
2450
2450
  var TITLE_TIMEOUT_MS = 9e4;
2451
2451
  var buildPrompt2 = (userText, assistantText) => {
2452
2452
  const exchange = [
2453
2453
  `User: ${clip(userText, 800)}`,
2454
2454
  assistantText ? `Assistant: ${clip(assistantText, 800)}` : ""
2455
2455
  ].filter(Boolean).join("\n");
2456
- return `Reply with ONLY a short title for this session: a noun phrase like a filename \u2014 at most 6 words or ~12 Chinese characters. It must NOT be a sentence, no punctuation, no markdown, no quotes, and never an opener like "\u597D\u7684"/"\u6536\u5230"/"I'll". Only if the exchange below has no real content at all (e.g. just a greeting or a test ping), reply with exactly NONE.
2457
-
2458
- ${exchange}`;
2456
+ return [
2457
+ "Name the TOPIC of this chat session \u2014 what it is ABOUT \u2014 as a short title:",
2458
+ "a noun phrase like a filename, at most 6 words or ~12 Chinese characters.",
2459
+ "Name the subject, do NOT restate the conclusion or the answer.",
2460
+ "No sentence, no punctuation at all (including Chinese \uFF0C\u3002\u3001\uFF1B\uFF01\uFF1F), no markdown,",
2461
+ `no quotes, and never an opener like "\u597D\u7684"/"\u6536\u5230"/"I'll".`,
2462
+ "",
2463
+ "Examples (exchange \u2192 title):",
2464
+ "User asks whether the material-ui repo needs updating; assistant says it is",
2465
+ "already up to date \u2192 material-ui \u4ED3\u5E93\u66F4\u65B0\u68C0\u67E5",
2466
+ "User: curl the health endpoint and report status \u2192 \u5065\u5EB7\u68C0\u67E5\u63A5\u53E3\u6392\u67E5",
2467
+ "",
2468
+ "Only if the exchange below has no real content at all (just a greeting or a",
2469
+ "test ping) reply with exactly NONE. Otherwise reply with ONLY the title.",
2470
+ "",
2471
+ exchange
2472
+ ].join("\n");
2459
2473
  };
2460
2474
  var generateTitle = async (input) => {
2461
2475
  const { worktreePath, userText, assistantText, queryFn } = input;
@@ -2575,6 +2589,10 @@ var removeWorktree = (repo, worktreePath) => {
2575
2589
  }
2576
2590
  };
2577
2591
 
2592
+ // src/worker/watchdog.ts
2593
+ var stale = (okAt, now, thresholdMs) => now - okAt > thresholdMs;
2594
+ var streamWedged = (isOpen, okAt, now, thresholdMs) => !isOpen && stale(okAt, now, thresholdMs);
2595
+
2578
2596
  // src/worker/daemon.ts
2579
2597
  var binPath = () => {
2580
2598
  const here = dirname2(fileURLToPath(import.meta.url));
@@ -2588,11 +2606,20 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2588
2606
  const log = (m) => console.log(`[worker #${cfg.workerId} ${cfg.name}] ${m}`);
2589
2607
  const HEARTBEAT_MS = 3e4;
2590
2608
  const HEARTBEAT_TIMEOUT_MS = 15e3;
2591
- const MAX_HEARTBEAT_FAILURES = 5;
2592
- let heartbeatFailures = 0;
2609
+ const HEARTBEAT_DEAD_MS = 15e4;
2610
+ const STREAM_STALE_MS = 9e4;
2611
+ const ES_OPEN = 1;
2612
+ let heartbeatOkAt = Date.now();
2613
+ let streamOkAt = Date.now();
2593
2614
  let fatal = false;
2594
2615
  let stop = () => {
2595
2616
  };
2617
+ const trip = (reason) => {
2618
+ if (fatal) return;
2619
+ log(`${reason} \u2014 exiting so the supervisor restarts a clean worker`);
2620
+ fatal = true;
2621
+ stop();
2622
+ };
2596
2623
  const ping = async () => {
2597
2624
  try {
2598
2625
  await Promise.race([
@@ -2601,20 +2628,11 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2601
2628
  (_, reject) => setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS)
2602
2629
  )
2603
2630
  ]);
2604
- heartbeatFailures = 0;
2631
+ heartbeatOkAt = Date.now();
2605
2632
  } catch (e) {
2606
- heartbeatFailures += 1;
2607
- log(`heartbeat failed (${heartbeatFailures}/${MAX_HEARTBEAT_FAILURES}): ${String(e)}`);
2608
- if (heartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
2609
- const downSec = MAX_HEARTBEAT_FAILURES * HEARTBEAT_MS / 1e3;
2610
- log(`heartbeat down ~${downSec}s \u2014 exiting so the supervisor restarts a clean worker`);
2611
- fatal = true;
2612
- stop();
2613
- }
2633
+ log(`heartbeat failed: ${String(e)}`);
2614
2634
  }
2615
2635
  };
2616
- void ping();
2617
- const hb = setInterval(() => void ping(), HEARTBEAT_MS);
2618
2636
  const killChild = (child) => {
2619
2637
  if (child.pid === void 0) return;
2620
2638
  try {
@@ -2716,8 +2734,12 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2716
2734
  }
2717
2735
  })
2718
2736
  });
2719
- es.onopen = () => void reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
2737
+ es.onopen = () => {
2738
+ streamOkAt = Date.now();
2739
+ void reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
2740
+ };
2720
2741
  es.onmessage = (e) => {
2742
+ streamOkAt = Date.now();
2721
2743
  try {
2722
2744
  const cmd = JSON.parse(e.data);
2723
2745
  if (cmd.cmd === "session.start")
@@ -2733,6 +2755,18 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2733
2755
  };
2734
2756
  es.onerror = () => log("command stream error (eventsource will retry)");
2735
2757
  log(`listening for commands (server ${cfg.server}, repo ${repo})`);
2758
+ const watchdog = () => {
2759
+ const now = Date.now();
2760
+ if (stale(heartbeatOkAt, now, HEARTBEAT_DEAD_MS))
2761
+ trip(`heartbeat down ~${HEARTBEAT_DEAD_MS / 1e3}s`);
2762
+ else if (streamWedged(es.readyState === ES_OPEN, streamOkAt, now, STREAM_STALE_MS))
2763
+ trip(`command stream wedged ~${STREAM_STALE_MS / 1e3}s`);
2764
+ };
2765
+ void ping();
2766
+ const hb = setInterval(() => {
2767
+ void ping();
2768
+ watchdog();
2769
+ }, HEARTBEAT_MS);
2736
2770
  await new Promise((resolve2) => {
2737
2771
  stop = resolve2;
2738
2772
  if (signal.aborted) return resolve2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
@@ -10,22 +10,23 @@
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
+ "scripts": {
14
+ "baton": "tsx src/index.ts",
15
+ "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",
16
+ "prepublishOnly": "pnpm build",
17
+ "typecheck": "tsc --noEmit -p tsconfig.json",
18
+ "test": "tsx --test 'src/**/*.test.ts'"
19
+ },
13
20
  "devDependencies": {
21
+ "@baton/shared": "workspace:*",
14
22
  "@types/node": "^25.9.1",
15
23
  "citty": "^0.2.2",
16
24
  "esbuild": "^0.28.0",
17
25
  "eventsource": "^4.1.0",
18
26
  "tsx": "^4.22.4",
19
- "typescript": "^6.0.3",
20
- "@baton/shared": "0.0.0"
27
+ "typescript": "^6.0.3"
21
28
  },
22
29
  "dependencies": {
23
30
  "@anthropic-ai/claude-agent-sdk": "^0.3.161"
24
- },
25
- "scripts": {
26
- "baton": "tsx src/index.ts",
27
- "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",
28
- "typecheck": "tsc --noEmit -p tsconfig.json",
29
- "test": "tsx --test 'src/**/*.test.ts'"
30
31
  }
31
- }
32
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 bencode
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.