@lesscap/baton-cli 0.2.5 → 0.2.7

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 +80 -22
  2. package/package.json +2 -1
package/dist/baton.mjs CHANGED
@@ -768,10 +768,17 @@ var common = {
768
768
  },
769
769
  json: { type: "boolean", description: "output JSON" }
770
770
  };
771
+ var resolveAuth = (env, fileToken) => {
772
+ const envBearer = env.BATON_TOKEN ?? env.BATON_WORKER_TOKEN;
773
+ if (envBearer) return { bearer: envBearer };
774
+ if (env.BATON_USER && env.BATON_PASS) return "cookie";
775
+ return fileToken ? { bearer: fileToken } : void 0;
776
+ };
771
777
  var clientFor = (args) => {
772
- const envAuth = process.env.BATON_TOKEN ?? (process.env.BATON_USER && process.env.BATON_PASS);
773
- const cfgToken = envAuth ? void 0 : loadProjectConfigOrNull(projectConfigPath())?.worker?.apiToken;
774
- return createClient(resolveBaseUrl(args.url), cfgToken ? { bearer: cfgToken } : void 0);
778
+ const baseUrl = resolveBaseUrl(args.url);
779
+ const fileToken = loadProjectConfigOrNull(projectConfigPath())?.worker?.apiToken;
780
+ const auth = resolveAuth(process.env, fileToken);
781
+ return createClient(baseUrl, auth && auth !== "cookie" ? auth : void 0);
775
782
  };
776
783
  var resolveProjectId = (args) => {
777
784
  if (args.project !== void 0 && args.project !== "") return Number(args.project);
@@ -2514,26 +2521,36 @@ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2514
2521
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
2515
2522
  var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
2516
2523
  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();
2517
- var TITLE_TIMEOUT_MS = 3e4;
2518
- var generateTitle = async (input) => {
2519
- const { worktreePath, userText, assistantText, queryFn } = input;
2520
- if (`${userText}${assistantText}`.trim().length < 4) return null;
2524
+ var TITLE_TIMEOUT_MS = 9e4;
2525
+ var buildPrompt2 = (userText, assistantText) => {
2521
2526
  const exchange = [
2522
2527
  `User: ${clip(userText, 800)}`,
2523
2528
  assistantText ? `Assistant: ${clip(assistantText, 800)}` : ""
2524
2529
  ].filter(Boolean).join("\n");
2525
- const prompt = `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". If it is too thin or generic to title meaningfully, reply with exactly NONE.
2530
+ 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.
2526
2531
 
2527
2532
  ${exchange}`;
2533
+ };
2534
+ var generateTitle = async (input) => {
2535
+ const { worktreePath, userText, assistantText, queryFn } = input;
2536
+ if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
2528
2537
  const exe = claudeExecutable();
2529
2538
  const abort = new AbortController();
2530
2539
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
2540
+ const stderrTail = [];
2531
2541
  let out = "";
2542
+ let resultSeen = false;
2543
+ let failure = "";
2532
2544
  try {
2533
2545
  const messages = queryFn({
2534
- prompt,
2546
+ prompt: buildPrompt2(userText, assistantText),
2535
2547
  options: {
2536
2548
  cwd: worktreePath,
2549
+ // A one-shot summarizer, not an agent: replace the ~18k-token Claude
2550
+ // Code preset system prompt. Inside the agent harness the model
2551
+ // routinely answers NONE to this ask (verified against reclaude);
2552
+ // a plain summarizer persona titles reliably — and ~100× cheaper.
2553
+ systemPrompt: "You title chat sessions.",
2537
2554
  // Don't load the worktree's CLAUDE.md / project settings — they steer the
2538
2555
  // model into verbose summaries instead of a terse title.
2539
2556
  settingSources: [],
@@ -2541,20 +2558,33 @@ ${exchange}`;
2541
2558
  allowedTools: [],
2542
2559
  includePartialMessages: false,
2543
2560
  abortController: abort,
2561
+ stderr: (line) => {
2562
+ if (line.trim()) stderrTail.push(line.trim().slice(0, 200));
2563
+ if (stderrTail.length > 3) stderrTail.shift();
2564
+ },
2544
2565
  ...exe ? { pathToClaudeCodeExecutable: exe } : {}
2545
2566
  }
2546
2567
  });
2547
2568
  for await (const m of messages) {
2548
- if (m.type === "result" && typeof m.result === "string")
2549
- out = m.result;
2569
+ if (m.type !== "result") continue;
2570
+ resultSeen = true;
2571
+ const r = m;
2572
+ if (r.subtype === "success" && typeof r.result === "string") out = r.result;
2573
+ else
2574
+ failure = `result ${r.subtype ?? "unknown"}${typeof r.result === "string" ? `: ${r.result.slice(0, 200)}` : ""}`;
2550
2575
  }
2551
- } catch {
2552
- return null;
2576
+ } catch (e) {
2577
+ failure = abort.signal.aborted ? `timeout after ${TITLE_TIMEOUT_MS / 1e3}s` : String(e).slice(0, 300);
2553
2578
  } finally {
2554
2579
  clearTimeout(timer);
2555
2580
  }
2581
+ if (!failure && !resultSeen) failure = "stream ended without a result message";
2582
+ if (failure) {
2583
+ const tail = stderrTail.length > 0 ? ` | stderr: ${stderrTail.join(" / ")}` : "";
2584
+ return { kind: "error", reason: `${failure}${tail}` };
2585
+ }
2556
2586
  const title = sanitizeTitle(out);
2557
- return !title || isDeclined(title) ? null : title;
2587
+ return !title || isDeclined(title) ? { kind: "declined" } : { kind: "titled", title };
2558
2588
  };
2559
2589
 
2560
2590
  // src/session/worktree.ts
@@ -2622,11 +2652,35 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2622
2652
  const worktreeDir = defaultWorktreeDir();
2623
2653
  const children = /* @__PURE__ */ new Map();
2624
2654
  const log = (m) => console.log(`[worker #${cfg.workerId} ${cfg.name}] ${m}`);
2625
- const ping = () => {
2626
- void client.workers.heartbeat(cfg.machineId).catch((e) => log(`heartbeat failed: ${String(e)}`));
2655
+ const HEARTBEAT_MS = 3e4;
2656
+ const HEARTBEAT_TIMEOUT_MS = 15e3;
2657
+ const MAX_HEARTBEAT_FAILURES = 5;
2658
+ let heartbeatFailures = 0;
2659
+ let fatal = false;
2660
+ let stop = () => {
2661
+ };
2662
+ const ping = async () => {
2663
+ try {
2664
+ await Promise.race([
2665
+ client.workers.heartbeat(cfg.machineId),
2666
+ new Promise(
2667
+ (_, reject) => setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS)
2668
+ )
2669
+ ]);
2670
+ heartbeatFailures = 0;
2671
+ } catch (e) {
2672
+ heartbeatFailures += 1;
2673
+ log(`heartbeat failed (${heartbeatFailures}/${MAX_HEARTBEAT_FAILURES}): ${String(e)}`);
2674
+ if (heartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
2675
+ const downSec = MAX_HEARTBEAT_FAILURES * HEARTBEAT_MS / 1e3;
2676
+ log(`heartbeat down ~${downSec}s \u2014 exiting so the supervisor restarts a clean worker`);
2677
+ fatal = true;
2678
+ stop();
2679
+ }
2680
+ }
2627
2681
  };
2628
- ping();
2629
- const hb = setInterval(ping, 3e4);
2682
+ void ping();
2683
+ const hb = setInterval(() => void ping(), HEARTBEAT_MS);
2630
2684
  const killChild = (child) => {
2631
2685
  if (child.pid === void 0) return;
2632
2686
  try {
@@ -2694,15 +2748,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2694
2748
  const onTitle = async (sessionId, agentSessionId, worktreePath) => {
2695
2749
  const exchange = readFirstExchange(agentSessionId);
2696
2750
  if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
2697
- const title = await generateTitle({
2751
+ const outcome = await generateTitle({
2698
2752
  worktreePath,
2699
2753
  userText: exchange.userText,
2700
2754
  assistantText: exchange.assistantText,
2701
2755
  queryFn: query2
2702
2756
  });
2703
- if (!title) return log(`title #${sessionId}: not enough to title yet, skipping`);
2704
- await client.sessions.setName(sessionId, title, cfg.apiToken);
2705
- log(`\u270E titled session #${sessionId} \u2192 ${title}`);
2757
+ if (outcome.kind === "error") return log(`title #${sessionId} failed: ${outcome.reason}`);
2758
+ if (outcome.kind === "declined")
2759
+ return log(`title #${sessionId}: not enough to title yet, skipping`);
2760
+ await client.sessions.setName(sessionId, outcome.title, cfg.apiToken);
2761
+ log(`\u270E titled session #${sessionId} \u2192 ${outcome.title}`);
2706
2762
  };
2707
2763
  const reconcile = async () => {
2708
2764
  const owned = (await client.sessions.listByProject(cfg.projectId)).filter(
@@ -2744,12 +2800,14 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2744
2800
  es.onerror = () => log("command stream error (eventsource will retry)");
2745
2801
  log(`listening for commands (server ${cfg.server}, repo ${repo})`);
2746
2802
  await new Promise((resolve2) => {
2803
+ stop = resolve2;
2747
2804
  if (signal.aborted) return resolve2();
2748
2805
  signal.addEventListener("abort", () => resolve2(), { once: true });
2749
2806
  });
2750
2807
  es.close();
2751
2808
  clearInterval(hb);
2752
2809
  for (const { child } of children.values()) killChild(child);
2810
+ if (fatal) process.exit(1);
2753
2811
  };
2754
2812
 
2755
2813
  // src/worker/machine-id.ts
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
6
7
  "bin": {