@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.
- package/dist/cli.js +123 -16
- 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/
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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