@termhub/agent 0.1.4 → 0.1.6
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 +128 -72
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -63,8 +63,13 @@ var RPC = {
|
|
|
63
63
|
"ai.credential": def(z.object({ provider: aiProvider, config_dir: machinePath.nullable() }), z.object({ stdout: z.string() }), 1e4),
|
|
64
64
|
"file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4),
|
|
65
65
|
/** Monitor hooks (see @termhub/machine-ops hooks.ts): the agent writes the script, env and config entries under its own $HOME. */
|
|
66
|
-
|
|
67
|
-
"hooks.
|
|
66
|
+
/** `claude_dirs`: Claude config dirs besides ~/.claude (accounts with CLAUDE_CONFIG_DIR), hooked when they exist; since agent 0.1.5. */
|
|
67
|
+
"hooks.install": def(z.object({
|
|
68
|
+
hooks_url: z.string().min(1).max(2048).regex(/^https?:\/\/[^\s'"]+$/),
|
|
69
|
+
token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/),
|
|
70
|
+
claude_dirs: z.array(machinePath).max(16).optional()
|
|
71
|
+
}), z.object({ home: z.string(), claude: z.enum(["installed", "skipped"]), codex: z.enum(["installed", "skipped"]), claude_dirs: z.array(z.string()).optional() }), 15e3),
|
|
72
|
+
"hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3)
|
|
68
73
|
};
|
|
69
74
|
var RPC_METHODS = Object.keys(RPC);
|
|
70
75
|
var rpcMethod = z.enum(RPC_METHODS);
|
|
@@ -534,6 +539,23 @@ function ptyEnv(base, shell) {
|
|
|
534
539
|
var HOOK_SCRIPT_REL = ".termhub/bin/termhub-hook";
|
|
535
540
|
var HOOK_ENV_REL = ".termhub/hook.env";
|
|
536
541
|
var HOOK_MARK = "termhub-hook";
|
|
542
|
+
var CLAUDE_DEFAULT_DIR = "~/.claude";
|
|
543
|
+
function claudeConfigDirs(accountDirs) {
|
|
544
|
+
const out = [CLAUDE_DEFAULT_DIR];
|
|
545
|
+
for (const raw of accountDirs) {
|
|
546
|
+
let d = (raw ?? "").trim().replace(/\/+$/, "");
|
|
547
|
+
if (!d || d === "~" || /[\0\n\r]/.test(d))
|
|
548
|
+
continue;
|
|
549
|
+
if (!d.startsWith("~/") && !d.startsWith("/"))
|
|
550
|
+
d = `~/${d}`;
|
|
551
|
+
if (!out.includes(d))
|
|
552
|
+
out.push(d);
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
function expandHome(dir, home) {
|
|
557
|
+
return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
|
|
558
|
+
}
|
|
537
559
|
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Notification", "Stop", "SessionEnd"];
|
|
538
560
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
539
561
|
# termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex hook events to
|
|
@@ -728,25 +750,61 @@ function createDispatcher(deps) {
|
|
|
728
750
|
}
|
|
729
751
|
|
|
730
752
|
// src/pty.ts
|
|
753
|
+
import fs3 from "fs";
|
|
754
|
+
import path3 from "path";
|
|
755
|
+
|
|
756
|
+
// src/pty-health.ts
|
|
731
757
|
import fs2 from "fs";
|
|
732
758
|
import path2 from "path";
|
|
733
|
-
|
|
759
|
+
import { createRequire } from "module";
|
|
760
|
+
function findSpawnHelper(resolveFrom = import.meta.url) {
|
|
761
|
+
let pkgJson;
|
|
762
|
+
try {
|
|
763
|
+
pkgJson = createRequire(resolveFrom).resolve("node-pty/package.json");
|
|
764
|
+
} catch {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const root = path2.dirname(pkgJson);
|
|
768
|
+
const candidates = [
|
|
769
|
+
path2.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
|
|
770
|
+
path2.join(root, "build", "Release", "spawn-helper")
|
|
771
|
+
];
|
|
772
|
+
return candidates.find((p) => fs2.existsSync(p)) ?? null;
|
|
773
|
+
}
|
|
774
|
+
var EXEC_BITS = 73;
|
|
775
|
+
function ensureSpawnHelperExecutable(helper = findSpawnHelper()) {
|
|
776
|
+
if (!helper) return { path: null, executable: true, repaired: false };
|
|
777
|
+
try {
|
|
778
|
+
const mode = fs2.statSync(helper).mode;
|
|
779
|
+
if (mode & EXEC_BITS) return { path: helper, executable: true, repaired: false };
|
|
780
|
+
fs2.chmodSync(helper, (mode | 493) & 4095);
|
|
781
|
+
return { path: helper, executable: true, repaired: true };
|
|
782
|
+
} catch (err) {
|
|
783
|
+
return { path: helper, executable: false, repaired: false, error: err instanceof Error ? err.message : String(err) };
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/pty.ts
|
|
788
|
+
function expandHome2(rawCwd, home) {
|
|
734
789
|
if (rawCwd === "~") return home;
|
|
735
|
-
if (rawCwd.startsWith("~/")) return
|
|
790
|
+
if (rawCwd.startsWith("~/")) return path3.join(home, rawCwd.slice(2));
|
|
736
791
|
return rawCwd;
|
|
737
792
|
}
|
|
738
793
|
function existsDir(p) {
|
|
739
794
|
try {
|
|
740
|
-
return
|
|
795
|
+
return fs3.statSync(p).isDirectory();
|
|
741
796
|
} catch {
|
|
742
797
|
return false;
|
|
743
798
|
}
|
|
744
799
|
}
|
|
745
800
|
function resolveCwd(rawCwd) {
|
|
746
801
|
const home = process.env.HOME || "/";
|
|
747
|
-
const expanded =
|
|
802
|
+
const expanded = expandHome2(rawCwd, home);
|
|
748
803
|
return existsDir(expanded) ? expanded : home;
|
|
749
804
|
}
|
|
805
|
+
function isSpawnHelperFailure(err) {
|
|
806
|
+
return /posix_spawnp failed/.test(err instanceof Error ? err.message : String(err));
|
|
807
|
+
}
|
|
750
808
|
function isEnoent(err) {
|
|
751
809
|
const e = err;
|
|
752
810
|
if (e?.code === "ENOENT") return true;
|
|
@@ -756,6 +814,7 @@ function isEnoent(err) {
|
|
|
756
814
|
function createPtyManager(deps) {
|
|
757
815
|
const procs = /* @__PURE__ */ new Map();
|
|
758
816
|
const tmux = deps.tmuxPath ?? tmuxPath();
|
|
817
|
+
const repairSpawnHelper = deps.repairSpawnHelper ?? (() => ensureSpawnHelperExecutable());
|
|
759
818
|
let spawnFn = deps.spawn;
|
|
760
819
|
async function resolveSpawn() {
|
|
761
820
|
if (!spawnFn) {
|
|
@@ -782,7 +841,16 @@ function createPtyManager(deps) {
|
|
|
782
841
|
TERMHUB_SESSION: params.session
|
|
783
842
|
};
|
|
784
843
|
const spawn = await resolveSpawn();
|
|
785
|
-
|
|
844
|
+
const spawnTmux = () => spawn(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
|
|
845
|
+
try {
|
|
846
|
+
proc = spawnTmux();
|
|
847
|
+
} catch (err) {
|
|
848
|
+
if (!isSpawnHelperFailure(err)) throw err;
|
|
849
|
+
const helper = repairSpawnHelper();
|
|
850
|
+
if (!helper.repaired) throw err;
|
|
851
|
+
deps.log("spawn-helper exec bit repaired on open", { path: helper.path });
|
|
852
|
+
proc = spawnTmux();
|
|
853
|
+
}
|
|
786
854
|
} catch (err) {
|
|
787
855
|
deps.log("pty open failed", { ch, session: params.session, tmux, cwd: params.cwd, error: err instanceof Error ? err.message : String(err) });
|
|
788
856
|
socket.sendControl({
|
|
@@ -866,37 +934,6 @@ function createPtyManager(deps) {
|
|
|
866
934
|
};
|
|
867
935
|
}
|
|
868
936
|
|
|
869
|
-
// src/pty-health.ts
|
|
870
|
-
import fs3 from "fs";
|
|
871
|
-
import path3 from "path";
|
|
872
|
-
import { createRequire } from "module";
|
|
873
|
-
function findSpawnHelper(resolveFrom = import.meta.url) {
|
|
874
|
-
let pkgJson;
|
|
875
|
-
try {
|
|
876
|
-
pkgJson = createRequire(resolveFrom).resolve("node-pty/package.json");
|
|
877
|
-
} catch {
|
|
878
|
-
return null;
|
|
879
|
-
}
|
|
880
|
-
const root = path3.dirname(pkgJson);
|
|
881
|
-
const candidates = [
|
|
882
|
-
path3.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
|
|
883
|
-
path3.join(root, "build", "Release", "spawn-helper")
|
|
884
|
-
];
|
|
885
|
-
return candidates.find((p) => fs3.existsSync(p)) ?? null;
|
|
886
|
-
}
|
|
887
|
-
var EXEC_BITS = 73;
|
|
888
|
-
function ensureSpawnHelperExecutable(helper = findSpawnHelper()) {
|
|
889
|
-
if (!helper) return { path: null, executable: true, repaired: false };
|
|
890
|
-
try {
|
|
891
|
-
const mode = fs3.statSync(helper).mode;
|
|
892
|
-
if (mode & EXEC_BITS) return { path: helper, executable: true, repaired: false };
|
|
893
|
-
fs3.chmodSync(helper, (mode | 493) & 4095);
|
|
894
|
-
return { path: helper, executable: true, repaired: true };
|
|
895
|
-
} catch (err) {
|
|
896
|
-
return { path: helper, executable: false, repaired: false, error: err instanceof Error ? err.message : String(err) };
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
937
|
// src/rpc/ai.ts
|
|
901
938
|
async function credential(params) {
|
|
902
939
|
let prefix;
|
|
@@ -935,7 +972,6 @@ async function mkdir(params) {
|
|
|
935
972
|
import { chmod, mkdir as mkdir2, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
936
973
|
import os3 from "os";
|
|
937
974
|
import path4 from "path";
|
|
938
|
-
var CLAUDE_SETTINGS_REL = ".claude/settings.json";
|
|
939
975
|
var CODEX_DIR_REL = ".codex";
|
|
940
976
|
var CODEX_CONFIG_REL = ".codex/config.toml";
|
|
941
977
|
var isEnoent2 = (err) => err?.code === "ENOENT";
|
|
@@ -960,65 +996,85 @@ async function writeAtomic(file, body, mode) {
|
|
|
960
996
|
await chmod(tmp, mode);
|
|
961
997
|
await rename(tmp, file);
|
|
962
998
|
}
|
|
963
|
-
|
|
999
|
+
var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
|
|
1000
|
+
function fsFailure(err, shown) {
|
|
964
1001
|
const code = err?.code;
|
|
965
|
-
if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em
|
|
966
|
-
return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever
|
|
1002
|
+
if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
|
|
1003
|
+
return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
|
|
1004
|
+
}
|
|
1005
|
+
async function claudeTargets(dirs, home) {
|
|
1006
|
+
const out = [];
|
|
1007
|
+
for (const d of claudeConfigDirs(dirs ?? [])) {
|
|
1008
|
+
const dir = expandHome(d, home);
|
|
1009
|
+
if (d !== CLAUDE_DEFAULT_DIR && !await isDir(dir)) continue;
|
|
1010
|
+
out.push({ dir, file: path4.join(dir, "settings.json"), shown: `${d}/settings.json` });
|
|
1011
|
+
}
|
|
1012
|
+
return out;
|
|
967
1013
|
}
|
|
968
1014
|
async function install(params, home = os3.homedir()) {
|
|
969
1015
|
const scriptPath = path4.join(home, HOOK_SCRIPT_REL);
|
|
970
|
-
const claudeFile = path4.join(home, CLAUDE_SETTINGS_REL);
|
|
971
1016
|
const codexFile = path4.join(home, CODEX_CONFIG_REL);
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1017
|
+
const targets = await claudeTargets(params.claude_dirs, home);
|
|
1018
|
+
const merged = [];
|
|
1019
|
+
for (const target of targets) {
|
|
1020
|
+
try {
|
|
1021
|
+
merged.push({ target, body: mergeClaudeSettings(await readOrEmpty(target.file), scriptPath) });
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
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);
|
|
1024
|
+
throw new RpcFailure("failed", message, relPath(target.shown));
|
|
1025
|
+
}
|
|
978
1026
|
}
|
|
979
1027
|
const hasCodex = await isDir(path4.join(home, CODEX_DIR_REL));
|
|
980
1028
|
const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty(codexFile), scriptPath) : null;
|
|
981
|
-
let current = HOOK_SCRIPT_REL
|
|
1029
|
+
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
982
1030
|
try {
|
|
983
1031
|
await mkdir2(path4.dirname(scriptPath), { recursive: true });
|
|
984
|
-
|
|
985
|
-
current = HOOK_ENV_REL;
|
|
1032
|
+
current = `~/${HOOK_ENV_REL}`;
|
|
986
1033
|
await writeAtomic(path4.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
|
|
987
|
-
current = HOOK_SCRIPT_REL
|
|
1034
|
+
current = `~/${HOOK_SCRIPT_REL}`;
|
|
988
1035
|
await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
989
|
-
|
|
990
|
-
|
|
1036
|
+
for (const { target, body } of merged) {
|
|
1037
|
+
current = target.shown;
|
|
1038
|
+
await mkdir2(target.dir, { recursive: true });
|
|
1039
|
+
await writeAtomic(target.file, body, 420);
|
|
1040
|
+
}
|
|
991
1041
|
if (mergedCodex !== null) {
|
|
992
|
-
current = CODEX_CONFIG_REL
|
|
1042
|
+
current = `~/${CODEX_CONFIG_REL}`;
|
|
993
1043
|
await writeAtomic(codexFile, mergedCodex, 420);
|
|
994
1044
|
}
|
|
995
1045
|
} catch (err) {
|
|
996
1046
|
throw fsFailure(err, current);
|
|
997
1047
|
}
|
|
998
|
-
return {
|
|
1048
|
+
return {
|
|
1049
|
+
home,
|
|
1050
|
+
claude: "installed",
|
|
1051
|
+
codex: mergedCodex !== null ? "installed" : "skipped",
|
|
1052
|
+
claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
|
|
1053
|
+
};
|
|
999
1054
|
}
|
|
1000
|
-
async function uninstall(
|
|
1001
|
-
const claudeFile = path4.join(home, CLAUDE_SETTINGS_REL);
|
|
1055
|
+
async function uninstall(params, home = os3.homedir()) {
|
|
1002
1056
|
const codexFile = path4.join(home, CODEX_CONFIG_REL);
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1057
|
+
const stripped = [];
|
|
1058
|
+
for (const target of await claudeTargets(params.claude_dirs, home)) {
|
|
1059
|
+
const current2 = await readOrEmpty(target.file);
|
|
1060
|
+
try {
|
|
1061
|
+
const body = current2.trim() ? stripClaudeSettings(current2) : current2;
|
|
1062
|
+
if (body !== current2) stripped.push({ target, body });
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1009
1065
|
}
|
|
1010
1066
|
const codexConfig = await isDir(path4.join(home, CODEX_DIR_REL)) ? await readOrEmpty(codexFile) : "";
|
|
1011
|
-
let current = HOOK_SCRIPT_REL
|
|
1067
|
+
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
1012
1068
|
try {
|
|
1013
1069
|
await rm(path4.join(home, HOOK_SCRIPT_REL), { force: true });
|
|
1014
|
-
current = HOOK_ENV_REL
|
|
1070
|
+
current = `~/${HOOK_ENV_REL}`;
|
|
1015
1071
|
await rm(path4.join(home, HOOK_ENV_REL), { force: true });
|
|
1016
|
-
|
|
1017
|
-
current =
|
|
1018
|
-
await writeAtomic(
|
|
1072
|
+
for (const { target, body } of stripped) {
|
|
1073
|
+
current = target.shown;
|
|
1074
|
+
await writeAtomic(target.file, body, 420);
|
|
1019
1075
|
}
|
|
1020
1076
|
if (codexConfig.includes(HOOK_MARK)) {
|
|
1021
|
-
current = CODEX_CONFIG_REL
|
|
1077
|
+
current = `~/${CODEX_CONFIG_REL}`;
|
|
1022
1078
|
await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
|
|
1023
1079
|
}
|
|
1024
1080
|
} catch (err) {
|
|
@@ -1186,7 +1242,7 @@ async function status(deps = {}) {
|
|
|
1186
1242
|
}
|
|
1187
1243
|
|
|
1188
1244
|
// src/version.ts
|
|
1189
|
-
var AGENT_VERSION = "0.1.
|
|
1245
|
+
var AGENT_VERSION = "0.1.6";
|
|
1190
1246
|
|
|
1191
1247
|
// src/run.ts
|
|
1192
1248
|
function detectOs(platform = process.platform) {
|
package/package.json
CHANGED