@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.
Files changed (2) hide show
  1. package/dist/cli.js +119 -13
  2. 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 only: the server-composed focus text forwarded onto the CLI's argv (see
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
- append_system_prompt: z2.string().max(4e3).nullable().optional()
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
- deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null });
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: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2 });
1515
+ deps.log("claude run timed out", { ch, timeoutMs });
1468
1516
  killRun();
1469
- }, deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
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.promptSent) {
1529
- deps.log("extra data on a claude channel ignored", { ch, bytes: data.length });
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.child.stdin?.end(data);
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.5.3";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.5.3",
3
+ "version": "0.7.0",
4
4
  "description": "Agente do termhub: conecta esta máquina ao servidor por WebSocket de saída (sem SSH) e expõe os terminais tmux no navegador.",
5
5
  "license": "MIT",
6
6
  "private": false,