@termhub/agent 0.4.1 → 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 +185 -24
- 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}"
|
|
@@ -626,8 +635,9 @@ SESSION=$(tmux display-message -p -t "$TMUX_PANE" '#{session_name}' 2>/dev/null)
|
|
|
626
635
|
[ -n "$SESSION" ] || exit 0
|
|
627
636
|
if [ "$TOOL" = codex ]; then EVENT="$2"; else EVENT=$(cat 2>/dev/null); fi
|
|
628
637
|
[ -n "$EVENT" ] || EVENT='{}'
|
|
629
|
-
# Tool calls: only the tool's name travels (never its input),
|
|
630
|
-
# last one for this session \u2014 twenty edits in a row
|
|
638
|
+
# Tool calls: only the tool's name travels (never its input), with the spinner's verb when one is on
|
|
639
|
+
# screen, and only when the pair changed since the last one for this session \u2014 twenty edits in a row
|
|
640
|
+
# are one request as long as the verb stays the same (a new verb mid-run is a new request). The marker is per tmux
|
|
631
641
|
# session, under TMPDIR, with the session name reduced to filename-safe characters.
|
|
632
642
|
MARK="\${TMPDIR:-/tmp}/termhub-hook-$(printf '%s' "$SESSION" | tr -c 'A-Za-z0-9_-' '_')"
|
|
633
643
|
case "$EVENT" in
|
|
@@ -643,9 +653,30 @@ case "$EVENT" in
|
|
|
643
653
|
REST=\${REST#*'"'}
|
|
644
654
|
NAME=\${REST%%'"'*}
|
|
645
655
|
case "$NAME" in '' | *[!A-Za-z0-9_.-]*) exit 0 ;; esac
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
656
|
+
# Claude Code's spinner verb ("\u273B Moonwalking\u2026 (12s \xB7 esc to interrupt)"): the visible pane is
|
|
657
|
+
# read here, on the machine, and only the verb may leave it \u2014 one word of 2 to 24 ASCII letters
|
|
658
|
+
# right after a spinner glyph at column 0 and a single space, immediately followed by "\u2026" or
|
|
659
|
+
# "...", then the end of the line or a space. Column 0 because Claude Code draws its spinner
|
|
660
|
+
# there, while a draft in the input box or indented tool output can look just like one. The
|
|
661
|
+
# lowest such line of the last 24 non-blank rows wins (the live spinner sits above the todo list
|
|
662
|
+
# and the input box; blank rows under a short session are skipped). Both grep and sed run under
|
|
663
|
+
# LC_ALL=C: bytes the locale calls invalid then neither trip grep's "binary file matches" (which
|
|
664
|
+
# would also swallow the rest of the screen) nor sed's "illegal byte sequence" on macOS, and
|
|
665
|
+
# the match works the same on GNU, BSD and busybox. ASCII only on purpose: a customised verb
|
|
666
|
+
# with accents is dropped rather than half-matched. The case below checks the result again, so
|
|
667
|
+
# the hand-built JSON only ever gets letters.
|
|
668
|
+
VERB=$(tmux capture-pane -p -t "$TMUX_PANE" 2>/dev/null | LC_ALL=C grep -v '^[[:space:]]*$' 2>/dev/null | tail -n 24 |
|
|
669
|
+
LC_ALL=C sed -n -E 's/^(\xB7|\u2722|\u2733|\u2736|\u273B|\u273D|\\*) ([A-Za-z]{2,24})(\u2026|\\.\\.\\.)( .*)?$/\\2/p' | tail -n 1)
|
|
670
|
+
case "$VERB" in *[!A-Za-z]*) VERB= ;; esac
|
|
671
|
+
[ "\${#VERB}" -le 24 ] || VERB=
|
|
672
|
+
KEY="$NAME\${VERB:+ $VERB}"
|
|
673
|
+
[ "$(cat "$MARK" 2>/dev/null)" = "$KEY" ] && exit 0
|
|
674
|
+
printf '%s' "$KEY" 2>/dev/null > "$MARK"
|
|
675
|
+
if [ -n "$VERB" ]; then
|
|
676
|
+
EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"}' "$NAME" "$VERB")
|
|
677
|
+
else
|
|
678
|
+
EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"}' "$NAME")
|
|
679
|
+
fi
|
|
649
680
|
;;
|
|
650
681
|
# A new turn starts fresh, and so does an answered notification: a permission prompt takes the tab
|
|
651
682
|
# out of working, and the tool the person approves is the same one that set the marker, so without
|
|
@@ -664,26 +695,35 @@ function hookEnvFile(hooksUrl, token) {
|
|
|
664
695
|
TERMHUB_HOOK_TOKEN=${shellQuote(token)}
|
|
665
696
|
`;
|
|
666
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
|
+
};
|
|
667
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));
|
|
668
|
-
function mergeClaudeSettings(current, scriptPath) {
|
|
705
|
+
function mergeClaudeSettings(current, scriptPath, shown = "~/.claude/settings.json") {
|
|
669
706
|
let settings = {};
|
|
670
707
|
if (current.trim()) {
|
|
671
708
|
const parsed = JSON.parse(current);
|
|
672
709
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
673
|
-
throw new Error(
|
|
710
|
+
throw new Error(`${shown} n\xE3o \xE9 um objeto JSON`);
|
|
674
711
|
settings = parsed;
|
|
675
712
|
}
|
|
676
|
-
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 ?? {};
|
|
677
717
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
678
|
-
const list3 = Array.isArray(
|
|
718
|
+
const list3 = Array.isArray(next[event]) ? next[event] : [];
|
|
679
719
|
const others = list3.filter((e) => !isOurs(e));
|
|
680
720
|
const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
|
|
681
721
|
if (event === "PreToolUse")
|
|
682
722
|
entry.matcher = "*";
|
|
683
723
|
others.push(entry);
|
|
684
|
-
|
|
724
|
+
next[event] = others;
|
|
685
725
|
}
|
|
686
|
-
settings.hooks =
|
|
726
|
+
settings.hooks = next;
|
|
687
727
|
return `${JSON.stringify(settings, null, 2)}
|
|
688
728
|
`;
|
|
689
729
|
}
|
|
@@ -728,6 +768,50 @@ function stripCodexConfig(current) {
|
|
|
728
768
|
const lines = current.split("\n").filter((l) => !(/^\s*notify\s*=/.test(l) && l.includes(HOOK_MARK)));
|
|
729
769
|
return lines.join("\n");
|
|
730
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
|
+
}
|
|
731
815
|
|
|
732
816
|
// ../../packages/machine-ops/dist/discover.js
|
|
733
817
|
var CLAUDE_MARKERS = ["settings.json", "projects", ".credentials.json"];
|
|
@@ -875,6 +959,8 @@ function sh(script, opts = {}) {
|
|
|
875
959
|
// src/rpc/hooks.ts
|
|
876
960
|
var CODEX_DIR_REL = ".codex";
|
|
877
961
|
var CODEX_CONFIG_REL = ".codex/config.toml";
|
|
962
|
+
var CURSOR_DIR_REL = ".cursor";
|
|
963
|
+
var CURSOR_HOOKS_REL = ".cursor/hooks.json";
|
|
878
964
|
var isEnoent = (err) => err?.code === "ENOENT";
|
|
879
965
|
async function readOrEmpty2(file) {
|
|
880
966
|
try {
|
|
@@ -912,6 +998,15 @@ async function claudeTargets(dirs, home) {
|
|
|
912
998
|
}
|
|
913
999
|
return out;
|
|
914
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
|
+
}
|
|
915
1010
|
async function install(params, home = os3.homedir()) {
|
|
916
1011
|
const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
|
|
917
1012
|
const codexFile = path3.join(home, CODEX_CONFIG_REL);
|
|
@@ -919,7 +1014,7 @@ async function install(params, home = os3.homedir()) {
|
|
|
919
1014
|
const merged = [];
|
|
920
1015
|
for (const target of targets) {
|
|
921
1016
|
try {
|
|
922
|
-
merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath) });
|
|
1017
|
+
merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath, target.shown) });
|
|
923
1018
|
} catch (err) {
|
|
924
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);
|
|
925
1020
|
throw new RpcFailure("failed", message2, relPath(target.shown));
|
|
@@ -927,6 +1022,7 @@ async function install(params, home = os3.homedir()) {
|
|
|
927
1022
|
}
|
|
928
1023
|
const hasCodex = await isDir2(path3.join(home, CODEX_DIR_REL));
|
|
929
1024
|
const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty2(codexFile), scriptPath) : null;
|
|
1025
|
+
const mergedCursor = await mergedCursorHooks(home, scriptPath);
|
|
930
1026
|
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
931
1027
|
try {
|
|
932
1028
|
await mkdir(path3.dirname(scriptPath), { recursive: true });
|
|
@@ -943,6 +1039,10 @@ async function install(params, home = os3.homedir()) {
|
|
|
943
1039
|
current = `~/${CODEX_CONFIG_REL}`;
|
|
944
1040
|
await writeAtomic(codexFile, mergedCodex, 420);
|
|
945
1041
|
}
|
|
1042
|
+
if (mergedCursor !== null) {
|
|
1043
|
+
current = `~/${CURSOR_HOOKS_REL}`;
|
|
1044
|
+
await writeAtomic(path3.join(home, CURSOR_HOOKS_REL), mergedCursor, 420);
|
|
1045
|
+
}
|
|
946
1046
|
} catch (err) {
|
|
947
1047
|
throw fsFailure(err, current);
|
|
948
1048
|
}
|
|
@@ -950,6 +1050,7 @@ async function install(params, home = os3.homedir()) {
|
|
|
950
1050
|
home,
|
|
951
1051
|
claude: "installed",
|
|
952
1052
|
codex: mergedCodex !== null ? "installed" : "skipped",
|
|
1053
|
+
cursor: mergedCursor !== null ? "installed" : "skipped",
|
|
953
1054
|
claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
|
|
954
1055
|
};
|
|
955
1056
|
}
|
|
@@ -959,22 +1060,71 @@ async function heal(home = os3.homedir()) {
|
|
|
959
1060
|
const script = await readOrEmpty2(scriptPath);
|
|
960
1061
|
if (!env.trim() || !script) return [];
|
|
961
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) {
|
|
962
1083
|
const healed = [];
|
|
963
1084
|
for (const dir of await discoverClaudeDirs(home)) {
|
|
964
1085
|
const file = path3.join(expandHome(dir, home), "settings.json");
|
|
965
|
-
const current = await readOrEmpty2(file);
|
|
966
|
-
let body;
|
|
967
1086
|
try {
|
|
968
|
-
|
|
969
|
-
|
|
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);
|
|
970
1093
|
continue;
|
|
971
1094
|
}
|
|
972
|
-
if (body === current) continue;
|
|
973
|
-
await writeAtomic(file, body, 420);
|
|
974
1095
|
healed.push(dir);
|
|
975
1096
|
}
|
|
976
1097
|
return healed;
|
|
977
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
|
+
}
|
|
978
1128
|
async function uninstall(params, home = os3.homedir()) {
|
|
979
1129
|
const codexFile = path3.join(home, CODEX_CONFIG_REL);
|
|
980
1130
|
const stripped = [];
|
|
@@ -987,6 +1137,13 @@ async function uninstall(params, home = os3.homedir()) {
|
|
|
987
1137
|
}
|
|
988
1138
|
}
|
|
989
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
|
+
}
|
|
990
1147
|
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
991
1148
|
try {
|
|
992
1149
|
await rm(path3.join(home, HOOK_SCRIPT_REL), { force: true });
|
|
@@ -1000,6 +1157,10 @@ async function uninstall(params, home = os3.homedir()) {
|
|
|
1000
1157
|
current = `~/${CODEX_CONFIG_REL}`;
|
|
1001
1158
|
await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
|
|
1002
1159
|
}
|
|
1160
|
+
if (strippedCursor !== null) {
|
|
1161
|
+
current = `~/${CURSOR_HOOKS_REL}`;
|
|
1162
|
+
await writeAtomic(cursorFile, strippedCursor, 420);
|
|
1163
|
+
}
|
|
1003
1164
|
} catch (err) {
|
|
1004
1165
|
throw fsFailure(err, current);
|
|
1005
1166
|
}
|
|
@@ -1866,7 +2027,7 @@ async function status3() {
|
|
|
1866
2027
|
}
|
|
1867
2028
|
|
|
1868
2029
|
// src/version.ts
|
|
1869
|
-
var AGENT_VERSION = "0.4.
|
|
2030
|
+
var AGENT_VERSION = "0.4.3";
|
|
1870
2031
|
|
|
1871
2032
|
// src/rpc/update.ts
|
|
1872
2033
|
var PACKAGE = "@termhub/agent";
|
package/package.json
CHANGED