@termhub/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +53 -0
  2. package/dist/cli.js +1489 -0
  3. package/package.json +48 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1489 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+
6
+ // src/commands/connect.ts
7
+ import readline from "readline";
8
+
9
+ // src/client.ts
10
+ import { WebSocket } from "ws";
11
+
12
+ // ../../packages/agent-protocol/dist/frames.js
13
+ var CONTROL_CHANNEL = 0;
14
+ var HEADER_BYTES = 4;
15
+ var MAX_FRAME = 1024 * 1024;
16
+ var MAX_PASTE_FRAME = 20 * 1024 * 1024 + 1024;
17
+ function encodeFrame(ch, payload) {
18
+ if (!Number.isInteger(ch) || ch < 0 || ch > 4294967295)
19
+ throw new Error(`invalid channel ${ch}`);
20
+ const body = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
21
+ const out = Buffer.allocUnsafe(HEADER_BYTES + body.length);
22
+ out.writeUInt32BE(ch, 0);
23
+ body.copy(out, HEADER_BYTES);
24
+ return out;
25
+ }
26
+ function decodeFrame(buf) {
27
+ if (buf.length < HEADER_BYTES)
28
+ throw new Error("truncated frame");
29
+ return { ch: buf.readUInt32BE(0), payload: buf.subarray(HEADER_BYTES) };
30
+ }
31
+
32
+ // ../../packages/agent-protocol/dist/messages.js
33
+ import { z as z2 } from "zod";
34
+
35
+ // ../../packages/agent-protocol/dist/rpc.js
36
+ import { z } from "zod";
37
+ var SESSION_RE = /^[A-Za-z0-9_-]+$/;
38
+ var sessionName = z.string().min(1).max(128).regex(SESSION_RE);
39
+ var machinePath = z.string().min(1).max(4096).refine((p) => (p === "~" || p.startsWith("~/") || p.startsWith("/")) && !/[\0\n\r]/.test(p), "invalid path");
40
+ var pasteName = z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/);
41
+ var aiProvider = z.enum(["claude", "chatgpt", "gemini", "antigravity"]);
42
+ var rpcErrorSchema = z.object({
43
+ code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal"]),
44
+ message: z.string().max(2e3),
45
+ path: z.string().max(4096).optional()
46
+ });
47
+ var DEFAULT_TIMEOUT = 8e3;
48
+ var def = (params, result, timeoutMs = DEFAULT_TIMEOUT) => ({ params, result, timeoutMs });
49
+ var RPC = {
50
+ "tmux.list": def(z.object({}), z.object({ sessions: z.array(sessionName) })),
51
+ "tmux.kill": def(z.object({ session: sessionName }), z.object({ killed: z.boolean() })),
52
+ "tmux.capture": def(z.object({ session: sessionName, lines: z.number().int().min(1).max(5e3) }), z.object({ text: z.string() })),
53
+ "tools.detect": def(z.object({}), z.object({ os: z.string().nullable(), tools: z.array(z.string().max(32)) })),
54
+ "hw.probe": def(z.object({}), z.object({ stdout: z.string() }), 15e3),
55
+ "fs.list": def(z.object({ path: machinePath }), z.object({ stdout: z.string() })),
56
+ "fs.mkdir": def(z.object({
57
+ parent: machinePath,
58
+ name: z.string().min(1).max(255).regex(/^[^/\\\0\n\r]+$/),
59
+ /** `mkdir -p`: create missing parents too (ensureDirectory parity with the ssh/local branch). Default: leaf only. */
60
+ recursive: z.boolean().optional()
61
+ }), z.object({ stdout: z.string() })),
62
+ "ai.credential": def(z.object({ provider: aiProvider, config_dir: machinePath.nullable() }), z.object({ stdout: z.string() }), 1e4),
63
+ "file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4)
64
+ };
65
+ var RPC_METHODS = Object.keys(RPC);
66
+ var rpcMethod = z.enum(RPC_METHODS);
67
+
68
+ // ../../packages/agent-protocol/dist/messages.js
69
+ var PROTOCOL_VERSION = 1;
70
+ var CLOSE = { UNAUTHORIZED: 4401, CONFLICT: 4409, VIOLATION: 1008 };
71
+ var channel = z2.number().int().min(1).max(4294967295);
72
+ var rpcId = z2.string().min(1).max(64);
73
+ var helloMessage = z2.object({
74
+ type: z2.literal("hello"),
75
+ protocol: z2.number().int().min(1),
76
+ agent_version: z2.string().max(32),
77
+ os: z2.enum(["macos", "linux"]),
78
+ arch: z2.string().max(16),
79
+ hostname: z2.string().max(255),
80
+ tmux: z2.boolean(),
81
+ tools: z2.array(z2.string().max(32)).max(64),
82
+ /** `status`/`doctor` reachability check: the server validates the token and protocol as
83
+ * usual but does not attach — it answers by closing 1000 `probe-ok`, so a probe never
84
+ * replaces the machine's live session. */
85
+ probe: z2.boolean().optional()
86
+ });
87
+ var agentMessage = z2.discriminatedUnion("type", [
88
+ helloMessage,
89
+ z2.object({ type: z2.literal("rpc_result"), id: rpcId, ok: z2.boolean(), result: z2.unknown().optional(), error: rpcErrorSchema.optional() }),
90
+ z2.object({ type: z2.literal("opened"), ch: channel }),
91
+ z2.object({ type: z2.literal("open_error"), ch: channel, error: rpcErrorSchema }),
92
+ z2.object({ type: z2.literal("closed"), ch: channel, code: z2.number().int().nullable() })
93
+ ]);
94
+ var ptyOpenParams = z2.object({ session: sessionName, cwd: machinePath, cols: z2.number().int().min(2).max(500), rows: z2.number().int().min(2).max(200) });
95
+ var serverMessage = z2.discriminatedUnion("type", [
96
+ z2.object({ type: z2.literal("rpc"), id: rpcId, method: rpcMethod, params: z2.unknown() }),
97
+ z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams }),
98
+ z2.object({ type: z2.literal("resize"), ch: channel, cols: z2.number().int().min(2).max(500), rows: z2.number().int().min(2).max(200) }),
99
+ z2.object({ type: z2.literal("close"), ch: channel })
100
+ ]);
101
+
102
+ // src/client.ts
103
+ var RevokedError = class extends Error {
104
+ };
105
+ var ProtocolMismatchError = class extends Error {
106
+ };
107
+ var UpgradeRejectedError = class extends Error {
108
+ constructor(status4) {
109
+ super(`upgrade rejected: HTTP ${status4}`);
110
+ this.status = status4;
111
+ }
112
+ status;
113
+ };
114
+ var DEFAULT_BACKOFF = { minMs: 1e3, maxMs: 3e4 };
115
+ var DEFAULT_PING_INTERVAL_MS = 2e4;
116
+ var DEFAULT_MAX_UNAUTHORIZED = 3;
117
+ var SESSION_OK_MS = 5e3;
118
+ function deriveWsUrl(base) {
119
+ const u = new URL(base);
120
+ u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
121
+ u.pathname = `${u.pathname.replace(/\/+$/, "")}/agent/ws`;
122
+ u.search = "";
123
+ u.hash = "";
124
+ return u.toString();
125
+ }
126
+ function toBuffer(data) {
127
+ if (Buffer.isBuffer(data)) return data;
128
+ if (Array.isArray(data)) return Buffer.concat(data);
129
+ return Buffer.from(data);
130
+ }
131
+ function connectOnce(opts, signal) {
132
+ return new Promise((resolve, reject) => {
133
+ if (signal?.aborted) {
134
+ reject(new Error("aborted"));
135
+ return;
136
+ }
137
+ const wsUrl = deriveWsUrl(opts.url);
138
+ const ws = new WebSocket(wsUrl, { headers: { Authorization: `Bearer ${opts.token}` } });
139
+ let opened = false;
140
+ let socket;
141
+ const onAbort = () => {
142
+ if (opened) ws.close(1e3, "aborted");
143
+ else ws.terminate();
144
+ };
145
+ signal?.addEventListener("abort", onAbort, { once: true });
146
+ const stopWatchingAbort = () => signal?.removeEventListener("abort", onAbort);
147
+ let resolveClosed;
148
+ const closed = new Promise((res) => {
149
+ resolveClosed = res;
150
+ });
151
+ let liveness;
152
+ let pongSeen = true;
153
+ ws.on("pong", () => {
154
+ pongSeen = true;
155
+ });
156
+ ws.on("close", (code, reasonBuf) => {
157
+ stopWatchingAbort();
158
+ if (liveness) clearInterval(liveness);
159
+ const info = { code, reason: reasonBuf.toString() };
160
+ resolveClosed(info);
161
+ if (!opened) reject(new Error(`connection closed before open (code ${code})`));
162
+ });
163
+ ws.on("unexpected-response", (_req, res) => {
164
+ stopWatchingAbort();
165
+ const statusCode = res.statusCode ?? 0;
166
+ res.resume();
167
+ ws.terminate();
168
+ reject(new UpgradeRejectedError(statusCode));
169
+ });
170
+ ws.on("error", (err) => {
171
+ if (!opened) {
172
+ stopWatchingAbort();
173
+ reject(err);
174
+ } else {
175
+ opts.log("agent socket error", { error: err.message });
176
+ }
177
+ });
178
+ ws.on("open", () => {
179
+ opened = true;
180
+ liveness = setInterval(() => {
181
+ if (!pongSeen) {
182
+ opts.log("agent liveness ping unanswered, terminating");
183
+ ws.terminate();
184
+ return;
185
+ }
186
+ pongSeen = false;
187
+ ws.ping();
188
+ }, opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS);
189
+ socket = {
190
+ sendControl: (msg) => ws.send(encodeFrame(CONTROL_CHANNEL, JSON.stringify(msg))),
191
+ sendStream: (ch, data) => ws.send(encodeFrame(ch, data))
192
+ };
193
+ const hello = { type: "hello", protocol: PROTOCOL_VERSION, ...opts.hello };
194
+ socket.sendControl(hello);
195
+ resolve({ socket, closed });
196
+ });
197
+ ws.on("message", (data) => {
198
+ if (!socket) return;
199
+ let frame;
200
+ try {
201
+ frame = decodeFrame(toBuffer(data));
202
+ } catch (err) {
203
+ opts.log("dropped malformed frame", { error: err.message });
204
+ return;
205
+ }
206
+ if (frame.ch === CONTROL_CHANNEL) {
207
+ let parsed;
208
+ try {
209
+ parsed = JSON.parse(frame.payload.toString("utf8"));
210
+ } catch {
211
+ opts.log("dropped non-JSON control message");
212
+ return;
213
+ }
214
+ const result = serverMessage.safeParse(parsed);
215
+ if (!result.success) {
216
+ opts.log("dropped invalid server message", { issues: result.error.issues.length });
217
+ return;
218
+ }
219
+ opts.onServerMessage(result.data, socket);
220
+ } else {
221
+ opts.onStream(frame.ch, frame.payload);
222
+ }
223
+ });
224
+ });
225
+ }
226
+ function sleep(ms, signal) {
227
+ return new Promise((resolve) => {
228
+ if (signal?.aborted) {
229
+ resolve();
230
+ return;
231
+ }
232
+ const timer = setTimeout(() => {
233
+ signal?.removeEventListener("abort", onAbort);
234
+ resolve();
235
+ }, ms);
236
+ function onAbort() {
237
+ clearTimeout(timer);
238
+ resolve();
239
+ }
240
+ signal?.addEventListener("abort", onAbort, { once: true });
241
+ });
242
+ }
243
+ function nextBackoff(prevMs, min, max, rand = Math.random) {
244
+ const base = Math.min(prevMs * 2, max);
245
+ const jittered = base * (0.8 + 0.4 * rand());
246
+ return Math.min(max, Math.max(min, jittered));
247
+ }
248
+ async function runForever(opts, signal) {
249
+ const min = opts.backoff?.minMs ?? DEFAULT_BACKOFF.minMs;
250
+ const max = opts.backoff?.maxMs ?? DEFAULT_BACKOFF.maxMs;
251
+ const maxUnauthorized = opts.maxUnauthorized ?? DEFAULT_MAX_UNAUTHORIZED;
252
+ let unauthorized = 0;
253
+ let backoff = min;
254
+ while (!signal?.aborted) {
255
+ let sessionOk = false;
256
+ let closeInfo;
257
+ let upgradeRejectedStatus;
258
+ const innerOpts = {
259
+ ...opts,
260
+ onServerMessage: (msg, conn) => {
261
+ sessionOk = true;
262
+ opts.onServerMessage(msg, conn);
263
+ }
264
+ };
265
+ try {
266
+ const { closed } = await connectOnce(innerOpts, signal);
267
+ const startedAt = Date.now();
268
+ closeInfo = await closed;
269
+ if (Date.now() - startedAt > SESSION_OK_MS) sessionOk = true;
270
+ opts.log("agent connection closed", { code: closeInfo.code, reason: closeInfo.reason });
271
+ } catch (err) {
272
+ if (err instanceof UpgradeRejectedError) {
273
+ upgradeRejectedStatus = err.status;
274
+ opts.log("agent upgrade rejected", { status: err.status });
275
+ } else {
276
+ opts.log("agent connect failed", { error: err.message });
277
+ }
278
+ }
279
+ opts.onDisconnect?.();
280
+ if (closeInfo?.code === CLOSE.UNAUTHORIZED || upgradeRejectedStatus === 401) {
281
+ unauthorized += 1;
282
+ opts.log("agent unauthorized", { attempt: unauthorized, maxUnauthorized });
283
+ if (unauthorized >= maxUnauthorized) {
284
+ throw new RevokedError(`token rejected ${unauthorized} times in a row`);
285
+ }
286
+ } else if (closeInfo?.code === CLOSE.CONFLICT && closeInfo.reason === "protocol") {
287
+ throw new ProtocolMismatchError("server rejected the agent protocol version");
288
+ } else if (sessionOk) {
289
+ unauthorized = 0;
290
+ backoff = min;
291
+ }
292
+ if (signal?.aborted) return;
293
+ const wait = nextBackoff(backoff, min, max);
294
+ opts.log("reconnecting", { inMs: Math.round(wait) });
295
+ await sleep(wait, signal);
296
+ backoff = wait;
297
+ }
298
+ }
299
+
300
+ // src/config.ts
301
+ import fs from "fs";
302
+ import os from "os";
303
+ import path from "path";
304
+ import { z as z3 } from "zod";
305
+ var CONFIG_FILE = "config.json";
306
+ var agentConfigSchema = z3.object({
307
+ url: z3.string().min(1),
308
+ token: z3.string().min(1),
309
+ // The server does not hand the agent its machine id/name in v1 (see task-13); `connect`
310
+ // stores '' for both until a later protocol version fills them in, so these can't require
311
+ // min(1) like the other fields.
312
+ machine_id: z3.string(),
313
+ machine_name: z3.string(),
314
+ created_at: z3.string().min(1)
315
+ });
316
+ function agentHome() {
317
+ const override = process.env.TERMHUB_AGENT_HOME?.trim();
318
+ return override && override.length > 0 ? override : path.join(os.homedir(), ".termhub");
319
+ }
320
+ function configPath() {
321
+ return path.join(agentHome(), CONFIG_FILE);
322
+ }
323
+ function readConfig() {
324
+ let raw;
325
+ try {
326
+ raw = fs.readFileSync(configPath(), "utf8");
327
+ } catch {
328
+ return null;
329
+ }
330
+ let parsed;
331
+ try {
332
+ parsed = JSON.parse(raw);
333
+ } catch {
334
+ return null;
335
+ }
336
+ const result = agentConfigSchema.safeParse(parsed);
337
+ return result.success ? result.data : null;
338
+ }
339
+ function writeConfig(config) {
340
+ const home = agentHome();
341
+ fs.mkdirSync(home, { recursive: true, mode: 448 });
342
+ fs.chmodSync(home, 448);
343
+ const dest = configPath();
344
+ const tmp = path.join(home, `.${CONFIG_FILE}.${process.pid}.${Date.now()}.tmp`);
345
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
346
+ fs.chmodSync(tmp, 384);
347
+ fs.renameSync(tmp, dest);
348
+ }
349
+ function deleteConfig() {
350
+ try {
351
+ fs.unlinkSync(configPath());
352
+ } catch (err) {
353
+ if (err.code !== "ENOENT") throw err;
354
+ }
355
+ }
356
+
357
+ // src/run.ts
358
+ import os4 from "os";
359
+
360
+ // src/exec.ts
361
+ import { execFile } from "child_process";
362
+ import os2 from "os";
363
+
364
+ // ../../packages/machine-ops/dist/shell.js
365
+ function shellQuote(s) {
366
+ return `'${s.replace(/'/g, `'\\''`)}'`;
367
+ }
368
+ var REMOTE_PATH_PREFIX = 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"; ';
369
+
370
+ // ../../packages/machine-ops/dist/detect.js
371
+ var DETECT_TOOLS = ["tmux", "claude", "gh", "git", "node", "pnpm", "xcodebuild", "docker", "adb", "python3"];
372
+ var WDA_RUNNER_APP = "$HOME/.termhub/WebDriverAgent/DerivedData/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app";
373
+ var DETECT_SCRIPT = `echo OS:$(uname -s); for t in ${DETECT_TOOLS.join(" ")}; do command -v $t >/dev/null 2>&1 && echo CAP:$t; done; [ -d "${WDA_RUNNER_APP}" ] && echo CAP:wda; exit 0`;
374
+ function parseDetect(stdout) {
375
+ let os8 = null;
376
+ const caps = [];
377
+ for (const line of stdout.split("\n")) {
378
+ if (line.startsWith("OS:")) {
379
+ const raw = line.slice(3).trim().toLowerCase();
380
+ os8 = raw === "darwin" ? "macos" : raw || null;
381
+ } else if (line.startsWith("CAP:"))
382
+ caps.push(line.slice(4).trim());
383
+ }
384
+ return { os: os8, capabilities: caps };
385
+ }
386
+
387
+ // ../../packages/machine-ops/dist/hardware-script.js
388
+ var LINUX = [
389
+ `echo "OS:linux"`,
390
+ `echo "HOST:$(hostname 2>/dev/null)"`,
391
+ `echo "CPUMODEL:$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | sed 's/^ *//')"`,
392
+ `echo "NCPU:$(nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo)"`,
393
+ `echo "UPTIME:$(cut -d' ' -f1 /proc/uptime)"`,
394
+ `echo "LOAD:$(cut -d' ' -f1-3 /proc/loadavg)"`,
395
+ `echo "STAT1:$(head -1 /proc/stat)"`,
396
+ `sleep 1`,
397
+ `echo "STAT2:$(head -1 /proc/stat)"`,
398
+ `grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree):' /proc/meminfo | sed 's/^/MEM:/'`,
399
+ `df -Pk 2>/dev/null | tail -n +2 | awk '{ for (i = 6; i <= NF; i++) if ($i ~ /^\\//) { m = $i; for (j = i + 1; j <= NF; j++) m = m " " $j; printf "DISK:%s\\t%s\\t%s\\t%s\\t%s\\n", $1, $2, $3, $4, m; break } }'`,
400
+ `for z in /sys/class/thermal/thermal_zone*; do [ -f "$z/temp" ] && printf 'TEMP:%s\\t%s\\n' "$(cat "$z/type" 2>/dev/null)" "$(cat "$z/temp" 2>/dev/null)"; done`,
401
+ `for h in /sys/class/hwmon/hwmon*; do n=$(cat "$h/name" 2>/dev/null); for t in "$h"/temp*_input; do [ -f "$t" ] && printf 'TEMP:%s\\t%s\\n' "$n $(cat "\${t%_input}_label" 2>/dev/null)" "$(cat "$t" 2>/dev/null)"; done; done`,
402
+ `command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi --query-gpu=name,utilization.gpu,memory.used,memory.total,temperature.gpu --format=csv,noheader,nounits 2>/dev/null | sed 's/^/GPU:/'`,
403
+ `ps -eo pcpu,pmem,comm --sort=-pcpu 2>/dev/null | tail -n +2 | head -7 | sed 's/^ *//;s/^/PROC:/'`,
404
+ `exit 0`
405
+ ].join("; ");
406
+ var DARWIN = [
407
+ `echo "OS:macos"`,
408
+ `echo "HOST:$(hostname 2>/dev/null)"`,
409
+ `echo "CPUMODEL:$(sysctl -n machdep.cpu.brand_string 2>/dev/null)"`,
410
+ `echo "NCPU:$(sysctl -n hw.ncpu)"`,
411
+ `echo "BOOT:$(sysctl -n kern.boottime | sed 's/^{ sec = \\([0-9]*\\),.*/\\1/')"`,
412
+ `echo "NOW:$(date +%s)"`,
413
+ `echo "LOAD:$(sysctl -n vm.loadavg | tr -d '{}')"`,
414
+ `echo "MEMSIZE:$(sysctl -n hw.memsize)"`,
415
+ `vm_stat | sed 's/^/VMSTAT:/'`,
416
+ `echo "SWAPUSAGE:$(sysctl -n vm.swapusage)"`,
417
+ `top -l 2 -s 1 -n 0 2>/dev/null | grep 'CPU usage' | tail -1 | sed 's/^/CPUUSAGE:/'`,
418
+ `df -Pk 2>/dev/null | tail -n +2 | awk '{ for (i = 6; i <= NF; i++) if ($i ~ /^\\//) { m = $i; for (j = i + 1; j <= NF; j++) m = m " " $j; printf "DISK:%s\\t%s\\t%s\\t%s\\t%s\\n", $1, $2, $3, $4, m; break } }'`,
419
+ `ps -A -o pcpu,pmem,comm -r 2>/dev/null | tail -n +2 | head -7 | sed 's/^ *//;s/^/PROC:/'`,
420
+ `exit 0`
421
+ ].join("; ");
422
+ var HARDWARE_SCRIPT = `if [ "$(uname -s)" = Darwin ]; then ${DARWIN}; else ${LINUX}; fi`;
423
+
424
+ // ../../packages/machine-ops/dist/fs-script.js
425
+ var EXPAND_HOME = `case "$P" in "~") P=$HOME;; "~/"*) P="$HOME/\${P#\\~/}";; esac`;
426
+ function buildFsListScript(quotedPath) {
427
+ return [
428
+ `P=${quotedPath}`,
429
+ `case "$P" in ""|"~") P=$HOME;; "~/"*) P="$HOME/\${P#\\~/}";; esac`,
430
+ `echo "HOME:$HOME"`,
431
+ // discos: fonte, tamanho, livre e mount point (mount pode ter espaços: fica no fim da linha)
432
+ `df -Pk 2>/dev/null | tail -n +2 | while IFS= read -r line; do set -- $line; src=$1; size=$2; avail=$4; shift 5; [ -d "$*" ] && printf 'MNT:%s\\t%s\\t%s\\t%s\\n' "$src" "$size" "$avail" "$*"; done`,
433
+ `if [ ! -e "$P" ]; then echo "ERR:notfound"; exit 0; fi`,
434
+ // notdir before eperm: a readable regular file fails `-x`, and "sem acesso" would be the
435
+ // wrong diagnosis for it — "não é um diretório" is.
436
+ `if [ ! -d "$P" ]; then echo "ERR:notdir"; exit 0; fi`,
437
+ `if [ ! -r "$P" ] || [ ! -x "$P" ]; then echo "ERR:eperm"; exit 0; fi`,
438
+ `cd -- "$P" 2>/dev/null || { echo "ERR:denied"; exit 0; }`,
439
+ `echo "PWD:$(pwd)"`,
440
+ `ls -1Ap 2>/dev/null | grep '/$' | sed 's#/$##' | while IFS= read -r n; do echo "DIR:$n"; done`,
441
+ `exit 0`
442
+ ].join("; ");
443
+ }
444
+ function buildMkdirScript(quotedParent, quotedName, opts = {}) {
445
+ if (opts.recursive) {
446
+ return [
447
+ `P=${quotedParent}`,
448
+ EXPAND_HOME,
449
+ `N=${quotedName}`,
450
+ `if [ -e "$P/$N" ]; then echo "ERR:exists"; exit 0; fi`,
451
+ `mkdir -p -- "$P/$N" 2>/dev/null || { echo "ERR:denied"; exit 0; }`,
452
+ `cd -- "$P/$N" && echo "PWD:$(pwd)"`,
453
+ `exit 0`
454
+ ].join("; ");
455
+ }
456
+ return [
457
+ `P=${quotedParent}`,
458
+ EXPAND_HOME,
459
+ `cd -- "$P" 2>/dev/null || { echo "ERR:parent"; exit 0; }`,
460
+ `N=${quotedName}`,
461
+ `if [ -e "$N" ]; then echo "ERR:exists"; exit 0; fi`,
462
+ `mkdir -- "$N" 2>/dev/null || { echo "ERR:denied"; exit 0; }`,
463
+ `cd -- "$N" && echo "PWD:$(pwd)"`,
464
+ `exit 0`
465
+ ].join("; ");
466
+ }
467
+
468
+ // ../../packages/machine-ops/dist/paste.js
469
+ import { randomBytes } from "crypto";
470
+ var PASTE_MAX_BYTES = 20 * 1024 * 1024;
471
+ var PASTE_DIR = ".cache/termhub/paste";
472
+ var PASTE_KEEP_DAYS = 7;
473
+ function buildPasteScript(name) {
474
+ return [
475
+ `d="$HOME/${PASTE_DIR}"`,
476
+ `mkdir -p "$d" || exit 1`,
477
+ `cat > "$d/${name}" || exit 1`,
478
+ `find "$d" -name 'paste-*' -type f -mtime +${PASTE_KEEP_DAYS} -delete 2>/dev/null`,
479
+ `printf '%s\\n' "$d/${name}"`
480
+ ].join("; ");
481
+ }
482
+
483
+ // ../../packages/machine-ops/dist/ai-credentials.js
484
+ function credentialScript(provider) {
485
+ switch (provider) {
486
+ case "claude":
487
+ return [
488
+ `if [ -f "$D/.credentials.json" ]; then cat "$D/.credentials.json"`,
489
+ `elif [ "$(uname -s)" = Darwin ]; then security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || true`,
490
+ `fi`
491
+ ].join("; ");
492
+ case "chatgpt":
493
+ return `if [ -f "$D/auth.json" ]; then cat "$D/auth.json"; fi`;
494
+ case "gemini":
495
+ return `if [ -f "$D/oauth_creds.json" ]; then cat "$D/oauth_creds.json"; fi`;
496
+ case "antigravity":
497
+ return `if [ -f "$D/antigravity-cli/antigravity-oauth-token" ]; then cat "$D/antigravity-cli/antigravity-oauth-token"; fi`;
498
+ }
499
+ }
500
+ var DEFAULT_CONFIG_DIRS = { claude: ".claude", chatgpt: ".codex", gemini: ".gemini", antigravity: ".gemini" };
501
+ function configDirPrefix(configDir, defaultDir) {
502
+ const raw = (configDir ?? "").trim();
503
+ if (!raw)
504
+ return `D="$HOME/${defaultDir}"`;
505
+ if (raw.includes("\0") || raw.includes("\n"))
506
+ throw new Error("Invalid config dir");
507
+ return `P=${shellQuote(raw)}; case "$P" in "~") P=$HOME;; "~/"*) P="$HOME/\${P#\\~/}";; esac; D="$P"`;
508
+ }
509
+
510
+ // ../../packages/machine-ops/dist/pty.js
511
+ var UTF8_LOCALE = "en_US.UTF-8";
512
+ function clampSize(size) {
513
+ const cols = Math.min(Math.max(Math.floor(size.cols ?? 80), 2), 500);
514
+ const rows = Math.min(Math.max(Math.floor(size.rows ?? 24), 2), 200);
515
+ return { cols, rows };
516
+ }
517
+ function ptyEnv(base, shell) {
518
+ return {
519
+ ...base,
520
+ TERM: "xterm-256color",
521
+ COLORTERM: "truecolor",
522
+ LANG: /utf-?8/i.test(base.LANG ?? "") ? base.LANG : UTF8_LOCALE,
523
+ LC_ALL: /utf-?8/i.test(base.LC_ALL ?? "") ? base.LC_ALL : UTF8_LOCALE,
524
+ SHELL: shell,
525
+ TERMHUB: "1"
526
+ };
527
+ }
528
+
529
+ // src/exec.ts
530
+ var DEFAULT_TIMEOUT_MS = 8e3;
531
+ var RpcFailure = class extends Error {
532
+ constructor(code, message, path8) {
533
+ super(message);
534
+ this.code = code;
535
+ this.path = path8;
536
+ }
537
+ code;
538
+ path;
539
+ };
540
+ function pathPrefixDirs() {
541
+ const m = REMOTE_PATH_PREFIX.match(/PATH="([^"]+):\$PATH"/);
542
+ if (!m) throw new Error("unexpected REMOTE_PATH_PREFIX format");
543
+ return m[1];
544
+ }
545
+ function agentEnv(base = process.env) {
546
+ const prefix = pathPrefixDirs().replace("$HOME", os2.homedir());
547
+ return { ...base, PATH: `${prefix}:${base.PATH ?? ""}` };
548
+ }
549
+ function tmuxPath() {
550
+ return process.env.TMUX_PATH || "tmux";
551
+ }
552
+ function run(file, args, opts = {}) {
553
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env = agentEnv() } = opts;
554
+ return new Promise((resolve) => {
555
+ const child = execFile(
556
+ file,
557
+ args,
558
+ { timeout: timeoutMs, env, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
559
+ (err, stdout, stderr) => {
560
+ const e = err;
561
+ resolve({
562
+ code: e ? typeof e.code === "number" ? e.code : null : 0,
563
+ stdout: String(stdout ?? ""),
564
+ stderr: String(stderr ?? ""),
565
+ timedOut: !!e?.killed || e?.signal === "SIGTERM",
566
+ error: e?.code === "ENOENT" ? "enoent" : e?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? "maxbuffer" : void 0
567
+ });
568
+ }
569
+ );
570
+ child.stdin?.on("error", () => {
571
+ });
572
+ if (input !== void 0) child.stdin?.end(input);
573
+ else child.stdin?.end();
574
+ });
575
+ }
576
+ function sh(script, opts = {}) {
577
+ return run("/bin/sh", ["-c", script], opts);
578
+ }
579
+
580
+ // src/dispatch.ts
581
+ async function handleRpc(msg, socket, handlers2, log2) {
582
+ const method = msg.method;
583
+ const def2 = RPC[method];
584
+ const handler = handlers2[method];
585
+ if (!def2 || !handler) {
586
+ socket.sendControl({ type: "rpc_result", id: msg.id, ok: false, error: { code: "invalid", message: "unknown method" } });
587
+ return;
588
+ }
589
+ const parsedParams = def2.params.safeParse(msg.params);
590
+ if (!parsedParams.success) {
591
+ socket.sendControl({ type: "rpc_result", id: msg.id, ok: false, error: { code: "invalid", message: "invalid params" } });
592
+ return;
593
+ }
594
+ let result;
595
+ try {
596
+ result = await handler(parsedParams.data);
597
+ } catch (err) {
598
+ if (err instanceof RpcFailure) {
599
+ socket.sendControl({ type: "rpc_result", id: msg.id, ok: false, error: { code: err.code, message: err.message, path: err.path } });
600
+ } else {
601
+ log2("rpc handler failed", { method, error: err instanceof Error ? err.stack ?? err.message : String(err) });
602
+ socket.sendControl({ type: "rpc_result", id: msg.id, ok: false, error: { code: "internal", message: "internal error" } });
603
+ }
604
+ return;
605
+ }
606
+ const parsedResult = def2.result.safeParse(result);
607
+ if (!parsedResult.success) {
608
+ log2("rpc handler returned an invalid result", { method, issues: parsedResult.error.issues.length });
609
+ socket.sendControl({ type: "rpc_result", id: msg.id, ok: false, error: { code: "internal", message: "internal error" } });
610
+ return;
611
+ }
612
+ socket.sendControl({ type: "rpc_result", id: msg.id, ok: true, result: parsedResult.data });
613
+ }
614
+ function createDispatcher(deps) {
615
+ return (msg, socket) => {
616
+ switch (msg.type) {
617
+ case "rpc":
618
+ void handleRpc(msg, socket, deps.handlers, deps.log);
619
+ break;
620
+ case "open":
621
+ deps.pty.open(msg.ch, msg.params, socket).catch((err) => {
622
+ deps.log("pty.open rejected unexpectedly", { ch: msg.ch, error: err instanceof Error ? err.message : String(err) });
623
+ });
624
+ break;
625
+ case "resize":
626
+ deps.pty.resize(msg.ch, msg.cols, msg.rows);
627
+ break;
628
+ case "close":
629
+ deps.pty.close(msg.ch);
630
+ break;
631
+ }
632
+ };
633
+ }
634
+
635
+ // src/pty.ts
636
+ import fs2 from "fs";
637
+ import path2 from "path";
638
+ function expandHome(rawCwd, home) {
639
+ if (rawCwd === "~") return home;
640
+ if (rawCwd.startsWith("~/")) return path2.join(home, rawCwd.slice(2));
641
+ return rawCwd;
642
+ }
643
+ function existsDir(p) {
644
+ try {
645
+ return fs2.statSync(p).isDirectory();
646
+ } catch {
647
+ return false;
648
+ }
649
+ }
650
+ function resolveCwd(rawCwd) {
651
+ const home = process.env.HOME || "/";
652
+ const expanded = expandHome(rawCwd, home);
653
+ return existsDir(expanded) ? expanded : home;
654
+ }
655
+ function isEnoent(err) {
656
+ const e = err;
657
+ if (e?.code === "ENOENT") return true;
658
+ const message = e instanceof Error ? e.message : String(err);
659
+ return /ENOENT/.test(message);
660
+ }
661
+ function createPtyManager(deps) {
662
+ const procs = /* @__PURE__ */ new Map();
663
+ const tmux = deps.tmuxPath ?? tmuxPath();
664
+ let spawnFn = deps.spawn;
665
+ async function resolveSpawn() {
666
+ if (!spawnFn) {
667
+ const nodePty = await import("node-pty");
668
+ spawnFn = nodePty.spawn;
669
+ }
670
+ return spawnFn;
671
+ }
672
+ return {
673
+ async open(ch, params, socket) {
674
+ if (procs.has(ch)) {
675
+ socket.sendControl({ type: "open_error", ch, error: { code: "invalid", message: "channel in use" } });
676
+ return;
677
+ }
678
+ let proc;
679
+ let cols;
680
+ let rows;
681
+ try {
682
+ ({ cols, rows } = clampSize(params));
683
+ const cwd = resolveCwd(params.cwd);
684
+ const env = {
685
+ ...ptyEnv(agentEnv(), process.env.SHELL ?? "/bin/sh"),
686
+ TERMHUB_TAB_ID: params.session,
687
+ TERMHUB_SESSION: params.session
688
+ };
689
+ const spawn = await resolveSpawn();
690
+ proc = spawn(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
691
+ } catch (err) {
692
+ socket.sendControl({
693
+ type: "open_error",
694
+ ch,
695
+ error: isEnoent(err) ? { code: "no_tmux", message: "tmux not found" } : { code: "internal", message: "failed to start pty" }
696
+ });
697
+ return;
698
+ }
699
+ let closedSent = false;
700
+ const entry = {
701
+ proc,
702
+ closed: false,
703
+ sendClosed: (code) => {
704
+ if (closedSent) return;
705
+ closedSent = true;
706
+ try {
707
+ socket.sendControl({ type: "closed", ch, code });
708
+ } catch (err) {
709
+ deps.log("pty closed send failed", { ch, error: err instanceof Error ? err.message : String(err) });
710
+ }
711
+ }
712
+ };
713
+ procs.set(ch, entry);
714
+ deps.log("pty opened", { ch, session: params.session, cols, rows });
715
+ proc.onData((data) => {
716
+ if (entry.closed) return;
717
+ try {
718
+ socket.sendStream(ch, Buffer.from(data, "utf8"));
719
+ } catch (err) {
720
+ deps.log("pty stream send failed", { ch, error: err instanceof Error ? err.message : String(err) });
721
+ }
722
+ });
723
+ proc.onExit(({ exitCode }) => {
724
+ if (procs.get(ch) === entry) procs.delete(ch);
725
+ const code = exitCode ?? null;
726
+ deps.log("pty exited", { ch, code });
727
+ entry.sendClosed(code);
728
+ });
729
+ try {
730
+ socket.sendControl({ type: "opened", ch });
731
+ } catch (err) {
732
+ deps.log("pty opened send failed", { ch, error: err instanceof Error ? err.message : String(err) });
733
+ }
734
+ },
735
+ write(ch, data) {
736
+ const entry = procs.get(ch);
737
+ if (!entry) return;
738
+ entry.proc.write(data.toString("utf8"));
739
+ },
740
+ resize(ch, cols, rows) {
741
+ const entry = procs.get(ch);
742
+ if (!entry) return;
743
+ const size = clampSize({ cols, rows });
744
+ try {
745
+ entry.proc.resize(size.cols, size.rows);
746
+ } catch {
747
+ }
748
+ },
749
+ close(ch) {
750
+ const entry = procs.get(ch);
751
+ if (!entry) return;
752
+ entry.closed = true;
753
+ procs.delete(ch);
754
+ try {
755
+ entry.proc.kill();
756
+ } catch {
757
+ }
758
+ entry.sendClosed(null);
759
+ },
760
+ closeAll() {
761
+ for (const [ch, entry] of procs) {
762
+ entry.closed = true;
763
+ procs.delete(ch);
764
+ try {
765
+ entry.proc.kill();
766
+ } catch {
767
+ }
768
+ }
769
+ }
770
+ };
771
+ }
772
+
773
+ // src/rpc/ai.ts
774
+ async function credential(params) {
775
+ let prefix;
776
+ try {
777
+ prefix = configDirPrefix(params.config_dir, DEFAULT_CONFIG_DIRS[params.provider]);
778
+ } catch (err) {
779
+ throw new RpcFailure("invalid", err instanceof Error ? err.message : "invalid config dir");
780
+ }
781
+ const script = `${prefix}; ${credentialScript(params.provider)}`;
782
+ const r = await sh(script);
783
+ if (r.timedOut) throw new RpcFailure("timeout", "ai.credential timed out");
784
+ if (r.code !== 0) throw new RpcFailure("internal", `ai.credential exited with code ${r.code}`);
785
+ return { stdout: r.stdout };
786
+ }
787
+
788
+ // src/rpc/fs.ts
789
+ function processFailure(method, r) {
790
+ if (r.timedOut) return new RpcFailure("timeout", `${method} timed out`);
791
+ if (r.code !== 0) return new RpcFailure("internal", `${method} exited with code ${r.code}`);
792
+ return null;
793
+ }
794
+ async function list(params) {
795
+ const r = await sh(buildFsListScript(shellQuote(params.path)));
796
+ const failure = processFailure("fs.list", r);
797
+ if (failure) throw failure;
798
+ return { stdout: r.stdout };
799
+ }
800
+ async function mkdir(params) {
801
+ const r = await sh(buildMkdirScript(shellQuote(params.parent), shellQuote(params.name), { recursive: params.recursive === true }));
802
+ const failure = processFailure("fs.mkdir", r);
803
+ if (failure) throw failure;
804
+ return { stdout: r.stdout };
805
+ }
806
+
807
+ // src/rpc/hw.ts
808
+ async function probe(_params) {
809
+ const r = await sh(HARDWARE_SCRIPT, { timeoutMs: 14e3 });
810
+ if (r.timedOut) throw new RpcFailure("timeout", "hw.probe timed out");
811
+ if (r.code !== 0) throw new RpcFailure("internal", `hw.probe exited with code ${r.code}`);
812
+ return { stdout: r.stdout };
813
+ }
814
+
815
+ // src/rpc/paste.ts
816
+ async function pasteFile(params) {
817
+ const data = Buffer.from(params.data_b64, "base64");
818
+ if (data.length > PASTE_MAX_BYTES) throw new RpcFailure("invalid", "file too large");
819
+ const r = await sh(buildPasteScript(params.name), { input: data, timeoutMs: 6e4 });
820
+ if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
821
+ if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
822
+ const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
823
+ const path8 = lines[lines.length - 1] ?? "";
824
+ if (!path8.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
825
+ return { path: path8 };
826
+ }
827
+
828
+ // src/rpc/tmux.ts
829
+ function processFailure2(r) {
830
+ if (r.timedOut) return new RpcFailure("timeout", "tmux timed out");
831
+ if (r.error === "enoent") return new RpcFailure("no_tmux", "tmux not found");
832
+ if (r.error === "maxbuffer") return new RpcFailure("internal", "output too large");
833
+ return null;
834
+ }
835
+ async function list2(_params) {
836
+ const r = await run(tmuxPath(), ["list-sessions", "-F", "#{session_name}"]);
837
+ const failure = processFailure2(r);
838
+ if (failure) throw failure;
839
+ if (r.code !== 0) return { sessions: [] };
840
+ const sessions = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
841
+ return { sessions };
842
+ }
843
+ async function kill(params) {
844
+ const r = await run(tmuxPath(), ["kill-session", "-t", `=${params.session}`]);
845
+ const failure = processFailure2(r);
846
+ if (failure) throw failure;
847
+ return { killed: r.code === 0 };
848
+ }
849
+ async function capture(params) {
850
+ const r = await run(tmuxPath(), ["capture-pane", "-p", "-S", `-${params.lines}`, "-t", `=${params.session}:`]);
851
+ const failure = processFailure2(r);
852
+ if (failure) throw failure;
853
+ if (r.code !== 0) throw new RpcFailure("notfound", "session not found");
854
+ return { text: r.stdout };
855
+ }
856
+
857
+ // src/rpc/tools.ts
858
+ async function detect(_params) {
859
+ const r = await sh(DETECT_SCRIPT);
860
+ if (r.timedOut) throw new RpcFailure("timeout", "tools.detect timed out");
861
+ if (r.code !== 0) throw new RpcFailure("internal", `tools.detect exited with code ${r.code}`);
862
+ const { os: os8, capabilities } = parseDetect(r.stdout);
863
+ return { os: os8, tools: capabilities };
864
+ }
865
+
866
+ // src/rpc/index.ts
867
+ var handlers = {
868
+ "tmux.list": list2,
869
+ "tmux.kill": kill,
870
+ "tmux.capture": capture,
871
+ "tools.detect": detect,
872
+ "hw.probe": probe,
873
+ "fs.list": list,
874
+ "fs.mkdir": mkdir,
875
+ "ai.credential": credential,
876
+ "file.paste": pasteFile
877
+ };
878
+
879
+ // src/service/launchd.ts
880
+ import fs3 from "fs";
881
+ import os3 from "os";
882
+ import path3 from "path";
883
+ var LABEL = "dev.termhub.agent";
884
+ function renderPlist({ label, node, script, logPath }) {
885
+ const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
886
+ return `<?xml version="1.0" encoding="UTF-8"?>
887
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
888
+ <plist version="1.0">
889
+ <dict>
890
+ <key>Label</key>
891
+ <string>${escape(label)}</string>
892
+ <key>ProgramArguments</key>
893
+ <array>
894
+ <string>${escape(node)}</string>
895
+ <string>${escape(script)}</string>
896
+ <string>run</string>
897
+ </array>
898
+ <key>RunAtLoad</key>
899
+ <true/>
900
+ <key>KeepAlive</key>
901
+ <dict>
902
+ <key>SuccessfulExit</key>
903
+ <false/>
904
+ </dict>
905
+ <key>StandardOutPath</key>
906
+ <string>${escape(logPath)}</string>
907
+ <key>StandardErrorPath</key>
908
+ <string>${escape(logPath)}</string>
909
+ <key>EnvironmentVariables</key>
910
+ <dict>
911
+ <key>PATH</key>
912
+ <string>${escape(agentEnv().PATH ?? "")}</string>
913
+ </dict>
914
+ <key>WorkingDirectory</key>
915
+ <string>${escape(os3.homedir())}</string>
916
+ </dict>
917
+ </plist>
918
+ `;
919
+ }
920
+ function plistPath() {
921
+ return path3.join(os3.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
922
+ }
923
+ function gui() {
924
+ return `gui/${process.getuid ? process.getuid() : 0}`;
925
+ }
926
+ async function install(opts, deps = {}) {
927
+ const runFn = deps.run ?? run;
928
+ const file = plistPath();
929
+ const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
930
+ fs3.mkdirSync(path3.dirname(file), { recursive: true });
931
+ fs3.writeFileSync(file, plist, "utf8");
932
+ await runFn("launchctl", ["bootout", gui(), file]);
933
+ const result = await runFn("launchctl", ["bootstrap", gui(), file]);
934
+ if (result.code !== 0) {
935
+ throw new Error(`launchctl bootstrap failed (code ${result.code}): ${result.stderr.trim() || result.stdout.trim()}`);
936
+ }
937
+ }
938
+ async function uninstall(deps = {}) {
939
+ const runFn = deps.run ?? run;
940
+ const file = plistPath();
941
+ await runFn("launchctl", ["bootout", gui(), file]);
942
+ try {
943
+ fs3.unlinkSync(file);
944
+ } catch (err) {
945
+ if (err.code !== "ENOENT") throw err;
946
+ }
947
+ }
948
+ async function stopRestartLoop(deps = {}) {
949
+ const platform = deps.platform ?? process.platform;
950
+ if (platform !== "darwin") return;
951
+ const runFn = deps.run ?? run;
952
+ try {
953
+ await runFn("launchctl", ["bootout", `${gui()}/${LABEL}`]);
954
+ } catch {
955
+ }
956
+ }
957
+ async function status(deps = {}) {
958
+ const runFn = deps.run ?? run;
959
+ const result = await runFn("launchctl", ["print", `${gui()}/${LABEL}`]);
960
+ return result.code === 0;
961
+ }
962
+
963
+ // src/version.ts
964
+ var AGENT_VERSION = "0.1.0";
965
+
966
+ // src/run.ts
967
+ function detectOs(platform = process.platform) {
968
+ if (platform === "darwin") return "macos";
969
+ if (platform === "linux") return "linux";
970
+ return null;
971
+ }
972
+ async function buildHello(osName) {
973
+ let tools = [];
974
+ try {
975
+ const result = await handlers["tools.detect"]({});
976
+ tools = result.tools;
977
+ } catch {
978
+ tools = [];
979
+ }
980
+ return {
981
+ agent_version: AGENT_VERSION,
982
+ os: osName,
983
+ arch: process.arch,
984
+ hostname: os4.hostname(),
985
+ tmux: tools.includes("tmux"),
986
+ tools
987
+ };
988
+ }
989
+ var REVOKED_MESSAGE = "Token inv\xE1lido ou revogado";
990
+ var UPGRADE_MESSAGE = "Atualize o agente: npm i -g @termhub/agent";
991
+ async function checkServerConnection(config, timeoutMs = 5e3) {
992
+ const osName = detectOs() ?? "linux";
993
+ const controller = new AbortController();
994
+ let timedOut = false;
995
+ const timer = setTimeout(() => {
996
+ timedOut = true;
997
+ controller.abort();
998
+ }, timeoutMs);
999
+ try {
1000
+ const { closed } = await connectOnce(
1001
+ {
1002
+ url: config.url,
1003
+ token: config.token,
1004
+ hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os4.hostname(), tmux: false, tools: [], probe: true },
1005
+ onServerMessage: () => {
1006
+ },
1007
+ onStream: () => {
1008
+ },
1009
+ log: () => {
1010
+ }
1011
+ },
1012
+ controller.signal
1013
+ );
1014
+ const info = await closed;
1015
+ if (info.code === 1e3 && info.reason === "probe-ok") return { ok: true };
1016
+ if (info.code === CLOSE.UNAUTHORIZED) return { ok: false, error: REVOKED_MESSAGE };
1017
+ if (info.code === CLOSE.CONFLICT && info.reason === "protocol") return { ok: false, error: UPGRADE_MESSAGE };
1018
+ if (timedOut) return { ok: false, error: "O servidor n\xE3o respondeu" };
1019
+ return { ok: false, error: `Conex\xE3o encerrada (${info.code}${info.reason ? ` ${info.reason}` : ""})` };
1020
+ } catch (err) {
1021
+ if (err instanceof UpgradeRejectedError && err.status === 401) {
1022
+ return { ok: false, error: REVOKED_MESSAGE };
1023
+ }
1024
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1025
+ } finally {
1026
+ clearTimeout(timer);
1027
+ controller.abort();
1028
+ }
1029
+ }
1030
+ async function exitWithoutRestart(message) {
1031
+ console.error(message);
1032
+ await stopRestartLoop();
1033
+ process.exit(78);
1034
+ }
1035
+ async function runAgent(config, opts) {
1036
+ const osName = detectOs();
1037
+ if (!osName) {
1038
+ console.error("Sistema n\xE3o suportado");
1039
+ process.exit(1);
1040
+ }
1041
+ const hello = await buildHello(osName);
1042
+ const pty = createPtyManager({ log: opts.log });
1043
+ const dispatch = createDispatcher({ handlers, pty, log: opts.log });
1044
+ try {
1045
+ await runForever(
1046
+ {
1047
+ url: config.url,
1048
+ token: config.token,
1049
+ hello,
1050
+ onServerMessage: dispatch,
1051
+ onStream: (ch, data) => pty.write(ch, data),
1052
+ onDisconnect: () => pty.closeAll(),
1053
+ log: opts.log
1054
+ },
1055
+ opts.signal
1056
+ );
1057
+ } catch (err) {
1058
+ if (err instanceof RevokedError) {
1059
+ await exitWithoutRestart("Token revogado. Rode: termhub-agent connect --url <url>");
1060
+ }
1061
+ if (err instanceof ProtocolMismatchError) {
1062
+ await exitWithoutRestart(UPGRADE_MESSAGE);
1063
+ }
1064
+ throw err;
1065
+ }
1066
+ }
1067
+
1068
+ // src/commands/connect.ts
1069
+ var TOKEN_RE = /^thb_ag_[A-Za-z0-9_-]{43}$/;
1070
+ function promptToken() {
1071
+ return new Promise((resolve) => {
1072
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1073
+ rl.question("Cole o token do agente: ", (answer) => {
1074
+ rl.close();
1075
+ resolve(answer.trim());
1076
+ });
1077
+ });
1078
+ }
1079
+ async function runForegroundUntilSignal(config, log2) {
1080
+ const controller = new AbortController();
1081
+ const onSignal = () => controller.abort();
1082
+ process.once("SIGINT", onSignal);
1083
+ process.once("SIGTERM", onSignal);
1084
+ try {
1085
+ await runAgent(config, { signal: controller.signal, log: log2 });
1086
+ } finally {
1087
+ process.off("SIGINT", onSignal);
1088
+ process.off("SIGTERM", onSignal);
1089
+ }
1090
+ process.exit(0);
1091
+ }
1092
+ async function connectCommand(values, log2) {
1093
+ const url = values.url ?? process.env.TERMHUB_URL;
1094
+ if (!url) {
1095
+ console.error("Uso: termhub-agent connect --url <url> [--token <token>]");
1096
+ process.exitCode = 2;
1097
+ return;
1098
+ }
1099
+ let token = values.token ?? process.env.TERMHUB_TOKEN;
1100
+ if (!token) {
1101
+ if (!process.stdin.isTTY) {
1102
+ console.error("Uso: termhub-agent connect --url <url> --token <token>");
1103
+ process.exitCode = 2;
1104
+ return;
1105
+ }
1106
+ token = await promptToken();
1107
+ }
1108
+ if (!TOKEN_RE.test(token)) {
1109
+ console.error("Token inv\xE1lido.");
1110
+ process.exitCode = 2;
1111
+ return;
1112
+ }
1113
+ const osName = detectOs();
1114
+ if (!osName) {
1115
+ console.error("Sistema n\xE3o suportado");
1116
+ process.exitCode = 1;
1117
+ return;
1118
+ }
1119
+ const hello = await buildHello(osName);
1120
+ const controller = new AbortController();
1121
+ try {
1122
+ await connectOnce({ url, token, hello: { ...hello, probe: true }, onServerMessage: () => {
1123
+ }, onStream: () => {
1124
+ }, log: log2 }, controller.signal);
1125
+ controller.abort();
1126
+ } catch (err) {
1127
+ if (err instanceof UpgradeRejectedError && err.status === 401) {
1128
+ console.error("Token inv\xE1lido ou revogado");
1129
+ } else {
1130
+ console.error(`N\xE3o foi poss\xEDvel conectar: ${err instanceof Error ? err.message : String(err)}`);
1131
+ }
1132
+ process.exitCode = 1;
1133
+ return;
1134
+ }
1135
+ const config = { url, token, machine_id: "", machine_name: "", created_at: (/* @__PURE__ */ new Date()).toISOString() };
1136
+ writeConfig(config);
1137
+ console.log("Conectado. Agora rode: termhub-agent service install");
1138
+ await runForegroundUntilSignal(config, log2);
1139
+ }
1140
+
1141
+ // src/commands/disconnect.ts
1142
+ function disconnectCommand() {
1143
+ deleteConfig();
1144
+ console.log("Configura\xE7\xE3o removida. Revogue o token no app.");
1145
+ }
1146
+
1147
+ // src/doctor.ts
1148
+ import fs4 from "fs";
1149
+ import os5 from "os";
1150
+ import path4 from "path";
1151
+ var SERVER_CHECK_TIMEOUT_MS = 5e3;
1152
+ function defaultDoctorPaths() {
1153
+ const home = os5.homedir();
1154
+ const paths = [home, path4.join(home, "Documents"), path4.join(home, "Desktop")];
1155
+ if (process.platform === "darwin") {
1156
+ try {
1157
+ for (const entry of fs4.readdirSync("/Volumes", { withFileTypes: true })) {
1158
+ if (entry.isDirectory()) paths.push(path4.join("/Volumes", entry.name));
1159
+ }
1160
+ } catch {
1161
+ }
1162
+ }
1163
+ return paths;
1164
+ }
1165
+ function pathErrorCode(err) {
1166
+ const e = err;
1167
+ if (e?.code === "EPERM" || e?.code === "EACCES") return "eperm";
1168
+ if (e?.code) return e.code;
1169
+ return err instanceof Error ? err.message : String(err);
1170
+ }
1171
+ function checkPathAccess(p, fsImpl = fs4) {
1172
+ try {
1173
+ fsImpl.readdirSync(p);
1174
+ return { path: p, ok: true };
1175
+ } catch (err) {
1176
+ return { path: p, ok: false, error: pathErrorCode(err) };
1177
+ }
1178
+ }
1179
+ function fullDiskAccessNote(execPath = process.execPath) {
1180
+ return `Conceda Acesso Total ao Disco a ${execPath} em Ajustes \u2192 Privacidade e Seguran\xE7a \u2192 Acesso Total ao Disco`;
1181
+ }
1182
+ async function runDoctor(paths, deps = {}) {
1183
+ const fsImpl = deps.fs ?? fs4;
1184
+ const connect = deps.connect ?? checkServerConnection;
1185
+ const config = readConfig();
1186
+ const configReport = { ok: config !== null, path: configPath() };
1187
+ const server = config ? await connect(config, SERVER_CHECK_TIMEOUT_MS) : { ok: false, error: "sem configura\xE7\xE3o" };
1188
+ const tmuxCheck = await sh("command -v tmux");
1189
+ const tmux = { ok: tmuxCheck.code === 0, path: tmuxCheck.code === 0 ? tmuxCheck.stdout.trim() || void 0 : void 0 };
1190
+ let nodePty;
1191
+ try {
1192
+ await import("node-pty");
1193
+ nodePty = { ok: true };
1194
+ } catch (err) {
1195
+ nodePty = { ok: false, error: err instanceof Error ? err.message : String(err) };
1196
+ }
1197
+ const pathsReport = paths.map((p) => checkPathAccess(p, fsImpl));
1198
+ return { config: configReport, server, tmux, nodePty, paths: pathsReport };
1199
+ }
1200
+ function mark(ok) {
1201
+ return ok ? "\u2713" : "\u2717";
1202
+ }
1203
+ function formatDoctor(report, deps = {}) {
1204
+ const platform = deps.platform ?? process.platform;
1205
+ const execPath = deps.execPath ?? process.execPath;
1206
+ const lines = [];
1207
+ lines.push(`${mark(report.config.ok)} Configura\xE7\xE3o (${report.config.path})`);
1208
+ lines.push(`${mark(report.server.ok)} Servidor${report.server.ok ? "" : report.server.error ? `: ${report.server.error}` : ""}`);
1209
+ lines.push(`${mark(report.tmux.ok)} tmux${report.tmux.path ? ` (${report.tmux.path})` : ""}`);
1210
+ lines.push(`${mark(report.nodePty.ok)} node-pty${report.nodePty.ok ? "" : report.nodePty.error ? `: ${report.nodePty.error}` : ""}`);
1211
+ for (const p of report.paths) {
1212
+ lines.push(`${mark(p.ok)} ${p.path}${p.ok ? "" : p.error ? `: ${p.error}` : ""}`);
1213
+ if (!p.ok && p.error === "eperm" && platform === "darwin") {
1214
+ lines.push(` \u2192 ${fullDiskAccessNote(execPath)}`);
1215
+ }
1216
+ }
1217
+ return lines.join("\n");
1218
+ }
1219
+
1220
+ // src/commands/doctor.ts
1221
+ async function doctorCommand(paths, json) {
1222
+ const targets = paths.length > 0 ? paths : defaultDoctorPaths();
1223
+ const report = await runDoctor(targets);
1224
+ if (json) {
1225
+ console.log(JSON.stringify(report));
1226
+ } else {
1227
+ console.log(formatDoctor(report));
1228
+ }
1229
+ const allOk = report.config.ok && report.server.ok && report.tmux.ok && report.nodePty.ok && report.paths.every((p) => p.ok);
1230
+ process.exitCode = allOk ? 0 : 1;
1231
+ }
1232
+
1233
+ // src/commands/run.ts
1234
+ async function runCommand(log2) {
1235
+ const config = readConfig();
1236
+ if (!config) {
1237
+ return exitWithoutRestart("Nenhuma configura\xE7\xE3o. Rode: termhub-agent connect --url <url>");
1238
+ }
1239
+ await runForegroundUntilSignal(config, log2);
1240
+ }
1241
+
1242
+ // src/commands/service.ts
1243
+ import os7 from "os";
1244
+
1245
+ // src/service/index.ts
1246
+ import path7 from "path";
1247
+
1248
+ // src/paths.ts
1249
+ import { realpathSync } from "fs";
1250
+ import path5 from "path";
1251
+ import { pathToFileURL } from "url";
1252
+ function resolveScriptPath(argv1) {
1253
+ if (!argv1) return "";
1254
+ try {
1255
+ return realpathSync(argv1);
1256
+ } catch {
1257
+ return path5.resolve(argv1);
1258
+ }
1259
+ }
1260
+ function isMainModule(importMetaUrl, argv1) {
1261
+ if (!argv1) return false;
1262
+ return importMetaUrl === pathToFileURL(resolveScriptPath(argv1)).href;
1263
+ }
1264
+
1265
+ // src/service/systemd.ts
1266
+ import fs5 from "fs";
1267
+ import os6 from "os";
1268
+ import path6 from "path";
1269
+ var UNIT_NAME = "termhub-agent";
1270
+ function renderUnit({ node, script, logPath }) {
1271
+ const lines = [
1272
+ "[Unit]",
1273
+ "Description=termhub agent",
1274
+ "After=network-online.target",
1275
+ "",
1276
+ "[Service]",
1277
+ `ExecStart=${node} ${script} run`,
1278
+ "Restart=on-failure",
1279
+ "RestartSec=2",
1280
+ "RestartPreventExitStatus=78",
1281
+ `Environment=PATH=${agentEnv().PATH ?? ""}`
1282
+ ];
1283
+ if (logPath) {
1284
+ lines.push(`StandardOutput=append:${logPath}`, `StandardError=append:${logPath}`);
1285
+ }
1286
+ lines.push("", "[Install]", "WantedBy=default.target", "");
1287
+ return lines.join("\n");
1288
+ }
1289
+ function unitPath() {
1290
+ return path6.join(os6.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1291
+ }
1292
+ async function install2(opts, deps = {}) {
1293
+ const runFn = deps.run ?? run;
1294
+ const file = unitPath();
1295
+ const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1296
+ fs5.mkdirSync(path6.dirname(file), { recursive: true });
1297
+ fs5.writeFileSync(file, unit, "utf8");
1298
+ const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1299
+ if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
1300
+ const enable = await runFn("systemctl", ["--user", "enable", "--now", UNIT_NAME]);
1301
+ if (enable.code !== 0) throw new Error(`systemctl enable --now failed (code ${enable.code}): ${enable.stderr.trim()}`);
1302
+ console.log("Para o agente continuar ap\xF3s o logout: loginctl enable-linger $USER");
1303
+ }
1304
+ async function uninstall2(deps = {}) {
1305
+ const runFn = deps.run ?? run;
1306
+ await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
1307
+ try {
1308
+ fs5.unlinkSync(unitPath());
1309
+ } catch (err) {
1310
+ if (err.code !== "ENOENT") throw err;
1311
+ }
1312
+ await runFn("systemctl", ["--user", "daemon-reload"]);
1313
+ }
1314
+ async function status2(deps = {}) {
1315
+ const runFn = deps.run ?? run;
1316
+ const result = await runFn("systemctl", ["--user", "is-active", UNIT_NAME]);
1317
+ return result.code === 0;
1318
+ }
1319
+
1320
+ // src/service/index.ts
1321
+ function serviceFileOptions() {
1322
+ return {
1323
+ node: process.execPath,
1324
+ script: resolveScriptPath(process.argv[1]),
1325
+ logPath: path7.join(agentHome(), "agent.log")
1326
+ };
1327
+ }
1328
+ function assertSupported(platform) {
1329
+ if (platform !== "darwin" && platform !== "linux") throw new Error("Sistema n\xE3o suportado");
1330
+ }
1331
+ async function install3() {
1332
+ const platform = process.platform;
1333
+ assertSupported(platform);
1334
+ const opts = serviceFileOptions();
1335
+ if (platform === "darwin") await install(opts);
1336
+ else await install2(opts);
1337
+ }
1338
+ async function uninstall3() {
1339
+ const platform = process.platform;
1340
+ assertSupported(platform);
1341
+ if (platform === "darwin") await uninstall();
1342
+ else await uninstall2();
1343
+ }
1344
+ async function status3() {
1345
+ const platform = process.platform;
1346
+ assertSupported(platform);
1347
+ if (platform === "darwin") return status();
1348
+ return status2();
1349
+ }
1350
+
1351
+ // src/commands/service.ts
1352
+ async function serviceCommand(sub) {
1353
+ switch (sub) {
1354
+ case "install": {
1355
+ const homeCheck = checkPathAccess(os7.homedir());
1356
+ if (!homeCheck.ok && homeCheck.error === "eperm" && process.platform === "darwin") {
1357
+ console.error(fullDiskAccessNote());
1358
+ }
1359
+ await install3();
1360
+ console.log("Servi\xE7o instalado.");
1361
+ return;
1362
+ }
1363
+ case "uninstall":
1364
+ await uninstall3();
1365
+ console.log("Servi\xE7o removido.");
1366
+ return;
1367
+ case "status": {
1368
+ const active = await status3();
1369
+ console.log(active ? "ativo \u2713" : "inativo \u2717");
1370
+ return;
1371
+ }
1372
+ default:
1373
+ console.error("Uso: termhub-agent service install|uninstall|status");
1374
+ process.exitCode = 2;
1375
+ }
1376
+ }
1377
+
1378
+ // src/commands/status.ts
1379
+ var CHECK_TIMEOUT_MS = 5e3;
1380
+ async function statusCommand(json) {
1381
+ const config = readConfig();
1382
+ if (!config) {
1383
+ console.error("Nenhuma configura\xE7\xE3o. Rode: termhub-agent connect --url <url>");
1384
+ process.exitCode = 1;
1385
+ return;
1386
+ }
1387
+ const check = await checkServerConnection(config, CHECK_TIMEOUT_MS);
1388
+ if (json) {
1389
+ console.log(JSON.stringify({ url: config.url, machine_name: config.machine_name || null, version: AGENT_VERSION, connected: check.ok }));
1390
+ return;
1391
+ }
1392
+ console.log(`Servidor: ${config.url}`);
1393
+ console.log(`M\xE1quina: ${config.machine_name || "\u2014"}`);
1394
+ console.log(`Vers\xE3o: ${AGENT_VERSION}`);
1395
+ console.log(check.ok ? "conectado \u2713" : `desconectado \u2717${check.error ? ` (${check.error})` : ""}`);
1396
+ }
1397
+
1398
+ // src/cli.ts
1399
+ var log = (msg, meta) => {
1400
+ console.error(meta ? `[termhub-agent] ${msg} ${JSON.stringify(meta)}` : `[termhub-agent] ${msg}`);
1401
+ };
1402
+ function printHelp() {
1403
+ console.log(`Uso: termhub-agent <comando> [op\xE7\xF5es]
1404
+
1405
+ Comandos:
1406
+ connect [--url <url>] [--token <token>] Conecta este agente ao termhub
1407
+ run Roda o agente em primeiro plano
1408
+ status Mostra o estado da conex\xE3o
1409
+ disconnect Remove a configura\xE7\xE3o local
1410
+ service install|uninstall|status Gerencia o servi\xE7o do sistema
1411
+ doctor [caminhos...] Diagnostica problemas comuns
1412
+
1413
+ Op\xE7\xF5es:
1414
+ --url <url> URL do servidor termhub (ou $TERMHUB_URL)
1415
+ --token <token> Token do agente (ou $TERMHUB_TOKEN)
1416
+ --json Sa\xEDda em JSON (status, doctor)
1417
+ --version Mostra a vers\xE3o e sai
1418
+ --help Mostra esta ajuda e sai`);
1419
+ }
1420
+ function printVersion() {
1421
+ console.log(`termhub-agent ${AGENT_VERSION}`);
1422
+ }
1423
+ async function main(argv = process.argv.slice(2)) {
1424
+ let values;
1425
+ let positionals;
1426
+ try {
1427
+ ({ values, positionals } = parseArgs({
1428
+ args: argv,
1429
+ allowPositionals: true,
1430
+ options: {
1431
+ url: { type: "string" },
1432
+ token: { type: "string" },
1433
+ version: { type: "boolean" },
1434
+ help: { type: "boolean" },
1435
+ json: { type: "boolean" }
1436
+ }
1437
+ }));
1438
+ } catch (err) {
1439
+ console.error(err instanceof Error ? err.message : String(err));
1440
+ printHelp();
1441
+ process.exitCode = 2;
1442
+ return;
1443
+ }
1444
+ if (values.version) {
1445
+ printVersion();
1446
+ return;
1447
+ }
1448
+ if (values.help || positionals.length === 0) {
1449
+ printHelp();
1450
+ process.exitCode = values.help ? 0 : 2;
1451
+ return;
1452
+ }
1453
+ const [cmd, ...rest] = positionals;
1454
+ try {
1455
+ switch (cmd) {
1456
+ case "connect":
1457
+ await connectCommand({ url: values.url, token: values.token }, log);
1458
+ break;
1459
+ case "run":
1460
+ await runCommand(log);
1461
+ break;
1462
+ case "status":
1463
+ await statusCommand(!!values.json);
1464
+ break;
1465
+ case "disconnect":
1466
+ disconnectCommand();
1467
+ break;
1468
+ case "service":
1469
+ await serviceCommand(rest[0]);
1470
+ break;
1471
+ case "doctor":
1472
+ await doctorCommand(rest, !!values.json);
1473
+ break;
1474
+ default:
1475
+ console.error(`Comando desconhecido: ${cmd}`);
1476
+ printHelp();
1477
+ process.exitCode = 2;
1478
+ }
1479
+ } catch (err) {
1480
+ console.error(err instanceof Error ? err.message : String(err));
1481
+ process.exitCode = 1;
1482
+ }
1483
+ }
1484
+ if (isMainModule(import.meta.url, process.argv[1])) {
1485
+ void main();
1486
+ }
1487
+ export {
1488
+ main
1489
+ };