@termhub/agent 0.5.2 → 0.6.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 +82 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -128,6 +128,8 @@ var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_sess
128
128
  var CAPABILITY_CLAUDE = "claude";
129
129
  var CAPABILITY_CLAUDE_SYSTEM_PROMPT = "claude.system_prompt";
130
130
  var CAPABILITY_SIM = "sim";
131
+ var CAPABILITY_CLAUDE_STREAM_INPUT = "claude.stream_input";
132
+ var STREAM_END_INPUT_LINE = '{"type":"termhub_end_input"}';
131
133
  var helloMessage = z2.object({
132
134
  type: z2.literal("hello"),
133
135
  protocol: z2.number().int().min(1),
@@ -171,9 +173,13 @@ var claudeOpenParams = z2.object({
171
173
  // belongs at, so there is no separate, later place to hand it over.
172
174
  token: z2.string(),
173
175
  model: z2.string().nullable().optional(),
174
- // Project chats only: the server-composed focus text forwarded onto the CLI's argv (see
176
+ // Project chats and streamed runs: the server-composed text forwarded onto the CLI's argv (see
175
177
  // `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()
178
+ // 8000 because a streamed run carries the orchestrator prompt next to a project's (at most 4000
179
+ // each); only agents that advertise `CAPABILITY_CLAUDE_STREAM_INPUT` are ever sent more than 4000.
180
+ append_system_prompt: z2.string().max(8e3).nullable().optional(),
181
+ // See `CAPABILITY_CLAUDE_STREAM_INPUT`. Absent on every one-shot run, whose open frame must not change.
182
+ stream_input: z2.boolean().optional()
177
183
  });
178
184
  var tcpOpenParams = z2.object({ port: wdaPort }).strict();
179
185
  var openPty = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams });
@@ -693,6 +699,15 @@ if [ "$KIND_REST" != "$EVENT" ]; then
693
699
  else
694
700
  KIND=
695
701
  fi
702
+ # A subagent's event (Claude Code 2.1.69+) carries an "agent_id" key before its own "hook_event_name" \u2014
703
+ # the order is session_id, transcript_path, cwd, prompt_id, permission_mode, agent_id, agent_type,
704
+ # hook_event_name, \u2026 \u2014 and the main thread's never does. Only that prefix is searched, and only for the
705
+ # key form: a value holding the text "agent_id": would have its quotes escaped. The reduced bodies below
706
+ # carry the flag; the server never lets a subagent's event close a card (spec 2026-09-26 \xA74.5). The
707
+ # dedupe marker ignores it.
708
+ BEFORE_KIND=\${EVENT%%'"hook_event_name"'*}
709
+ SUB=
710
+ case "$BEFORE_KIND" in *'"agent_id":'*) SUB=',"subagent":true' ;; esac
696
711
  case "$KIND" in
697
712
  PreToolUse)
698
713
  # The event's own tool name is the FIRST "tool_name" of the payload (Claude Code serialises it
@@ -742,9 +757,9 @@ case "$KIND" in
742
757
  [ "$(cat "$MARK" 2>/dev/null)" = "$KEY" ] && exit 0
743
758
  printf '%s' "$KEY" 2>/dev/null > "$MARK"
744
759
  if [ -n "$VERB" ]; then
745
- EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"}' "$NAME" "$VERB")
760
+ EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"%s}' "$NAME" "$VERB" "$SUB")
746
761
  else
747
- EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"}' "$NAME")
762
+ EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"%s}' "$NAME" "$SUB")
748
763
  fi
749
764
  fi
750
765
  ;;
@@ -757,7 +772,7 @@ case "$KIND" in
757
772
  REST=\${REST#*'"'}
758
773
  NAME=\${REST%%'"'*}
759
774
  case "$NAME" in '' | *[!A-Za-z0-9_.-]* | AskUserQuestion) exit 0 ;; esac
760
- EVENT=$(printf '{"hook_event_name":"PermissionRequest","tool_name":"%s"}' "$NAME")
775
+ EVENT=$(printf '{"hook_event_name":"PermissionRequest","tool_name":"%s"%s}' "$NAME" "$SUB")
761
776
  ;;
762
777
  # A new turn starts fresh, and so does an answered notification: a permission prompt takes the tab
763
778
  # out of working, and the tool the person approves is the same one that set the marker, so without
@@ -1291,6 +1306,10 @@ async function uninstall(params, home = os3.homedir()) {
1291
1306
 
1292
1307
  // ../../packages/claude-cli/dist/index.js
1293
1308
  var DISALLOWED_TOOLS = "Bash,Read,Write,Edit,WebFetch,WebSearch";
1309
+ 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`;
1310
+ var CONCIERGE_SETTINGS = JSON.stringify({
1311
+ hooks: { PreToolUse: [{ matcher: "Agent|Task", hooks: [{ type: "command", command: BACKGROUND_AGENT_HOOK }] }] }
1312
+ });
1294
1313
  function buildClaudeArgs(spec) {
1295
1314
  return [
1296
1315
  "-p",
@@ -1313,6 +1332,7 @@ function buildClaudeArgs(spec) {
1313
1332
  "mcp__termhub__*",
1314
1333
  "--disallowed-tools",
1315
1334
  DISALLOWED_TOOLS,
1335
+ ...spec.stream_input ? ["--input-format", "stream-json", "--replay-user-messages", "--settings", CONCIERGE_SETTINGS] : [],
1316
1336
  ...spec.model ? ["--model", spec.model] : [],
1317
1337
  // Last, and only when set: the account-wide chat's argv stays exactly what it was. It is our own
1318
1338
  // server-composed text (a project's name, key and paths), never the user's prompt, which still
@@ -1337,6 +1357,8 @@ import { mkdtempSync, rmSync, writeFileSync } from "fs";
1337
1357
  import { tmpdir } from "os";
1338
1358
  import { join } from "path";
1339
1359
  var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
1360
+ var STREAM_TIMEOUT_MS = 60 * 60 * 1e3;
1361
+ var MAX_PENDING_INPUT_BYTES = 256 * 1024;
1340
1362
  var KILL_GRACE_MS = 2e3;
1341
1363
  var PROMPT_TIMEOUT_MS = 3e4;
1342
1364
  var MAX_LINE_BYTES = MAX_FRAME - HEADER_BYTES - 1;
@@ -1369,7 +1391,8 @@ function createClaudeManager(deps) {
1369
1391
  }
1370
1392
  const runDir = dir;
1371
1393
  socket.sendControl({ type: "opened", ch });
1372
- deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null });
1394
+ const stream = params.stream_input === true;
1395
+ deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null, stream });
1373
1396
  const env2 = { ...deps.env ?? agentEnv() };
1374
1397
  if (params.config_dir === null) delete env2.CLAUDE_CONFIG_DIR;
1375
1398
  else env2.CLAUDE_CONFIG_DIR = params.config_dir;
@@ -1378,7 +1401,8 @@ function createClaudeManager(deps) {
1378
1401
  resume: params.resume,
1379
1402
  mcp_config_path: mcpConfigPath,
1380
1403
  model: params.model ?? null,
1381
- append_system_prompt: params.append_system_prompt ?? null
1404
+ append_system_prompt: params.append_system_prompt ?? null,
1405
+ stream_input: stream
1382
1406
  });
1383
1407
  let child;
1384
1408
  try {
@@ -1454,10 +1478,11 @@ function createClaudeManager(deps) {
1454
1478
  deps.log("claude stream send failed", { ch, error: message(err) });
1455
1479
  }
1456
1480
  }
1481
+ const timeoutMs = deps.timeoutMs ?? (stream ? STREAM_TIMEOUT_MS : DEFAULT_TIMEOUT_MS2);
1457
1482
  const timer = setTimeout(() => {
1458
- deps.log("claude run timed out", { ch, timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2 });
1483
+ deps.log("claude run timed out", { ch, timeoutMs });
1459
1484
  killRun();
1460
- }, deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
1485
+ }, timeoutMs);
1461
1486
  timer.unref();
1462
1487
  const promptTimer = setTimeout(() => {
1463
1488
  deps.log("claude run got no prompt", { ch, promptTimeoutMs: deps.promptTimeoutMs ?? PROMPT_TIMEOUT_MS });
@@ -1470,7 +1495,11 @@ function createClaudeManager(deps) {
1470
1495
  promptSent: false,
1471
1496
  kill: killRun,
1472
1497
  promptArrived: () => clearTimeout(promptTimer),
1473
- settle
1498
+ settle,
1499
+ stream,
1500
+ inputTail: "",
1501
+ inputEnded: false,
1502
+ discarding: false
1474
1503
  };
1475
1504
  runs.set(ch, run2);
1476
1505
  child.stdin?.on("error", () => {
@@ -1516,13 +1545,51 @@ function createClaudeManager(deps) {
1516
1545
  write(ch, data) {
1517
1546
  const run2 = runs.get(ch);
1518
1547
  if (!run2) return false;
1519
- if (run2.promptSent) {
1520
- deps.log("extra data on a claude channel ignored", { ch, bytes: data.length });
1548
+ if (!run2.stream) {
1549
+ if (run2.promptSent) {
1550
+ deps.log("extra data on a claude channel ignored", { ch, bytes: data.length });
1551
+ return true;
1552
+ }
1553
+ run2.promptSent = true;
1554
+ run2.promptArrived();
1555
+ run2.child.stdin?.end(data);
1521
1556
  return true;
1522
1557
  }
1523
1558
  run2.promptSent = true;
1524
1559
  run2.promptArrived();
1525
- run2.child.stdin?.end(data);
1560
+ if (run2.inputEnded) {
1561
+ deps.log("claude input after its end ignored", { ch, bytes: data.length });
1562
+ return true;
1563
+ }
1564
+ let text = data.toString("utf8");
1565
+ if (run2.discarding) {
1566
+ const nl = text.indexOf("\n");
1567
+ if (nl === -1) return true;
1568
+ deps.log("claude input line too large, rest dropped", { ch, bytes: Buffer.byteLength(text.slice(0, nl), "utf8") });
1569
+ run2.discarding = false;
1570
+ text = text.slice(nl + 1);
1571
+ }
1572
+ run2.inputTail += text;
1573
+ for (; ; ) {
1574
+ const nl = run2.inputTail.indexOf("\n");
1575
+ if (nl === -1) break;
1576
+ const line = run2.inputTail.slice(0, nl);
1577
+ run2.inputTail = run2.inputTail.slice(nl + 1);
1578
+ if (line === STREAM_END_INPUT_LINE) {
1579
+ run2.inputEnded = true;
1580
+ if (run2.inputTail.length > 0) deps.log("claude input after its end ignored", { ch, bytes: Buffer.byteLength(run2.inputTail, "utf8") });
1581
+ run2.inputTail = "";
1582
+ run2.child.stdin?.end();
1583
+ return true;
1584
+ }
1585
+ if (line.trim()) run2.child.stdin?.write(`${line}
1586
+ `);
1587
+ }
1588
+ if (Buffer.byteLength(run2.inputTail, "utf8") > (deps.maxPendingInputBytes ?? MAX_PENDING_INPUT_BYTES)) {
1589
+ deps.log("claude input line too large, dropped", { ch, bytes: Buffer.byteLength(run2.inputTail, "utf8") });
1590
+ run2.inputTail = "";
1591
+ run2.discarding = true;
1592
+ }
1526
1593
  return true;
1527
1594
  },
1528
1595
  close(ch) {
@@ -2317,7 +2384,7 @@ async function status3() {
2317
2384
  }
2318
2385
 
2319
2386
  // src/version.ts
2320
- var AGENT_VERSION = "0.5.2";
2387
+ var AGENT_VERSION = "0.6.0";
2321
2388
 
2322
2389
  // src/rpc/update.ts
2323
2390
  var PACKAGE = "@termhub/agent";
@@ -2456,7 +2523,7 @@ function detectOs(platform = process.platform) {
2456
2523
  if (platform === "linux") return "linux";
2457
2524
  return null;
2458
2525
  }
2459
- var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT];
2526
+ var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT, CAPABILITY_CLAUDE_STREAM_INPUT];
2460
2527
  function capabilitiesFor(osName) {
2461
2528
  return osName === "macos" ? [...CAPABILITIES, CAPABILITY_SIM] : [...CAPABILITIES];
2462
2529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.5.2",
3
+ "version": "0.6.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,