@termhub/agent 0.5.3 → 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 +70 -12
  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 });
@@ -1300,6 +1306,10 @@ async function uninstall(params, home = os3.homedir()) {
1300
1306
 
1301
1307
  // ../../packages/claude-cli/dist/index.js
1302
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
+ });
1303
1313
  function buildClaudeArgs(spec) {
1304
1314
  return [
1305
1315
  "-p",
@@ -1322,6 +1332,7 @@ function buildClaudeArgs(spec) {
1322
1332
  "mcp__termhub__*",
1323
1333
  "--disallowed-tools",
1324
1334
  DISALLOWED_TOOLS,
1335
+ ...spec.stream_input ? ["--input-format", "stream-json", "--replay-user-messages", "--settings", CONCIERGE_SETTINGS] : [],
1325
1336
  ...spec.model ? ["--model", spec.model] : [],
1326
1337
  // Last, and only when set: the account-wide chat's argv stays exactly what it was. It is our own
1327
1338
  // server-composed text (a project's name, key and paths), never the user's prompt, which still
@@ -1346,6 +1357,8 @@ import { mkdtempSync, rmSync, writeFileSync } from "fs";
1346
1357
  import { tmpdir } from "os";
1347
1358
  import { join } from "path";
1348
1359
  var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
1360
+ var STREAM_TIMEOUT_MS = 60 * 60 * 1e3;
1361
+ var MAX_PENDING_INPUT_BYTES = 256 * 1024;
1349
1362
  var KILL_GRACE_MS = 2e3;
1350
1363
  var PROMPT_TIMEOUT_MS = 3e4;
1351
1364
  var MAX_LINE_BYTES = MAX_FRAME - HEADER_BYTES - 1;
@@ -1378,7 +1391,8 @@ function createClaudeManager(deps) {
1378
1391
  }
1379
1392
  const runDir = dir;
1380
1393
  socket.sendControl({ type: "opened", ch });
1381
- 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 });
1382
1396
  const env2 = { ...deps.env ?? agentEnv() };
1383
1397
  if (params.config_dir === null) delete env2.CLAUDE_CONFIG_DIR;
1384
1398
  else env2.CLAUDE_CONFIG_DIR = params.config_dir;
@@ -1387,7 +1401,8 @@ function createClaudeManager(deps) {
1387
1401
  resume: params.resume,
1388
1402
  mcp_config_path: mcpConfigPath,
1389
1403
  model: params.model ?? null,
1390
- append_system_prompt: params.append_system_prompt ?? null
1404
+ append_system_prompt: params.append_system_prompt ?? null,
1405
+ stream_input: stream
1391
1406
  });
1392
1407
  let child;
1393
1408
  try {
@@ -1463,10 +1478,11 @@ function createClaudeManager(deps) {
1463
1478
  deps.log("claude stream send failed", { ch, error: message(err) });
1464
1479
  }
1465
1480
  }
1481
+ const timeoutMs = deps.timeoutMs ?? (stream ? STREAM_TIMEOUT_MS : DEFAULT_TIMEOUT_MS2);
1466
1482
  const timer = setTimeout(() => {
1467
- deps.log("claude run timed out", { ch, timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2 });
1483
+ deps.log("claude run timed out", { ch, timeoutMs });
1468
1484
  killRun();
1469
- }, deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
1485
+ }, timeoutMs);
1470
1486
  timer.unref();
1471
1487
  const promptTimer = setTimeout(() => {
1472
1488
  deps.log("claude run got no prompt", { ch, promptTimeoutMs: deps.promptTimeoutMs ?? PROMPT_TIMEOUT_MS });
@@ -1479,7 +1495,11 @@ function createClaudeManager(deps) {
1479
1495
  promptSent: false,
1480
1496
  kill: killRun,
1481
1497
  promptArrived: () => clearTimeout(promptTimer),
1482
- settle
1498
+ settle,
1499
+ stream,
1500
+ inputTail: "",
1501
+ inputEnded: false,
1502
+ discarding: false
1483
1503
  };
1484
1504
  runs.set(ch, run2);
1485
1505
  child.stdin?.on("error", () => {
@@ -1525,13 +1545,51 @@ function createClaudeManager(deps) {
1525
1545
  write(ch, data) {
1526
1546
  const run2 = runs.get(ch);
1527
1547
  if (!run2) return false;
1528
- if (run2.promptSent) {
1529
- 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);
1530
1556
  return true;
1531
1557
  }
1532
1558
  run2.promptSent = true;
1533
1559
  run2.promptArrived();
1534
- 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
+ }
1535
1593
  return true;
1536
1594
  },
1537
1595
  close(ch) {
@@ -2326,7 +2384,7 @@ async function status3() {
2326
2384
  }
2327
2385
 
2328
2386
  // src/version.ts
2329
- var AGENT_VERSION = "0.5.3";
2387
+ var AGENT_VERSION = "0.6.0";
2330
2388
 
2331
2389
  // src/rpc/update.ts
2332
2390
  var PACKAGE = "@termhub/agent";
@@ -2465,7 +2523,7 @@ function detectOs(platform = process.platform) {
2465
2523
  if (platform === "linux") return "linux";
2466
2524
  return null;
2467
2525
  }
2468
- var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT];
2526
+ var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT, CAPABILITY_CLAUDE_STREAM_INPUT];
2469
2527
  function capabilitiesFor(osName) {
2470
2528
  return osName === "macos" ? [...CAPABILITIES, CAPABILITY_SIM] : [...CAPABILITIES];
2471
2529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.5.3",
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,