@termhub/agent 0.4.0 → 0.4.2
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 +59 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -611,7 +611,7 @@ function claudeConfigDirs(accountDirs) {
|
|
|
611
611
|
function expandHome(dir, home) {
|
|
612
612
|
return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
|
|
613
613
|
}
|
|
614
|
-
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Notification", "Stop", "SessionEnd"];
|
|
614
|
+
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Notification", "Stop", "SessionEnd"];
|
|
615
615
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
616
616
|
# termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex hook events to
|
|
617
617
|
# termhub tagged with the tmux session, so the app knows which tab is waiting for you.
|
|
@@ -626,6 +626,56 @@ SESSION=$(tmux display-message -p -t "$TMUX_PANE" '#{session_name}' 2>/dev/null)
|
|
|
626
626
|
[ -n "$SESSION" ] || exit 0
|
|
627
627
|
if [ "$TOOL" = codex ]; then EVENT="$2"; else EVENT=$(cat 2>/dev/null); fi
|
|
628
628
|
[ -n "$EVENT" ] || EVENT='{}'
|
|
629
|
+
# Tool calls: only the tool's name travels (never its input), with the spinner's verb when one is on
|
|
630
|
+
# screen, and only when the pair changed since the last one for this session \u2014 twenty edits in a row
|
|
631
|
+
# 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
|
|
632
|
+
# session, under TMPDIR, with the session name reduced to filename-safe characters.
|
|
633
|
+
MARK="\${TMPDIR:-/tmp}/termhub-hook-$(printf '%s' "$SESSION" | tr -c 'A-Za-z0-9_-' '_')"
|
|
634
|
+
case "$EVENT" in
|
|
635
|
+
*'"hook_event_name":"PreToolUse"'*|*'"hook_event_name": "PreToolUse"'*)
|
|
636
|
+
# The event's own tool name is the FIRST "tool_name" of the payload (Claude Code serialises it
|
|
637
|
+
# before tool_input), so the shortest prefix is cut \u2014 a "tool_name" nested in a tool's input
|
|
638
|
+
# must not win. Only letters, digits, "_", "." and "-" are posted (a bare Claude Code tool name,
|
|
639
|
+
# or an MCP tool name such as mcp__claude-in-chrome__click): anything else (a number, a name with
|
|
640
|
+
# a quote or a backslash) is dropped rather than sent \u2014 those are the only characters the
|
|
641
|
+
# hand-built JSON body below cannot survive as-is.
|
|
642
|
+
REST=\${EVENT#*'"tool_name"'}
|
|
643
|
+
[ "$REST" != "$EVENT" ] || exit 0
|
|
644
|
+
REST=\${REST#*'"'}
|
|
645
|
+
NAME=\${REST%%'"'*}
|
|
646
|
+
case "$NAME" in '' | *[!A-Za-z0-9_.-]*) exit 0 ;; esac
|
|
647
|
+
# Claude Code's spinner verb ("\u273B Moonwalking\u2026 (12s \xB7 esc to interrupt)"): the visible pane is
|
|
648
|
+
# read here, on the machine, and only the verb may leave it \u2014 one word of 2 to 24 ASCII letters
|
|
649
|
+
# right after a spinner glyph at column 0 and a single space, immediately followed by "\u2026" or
|
|
650
|
+
# "...", then the end of the line or a space. Column 0 because Claude Code draws its spinner
|
|
651
|
+
# there, while a draft in the input box or indented tool output can look just like one. The
|
|
652
|
+
# lowest such line of the last 24 non-blank rows wins (the live spinner sits above the todo list
|
|
653
|
+
# and the input box; blank rows under a short session are skipped). Both grep and sed run under
|
|
654
|
+
# LC_ALL=C: bytes the locale calls invalid then neither trip grep's "binary file matches" (which
|
|
655
|
+
# would also swallow the rest of the screen) nor sed's "illegal byte sequence" on macOS, and
|
|
656
|
+
# the match works the same on GNU, BSD and busybox. ASCII only on purpose: a customised verb
|
|
657
|
+
# with accents is dropped rather than half-matched. The case below checks the result again, so
|
|
658
|
+
# the hand-built JSON only ever gets letters.
|
|
659
|
+
VERB=$(tmux capture-pane -p -t "$TMUX_PANE" 2>/dev/null | LC_ALL=C grep -v '^[[:space:]]*$' 2>/dev/null | tail -n 24 |
|
|
660
|
+
LC_ALL=C sed -n -E 's/^(\xB7|\u2722|\u2733|\u2736|\u273B|\u273D|\\*) ([A-Za-z]{2,24})(\u2026|\\.\\.\\.)( .*)?$/\\2/p' | tail -n 1)
|
|
661
|
+
case "$VERB" in *[!A-Za-z]*) VERB= ;; esac
|
|
662
|
+
[ "\${#VERB}" -le 24 ] || VERB=
|
|
663
|
+
KEY="$NAME\${VERB:+ $VERB}"
|
|
664
|
+
[ "$(cat "$MARK" 2>/dev/null)" = "$KEY" ] && exit 0
|
|
665
|
+
printf '%s' "$KEY" 2>/dev/null > "$MARK"
|
|
666
|
+
if [ -n "$VERB" ]; then
|
|
667
|
+
EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"}' "$NAME" "$VERB")
|
|
668
|
+
else
|
|
669
|
+
EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"}' "$NAME")
|
|
670
|
+
fi
|
|
671
|
+
;;
|
|
672
|
+
# A new turn starts fresh, and so does an answered notification: a permission prompt takes the tab
|
|
673
|
+
# out of working, and the tool the person approves is the same one that set the marker, so without
|
|
674
|
+
# this reset the retry is suppressed and nothing says the tab is working again.
|
|
675
|
+
*'"hook_event_name":"SessionStart"'*|*'"hook_event_name": "SessionStart"'*|*'"hook_event_name":"UserPromptSubmit"'*|*'"hook_event_name": "UserPromptSubmit"'*|*'"hook_event_name":"Notification"'*|*'"hook_event_name": "Notification"'*)
|
|
676
|
+
rm -f "$MARK"
|
|
677
|
+
;;
|
|
678
|
+
esac
|
|
629
679
|
{ printf '{"tool":"%s","session":"%s","event":' "$TOOL" "$SESSION"; printf '%s' "$EVENT"; printf '}'; } |
|
|
630
680
|
curl -s -m 5 -o /dev/null -X POST "$TERMHUB_HOOK_URL" \\
|
|
631
681
|
-H "authorization: Bearer $TERMHUB_HOOK_TOKEN" -H 'content-type: application/json' --data-binary @- >/dev/null 2>&1 &
|
|
@@ -649,7 +699,10 @@ function mergeClaudeSettings(current, scriptPath) {
|
|
|
649
699
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
650
700
|
const list3 = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
651
701
|
const others = list3.filter((e) => !isOurs(e));
|
|
652
|
-
|
|
702
|
+
const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
|
|
703
|
+
if (event === "PreToolUse")
|
|
704
|
+
entry.matcher = "*";
|
|
705
|
+
others.push(entry);
|
|
653
706
|
hooks[event] = others;
|
|
654
707
|
}
|
|
655
708
|
settings.hooks = hooks;
|
|
@@ -925,7 +978,9 @@ async function install(params, home = os3.homedir()) {
|
|
|
925
978
|
async function heal(home = os3.homedir()) {
|
|
926
979
|
const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
|
|
927
980
|
const env = await readOrEmpty2(path3.join(home, HOOK_ENV_REL));
|
|
928
|
-
|
|
981
|
+
const script = await readOrEmpty2(scriptPath);
|
|
982
|
+
if (!env.trim() || !script) return [];
|
|
983
|
+
if (script !== HOOK_SCRIPT) await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
929
984
|
const healed = [];
|
|
930
985
|
for (const dir of await discoverClaudeDirs(home)) {
|
|
931
986
|
const file = path3.join(expandHome(dir, home), "settings.json");
|
|
@@ -1833,7 +1888,7 @@ async function status3() {
|
|
|
1833
1888
|
}
|
|
1834
1889
|
|
|
1835
1890
|
// src/version.ts
|
|
1836
|
-
var AGENT_VERSION = "0.4.
|
|
1891
|
+
var AGENT_VERSION = "0.4.2";
|
|
1837
1892
|
|
|
1838
1893
|
// src/rpc/update.ts
|
|
1839
1894
|
var PACKAGE = "@termhub/agent";
|
package/package.json
CHANGED