@termhub/agent 0.2.3 → 0.2.4

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 +271 -153
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -211,6 +211,7 @@ function connectOnce(opts, signal) {
211
211
  };
212
212
  const hello = { type: "hello", protocol: PROTOCOL_VERSION, ...opts.hello };
213
213
  socket.sendControl(hello);
214
+ opts.onConnect?.();
214
215
  resolve({ socket, closed });
215
216
  });
216
217
  ws.on("message", (data) => {
@@ -376,9 +377,10 @@ function deleteConfig() {
376
377
  // src/run.ts
377
378
  import os6 from "os";
378
379
 
379
- // src/exec.ts
380
- import { execFile } from "child_process";
381
- import os2 from "os";
380
+ // src/rpc/hooks.ts
381
+ import { chmod, mkdir, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "fs/promises";
382
+ import os3 from "os";
383
+ import path3 from "path";
382
384
 
383
385
  // ../../packages/machine-ops/dist/shell.js
384
386
  function shellQuote(s) {
@@ -665,13 +667,105 @@ function stripCodexConfig(current) {
665
667
  return lines.join("\n");
666
668
  }
667
669
 
670
+ // ../../packages/machine-ops/dist/discover.js
671
+ var CLAUDE_MARKERS = ["settings.json", "projects", ".credentials.json"];
672
+ var CLAUDE_DIR_NAME = /^\.claude[A-Za-z0-9._-]*$/;
673
+ function claudeDirsFromHome(entries) {
674
+ const out = [];
675
+ for (const entry of entries) {
676
+ if (!CLAUDE_DIR_NAME.test(entry.name))
677
+ continue;
678
+ if (!entry.files.some((f) => CLAUDE_MARKERS.includes(f)))
679
+ continue;
680
+ const dir = `~/${entry.name}`;
681
+ if (!out.includes(dir))
682
+ out.push(dir);
683
+ }
684
+ return out;
685
+ }
686
+ var ASSIGNMENT = /CLAUDE_CONFIG_DIR=(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"';]*))/g;
687
+ var FISH_SET = /CLAUDE_CONFIG_DIR\s+(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"';]+))/g;
688
+ function normalize(raw) {
689
+ let value = raw.trim().replace(/\/+$/, "");
690
+ value = value.replace(/^\$\{?HOME\}?(?=\/|$)/, "~");
691
+ if (!value || value === "~" || value.includes("$") || /[\0\n\r]/.test(value))
692
+ return null;
693
+ if (!value.startsWith("~/") && !value.startsWith("/"))
694
+ value = `~/${value}`;
695
+ return value;
696
+ }
697
+ function configDirsFromRc(text) {
698
+ const out = [];
699
+ for (const line of text.split("\n")) {
700
+ if (/^\s*#/.test(line))
701
+ continue;
702
+ const pattern = /^\s*set\s/.test(line) ? FISH_SET : ASSIGNMENT;
703
+ pattern.lastIndex = 0;
704
+ for (let m = pattern.exec(line); m; m = pattern.exec(line)) {
705
+ const dir = normalize(m[1] ?? m[2] ?? m[3] ?? "");
706
+ if (dir && !out.includes(dir))
707
+ out.push(dir);
708
+ }
709
+ }
710
+ return out;
711
+ }
712
+
713
+ // src/claude-dirs.ts
714
+ import { readdir, readFile, stat } from "fs/promises";
715
+ import path2 from "path";
716
+ var RC_FILES = [".zshrc", ".bashrc", ".bash_profile", ".profile", ".config/fish/config.fish"];
717
+ async function readOrEmpty(file) {
718
+ try {
719
+ return await readFile(file, "utf8");
720
+ } catch {
721
+ return "";
722
+ }
723
+ }
724
+ async function isDir(dir) {
725
+ try {
726
+ return (await stat(dir)).isDirectory();
727
+ } catch {
728
+ return false;
729
+ }
730
+ }
731
+ async function fileNames(dir) {
732
+ try {
733
+ return await readdir(dir);
734
+ } catch {
735
+ return [];
736
+ }
737
+ }
738
+ async function candidateNames(home) {
739
+ try {
740
+ const list3 = await readdir(home, { withFileTypes: true });
741
+ return list3.filter((d) => d.isDirectory() && d.name.startsWith(".claude")).map((d) => d.name);
742
+ } catch {
743
+ return [];
744
+ }
745
+ }
746
+ async function discoverClaudeDirs(home) {
747
+ const entries = [];
748
+ for (const name of await candidateNames(home)) {
749
+ entries.push({ name, files: await fileNames(path2.join(home, name)) });
750
+ }
751
+ const dirs = claudeDirsFromHome(entries);
752
+ for (const rc of RC_FILES) {
753
+ for (const dir of configDirsFromRc(await readOrEmpty(path2.join(home, rc)))) {
754
+ if (!dirs.includes(dir) && await isDir(expandHome(dir, home))) dirs.push(dir);
755
+ }
756
+ }
757
+ return dirs;
758
+ }
759
+
668
760
  // src/exec.ts
761
+ import { execFile } from "child_process";
762
+ import os2 from "os";
669
763
  var DEFAULT_TIMEOUT_MS = 8e3;
670
764
  var RpcFailure = class extends Error {
671
- constructor(code, message, path11) {
765
+ constructor(code, message, path12) {
672
766
  super(message);
673
767
  this.code = code;
674
- this.path = path11;
768
+ this.path = path12;
675
769
  }
676
770
  code;
677
771
  path;
@@ -716,6 +810,138 @@ function sh(script, opts = {}) {
716
810
  return run("/bin/sh", ["-c", script], opts);
717
811
  }
718
812
 
813
+ // src/rpc/hooks.ts
814
+ var CODEX_DIR_REL = ".codex";
815
+ var CODEX_CONFIG_REL = ".codex/config.toml";
816
+ var isEnoent = (err) => err?.code === "ENOENT";
817
+ async function readOrEmpty2(file) {
818
+ try {
819
+ return await readFile2(file, "utf8");
820
+ } catch (err) {
821
+ if (isEnoent(err)) return "";
822
+ throw err;
823
+ }
824
+ }
825
+ async function isDir2(dir) {
826
+ try {
827
+ return (await stat2(dir)).isDirectory();
828
+ } catch {
829
+ return false;
830
+ }
831
+ }
832
+ async function writeAtomic(file, body, mode) {
833
+ const tmp = `${file}.termhub-new`;
834
+ await writeFile(tmp, body, { encoding: "utf8", mode });
835
+ await chmod(tmp, mode);
836
+ await rename(tmp, file);
837
+ }
838
+ var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
839
+ function fsFailure(err, shown) {
840
+ const code = err?.code;
841
+ if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
842
+ return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
843
+ }
844
+ async function claudeTargets(dirs, home) {
845
+ const out = [];
846
+ for (const d of claudeConfigDirs([...dirs ?? [], ...await discoverClaudeDirs(home)])) {
847
+ const dir = expandHome(d, home);
848
+ if (d !== CLAUDE_DEFAULT_DIR && !await isDir2(dir)) continue;
849
+ out.push({ dir, file: path3.join(dir, "settings.json"), shown: `${d}/settings.json` });
850
+ }
851
+ return out;
852
+ }
853
+ async function install(params, home = os3.homedir()) {
854
+ const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
855
+ const codexFile = path3.join(home, CODEX_CONFIG_REL);
856
+ const targets = await claudeTargets(params.claude_dirs, home);
857
+ const merged = [];
858
+ for (const target of targets) {
859
+ try {
860
+ merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath) });
861
+ } catch (err) {
862
+ 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);
863
+ throw new RpcFailure("failed", message, relPath(target.shown));
864
+ }
865
+ }
866
+ const hasCodex = await isDir2(path3.join(home, CODEX_DIR_REL));
867
+ const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty2(codexFile), scriptPath) : null;
868
+ let current = `~/${HOOK_SCRIPT_REL}`;
869
+ try {
870
+ await mkdir(path3.dirname(scriptPath), { recursive: true });
871
+ current = `~/${HOOK_ENV_REL}`;
872
+ await writeAtomic(path3.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
873
+ current = `~/${HOOK_SCRIPT_REL}`;
874
+ await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
875
+ for (const { target, body } of merged) {
876
+ current = target.shown;
877
+ await mkdir(target.dir, { recursive: true });
878
+ await writeAtomic(target.file, body, 420);
879
+ }
880
+ if (mergedCodex !== null) {
881
+ current = `~/${CODEX_CONFIG_REL}`;
882
+ await writeAtomic(codexFile, mergedCodex, 420);
883
+ }
884
+ } catch (err) {
885
+ throw fsFailure(err, current);
886
+ }
887
+ return {
888
+ home,
889
+ claude: "installed",
890
+ codex: mergedCodex !== null ? "installed" : "skipped",
891
+ claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
892
+ };
893
+ }
894
+ async function heal(home = os3.homedir()) {
895
+ const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
896
+ const env = await readOrEmpty2(path3.join(home, HOOK_ENV_REL));
897
+ if (!env.trim() || !await readOrEmpty2(scriptPath)) return [];
898
+ const healed = [];
899
+ for (const dir of await discoverClaudeDirs(home)) {
900
+ const file = path3.join(expandHome(dir, home), "settings.json");
901
+ const current = await readOrEmpty2(file);
902
+ let body;
903
+ try {
904
+ body = mergeClaudeSettings(current, scriptPath);
905
+ } catch {
906
+ continue;
907
+ }
908
+ if (body === current) continue;
909
+ await writeAtomic(file, body, 420);
910
+ healed.push(dir);
911
+ }
912
+ return healed;
913
+ }
914
+ async function uninstall(params, home = os3.homedir()) {
915
+ const codexFile = path3.join(home, CODEX_CONFIG_REL);
916
+ const stripped = [];
917
+ for (const target of await claudeTargets(params.claude_dirs, home)) {
918
+ const current2 = await readOrEmpty2(target.file);
919
+ try {
920
+ const body = current2.trim() ? stripClaudeSettings(current2) : current2;
921
+ if (body !== current2) stripped.push({ target, body });
922
+ } catch {
923
+ }
924
+ }
925
+ const codexConfig = await isDir2(path3.join(home, CODEX_DIR_REL)) ? await readOrEmpty2(codexFile) : "";
926
+ let current = `~/${HOOK_SCRIPT_REL}`;
927
+ try {
928
+ await rm(path3.join(home, HOOK_SCRIPT_REL), { force: true });
929
+ current = `~/${HOOK_ENV_REL}`;
930
+ await rm(path3.join(home, HOOK_ENV_REL), { force: true });
931
+ for (const { target, body } of stripped) {
932
+ current = target.shown;
933
+ await writeAtomic(target.file, body, 420);
934
+ }
935
+ if (codexConfig.includes(HOOK_MARK)) {
936
+ current = `~/${CODEX_CONFIG_REL}`;
937
+ await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
938
+ }
939
+ } catch (err) {
940
+ throw fsFailure(err, current);
941
+ }
942
+ return { removed: true };
943
+ }
944
+
719
945
  // src/dispatch.ts
720
946
  async function handleRpc(msg, socket, handlers2, log2) {
721
947
  const method = msg.method;
@@ -773,11 +999,11 @@ function createDispatcher(deps) {
773
999
 
774
1000
  // src/pty.ts
775
1001
  import fs3 from "fs";
776
- import path3 from "path";
1002
+ import path5 from "path";
777
1003
 
778
1004
  // src/pty-health.ts
779
1005
  import fs2 from "fs";
780
- import path2 from "path";
1006
+ import path4 from "path";
781
1007
  import { createRequire } from "module";
782
1008
  function findSpawnHelper(resolveFrom = import.meta.url) {
783
1009
  let pkgJson;
@@ -786,10 +1012,10 @@ function findSpawnHelper(resolveFrom = import.meta.url) {
786
1012
  } catch {
787
1013
  return null;
788
1014
  }
789
- const root = path2.dirname(pkgJson);
1015
+ const root = path4.dirname(pkgJson);
790
1016
  const candidates = [
791
- path2.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
792
- path2.join(root, "build", "Release", "spawn-helper")
1017
+ path4.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
1018
+ path4.join(root, "build", "Release", "spawn-helper")
793
1019
  ];
794
1020
  return candidates.find((p) => fs2.existsSync(p)) ?? null;
795
1021
  }
@@ -809,7 +1035,7 @@ function ensureSpawnHelperExecutable(helper = findSpawnHelper()) {
809
1035
  // src/pty.ts
810
1036
  function expandHome2(rawCwd, home) {
811
1037
  if (rawCwd === "~") return home;
812
- if (rawCwd.startsWith("~/")) return path3.join(home, rawCwd.slice(2));
1038
+ if (rawCwd.startsWith("~/")) return path5.join(home, rawCwd.slice(2));
813
1039
  return rawCwd;
814
1040
  }
815
1041
  function existsDir(p) {
@@ -827,7 +1053,7 @@ function resolveCwd(rawCwd) {
827
1053
  function isSpawnHelperFailure(err) {
828
1054
  return /posix_spawnp failed/.test(err instanceof Error ? err.message : String(err));
829
1055
  }
830
- function isEnoent(err) {
1056
+ function isEnoent2(err) {
831
1057
  const e = err;
832
1058
  if (e?.code === "ENOENT") return true;
833
1059
  const message = e instanceof Error ? e.message : String(err);
@@ -878,7 +1104,7 @@ function createPtyManager(deps) {
878
1104
  socket.sendControl({
879
1105
  type: "open_error",
880
1106
  ch,
881
- error: isEnoent(err) ? { code: "no_tmux", message: "tmux not found" } : { code: "internal", message: "failed to start pty" }
1107
+ error: isEnoent2(err) ? { code: "no_tmux", message: "tmux not found" } : { code: "internal", message: "failed to start pty" }
882
1108
  });
883
1109
  return;
884
1110
  }
@@ -983,128 +1209,13 @@ async function list(params) {
983
1209
  if (failure) throw failure;
984
1210
  return { stdout: r.stdout };
985
1211
  }
986
- async function mkdir(params) {
1212
+ async function mkdir2(params) {
987
1213
  const r = await sh(buildMkdirScript(shellQuote(params.parent), shellQuote(params.name), { recursive: params.recursive === true }));
988
1214
  const failure = processFailure("fs.mkdir", r);
989
1215
  if (failure) throw failure;
990
1216
  return { stdout: r.stdout };
991
1217
  }
992
1218
 
993
- // src/rpc/hooks.ts
994
- import { chmod, mkdir as mkdir2, readFile, rename, rm, stat, writeFile } from "fs/promises";
995
- import os3 from "os";
996
- import path4 from "path";
997
- var CODEX_DIR_REL = ".codex";
998
- var CODEX_CONFIG_REL = ".codex/config.toml";
999
- var isEnoent2 = (err) => err?.code === "ENOENT";
1000
- async function readOrEmpty(file) {
1001
- try {
1002
- return await readFile(file, "utf8");
1003
- } catch (err) {
1004
- if (isEnoent2(err)) return "";
1005
- throw err;
1006
- }
1007
- }
1008
- async function isDir(dir) {
1009
- try {
1010
- return (await stat(dir)).isDirectory();
1011
- } catch {
1012
- return false;
1013
- }
1014
- }
1015
- async function writeAtomic(file, body, mode) {
1016
- const tmp = `${file}.termhub-new`;
1017
- await writeFile(tmp, body, { encoding: "utf8", mode });
1018
- await chmod(tmp, mode);
1019
- await rename(tmp, file);
1020
- }
1021
- var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
1022
- function fsFailure(err, shown) {
1023
- const code = err?.code;
1024
- if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
1025
- return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
1026
- }
1027
- async function claudeTargets(dirs, home) {
1028
- const out = [];
1029
- for (const d of claudeConfigDirs(dirs ?? [])) {
1030
- const dir = expandHome(d, home);
1031
- if (d !== CLAUDE_DEFAULT_DIR && !await isDir(dir)) continue;
1032
- out.push({ dir, file: path4.join(dir, "settings.json"), shown: `${d}/settings.json` });
1033
- }
1034
- return out;
1035
- }
1036
- async function install(params, home = os3.homedir()) {
1037
- const scriptPath = path4.join(home, HOOK_SCRIPT_REL);
1038
- const codexFile = path4.join(home, CODEX_CONFIG_REL);
1039
- const targets = await claudeTargets(params.claude_dirs, home);
1040
- const merged = [];
1041
- for (const target of targets) {
1042
- try {
1043
- merged.push({ target, body: mergeClaudeSettings(await readOrEmpty(target.file), scriptPath) });
1044
- } catch (err) {
1045
- 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);
1046
- throw new RpcFailure("failed", message, relPath(target.shown));
1047
- }
1048
- }
1049
- const hasCodex = await isDir(path4.join(home, CODEX_DIR_REL));
1050
- const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty(codexFile), scriptPath) : null;
1051
- let current = `~/${HOOK_SCRIPT_REL}`;
1052
- try {
1053
- await mkdir2(path4.dirname(scriptPath), { recursive: true });
1054
- current = `~/${HOOK_ENV_REL}`;
1055
- await writeAtomic(path4.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
1056
- current = `~/${HOOK_SCRIPT_REL}`;
1057
- await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
1058
- for (const { target, body } of merged) {
1059
- current = target.shown;
1060
- await mkdir2(target.dir, { recursive: true });
1061
- await writeAtomic(target.file, body, 420);
1062
- }
1063
- if (mergedCodex !== null) {
1064
- current = `~/${CODEX_CONFIG_REL}`;
1065
- await writeAtomic(codexFile, mergedCodex, 420);
1066
- }
1067
- } catch (err) {
1068
- throw fsFailure(err, current);
1069
- }
1070
- return {
1071
- home,
1072
- claude: "installed",
1073
- codex: mergedCodex !== null ? "installed" : "skipped",
1074
- claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
1075
- };
1076
- }
1077
- async function uninstall(params, home = os3.homedir()) {
1078
- const codexFile = path4.join(home, CODEX_CONFIG_REL);
1079
- const stripped = [];
1080
- for (const target of await claudeTargets(params.claude_dirs, home)) {
1081
- const current2 = await readOrEmpty(target.file);
1082
- try {
1083
- const body = current2.trim() ? stripClaudeSettings(current2) : current2;
1084
- if (body !== current2) stripped.push({ target, body });
1085
- } catch {
1086
- }
1087
- }
1088
- const codexConfig = await isDir(path4.join(home, CODEX_DIR_REL)) ? await readOrEmpty(codexFile) : "";
1089
- let current = `~/${HOOK_SCRIPT_REL}`;
1090
- try {
1091
- await rm(path4.join(home, HOOK_SCRIPT_REL), { force: true });
1092
- current = `~/${HOOK_ENV_REL}`;
1093
- await rm(path4.join(home, HOOK_ENV_REL), { force: true });
1094
- for (const { target, body } of stripped) {
1095
- current = target.shown;
1096
- await writeAtomic(target.file, body, 420);
1097
- }
1098
- if (codexConfig.includes(HOOK_MARK)) {
1099
- current = `~/${CODEX_CONFIG_REL}`;
1100
- await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
1101
- }
1102
- } catch (err) {
1103
- throw fsFailure(err, current);
1104
- }
1105
- return { removed: true };
1106
- }
1107
-
1108
1219
  // src/rpc/hw.ts
1109
1220
  async function probe(_params) {
1110
1221
  const r = await sh(HARDWARE_SCRIPT, { timeoutMs: 14e3 });
@@ -1121,9 +1232,9 @@ async function pasteFile(params) {
1121
1232
  if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
1122
1233
  if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
1123
1234
  const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
1124
- const path11 = lines[lines.length - 1] ?? "";
1125
- if (!path11.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
1126
- return { path: path11 };
1235
+ const path12 = lines[lines.length - 1] ?? "";
1236
+ if (!path12.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
1237
+ return { path: path12 };
1127
1238
  }
1128
1239
 
1129
1240
  // src/rpc/tmux.ts
@@ -1203,19 +1314,19 @@ async function detect(_params) {
1203
1314
 
1204
1315
  // src/rpc/update.ts
1205
1316
  import { existsSync, realpathSync as realpathSync2 } from "fs";
1206
- import { readFile as readFile2 } from "fs/promises";
1207
- import path9 from "path";
1317
+ import { readFile as readFile3 } from "fs/promises";
1318
+ import path10 from "path";
1208
1319
 
1209
1320
  // src/paths.ts
1210
1321
  import { realpathSync } from "fs";
1211
- import path5 from "path";
1322
+ import path6 from "path";
1212
1323
  import { pathToFileURL } from "url";
1213
1324
  function resolveScriptPath(argv1) {
1214
1325
  if (!argv1) return "";
1215
1326
  try {
1216
1327
  return realpathSync(argv1);
1217
1328
  } catch {
1218
- return path5.resolve(argv1);
1329
+ return path6.resolve(argv1);
1219
1330
  }
1220
1331
  }
1221
1332
  function isMainModule(importMetaUrl, argv1) {
@@ -1224,12 +1335,12 @@ function isMainModule(importMetaUrl, argv1) {
1224
1335
  }
1225
1336
 
1226
1337
  // src/service/index.ts
1227
- import path8 from "path";
1338
+ import path9 from "path";
1228
1339
 
1229
1340
  // src/service/launchd.ts
1230
1341
  import fs4 from "fs";
1231
1342
  import os4 from "os";
1232
- import path6 from "path";
1343
+ import path7 from "path";
1233
1344
  var LABEL = "dev.termhub.agent";
1234
1345
  function renderPlist({ label, node, script, logPath }) {
1235
1346
  const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1268,7 +1379,7 @@ function renderPlist({ label, node, script, logPath }) {
1268
1379
  `;
1269
1380
  }
1270
1381
  function plistPath(home = os4.homedir()) {
1271
- return path6.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
1382
+ return path7.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
1272
1383
  }
1273
1384
  function gui() {
1274
1385
  return `gui/${process.getuid ? process.getuid() : 0}`;
@@ -1277,7 +1388,7 @@ async function install2(opts, deps = {}) {
1277
1388
  const runFn = deps.run ?? run;
1278
1389
  const file = plistPath(deps.home);
1279
1390
  const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
1280
- fs4.mkdirSync(path6.dirname(file), { recursive: true });
1391
+ fs4.mkdirSync(path7.dirname(file), { recursive: true });
1281
1392
  fs4.writeFileSync(file, plist, "utf8");
1282
1393
  await runFn("launchctl", ["bootout", gui(), file]);
1283
1394
  const result = await runFn("launchctl", ["bootstrap", gui(), file]);
@@ -1313,7 +1424,7 @@ async function status(deps = {}) {
1313
1424
  // src/service/systemd.ts
1314
1425
  import fs5 from "fs";
1315
1426
  import os5 from "os";
1316
- import path7 from "path";
1427
+ import path8 from "path";
1317
1428
  var UNIT_NAME = "termhub-agent";
1318
1429
  function renderUnit({ node, script, logPath, pathEnv }) {
1319
1430
  const lines = [
@@ -1336,13 +1447,13 @@ function renderUnit({ node, script, logPath, pathEnv }) {
1336
1447
  return lines.join("\n");
1337
1448
  }
1338
1449
  function unitPath(home = os5.homedir()) {
1339
- return path7.join(home, ".config", "systemd", "user", `${UNIT_NAME}.service`);
1450
+ return path8.join(home, ".config", "systemd", "user", `${UNIT_NAME}.service`);
1340
1451
  }
1341
1452
  async function install3(opts, deps = {}) {
1342
1453
  const runFn = deps.run ?? run;
1343
1454
  const file = unitPath(deps.home);
1344
1455
  const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1345
- fs5.mkdirSync(path7.dirname(file), { recursive: true });
1456
+ fs5.mkdirSync(path8.dirname(file), { recursive: true });
1346
1457
  fs5.writeFileSync(file, unit, "utf8");
1347
1458
  const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1348
1459
  if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
@@ -1391,7 +1502,7 @@ function serviceFileOptions() {
1391
1502
  return {
1392
1503
  node: process.execPath,
1393
1504
  script: resolveScriptPath(process.argv[1]),
1394
- logPath: path8.join(agentHome(), "agent.log")
1505
+ logPath: path9.join(agentHome(), "agent.log")
1395
1506
  };
1396
1507
  }
1397
1508
  function assertSupported(platform) {
@@ -1422,24 +1533,24 @@ async function status3() {
1422
1533
  }
1423
1534
 
1424
1535
  // src/version.ts
1425
- var AGENT_VERSION = "0.2.3";
1536
+ var AGENT_VERSION = "0.2.4";
1426
1537
 
1427
1538
  // src/rpc/update.ts
1428
1539
  var PACKAGE = "@termhub/agent";
1429
1540
  var NPM_TIMEOUT_MS = 15e4;
1430
1541
  var EXIT_DELAY_MS = 750;
1431
1542
  function npmCliBesideNode(execPath = process.execPath) {
1432
- const dir = path9.dirname(execPath);
1433
- const symlink = path9.join(dir, "npm");
1543
+ const dir = path10.dirname(execPath);
1544
+ const symlink = path10.join(dir, "npm");
1434
1545
  if (existsSync(symlink)) return realpathSync2(symlink);
1435
- const fallback = path9.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
1546
+ const fallback = path10.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
1436
1547
  return existsSync(fallback) ? fallback : null;
1437
1548
  }
1438
1549
  async function installedAgentVersion(argv1 = process.argv[1]) {
1439
1550
  const script = resolveScriptPath(argv1);
1440
1551
  if (!script) return null;
1441
1552
  try {
1442
- const pkg = JSON.parse(await readFile2(path9.join(path9.dirname(script), "..", "package.json"), "utf8"));
1553
+ const pkg = JSON.parse(await readFile3(path10.join(path10.dirname(script), "..", "package.json"), "utf8"));
1443
1554
  return typeof pkg.version === "string" ? pkg.version : null;
1444
1555
  } catch {
1445
1556
  return null;
@@ -1497,7 +1608,7 @@ var handlers = {
1497
1608
  "tools.detect": detect,
1498
1609
  "hw.probe": probe,
1499
1610
  "fs.list": list,
1500
- "fs.mkdir": mkdir,
1611
+ "fs.mkdir": mkdir2,
1501
1612
  "ai.credential": credential,
1502
1613
  "file.paste": pasteFile,
1503
1614
  "hooks.install": install,
@@ -1586,6 +1697,12 @@ async function runAgent(config, opts) {
1586
1697
  else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
1587
1698
  const pty = createPtyManager({ log: opts.log });
1588
1699
  const dispatch = createDispatcher({ handlers, pty, log: opts.log });
1700
+ const healHooks = () => {
1701
+ heal().then((dirs) => {
1702
+ if (dirs.length) opts.log("monitor hooks repaired", { dirs: dirs.length });
1703
+ }).catch((err) => opts.log("monitor hooks could not be repaired", { error: err instanceof Error ? err.message : String(err) }));
1704
+ };
1705
+ healHooks();
1589
1706
  try {
1590
1707
  await runForever(
1591
1708
  {
@@ -1594,6 +1711,7 @@ async function runAgent(config, opts) {
1594
1711
  hello,
1595
1712
  onServerMessage: dispatch,
1596
1713
  onStream: (ch, data) => pty.write(ch, data),
1714
+ onConnect: healHooks,
1597
1715
  onDisconnect: () => pty.closeAll(),
1598
1716
  log: opts.log
1599
1717
  },
@@ -1695,15 +1813,15 @@ function disconnectCommand() {
1695
1813
  // src/doctor.ts
1696
1814
  import fs6 from "fs";
1697
1815
  import os7 from "os";
1698
- import path10 from "path";
1816
+ import path11 from "path";
1699
1817
  var SERVER_CHECK_TIMEOUT_MS = 5e3;
1700
1818
  function defaultDoctorPaths() {
1701
1819
  const home = os7.homedir();
1702
- const paths = [home, path10.join(home, "Documents"), path10.join(home, "Desktop")];
1820
+ const paths = [home, path11.join(home, "Documents"), path11.join(home, "Desktop")];
1703
1821
  if (process.platform === "darwin") {
1704
1822
  try {
1705
1823
  for (const entry of fs6.readdirSync("/Volumes", { withFileTypes: true })) {
1706
- if (entry.isDirectory()) paths.push(path10.join("/Volumes", entry.name));
1824
+ if (entry.isDirectory()) paths.push(path11.join("/Volumes", entry.name));
1707
1825
  }
1708
1826
  } catch {
1709
1827
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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,