@termhub/agent 0.7.0 → 0.8.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 +129 -17
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -38,6 +38,8 @@ var SESSION_RE = /^[A-Za-z0-9_-]+$/;
38
38
  var sessionName = z.string().min(1).max(128).regex(SESSION_RE);
39
39
  var machinePath = z.string().min(1).max(4096).refine((p) => (p === "~" || p.startsWith("~/") || p.startsWith("/")) && !/[\0\n\r]/.test(p), "invalid path");
40
40
  var pasteName = z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/);
41
+ var DOC_PATH_RE = /^docs\/superpowers\/(specs|plans)\/[A-Za-z0-9._-]{1,200}\.md$/;
42
+ var docPath = z.string().regex(DOC_PATH_RE);
41
43
  var aiProvider = z.enum(["claude", "chatgpt", "gemini", "antigravity"]);
42
44
  var UDID_RE = /^[A-Fa-f0-9-]{8,64}$/;
43
45
  var udid = z.string().regex(UDID_RE);
@@ -120,7 +122,18 @@ var RPC = {
120
122
  /** Writes ~/.termhub/wda-setup.sh and runs it in tmux `termhub-wda-setup`; `started: false` when already running. */
121
123
  "wda.setup.start": def(z.object({}), z.object({ started: z.boolean() }), 1e4),
122
124
  /** Raw `STATE:/VERSION:/TAIL:` text; `parseSetupOutput` on the server reads it. */
123
- "wda.setup.state": def(z.object({}), z.object({ stdout: z.string() }))
125
+ "wda.setup.state": def(z.object({}), z.object({ stdout: z.string() })),
126
+ /**
127
+ * Manifest of `docs/superpowers/{specs,plans}/*.md` under `cwd`: `F\t<sha256>\t<size>\t<relpath>`
128
+ * lines, `@termhub/machine-ops`'s `parseDocsScan` reads them back (since agent 0.8.0).
129
+ */
130
+ "docs.scan": def(z.object({ cwd: machinePath }), z.object({ stdout: z.string() }), 15e3),
131
+ /**
132
+ * Base64 bodies for up to 20 chosen paths, each re-validated against the same tree on the
133
+ * machine (defence in depth — `docPath` already restricts what reaches this call); a file over
134
+ * `DOCS_MAX_BYTES` is skipped silently. `parseDocsRead` reads the output back (since agent 0.8.0).
135
+ */
136
+ "docs.read": def(z.object({ cwd: machinePath, paths: z.array(docPath).min(1).max(20) }), z.object({ stdout: z.string() }), 2e4)
124
137
  };
125
138
  var RPC_METHODS = Object.keys(RPC);
126
139
  var rpcMethod = z.enum(RPC_METHODS);
@@ -576,6 +589,79 @@ function buildMkdirScript(quotedParent, quotedName, opts = {}) {
576
589
  ].join("; ");
577
590
  }
578
591
 
592
+ // ../../packages/machine-ops/dist/docs-script.js
593
+ var DOCS_MAX_BYTES = 256 * 1024;
594
+ var DOCS_MAX_FILES = 200;
595
+ var DOCS_READ_MAX_BYTES = 600 * 1024;
596
+ var SYMLINKED_DIR_GUARD = [
597
+ `specs_ok=1; plans_ok=1`,
598
+ `[ ! -L docs ] || { specs_ok=0; plans_ok=0; }`,
599
+ `[ ! -L docs/superpowers ] || { specs_ok=0; plans_ok=0; }`,
600
+ `[ ! -L docs/superpowers/specs ] || specs_ok=0`,
601
+ `[ ! -L docs/superpowers/plans ] || plans_ok=0`
602
+ ].join("\n");
603
+ function buildDocsScanScript(cwdQuoted) {
604
+ return [
605
+ `P=${cwdQuoted}`,
606
+ EXPAND_HOME,
607
+ `cd -- "$P" 2>/dev/null || { echo 'ERR:notfound'; exit 0; }`,
608
+ `if command -v sha256sum >/dev/null 2>&1; then H='sha256sum'`,
609
+ `elif command -v shasum >/dev/null 2>&1; then H='shasum -a 256'`,
610
+ `else echo 'ERR:nohash'; exit 0`,
611
+ `fi`,
612
+ SYMLINKED_DIR_GUARD,
613
+ `n=0`,
614
+ `for d in docs/superpowers/specs docs/superpowers/plans; do`,
615
+ ` [ -d "$d" ] || continue`,
616
+ ` case "$d" in`,
617
+ ` docs/superpowers/specs) [ "$specs_ok" = 1 ] || continue ;;`,
618
+ ` docs/superpowers/plans) [ "$plans_ok" = 1 ] || continue ;;`,
619
+ ` esac`,
620
+ ` for f in "$d"/*.md; do`,
621
+ ` [ -f "$f" ] && [ ! -L "$f" ] && [ -r "$f" ] || continue`,
622
+ ` case "$f" in *[!A-Za-z0-9._/-]*) continue;; esac`,
623
+ ` n=$((n+1)); [ "$n" -le ${DOCS_MAX_FILES} ] || break 2`,
624
+ ` s=$(wc -c < "$f" | tr -d ' ')`,
625
+ ` [ -n "$s" ] || continue`,
626
+ ` if [ "$s" -le ${DOCS_MAX_BYTES} ]; then`,
627
+ ` h=$($H "$f" | cut -d' ' -f1)`,
628
+ ` printf 'F\\t%s\\t%s\\t%s\\n' "$h" "$s" "$f"`,
629
+ ` else`,
630
+ ` printf 'S\\t%s\\t%s\\n' "$s" "$f"`,
631
+ ` fi`,
632
+ ` done`,
633
+ `done`
634
+ ].join("\n");
635
+ }
636
+ function buildDocsReadScript(cwdQuoted, quotedPaths) {
637
+ return [
638
+ `P=${cwdQuoted}`,
639
+ EXPAND_HOME,
640
+ `cd -- "$P" 2>/dev/null || { echo 'ERR:notfound'; exit 0; }`,
641
+ SYMLINKED_DIR_GUARD,
642
+ `total=0`,
643
+ `for f in ${quotedPaths.join(" ")}; do`,
644
+ ` case "$f" in`,
645
+ ` docs/superpowers/specs/*.md) [ "$specs_ok" = 1 ] || continue ;;`,
646
+ ` docs/superpowers/plans/*.md) [ "$plans_ok" = 1 ] || continue ;;`,
647
+ ` *) continue ;;`,
648
+ ` esac`,
649
+ ` case "$f" in docs/superpowers/specs/*/*|docs/superpowers/plans/*/*) continue;; esac`,
650
+ ` case "$f" in *[!A-Za-z0-9._/-]*) continue;; esac`,
651
+ ` [ -f "$f" ] && [ ! -L "$f" ] && [ -r "$f" ] || continue`,
652
+ ` s=$(wc -c < "$f" | tr -d ' ')`,
653
+ ` [ -n "$s" ] || continue`,
654
+ ` [ "$s" -le ${DOCS_MAX_BYTES} ] || continue`,
655
+ ` newtotal=$((total + s))`,
656
+ ` [ "$newtotal" -le ${DOCS_READ_MAX_BYTES} ] || break`,
657
+ ` total=$newtotal`,
658
+ ` printf 'B\\t%s\\t%s\\n' "$s" "$f"`,
659
+ ` base64 < "$f"`,
660
+ ` echo E`,
661
+ `done`
662
+ ].join("\n");
663
+ }
664
+
579
665
  // ../../packages/machine-ops/dist/paste.js
580
666
  import { randomBytes } from "crypto";
581
667
  var PASTE_MAX_BYTES = 20 * 1024 * 1024;
@@ -1337,7 +1423,8 @@ async function uninstall(params, home = os3.homedir()) {
1337
1423
  }
1338
1424
 
1339
1425
  // ../../packages/claude-cli/dist/index.js
1340
- var DISALLOWED_TOOLS = "Bash,Read,Write,Edit,WebFetch,WebSearch";
1426
+ var CONCIERGE_TOOLS = "Agent";
1427
+ var DISALLOWED_TOOLS = "Bash,PowerShell,Monitor,Read,Write,Edit,NotebookEdit,Glob,Grep,EnterWorktree,WebFetch,WebSearch,RemoteTrigger";
1341
1428
  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
1429
  var CONCIERGE_SETTINGS = JSON.stringify({
1343
1430
  hooks: { PreToolUse: [{ matcher: "Agent|Task", hooks: [{ type: "command", command: BACKGROUND_AGENT_HOOK }] }] }
@@ -1364,6 +1451,8 @@ function buildClaudeArgs(spec) {
1364
1451
  "mcp__termhub__*",
1365
1452
  "--disallowed-tools",
1366
1453
  DISALLOWED_TOOLS,
1454
+ "--tools",
1455
+ CONCIERGE_TOOLS,
1367
1456
  ...spec.stream_input ? ["--input-format", "stream-json", "--replay-user-messages", "--settings", CONCIERGE_SETTINGS] : [],
1368
1457
  ...spec.model ? ["--model", spec.model] : [],
1369
1458
  // Last, and only when set: the account-wide chat's argv stays exactly what it was. It is our own
@@ -1380,6 +1469,8 @@ function classifyFailure(stderr) {
1380
1469
  return "missing_session";
1381
1470
  if (/^Error: --/m.test(stderr))
1382
1471
  return "cli_rejected";
1472
+ if (/^error: unknown option/m.test(stderr))
1473
+ return "cli_rejected";
1383
1474
  return "run_failed";
1384
1475
  }
1385
1476
 
@@ -2048,21 +2139,40 @@ async function linkSession(params) {
2048
2139
  return { status: status4 };
2049
2140
  }
2050
2141
 
2051
- // src/rpc/fs.ts
2142
+ // src/rpc/docs.ts
2052
2143
  function processFailure(method, r) {
2053
2144
  if (r.timedOut) return new RpcFailure("timeout", `${method} timed out`);
2054
2145
  if (r.code !== 0) return new RpcFailure("internal", `${method} exited with code ${r.code}`);
2055
2146
  return null;
2056
2147
  }
2148
+ async function scan(params) {
2149
+ const r = await sh(buildDocsScanScript(shellQuote(params.cwd)));
2150
+ const failure = processFailure("docs.scan", r);
2151
+ if (failure) throw failure;
2152
+ return { stdout: r.stdout };
2153
+ }
2154
+ async function read(params) {
2155
+ const r = await sh(buildDocsReadScript(shellQuote(params.cwd), params.paths.map(shellQuote)));
2156
+ const failure = processFailure("docs.read", r);
2157
+ if (failure) throw failure;
2158
+ return { stdout: r.stdout };
2159
+ }
2160
+
2161
+ // src/rpc/fs.ts
2162
+ function processFailure2(method, r) {
2163
+ if (r.timedOut) return new RpcFailure("timeout", `${method} timed out`);
2164
+ if (r.code !== 0) return new RpcFailure("internal", `${method} exited with code ${r.code}`);
2165
+ return null;
2166
+ }
2057
2167
  async function list(params) {
2058
2168
  const r = await sh(buildFsListScript(shellQuote(params.path)));
2059
- const failure = processFailure("fs.list", r);
2169
+ const failure = processFailure2("fs.list", r);
2060
2170
  if (failure) throw failure;
2061
2171
  return { stdout: r.stdout };
2062
2172
  }
2063
2173
  async function mkdir2(params) {
2064
2174
  const r = await sh(buildMkdirScript(shellQuote(params.parent), shellQuote(params.name), { recursive: params.recursive === true }));
2065
- const failure = processFailure("fs.mkdir", r);
2175
+ const failure = processFailure2("fs.mkdir", r);
2066
2176
  if (failure) throw failure;
2067
2177
  return { stdout: r.stdout };
2068
2178
  }
@@ -2118,7 +2228,7 @@ async function boot(params) {
2118
2228
  // src/rpc/tmux.ts
2119
2229
  import { randomUUID } from "crypto";
2120
2230
  var ENTER_PAUSE_MS = 300;
2121
- function processFailure2(r) {
2231
+ function processFailure3(r) {
2122
2232
  if (r.timedOut) return new RpcFailure("timeout", "tmux timed out");
2123
2233
  if (r.error === "enoent") return new RpcFailure("no_tmux", "tmux not found");
2124
2234
  if (r.error === "maxbuffer") return new RpcFailure("internal", "output too large");
@@ -2126,7 +2236,7 @@ function processFailure2(r) {
2126
2236
  }
2127
2237
  async function list3(_params) {
2128
2238
  const r = await run(tmuxPath(), ["list-sessions", "-F", "#{session_name}"]);
2129
- const failure = processFailure2(r);
2239
+ const failure = processFailure3(r);
2130
2240
  if (failure) throw failure;
2131
2241
  if (r.code !== 0) return { sessions: [] };
2132
2242
  const sessions = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -2134,13 +2244,13 @@ async function list3(_params) {
2134
2244
  }
2135
2245
  async function kill(params) {
2136
2246
  const r = await run(tmuxPath(), ["kill-session", "-t", `=${params.session}`]);
2137
- const failure = processFailure2(r);
2247
+ const failure = processFailure3(r);
2138
2248
  if (failure) throw failure;
2139
2249
  return { killed: r.code === 0 };
2140
2250
  }
2141
2251
  async function capture(params) {
2142
2252
  const r = await run(tmuxPath(), ["capture-pane", "-p", ...params.escapes ? ["-e"] : [], "-S", `-${params.lines}`, "-t", `=${params.session}:`]);
2143
- const failure = processFailure2(r);
2253
+ const failure = processFailure3(r);
2144
2254
  if (failure) throw failure;
2145
2255
  if (r.code !== 0) throw new RpcFailure("notfound", "session not found");
2146
2256
  return params.escapes ? { text: r.stdout, escapes: true } : { text: r.stdout };
@@ -2149,11 +2259,11 @@ var pane = (session) => `=${session}:`;
2149
2259
  var why = (stderr, fallback) => stderr.trim().split("\n")[0] || fallback;
2150
2260
  async function ensure(params) {
2151
2261
  const has = await run(tmuxPath(), ["has-session", "-t", `=${params.session}`]);
2152
- const hasFailure = processFailure2(has);
2262
+ const hasFailure = processFailure3(has);
2153
2263
  if (hasFailure) throw hasFailure;
2154
2264
  if (has.code === 0) return { created: false };
2155
2265
  const made = await run(tmuxPath(), ["new-session", "-d", "-s", params.session, "-c", params.cwd]);
2156
- const madeFailure = processFailure2(made);
2266
+ const madeFailure = processFailure3(made);
2157
2267
  if (madeFailure) throw madeFailure;
2158
2268
  if (made.code !== 0) throw new RpcFailure("failed", why(made.stderr, "tmux new-session falhou"), params.cwd);
2159
2269
  return { created: true };
@@ -2162,11 +2272,11 @@ async function pasteText(session, text) {
2162
2272
  const bufferName = `termhub-paste-${randomUUID()}`;
2163
2273
  try {
2164
2274
  const loaded = await run(tmuxPath(), ["load-buffer", "-b", bufferName, "-"], { input: Buffer.from(text, "utf8") });
2165
- const loadFailure = processFailure2(loaded);
2275
+ const loadFailure = processFailure3(loaded);
2166
2276
  if (loadFailure) throw loadFailure;
2167
2277
  if (loaded.code !== 0) throw new RpcFailure("internal", why(loaded.stderr, "tmux load-buffer failed"));
2168
2278
  const pasted = await run(tmuxPath(), ["paste-buffer", "-p", "-d", "-b", bufferName, "-t", pane(session)]);
2169
- const pasteFailure = processFailure2(pasted);
2279
+ const pasteFailure = processFailure3(pasted);
2170
2280
  if (pasteFailure) throw pasteFailure;
2171
2281
  if (pasted.code !== 0) throw new RpcFailure("notfound", why(pasted.stderr, "session not found"));
2172
2282
  } finally {
@@ -2179,7 +2289,7 @@ async function sendText(params) {
2179
2289
  await pasteText(params.session, params.text);
2180
2290
  } else {
2181
2291
  const typed = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "-l", "--", params.text]);
2182
- const failure = processFailure2(typed);
2292
+ const failure = processFailure3(typed);
2183
2293
  if (failure) throw failure;
2184
2294
  if (typed.code !== 0) throw new RpcFailure("notfound", why(typed.stderr, "session not found"));
2185
2295
  }
@@ -2187,7 +2297,7 @@ async function sendText(params) {
2187
2297
  }
2188
2298
  if (params.enter) {
2189
2299
  const entered = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "Enter"]);
2190
- const failure = processFailure2(entered);
2300
+ const failure = processFailure3(entered);
2191
2301
  if (failure) throw failure;
2192
2302
  if (entered.code !== 0) throw new RpcFailure("notfound", why(entered.stderr, "session not found"));
2193
2303
  }
@@ -2195,7 +2305,7 @@ async function sendText(params) {
2195
2305
  }
2196
2306
  async function sendKey(params) {
2197
2307
  const r = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), params.key]);
2198
- const failure = processFailure2(r);
2308
+ const failure = processFailure3(r);
2199
2309
  if (failure) throw failure;
2200
2310
  if (r.code !== 0) throw new RpcFailure("notfound", why(r.stderr, "session not found"));
2201
2311
  return { sent: true };
@@ -2431,7 +2541,7 @@ async function status3() {
2431
2541
  }
2432
2542
 
2433
2543
  // src/version.ts
2434
- var AGENT_VERSION = "0.7.0";
2544
+ var AGENT_VERSION = "0.8.0";
2435
2545
 
2436
2546
  // src/rpc/update.ts
2437
2547
  var PACKAGE = "@termhub/agent";
@@ -2552,6 +2662,8 @@ var handlers = {
2552
2662
  "fs.mkdir": mkdir2,
2553
2663
  "ai.credential": credential,
2554
2664
  "claude.linkSession": linkSession,
2665
+ "docs.scan": scan,
2666
+ "docs.read": read,
2555
2667
  "file.paste": pasteFile,
2556
2668
  "hooks.install": install,
2557
2669
  "hooks.uninstall": uninstall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.7.0",
3
+ "version": "0.8.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,