@termhub/agent 0.4.3 → 0.5.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 +331 -32
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -39,12 +39,19 @@ 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
41
|
var aiProvider = z.enum(["claude", "chatgpt", "gemini", "antigravity"]);
|
|
42
|
+
var UDID_RE = /^[A-Fa-f0-9-]{8,64}$/;
|
|
43
|
+
var udid = z.string().regex(UDID_RE);
|
|
44
|
+
function isWdaPort(port) {
|
|
45
|
+
return Number.isInteger(port) && (port >= 8100 && port <= 8199 || port >= 9100 && port <= 9199);
|
|
46
|
+
}
|
|
47
|
+
var wdaPort = z.number().int().refine(isWdaPort, "port outside the WDA ranges");
|
|
42
48
|
var TMUX_KEYS = ["Enter", "Escape", "C-c", "Up", "Down", "Tab", "y", "n", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
43
49
|
var tmuxKey = z.enum(TMUX_KEYS);
|
|
44
50
|
var TEXT_MAX_CHARS = 4e3;
|
|
45
51
|
var rpcErrorSchema = z.object({
|
|
46
|
-
/** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user.
|
|
47
|
-
|
|
52
|
+
/** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user.
|
|
53
|
+
* `refused`: a `tcp` open found nothing listening on the port (ECONNREFUSED). */
|
|
54
|
+
code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal", "failed", "refused"]),
|
|
48
55
|
message: z.string().max(2e3),
|
|
49
56
|
path: z.string().max(4096).optional()
|
|
50
57
|
});
|
|
@@ -90,7 +97,19 @@ var RPC = {
|
|
|
90
97
|
}), 15e3),
|
|
91
98
|
"hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3),
|
|
92
99
|
/** Installs `version` of @termhub/agent with npm; when the agent runs as a service it then exits so the service relaunches the new code (since agent 0.2.1). */
|
|
93
|
-
"agent.update": def(z.object({ version: z.string().regex(/^\d+\.\d+\.\d+$/) }), z.object({ installed_version: z.string(), restart: z.enum(["service", "manual"]) }), 18e4)
|
|
100
|
+
"agent.update": def(z.object({ version: z.string().regex(/^\d+\.\d+\.\d+$/) }), z.object({ installed_version: z.string(), restart: z.enum(["service", "manual"]) }), 18e4),
|
|
101
|
+
/** iOS simulator over the agent (spec 2026-09-24): raw `xcrun simctl list devices -j`; the server parses it. */
|
|
102
|
+
"sim.list": def(z.object({}), z.object({ stdout: z.string() }), 15e3),
|
|
103
|
+
/** `xcrun simctl boot`; combined output, "already booted" included — the server decides what is a failure. */
|
|
104
|
+
"sim.boot": def(z.object({ udid }), z.object({ stdout: z.string() }), 6e4),
|
|
105
|
+
/** Starts the WDA runner in its tmux session; `started: false` when the session already existed. */
|
|
106
|
+
"wda.runner.start": def(z.object({ udid, wda_port: wdaPort, mjpeg_port: wdaPort }), z.object({ started: z.boolean() }), 1e4),
|
|
107
|
+
"wda.runner.alive": def(z.object({ udid }), z.object({ alive: z.boolean() })),
|
|
108
|
+
"wda.runner.tail": def(z.object({ udid, lines: z.number().int().min(1).max(200) }), z.object({ lines: z.array(z.string()) })),
|
|
109
|
+
/** Writes ~/.termhub/wda-setup.sh and runs it in tmux `termhub-wda-setup`; `started: false` when already running. */
|
|
110
|
+
"wda.setup.start": def(z.object({}), z.object({ started: z.boolean() }), 1e4),
|
|
111
|
+
/** Raw `STATE:/VERSION:/TAIL:` text; `parseSetupOutput` on the server reads it. */
|
|
112
|
+
"wda.setup.state": def(z.object({}), z.object({ stdout: z.string() }))
|
|
94
113
|
};
|
|
95
114
|
var RPC_METHODS = Object.keys(RPC);
|
|
96
115
|
var rpcMethod = z.enum(RPC_METHODS);
|
|
@@ -100,8 +119,10 @@ var PROTOCOL_VERSION = 1;
|
|
|
100
119
|
var CLOSE = { UNAUTHORIZED: 4401, CONFLICT: 4409, VIOLATION: 1008 };
|
|
101
120
|
var channel = z2.number().int().min(1).max(4294967295);
|
|
102
121
|
var rpcId = z2.string().min(1).max(64);
|
|
103
|
-
var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_session", "cli_rejected"]);
|
|
122
|
+
var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_session", "cli_rejected", "reset"]);
|
|
104
123
|
var CAPABILITY_CLAUDE = "claude";
|
|
124
|
+
var CAPABILITY_CLAUDE_SYSTEM_PROMPT = "claude.system_prompt";
|
|
125
|
+
var CAPABILITY_SIM = "sim";
|
|
105
126
|
var helloMessage = z2.object({
|
|
106
127
|
type: z2.literal("hello"),
|
|
107
128
|
protocol: z2.number().int().min(1),
|
|
@@ -144,19 +165,26 @@ var claudeOpenParams = z2.object({
|
|
|
144
165
|
// opening a channel to the user's own machine is exactly the trust boundary this grant
|
|
145
166
|
// belongs at, so there is no separate, later place to hand it over.
|
|
146
167
|
token: z2.string(),
|
|
147
|
-
model: z2.string().nullable().optional()
|
|
168
|
+
model: z2.string().nullable().optional(),
|
|
169
|
+
// Project chats only: the server-composed focus text forwarded onto the CLI's argv (see
|
|
170
|
+
// `CAPABILITY_CLAUDE_SYSTEM_PROMPT`). Absent for the account-wide chat, whose argv must not change.
|
|
171
|
+
append_system_prompt: z2.string().max(4e3).nullable().optional()
|
|
148
172
|
});
|
|
173
|
+
var tcpOpenParams = z2.object({ port: wdaPort }).strict();
|
|
149
174
|
var openPty = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams });
|
|
150
175
|
var openClaude = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("claude"), params: claudeOpenParams });
|
|
176
|
+
var openTcp = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("tcp"), params: tcpOpenParams });
|
|
151
177
|
var serverMessage = z2.union([
|
|
152
178
|
z2.object({ type: z2.literal("rpc"), id: rpcId, method: rpcMethod, params: z2.unknown() }),
|
|
153
179
|
openPty,
|
|
154
180
|
openClaude,
|
|
181
|
+
openTcp,
|
|
155
182
|
z2.object({ type: z2.literal("resize"), ch: channel, cols: z2.number().int().min(2).max(500), rows: z2.number().int().min(2).max(200) }),
|
|
156
183
|
z2.object({ type: z2.literal("close"), ch: channel })
|
|
157
184
|
]);
|
|
158
185
|
|
|
159
186
|
// src/client.ts
|
|
187
|
+
var MAX_STREAM_PAYLOAD = MAX_FRAME - HEADER_BYTES;
|
|
160
188
|
var RevokedError = class extends Error {
|
|
161
189
|
};
|
|
162
190
|
var ProtocolMismatchError = class extends Error {
|
|
@@ -245,7 +273,14 @@ function connectOnce(opts, signal) {
|
|
|
245
273
|
}, opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS);
|
|
246
274
|
socket = {
|
|
247
275
|
sendControl: (msg) => ws.send(encodeFrame(CONTROL_CHANNEL, JSON.stringify(msg))),
|
|
248
|
-
sendStream: (ch, data) =>
|
|
276
|
+
sendStream: (ch, data) => {
|
|
277
|
+
if (data.length <= MAX_STREAM_PAYLOAD) {
|
|
278
|
+
ws.send(encodeFrame(ch, data));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
for (let off = 0; off < data.length; off += MAX_STREAM_PAYLOAD) ws.send(encodeFrame(ch, data.subarray(off, off + MAX_STREAM_PAYLOAD)));
|
|
282
|
+
},
|
|
283
|
+
bufferedAmount: () => ws.bufferedAmount
|
|
249
284
|
};
|
|
250
285
|
const hello = { type: "hello", protocol: PROTOCOL_VERSION, ...opts.hello };
|
|
251
286
|
socket.sendControl(hello);
|
|
@@ -715,8 +750,8 @@ function mergeClaudeSettings(current, scriptPath, shown = "~/.claude/settings.js
|
|
|
715
750
|
throw new Error(`${shown}: o campo "hooks" n\xE3o \xE9 um objeto`);
|
|
716
751
|
const next = hooks ?? {};
|
|
717
752
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
718
|
-
const
|
|
719
|
-
const others =
|
|
753
|
+
const list4 = Array.isArray(next[event]) ? next[event] : [];
|
|
754
|
+
const others = list4.filter((e) => !isOurs(e));
|
|
720
755
|
const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
|
|
721
756
|
if (event === "PreToolUse")
|
|
722
757
|
entry.matcher = "*";
|
|
@@ -856,6 +891,47 @@ function configDirsFromRc(text) {
|
|
|
856
891
|
return out;
|
|
857
892
|
}
|
|
858
893
|
|
|
894
|
+
// ../../packages/machine-ops/dist/simulator.js
|
|
895
|
+
var WDA_DIR = "$HOME/.termhub/WebDriverAgent";
|
|
896
|
+
var WDA_SETUP_SESSION = "termhub-wda-setup";
|
|
897
|
+
var UDID_RE2 = /^[A-Fa-f0-9-]{8,64}$/;
|
|
898
|
+
function runnerSessionName(udid2) {
|
|
899
|
+
return `termhub-wda-${udid2.slice(0, 8).toLowerCase()}`;
|
|
900
|
+
}
|
|
901
|
+
var SIMCTL_LIST_SCRIPT = "xcrun simctl list devices -j";
|
|
902
|
+
var SIMCTL_BOOT_SCRIPT = 'xcrun simctl boot "$UDID" 2>&1 || true';
|
|
903
|
+
var WDA_RUNNER_START_SCRIPT = `tmux new-session -d -s "$SESSION" "cd ${WDA_DIR} && xcodebuild test-without-building -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination id=$UDID -derivedDataPath DerivedData USE_PORT=$WDA_PORT MJPEG_SERVER_PORT=$MJPEG_PORT"`;
|
|
904
|
+
var WDA_RUNNER_ALIVE_SCRIPT = `tmux has-session -t "=$SESSION" 2>/dev/null && echo yes || echo no`;
|
|
905
|
+
var WDA_RUNNER_TAIL_SCRIPT = `tmux capture-pane -p -t "=$SESSION" 2>/dev/null | grep -v '^$' | tail -n "$LINES"`;
|
|
906
|
+
var WDA_SETUP_SH = [
|
|
907
|
+
"#!/bin/sh",
|
|
908
|
+
'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"',
|
|
909
|
+
'mkdir -p "$HOME/.termhub"',
|
|
910
|
+
'rm -f "$HOME/.termhub/wda-setup.status"',
|
|
911
|
+
"{",
|
|
912
|
+
` if [ -d "${WDA_DIR}/.git" ]; then git -C "${WDA_DIR}" pull --ff-only; else git clone --depth 1 https://github.com/appium/WebDriverAgent "${WDA_DIR}"; fi &&`,
|
|
913
|
+
` cd "${WDA_DIR}" &&`,
|
|
914
|
+
" xcodebuild build-for-testing -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'generic/platform=iOS Simulator' -derivedDataPath DerivedData CODE_SIGNING_ALLOWED=NO",
|
|
915
|
+
'} > "$HOME/.termhub/wda-setup.log" 2>&1',
|
|
916
|
+
'echo $? > "$HOME/.termhub/wda-setup.status"',
|
|
917
|
+
""
|
|
918
|
+
].join("\n");
|
|
919
|
+
var WDA_SETUP_START_SCRIPT = [
|
|
920
|
+
`if tmux has-session -t '=${WDA_SETUP_SESSION}' 2>/dev/null; then echo STARTED:no; exit 0; fi`,
|
|
921
|
+
`mkdir -p "$HOME/.termhub" && cat > "$HOME/.termhub/wda-setup.sh" <<'TERMHUB_EOF'`,
|
|
922
|
+
WDA_SETUP_SH + "TERMHUB_EOF",
|
|
923
|
+
`chmod +x "$HOME/.termhub/wda-setup.sh" && tmux new-session -d -s ${WDA_SETUP_SESSION} 'sh "$HOME/.termhub/wda-setup.sh"' && echo STARTED:yes`
|
|
924
|
+
].join("\n");
|
|
925
|
+
var WDA_SETUP_STATE_SCRIPT = `
|
|
926
|
+
if tmux has-session -t '=${WDA_SETUP_SESSION}' 2>/dev/null; then echo STATE:running;
|
|
927
|
+
elif [ -f "$HOME/.termhub/wda-setup.status" ]; then
|
|
928
|
+
if [ "$(cat "$HOME/.termhub/wda-setup.status")" = 0 ]; then echo STATE:ok; else echo STATE:failed; fi;
|
|
929
|
+
else echo STATE:idle; fi
|
|
930
|
+
echo VERSION:$(sed -n 's/.*"version": *"\\([^"]*\\)".*/\\1/p' "${WDA_DIR}/package.json" 2>/dev/null | head -1)
|
|
931
|
+
echo TAIL:
|
|
932
|
+
tail -n 40 "$HOME/.termhub/wda-setup.log" 2>/dev/null
|
|
933
|
+
exit 0`;
|
|
934
|
+
|
|
859
935
|
// src/claude-dirs.ts
|
|
860
936
|
import { readdir, readFile, stat } from "fs/promises";
|
|
861
937
|
import path2 from "path";
|
|
@@ -883,8 +959,8 @@ async function fileNames(dir) {
|
|
|
883
959
|
}
|
|
884
960
|
async function candidateNames(home) {
|
|
885
961
|
try {
|
|
886
|
-
const
|
|
887
|
-
return
|
|
962
|
+
const list4 = await readdir(home, { withFileTypes: true });
|
|
963
|
+
return list4.filter((d) => d.isDirectory() && d.name.startsWith(".claude")).map((d) => d.name);
|
|
888
964
|
} catch {
|
|
889
965
|
return [];
|
|
890
966
|
}
|
|
@@ -929,12 +1005,12 @@ function tmuxPath() {
|
|
|
929
1005
|
return process.env.TMUX_PATH || "tmux";
|
|
930
1006
|
}
|
|
931
1007
|
function run(file, args, opts = {}) {
|
|
932
|
-
const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env = agentEnv() } = opts;
|
|
1008
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env: env2 = agentEnv() } = opts;
|
|
933
1009
|
return new Promise((resolve) => {
|
|
934
1010
|
const child = execFile(
|
|
935
1011
|
file,
|
|
936
1012
|
args,
|
|
937
|
-
{ timeout: timeoutMs, env, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
|
|
1013
|
+
{ timeout: timeoutMs, env: env2, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
|
|
938
1014
|
(err, stdout, stderr) => {
|
|
939
1015
|
const e = err;
|
|
940
1016
|
resolve({
|
|
@@ -1056,9 +1132,9 @@ async function install(params, home = os3.homedir()) {
|
|
|
1056
1132
|
}
|
|
1057
1133
|
async function heal(home = os3.homedir()) {
|
|
1058
1134
|
const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
|
|
1059
|
-
const
|
|
1135
|
+
const env2 = await readOrEmpty2(path3.join(home, HOOK_ENV_REL));
|
|
1060
1136
|
const script = await readOrEmpty2(scriptPath);
|
|
1061
|
-
if (!
|
|
1137
|
+
if (!env2.trim() || !script) return [];
|
|
1062
1138
|
if (script !== HOOK_SCRIPT) await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
1063
1139
|
const steps = [healClaudeDirs, healCursor, healCodex];
|
|
1064
1140
|
const healed = [];
|
|
@@ -1191,7 +1267,11 @@ function buildClaudeArgs(spec) {
|
|
|
1191
1267
|
"mcp__termhub__*",
|
|
1192
1268
|
"--disallowed-tools",
|
|
1193
1269
|
DISALLOWED_TOOLS,
|
|
1194
|
-
...spec.model ? ["--model", spec.model] : []
|
|
1270
|
+
...spec.model ? ["--model", spec.model] : [],
|
|
1271
|
+
// Last, and only when set: the account-wide chat's argv stays exactly what it was. It is our own
|
|
1272
|
+
// server-composed text (a project's name, key and paths), never the user's prompt, which still
|
|
1273
|
+
// travels on stdin only.
|
|
1274
|
+
...spec.append_system_prompt ? ["--append-system-prompt", spec.append_system_prompt] : []
|
|
1195
1275
|
];
|
|
1196
1276
|
}
|
|
1197
1277
|
function mcpConfig(url, token) {
|
|
@@ -1244,18 +1324,19 @@ function createClaudeManager(deps) {
|
|
|
1244
1324
|
const runDir = dir;
|
|
1245
1325
|
socket.sendControl({ type: "opened", ch });
|
|
1246
1326
|
deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null });
|
|
1247
|
-
const
|
|
1248
|
-
if (params.config_dir === null) delete
|
|
1249
|
-
else
|
|
1327
|
+
const env2 = { ...deps.env ?? agentEnv() };
|
|
1328
|
+
if (params.config_dir === null) delete env2.CLAUDE_CONFIG_DIR;
|
|
1329
|
+
else env2.CLAUDE_CONFIG_DIR = params.config_dir;
|
|
1250
1330
|
const args = buildClaudeArgs({
|
|
1251
1331
|
session_id: params.session_id,
|
|
1252
1332
|
resume: params.resume,
|
|
1253
1333
|
mcp_config_path: mcpConfigPath,
|
|
1254
|
-
model: params.model ?? null
|
|
1334
|
+
model: params.model ?? null,
|
|
1335
|
+
append_system_prompt: params.append_system_prompt ?? null
|
|
1255
1336
|
});
|
|
1256
1337
|
let child;
|
|
1257
1338
|
try {
|
|
1258
|
-
child = spawn(CLI, args, { env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
1339
|
+
child = spawn(CLI, args, { env: env2, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
1259
1340
|
} catch (err) {
|
|
1260
1341
|
deps.log("claude run could not be started", { ch, cli: CLI, error: message(err) });
|
|
1261
1342
|
rmSync(runDir, { recursive: true, force: true });
|
|
@@ -1454,7 +1535,7 @@ function createDispatcher(deps) {
|
|
|
1454
1535
|
void handleRpc(msg, socket, deps.handlers, deps.log);
|
|
1455
1536
|
break;
|
|
1456
1537
|
case "open": {
|
|
1457
|
-
const opened = msg.kind === "claude" ? deps.claude.open(msg.ch, msg.params, socket) : deps.pty.open(msg.ch, msg.params, socket);
|
|
1538
|
+
const opened = msg.kind === "claude" ? deps.claude.open(msg.ch, msg.params, socket) : msg.kind === "tcp" ? deps.tcp.open(msg.ch, msg.params, socket) : deps.pty.open(msg.ch, msg.params, socket);
|
|
1458
1539
|
opened.catch((err) => {
|
|
1459
1540
|
deps.log(`${msg.kind}.open rejected unexpectedly`, { ch: msg.ch, error: err instanceof Error ? err.message : String(err) });
|
|
1460
1541
|
});
|
|
@@ -1466,6 +1547,7 @@ function createDispatcher(deps) {
|
|
|
1466
1547
|
case "close":
|
|
1467
1548
|
deps.pty.close(msg.ch);
|
|
1468
1549
|
deps.claude.close(msg.ch);
|
|
1550
|
+
deps.tcp.close(msg.ch);
|
|
1469
1551
|
break;
|
|
1470
1552
|
}
|
|
1471
1553
|
};
|
|
@@ -1557,13 +1639,13 @@ function createPtyManager(deps) {
|
|
|
1557
1639
|
try {
|
|
1558
1640
|
({ cols, rows } = clampSize(params));
|
|
1559
1641
|
const cwd = resolveCwd(params.cwd);
|
|
1560
|
-
const
|
|
1642
|
+
const env2 = {
|
|
1561
1643
|
...ptyEnv(agentEnv(), process.env.SHELL ?? "/bin/sh"),
|
|
1562
1644
|
TERMHUB_TAB_ID: params.session,
|
|
1563
1645
|
TERMHUB_SESSION: params.session
|
|
1564
1646
|
};
|
|
1565
1647
|
const spawn2 = await resolveSpawn();
|
|
1566
|
-
const spawnTmux = () => spawn2(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
|
|
1648
|
+
const spawnTmux = () => spawn2(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env: env2 });
|
|
1567
1649
|
try {
|
|
1568
1650
|
proc = spawnTmux();
|
|
1569
1651
|
} catch (err) {
|
|
@@ -1656,6 +1738,141 @@ function createPtyManager(deps) {
|
|
|
1656
1738
|
};
|
|
1657
1739
|
}
|
|
1658
1740
|
|
|
1741
|
+
// src/tcp.ts
|
|
1742
|
+
import net from "net";
|
|
1743
|
+
var DEFAULT_HIGH_WATER = 4 * 1024 * 1024;
|
|
1744
|
+
var DEFAULT_LOW_WATER = 1024 * 1024;
|
|
1745
|
+
var DEFAULT_RESUME_POLL_MS = 50;
|
|
1746
|
+
function isRefused(err) {
|
|
1747
|
+
return err?.code === "ECONNREFUSED";
|
|
1748
|
+
}
|
|
1749
|
+
function createTcpManager(deps) {
|
|
1750
|
+
const channels = /* @__PURE__ */ new Map();
|
|
1751
|
+
const allowPort = deps.allowPort ?? isWdaPort;
|
|
1752
|
+
const highWater = deps.highWater ?? DEFAULT_HIGH_WATER;
|
|
1753
|
+
const lowWater = deps.lowWater ?? DEFAULT_LOW_WATER;
|
|
1754
|
+
const resumePollMs = deps.resumePollMs ?? DEFAULT_RESUME_POLL_MS;
|
|
1755
|
+
const control = (socket, msg) => {
|
|
1756
|
+
try {
|
|
1757
|
+
socket.sendControl(msg);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
deps.log("tcp control send failed", { type: msg.type, error: err instanceof Error ? err.message : String(err) });
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
const stopResumeTimer = (entry) => {
|
|
1763
|
+
if (entry.resumeTimer) clearInterval(entry.resumeTimer);
|
|
1764
|
+
entry.resumeTimer = null;
|
|
1765
|
+
};
|
|
1766
|
+
const drop = (ch, entry) => {
|
|
1767
|
+
entry.closed = true;
|
|
1768
|
+
stopResumeTimer(entry);
|
|
1769
|
+
if (channels.get(ch) === entry) channels.delete(ch);
|
|
1770
|
+
entry.sock.destroy();
|
|
1771
|
+
};
|
|
1772
|
+
return {
|
|
1773
|
+
async open(ch, params, socket) {
|
|
1774
|
+
if (channels.has(ch)) {
|
|
1775
|
+
control(socket, { type: "open_error", ch, error: { code: "invalid", message: "channel in use" } });
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (!allowPort(params.port)) {
|
|
1779
|
+
deps.log("tcp open refused: port not allowed", { ch, port: params.port });
|
|
1780
|
+
control(socket, { type: "open_error", ch, error: { code: "invalid", message: "port not allowed" } });
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const sock = net.connect({ host: "127.0.0.1", port: params.port });
|
|
1784
|
+
let opened = false;
|
|
1785
|
+
let closedSent = false;
|
|
1786
|
+
const entry = {
|
|
1787
|
+
sock,
|
|
1788
|
+
paused: false,
|
|
1789
|
+
resumeTimer: null,
|
|
1790
|
+
closed: false,
|
|
1791
|
+
sendClosed: (reason) => {
|
|
1792
|
+
if (closedSent) return;
|
|
1793
|
+
closedSent = true;
|
|
1794
|
+
control(socket, reason ? { type: "closed", ch, code: null, reason } : { type: "closed", ch, code: null });
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
channels.set(ch, entry);
|
|
1798
|
+
await new Promise((resolve) => {
|
|
1799
|
+
sock.once("connect", () => {
|
|
1800
|
+
opened = true;
|
|
1801
|
+
deps.log("tcp opened", { ch, port: params.port });
|
|
1802
|
+
control(socket, { type: "opened", ch });
|
|
1803
|
+
resolve();
|
|
1804
|
+
});
|
|
1805
|
+
sock.once("error", (err) => {
|
|
1806
|
+
if (!opened) {
|
|
1807
|
+
channels.delete(ch);
|
|
1808
|
+
stopResumeTimer(entry);
|
|
1809
|
+
deps.log("tcp open failed", { ch, port: params.port, code: err.code ?? "unknown" });
|
|
1810
|
+
control(socket, { type: "open_error", ch, error: isRefused(err) ? { code: "refused", message: "connection refused" } : { code: "internal", message: "connect failed" } });
|
|
1811
|
+
resolve();
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
deps.log("tcp socket error", { ch, port: params.port, code: err.code ?? "unknown" });
|
|
1815
|
+
entry.sock.destroy();
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
if (!channels.has(ch)) return;
|
|
1819
|
+
sock.on("data", (chunk) => {
|
|
1820
|
+
if (entry.closed) return;
|
|
1821
|
+
try {
|
|
1822
|
+
socket.sendStream(ch, chunk);
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
deps.log("tcp stream send failed", { ch, error: err instanceof Error ? err.message : String(err) });
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
const queued = socket.bufferedAmount?.() ?? 0;
|
|
1828
|
+
if (queued > highWater && !entry.paused) {
|
|
1829
|
+
entry.paused = true;
|
|
1830
|
+
sock.pause();
|
|
1831
|
+
deps.log("tcp paused", { ch, queued });
|
|
1832
|
+
entry.resumeTimer = setInterval(() => {
|
|
1833
|
+
if (entry.closed) {
|
|
1834
|
+
stopResumeTimer(entry);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if ((socket.bufferedAmount?.() ?? 0) <= lowWater) {
|
|
1838
|
+
stopResumeTimer(entry);
|
|
1839
|
+
entry.paused = false;
|
|
1840
|
+
sock.resume();
|
|
1841
|
+
deps.log("tcp resumed", { ch });
|
|
1842
|
+
}
|
|
1843
|
+
}, resumePollMs);
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
sock.on("close", (hadError) => {
|
|
1847
|
+
if (!opened) return;
|
|
1848
|
+
stopResumeTimer(entry);
|
|
1849
|
+
if (channels.get(ch) === entry) channels.delete(ch);
|
|
1850
|
+
if (entry.closed) return;
|
|
1851
|
+
deps.log("tcp closed", { ch, port: params.port, hadError });
|
|
1852
|
+
entry.sendClosed(hadError ? "reset" : void 0);
|
|
1853
|
+
});
|
|
1854
|
+
},
|
|
1855
|
+
write(ch, data) {
|
|
1856
|
+
const entry = channels.get(ch);
|
|
1857
|
+
if (!entry || entry.closed) return false;
|
|
1858
|
+
entry.sock.write(data);
|
|
1859
|
+
return true;
|
|
1860
|
+
},
|
|
1861
|
+
close(ch) {
|
|
1862
|
+
const entry = channels.get(ch);
|
|
1863
|
+
if (!entry) return;
|
|
1864
|
+
drop(ch, entry);
|
|
1865
|
+
entry.sendClosed();
|
|
1866
|
+
},
|
|
1867
|
+
closeAll() {
|
|
1868
|
+
for (const [ch, entry] of channels) drop(ch, entry);
|
|
1869
|
+
},
|
|
1870
|
+
isPaused(ch) {
|
|
1871
|
+
return channels.get(ch)?.paused ?? false;
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1659
1876
|
// src/rpc/ai.ts
|
|
1660
1877
|
async function credential(params) {
|
|
1661
1878
|
let prefix;
|
|
@@ -1711,6 +1928,33 @@ async function pasteFile(params) {
|
|
|
1711
1928
|
return { path: path12 };
|
|
1712
1929
|
}
|
|
1713
1930
|
|
|
1931
|
+
// src/rpc/sim.ts
|
|
1932
|
+
function checkUdid(udid2) {
|
|
1933
|
+
if (!UDID_RE2.test(udid2)) throw new RpcFailure("invalid", "invalid udid");
|
|
1934
|
+
}
|
|
1935
|
+
function scriptFailure(r, what) {
|
|
1936
|
+
if (r.timedOut) return new RpcFailure("timeout", `${what} timed out`);
|
|
1937
|
+
if (r.error === "enoent") return new RpcFailure("internal", "/bin/sh not found");
|
|
1938
|
+
if (r.code !== 0) {
|
|
1939
|
+
const why2 = r.stderr.trim().split("\n")[0] || r.stdout.trim().split("\n")[0] || `${what} exited with code ${r.code}`;
|
|
1940
|
+
return /tmux: (command )?not found/.test(r.stderr) ? new RpcFailure("no_tmux", "tmux not found") : new RpcFailure("failed", why2);
|
|
1941
|
+
}
|
|
1942
|
+
return null;
|
|
1943
|
+
}
|
|
1944
|
+
async function list2(_params) {
|
|
1945
|
+
const r = await sh(SIMCTL_LIST_SCRIPT, { timeoutMs: 14e3 });
|
|
1946
|
+
const failure = scriptFailure(r, "sim.list");
|
|
1947
|
+
if (failure) throw failure;
|
|
1948
|
+
return { stdout: r.stdout };
|
|
1949
|
+
}
|
|
1950
|
+
async function boot(params) {
|
|
1951
|
+
checkUdid(params.udid);
|
|
1952
|
+
const r = await sh(SIMCTL_BOOT_SCRIPT, { timeoutMs: 59e3, env: { ...agentEnv(), UDID: params.udid } });
|
|
1953
|
+
const failure = scriptFailure(r, "sim.boot");
|
|
1954
|
+
if (failure) throw failure;
|
|
1955
|
+
return { stdout: r.stdout + r.stderr };
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1714
1958
|
// src/rpc/tmux.ts
|
|
1715
1959
|
import { randomUUID } from "crypto";
|
|
1716
1960
|
var ENTER_PAUSE_MS = 300;
|
|
@@ -1720,7 +1964,7 @@ function processFailure2(r) {
|
|
|
1720
1964
|
if (r.error === "maxbuffer") return new RpcFailure("internal", "output too large");
|
|
1721
1965
|
return null;
|
|
1722
1966
|
}
|
|
1723
|
-
async function
|
|
1967
|
+
async function list3(_params) {
|
|
1724
1968
|
const r = await run(tmuxPath(), ["list-sessions", "-F", "#{session_name}"]);
|
|
1725
1969
|
const failure = processFailure2(r);
|
|
1726
1970
|
if (failure) throw failure;
|
|
@@ -2027,7 +2271,7 @@ async function status3() {
|
|
|
2027
2271
|
}
|
|
2028
2272
|
|
|
2029
2273
|
// src/version.ts
|
|
2030
|
-
var AGENT_VERSION = "0.
|
|
2274
|
+
var AGENT_VERSION = "0.5.0";
|
|
2031
2275
|
|
|
2032
2276
|
// src/rpc/update.ts
|
|
2033
2277
|
var PACKAGE = "@termhub/agent";
|
|
@@ -2091,9 +2335,52 @@ async function doUpdate(version, deps) {
|
|
|
2091
2335
|
}
|
|
2092
2336
|
var update = (params) => updateAgent(params);
|
|
2093
2337
|
|
|
2338
|
+
// src/rpc/wda.ts
|
|
2339
|
+
function env(vars) {
|
|
2340
|
+
return { ...agentEnv(), ...vars };
|
|
2341
|
+
}
|
|
2342
|
+
async function runnerStart(params) {
|
|
2343
|
+
checkUdid(params.udid);
|
|
2344
|
+
if (!isWdaPort(params.wda_port) || !isWdaPort(params.mjpeg_port)) throw new RpcFailure("invalid", "port outside the WDA ranges");
|
|
2345
|
+
const vars = { SESSION: runnerSessionName(params.udid), UDID: params.udid, WDA_PORT: String(params.wda_port), MJPEG_PORT: String(params.mjpeg_port) };
|
|
2346
|
+
const r = await sh(WDA_RUNNER_START_SCRIPT, { timeoutMs: 9e3, env: env(vars) });
|
|
2347
|
+
if (r.code !== 0 && r.stderr.includes("duplicate session")) return { started: false };
|
|
2348
|
+
const failure = scriptFailure(r, "wda.runner.start");
|
|
2349
|
+
if (failure) throw failure;
|
|
2350
|
+
return { started: true };
|
|
2351
|
+
}
|
|
2352
|
+
async function runnerAlive(params) {
|
|
2353
|
+
checkUdid(params.udid);
|
|
2354
|
+
const r = await sh(WDA_RUNNER_ALIVE_SCRIPT, { env: env({ SESSION: runnerSessionName(params.udid) }) });
|
|
2355
|
+
const failure = scriptFailure(r, "wda.runner.alive");
|
|
2356
|
+
if (failure) throw failure;
|
|
2357
|
+
return { alive: r.stdout.includes("yes") };
|
|
2358
|
+
}
|
|
2359
|
+
async function runnerTail(params) {
|
|
2360
|
+
checkUdid(params.udid);
|
|
2361
|
+
const r = await sh(WDA_RUNNER_TAIL_SCRIPT, { env: env({ SESSION: runnerSessionName(params.udid), LINES: String(params.lines) }) });
|
|
2362
|
+
const failure = scriptFailure(r, "wda.runner.tail");
|
|
2363
|
+
if (failure) throw failure;
|
|
2364
|
+
return { lines: r.stdout.split("\n").filter((l) => l.trim()) };
|
|
2365
|
+
}
|
|
2366
|
+
async function setupStart(_params) {
|
|
2367
|
+
const r = await sh(WDA_SETUP_START_SCRIPT, { timeoutMs: 9e3 });
|
|
2368
|
+
const failure = scriptFailure(r, "wda.setup.start");
|
|
2369
|
+
if (failure) throw failure;
|
|
2370
|
+
if (r.stdout.includes("STARTED:no")) return { started: false };
|
|
2371
|
+
if (r.stdout.includes("STARTED:yes")) return { started: true };
|
|
2372
|
+
throw new RpcFailure("failed", r.stderr.trim().split("\n")[0] || "setup did not start");
|
|
2373
|
+
}
|
|
2374
|
+
async function setupState(_params) {
|
|
2375
|
+
const r = await sh(WDA_SETUP_STATE_SCRIPT);
|
|
2376
|
+
const failure = scriptFailure(r, "wda.setup.state");
|
|
2377
|
+
if (failure) throw failure;
|
|
2378
|
+
return { stdout: r.stdout };
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2094
2381
|
// src/rpc/index.ts
|
|
2095
2382
|
var handlers = {
|
|
2096
|
-
"tmux.list":
|
|
2383
|
+
"tmux.list": list3,
|
|
2097
2384
|
"tmux.kill": kill,
|
|
2098
2385
|
"tmux.capture": capture,
|
|
2099
2386
|
"tmux.ensure": ensure,
|
|
@@ -2107,7 +2394,14 @@ var handlers = {
|
|
|
2107
2394
|
"file.paste": pasteFile,
|
|
2108
2395
|
"hooks.install": install,
|
|
2109
2396
|
"hooks.uninstall": uninstall,
|
|
2110
|
-
"agent.update": update
|
|
2397
|
+
"agent.update": update,
|
|
2398
|
+
"sim.list": list2,
|
|
2399
|
+
"sim.boot": boot,
|
|
2400
|
+
"wda.runner.start": runnerStart,
|
|
2401
|
+
"wda.runner.alive": runnerAlive,
|
|
2402
|
+
"wda.runner.tail": runnerTail,
|
|
2403
|
+
"wda.setup.start": setupStart,
|
|
2404
|
+
"wda.setup.state": setupState
|
|
2111
2405
|
};
|
|
2112
2406
|
|
|
2113
2407
|
// src/run.ts
|
|
@@ -2116,7 +2410,10 @@ function detectOs(platform = process.platform) {
|
|
|
2116
2410
|
if (platform === "linux") return "linux";
|
|
2117
2411
|
return null;
|
|
2118
2412
|
}
|
|
2119
|
-
var CAPABILITIES = [CAPABILITY_CLAUDE];
|
|
2413
|
+
var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT];
|
|
2414
|
+
function capabilitiesFor(osName) {
|
|
2415
|
+
return osName === "macos" ? [...CAPABILITIES, CAPABILITY_SIM] : [...CAPABILITIES];
|
|
2416
|
+
}
|
|
2120
2417
|
async function buildHello(osName) {
|
|
2121
2418
|
let tools = [];
|
|
2122
2419
|
try {
|
|
@@ -2132,7 +2429,7 @@ async function buildHello(osName) {
|
|
|
2132
2429
|
hostname: os6.hostname(),
|
|
2133
2430
|
tmux: tools.includes("tmux"),
|
|
2134
2431
|
tools,
|
|
2135
|
-
capabilities:
|
|
2432
|
+
capabilities: capabilitiesFor(osName)
|
|
2136
2433
|
};
|
|
2137
2434
|
}
|
|
2138
2435
|
var REVOKED_MESSAGE = "Token inv\xE1lido ou revogado";
|
|
@@ -2150,7 +2447,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
|
|
|
2150
2447
|
{
|
|
2151
2448
|
url: config.url,
|
|
2152
2449
|
token: config.token,
|
|
2153
|
-
hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], capabilities:
|
|
2450
|
+
hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], capabilities: capabilitiesFor(osName), probe: true },
|
|
2154
2451
|
onServerMessage: () => {
|
|
2155
2452
|
},
|
|
2156
2453
|
onStream: () => {
|
|
@@ -2193,7 +2490,8 @@ async function runAgent(config, opts) {
|
|
|
2193
2490
|
else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
|
|
2194
2491
|
const pty = createPtyManager({ log: opts.log });
|
|
2195
2492
|
const claude = createClaudeManager({ log: opts.log });
|
|
2196
|
-
const
|
|
2493
|
+
const tcp = createTcpManager({ log: opts.log });
|
|
2494
|
+
const dispatch = createDispatcher({ handlers, pty, claude, tcp, log: opts.log });
|
|
2197
2495
|
const healHooks = () => {
|
|
2198
2496
|
heal().then((dirs) => {
|
|
2199
2497
|
if (dirs.length) opts.log("monitor hooks repaired", { dirs: dirs.length });
|
|
@@ -2210,12 +2508,13 @@ async function runAgent(config, opts) {
|
|
|
2210
2508
|
// A frame belongs to whichever manager holds that channel: the claude one says so, and
|
|
2211
2509
|
// anything it does not own is a terminal's.
|
|
2212
2510
|
onStream: (ch, data) => {
|
|
2213
|
-
if (!claude.write(ch, data)) pty.write(ch, data);
|
|
2511
|
+
if (!claude.write(ch, data) && !tcp.write(ch, data)) pty.write(ch, data);
|
|
2214
2512
|
},
|
|
2215
2513
|
onConnect: healHooks,
|
|
2216
2514
|
onDisconnect: () => {
|
|
2217
2515
|
pty.closeAll();
|
|
2218
2516
|
claude.closeAll();
|
|
2517
|
+
tcp.closeAll();
|
|
2219
2518
|
},
|
|
2220
2519
|
log: opts.log
|
|
2221
2520
|
},
|
package/package.json
CHANGED