@lesscap/baton-cli 0.2.4 → 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 +75 -28
  2. package/package.json +2 -1
package/dist/baton.mjs CHANGED
@@ -768,7 +768,11 @@ var common = {
768
768
  },
769
769
  json: { type: "boolean", description: "output JSON" }
770
770
  };
771
- var clientFor = (args) => createClient(resolveBaseUrl(args.url));
771
+ 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);
775
+ };
772
776
  var resolveProjectId = (args) => {
773
777
  if (args.project !== void 0 && args.project !== "") return Number(args.project);
774
778
  const cfg = loadProjectConfigOrNull(projectConfigPath());
@@ -783,6 +787,10 @@ var resolveWorkspaceId = (args) => {
783
787
  "no workspace in scope. pass --workspace <id> or run `baton init` in this directory."
784
788
  );
785
789
  };
790
+ var parseWorkerHandle = (handle) => {
791
+ const m = handle.trim().match(/^(?:[wW]-)?(\d+)$/);
792
+ return m ? Number(m[1]) : null;
793
+ };
786
794
  var parseIssueUrl = (url) => {
787
795
  const m = url.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
788
796
  if (!m)
@@ -863,7 +871,7 @@ var fmtComment = (c) => `${c.workerId !== void 0 ? `worker#${c.workerId}` : "you
863
871
  var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.worktreePath ?? "(pending)"}`;
864
872
  var fmtWorker = (w) => {
865
873
  const offline = w.alive === false ? " [offline]" : "";
866
- return `${w.id} ${w.name} ${w.hostname}${offline}`;
874
+ return `W-${w.id} ${w.name} ${w.hostname}${offline}`;
867
875
  };
868
876
  var renderOne = (item, fmt, json) => json ? toJson(item) : fmt(item);
869
877
  var renderList = (items, fmt, json) => {
@@ -1113,7 +1121,10 @@ var resolveSession = async (client, projectId, handle) => {
1113
1121
 
1114
1122
  // src/commands/session/abort.ts
1115
1123
  var sessionAbortCommand = defineCommand({
1116
- meta: { name: "abort", description: "interrupt the in-flight turn (like Esc); keeps the session" },
1124
+ meta: {
1125
+ name: "abort",
1126
+ description: "interrupt the in-flight turn (like Esc); keeps the session"
1127
+ },
1117
1128
  args: {
1118
1129
  session: { type: "positional", required: true, description: "session int id or name" },
1119
1130
  project: { type: "string", description: "project id (overrides .baton.json)" },
@@ -1129,7 +1140,10 @@ var sessionAbortCommand = defineCommand({
1129
1140
 
1130
1141
  // src/commands/session/clear.ts
1131
1142
  var sessionClearCommand = defineCommand({
1132
- meta: { name: "clear", description: "reset the conversation context (keeps the session + worktree)" },
1143
+ meta: {
1144
+ name: "clear",
1145
+ description: "reset the conversation context (keeps the session + worktree)"
1146
+ },
1133
1147
  args: {
1134
1148
  session: { type: "positional", required: true, description: "session int id or name" },
1135
1149
  project: { type: "string", description: "project id (overrides .baton.json)" },
@@ -1148,16 +1162,24 @@ var sessionCreateCommand = defineCommand({
1148
1162
  meta: { name: "create", description: "create + start a session on a worker" },
1149
1163
  args: {
1150
1164
  name: { type: "positional", required: true, description: "session name" },
1151
- worker: { type: "string", description: "worker id (default: this machine's worker)" },
1152
- project: { type: "string", description: "project id (overrides .baton.json)" },
1165
+ worker: {
1166
+ type: "string",
1167
+ description: "worker handle: W-N or int id (default: this machine's worker)"
1168
+ },
1169
+ project: {
1170
+ type: "string",
1171
+ description: "project id (default: the worker's own project, else .baton.json)"
1172
+ },
1153
1173
  ...common
1154
1174
  },
1155
1175
  run: async ({ args }) => {
1156
1176
  const c = clientFor(args);
1157
- const projectId = resolveProjectId(args);
1158
- const workerId = args.worker ? Number(args.worker) : loadProjectConfigOrNull(projectConfigPath())?.worker?.id;
1159
- if (!workerId)
1160
- throw new Error("no worker id \u2014 pass --worker or run `baton worker register` first");
1177
+ const workerId = args.worker ? parseWorkerHandle(args.worker) : loadProjectConfigOrNull(projectConfigPath())?.worker?.id ?? null;
1178
+ if (workerId === null)
1179
+ throw new Error(
1180
+ "no worker \u2014 pass --worker <W-N|id> or run `baton worker register` first (names need --project)"
1181
+ );
1182
+ const projectId = args.project !== void 0 && args.project !== "" ? Number(args.project) : args.worker ? (await c.workers.get(workerId)).projectId : resolveProjectId(args);
1161
1183
  const s = await c.sessions.create({ projectId, workerId, name: args.name });
1162
1184
  console.log(renderOne(s, fmtSession, Boolean(args.json)));
1163
1185
  }
@@ -2492,26 +2514,36 @@ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2492
2514
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
2493
2515
  var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
2494
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();
2495
- var TITLE_TIMEOUT_MS = 3e4;
2496
- var generateTitle = async (input) => {
2497
- const { worktreePath, userText, assistantText, queryFn } = input;
2498
- if (`${userText}${assistantText}`.trim().length < 4) return null;
2517
+ var TITLE_TIMEOUT_MS = 9e4;
2518
+ var buildPrompt2 = (userText, assistantText) => {
2499
2519
  const exchange = [
2500
2520
  `User: ${clip(userText, 800)}`,
2501
2521
  assistantText ? `Assistant: ${clip(assistantText, 800)}` : ""
2502
2522
  ].filter(Boolean).join("\n");
2503
- 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.
2504
2524
 
2505
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" };
2506
2530
  const exe = claudeExecutable();
2507
2531
  const abort = new AbortController();
2508
2532
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
2533
+ const stderrTail = [];
2509
2534
  let out = "";
2535
+ let resultSeen = false;
2536
+ let failure = "";
2510
2537
  try {
2511
2538
  const messages = queryFn({
2512
- prompt,
2539
+ prompt: buildPrompt2(userText, assistantText),
2513
2540
  options: {
2514
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.",
2515
2547
  // Don't load the worktree's CLAUDE.md / project settings — they steer the
2516
2548
  // model into verbose summaries instead of a terse title.
2517
2549
  settingSources: [],
@@ -2519,20 +2551,33 @@ ${exchange}`;
2519
2551
  allowedTools: [],
2520
2552
  includePartialMessages: false,
2521
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
+ },
2522
2558
  ...exe ? { pathToClaudeCodeExecutable: exe } : {}
2523
2559
  }
2524
2560
  });
2525
2561
  for await (const m of messages) {
2526
- if (m.type === "result" && typeof m.result === "string")
2527
- 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)}` : ""}`;
2528
2568
  }
2529
- } catch {
2530
- return null;
2569
+ } catch (e) {
2570
+ failure = abort.signal.aborted ? `timeout after ${TITLE_TIMEOUT_MS / 1e3}s` : String(e).slice(0, 300);
2531
2571
  } finally {
2532
2572
  clearTimeout(timer);
2533
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
+ }
2534
2579
  const title = sanitizeTitle(out);
2535
- return !title || isDeclined(title) ? null : title;
2580
+ return !title || isDeclined(title) ? { kind: "declined" } : { kind: "titled", title };
2536
2581
  };
2537
2582
 
2538
2583
  // src/session/worktree.ts
@@ -2672,15 +2717,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
2672
2717
  const onTitle = async (sessionId, agentSessionId, worktreePath) => {
2673
2718
  const exchange = readFirstExchange(agentSessionId);
2674
2719
  if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
2675
- const title = await generateTitle({
2720
+ const outcome = await generateTitle({
2676
2721
  worktreePath,
2677
2722
  userText: exchange.userText,
2678
2723
  assistantText: exchange.assistantText,
2679
2724
  queryFn: query2
2680
2725
  });
2681
- if (!title) return log(`title #${sessionId}: not enough to title yet, skipping`);
2682
- await client.sessions.setName(sessionId, title, cfg.apiToken);
2683
- 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}`);
2684
2731
  };
2685
2732
  const reconcile = async () => {
2686
2733
  const owned = (await client.sessions.listByProject(cfg.projectId)).filter(
@@ -2750,9 +2797,9 @@ var readOrCreateMachineId = (path = machineIdPath()) => {
2750
2797
 
2751
2798
  // src/commands/worker.ts
2752
2799
  var resolveWorker = async (client, projectId, handle) => {
2753
- const asInt = Number(handle);
2754
- if (Number.isInteger(asInt) && asInt > 0) {
2755
- const byId = await client.workers.get(asInt).catch(() => null);
2800
+ const asId = parseWorkerHandle(handle);
2801
+ if (asId !== null) {
2802
+ const byId = await client.workers.get(asId).catch(() => null);
2756
2803
  if (byId) return { id: byId.id, name: byId.name };
2757
2804
  }
2758
2805
  const byName = await client.workers.findByName(projectId, handle);
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.2.4",
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": {