@termhub/agent 0.1.3 → 0.1.5

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 (2) hide show
  1. package/dist/cli.js +285 -51
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -40,7 +40,8 @@ var machinePath = z.string().min(1).max(4096).refine((p) => (p === "~" || p.star
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
42
  var rpcErrorSchema = z.object({
43
- code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal"]),
43
+ /** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user. */
44
+ code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal", "failed"]),
44
45
  message: z.string().max(2e3),
45
46
  path: z.string().max(4096).optional()
46
47
  });
@@ -60,7 +61,15 @@ var RPC = {
60
61
  recursive: z.boolean().optional()
61
62
  }), z.object({ stdout: z.string() })),
62
63
  "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
+ "file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4),
65
+ /** Monitor hooks (see @termhub/machine-ops hooks.ts): the agent writes the script, env and config entries under its own $HOME. */
66
+ /** `claude_dirs`: Claude config dirs besides ~/.claude (accounts with CLAUDE_CONFIG_DIR), hooked when they exist; since agent 0.1.5. */
67
+ "hooks.install": def(z.object({
68
+ hooks_url: z.string().min(1).max(2048).regex(/^https?:\/\/[^\s'"]+$/),
69
+ token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/),
70
+ claude_dirs: z.array(machinePath).max(16).optional()
71
+ }), z.object({ home: z.string(), claude: z.enum(["installed", "skipped"]), codex: z.enum(["installed", "skipped"]), claude_dirs: z.array(z.string()).optional() }), 15e3),
72
+ "hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3)
64
73
  };
65
74
  var RPC_METHODS = Object.keys(RPC);
66
75
  var rpcMethod = z.enum(RPC_METHODS);
@@ -355,7 +364,7 @@ function deleteConfig() {
355
364
  }
356
365
 
357
366
  // src/run.ts
358
- import os4 from "os";
367
+ import os5 from "os";
359
368
 
360
369
  // src/exec.ts
361
370
  import { execFile } from "child_process";
@@ -372,16 +381,16 @@ var DETECT_TOOLS = ["tmux", "claude", "gh", "git", "node", "pnpm", "xcodebuild",
372
381
  var WDA_RUNNER_APP = "$HOME/.termhub/WebDriverAgent/DerivedData/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app";
373
382
  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
383
  function parseDetect(stdout) {
375
- let os8 = null;
384
+ let os9 = null;
376
385
  const caps = [];
377
386
  for (const line of stdout.split("\n")) {
378
387
  if (line.startsWith("OS:")) {
379
388
  const raw = line.slice(3).trim().toLowerCase();
380
- os8 = raw === "darwin" ? "macos" : raw || null;
389
+ os9 = raw === "darwin" ? "macos" : raw || null;
381
390
  } else if (line.startsWith("CAP:"))
382
391
  caps.push(line.slice(4).trim());
383
392
  }
384
- return { os: os8, capabilities: caps };
393
+ return { os: os9, capabilities: caps };
385
394
  }
386
395
 
387
396
  // ../../packages/machine-ops/dist/hardware-script.js
@@ -526,13 +535,121 @@ function ptyEnv(base, shell) {
526
535
  };
527
536
  }
528
537
 
538
+ // ../../packages/machine-ops/dist/hooks.js
539
+ var HOOK_SCRIPT_REL = ".termhub/bin/termhub-hook";
540
+ var HOOK_ENV_REL = ".termhub/hook.env";
541
+ var HOOK_MARK = "termhub-hook";
542
+ var CLAUDE_DEFAULT_DIR = "~/.claude";
543
+ function claudeConfigDirs(accountDirs) {
544
+ const out = [CLAUDE_DEFAULT_DIR];
545
+ for (const raw of accountDirs) {
546
+ let d = (raw ?? "").trim().replace(/\/+$/, "");
547
+ if (!d || d === "~" || /[\0\n\r]/.test(d))
548
+ continue;
549
+ if (!d.startsWith("~/") && !d.startsWith("/"))
550
+ d = `~/${d}`;
551
+ if (!out.includes(d))
552
+ out.push(d);
553
+ }
554
+ return out;
555
+ }
556
+ function expandHome(dir, home) {
557
+ return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
558
+ }
559
+ var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Notification", "Stop", "SessionEnd"];
560
+ var HOOK_SCRIPT = `#!/bin/sh
561
+ # termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex hook events to
562
+ # termhub tagged with the tmux session, so the app knows which tab is waiting for you.
563
+ # Safe to delete (also remove the entries in ~/.claude/settings.json and ~/.codex/config.toml).
564
+ TOOL="\${1:-claude}"
565
+ [ -f "$HOME/${HOOK_ENV_REL}" ] || exit 0
566
+ . "$HOME/${HOOK_ENV_REL}"
567
+ [ -n "$TERMHUB_HOOK_URL" ] && [ -n "$TERMHUB_HOOK_TOKEN" ] || exit 0
568
+ [ -n "$TMUX_PANE" ] || exit 0
569
+ PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
570
+ SESSION=$(tmux display-message -p -t "$TMUX_PANE" '#{session_name}' 2>/dev/null) || exit 0
571
+ [ -n "$SESSION" ] || exit 0
572
+ if [ "$TOOL" = codex ]; then EVENT="$2"; else EVENT=$(cat 2>/dev/null); fi
573
+ [ -n "$EVENT" ] || EVENT='{}'
574
+ { printf '{"tool":"%s","session":"%s","event":' "$TOOL" "$SESSION"; printf '%s' "$EVENT"; printf '}'; } |
575
+ curl -s -m 5 -o /dev/null -X POST "$TERMHUB_HOOK_URL" \\
576
+ -H "authorization: Bearer $TERMHUB_HOOK_TOKEN" -H 'content-type: application/json' --data-binary @- >/dev/null 2>&1 &
577
+ exit 0
578
+ `;
579
+ function hookEnvFile(hooksUrl, token) {
580
+ return `TERMHUB_HOOK_URL=${shellQuote(hooksUrl)}
581
+ TERMHUB_HOOK_TOKEN=${shellQuote(token)}
582
+ `;
583
+ }
584
+ var isOurs = (e) => !!e && typeof e === "object" && Array.isArray(e.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes(HOOK_MARK));
585
+ function mergeClaudeSettings(current, scriptPath) {
586
+ let settings = {};
587
+ if (current.trim()) {
588
+ const parsed = JSON.parse(current);
589
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
590
+ throw new Error("~/.claude/settings.json n\xE3o \xE9 um objeto JSON");
591
+ settings = parsed;
592
+ }
593
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
594
+ for (const event of CLAUDE_HOOK_EVENTS) {
595
+ const list3 = Array.isArray(hooks[event]) ? hooks[event] : [];
596
+ const others = list3.filter((e) => !isOurs(e));
597
+ others.push({ hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] });
598
+ hooks[event] = others;
599
+ }
600
+ settings.hooks = hooks;
601
+ return `${JSON.stringify(settings, null, 2)}
602
+ `;
603
+ }
604
+ function stripClaudeSettings(current) {
605
+ if (!current.trim())
606
+ return current;
607
+ const parsed = JSON.parse(current);
608
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
609
+ return current;
610
+ const settings = parsed;
611
+ const hooks = settings.hooks;
612
+ if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
613
+ const h = hooks;
614
+ for (const key of Object.keys(h)) {
615
+ if (!Array.isArray(h[key]))
616
+ continue;
617
+ const kept = h[key].filter((e) => !isOurs(e));
618
+ if (kept.length)
619
+ h[key] = kept;
620
+ else
621
+ delete h[key];
622
+ }
623
+ if (Object.keys(h).length === 0)
624
+ delete settings.hooks;
625
+ }
626
+ return `${JSON.stringify(settings, null, 2)}
627
+ `;
628
+ }
629
+ function mergeCodexConfig(current, scriptPath) {
630
+ const line = `notify = [${JSON.stringify(scriptPath)}, "codex"]`;
631
+ const lines = current.split("\n");
632
+ const idx = lines.findIndex((l) => /^\s*notify\s*=/.test(l));
633
+ if (idx !== -1)
634
+ lines[idx] = line;
635
+ else
636
+ lines.unshift(line);
637
+ const out = lines.join("\n");
638
+ return out.endsWith("\n") ? out : `${out}
639
+ `;
640
+ }
641
+ function stripCodexConfig(current) {
642
+ const lines = current.split("\n").filter((l) => !(/^\s*notify\s*=/.test(l) && l.includes(HOOK_MARK)));
643
+ return lines.join("\n");
644
+ }
645
+
529
646
  // src/exec.ts
530
647
  var DEFAULT_TIMEOUT_MS = 8e3;
531
648
  var RpcFailure = class extends Error {
532
- constructor(code, message, path9) {
649
+ constructor(code, message, path10) {
533
650
  super(message);
534
651
  this.code = code;
535
- this.path = path9;
652
+ this.path = path10;
536
653
  }
537
654
  code;
538
655
  path;
@@ -635,7 +752,7 @@ function createDispatcher(deps) {
635
752
  // src/pty.ts
636
753
  import fs2 from "fs";
637
754
  import path2 from "path";
638
- function expandHome(rawCwd, home) {
755
+ function expandHome2(rawCwd, home) {
639
756
  if (rawCwd === "~") return home;
640
757
  if (rawCwd.startsWith("~/")) return path2.join(home, rawCwd.slice(2));
641
758
  return rawCwd;
@@ -649,7 +766,7 @@ function existsDir(p) {
649
766
  }
650
767
  function resolveCwd(rawCwd) {
651
768
  const home = process.env.HOME || "/";
652
- const expanded = expandHome(rawCwd, home);
769
+ const expanded = expandHome2(rawCwd, home);
653
770
  return existsDir(expanded) ? expanded : home;
654
771
  }
655
772
  function isEnoent(err) {
@@ -836,6 +953,121 @@ async function mkdir(params) {
836
953
  return { stdout: r.stdout };
837
954
  }
838
955
 
956
+ // src/rpc/hooks.ts
957
+ import { chmod, mkdir as mkdir2, readFile, rename, rm, stat, writeFile } from "fs/promises";
958
+ import os3 from "os";
959
+ import path4 from "path";
960
+ var CODEX_DIR_REL = ".codex";
961
+ var CODEX_CONFIG_REL = ".codex/config.toml";
962
+ var isEnoent2 = (err) => err?.code === "ENOENT";
963
+ async function readOrEmpty(file) {
964
+ try {
965
+ return await readFile(file, "utf8");
966
+ } catch (err) {
967
+ if (isEnoent2(err)) return "";
968
+ throw err;
969
+ }
970
+ }
971
+ async function isDir(dir) {
972
+ try {
973
+ return (await stat(dir)).isDirectory();
974
+ } catch {
975
+ return false;
976
+ }
977
+ }
978
+ async function writeAtomic(file, body, mode) {
979
+ const tmp = `${file}.termhub-new`;
980
+ await writeFile(tmp, body, { encoding: "utf8", mode });
981
+ await chmod(tmp, mode);
982
+ await rename(tmp, file);
983
+ }
984
+ var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
985
+ function fsFailure(err, shown) {
986
+ const code = err?.code;
987
+ if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
988
+ return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
989
+ }
990
+ async function claudeTargets(dirs, home) {
991
+ const out = [];
992
+ for (const d of claudeConfigDirs(dirs ?? [])) {
993
+ const dir = expandHome(d, home);
994
+ if (d !== CLAUDE_DEFAULT_DIR && !await isDir(dir)) continue;
995
+ out.push({ dir, file: path4.join(dir, "settings.json"), shown: `${d}/settings.json` });
996
+ }
997
+ return out;
998
+ }
999
+ async function install(params, home = os3.homedir()) {
1000
+ const scriptPath = path4.join(home, HOOK_SCRIPT_REL);
1001
+ const codexFile = path4.join(home, CODEX_CONFIG_REL);
1002
+ const targets = await claudeTargets(params.claude_dirs, home);
1003
+ const merged = [];
1004
+ for (const target of targets) {
1005
+ try {
1006
+ merged.push({ target, body: mergeClaudeSettings(await readOrEmpty(target.file), scriptPath) });
1007
+ } catch (err) {
1008
+ const message = err instanceof SyntaxError || err instanceof Error && err.message.includes("n\xE3o \xE9 um objeto JSON") ? `${target.shown} n\xE3o \xE9 JSON v\xE1lido` : err instanceof Error ? err.message : String(err);
1009
+ throw new RpcFailure("failed", message, relPath(target.shown));
1010
+ }
1011
+ }
1012
+ const hasCodex = await isDir(path4.join(home, CODEX_DIR_REL));
1013
+ const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty(codexFile), scriptPath) : null;
1014
+ let current = `~/${HOOK_SCRIPT_REL}`;
1015
+ try {
1016
+ await mkdir2(path4.dirname(scriptPath), { recursive: true });
1017
+ current = `~/${HOOK_ENV_REL}`;
1018
+ await writeAtomic(path4.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
1019
+ current = `~/${HOOK_SCRIPT_REL}`;
1020
+ await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
1021
+ for (const { target, body } of merged) {
1022
+ current = target.shown;
1023
+ await mkdir2(target.dir, { recursive: true });
1024
+ await writeAtomic(target.file, body, 420);
1025
+ }
1026
+ if (mergedCodex !== null) {
1027
+ current = `~/${CODEX_CONFIG_REL}`;
1028
+ await writeAtomic(codexFile, mergedCodex, 420);
1029
+ }
1030
+ } catch (err) {
1031
+ throw fsFailure(err, current);
1032
+ }
1033
+ return {
1034
+ home,
1035
+ claude: "installed",
1036
+ codex: mergedCodex !== null ? "installed" : "skipped",
1037
+ claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
1038
+ };
1039
+ }
1040
+ async function uninstall(params, home = os3.homedir()) {
1041
+ const codexFile = path4.join(home, CODEX_CONFIG_REL);
1042
+ const stripped = [];
1043
+ for (const target of await claudeTargets(params.claude_dirs, home)) {
1044
+ const current2 = await readOrEmpty(target.file);
1045
+ try {
1046
+ const body = current2.trim() ? stripClaudeSettings(current2) : current2;
1047
+ if (body !== current2) stripped.push({ target, body });
1048
+ } catch {
1049
+ }
1050
+ }
1051
+ const codexConfig = await isDir(path4.join(home, CODEX_DIR_REL)) ? await readOrEmpty(codexFile) : "";
1052
+ let current = `~/${HOOK_SCRIPT_REL}`;
1053
+ try {
1054
+ await rm(path4.join(home, HOOK_SCRIPT_REL), { force: true });
1055
+ current = `~/${HOOK_ENV_REL}`;
1056
+ await rm(path4.join(home, HOOK_ENV_REL), { force: true });
1057
+ for (const { target, body } of stripped) {
1058
+ current = target.shown;
1059
+ await writeAtomic(target.file, body, 420);
1060
+ }
1061
+ if (codexConfig.includes(HOOK_MARK)) {
1062
+ current = `~/${CODEX_CONFIG_REL}`;
1063
+ await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
1064
+ }
1065
+ } catch (err) {
1066
+ throw fsFailure(err, current);
1067
+ }
1068
+ return { removed: true };
1069
+ }
1070
+
839
1071
  // src/rpc/hw.ts
840
1072
  async function probe(_params) {
841
1073
  const r = await sh(HARDWARE_SCRIPT, { timeoutMs: 14e3 });
@@ -852,9 +1084,9 @@ async function pasteFile(params) {
852
1084
  if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
853
1085
  if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
854
1086
  const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
855
- const path9 = lines[lines.length - 1] ?? "";
856
- if (!path9.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
857
- return { path: path9 };
1087
+ const path10 = lines[lines.length - 1] ?? "";
1088
+ if (!path10.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
1089
+ return { path: path10 };
858
1090
  }
859
1091
 
860
1092
  // src/rpc/tmux.ts
@@ -891,8 +1123,8 @@ async function detect(_params) {
891
1123
  const r = await sh(DETECT_SCRIPT);
892
1124
  if (r.timedOut) throw new RpcFailure("timeout", "tools.detect timed out");
893
1125
  if (r.code !== 0) throw new RpcFailure("internal", `tools.detect exited with code ${r.code}`);
894
- const { os: os8, capabilities } = parseDetect(r.stdout);
895
- return { os: os8, tools: capabilities };
1126
+ const { os: os9, capabilities } = parseDetect(r.stdout);
1127
+ return { os: os9, tools: capabilities };
896
1128
  }
897
1129
 
898
1130
  // src/rpc/index.ts
@@ -905,13 +1137,15 @@ var handlers = {
905
1137
  "fs.list": list,
906
1138
  "fs.mkdir": mkdir,
907
1139
  "ai.credential": credential,
908
- "file.paste": pasteFile
1140
+ "file.paste": pasteFile,
1141
+ "hooks.install": install,
1142
+ "hooks.uninstall": uninstall
909
1143
  };
910
1144
 
911
1145
  // src/service/launchd.ts
912
1146
  import fs4 from "fs";
913
- import os3 from "os";
914
- import path4 from "path";
1147
+ import os4 from "os";
1148
+ import path5 from "path";
915
1149
  var LABEL = "dev.termhub.agent";
916
1150
  function renderPlist({ label, node, script, logPath }) {
917
1151
  const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -944,22 +1178,22 @@ function renderPlist({ label, node, script, logPath }) {
944
1178
  <string>${escape(agentEnv().PATH ?? "")}</string>
945
1179
  </dict>
946
1180
  <key>WorkingDirectory</key>
947
- <string>${escape(os3.homedir())}</string>
1181
+ <string>${escape(os4.homedir())}</string>
948
1182
  </dict>
949
1183
  </plist>
950
1184
  `;
951
1185
  }
952
1186
  function plistPath() {
953
- return path4.join(os3.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
1187
+ return path5.join(os4.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
954
1188
  }
955
1189
  function gui() {
956
1190
  return `gui/${process.getuid ? process.getuid() : 0}`;
957
1191
  }
958
- async function install(opts, deps = {}) {
1192
+ async function install2(opts, deps = {}) {
959
1193
  const runFn = deps.run ?? run;
960
1194
  const file = plistPath();
961
1195
  const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
962
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
1196
+ fs4.mkdirSync(path5.dirname(file), { recursive: true });
963
1197
  fs4.writeFileSync(file, plist, "utf8");
964
1198
  await runFn("launchctl", ["bootout", gui(), file]);
965
1199
  const result = await runFn("launchctl", ["bootstrap", gui(), file]);
@@ -967,7 +1201,7 @@ async function install(opts, deps = {}) {
967
1201
  throw new Error(`launchctl bootstrap failed (code ${result.code}): ${result.stderr.trim() || result.stdout.trim()}`);
968
1202
  }
969
1203
  }
970
- async function uninstall(deps = {}) {
1204
+ async function uninstall2(deps = {}) {
971
1205
  const runFn = deps.run ?? run;
972
1206
  const file = plistPath();
973
1207
  await runFn("launchctl", ["bootout", gui(), file]);
@@ -993,7 +1227,7 @@ async function status(deps = {}) {
993
1227
  }
994
1228
 
995
1229
  // src/version.ts
996
- var AGENT_VERSION = "0.1.3";
1230
+ var AGENT_VERSION = "0.1.5";
997
1231
 
998
1232
  // src/run.ts
999
1233
  function detectOs(platform = process.platform) {
@@ -1013,7 +1247,7 @@ async function buildHello(osName) {
1013
1247
  agent_version: AGENT_VERSION,
1014
1248
  os: osName,
1015
1249
  arch: process.arch,
1016
- hostname: os4.hostname(),
1250
+ hostname: os5.hostname(),
1017
1251
  tmux: tools.includes("tmux"),
1018
1252
  tools
1019
1253
  };
@@ -1033,7 +1267,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
1033
1267
  {
1034
1268
  url: config.url,
1035
1269
  token: config.token,
1036
- hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os4.hostname(), tmux: false, tools: [], probe: true },
1270
+ hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os5.hostname(), tmux: false, tools: [], probe: true },
1037
1271
  onServerMessage: () => {
1038
1272
  },
1039
1273
  onStream: () => {
@@ -1184,16 +1418,16 @@ function disconnectCommand() {
1184
1418
 
1185
1419
  // src/doctor.ts
1186
1420
  import fs5 from "fs";
1187
- import os5 from "os";
1188
- import path5 from "path";
1421
+ import os6 from "os";
1422
+ import path6 from "path";
1189
1423
  var SERVER_CHECK_TIMEOUT_MS = 5e3;
1190
1424
  function defaultDoctorPaths() {
1191
- const home = os5.homedir();
1192
- const paths = [home, path5.join(home, "Documents"), path5.join(home, "Desktop")];
1425
+ const home = os6.homedir();
1426
+ const paths = [home, path6.join(home, "Documents"), path6.join(home, "Desktop")];
1193
1427
  if (process.platform === "darwin") {
1194
1428
  try {
1195
1429
  for (const entry of fs5.readdirSync("/Volumes", { withFileTypes: true })) {
1196
- if (entry.isDirectory()) paths.push(path5.join("/Volumes", entry.name));
1430
+ if (entry.isDirectory()) paths.push(path6.join("/Volumes", entry.name));
1197
1431
  }
1198
1432
  } catch {
1199
1433
  }
@@ -1285,21 +1519,21 @@ async function runCommand(log2) {
1285
1519
  }
1286
1520
 
1287
1521
  // src/commands/service.ts
1288
- import os7 from "os";
1522
+ import os8 from "os";
1289
1523
 
1290
1524
  // src/service/index.ts
1291
- import path8 from "path";
1525
+ import path9 from "path";
1292
1526
 
1293
1527
  // src/paths.ts
1294
1528
  import { realpathSync } from "fs";
1295
- import path6 from "path";
1529
+ import path7 from "path";
1296
1530
  import { pathToFileURL } from "url";
1297
1531
  function resolveScriptPath(argv1) {
1298
1532
  if (!argv1) return "";
1299
1533
  try {
1300
1534
  return realpathSync(argv1);
1301
1535
  } catch {
1302
- return path6.resolve(argv1);
1536
+ return path7.resolve(argv1);
1303
1537
  }
1304
1538
  }
1305
1539
  function isMainModule(importMetaUrl, argv1) {
@@ -1309,8 +1543,8 @@ function isMainModule(importMetaUrl, argv1) {
1309
1543
 
1310
1544
  // src/service/systemd.ts
1311
1545
  import fs6 from "fs";
1312
- import os6 from "os";
1313
- import path7 from "path";
1546
+ import os7 from "os";
1547
+ import path8 from "path";
1314
1548
  var UNIT_NAME = "termhub-agent";
1315
1549
  function renderUnit({ node, script, logPath }) {
1316
1550
  const lines = [
@@ -1332,13 +1566,13 @@ function renderUnit({ node, script, logPath }) {
1332
1566
  return lines.join("\n");
1333
1567
  }
1334
1568
  function unitPath() {
1335
- return path7.join(os6.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1569
+ return path8.join(os7.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1336
1570
  }
1337
- async function install2(opts, deps = {}) {
1571
+ async function install3(opts, deps = {}) {
1338
1572
  const runFn = deps.run ?? run;
1339
1573
  const file = unitPath();
1340
1574
  const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1341
- fs6.mkdirSync(path7.dirname(file), { recursive: true });
1575
+ fs6.mkdirSync(path8.dirname(file), { recursive: true });
1342
1576
  fs6.writeFileSync(file, unit, "utf8");
1343
1577
  const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1344
1578
  if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
@@ -1346,7 +1580,7 @@ async function install2(opts, deps = {}) {
1346
1580
  if (enable.code !== 0) throw new Error(`systemctl enable --now failed (code ${enable.code}): ${enable.stderr.trim()}`);
1347
1581
  console.log("Para o agente continuar ap\xF3s o logout: loginctl enable-linger $USER");
1348
1582
  }
1349
- async function uninstall2(deps = {}) {
1583
+ async function uninstall3(deps = {}) {
1350
1584
  const runFn = deps.run ?? run;
1351
1585
  await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
1352
1586
  try {
@@ -1367,24 +1601,24 @@ function serviceFileOptions() {
1367
1601
  return {
1368
1602
  node: process.execPath,
1369
1603
  script: resolveScriptPath(process.argv[1]),
1370
- logPath: path8.join(agentHome(), "agent.log")
1604
+ logPath: path9.join(agentHome(), "agent.log")
1371
1605
  };
1372
1606
  }
1373
1607
  function assertSupported(platform) {
1374
1608
  if (platform !== "darwin" && platform !== "linux") throw new Error("Sistema n\xE3o suportado");
1375
1609
  }
1376
- async function install3() {
1610
+ async function install4() {
1377
1611
  const platform = process.platform;
1378
1612
  assertSupported(platform);
1379
1613
  const opts = serviceFileOptions();
1380
- if (platform === "darwin") await install(opts);
1381
- else await install2(opts);
1614
+ if (platform === "darwin") await install2(opts);
1615
+ else await install3(opts);
1382
1616
  }
1383
- async function uninstall3() {
1617
+ async function uninstall4() {
1384
1618
  const platform = process.platform;
1385
1619
  assertSupported(platform);
1386
- if (platform === "darwin") await uninstall();
1387
- else await uninstall2();
1620
+ if (platform === "darwin") await uninstall2();
1621
+ else await uninstall3();
1388
1622
  }
1389
1623
  async function status3() {
1390
1624
  const platform = process.platform;
@@ -1397,16 +1631,16 @@ async function status3() {
1397
1631
  async function serviceCommand(sub) {
1398
1632
  switch (sub) {
1399
1633
  case "install": {
1400
- const homeCheck = checkPathAccess(os7.homedir());
1634
+ const homeCheck = checkPathAccess(os8.homedir());
1401
1635
  if (!homeCheck.ok && homeCheck.error === "eperm" && process.platform === "darwin") {
1402
1636
  console.error(fullDiskAccessNote());
1403
1637
  }
1404
- await install3();
1638
+ await install4();
1405
1639
  console.log("Servi\xE7o instalado.");
1406
1640
  return;
1407
1641
  }
1408
1642
  case "uninstall":
1409
- await uninstall3();
1643
+ await uninstall4();
1410
1644
  console.log("Servi\xE7o removido.");
1411
1645
  return;
1412
1646
  case "status": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Agente do termhub: conecta esta máquina ao servidor por WebSocket de saída (sem SSH) e expõe os terminais tmux no navegador.",
5
5
  "license": "MIT",
6
6
  "private": false,