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