@lesscap/baton-cli 0.1.3 → 0.2.0
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 +116 -178
- package/package.json +5 -2
package/dist/baton.mjs
CHANGED
|
@@ -1637,34 +1637,15 @@ function getBaseURL() {
|
|
|
1637
1637
|
}
|
|
1638
1638
|
|
|
1639
1639
|
// src/session/runner.ts
|
|
1640
|
-
import {
|
|
1640
|
+
import { spawnSync } from "node:child_process";
|
|
1641
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1641
1642
|
|
|
1642
1643
|
// src/session/runner/log.ts
|
|
1643
|
-
var claudeBin = () => process.env.BATON_CLAUDE_BIN ?? "claude";
|
|
1644
|
-
var buildClaudeArgs = (claudeSessionId, text, resuming) => [
|
|
1645
|
-
"--print",
|
|
1646
|
-
resuming ? "--resume" : "--session-id",
|
|
1647
|
-
claudeSessionId,
|
|
1648
|
-
"--output-format",
|
|
1649
|
-
"stream-json",
|
|
1650
|
-
"--verbose",
|
|
1651
|
-
"--dangerously-skip-permissions",
|
|
1652
|
-
text
|
|
1653
|
-
];
|
|
1654
|
-
var STDERR_TAIL_BYTES = 2048;
|
|
1655
|
-
var tailBuffer = (cap = STDERR_TAIL_BYTES) => {
|
|
1656
|
-
let buf = "";
|
|
1657
|
-
return {
|
|
1658
|
-
append(chunk) {
|
|
1659
|
-
buf = (buf + chunk).slice(-cap);
|
|
1660
|
-
},
|
|
1661
|
-
toString() {
|
|
1662
|
-
return buf;
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1665
|
-
};
|
|
1666
1644
|
var maskedEnvKeys = (env) => env ? Object.keys(env).sort().join(", ") : "(none)";
|
|
1667
|
-
|
|
1645
|
+
|
|
1646
|
+
// src/session/runner/sdk-env.ts
|
|
1647
|
+
var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
|
|
1648
|
+
var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
|
|
1668
1649
|
|
|
1669
1650
|
// src/session/runner/transcript.ts
|
|
1670
1651
|
import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
@@ -1788,90 +1769,73 @@ var augmentPrompt = (text, relPaths, labels) => {
|
|
|
1788
1769
|
${text}` : header;
|
|
1789
1770
|
};
|
|
1790
1771
|
|
|
1791
|
-
// src/session/runner/
|
|
1792
|
-
import
|
|
1793
|
-
var
|
|
1794
|
-
const
|
|
1795
|
-
const
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
} catch (err) {
|
|
1808
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1809
|
-
log(`[spawn] failed: ${message}`);
|
|
1810
|
-
await worker2.emitEvent("turn_error", { message: `spawn failed: ${message}` });
|
|
1811
|
-
return null;
|
|
1812
|
-
}
|
|
1813
|
-
if (!child.stdout) {
|
|
1814
|
-
log("[spawn] no stdout from claude \u2014 aborting turn");
|
|
1815
|
-
await worker2.emitEvent("turn_error", { message: "no stdout from claude" });
|
|
1816
|
-
return null;
|
|
1817
|
-
}
|
|
1818
|
-
const tail = tailBuffer();
|
|
1819
|
-
if (child.stderr) {
|
|
1820
|
-
const errLines = createInterface2({ input: child.stderr });
|
|
1821
|
-
void (async () => {
|
|
1822
|
-
for await (const line of errLines) {
|
|
1823
|
-
tail.append(`${line}
|
|
1824
|
-
`);
|
|
1825
|
-
log(`[claude] ${line}`);
|
|
1826
|
-
}
|
|
1827
|
-
})();
|
|
1828
|
-
}
|
|
1829
|
-
return { child, tail };
|
|
1772
|
+
// src/session/runner/query.ts
|
|
1773
|
+
import "@anthropic-ai/claude-agent-sdk";
|
|
1774
|
+
var startQuery = (config, text, resuming, queryFn, abortController, log, envOverlay) => {
|
|
1775
|
+
const env = buildSdkEnv(envOverlay);
|
|
1776
|
+
const exe = claudeExecutable();
|
|
1777
|
+
const options = {
|
|
1778
|
+
cwd: config.worktreePath,
|
|
1779
|
+
permissionMode: "bypassPermissions",
|
|
1780
|
+
includePartialMessages: false,
|
|
1781
|
+
abortController,
|
|
1782
|
+
stderr: (line) => log(`[claude] ${line.trimEnd()}`),
|
|
1783
|
+
...resuming ? { resume: config.agentSessionId } : { sessionId: config.agentSessionId },
|
|
1784
|
+
...env ? { env } : {},
|
|
1785
|
+
...exe ? { pathToClaudeCodeExecutable: exe } : {}
|
|
1786
|
+
};
|
|
1787
|
+
return queryFn({ prompt: text, options });
|
|
1830
1788
|
};
|
|
1831
1789
|
|
|
1832
1790
|
// src/session/runner/stream.ts
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
if (!trimmed) continue;
|
|
1839
|
-
let payload;
|
|
1840
|
-
try {
|
|
1841
|
-
payload = JSON.parse(trimmed);
|
|
1842
|
-
} catch {
|
|
1843
|
-
payload = { raw: trimmed };
|
|
1844
|
-
}
|
|
1845
|
-
await worker2.emitEvent("sdk_event", payload);
|
|
1791
|
+
var streamSdkEvents = async (messages, worker2) => {
|
|
1792
|
+
let result = null;
|
|
1793
|
+
for await (const message of messages) {
|
|
1794
|
+
await worker2.emitEvent("sdk_event", message);
|
|
1795
|
+
if (message.type === "result") result = toTurnResult(message);
|
|
1846
1796
|
}
|
|
1797
|
+
return result;
|
|
1798
|
+
};
|
|
1799
|
+
var toTurnResult = (message) => {
|
|
1800
|
+
const rec = message;
|
|
1801
|
+
const subtype = typeof rec.subtype === "string" ? rec.subtype : "unknown";
|
|
1802
|
+
const isError = rec.is_error === true || subtype !== "success";
|
|
1803
|
+
const resultText = typeof rec.result === "string" ? rec.result : "";
|
|
1804
|
+
return { subtype, isError, resultText };
|
|
1847
1805
|
};
|
|
1848
1806
|
|
|
1849
1807
|
// src/session/runner/turn.ts
|
|
1850
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1851
1808
|
var turnTimeoutMs = () => Number(process.env.BATON_TURN_TIMEOUT_MS) || 30 * 6e4;
|
|
1852
|
-
var
|
|
1853
|
-
var
|
|
1854
|
-
if (
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
};
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (
|
|
1809
|
+
var TIMEOUT = /* @__PURE__ */ Symbol("turn-timeout");
|
|
1810
|
+
var finalizeTurn = async (result, worker2, log) => {
|
|
1811
|
+
if (!result) {
|
|
1812
|
+
log("[exit] stream ended with no result");
|
|
1813
|
+
await worker2.emitEvent("turn_error", { message: "claude produced no result" });
|
|
1814
|
+
await worker2.emitEvent("turn_complete", {});
|
|
1815
|
+
return -1;
|
|
1816
|
+
}
|
|
1817
|
+
log(`[exit] subtype=${result.subtype} isError=${result.isError}`);
|
|
1818
|
+
if (result.isError) {
|
|
1862
1819
|
await worker2.emitEvent("turn_error", {
|
|
1863
|
-
message: `claude
|
|
1864
|
-
|
|
1865
|
-
stderrTail
|
|
1820
|
+
message: result.resultText || `claude result: ${result.subtype}`,
|
|
1821
|
+
subtype: result.subtype
|
|
1866
1822
|
});
|
|
1867
1823
|
}
|
|
1868
|
-
await worker2.emitEvent("turn_complete", {
|
|
1869
|
-
|
|
1870
|
-
|
|
1824
|
+
await worker2.emitEvent("turn_complete", { subtype: result.subtype });
|
|
1825
|
+
return result.isError ? 1 : 0;
|
|
1826
|
+
};
|
|
1827
|
+
var buildPrompt = async (config, rawText, attachments, log, fetchImpl) => {
|
|
1828
|
+
if (attachments.length === 0) return rawText;
|
|
1829
|
+
const relPaths = await materializeAttachments({
|
|
1830
|
+
worktreePath: config.worktreePath,
|
|
1831
|
+
serverBase: config.server,
|
|
1832
|
+
attachments,
|
|
1833
|
+
fetchImpl
|
|
1871
1834
|
});
|
|
1872
|
-
|
|
1835
|
+
log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
|
|
1836
|
+
return augmentPrompt(rawText, relPaths, labelAttachments(attachments));
|
|
1873
1837
|
};
|
|
1874
|
-
var runTurn = async (config, worker2, msg, resuming,
|
|
1838
|
+
var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => console.log(m), envOverlay, fetchImpl) => {
|
|
1875
1839
|
await worker2.emitEvent("turn_start", { messageId: msg.id });
|
|
1876
1840
|
const payload = msg.payload;
|
|
1877
1841
|
const rawText = typeof payload?.text === "string" ? payload.text : "";
|
|
@@ -1880,56 +1844,42 @@ var runTurn = async (config, worker2, msg, resuming, spawnImpl, log = (m) => con
|
|
|
1880
1844
|
await worker2.emitEvent("turn_error", { message: "user_message missing text and attachments" });
|
|
1881
1845
|
return -1;
|
|
1882
1846
|
}
|
|
1883
|
-
let text
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
fetchImpl
|
|
1891
|
-
});
|
|
1892
|
-
log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
|
|
1893
|
-
text = augmentPrompt(rawText, relPaths, labelAttachments(attachments));
|
|
1894
|
-
} catch (err) {
|
|
1895
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1896
|
-
await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
|
|
1897
|
-
return -1;
|
|
1898
|
-
}
|
|
1847
|
+
let text;
|
|
1848
|
+
try {
|
|
1849
|
+
text = await buildPrompt(config, rawText, attachments, log, fetchImpl);
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1852
|
+
await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
|
|
1853
|
+
return -1;
|
|
1899
1854
|
}
|
|
1900
|
-
const
|
|
1901
|
-
if (!result) return -1;
|
|
1902
|
-
const { child, tail } = result;
|
|
1903
|
-
const stdout = child.stdout;
|
|
1904
|
-
const exited = waitExit(child);
|
|
1905
|
-
let timedOut = false;
|
|
1855
|
+
const abort = new AbortController();
|
|
1906
1856
|
const ceiling = turnTimeoutMs();
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
}, ceiling);
|
|
1857
|
+
let timer;
|
|
1858
|
+
const timeout = new Promise((resolve) => {
|
|
1859
|
+
timer = setTimeout(() => resolve(TIMEOUT), ceiling);
|
|
1860
|
+
});
|
|
1912
1861
|
try {
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1862
|
+
const messages = startQuery(config, text, resuming, queryFn, abort, log, envOverlay);
|
|
1863
|
+
const consume = streamSdkEvents(messages, worker2);
|
|
1864
|
+
void consume.catch(() => {
|
|
1915
1865
|
});
|
|
1916
|
-
await Promise.race([
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
await worker2.emitEvent("turn_error", {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
});
|
|
1866
|
+
const outcome = await Promise.race([consume, timeout]);
|
|
1867
|
+
if (outcome === TIMEOUT) {
|
|
1868
|
+
log(`[watchdog] turn exceeded ${ceiling}ms \u2014 aborting claude`);
|
|
1869
|
+
abort.abort();
|
|
1870
|
+
await worker2.emitEvent("turn_error", { message: `turn exceeded ${ceiling}ms \u2014 claude aborted` });
|
|
1871
|
+
await worker2.emitEvent("turn_complete", {});
|
|
1872
|
+
return -1;
|
|
1924
1873
|
}
|
|
1925
|
-
return await finalizeTurn(
|
|
1874
|
+
return await finalizeTurn(outcome, worker2, log);
|
|
1926
1875
|
} catch (err) {
|
|
1927
1876
|
const message = err instanceof Error ? err.message : String(err);
|
|
1928
1877
|
log(`[run] ${message}`);
|
|
1929
|
-
await worker2.emitEvent("turn_error", { message
|
|
1878
|
+
await worker2.emitEvent("turn_error", { message });
|
|
1879
|
+
await worker2.emitEvent("turn_complete", {});
|
|
1930
1880
|
return -1;
|
|
1931
1881
|
} finally {
|
|
1932
|
-
clearTimeout(
|
|
1882
|
+
clearTimeout(timer);
|
|
1933
1883
|
}
|
|
1934
1884
|
};
|
|
1935
1885
|
|
|
@@ -1964,9 +1914,9 @@ var restoreTty = () => {
|
|
|
1964
1914
|
};
|
|
1965
1915
|
var runDaemon = async (config, deps, signal) => {
|
|
1966
1916
|
const log = deps.log ?? ((m) => console.log(`[#${config.sessionId} ${config.name}] ${m}`));
|
|
1967
|
-
const
|
|
1917
|
+
const qf = deps.queryFn ?? query2;
|
|
1968
1918
|
const ESCtor = deps.eventSourceImpl ?? EventSource;
|
|
1969
|
-
log(`bin: ${
|
|
1919
|
+
log(`bin: ${claudeExecutable() ?? "(sdk bundled)"} cwd: ${config.worktreePath}`);
|
|
1970
1920
|
log(`runtime env keys: ${maskedEnvKeys(deps.env)}`);
|
|
1971
1921
|
const state = {
|
|
1972
1922
|
firstSpawnDone: findTranscriptPath(config.agentSessionId) !== null,
|
|
@@ -1989,7 +1939,7 @@ var runDaemon = async (config, deps, signal) => {
|
|
|
1989
1939
|
deps.worker,
|
|
1990
1940
|
msg,
|
|
1991
1941
|
resuming,
|
|
1992
|
-
|
|
1942
|
+
qf,
|
|
1993
1943
|
log,
|
|
1994
1944
|
deps.env,
|
|
1995
1945
|
deps.fetchImpl
|
|
@@ -2345,11 +2295,12 @@ var task = defineCommand({
|
|
|
2345
2295
|
import { hostname as osHostname } from "node:os";
|
|
2346
2296
|
|
|
2347
2297
|
// src/worker/daemon.ts
|
|
2348
|
-
import { spawn
|
|
2298
|
+
import { spawn } from "node:child_process";
|
|
2349
2299
|
import { randomUUID } from "node:crypto";
|
|
2350
2300
|
import { existsSync as existsSync4 } from "node:fs";
|
|
2351
2301
|
import { dirname as dirname2, join as join5 } from "node:path";
|
|
2352
2302
|
import { fileURLToPath } from "node:url";
|
|
2303
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
2353
2304
|
|
|
2354
2305
|
// src/session/runner/title.ts
|
|
2355
2306
|
var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
|
|
@@ -2357,7 +2308,7 @@ var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NON
|
|
|
2357
2308
|
var sanitizeTitle = (raw) => raw.replace(/[\r\n]+/g, " ").replace(/["'`]/g, "").replace(/^\s*(title|session)\s*[:-]\s*/i, "").replace(/\s+/g, " ").trim().slice(0, 40).trim();
|
|
2358
2309
|
var TITLE_TIMEOUT_MS = 3e4;
|
|
2359
2310
|
var generateTitle = async (input) => {
|
|
2360
|
-
const { worktreePath, userText, assistantText,
|
|
2311
|
+
const { worktreePath, userText, assistantText, queryFn } = input;
|
|
2361
2312
|
if (`${userText}${assistantText}`.trim().length < 4) return null;
|
|
2362
2313
|
const exchange = [
|
|
2363
2314
|
`User: ${clip(userText, 800)}`,
|
|
@@ -2365,44 +2316,31 @@ var generateTitle = async (input) => {
|
|
|
2365
2316
|
].filter(Boolean).join("\n");
|
|
2366
2317
|
const prompt = `Reply with ONLY a 2-5 word title (no quotes, no punctuation) summarizing this coding session. If it is too short or generic to title meaningfully, reply with exactly NONE:
|
|
2367
2318
|
${exchange}`;
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
};
|
|
2377
|
-
let child;
|
|
2378
|
-
try {
|
|
2379
|
-
child = spawnImpl(claudeBin(), ["--print", "--dangerously-skip-permissions", prompt], {
|
|
2319
|
+
const exe = claudeExecutable();
|
|
2320
|
+
const abort = new AbortController();
|
|
2321
|
+
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
2322
|
+
let out = "";
|
|
2323
|
+
try {
|
|
2324
|
+
const messages = queryFn({
|
|
2325
|
+
prompt,
|
|
2326
|
+
options: {
|
|
2380
2327
|
cwd: worktreePath,
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
}
|
|
2387
|
-
const timer = setTimeout(() => {
|
|
2388
|
-
try {
|
|
2389
|
-
child.kill();
|
|
2390
|
-
} catch {
|
|
2328
|
+
permissionMode: "bypassPermissions",
|
|
2329
|
+
allowedTools: [],
|
|
2330
|
+
includePartialMessages: false,
|
|
2331
|
+
abortController: abort,
|
|
2332
|
+
...exe ? { pathToClaudeCodeExecutable: exe } : {}
|
|
2391
2333
|
}
|
|
2392
|
-
finish("");
|
|
2393
|
-
}, TITLE_TIMEOUT_MS);
|
|
2394
|
-
child.stdout?.on("data", (d) => {
|
|
2395
|
-
buf += String(d);
|
|
2396
|
-
});
|
|
2397
|
-
child.on("exit", () => {
|
|
2398
|
-
clearTimeout(timer);
|
|
2399
|
-
finish(buf);
|
|
2400
|
-
});
|
|
2401
|
-
child.on("error", () => {
|
|
2402
|
-
clearTimeout(timer);
|
|
2403
|
-
finish("");
|
|
2404
2334
|
});
|
|
2405
|
-
|
|
2335
|
+
for await (const m of messages) {
|
|
2336
|
+
if (m.type === "result" && typeof m.result === "string")
|
|
2337
|
+
out = m.result;
|
|
2338
|
+
}
|
|
2339
|
+
} catch {
|
|
2340
|
+
return null;
|
|
2341
|
+
} finally {
|
|
2342
|
+
clearTimeout(timer);
|
|
2343
|
+
}
|
|
2406
2344
|
const title = sanitizeTitle(out);
|
|
2407
2345
|
return !title || isDeclined(title) ? null : title;
|
|
2408
2346
|
};
|
|
@@ -2468,7 +2406,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
2468
2406
|
};
|
|
2469
2407
|
const spawnChild = (sessionId, worktreePath) => {
|
|
2470
2408
|
if (children.has(sessionId)) return;
|
|
2471
|
-
const child =
|
|
2409
|
+
const child = spawn(process.execPath, [binPath(), "session", "run", String(sessionId)], {
|
|
2472
2410
|
detached: true,
|
|
2473
2411
|
stdio: "inherit",
|
|
2474
2412
|
env: {
|
|
@@ -2528,7 +2466,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
2528
2466
|
worktreePath,
|
|
2529
2467
|
userText: exchange.userText,
|
|
2530
2468
|
assistantText: exchange.assistantText,
|
|
2531
|
-
|
|
2469
|
+
queryFn: query3
|
|
2532
2470
|
});
|
|
2533
2471
|
if (!title) return log(`title #${sessionId}: not enough to title yet, skipping`);
|
|
2534
2472
|
await client.sessions.setName(sessionId, title, cfg.apiToken);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lesscap/baton-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"baton": "tsx src/index.ts",
|
|
14
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --packages=bundle --banner:js=\"#!/usr/bin/env node\" --outfile=dist/baton.mjs && chmod +x dist/baton.mjs",
|
|
14
|
+
"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",
|
|
15
15
|
"prepublishOnly": "pnpm build",
|
|
16
16
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
17
17
|
"test": "tsx --test 'src/**/*.test.ts'"
|
|
@@ -24,5 +24,8 @@
|
|
|
24
24
|
"eventsource": "^4.1.0",
|
|
25
25
|
"tsx": "^4.22.4",
|
|
26
26
|
"typescript": "^6.0.3"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.161"
|
|
27
30
|
}
|
|
28
31
|
}
|