@termhub/agent 0.4.4 → 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 +318 -28
- 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,9 +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";
|
|
105
124
|
var CAPABILITY_CLAUDE_SYSTEM_PROMPT = "claude.system_prompt";
|
|
125
|
+
var CAPABILITY_SIM = "sim";
|
|
106
126
|
var helloMessage = z2.object({
|
|
107
127
|
type: z2.literal("hello"),
|
|
108
128
|
protocol: z2.number().int().min(1),
|
|
@@ -150,17 +170,21 @@ var claudeOpenParams = z2.object({
|
|
|
150
170
|
// `CAPABILITY_CLAUDE_SYSTEM_PROMPT`). Absent for the account-wide chat, whose argv must not change.
|
|
151
171
|
append_system_prompt: z2.string().max(4e3).nullable().optional()
|
|
152
172
|
});
|
|
173
|
+
var tcpOpenParams = z2.object({ port: wdaPort }).strict();
|
|
153
174
|
var openPty = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams });
|
|
154
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 });
|
|
155
177
|
var serverMessage = z2.union([
|
|
156
178
|
z2.object({ type: z2.literal("rpc"), id: rpcId, method: rpcMethod, params: z2.unknown() }),
|
|
157
179
|
openPty,
|
|
158
180
|
openClaude,
|
|
181
|
+
openTcp,
|
|
159
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) }),
|
|
160
183
|
z2.object({ type: z2.literal("close"), ch: channel })
|
|
161
184
|
]);
|
|
162
185
|
|
|
163
186
|
// src/client.ts
|
|
187
|
+
var MAX_STREAM_PAYLOAD = MAX_FRAME - HEADER_BYTES;
|
|
164
188
|
var RevokedError = class extends Error {
|
|
165
189
|
};
|
|
166
190
|
var ProtocolMismatchError = class extends Error {
|
|
@@ -249,7 +273,14 @@ function connectOnce(opts, signal) {
|
|
|
249
273
|
}, opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS);
|
|
250
274
|
socket = {
|
|
251
275
|
sendControl: (msg) => ws.send(encodeFrame(CONTROL_CHANNEL, JSON.stringify(msg))),
|
|
252
|
-
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
|
|
253
284
|
};
|
|
254
285
|
const hello = { type: "hello", protocol: PROTOCOL_VERSION, ...opts.hello };
|
|
255
286
|
socket.sendControl(hello);
|
|
@@ -719,8 +750,8 @@ function mergeClaudeSettings(current, scriptPath, shown = "~/.claude/settings.js
|
|
|
719
750
|
throw new Error(`${shown}: o campo "hooks" n\xE3o \xE9 um objeto`);
|
|
720
751
|
const next = hooks ?? {};
|
|
721
752
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
722
|
-
const
|
|
723
|
-
const others =
|
|
753
|
+
const list4 = Array.isArray(next[event]) ? next[event] : [];
|
|
754
|
+
const others = list4.filter((e) => !isOurs(e));
|
|
724
755
|
const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
|
|
725
756
|
if (event === "PreToolUse")
|
|
726
757
|
entry.matcher = "*";
|
|
@@ -860,6 +891,47 @@ function configDirsFromRc(text) {
|
|
|
860
891
|
return out;
|
|
861
892
|
}
|
|
862
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
|
+
|
|
863
935
|
// src/claude-dirs.ts
|
|
864
936
|
import { readdir, readFile, stat } from "fs/promises";
|
|
865
937
|
import path2 from "path";
|
|
@@ -887,8 +959,8 @@ async function fileNames(dir) {
|
|
|
887
959
|
}
|
|
888
960
|
async function candidateNames(home) {
|
|
889
961
|
try {
|
|
890
|
-
const
|
|
891
|
-
return
|
|
962
|
+
const list4 = await readdir(home, { withFileTypes: true });
|
|
963
|
+
return list4.filter((d) => d.isDirectory() && d.name.startsWith(".claude")).map((d) => d.name);
|
|
892
964
|
} catch {
|
|
893
965
|
return [];
|
|
894
966
|
}
|
|
@@ -933,12 +1005,12 @@ function tmuxPath() {
|
|
|
933
1005
|
return process.env.TMUX_PATH || "tmux";
|
|
934
1006
|
}
|
|
935
1007
|
function run(file, args, opts = {}) {
|
|
936
|
-
const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env = agentEnv() } = opts;
|
|
1008
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env: env2 = agentEnv() } = opts;
|
|
937
1009
|
return new Promise((resolve) => {
|
|
938
1010
|
const child = execFile(
|
|
939
1011
|
file,
|
|
940
1012
|
args,
|
|
941
|
-
{ timeout: timeoutMs, env, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
|
|
1013
|
+
{ timeout: timeoutMs, env: env2, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
|
|
942
1014
|
(err, stdout, stderr) => {
|
|
943
1015
|
const e = err;
|
|
944
1016
|
resolve({
|
|
@@ -1060,9 +1132,9 @@ async function install(params, home = os3.homedir()) {
|
|
|
1060
1132
|
}
|
|
1061
1133
|
async function heal(home = os3.homedir()) {
|
|
1062
1134
|
const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
|
|
1063
|
-
const
|
|
1135
|
+
const env2 = await readOrEmpty2(path3.join(home, HOOK_ENV_REL));
|
|
1064
1136
|
const script = await readOrEmpty2(scriptPath);
|
|
1065
|
-
if (!
|
|
1137
|
+
if (!env2.trim() || !script) return [];
|
|
1066
1138
|
if (script !== HOOK_SCRIPT) await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
1067
1139
|
const steps = [healClaudeDirs, healCursor, healCodex];
|
|
1068
1140
|
const healed = [];
|
|
@@ -1252,9 +1324,9 @@ function createClaudeManager(deps) {
|
|
|
1252
1324
|
const runDir = dir;
|
|
1253
1325
|
socket.sendControl({ type: "opened", ch });
|
|
1254
1326
|
deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null });
|
|
1255
|
-
const
|
|
1256
|
-
if (params.config_dir === null) delete
|
|
1257
|
-
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;
|
|
1258
1330
|
const args = buildClaudeArgs({
|
|
1259
1331
|
session_id: params.session_id,
|
|
1260
1332
|
resume: params.resume,
|
|
@@ -1264,7 +1336,7 @@ function createClaudeManager(deps) {
|
|
|
1264
1336
|
});
|
|
1265
1337
|
let child;
|
|
1266
1338
|
try {
|
|
1267
|
-
child = spawn(CLI, args, { env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
1339
|
+
child = spawn(CLI, args, { env: env2, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
1268
1340
|
} catch (err) {
|
|
1269
1341
|
deps.log("claude run could not be started", { ch, cli: CLI, error: message(err) });
|
|
1270
1342
|
rmSync(runDir, { recursive: true, force: true });
|
|
@@ -1463,7 +1535,7 @@ function createDispatcher(deps) {
|
|
|
1463
1535
|
void handleRpc(msg, socket, deps.handlers, deps.log);
|
|
1464
1536
|
break;
|
|
1465
1537
|
case "open": {
|
|
1466
|
-
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);
|
|
1467
1539
|
opened.catch((err) => {
|
|
1468
1540
|
deps.log(`${msg.kind}.open rejected unexpectedly`, { ch: msg.ch, error: err instanceof Error ? err.message : String(err) });
|
|
1469
1541
|
});
|
|
@@ -1475,6 +1547,7 @@ function createDispatcher(deps) {
|
|
|
1475
1547
|
case "close":
|
|
1476
1548
|
deps.pty.close(msg.ch);
|
|
1477
1549
|
deps.claude.close(msg.ch);
|
|
1550
|
+
deps.tcp.close(msg.ch);
|
|
1478
1551
|
break;
|
|
1479
1552
|
}
|
|
1480
1553
|
};
|
|
@@ -1566,13 +1639,13 @@ function createPtyManager(deps) {
|
|
|
1566
1639
|
try {
|
|
1567
1640
|
({ cols, rows } = clampSize(params));
|
|
1568
1641
|
const cwd = resolveCwd(params.cwd);
|
|
1569
|
-
const
|
|
1642
|
+
const env2 = {
|
|
1570
1643
|
...ptyEnv(agentEnv(), process.env.SHELL ?? "/bin/sh"),
|
|
1571
1644
|
TERMHUB_TAB_ID: params.session,
|
|
1572
1645
|
TERMHUB_SESSION: params.session
|
|
1573
1646
|
};
|
|
1574
1647
|
const spawn2 = await resolveSpawn();
|
|
1575
|
-
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 });
|
|
1576
1649
|
try {
|
|
1577
1650
|
proc = spawnTmux();
|
|
1578
1651
|
} catch (err) {
|
|
@@ -1665,6 +1738,141 @@ function createPtyManager(deps) {
|
|
|
1665
1738
|
};
|
|
1666
1739
|
}
|
|
1667
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
|
+
|
|
1668
1876
|
// src/rpc/ai.ts
|
|
1669
1877
|
async function credential(params) {
|
|
1670
1878
|
let prefix;
|
|
@@ -1720,6 +1928,33 @@ async function pasteFile(params) {
|
|
|
1720
1928
|
return { path: path12 };
|
|
1721
1929
|
}
|
|
1722
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
|
+
|
|
1723
1958
|
// src/rpc/tmux.ts
|
|
1724
1959
|
import { randomUUID } from "crypto";
|
|
1725
1960
|
var ENTER_PAUSE_MS = 300;
|
|
@@ -1729,7 +1964,7 @@ function processFailure2(r) {
|
|
|
1729
1964
|
if (r.error === "maxbuffer") return new RpcFailure("internal", "output too large");
|
|
1730
1965
|
return null;
|
|
1731
1966
|
}
|
|
1732
|
-
async function
|
|
1967
|
+
async function list3(_params) {
|
|
1733
1968
|
const r = await run(tmuxPath(), ["list-sessions", "-F", "#{session_name}"]);
|
|
1734
1969
|
const failure = processFailure2(r);
|
|
1735
1970
|
if (failure) throw failure;
|
|
@@ -2036,7 +2271,7 @@ async function status3() {
|
|
|
2036
2271
|
}
|
|
2037
2272
|
|
|
2038
2273
|
// src/version.ts
|
|
2039
|
-
var AGENT_VERSION = "0.
|
|
2274
|
+
var AGENT_VERSION = "0.5.0";
|
|
2040
2275
|
|
|
2041
2276
|
// src/rpc/update.ts
|
|
2042
2277
|
var PACKAGE = "@termhub/agent";
|
|
@@ -2100,9 +2335,52 @@ async function doUpdate(version, deps) {
|
|
|
2100
2335
|
}
|
|
2101
2336
|
var update = (params) => updateAgent(params);
|
|
2102
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
|
+
|
|
2103
2381
|
// src/rpc/index.ts
|
|
2104
2382
|
var handlers = {
|
|
2105
|
-
"tmux.list":
|
|
2383
|
+
"tmux.list": list3,
|
|
2106
2384
|
"tmux.kill": kill,
|
|
2107
2385
|
"tmux.capture": capture,
|
|
2108
2386
|
"tmux.ensure": ensure,
|
|
@@ -2116,7 +2394,14 @@ var handlers = {
|
|
|
2116
2394
|
"file.paste": pasteFile,
|
|
2117
2395
|
"hooks.install": install,
|
|
2118
2396
|
"hooks.uninstall": uninstall,
|
|
2119
|
-
"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
|
|
2120
2405
|
};
|
|
2121
2406
|
|
|
2122
2407
|
// src/run.ts
|
|
@@ -2126,6 +2411,9 @@ function detectOs(platform = process.platform) {
|
|
|
2126
2411
|
return null;
|
|
2127
2412
|
}
|
|
2128
2413
|
var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT];
|
|
2414
|
+
function capabilitiesFor(osName) {
|
|
2415
|
+
return osName === "macos" ? [...CAPABILITIES, CAPABILITY_SIM] : [...CAPABILITIES];
|
|
2416
|
+
}
|
|
2129
2417
|
async function buildHello(osName) {
|
|
2130
2418
|
let tools = [];
|
|
2131
2419
|
try {
|
|
@@ -2141,7 +2429,7 @@ async function buildHello(osName) {
|
|
|
2141
2429
|
hostname: os6.hostname(),
|
|
2142
2430
|
tmux: tools.includes("tmux"),
|
|
2143
2431
|
tools,
|
|
2144
|
-
capabilities:
|
|
2432
|
+
capabilities: capabilitiesFor(osName)
|
|
2145
2433
|
};
|
|
2146
2434
|
}
|
|
2147
2435
|
var REVOKED_MESSAGE = "Token inv\xE1lido ou revogado";
|
|
@@ -2159,7 +2447,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
|
|
|
2159
2447
|
{
|
|
2160
2448
|
url: config.url,
|
|
2161
2449
|
token: config.token,
|
|
2162
|
-
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 },
|
|
2163
2451
|
onServerMessage: () => {
|
|
2164
2452
|
},
|
|
2165
2453
|
onStream: () => {
|
|
@@ -2202,7 +2490,8 @@ async function runAgent(config, opts) {
|
|
|
2202
2490
|
else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
|
|
2203
2491
|
const pty = createPtyManager({ log: opts.log });
|
|
2204
2492
|
const claude = createClaudeManager({ log: opts.log });
|
|
2205
|
-
const
|
|
2493
|
+
const tcp = createTcpManager({ log: opts.log });
|
|
2494
|
+
const dispatch = createDispatcher({ handlers, pty, claude, tcp, log: opts.log });
|
|
2206
2495
|
const healHooks = () => {
|
|
2207
2496
|
heal().then((dirs) => {
|
|
2208
2497
|
if (dirs.length) opts.log("monitor hooks repaired", { dirs: dirs.length });
|
|
@@ -2219,12 +2508,13 @@ async function runAgent(config, opts) {
|
|
|
2219
2508
|
// A frame belongs to whichever manager holds that channel: the claude one says so, and
|
|
2220
2509
|
// anything it does not own is a terminal's.
|
|
2221
2510
|
onStream: (ch, data) => {
|
|
2222
|
-
if (!claude.write(ch, data)) pty.write(ch, data);
|
|
2511
|
+
if (!claude.write(ch, data) && !tcp.write(ch, data)) pty.write(ch, data);
|
|
2223
2512
|
},
|
|
2224
2513
|
onConnect: healHooks,
|
|
2225
2514
|
onDisconnect: () => {
|
|
2226
2515
|
pty.closeAll();
|
|
2227
2516
|
claude.closeAll();
|
|
2517
|
+
tcp.closeAll();
|
|
2228
2518
|
},
|
|
2229
2519
|
log: opts.log
|
|
2230
2520
|
},
|
package/package.json
CHANGED