@termhub/agent 0.7.1 → 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 +123 -16
  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;
@@ -2053,21 +2139,40 @@ async function linkSession(params) {
2053
2139
  return { status: status4 };
2054
2140
  }
2055
2141
 
2056
- // src/rpc/fs.ts
2142
+ // src/rpc/docs.ts
2057
2143
  function processFailure(method, r) {
2058
2144
  if (r.timedOut) return new RpcFailure("timeout", `${method} timed out`);
2059
2145
  if (r.code !== 0) return new RpcFailure("internal", `${method} exited with code ${r.code}`);
2060
2146
  return null;
2061
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
+ }
2062
2167
  async function list(params) {
2063
2168
  const r = await sh(buildFsListScript(shellQuote(params.path)));
2064
- const failure = processFailure("fs.list", r);
2169
+ const failure = processFailure2("fs.list", r);
2065
2170
  if (failure) throw failure;
2066
2171
  return { stdout: r.stdout };
2067
2172
  }
2068
2173
  async function mkdir2(params) {
2069
2174
  const r = await sh(buildMkdirScript(shellQuote(params.parent), shellQuote(params.name), { recursive: params.recursive === true }));
2070
- const failure = processFailure("fs.mkdir", r);
2175
+ const failure = processFailure2("fs.mkdir", r);
2071
2176
  if (failure) throw failure;
2072
2177
  return { stdout: r.stdout };
2073
2178
  }
@@ -2123,7 +2228,7 @@ async function boot(params) {
2123
2228
  // src/rpc/tmux.ts
2124
2229
  import { randomUUID } from "crypto";
2125
2230
  var ENTER_PAUSE_MS = 300;
2126
- function processFailure2(r) {
2231
+ function processFailure3(r) {
2127
2232
  if (r.timedOut) return new RpcFailure("timeout", "tmux timed out");
2128
2233
  if (r.error === "enoent") return new RpcFailure("no_tmux", "tmux not found");
2129
2234
  if (r.error === "maxbuffer") return new RpcFailure("internal", "output too large");
@@ -2131,7 +2236,7 @@ function processFailure2(r) {
2131
2236
  }
2132
2237
  async function list3(_params) {
2133
2238
  const r = await run(tmuxPath(), ["list-sessions", "-F", "#{session_name}"]);
2134
- const failure = processFailure2(r);
2239
+ const failure = processFailure3(r);
2135
2240
  if (failure) throw failure;
2136
2241
  if (r.code !== 0) return { sessions: [] };
2137
2242
  const sessions = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -2139,13 +2244,13 @@ async function list3(_params) {
2139
2244
  }
2140
2245
  async function kill(params) {
2141
2246
  const r = await run(tmuxPath(), ["kill-session", "-t", `=${params.session}`]);
2142
- const failure = processFailure2(r);
2247
+ const failure = processFailure3(r);
2143
2248
  if (failure) throw failure;
2144
2249
  return { killed: r.code === 0 };
2145
2250
  }
2146
2251
  async function capture(params) {
2147
2252
  const r = await run(tmuxPath(), ["capture-pane", "-p", ...params.escapes ? ["-e"] : [], "-S", `-${params.lines}`, "-t", `=${params.session}:`]);
2148
- const failure = processFailure2(r);
2253
+ const failure = processFailure3(r);
2149
2254
  if (failure) throw failure;
2150
2255
  if (r.code !== 0) throw new RpcFailure("notfound", "session not found");
2151
2256
  return params.escapes ? { text: r.stdout, escapes: true } : { text: r.stdout };
@@ -2154,11 +2259,11 @@ var pane = (session) => `=${session}:`;
2154
2259
  var why = (stderr, fallback) => stderr.trim().split("\n")[0] || fallback;
2155
2260
  async function ensure(params) {
2156
2261
  const has = await run(tmuxPath(), ["has-session", "-t", `=${params.session}`]);
2157
- const hasFailure = processFailure2(has);
2262
+ const hasFailure = processFailure3(has);
2158
2263
  if (hasFailure) throw hasFailure;
2159
2264
  if (has.code === 0) return { created: false };
2160
2265
  const made = await run(tmuxPath(), ["new-session", "-d", "-s", params.session, "-c", params.cwd]);
2161
- const madeFailure = processFailure2(made);
2266
+ const madeFailure = processFailure3(made);
2162
2267
  if (madeFailure) throw madeFailure;
2163
2268
  if (made.code !== 0) throw new RpcFailure("failed", why(made.stderr, "tmux new-session falhou"), params.cwd);
2164
2269
  return { created: true };
@@ -2167,11 +2272,11 @@ async function pasteText(session, text) {
2167
2272
  const bufferName = `termhub-paste-${randomUUID()}`;
2168
2273
  try {
2169
2274
  const loaded = await run(tmuxPath(), ["load-buffer", "-b", bufferName, "-"], { input: Buffer.from(text, "utf8") });
2170
- const loadFailure = processFailure2(loaded);
2275
+ const loadFailure = processFailure3(loaded);
2171
2276
  if (loadFailure) throw loadFailure;
2172
2277
  if (loaded.code !== 0) throw new RpcFailure("internal", why(loaded.stderr, "tmux load-buffer failed"));
2173
2278
  const pasted = await run(tmuxPath(), ["paste-buffer", "-p", "-d", "-b", bufferName, "-t", pane(session)]);
2174
- const pasteFailure = processFailure2(pasted);
2279
+ const pasteFailure = processFailure3(pasted);
2175
2280
  if (pasteFailure) throw pasteFailure;
2176
2281
  if (pasted.code !== 0) throw new RpcFailure("notfound", why(pasted.stderr, "session not found"));
2177
2282
  } finally {
@@ -2184,7 +2289,7 @@ async function sendText(params) {
2184
2289
  await pasteText(params.session, params.text);
2185
2290
  } else {
2186
2291
  const typed = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "-l", "--", params.text]);
2187
- const failure = processFailure2(typed);
2292
+ const failure = processFailure3(typed);
2188
2293
  if (failure) throw failure;
2189
2294
  if (typed.code !== 0) throw new RpcFailure("notfound", why(typed.stderr, "session not found"));
2190
2295
  }
@@ -2192,7 +2297,7 @@ async function sendText(params) {
2192
2297
  }
2193
2298
  if (params.enter) {
2194
2299
  const entered = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "Enter"]);
2195
- const failure = processFailure2(entered);
2300
+ const failure = processFailure3(entered);
2196
2301
  if (failure) throw failure;
2197
2302
  if (entered.code !== 0) throw new RpcFailure("notfound", why(entered.stderr, "session not found"));
2198
2303
  }
@@ -2200,7 +2305,7 @@ async function sendText(params) {
2200
2305
  }
2201
2306
  async function sendKey(params) {
2202
2307
  const r = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), params.key]);
2203
- const failure = processFailure2(r);
2308
+ const failure = processFailure3(r);
2204
2309
  if (failure) throw failure;
2205
2310
  if (r.code !== 0) throw new RpcFailure("notfound", why(r.stderr, "session not found"));
2206
2311
  return { sent: true };
@@ -2436,7 +2541,7 @@ async function status3() {
2436
2541
  }
2437
2542
 
2438
2543
  // src/version.ts
2439
- var AGENT_VERSION = "0.7.1";
2544
+ var AGENT_VERSION = "0.8.0";
2440
2545
 
2441
2546
  // src/rpc/update.ts
2442
2547
  var PACKAGE = "@termhub/agent";
@@ -2557,6 +2662,8 @@ var handlers = {
2557
2662
  "fs.mkdir": mkdir2,
2558
2663
  "ai.credential": credential,
2559
2664
  "claude.linkSession": linkSession,
2665
+ "docs.scan": scan,
2666
+ "docs.read": read,
2560
2667
  "file.paste": pasteFile,
2561
2668
  "hooks.install": install,
2562
2669
  "hooks.uninstall": uninstall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.7.1",
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,