@termhub/agent 0.2.3 → 0.3.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 (2) hide show
  1. package/dist/cli.js +301 -159
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -56,8 +56,12 @@ var RPC = {
56
56
  "tmux.capture": def(z.object({ session: sessionName, lines: z.number().int().min(1).max(5e3) }), z.object({ text: z.string() })),
57
57
  /** Idempotent: creates the detached session in `cwd` when it is missing. `created` says whether it had to. */
58
58
  "tmux.ensure": def(z.object({ session: sessionName, cwd: machinePath }), z.object({ created: z.boolean() }), 1e4),
59
- /** Types `text` literally, then (with `enter`) presses Enter on its own after a short pause. */
60
- "tmux.sendText": def(z.object({ session: sessionName, text: z.string().max(TEXT_MAX_CHARS), enter: z.boolean() }), z.object({ sent: z.literal(true) }), 1e4),
59
+ /**
60
+ * Types `text` literally, then (with `enter`) presses Enter on its own after a short pause.
61
+ * `paste`: deliver `text` as a tmux buffer paste instead of typed keystrokes, so a TUI reads
62
+ * an embedded newline as part of the pasted text rather than as Enter (since agent 0.3.0).
63
+ */
64
+ "tmux.sendText": def(z.object({ session: sessionName, text: z.string().max(TEXT_MAX_CHARS), enter: z.boolean(), paste: z.boolean().optional() }), z.object({ sent: z.literal(true) }), 1e4),
61
65
  "tmux.sendKey": def(z.object({ session: sessionName, key: tmuxKey }), z.object({ sent: z.literal(true) }), 1e4),
62
66
  "tools.detect": def(z.object({}), z.object({ os: z.string().nullable(), tools: z.array(z.string().max(32)) })),
63
67
  "hw.probe": def(z.object({}), z.object({ stdout: z.string() }), 15e3),
@@ -211,6 +215,7 @@ function connectOnce(opts, signal) {
211
215
  };
212
216
  const hello = { type: "hello", protocol: PROTOCOL_VERSION, ...opts.hello };
213
217
  socket.sendControl(hello);
218
+ opts.onConnect?.();
214
219
  resolve({ socket, closed });
215
220
  });
216
221
  ws.on("message", (data) => {
@@ -376,9 +381,10 @@ function deleteConfig() {
376
381
  // src/run.ts
377
382
  import os6 from "os";
378
383
 
379
- // src/exec.ts
380
- import { execFile } from "child_process";
381
- import os2 from "os";
384
+ // src/rpc/hooks.ts
385
+ import { chmod, mkdir, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "fs/promises";
386
+ import os3 from "os";
387
+ import path3 from "path";
382
388
 
383
389
  // ../../packages/machine-ops/dist/shell.js
384
390
  function shellQuote(s) {
@@ -665,13 +671,105 @@ function stripCodexConfig(current) {
665
671
  return lines.join("\n");
666
672
  }
667
673
 
674
+ // ../../packages/machine-ops/dist/discover.js
675
+ var CLAUDE_MARKERS = ["settings.json", "projects", ".credentials.json"];
676
+ var CLAUDE_DIR_NAME = /^\.claude[A-Za-z0-9._-]*$/;
677
+ function claudeDirsFromHome(entries) {
678
+ const out = [];
679
+ for (const entry of entries) {
680
+ if (!CLAUDE_DIR_NAME.test(entry.name))
681
+ continue;
682
+ if (!entry.files.some((f) => CLAUDE_MARKERS.includes(f)))
683
+ continue;
684
+ const dir = `~/${entry.name}`;
685
+ if (!out.includes(dir))
686
+ out.push(dir);
687
+ }
688
+ return out;
689
+ }
690
+ var ASSIGNMENT = /CLAUDE_CONFIG_DIR=(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"';]*))/g;
691
+ var FISH_SET = /CLAUDE_CONFIG_DIR\s+(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"';]+))/g;
692
+ function normalize(raw) {
693
+ let value = raw.trim().replace(/\/+$/, "");
694
+ value = value.replace(/^\$\{?HOME\}?(?=\/|$)/, "~");
695
+ if (!value || value === "~" || value.includes("$") || /[\0\n\r]/.test(value))
696
+ return null;
697
+ if (!value.startsWith("~/") && !value.startsWith("/"))
698
+ value = `~/${value}`;
699
+ return value;
700
+ }
701
+ function configDirsFromRc(text) {
702
+ const out = [];
703
+ for (const line of text.split("\n")) {
704
+ if (/^\s*#/.test(line))
705
+ continue;
706
+ const pattern = /^\s*set\s/.test(line) ? FISH_SET : ASSIGNMENT;
707
+ pattern.lastIndex = 0;
708
+ for (let m = pattern.exec(line); m; m = pattern.exec(line)) {
709
+ const dir = normalize(m[1] ?? m[2] ?? m[3] ?? "");
710
+ if (dir && !out.includes(dir))
711
+ out.push(dir);
712
+ }
713
+ }
714
+ return out;
715
+ }
716
+
717
+ // src/claude-dirs.ts
718
+ import { readdir, readFile, stat } from "fs/promises";
719
+ import path2 from "path";
720
+ var RC_FILES = [".zshrc", ".bashrc", ".bash_profile", ".profile", ".config/fish/config.fish"];
721
+ async function readOrEmpty(file) {
722
+ try {
723
+ return await readFile(file, "utf8");
724
+ } catch {
725
+ return "";
726
+ }
727
+ }
728
+ async function isDir(dir) {
729
+ try {
730
+ return (await stat(dir)).isDirectory();
731
+ } catch {
732
+ return false;
733
+ }
734
+ }
735
+ async function fileNames(dir) {
736
+ try {
737
+ return await readdir(dir);
738
+ } catch {
739
+ return [];
740
+ }
741
+ }
742
+ async function candidateNames(home) {
743
+ try {
744
+ const list3 = await readdir(home, { withFileTypes: true });
745
+ return list3.filter((d) => d.isDirectory() && d.name.startsWith(".claude")).map((d) => d.name);
746
+ } catch {
747
+ return [];
748
+ }
749
+ }
750
+ async function discoverClaudeDirs(home) {
751
+ const entries = [];
752
+ for (const name of await candidateNames(home)) {
753
+ entries.push({ name, files: await fileNames(path2.join(home, name)) });
754
+ }
755
+ const dirs = claudeDirsFromHome(entries);
756
+ for (const rc of RC_FILES) {
757
+ for (const dir of configDirsFromRc(await readOrEmpty(path2.join(home, rc)))) {
758
+ if (!dirs.includes(dir) && await isDir(expandHome(dir, home))) dirs.push(dir);
759
+ }
760
+ }
761
+ return dirs;
762
+ }
763
+
668
764
  // src/exec.ts
765
+ import { execFile } from "child_process";
766
+ import os2 from "os";
669
767
  var DEFAULT_TIMEOUT_MS = 8e3;
670
768
  var RpcFailure = class extends Error {
671
- constructor(code, message, path11) {
769
+ constructor(code, message, path12) {
672
770
  super(message);
673
771
  this.code = code;
674
- this.path = path11;
772
+ this.path = path12;
675
773
  }
676
774
  code;
677
775
  path;
@@ -716,6 +814,138 @@ function sh(script, opts = {}) {
716
814
  return run("/bin/sh", ["-c", script], opts);
717
815
  }
718
816
 
817
+ // src/rpc/hooks.ts
818
+ var CODEX_DIR_REL = ".codex";
819
+ var CODEX_CONFIG_REL = ".codex/config.toml";
820
+ var isEnoent = (err) => err?.code === "ENOENT";
821
+ async function readOrEmpty2(file) {
822
+ try {
823
+ return await readFile2(file, "utf8");
824
+ } catch (err) {
825
+ if (isEnoent(err)) return "";
826
+ throw err;
827
+ }
828
+ }
829
+ async function isDir2(dir) {
830
+ try {
831
+ return (await stat2(dir)).isDirectory();
832
+ } catch {
833
+ return false;
834
+ }
835
+ }
836
+ async function writeAtomic(file, body, mode) {
837
+ const tmp = `${file}.termhub-new`;
838
+ await writeFile(tmp, body, { encoding: "utf8", mode });
839
+ await chmod(tmp, mode);
840
+ await rename(tmp, file);
841
+ }
842
+ var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
843
+ function fsFailure(err, shown) {
844
+ const code = err?.code;
845
+ if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
846
+ return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
847
+ }
848
+ async function claudeTargets(dirs, home) {
849
+ const out = [];
850
+ for (const d of claudeConfigDirs([...dirs ?? [], ...await discoverClaudeDirs(home)])) {
851
+ const dir = expandHome(d, home);
852
+ if (d !== CLAUDE_DEFAULT_DIR && !await isDir2(dir)) continue;
853
+ out.push({ dir, file: path3.join(dir, "settings.json"), shown: `${d}/settings.json` });
854
+ }
855
+ return out;
856
+ }
857
+ async function install(params, home = os3.homedir()) {
858
+ const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
859
+ const codexFile = path3.join(home, CODEX_CONFIG_REL);
860
+ const targets = await claudeTargets(params.claude_dirs, home);
861
+ const merged = [];
862
+ for (const target of targets) {
863
+ try {
864
+ merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath) });
865
+ } catch (err) {
866
+ 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);
867
+ throw new RpcFailure("failed", message, relPath(target.shown));
868
+ }
869
+ }
870
+ const hasCodex = await isDir2(path3.join(home, CODEX_DIR_REL));
871
+ const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty2(codexFile), scriptPath) : null;
872
+ let current = `~/${HOOK_SCRIPT_REL}`;
873
+ try {
874
+ await mkdir(path3.dirname(scriptPath), { recursive: true });
875
+ current = `~/${HOOK_ENV_REL}`;
876
+ await writeAtomic(path3.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
877
+ current = `~/${HOOK_SCRIPT_REL}`;
878
+ await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
879
+ for (const { target, body } of merged) {
880
+ current = target.shown;
881
+ await mkdir(target.dir, { recursive: true });
882
+ await writeAtomic(target.file, body, 420);
883
+ }
884
+ if (mergedCodex !== null) {
885
+ current = `~/${CODEX_CONFIG_REL}`;
886
+ await writeAtomic(codexFile, mergedCodex, 420);
887
+ }
888
+ } catch (err) {
889
+ throw fsFailure(err, current);
890
+ }
891
+ return {
892
+ home,
893
+ claude: "installed",
894
+ codex: mergedCodex !== null ? "installed" : "skipped",
895
+ claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
896
+ };
897
+ }
898
+ async function heal(home = os3.homedir()) {
899
+ const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
900
+ const env = await readOrEmpty2(path3.join(home, HOOK_ENV_REL));
901
+ if (!env.trim() || !await readOrEmpty2(scriptPath)) return [];
902
+ const healed = [];
903
+ for (const dir of await discoverClaudeDirs(home)) {
904
+ const file = path3.join(expandHome(dir, home), "settings.json");
905
+ const current = await readOrEmpty2(file);
906
+ let body;
907
+ try {
908
+ body = mergeClaudeSettings(current, scriptPath);
909
+ } catch {
910
+ continue;
911
+ }
912
+ if (body === current) continue;
913
+ await writeAtomic(file, body, 420);
914
+ healed.push(dir);
915
+ }
916
+ return healed;
917
+ }
918
+ async function uninstall(params, home = os3.homedir()) {
919
+ const codexFile = path3.join(home, CODEX_CONFIG_REL);
920
+ const stripped = [];
921
+ for (const target of await claudeTargets(params.claude_dirs, home)) {
922
+ const current2 = await readOrEmpty2(target.file);
923
+ try {
924
+ const body = current2.trim() ? stripClaudeSettings(current2) : current2;
925
+ if (body !== current2) stripped.push({ target, body });
926
+ } catch {
927
+ }
928
+ }
929
+ const codexConfig = await isDir2(path3.join(home, CODEX_DIR_REL)) ? await readOrEmpty2(codexFile) : "";
930
+ let current = `~/${HOOK_SCRIPT_REL}`;
931
+ try {
932
+ await rm(path3.join(home, HOOK_SCRIPT_REL), { force: true });
933
+ current = `~/${HOOK_ENV_REL}`;
934
+ await rm(path3.join(home, HOOK_ENV_REL), { force: true });
935
+ for (const { target, body } of stripped) {
936
+ current = target.shown;
937
+ await writeAtomic(target.file, body, 420);
938
+ }
939
+ if (codexConfig.includes(HOOK_MARK)) {
940
+ current = `~/${CODEX_CONFIG_REL}`;
941
+ await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
942
+ }
943
+ } catch (err) {
944
+ throw fsFailure(err, current);
945
+ }
946
+ return { removed: true };
947
+ }
948
+
719
949
  // src/dispatch.ts
720
950
  async function handleRpc(msg, socket, handlers2, log2) {
721
951
  const method = msg.method;
@@ -773,11 +1003,11 @@ function createDispatcher(deps) {
773
1003
 
774
1004
  // src/pty.ts
775
1005
  import fs3 from "fs";
776
- import path3 from "path";
1006
+ import path5 from "path";
777
1007
 
778
1008
  // src/pty-health.ts
779
1009
  import fs2 from "fs";
780
- import path2 from "path";
1010
+ import path4 from "path";
781
1011
  import { createRequire } from "module";
782
1012
  function findSpawnHelper(resolveFrom = import.meta.url) {
783
1013
  let pkgJson;
@@ -786,10 +1016,10 @@ function findSpawnHelper(resolveFrom = import.meta.url) {
786
1016
  } catch {
787
1017
  return null;
788
1018
  }
789
- const root = path2.dirname(pkgJson);
1019
+ const root = path4.dirname(pkgJson);
790
1020
  const candidates = [
791
- path2.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
792
- path2.join(root, "build", "Release", "spawn-helper")
1021
+ path4.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
1022
+ path4.join(root, "build", "Release", "spawn-helper")
793
1023
  ];
794
1024
  return candidates.find((p) => fs2.existsSync(p)) ?? null;
795
1025
  }
@@ -809,7 +1039,7 @@ function ensureSpawnHelperExecutable(helper = findSpawnHelper()) {
809
1039
  // src/pty.ts
810
1040
  function expandHome2(rawCwd, home) {
811
1041
  if (rawCwd === "~") return home;
812
- if (rawCwd.startsWith("~/")) return path3.join(home, rawCwd.slice(2));
1042
+ if (rawCwd.startsWith("~/")) return path5.join(home, rawCwd.slice(2));
813
1043
  return rawCwd;
814
1044
  }
815
1045
  function existsDir(p) {
@@ -827,7 +1057,7 @@ function resolveCwd(rawCwd) {
827
1057
  function isSpawnHelperFailure(err) {
828
1058
  return /posix_spawnp failed/.test(err instanceof Error ? err.message : String(err));
829
1059
  }
830
- function isEnoent(err) {
1060
+ function isEnoent2(err) {
831
1061
  const e = err;
832
1062
  if (e?.code === "ENOENT") return true;
833
1063
  const message = e instanceof Error ? e.message : String(err);
@@ -878,7 +1108,7 @@ function createPtyManager(deps) {
878
1108
  socket.sendControl({
879
1109
  type: "open_error",
880
1110
  ch,
881
- error: isEnoent(err) ? { code: "no_tmux", message: "tmux not found" } : { code: "internal", message: "failed to start pty" }
1111
+ error: isEnoent2(err) ? { code: "no_tmux", message: "tmux not found" } : { code: "internal", message: "failed to start pty" }
882
1112
  });
883
1113
  return;
884
1114
  }
@@ -983,128 +1213,13 @@ async function list(params) {
983
1213
  if (failure) throw failure;
984
1214
  return { stdout: r.stdout };
985
1215
  }
986
- async function mkdir(params) {
1216
+ async function mkdir2(params) {
987
1217
  const r = await sh(buildMkdirScript(shellQuote(params.parent), shellQuote(params.name), { recursive: params.recursive === true }));
988
1218
  const failure = processFailure("fs.mkdir", r);
989
1219
  if (failure) throw failure;
990
1220
  return { stdout: r.stdout };
991
1221
  }
992
1222
 
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
1223
  // src/rpc/hw.ts
1109
1224
  async function probe(_params) {
1110
1225
  const r = await sh(HARDWARE_SCRIPT, { timeoutMs: 14e3 });
@@ -1121,12 +1236,13 @@ async function pasteFile(params) {
1121
1236
  if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
1122
1237
  if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
1123
1238
  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 };
1239
+ const path12 = lines[lines.length - 1] ?? "";
1240
+ if (!path12.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
1241
+ return { path: path12 };
1127
1242
  }
1128
1243
 
1129
1244
  // src/rpc/tmux.ts
1245
+ import { randomUUID } from "crypto";
1130
1246
  var ENTER_PAUSE_MS = 300;
1131
1247
  function processFailure2(r) {
1132
1248
  if (r.timedOut) return new RpcFailure("timeout", "tmux timed out");
@@ -1168,12 +1284,31 @@ async function ensure(params) {
1168
1284
  if (made.code !== 0) throw new RpcFailure("failed", why(made.stderr, "tmux new-session falhou"), params.cwd);
1169
1285
  return { created: true };
1170
1286
  }
1287
+ async function pasteText(session, text) {
1288
+ const bufferName = `termhub-paste-${randomUUID()}`;
1289
+ try {
1290
+ const loaded = await run(tmuxPath(), ["load-buffer", "-b", bufferName, "-"], { input: Buffer.from(text, "utf8") });
1291
+ const loadFailure = processFailure2(loaded);
1292
+ if (loadFailure) throw loadFailure;
1293
+ if (loaded.code !== 0) throw new RpcFailure("internal", why(loaded.stderr, "tmux load-buffer failed"));
1294
+ const pasted = await run(tmuxPath(), ["paste-buffer", "-p", "-d", "-b", bufferName, "-t", pane(session)]);
1295
+ const pasteFailure = processFailure2(pasted);
1296
+ if (pasteFailure) throw pasteFailure;
1297
+ if (pasted.code !== 0) throw new RpcFailure("notfound", why(pasted.stderr, "session not found"));
1298
+ } finally {
1299
+ await run(tmuxPath(), ["delete-buffer", "-b", bufferName]);
1300
+ }
1301
+ }
1171
1302
  async function sendText(params) {
1172
1303
  if (params.text) {
1173
- const typed = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "-l", "--", params.text]);
1174
- const failure = processFailure2(typed);
1175
- if (failure) throw failure;
1176
- if (typed.code !== 0) throw new RpcFailure("notfound", why(typed.stderr, "session not found"));
1304
+ if (params.paste) {
1305
+ await pasteText(params.session, params.text);
1306
+ } else {
1307
+ const typed = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "-l", "--", params.text]);
1308
+ const failure = processFailure2(typed);
1309
+ if (failure) throw failure;
1310
+ if (typed.code !== 0) throw new RpcFailure("notfound", why(typed.stderr, "session not found"));
1311
+ }
1177
1312
  if (params.enter) await new Promise((r) => setTimeout(r, ENTER_PAUSE_MS));
1178
1313
  }
1179
1314
  if (params.enter) {
@@ -1203,19 +1338,19 @@ async function detect(_params) {
1203
1338
 
1204
1339
  // src/rpc/update.ts
1205
1340
  import { existsSync, realpathSync as realpathSync2 } from "fs";
1206
- import { readFile as readFile2 } from "fs/promises";
1207
- import path9 from "path";
1341
+ import { readFile as readFile3 } from "fs/promises";
1342
+ import path10 from "path";
1208
1343
 
1209
1344
  // src/paths.ts
1210
1345
  import { realpathSync } from "fs";
1211
- import path5 from "path";
1346
+ import path6 from "path";
1212
1347
  import { pathToFileURL } from "url";
1213
1348
  function resolveScriptPath(argv1) {
1214
1349
  if (!argv1) return "";
1215
1350
  try {
1216
1351
  return realpathSync(argv1);
1217
1352
  } catch {
1218
- return path5.resolve(argv1);
1353
+ return path6.resolve(argv1);
1219
1354
  }
1220
1355
  }
1221
1356
  function isMainModule(importMetaUrl, argv1) {
@@ -1224,12 +1359,12 @@ function isMainModule(importMetaUrl, argv1) {
1224
1359
  }
1225
1360
 
1226
1361
  // src/service/index.ts
1227
- import path8 from "path";
1362
+ import path9 from "path";
1228
1363
 
1229
1364
  // src/service/launchd.ts
1230
1365
  import fs4 from "fs";
1231
1366
  import os4 from "os";
1232
- import path6 from "path";
1367
+ import path7 from "path";
1233
1368
  var LABEL = "dev.termhub.agent";
1234
1369
  function renderPlist({ label, node, script, logPath }) {
1235
1370
  const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1268,7 +1403,7 @@ function renderPlist({ label, node, script, logPath }) {
1268
1403
  `;
1269
1404
  }
1270
1405
  function plistPath(home = os4.homedir()) {
1271
- return path6.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
1406
+ return path7.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
1272
1407
  }
1273
1408
  function gui() {
1274
1409
  return `gui/${process.getuid ? process.getuid() : 0}`;
@@ -1277,7 +1412,7 @@ async function install2(opts, deps = {}) {
1277
1412
  const runFn = deps.run ?? run;
1278
1413
  const file = plistPath(deps.home);
1279
1414
  const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
1280
- fs4.mkdirSync(path6.dirname(file), { recursive: true });
1415
+ fs4.mkdirSync(path7.dirname(file), { recursive: true });
1281
1416
  fs4.writeFileSync(file, plist, "utf8");
1282
1417
  await runFn("launchctl", ["bootout", gui(), file]);
1283
1418
  const result = await runFn("launchctl", ["bootstrap", gui(), file]);
@@ -1313,7 +1448,7 @@ async function status(deps = {}) {
1313
1448
  // src/service/systemd.ts
1314
1449
  import fs5 from "fs";
1315
1450
  import os5 from "os";
1316
- import path7 from "path";
1451
+ import path8 from "path";
1317
1452
  var UNIT_NAME = "termhub-agent";
1318
1453
  function renderUnit({ node, script, logPath, pathEnv }) {
1319
1454
  const lines = [
@@ -1336,13 +1471,13 @@ function renderUnit({ node, script, logPath, pathEnv }) {
1336
1471
  return lines.join("\n");
1337
1472
  }
1338
1473
  function unitPath(home = os5.homedir()) {
1339
- return path7.join(home, ".config", "systemd", "user", `${UNIT_NAME}.service`);
1474
+ return path8.join(home, ".config", "systemd", "user", `${UNIT_NAME}.service`);
1340
1475
  }
1341
1476
  async function install3(opts, deps = {}) {
1342
1477
  const runFn = deps.run ?? run;
1343
1478
  const file = unitPath(deps.home);
1344
1479
  const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1345
- fs5.mkdirSync(path7.dirname(file), { recursive: true });
1480
+ fs5.mkdirSync(path8.dirname(file), { recursive: true });
1346
1481
  fs5.writeFileSync(file, unit, "utf8");
1347
1482
  const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1348
1483
  if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
@@ -1391,7 +1526,7 @@ function serviceFileOptions() {
1391
1526
  return {
1392
1527
  node: process.execPath,
1393
1528
  script: resolveScriptPath(process.argv[1]),
1394
- logPath: path8.join(agentHome(), "agent.log")
1529
+ logPath: path9.join(agentHome(), "agent.log")
1395
1530
  };
1396
1531
  }
1397
1532
  function assertSupported(platform) {
@@ -1422,24 +1557,24 @@ async function status3() {
1422
1557
  }
1423
1558
 
1424
1559
  // src/version.ts
1425
- var AGENT_VERSION = "0.2.3";
1560
+ var AGENT_VERSION = "0.3.0";
1426
1561
 
1427
1562
  // src/rpc/update.ts
1428
1563
  var PACKAGE = "@termhub/agent";
1429
1564
  var NPM_TIMEOUT_MS = 15e4;
1430
1565
  var EXIT_DELAY_MS = 750;
1431
1566
  function npmCliBesideNode(execPath = process.execPath) {
1432
- const dir = path9.dirname(execPath);
1433
- const symlink = path9.join(dir, "npm");
1567
+ const dir = path10.dirname(execPath);
1568
+ const symlink = path10.join(dir, "npm");
1434
1569
  if (existsSync(symlink)) return realpathSync2(symlink);
1435
- const fallback = path9.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
1570
+ const fallback = path10.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
1436
1571
  return existsSync(fallback) ? fallback : null;
1437
1572
  }
1438
1573
  async function installedAgentVersion(argv1 = process.argv[1]) {
1439
1574
  const script = resolveScriptPath(argv1);
1440
1575
  if (!script) return null;
1441
1576
  try {
1442
- const pkg = JSON.parse(await readFile2(path9.join(path9.dirname(script), "..", "package.json"), "utf8"));
1577
+ const pkg = JSON.parse(await readFile3(path10.join(path10.dirname(script), "..", "package.json"), "utf8"));
1443
1578
  return typeof pkg.version === "string" ? pkg.version : null;
1444
1579
  } catch {
1445
1580
  return null;
@@ -1497,7 +1632,7 @@ var handlers = {
1497
1632
  "tools.detect": detect,
1498
1633
  "hw.probe": probe,
1499
1634
  "fs.list": list,
1500
- "fs.mkdir": mkdir,
1635
+ "fs.mkdir": mkdir2,
1501
1636
  "ai.credential": credential,
1502
1637
  "file.paste": pasteFile,
1503
1638
  "hooks.install": install,
@@ -1586,6 +1721,12 @@ async function runAgent(config, opts) {
1586
1721
  else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
1587
1722
  const pty = createPtyManager({ log: opts.log });
1588
1723
  const dispatch = createDispatcher({ handlers, pty, log: opts.log });
1724
+ const healHooks = () => {
1725
+ heal().then((dirs) => {
1726
+ if (dirs.length) opts.log("monitor hooks repaired", { dirs: dirs.length });
1727
+ }).catch((err) => opts.log("monitor hooks could not be repaired", { error: err instanceof Error ? err.message : String(err) }));
1728
+ };
1729
+ healHooks();
1589
1730
  try {
1590
1731
  await runForever(
1591
1732
  {
@@ -1594,6 +1735,7 @@ async function runAgent(config, opts) {
1594
1735
  hello,
1595
1736
  onServerMessage: dispatch,
1596
1737
  onStream: (ch, data) => pty.write(ch, data),
1738
+ onConnect: healHooks,
1597
1739
  onDisconnect: () => pty.closeAll(),
1598
1740
  log: opts.log
1599
1741
  },
@@ -1695,15 +1837,15 @@ function disconnectCommand() {
1695
1837
  // src/doctor.ts
1696
1838
  import fs6 from "fs";
1697
1839
  import os7 from "os";
1698
- import path10 from "path";
1840
+ import path11 from "path";
1699
1841
  var SERVER_CHECK_TIMEOUT_MS = 5e3;
1700
1842
  function defaultDoctorPaths() {
1701
1843
  const home = os7.homedir();
1702
- const paths = [home, path10.join(home, "Documents"), path10.join(home, "Desktop")];
1844
+ const paths = [home, path11.join(home, "Documents"), path11.join(home, "Desktop")];
1703
1845
  if (process.platform === "darwin") {
1704
1846
  try {
1705
1847
  for (const entry of fs6.readdirSync("/Volumes", { withFileTypes: true })) {
1706
- if (entry.isDirectory()) paths.push(path10.join("/Volumes", entry.name));
1848
+ if (entry.isDirectory()) paths.push(path11.join("/Volumes", entry.name));
1707
1849
  }
1708
1850
  } catch {
1709
1851
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
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,