@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.
- package/dist/baton.mjs +80 -22
- 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
|
|
773
|
-
const
|
|
774
|
-
|
|
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 =
|
|
2518
|
-
var
|
|
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
|
-
|
|
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
|
|
2549
|
-
|
|
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
|
-
|
|
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) ?
|
|
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
|
|
2626
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|