@lesscap/baton-cli 0.2.5 → 0.2.6

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 +40 -15
  2. package/package.json +2 -1
package/dist/baton.mjs CHANGED
@@ -2514,26 +2514,36 @@ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2514
2514
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
2515
2515
  var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
2516
2516
  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;
2517
+ var TITLE_TIMEOUT_MS = 9e4;
2518
+ var buildPrompt2 = (userText, assistantText) => {
2521
2519
  const exchange = [
2522
2520
  `User: ${clip(userText, 800)}`,
2523
2521
  assistantText ? `Assistant: ${clip(assistantText, 800)}` : ""
2524
2522
  ].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.
2523
+ 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
2524
 
2527
2525
  ${exchange}`;
2526
+ };
2527
+ var generateTitle = async (input) => {
2528
+ const { worktreePath, userText, assistantText, queryFn } = input;
2529
+ if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
2528
2530
  const exe = claudeExecutable();
2529
2531
  const abort = new AbortController();
2530
2532
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
2533
+ const stderrTail = [];
2531
2534
  let out = "";
2535
+ let resultSeen = false;
2536
+ let failure = "";
2532
2537
  try {
2533
2538
  const messages = queryFn({
2534
- prompt,
2539
+ prompt: buildPrompt2(userText, assistantText),
2535
2540
  options: {
2536
2541
  cwd: worktreePath,
2542
+ // A one-shot summarizer, not an agent: replace the ~18k-token Claude
2543
+ // Code preset system prompt. Inside the agent harness the model
2544
+ // routinely answers NONE to this ask (verified against reclaude);
2545
+ // a plain summarizer persona titles reliably — and ~100× cheaper.
2546
+ systemPrompt: "You title chat sessions.",
2537
2547
  // Don't load the worktree's CLAUDE.md / project settings — they steer the
2538
2548
  // model into verbose summaries instead of a terse title.
2539
2549
  settingSources: [],
@@ -2541,20 +2551,33 @@ ${exchange}`;
2541
2551
  allowedTools: [],
2542
2552
  includePartialMessages: false,
2543
2553
  abortController: abort,
2554
+ stderr: (line) => {
2555
+ if (line.trim()) stderrTail.push(line.trim().slice(0, 200));
2556
+ if (stderrTail.length > 3) stderrTail.shift();
2557
+ },
2544
2558
  ...exe ? { pathToClaudeCodeExecutable: exe } : {}
2545
2559
  }
2546
2560
  });
2547
2561
  for await (const m of messages) {
2548
- if (m.type === "result" && typeof m.result === "string")
2549
- out = m.result;
2562
+ if (m.type !== "result") continue;
2563
+ resultSeen = true;
2564
+ const r = m;
2565
+ if (r.subtype === "success" && typeof r.result === "string") out = r.result;
2566
+ else
2567
+ failure = `result ${r.subtype ?? "unknown"}${typeof r.result === "string" ? `: ${r.result.slice(0, 200)}` : ""}`;
2550
2568
  }
2551
- } catch {
2552
- return null;
2569
+ } catch (e) {
2570
+ failure = abort.signal.aborted ? `timeout after ${TITLE_TIMEOUT_MS / 1e3}s` : String(e).slice(0, 300);
2553
2571
  } finally {
2554
2572
  clearTimeout(timer);
2555
2573
  }
2574
+ if (!failure && !resultSeen) failure = "stream ended without a result message";
2575
+ if (failure) {
2576
+ const tail = stderrTail.length > 0 ? ` | stderr: ${stderrTail.join(" / ")}` : "";
2577
+ return { kind: "error", reason: `${failure}${tail}` };
2578
+ }
2556
2579
  const title = sanitizeTitle(out);
2557
- return !title || isDeclined(title) ? null : title;
2580
+ return !title || isDeclined(title) ? { kind: "declined" } : { kind: "titled", title };
2558
2581
  };
2559
2582
 
2560
2583
  // src/session/worktree.ts
@@ -2694,15 +2717,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2694
2717
  const onTitle = async (sessionId, agentSessionId, worktreePath) => {
2695
2718
  const exchange = readFirstExchange(agentSessionId);
2696
2719
  if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
2697
- const title = await generateTitle({
2720
+ const outcome = await generateTitle({
2698
2721
  worktreePath,
2699
2722
  userText: exchange.userText,
2700
2723
  assistantText: exchange.assistantText,
2701
2724
  queryFn: query2
2702
2725
  });
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}`);
2726
+ if (outcome.kind === "error") return log(`title #${sessionId} failed: ${outcome.reason}`);
2727
+ if (outcome.kind === "declined")
2728
+ return log(`title #${sessionId}: not enough to title yet, skipping`);
2729
+ await client.sessions.setName(sessionId, outcome.title, cfg.apiToken);
2730
+ log(`\u270E titled session #${sessionId} \u2192 ${outcome.title}`);
2706
2731
  };
2707
2732
  const reconcile = async () => {
2708
2733
  const owned = (await client.sessions.listByProject(cfg.projectId)).filter(
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.6",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
6
7
  "bin": {