@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.
- package/dist/cli.js +301 -159
- 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
|
-
/**
|
|
60
|
-
|
|
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/
|
|
380
|
-
import {
|
|
381
|
-
import
|
|
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,
|
|
769
|
+
constructor(code, message, path12) {
|
|
672
770
|
super(message);
|
|
673
771
|
this.code = code;
|
|
674
|
-
this.path =
|
|
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
|
|
1006
|
+
import path5 from "path";
|
|
777
1007
|
|
|
778
1008
|
// src/pty-health.ts
|
|
779
1009
|
import fs2 from "fs";
|
|
780
|
-
import
|
|
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 =
|
|
1019
|
+
const root = path4.dirname(pkgJson);
|
|
790
1020
|
const candidates = [
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
1125
|
-
if (!
|
|
1126
|
-
return { path:
|
|
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
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1207
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1268,7 +1403,7 @@ function renderPlist({ label, node, script, logPath }) {
|
|
|
1268
1403
|
`;
|
|
1269
1404
|
}
|
|
1270
1405
|
function plistPath(home = os4.homedir()) {
|
|
1271
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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.
|
|
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 =
|
|
1433
|
-
const symlink =
|
|
1567
|
+
const dir = path10.dirname(execPath);
|
|
1568
|
+
const symlink = path10.join(dir, "npm");
|
|
1434
1569
|
if (existsSync(symlink)) return realpathSync2(symlink);
|
|
1435
|
-
const fallback =
|
|
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
|
|
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":
|
|
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
|
|
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,
|
|
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(
|
|
1848
|
+
if (entry.isDirectory()) paths.push(path11.join("/Volumes", entry.name));
|
|
1707
1849
|
}
|
|
1708
1850
|
} catch {
|
|
1709
1851
|
}
|
package/package.json
CHANGED