@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.
- package/dist/cli.js +271 -153
- 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/
|
|
380
|
-
import {
|
|
381
|
-
import
|
|
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,
|
|
765
|
+
constructor(code, message, path12) {
|
|
672
766
|
super(message);
|
|
673
767
|
this.code = code;
|
|
674
|
-
this.path =
|
|
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
|
|
1002
|
+
import path5 from "path";
|
|
777
1003
|
|
|
778
1004
|
// src/pty-health.ts
|
|
779
1005
|
import fs2 from "fs";
|
|
780
|
-
import
|
|
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 =
|
|
1015
|
+
const root = path4.dirname(pkgJson);
|
|
790
1016
|
const candidates = [
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
1125
|
-
if (!
|
|
1126
|
-
return { path:
|
|
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
|
|
1207
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1268,7 +1379,7 @@ function renderPlist({ label, node, script, logPath }) {
|
|
|
1268
1379
|
`;
|
|
1269
1380
|
}
|
|
1270
1381
|
function plistPath(home = os4.homedir()) {
|
|
1271
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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.
|
|
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 =
|
|
1433
|
-
const symlink =
|
|
1543
|
+
const dir = path10.dirname(execPath);
|
|
1544
|
+
const symlink = path10.join(dir, "npm");
|
|
1434
1545
|
if (existsSync(symlink)) return realpathSync2(symlink);
|
|
1435
|
-
const fallback =
|
|
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
|
|
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":
|
|
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
|
|
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,
|
|
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(
|
|
1824
|
+
if (entry.isDirectory()) paths.push(path11.join("/Volumes", entry.name));
|
|
1707
1825
|
}
|
|
1708
1826
|
} catch {
|
|
1709
1827
|
}
|
package/package.json
CHANGED