@cocorograph/hub-agent 0.7.10 → 0.7.11
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/package.json +1 -1
- package/src/extract-paragraph.mjs +94 -0
- package/src/main.mjs +33 -3
package/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude TUI (Claude Code 2.1.x) のペイン出力から、
|
|
3
|
+
* 「直近のアシスタント説明テキスト段落」を保守的に抽出する。
|
|
4
|
+
*
|
|
5
|
+
* 目的: PreToolUse フック発火時点で jsonl はまだ前ターン止まりのため、フックの
|
|
6
|
+
* transcript ベース context_text 抽出は前ターンの古い説明を掴む「取り違え」を起こす。
|
|
7
|
+
* 一方、claude TUI のペインには「いま回答中の assistant 本文」が既に描画されている
|
|
8
|
+
* ため、tmux capture-pane の結果からこれを抜き出して permission.request に同梱する
|
|
9
|
+
* のが回答前に説明文をユーザーへ届ける唯一の経路。
|
|
10
|
+
*
|
|
11
|
+
* 出力フォーマット観察 (2026-06-24 実機 claude 2.1.x):
|
|
12
|
+
* ⏺ <本文テキスト> ← アシスタント説明 (これを抽出したい)
|
|
13
|
+
* ⏺ Bash(<cmd>) ← ツール呼び出し (除外したい)
|
|
14
|
+
* ⏺ Read(...) ← ツール呼び出し
|
|
15
|
+
* ⎿ <結果> ← ツール結果
|
|
16
|
+
*
|
|
17
|
+
* 抽出戦略: ペイン末尾から後ろ向き走査で「⏺ で始まり、続く語がツール名形式
|
|
18
|
+
* (CamelCase + `(`) でない最初の行」を探し、そこから下方向に連続する「説明文の段落」
|
|
19
|
+
* を取る。少しでも自信が無いケースは null 返却して呼び出し側で「説明なし」へ縮退。
|
|
20
|
+
*
|
|
21
|
+
* 安全方針: 抽出失敗 = null (壊さない)。誤抽出より null の方が UX 上安全。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ASSIST_PREFIX = "⏺"
|
|
25
|
+
|
|
26
|
+
const _MAX_CHARS = 2000
|
|
27
|
+
|
|
28
|
+
const _MIN_CHARS = 6
|
|
29
|
+
|
|
30
|
+
const _TOOL_CALL_RE = /^[A-Z][A-Za-z0-9_]*\s*\(/
|
|
31
|
+
|
|
32
|
+
const _BOX_LINE_RE = /^[─-▟]/
|
|
33
|
+
|
|
34
|
+
const _STATUS_LINE_RE = /^[✨✹●⚫◯]\s/
|
|
35
|
+
|
|
36
|
+
const _ELLIPSIS_TAIL_RE = /\n[ \t]*…\s*\+\d+\s+lines[^\n]*$/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string|undefined|null} paneText capturePane の出力 (ANSI 除去済み)
|
|
40
|
+
* @returns {string|null} 直近のアシスタント説明 (失敗時 null)
|
|
41
|
+
*/
|
|
42
|
+
export function extractLastAssistantText(paneText) {
|
|
43
|
+
if (typeof paneText !== "string" || !paneText) return null
|
|
44
|
+
const lines = paneText.replace(/\r/g, "").split("\n")
|
|
45
|
+
|
|
46
|
+
let startIdx = -1
|
|
47
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
48
|
+
const line = lines[i]
|
|
49
|
+
if (!line.startsWith(ASSIST_PREFIX + " ")) continue
|
|
50
|
+
const rest = line.slice(ASSIST_PREFIX.length + 1).trimStart()
|
|
51
|
+
if (_TOOL_CALL_RE.test(rest)) continue
|
|
52
|
+
startIdx = i
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
if (startIdx < 0) return null
|
|
56
|
+
|
|
57
|
+
const parts = []
|
|
58
|
+
let consecBlank = 0
|
|
59
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
60
|
+
const line = lines[i]
|
|
61
|
+
if (i > startIdx) {
|
|
62
|
+
if (line.startsWith(ASSIST_PREFIX + " ")) break
|
|
63
|
+
if (_BOX_LINE_RE.test(line)) break
|
|
64
|
+
if (_STATUS_LINE_RE.test(line.trim())) break
|
|
65
|
+
}
|
|
66
|
+
if (!line.trim()) {
|
|
67
|
+
consecBlank++
|
|
68
|
+
if (consecBlank >= 2) break
|
|
69
|
+
parts.push(line)
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
consecBlank = 0
|
|
73
|
+
parts.push(line)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let text = parts.join("\n").trimEnd()
|
|
77
|
+
if (text.startsWith(ASSIST_PREFIX)) {
|
|
78
|
+
text = text.slice(ASSIST_PREFIX.length).trimStart()
|
|
79
|
+
}
|
|
80
|
+
text = text.replace(_ELLIPSIS_TAIL_RE, "").trim()
|
|
81
|
+
if (text.length < _MIN_CHARS) return null
|
|
82
|
+
|
|
83
|
+
if (text.length > _MAX_CHARS) {
|
|
84
|
+
text = "…" + text.slice(-_MAX_CHARS)
|
|
85
|
+
}
|
|
86
|
+
return text
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const __test = {
|
|
90
|
+
_MAX_CHARS,
|
|
91
|
+
_MIN_CHARS,
|
|
92
|
+
_TOOL_CALL_RE,
|
|
93
|
+
_BOX_LINE_RE,
|
|
94
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import path from "node:path"
|
|
|
20
20
|
import pino from "pino"
|
|
21
21
|
|
|
22
22
|
import { detectPlatform, readConfig, writeConfig } from "./config.mjs"
|
|
23
|
+
import { extractLastAssistantText } from "./extract-paragraph.mjs"
|
|
23
24
|
import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs"
|
|
24
25
|
import { WsClient } from "./ws-client.mjs"
|
|
25
26
|
import { PtyBridge } from "./pty-bridge.mjs"
|
|
@@ -670,7 +671,7 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
670
671
|
const tuiPermissionBridge = new TuiPermissionBridge({ logger })
|
|
671
672
|
tuiPermissionBridge.on(
|
|
672
673
|
"permission",
|
|
673
|
-
({ request_id, session_id, cwd, tool_name, input, context_text }) => {
|
|
674
|
+
async ({ request_id, session_id, cwd, tool_name, input, context_text }) => {
|
|
674
675
|
if (cwd) {
|
|
675
676
|
try {
|
|
676
677
|
recordChatActivity(cwd, { inputPending: true })
|
|
@@ -678,6 +679,33 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
678
679
|
/* ignore */
|
|
679
680
|
}
|
|
680
681
|
}
|
|
682
|
+
// ── 直前アシスタント説明の抽出 (T05xxx 根治) ────────────────────────────
|
|
683
|
+
// PreToolUse フック発火時点で jsonl はまだ前ターン止まり (フックがブロック
|
|
684
|
+
// している間 claude は jsonl を commit しない) のため、フック側の transcript
|
|
685
|
+
// 抽出は前ターンの古い説明を掴む「取り違え」を起こす。
|
|
686
|
+
// 代わりに hub-agent がここで tmux capture-pane を打ち、TUI のペイン上に
|
|
687
|
+
// 既に描画されている「現ターンの assistant 本文段落 (⏺ プレフィックス)」を
|
|
688
|
+
// 抜き出して context_text に注入する。フックが既に context_text を
|
|
689
|
+
// 持っている場合 (旧経路) はそれを尊重し、空 / null のときだけ上書きする。
|
|
690
|
+
// 抽出失敗時は null フォールバック (= 説明なし) で安全縮退。
|
|
691
|
+
let liveContext = null
|
|
692
|
+
if (!context_text && cwd) {
|
|
693
|
+
try {
|
|
694
|
+
const sessions = await listTmuxSessions({ logger })
|
|
695
|
+
const match = Array.isArray(sessions)
|
|
696
|
+
? sessions.find((s) => s && s.cwd === cwd)
|
|
697
|
+
: null
|
|
698
|
+
if (match && match.name) {
|
|
699
|
+
const pane = await capturePane(match.name, { noCache: true })
|
|
700
|
+
liveContext = extractLastAssistantText(pane)
|
|
701
|
+
}
|
|
702
|
+
} catch (err) {
|
|
703
|
+
logger?.debug?.(
|
|
704
|
+
{ err: err?.message, cwd },
|
|
705
|
+
"capture-pane based context_text extraction failed; falling back to null",
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
681
709
|
client.send({
|
|
682
710
|
type: "claude.permission.request",
|
|
683
711
|
stream_id: null,
|
|
@@ -686,8 +714,10 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
686
714
|
request_id,
|
|
687
715
|
tool_name,
|
|
688
716
|
input,
|
|
689
|
-
// 質問/承認カードの直前アシスタント説明 (
|
|
690
|
-
|
|
717
|
+
// 質問/承認カードの直前アシスタント説明 (browser がカード上部に表示)。
|
|
718
|
+
// 優先順位: (1) フックが既に同梱した context_text、(2) hub-agent が
|
|
719
|
+
// tmux capture-pane で抽出した liveContext、(3) null。
|
|
720
|
+
context_text: context_text || liveContext || null,
|
|
691
721
|
})
|
|
692
722
|
},
|
|
693
723
|
)
|