@termhub/agent 0.5.3 → 0.7.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/cli.js +119 -13
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -85,6 +85,12 @@ var RPC = {
|
|
|
85
85
|
recursive: z.boolean().optional()
|
|
86
86
|
}), z.object({ stdout: z.string() })),
|
|
87
87
|
"ai.credential": def(z.object({ provider: aiProvider, config_dir: machinePath.nullable() }), z.object({ stdout: z.string() }), 1e4),
|
|
88
|
+
/** Symlinks a Claude Code transcript into another account's config dir so `claude --resume` finds it there (since agent 0.7.0). */
|
|
89
|
+
"claude.linkSession": def(z.object({
|
|
90
|
+
transcript_path: machinePath,
|
|
91
|
+
session_id: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/),
|
|
92
|
+
config_dir: machinePath.nullable()
|
|
93
|
+
}), z.object({ status: z.enum(["linked", "same_account", "no_transcript", "no_config_dir", "conflict"]) }), 1e4),
|
|
88
94
|
"file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4),
|
|
89
95
|
/** Monitor hooks (see @termhub/machine-ops hooks.ts): the agent writes the script, env and config entries under its own $HOME. */
|
|
90
96
|
/** `claude_dirs`: Claude config dirs besides ~/.claude (accounts with CLAUDE_CONFIG_DIR), hooked when they exist; since agent 0.1.5. */
|
|
@@ -128,6 +134,8 @@ var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_sess
|
|
|
128
134
|
var CAPABILITY_CLAUDE = "claude";
|
|
129
135
|
var CAPABILITY_CLAUDE_SYSTEM_PROMPT = "claude.system_prompt";
|
|
130
136
|
var CAPABILITY_SIM = "sim";
|
|
137
|
+
var CAPABILITY_CLAUDE_STREAM_INPUT = "claude.stream_input";
|
|
138
|
+
var STREAM_END_INPUT_LINE = '{"type":"termhub_end_input"}';
|
|
131
139
|
var helloMessage = z2.object({
|
|
132
140
|
type: z2.literal("hello"),
|
|
133
141
|
protocol: z2.number().int().min(1),
|
|
@@ -171,9 +179,13 @@ var claudeOpenParams = z2.object({
|
|
|
171
179
|
// belongs at, so there is no separate, later place to hand it over.
|
|
172
180
|
token: z2.string(),
|
|
173
181
|
model: z2.string().nullable().optional(),
|
|
174
|
-
// Project chats
|
|
182
|
+
// Project chats and streamed runs: the server-composed text forwarded onto the CLI's argv (see
|
|
175
183
|
// `CAPABILITY_CLAUDE_SYSTEM_PROMPT`). Absent for the account-wide chat, whose argv must not change.
|
|
176
|
-
|
|
184
|
+
// 8000 because a streamed run carries the orchestrator prompt next to a project's (at most 4000
|
|
185
|
+
// each); only agents that advertise `CAPABILITY_CLAUDE_STREAM_INPUT` are ever sent more than 4000.
|
|
186
|
+
append_system_prompt: z2.string().max(8e3).nullable().optional(),
|
|
187
|
+
// See `CAPABILITY_CLAUDE_STREAM_INPUT`. Absent on every one-shot run, whose open frame must not change.
|
|
188
|
+
stream_input: z2.boolean().optional()
|
|
177
189
|
});
|
|
178
190
|
var tcpOpenParams = z2.object({ port: wdaPort }).strict();
|
|
179
191
|
var openPty = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams });
|
|
@@ -658,7 +670,7 @@ function claudeConfigDirs(accountDirs) {
|
|
|
658
670
|
function expandHome(dir, home) {
|
|
659
671
|
return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
|
|
660
672
|
}
|
|
661
|
-
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd"];
|
|
673
|
+
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "StopFailure", "SessionEnd"];
|
|
662
674
|
var CLAUDE_TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PermissionRequest"]);
|
|
663
675
|
var CURSOR_HOOK_EVENTS = ["sessionStart", "beforeSubmitPrompt", "afterAgentResponse", "stop", "sessionEnd"];
|
|
664
676
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
@@ -903,6 +915,32 @@ function stripCursorHooks(current) {
|
|
|
903
915
|
`;
|
|
904
916
|
}
|
|
905
917
|
|
|
918
|
+
// ../../packages/machine-ops/dist/claude-session.js
|
|
919
|
+
var CLAUDE_LINK_STATUSES = ["linked", "same_account", "no_transcript", "no_config_dir", "conflict"];
|
|
920
|
+
function claudeLinkScript(transcriptPath, sessionId, configDir) {
|
|
921
|
+
return [
|
|
922
|
+
configDirPrefix(configDir, ".claude"),
|
|
923
|
+
`SRC=${shellQuote(transcriptPath)}; SID=${shellQuote(sessionId)}`,
|
|
924
|
+
'[ -f "$SRC" ] || { echo no_transcript; exit 0; }',
|
|
925
|
+
'SLUGDIR=$(dirname "$SRC"); SLUG=$(basename "$SLUGDIR"); SRCROOT=$(dirname "$(dirname "$SLUGDIR")")',
|
|
926
|
+
'[ -d "$D" ] || { echo no_config_dir; exit 0; }',
|
|
927
|
+
'if [ "$(cd "$SRCROOT" && pwd -P)" = "$(cd "$D" && pwd -P)" ]; then echo same_account; exit 0; fi',
|
|
928
|
+
'mkdir -p "$D/projects/$SLUG" 2>/dev/null || { echo no_config_dir; exit 0; }',
|
|
929
|
+
'T="$D/projects/$SLUG/$SID.jsonl"',
|
|
930
|
+
'if [ -e "$T" ] || [ -L "$T" ]; then [ "$T" -ef "$SRC" ] || { echo conflict; exit 0; }; else ln -s "$SRC" "$T" 2>/dev/null || { echo conflict; exit 0; }; fi',
|
|
931
|
+
'if [ -d "$SLUGDIR/$SID" ] && [ ! -e "$D/projects/$SLUG/$SID" ] && [ ! -L "$D/projects/$SLUG/$SID" ]; then ln -s "$SLUGDIR/$SID" "$D/projects/$SLUG/$SID" 2>/dev/null; fi',
|
|
932
|
+
"echo linked"
|
|
933
|
+
].join("\n");
|
|
934
|
+
}
|
|
935
|
+
function parseClaudeLinkStatus(stdout) {
|
|
936
|
+
const words = stdout.split(/\s+/).filter(Boolean);
|
|
937
|
+
for (let i = words.length - 1; i >= 0; i--) {
|
|
938
|
+
if (CLAUDE_LINK_STATUSES.includes(words[i]))
|
|
939
|
+
return words[i];
|
|
940
|
+
}
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
|
|
906
944
|
// ../../packages/machine-ops/dist/discover.js
|
|
907
945
|
var CLAUDE_MARKERS = ["settings.json", "projects", ".credentials.json"];
|
|
908
946
|
var CLAUDE_DIR_NAME = /^\.claude[A-Za-z0-9._-]*$/;
|
|
@@ -1300,6 +1338,10 @@ async function uninstall(params, home = os3.homedir()) {
|
|
|
1300
1338
|
|
|
1301
1339
|
// ../../packages/claude-cli/dist/index.js
|
|
1302
1340
|
var DISALLOWED_TOOLS = "Bash,Read,Write,Edit,WebFetch,WebSearch";
|
|
1341
|
+
var BACKGROUND_AGENT_HOOK = `input=$(cat); case "$input" in *'"agent_id"'*) exit 0;; esac; printf '%s' "$input" | grep -Eq '"run_in_background"[[:space:]]*:[[:space:]]*true' && exit 0; echo 'No chat do termhub todo subagente roda em segundo plano: repita esta chamada do Agent com run_in_background: true.' >&2; exit 2`;
|
|
1342
|
+
var CONCIERGE_SETTINGS = JSON.stringify({
|
|
1343
|
+
hooks: { PreToolUse: [{ matcher: "Agent|Task", hooks: [{ type: "command", command: BACKGROUND_AGENT_HOOK }] }] }
|
|
1344
|
+
});
|
|
1303
1345
|
function buildClaudeArgs(spec) {
|
|
1304
1346
|
return [
|
|
1305
1347
|
"-p",
|
|
@@ -1322,6 +1364,7 @@ function buildClaudeArgs(spec) {
|
|
|
1322
1364
|
"mcp__termhub__*",
|
|
1323
1365
|
"--disallowed-tools",
|
|
1324
1366
|
DISALLOWED_TOOLS,
|
|
1367
|
+
...spec.stream_input ? ["--input-format", "stream-json", "--replay-user-messages", "--settings", CONCIERGE_SETTINGS] : [],
|
|
1325
1368
|
...spec.model ? ["--model", spec.model] : [],
|
|
1326
1369
|
// Last, and only when set: the account-wide chat's argv stays exactly what it was. It is our own
|
|
1327
1370
|
// server-composed text (a project's name, key and paths), never the user's prompt, which still
|
|
@@ -1346,6 +1389,8 @@ import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
|
1346
1389
|
import { tmpdir } from "os";
|
|
1347
1390
|
import { join } from "path";
|
|
1348
1391
|
var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
1392
|
+
var STREAM_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
1393
|
+
var MAX_PENDING_INPUT_BYTES = 256 * 1024;
|
|
1349
1394
|
var KILL_GRACE_MS = 2e3;
|
|
1350
1395
|
var PROMPT_TIMEOUT_MS = 3e4;
|
|
1351
1396
|
var MAX_LINE_BYTES = MAX_FRAME - HEADER_BYTES - 1;
|
|
@@ -1378,7 +1423,8 @@ function createClaudeManager(deps) {
|
|
|
1378
1423
|
}
|
|
1379
1424
|
const runDir = dir;
|
|
1380
1425
|
socket.sendControl({ type: "opened", ch });
|
|
1381
|
-
|
|
1426
|
+
const stream = params.stream_input === true;
|
|
1427
|
+
deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null, stream });
|
|
1382
1428
|
const env2 = { ...deps.env ?? agentEnv() };
|
|
1383
1429
|
if (params.config_dir === null) delete env2.CLAUDE_CONFIG_DIR;
|
|
1384
1430
|
else env2.CLAUDE_CONFIG_DIR = params.config_dir;
|
|
@@ -1387,7 +1433,8 @@ function createClaudeManager(deps) {
|
|
|
1387
1433
|
resume: params.resume,
|
|
1388
1434
|
mcp_config_path: mcpConfigPath,
|
|
1389
1435
|
model: params.model ?? null,
|
|
1390
|
-
append_system_prompt: params.append_system_prompt ?? null
|
|
1436
|
+
append_system_prompt: params.append_system_prompt ?? null,
|
|
1437
|
+
stream_input: stream
|
|
1391
1438
|
});
|
|
1392
1439
|
let child;
|
|
1393
1440
|
try {
|
|
@@ -1463,10 +1510,11 @@ function createClaudeManager(deps) {
|
|
|
1463
1510
|
deps.log("claude stream send failed", { ch, error: message(err) });
|
|
1464
1511
|
}
|
|
1465
1512
|
}
|
|
1513
|
+
const timeoutMs = deps.timeoutMs ?? (stream ? STREAM_TIMEOUT_MS : DEFAULT_TIMEOUT_MS2);
|
|
1466
1514
|
const timer = setTimeout(() => {
|
|
1467
|
-
deps.log("claude run timed out", { ch, timeoutMs
|
|
1515
|
+
deps.log("claude run timed out", { ch, timeoutMs });
|
|
1468
1516
|
killRun();
|
|
1469
|
-
},
|
|
1517
|
+
}, timeoutMs);
|
|
1470
1518
|
timer.unref();
|
|
1471
1519
|
const promptTimer = setTimeout(() => {
|
|
1472
1520
|
deps.log("claude run got no prompt", { ch, promptTimeoutMs: deps.promptTimeoutMs ?? PROMPT_TIMEOUT_MS });
|
|
@@ -1479,7 +1527,11 @@ function createClaudeManager(deps) {
|
|
|
1479
1527
|
promptSent: false,
|
|
1480
1528
|
kill: killRun,
|
|
1481
1529
|
promptArrived: () => clearTimeout(promptTimer),
|
|
1482
|
-
settle
|
|
1530
|
+
settle,
|
|
1531
|
+
stream,
|
|
1532
|
+
inputTail: "",
|
|
1533
|
+
inputEnded: false,
|
|
1534
|
+
discarding: false
|
|
1483
1535
|
};
|
|
1484
1536
|
runs.set(ch, run2);
|
|
1485
1537
|
child.stdin?.on("error", () => {
|
|
@@ -1525,13 +1577,51 @@ function createClaudeManager(deps) {
|
|
|
1525
1577
|
write(ch, data) {
|
|
1526
1578
|
const run2 = runs.get(ch);
|
|
1527
1579
|
if (!run2) return false;
|
|
1528
|
-
if (run2.
|
|
1529
|
-
|
|
1580
|
+
if (!run2.stream) {
|
|
1581
|
+
if (run2.promptSent) {
|
|
1582
|
+
deps.log("extra data on a claude channel ignored", { ch, bytes: data.length });
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
run2.promptSent = true;
|
|
1586
|
+
run2.promptArrived();
|
|
1587
|
+
run2.child.stdin?.end(data);
|
|
1530
1588
|
return true;
|
|
1531
1589
|
}
|
|
1532
1590
|
run2.promptSent = true;
|
|
1533
1591
|
run2.promptArrived();
|
|
1534
|
-
run2.
|
|
1592
|
+
if (run2.inputEnded) {
|
|
1593
|
+
deps.log("claude input after its end ignored", { ch, bytes: data.length });
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
let text = data.toString("utf8");
|
|
1597
|
+
if (run2.discarding) {
|
|
1598
|
+
const nl = text.indexOf("\n");
|
|
1599
|
+
if (nl === -1) return true;
|
|
1600
|
+
deps.log("claude input line too large, rest dropped", { ch, bytes: Buffer.byteLength(text.slice(0, nl), "utf8") });
|
|
1601
|
+
run2.discarding = false;
|
|
1602
|
+
text = text.slice(nl + 1);
|
|
1603
|
+
}
|
|
1604
|
+
run2.inputTail += text;
|
|
1605
|
+
for (; ; ) {
|
|
1606
|
+
const nl = run2.inputTail.indexOf("\n");
|
|
1607
|
+
if (nl === -1) break;
|
|
1608
|
+
const line = run2.inputTail.slice(0, nl);
|
|
1609
|
+
run2.inputTail = run2.inputTail.slice(nl + 1);
|
|
1610
|
+
if (line === STREAM_END_INPUT_LINE) {
|
|
1611
|
+
run2.inputEnded = true;
|
|
1612
|
+
if (run2.inputTail.length > 0) deps.log("claude input after its end ignored", { ch, bytes: Buffer.byteLength(run2.inputTail, "utf8") });
|
|
1613
|
+
run2.inputTail = "";
|
|
1614
|
+
run2.child.stdin?.end();
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
if (line.trim()) run2.child.stdin?.write(`${line}
|
|
1618
|
+
`);
|
|
1619
|
+
}
|
|
1620
|
+
if (Buffer.byteLength(run2.inputTail, "utf8") > (deps.maxPendingInputBytes ?? MAX_PENDING_INPUT_BYTES)) {
|
|
1621
|
+
deps.log("claude input line too large, dropped", { ch, bytes: Buffer.byteLength(run2.inputTail, "utf8") });
|
|
1622
|
+
run2.inputTail = "";
|
|
1623
|
+
run2.discarding = true;
|
|
1624
|
+
}
|
|
1535
1625
|
return true;
|
|
1536
1626
|
},
|
|
1537
1627
|
close(ch) {
|
|
@@ -1943,6 +2033,21 @@ async function credential(params) {
|
|
|
1943
2033
|
return { stdout: r.stdout };
|
|
1944
2034
|
}
|
|
1945
2035
|
|
|
2036
|
+
// src/rpc/claude.ts
|
|
2037
|
+
async function linkSession(params) {
|
|
2038
|
+
let script;
|
|
2039
|
+
try {
|
|
2040
|
+
script = claudeLinkScript(params.transcript_path, params.session_id, params.config_dir);
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
throw new RpcFailure("invalid", err instanceof Error ? err.message : "invalid config dir");
|
|
2043
|
+
}
|
|
2044
|
+
const r = await sh(script);
|
|
2045
|
+
if (r.timedOut) throw new RpcFailure("timeout", "claude.linkSession timed out");
|
|
2046
|
+
const status4 = parseClaudeLinkStatus(r.stdout);
|
|
2047
|
+
if (r.code !== 0 || !status4) throw new RpcFailure("internal", `claude.linkSession exited with code ${r.code}`);
|
|
2048
|
+
return { status: status4 };
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1946
2051
|
// src/rpc/fs.ts
|
|
1947
2052
|
function processFailure(method, r) {
|
|
1948
2053
|
if (r.timedOut) return new RpcFailure("timeout", `${method} timed out`);
|
|
@@ -2326,7 +2431,7 @@ async function status3() {
|
|
|
2326
2431
|
}
|
|
2327
2432
|
|
|
2328
2433
|
// src/version.ts
|
|
2329
|
-
var AGENT_VERSION = "0.
|
|
2434
|
+
var AGENT_VERSION = "0.7.0";
|
|
2330
2435
|
|
|
2331
2436
|
// src/rpc/update.ts
|
|
2332
2437
|
var PACKAGE = "@termhub/agent";
|
|
@@ -2446,6 +2551,7 @@ var handlers = {
|
|
|
2446
2551
|
"fs.list": list,
|
|
2447
2552
|
"fs.mkdir": mkdir2,
|
|
2448
2553
|
"ai.credential": credential,
|
|
2554
|
+
"claude.linkSession": linkSession,
|
|
2449
2555
|
"file.paste": pasteFile,
|
|
2450
2556
|
"hooks.install": install,
|
|
2451
2557
|
"hooks.uninstall": uninstall,
|
|
@@ -2465,7 +2571,7 @@ function detectOs(platform = process.platform) {
|
|
|
2465
2571
|
if (platform === "linux") return "linux";
|
|
2466
2572
|
return null;
|
|
2467
2573
|
}
|
|
2468
|
-
var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT];
|
|
2574
|
+
var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT, CAPABILITY_CLAUDE_STREAM_INPUT];
|
|
2469
2575
|
function capabilitiesFor(osName) {
|
|
2470
2576
|
return osName === "macos" ? [...CAPABILITIES, CAPABILITY_SIM] : [...CAPABILITIES];
|
|
2471
2577
|
}
|
package/package.json
CHANGED