@termhub/agent 0.4.2 → 0.4.3
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 +158 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -76,11 +76,18 @@ var RPC = {
|
|
|
76
76
|
"file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4),
|
|
77
77
|
/** Monitor hooks (see @termhub/machine-ops hooks.ts): the agent writes the script, env and config entries under its own $HOME. */
|
|
78
78
|
/** `claude_dirs`: Claude config dirs besides ~/.claude (accounts with CLAUDE_CONFIG_DIR), hooked when they exist; since agent 0.1.5. */
|
|
79
|
+
/** `cursor` in the result: ~/.cursor/hooks.json (Cursor CLI), written when ~/.cursor exists; since agent 0.4.3 (older agents leave it out). */
|
|
79
80
|
"hooks.install": def(z.object({
|
|
80
81
|
hooks_url: z.string().min(1).max(2048).regex(/^https?:\/\/[^\s'"]+$/),
|
|
81
82
|
token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/),
|
|
82
83
|
claude_dirs: z.array(machinePath).max(16).optional()
|
|
83
|
-
}), z.object({
|
|
84
|
+
}), z.object({
|
|
85
|
+
home: z.string(),
|
|
86
|
+
claude: z.enum(["installed", "skipped"]),
|
|
87
|
+
codex: z.enum(["installed", "skipped"]),
|
|
88
|
+
cursor: z.enum(["installed", "skipped"]).optional(),
|
|
89
|
+
claude_dirs: z.array(z.string()).optional()
|
|
90
|
+
}), 15e3),
|
|
84
91
|
"hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3),
|
|
85
92
|
/** Installs `version` of @termhub/agent with npm; when the agent runs as a service it then exits so the service relaunches the new code (since agent 0.2.1). */
|
|
86
93
|
"agent.update": def(z.object({ version: z.string().regex(/^\d+\.\d+\.\d+$/) }), z.object({ installed_version: z.string(), restart: z.enum(["service", "manual"]) }), 18e4)
|
|
@@ -420,7 +427,7 @@ function shellQuote(s) {
|
|
|
420
427
|
var REMOTE_PATH_PREFIX = 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"; ';
|
|
421
428
|
|
|
422
429
|
// ../../packages/machine-ops/dist/detect.js
|
|
423
|
-
var DETECT_TOOLS = ["tmux", "claude", "codex", "gh", "git", "node", "pnpm", "xcodebuild", "docker", "adb", "python3"];
|
|
430
|
+
var DETECT_TOOLS = ["tmux", "claude", "codex", "cursor-agent", "gh", "git", "node", "pnpm", "xcodebuild", "docker", "adb", "python3"];
|
|
424
431
|
var WDA_RUNNER_APP = "$HOME/.termhub/WebDriverAgent/DerivedData/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app";
|
|
425
432
|
var DETECT_SCRIPT = `echo OS:$(uname -s); for t in ${DETECT_TOOLS.join(" ")}; do command -v $t >/dev/null 2>&1 && echo CAP:$t; done; [ -d "${WDA_RUNNER_APP}" ] && echo CAP:wda; exit 0`;
|
|
426
433
|
function parseDetect(stdout) {
|
|
@@ -612,10 +619,12 @@ function expandHome(dir, home) {
|
|
|
612
619
|
return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
|
|
613
620
|
}
|
|
614
621
|
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Notification", "Stop", "SessionEnd"];
|
|
622
|
+
var CURSOR_HOOK_EVENTS = ["sessionStart", "beforeSubmitPrompt", "afterAgentResponse", "stop", "sessionEnd"];
|
|
615
623
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
616
|
-
# termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex
|
|
617
|
-
# termhub tagged with the tmux session, so the app knows which tab is waiting for you.
|
|
618
|
-
# Safe to delete (also remove the entries in ~/.claude/settings.json
|
|
624
|
+
# termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex / Cursor CLI hook
|
|
625
|
+
# events to termhub tagged with the tmux session, so the app knows which tab is waiting for you.
|
|
626
|
+
# Safe to delete (also remove the entries in ~/.claude/settings.json, ~/.codex/config.toml and
|
|
627
|
+
# ~/.cursor/hooks.json).
|
|
619
628
|
TOOL="\${1:-claude}"
|
|
620
629
|
[ -f "$HOME/${HOOK_ENV_REL}" ] || exit 0
|
|
621
630
|
. "$HOME/${HOOK_ENV_REL}"
|
|
@@ -686,26 +695,35 @@ function hookEnvFile(hooksUrl, token) {
|
|
|
686
695
|
TERMHUB_HOOK_TOKEN=${shellQuote(token)}
|
|
687
696
|
`;
|
|
688
697
|
}
|
|
698
|
+
var asObject = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
699
|
+
var asHooksRecord = (v) => {
|
|
700
|
+
if (v == null || Array.isArray(v) && v.length === 0)
|
|
701
|
+
return {};
|
|
702
|
+
return asObject(v);
|
|
703
|
+
};
|
|
689
704
|
var isOurs = (e) => !!e && typeof e === "object" && Array.isArray(e.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes(HOOK_MARK));
|
|
690
|
-
function mergeClaudeSettings(current, scriptPath) {
|
|
705
|
+
function mergeClaudeSettings(current, scriptPath, shown = "~/.claude/settings.json") {
|
|
691
706
|
let settings = {};
|
|
692
707
|
if (current.trim()) {
|
|
693
708
|
const parsed = JSON.parse(current);
|
|
694
709
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
695
|
-
throw new Error(
|
|
710
|
+
throw new Error(`${shown} n\xE3o \xE9 um objeto JSON`);
|
|
696
711
|
settings = parsed;
|
|
697
712
|
}
|
|
698
|
-
const hooks =
|
|
713
|
+
const hooks = asHooksRecord(settings.hooks);
|
|
714
|
+
if (settings.hooks != null && hooks === null)
|
|
715
|
+
throw new Error(`${shown}: o campo "hooks" n\xE3o \xE9 um objeto`);
|
|
716
|
+
const next = hooks ?? {};
|
|
699
717
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
700
|
-
const list3 = Array.isArray(
|
|
718
|
+
const list3 = Array.isArray(next[event]) ? next[event] : [];
|
|
701
719
|
const others = list3.filter((e) => !isOurs(e));
|
|
702
720
|
const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
|
|
703
721
|
if (event === "PreToolUse")
|
|
704
722
|
entry.matcher = "*";
|
|
705
723
|
others.push(entry);
|
|
706
|
-
|
|
724
|
+
next[event] = others;
|
|
707
725
|
}
|
|
708
|
-
settings.hooks =
|
|
726
|
+
settings.hooks = next;
|
|
709
727
|
return `${JSON.stringify(settings, null, 2)}
|
|
710
728
|
`;
|
|
711
729
|
}
|
|
@@ -750,6 +768,50 @@ function stripCodexConfig(current) {
|
|
|
750
768
|
const lines = current.split("\n").filter((l) => !(/^\s*notify\s*=/.test(l) && l.includes(HOOK_MARK)));
|
|
751
769
|
return lines.join("\n");
|
|
752
770
|
}
|
|
771
|
+
var isOurCursorEntry = (e) => !!e && typeof e === "object" && typeof e.command === "string" && e.command.includes(HOOK_MARK);
|
|
772
|
+
function mergeCursorHooks(current, scriptPath, shown = "~/.cursor/hooks.json") {
|
|
773
|
+
let file = {};
|
|
774
|
+
if (current.trim()) {
|
|
775
|
+
const parsed = asObject(JSON.parse(current));
|
|
776
|
+
if (!parsed)
|
|
777
|
+
throw new Error(`${shown} n\xE3o \xE9 um objeto JSON`);
|
|
778
|
+
file = parsed;
|
|
779
|
+
}
|
|
780
|
+
const hooks = asHooksRecord(file.hooks);
|
|
781
|
+
if (file.hooks != null && hooks === null)
|
|
782
|
+
throw new Error(`${shown}: o campo "hooks" n\xE3o \xE9 um objeto`);
|
|
783
|
+
const next = hooks ?? {};
|
|
784
|
+
for (const event of CURSOR_HOOK_EVENTS) {
|
|
785
|
+
const others = (Array.isArray(next[event]) ? next[event] : []).filter((e) => !isOurCursorEntry(e));
|
|
786
|
+
others.push({ command: `${scriptPath} cursor` });
|
|
787
|
+
next[event] = others;
|
|
788
|
+
}
|
|
789
|
+
return `${JSON.stringify({ ...file, version: typeof file.version === "number" ? file.version : 1, hooks: next }, null, 2)}
|
|
790
|
+
`;
|
|
791
|
+
}
|
|
792
|
+
function stripCursorHooks(current) {
|
|
793
|
+
if (!current.trim())
|
|
794
|
+
return current;
|
|
795
|
+
const file = asObject(JSON.parse(current));
|
|
796
|
+
if (!file)
|
|
797
|
+
return current;
|
|
798
|
+
const hooks = asObject(file.hooks);
|
|
799
|
+
if (hooks) {
|
|
800
|
+
for (const key of Object.keys(hooks)) {
|
|
801
|
+
if (!Array.isArray(hooks[key]))
|
|
802
|
+
continue;
|
|
803
|
+
const kept = hooks[key].filter((e) => !isOurCursorEntry(e));
|
|
804
|
+
if (kept.length)
|
|
805
|
+
hooks[key] = kept;
|
|
806
|
+
else
|
|
807
|
+
delete hooks[key];
|
|
808
|
+
}
|
|
809
|
+
if (Object.keys(hooks).length === 0)
|
|
810
|
+
delete file.hooks;
|
|
811
|
+
}
|
|
812
|
+
return `${JSON.stringify(file, null, 2)}
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
753
815
|
|
|
754
816
|
// ../../packages/machine-ops/dist/discover.js
|
|
755
817
|
var CLAUDE_MARKERS = ["settings.json", "projects", ".credentials.json"];
|
|
@@ -897,6 +959,8 @@ function sh(script, opts = {}) {
|
|
|
897
959
|
// src/rpc/hooks.ts
|
|
898
960
|
var CODEX_DIR_REL = ".codex";
|
|
899
961
|
var CODEX_CONFIG_REL = ".codex/config.toml";
|
|
962
|
+
var CURSOR_DIR_REL = ".cursor";
|
|
963
|
+
var CURSOR_HOOKS_REL = ".cursor/hooks.json";
|
|
900
964
|
var isEnoent = (err) => err?.code === "ENOENT";
|
|
901
965
|
async function readOrEmpty2(file) {
|
|
902
966
|
try {
|
|
@@ -934,6 +998,15 @@ async function claudeTargets(dirs, home) {
|
|
|
934
998
|
}
|
|
935
999
|
return out;
|
|
936
1000
|
}
|
|
1001
|
+
async function mergedCursorHooks(home, scriptPath) {
|
|
1002
|
+
if (!await isDir2(path3.join(home, CURSOR_DIR_REL))) return null;
|
|
1003
|
+
try {
|
|
1004
|
+
return mergeCursorHooks(await readOrEmpty2(path3.join(home, CURSOR_HOOKS_REL)), scriptPath, `~/${CURSOR_HOOKS_REL}`);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
const message2 = err instanceof SyntaxError || err instanceof Error && err.message.includes("n\xE3o \xE9 um objeto JSON") ? `~/${CURSOR_HOOKS_REL} n\xE3o \xE9 JSON v\xE1lido` : err instanceof Error ? err.message : String(err);
|
|
1007
|
+
throw new RpcFailure("failed", message2, CURSOR_HOOKS_REL);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
937
1010
|
async function install(params, home = os3.homedir()) {
|
|
938
1011
|
const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
|
|
939
1012
|
const codexFile = path3.join(home, CODEX_CONFIG_REL);
|
|
@@ -941,7 +1014,7 @@ async function install(params, home = os3.homedir()) {
|
|
|
941
1014
|
const merged = [];
|
|
942
1015
|
for (const target of targets) {
|
|
943
1016
|
try {
|
|
944
|
-
merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath) });
|
|
1017
|
+
merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath, target.shown) });
|
|
945
1018
|
} catch (err) {
|
|
946
1019
|
const message2 = 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);
|
|
947
1020
|
throw new RpcFailure("failed", message2, relPath(target.shown));
|
|
@@ -949,6 +1022,7 @@ async function install(params, home = os3.homedir()) {
|
|
|
949
1022
|
}
|
|
950
1023
|
const hasCodex = await isDir2(path3.join(home, CODEX_DIR_REL));
|
|
951
1024
|
const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty2(codexFile), scriptPath) : null;
|
|
1025
|
+
const mergedCursor = await mergedCursorHooks(home, scriptPath);
|
|
952
1026
|
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
953
1027
|
try {
|
|
954
1028
|
await mkdir(path3.dirname(scriptPath), { recursive: true });
|
|
@@ -965,6 +1039,10 @@ async function install(params, home = os3.homedir()) {
|
|
|
965
1039
|
current = `~/${CODEX_CONFIG_REL}`;
|
|
966
1040
|
await writeAtomic(codexFile, mergedCodex, 420);
|
|
967
1041
|
}
|
|
1042
|
+
if (mergedCursor !== null) {
|
|
1043
|
+
current = `~/${CURSOR_HOOKS_REL}`;
|
|
1044
|
+
await writeAtomic(path3.join(home, CURSOR_HOOKS_REL), mergedCursor, 420);
|
|
1045
|
+
}
|
|
968
1046
|
} catch (err) {
|
|
969
1047
|
throw fsFailure(err, current);
|
|
970
1048
|
}
|
|
@@ -972,6 +1050,7 @@ async function install(params, home = os3.homedir()) {
|
|
|
972
1050
|
home,
|
|
973
1051
|
claude: "installed",
|
|
974
1052
|
codex: mergedCodex !== null ? "installed" : "skipped",
|
|
1053
|
+
cursor: mergedCursor !== null ? "installed" : "skipped",
|
|
975
1054
|
claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
|
|
976
1055
|
};
|
|
977
1056
|
}
|
|
@@ -981,22 +1060,71 @@ async function heal(home = os3.homedir()) {
|
|
|
981
1060
|
const script = await readOrEmpty2(scriptPath);
|
|
982
1061
|
if (!env.trim() || !script) return [];
|
|
983
1062
|
if (script !== HOOK_SCRIPT) await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
1063
|
+
const steps = [healClaudeDirs, healCursor, healCodex];
|
|
1064
|
+
const healed = [];
|
|
1065
|
+
for (const step of steps) {
|
|
1066
|
+
try {
|
|
1067
|
+
healed.push(...await step(home, scriptPath));
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return healed;
|
|
1072
|
+
}
|
|
1073
|
+
var healSkipLogged = /* @__PURE__ */ new Set();
|
|
1074
|
+
function logHealSkip(shown, err, key = shown) {
|
|
1075
|
+
if (healSkipLogged.has(key)) return;
|
|
1076
|
+
healSkipLogged.add(key);
|
|
1077
|
+
const code = err?.code;
|
|
1078
|
+
console.error(
|
|
1079
|
+
`[termhub-agent] monitor hooks heal skipped ${JSON.stringify({ path: shown, error: code ?? (err instanceof Error ? err.message : String(err)) })}`
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
async function healClaudeDirs(home, scriptPath) {
|
|
984
1083
|
const healed = [];
|
|
985
1084
|
for (const dir of await discoverClaudeDirs(home)) {
|
|
986
1085
|
const file = path3.join(expandHome(dir, home), "settings.json");
|
|
987
|
-
const current = await readOrEmpty2(file);
|
|
988
|
-
let body;
|
|
989
1086
|
try {
|
|
990
|
-
|
|
991
|
-
|
|
1087
|
+
const current = await readOrEmpty2(file);
|
|
1088
|
+
const body = mergeClaudeSettings(current, scriptPath, `${dir}/settings.json`);
|
|
1089
|
+
if (body === current) continue;
|
|
1090
|
+
await writeAtomic(file, body, 420);
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
logHealSkip(dir, err, file);
|
|
992
1093
|
continue;
|
|
993
1094
|
}
|
|
994
|
-
if (body === current) continue;
|
|
995
|
-
await writeAtomic(file, body, 420);
|
|
996
1095
|
healed.push(dir);
|
|
997
1096
|
}
|
|
998
1097
|
return healed;
|
|
999
1098
|
}
|
|
1099
|
+
async function healCursor(home, scriptPath) {
|
|
1100
|
+
if (!await isDir2(path3.join(home, CURSOR_DIR_REL))) return [];
|
|
1101
|
+
const file = path3.join(home, CURSOR_HOOKS_REL);
|
|
1102
|
+
const shown = `~/${CURSOR_DIR_REL}`;
|
|
1103
|
+
try {
|
|
1104
|
+
const current = await readOrEmpty2(file);
|
|
1105
|
+
const body = mergeCursorHooks(current, scriptPath);
|
|
1106
|
+
if (body === current) return [];
|
|
1107
|
+
await writeAtomic(file, body, 420);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
logHealSkip(shown, err, file);
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
return [shown];
|
|
1113
|
+
}
|
|
1114
|
+
async function healCodex(home, scriptPath) {
|
|
1115
|
+
if (!await isDir2(path3.join(home, CODEX_DIR_REL))) return [];
|
|
1116
|
+
const shown = `~/${CODEX_DIR_REL}`;
|
|
1117
|
+
try {
|
|
1118
|
+
const file = path3.join(home, CODEX_CONFIG_REL);
|
|
1119
|
+
const current = await readOrEmpty2(file);
|
|
1120
|
+
if (/^\s*notify\s*=/m.test(current)) return [];
|
|
1121
|
+
await writeAtomic(file, mergeCodexConfig(current, scriptPath), 420);
|
|
1122
|
+
return [shown];
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
logHealSkip(shown, err, path3.join(home, CODEX_CONFIG_REL));
|
|
1125
|
+
return [];
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1000
1128
|
async function uninstall(params, home = os3.homedir()) {
|
|
1001
1129
|
const codexFile = path3.join(home, CODEX_CONFIG_REL);
|
|
1002
1130
|
const stripped = [];
|
|
@@ -1009,6 +1137,13 @@ async function uninstall(params, home = os3.homedir()) {
|
|
|
1009
1137
|
}
|
|
1010
1138
|
}
|
|
1011
1139
|
const codexConfig = await isDir2(path3.join(home, CODEX_DIR_REL)) ? await readOrEmpty2(codexFile) : "";
|
|
1140
|
+
const cursorFile = path3.join(home, CURSOR_HOOKS_REL);
|
|
1141
|
+
const cursorHooks = await readOrEmpty2(cursorFile);
|
|
1142
|
+
let strippedCursor = null;
|
|
1143
|
+
try {
|
|
1144
|
+
strippedCursor = cursorHooks.includes(HOOK_MARK) ? stripCursorHooks(cursorHooks) : null;
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1012
1147
|
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
1013
1148
|
try {
|
|
1014
1149
|
await rm(path3.join(home, HOOK_SCRIPT_REL), { force: true });
|
|
@@ -1022,6 +1157,10 @@ async function uninstall(params, home = os3.homedir()) {
|
|
|
1022
1157
|
current = `~/${CODEX_CONFIG_REL}`;
|
|
1023
1158
|
await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
|
|
1024
1159
|
}
|
|
1160
|
+
if (strippedCursor !== null) {
|
|
1161
|
+
current = `~/${CURSOR_HOOKS_REL}`;
|
|
1162
|
+
await writeAtomic(cursorFile, strippedCursor, 420);
|
|
1163
|
+
}
|
|
1025
1164
|
} catch (err) {
|
|
1026
1165
|
throw fsFailure(err, current);
|
|
1027
1166
|
}
|
|
@@ -1888,7 +2027,7 @@ async function status3() {
|
|
|
1888
2027
|
}
|
|
1889
2028
|
|
|
1890
2029
|
// src/version.ts
|
|
1891
|
-
var AGENT_VERSION = "0.4.
|
|
2030
|
+
var AGENT_VERSION = "0.4.3";
|
|
1892
2031
|
|
|
1893
2032
|
// src/rpc/update.ts
|
|
1894
2033
|
var PACKAGE = "@termhub/agent";
|
package/package.json
CHANGED