@termhub/agent 0.5.0 → 0.5.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +70 -29
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -653,7 +653,8 @@ function claudeConfigDirs(accountDirs) {
653
653
  function expandHome(dir, home) {
654
654
  return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
655
655
  }
656
- var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Notification", "Stop", "SessionEnd"];
656
+ var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd"];
657
+ var CLAUDE_TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PermissionRequest"]);
657
658
  var CURSOR_HOOK_EVENTS = ["sessionStart", "beforeSubmitPrompt", "afterAgentResponse", "stop", "sessionEnd"];
658
659
  var HOOK_SCRIPT = `#!/bin/sh
659
660
  # termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex / Cursor CLI hook
@@ -674,9 +675,21 @@ if [ "$TOOL" = codex ]; then EVENT="$2"; else EVENT=$(cat 2>/dev/null); fi
674
675
  # screen, and only when the pair changed since the last one for this session \u2014 twenty edits in a row
675
676
  # 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
676
677
  # session, under TMPDIR, with the session name reduced to filename-safe characters.
678
+ # AskUserQuestion is the one exception (below).
677
679
  MARK="\${TMPDIR:-/tmp}/termhub-hook-$(printf '%s' "$SESSION" | tr -c 'A-Za-z0-9_-' '_')"
678
- case "$EVENT" in
679
- *'"hook_event_name":"PreToolUse"'*|*'"hook_event_name": "PreToolUse"'*)
680
+ # The branch below is picked on the event's OWN hook_event_name \u2014 the FIRST "hook_event_name" key of
681
+ # the payload (Claude Code serialises it before tool_input, same reasoning as tool_name below) \u2014
682
+ # never a value a substring search could find nested inside a tool's input (e.g. a PermissionRequest
683
+ # whose tool_input happened to contain the text "hook_event_name":"PreToolUse").
684
+ KIND_REST=\${EVENT#*'"hook_event_name"'}
685
+ if [ "$KIND_REST" != "$EVENT" ]; then
686
+ KIND_REST=\${KIND_REST#*'"'}
687
+ KIND=\${KIND_REST%%'"'*}
688
+ else
689
+ KIND=
690
+ fi
691
+ case "$KIND" in
692
+ PreToolUse)
680
693
  # The event's own tool name is the FIRST "tool_name" of the payload (Claude Code serialises it
681
694
  # before tool_input), so the shortest prefix is cut \u2014 a "tool_name" nested in a tool's input
682
695
  # must not win. Only letters, digits, "_", "." and "-" are posted (a bare Claude Code tool name,
@@ -688,35 +701,63 @@ case "$EVENT" in
688
701
  REST=\${REST#*'"'}
689
702
  NAME=\${REST%%'"'*}
690
703
  case "$NAME" in '' | *[!A-Za-z0-9_.-]*) exit 0 ;; esac
691
- # Claude Code's spinner verb ("\u273B Moonwalking\u2026 (12s \xB7 esc to interrupt)"): the visible pane is
692
- # read here, on the machine, and only the verb may leave it \u2014 one word of 2 to 24 ASCII letters
693
- # right after a spinner glyph at column 0 and a single space, immediately followed by "\u2026" or
694
- # "...", then the end of the line or a space. Column 0 because Claude Code draws its spinner
695
- # there, while a draft in the input box or indented tool output can look just like one. The
696
- # lowest such line of the last 24 non-blank rows wins (the live spinner sits above the todo list
697
- # and the input box; blank rows under a short session are skipped). Both grep and sed run under
698
- # LC_ALL=C: bytes the locale calls invalid then neither trip grep's "binary file matches" (which
699
- # would also swallow the rest of the screen) nor sed's "illegal byte sequence" on macOS, and
700
- # the match works the same on GNU, BSD and busybox. ASCII only on purpose: a customised verb
701
- # with accents is dropped rather than half-matched. The case below checks the result again, so
702
- # the hand-built JSON only ever gets letters.
703
- VERB=$(tmux capture-pane -p -t "$TMUX_PANE" 2>/dev/null | LC_ALL=C grep -v '^[[:space:]]*$' 2>/dev/null | tail -n 24 |
704
- LC_ALL=C sed -n -E 's/^(\xB7|\u2722|\u2733|\u2736|\u273B|\u273D|\\*) ([A-Za-z]{2,24})(\u2026|\\.\\.\\.)( .*)?$/\\2/p' | tail -n 1)
705
- case "$VERB" in *[!A-Za-z]*) VERB= ;; esac
706
- [ "\${#VERB}" -le 24 ] || VERB=
707
- KEY="$NAME\${VERB:+ $VERB}"
708
- [ "$(cat "$MARK" 2>/dev/null)" = "$KEY" ] && exit 0
709
- printf '%s' "$KEY" 2>/dev/null > "$MARK"
710
- if [ -n "$VERB" ]; then
711
- EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"}' "$NAME" "$VERB")
712
- else
713
- EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"}' "$NAME")
704
+ # AskUserQuestion's input is the question itself, written to be shown to the person (spec
705
+ # 2026-09-25 \xA74.1): the whole event goes as it came \u2014 the server keeps tool_use_id and
706
+ # tool_input and drops the rest \u2014 and the marker is neither read nor written, so two questions
707
+ # in a row are two questions. NAME is real only when $REST (everything from the first "tool_name"
708
+ # on) holds no SECOND "tool_name": a real question never repeats that key, so a second one means
709
+ # the first was actually nested inside another tool's tool_input (tool_input serialised before
710
+ # tool_name) and NAME does not name the real tool \u2014 fall back to the ordinary name-only path below
711
+ # (which, worst case, mislabels that one event; it never forwards the input).
712
+ ASK=false
713
+ if [ "$NAME" = AskUserQuestion ]; then
714
+ case "$REST" in
715
+ *'"tool_name"'*) ;;
716
+ *) ASK=true ;;
717
+ esac
714
718
  fi
719
+ if [ "$ASK" != true ]; then
720
+ # Claude Code's spinner verb ("\u273B Moonwalking\u2026 (12s \xB7 esc to interrupt)"): the visible pane is
721
+ # read here, on the machine, and only the verb may leave it \u2014 one word of 2 to 24 ASCII letters
722
+ # right after a spinner glyph at column 0 and a single space, immediately followed by "\u2026" or
723
+ # "...", then the end of the line or a space. Column 0 because Claude Code draws its spinner
724
+ # there, while a draft in the input box or indented tool output can look just like one. The
725
+ # lowest such line of the last 24 non-blank rows wins (the live spinner sits above the todo list
726
+ # and the input box; blank rows under a short session are skipped). Both grep and sed run under
727
+ # LC_ALL=C: bytes the locale calls invalid then neither trip grep's "binary file matches" (which
728
+ # would also swallow the rest of the screen) nor sed's "illegal byte sequence" on macOS, and
729
+ # the match works the same on GNU, BSD and busybox. ASCII only on purpose: a customised verb
730
+ # with accents is dropped rather than half-matched. The case below checks the result again, so
731
+ # the hand-built JSON only ever gets letters.
732
+ VERB=$(tmux capture-pane -p -t "$TMUX_PANE" 2>/dev/null | LC_ALL=C grep -v '^[[:space:]]*$' 2>/dev/null | tail -n 24 |
733
+ LC_ALL=C sed -n -E 's/^(\xB7|\u2722|\u2733|\u2736|\u273B|\u273D|\\*) ([A-Za-z]{2,24})(\u2026|\\.\\.\\.)( .*)?$/\\2/p' | tail -n 1)
734
+ case "$VERB" in *[!A-Za-z]*) VERB= ;; esac
735
+ [ "\${#VERB}" -le 24 ] || VERB=
736
+ KEY="$NAME\${VERB:+ $VERB}"
737
+ [ "$(cat "$MARK" 2>/dev/null)" = "$KEY" ] && exit 0
738
+ printf '%s' "$KEY" 2>/dev/null > "$MARK"
739
+ if [ -n "$VERB" ]; then
740
+ EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"}' "$NAME" "$VERB")
741
+ else
742
+ EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"}' "$NAME")
743
+ fi
744
+ fi
745
+ ;;
746
+ PermissionRequest)
747
+ # A permission prompt: only the tool's name travels, exactly like a tool call (never its input,
748
+ # never the suggestions). AskUserQuestion's own prompt is dropped \u2014 its PreToolUse already carried
749
+ # the question. Same first-"tool_name" rule and character set as above.
750
+ REST=\${EVENT#*'"tool_name"'}
751
+ [ "$REST" != "$EVENT" ] || exit 0
752
+ REST=\${REST#*'"'}
753
+ NAME=\${REST%%'"'*}
754
+ case "$NAME" in '' | *[!A-Za-z0-9_.-]* | AskUserQuestion) exit 0 ;; esac
755
+ EVENT=$(printf '{"hook_event_name":"PermissionRequest","tool_name":"%s"}' "$NAME")
715
756
  ;;
716
757
  # A new turn starts fresh, and so does an answered notification: a permission prompt takes the tab
717
758
  # out of working, and the tool the person approves is the same one that set the marker, so without
718
759
  # this reset the retry is suppressed and nothing says the tab is working again.
719
- *'"hook_event_name":"SessionStart"'*|*'"hook_event_name": "SessionStart"'*|*'"hook_event_name":"UserPromptSubmit"'*|*'"hook_event_name": "UserPromptSubmit"'*|*'"hook_event_name":"Notification"'*|*'"hook_event_name": "Notification"'*)
760
+ SessionStart | UserPromptSubmit | Notification)
720
761
  rm -f "$MARK"
721
762
  ;;
722
763
  esac
@@ -753,7 +794,7 @@ function mergeClaudeSettings(current, scriptPath, shown = "~/.claude/settings.js
753
794
  const list4 = Array.isArray(next[event]) ? next[event] : [];
754
795
  const others = list4.filter((e) => !isOurs(e));
755
796
  const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
756
- if (event === "PreToolUse")
797
+ if (CLAUDE_TOOL_EVENTS.has(event))
757
798
  entry.matcher = "*";
758
799
  others.push(entry);
759
800
  next[event] = others;
@@ -2271,7 +2312,7 @@ async function status3() {
2271
2312
  }
2272
2313
 
2273
2314
  // src/version.ts
2274
- var AGENT_VERSION = "0.5.0";
2315
+ var AGENT_VERSION = "0.5.1";
2275
2316
 
2276
2317
  // src/rpc/update.ts
2277
2318
  var PACKAGE = "@termhub/agent";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Agente do termhub: conecta esta máquina ao servidor por WebSocket de saída (sem SSH) e expõe os terminais tmux no navegador.",
5
5
  "license": "MIT",
6
6
  "private": false,