@winspan/claude-forge 0.3.1 → 0.3.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/README.md +1 -1
- package/dist/hooks/check-context-limit.sh +11 -26
- package/dist/hooks/notification.sh +15 -21
- package/dist/hooks/post-tool-use.sh +21 -31
- package/dist/hooks/pre-tool-use.sh +19 -28
- package/dist/hooks/stop.sh +12 -17
- package/dist/hooks/user-prompt-submit.sh +22 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ Claude 读取 additionalContext,按指令执行
|
|
|
146
146
|
| `{project}/.claude-forge/decisions.md` | 项目决策日志 |
|
|
147
147
|
| `{project}/.claude-forge/timeline.md` | 项目时间线 |
|
|
148
148
|
| `{project}/.claude-forge/custom-convention.yaml` | 项目级自定义规范 |
|
|
149
|
-
| `{project}/docs/plan-*.md` | Plan-First 计划文件(按版本存储,Claude 负责写,Forge 负责读;`plan-reader.ts` 扫描 `docs/plan-*.md`,不再使用 `.claude-forge/plans/` 按日期文件)([x] 2026-04-13)|
|
|
149
|
+
| `{project}/docs/plan-*.md` | Plan-First 计划文件(按版本存储,Claude 负责写,Forge 负责读;`plan-reader.ts` 扫描 `docs/plan-*.md`,不再使用 `.claude-forge/plans/` 按日期文件)([x] 2026-04-13)|
|
|
150
150
|
|
|
151
151
|
---
|
|
152
152
|
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
WARN_THRESHOLD="${CONTEXT_WARN_THRESHOLD:-150000}"
|
|
5
5
|
ALERT_THRESHOLD="${CONTEXT_ALERT_THRESHOLD:-180000}"
|
|
6
6
|
|
|
7
|
-
# 从 stdin 读取 hook 输入,提取 session_id 和 cwd
|
|
7
|
+
# 从 stdin 读取 hook 输入,提取 session_id 和 cwd(jq 替代 python3)
|
|
8
8
|
INPUT=$(cat)
|
|
9
|
-
SESSION_ID=$(echo "$INPUT" |
|
|
10
|
-
CWD=$(echo "$INPUT" |
|
|
9
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
|
10
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
11
11
|
|
|
12
12
|
# 定位 session 文件
|
|
13
13
|
find_session_file() {
|
|
@@ -34,29 +34,14 @@ if [ -z "$SESSION_FILE" ] || [ ! -f "$SESSION_FILE" ]; then
|
|
|
34
34
|
exit 0
|
|
35
35
|
fi
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
d = json.loads(line)
|
|
47
|
-
if d.get('type') == 'assistant' and 'message' in d:
|
|
48
|
-
usage = d['message'].get('usage', {})
|
|
49
|
-
total = (usage.get('input_tokens', 0) +
|
|
50
|
-
usage.get('cache_read_input_tokens', 0) +
|
|
51
|
-
usage.get('cache_creation_input_tokens', 0))
|
|
52
|
-
if total > max_tokens:
|
|
53
|
-
max_tokens = total
|
|
54
|
-
except Exception:
|
|
55
|
-
pass
|
|
56
|
-
|
|
57
|
-
print(max_tokens)
|
|
58
|
-
PYEOF
|
|
59
|
-
)
|
|
37
|
+
# 只扫描最后 200 行(token 用量只会增大,最新的 assistant 消息在末尾)
|
|
38
|
+
# 避免随 session 增长的全文件 O(n) 扫描
|
|
39
|
+
CONTEXT_TOKENS=$(tail -200 "$SESSION_FILE" | jq -r '
|
|
40
|
+
select(.type == "assistant" and .message.usage != null)
|
|
41
|
+
| (.message.usage.input_tokens // 0)
|
|
42
|
+
+ (.message.usage.cache_read_input_tokens // 0)
|
|
43
|
+
+ (.message.usage.cache_creation_input_tokens // 0)
|
|
44
|
+
' 2>/dev/null | awk 'BEGIN{max=0} {if($1+0>max) max=$1+0} END{print max}')
|
|
60
45
|
|
|
61
46
|
CONTEXT_TOKENS="${CONTEXT_TOKENS:-0}"
|
|
62
47
|
|
|
@@ -7,29 +7,23 @@ AUTH_TOKEN=$(cat "$HOME/.claude-forge/daemon.token" 2>/dev/null || echo '')
|
|
|
7
7
|
# 读取 stdin
|
|
8
8
|
INPUT=$(cat)
|
|
9
9
|
|
|
10
|
-
# 提取 cwd
|
|
11
|
-
PROJECT_PATH=$(echo "$INPUT" |
|
|
10
|
+
# 提取 cwd(jq 替代 python3)
|
|
11
|
+
PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
12
12
|
PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
|
|
13
13
|
|
|
14
|
-
# 构造事件 JSON
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'project_path': os.environ.get('FORGE_PROJECT_PATH', ''),
|
|
28
|
-
'tool_name': 'notification',
|
|
29
|
-
'tool_input': tool_input,
|
|
30
|
-
'_auth': os.environ.get('FORGE_AUTH_TOKEN', ''),
|
|
31
|
-
}))
|
|
32
|
-
" 2>/dev/null)
|
|
14
|
+
# 构造事件 JSON(原始 INPUT 作为 tool_input,保留完整通知内容)
|
|
15
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
|
|
16
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.')
|
|
17
|
+
EVENT=$(jq -n \
|
|
18
|
+
--arg hook_type "Notification" \
|
|
19
|
+
--arg timestamp "$TIMESTAMP" \
|
|
20
|
+
--arg session_id "${CLAUDE_CODE_SESSION_ID:-cli}" \
|
|
21
|
+
--arg project_path "$PROJECT_PATH" \
|
|
22
|
+
--argjson tool_input "$TOOL_INPUT" \
|
|
23
|
+
--arg auth "$AUTH_TOKEN" \
|
|
24
|
+
'{hook_type: $hook_type, timestamp: $timestamp, session_id: $session_id,
|
|
25
|
+
project_path: $project_path, tool_name: "notification",
|
|
26
|
+
tool_input: $tool_input, _auth: $auth}')
|
|
33
27
|
|
|
34
28
|
echo "$EVENT" | nc -U -w 1 "$SOCKET_PATH" 2>/dev/null || true
|
|
35
29
|
exit 0
|
|
@@ -7,43 +7,33 @@ AUTH_TOKEN=$(cat "$HOME/.claude-forge/daemon.token" 2>/dev/null || echo '')
|
|
|
7
7
|
# 读取 stdin
|
|
8
8
|
INPUT=$(cat)
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
PROJECT_PATH=$(echo "$INPUT" |
|
|
10
|
+
# 提取字段(单次 jq 调用,替代 4 个 python3 进程)
|
|
11
|
+
# tool_response 是 Claude Code 的字段名
|
|
12
|
+
TOOL_NAME="${CLAUDE_TOOL_NAME:-$(echo "$INPUT" | jq -r '.tool_name // ""')}"
|
|
13
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
14
|
+
TOOL_OUTPUT=$(echo "$INPUT" | jq -c '.tool_response // {}')
|
|
15
|
+
PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
16
16
|
PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
|
|
17
17
|
|
|
18
|
-
# 构造事件 JSON
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'hook_type': 'PostToolUse',
|
|
33
|
-
'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
|
|
34
|
-
'session_id': os.environ.get('CLAUDE_CODE_SESSION_ID', 'cli'),
|
|
35
|
-
'project_path': os.environ.get('FORGE_PROJECT_PATH', ''),
|
|
36
|
-
'tool_name': os.environ.get('FORGE_TOOL_NAME', ''),
|
|
37
|
-
'tool_input': tool_input,
|
|
38
|
-
'tool_output': tool_output,
|
|
39
|
-
'_auth': os.environ.get('FORGE_AUTH_TOKEN', ''),
|
|
40
|
-
}))
|
|
41
|
-
" 2>/dev/null)
|
|
18
|
+
# 构造事件 JSON
|
|
19
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
|
|
20
|
+
EVENT=$(jq -n \
|
|
21
|
+
--arg hook_type "PostToolUse" \
|
|
22
|
+
--arg timestamp "$TIMESTAMP" \
|
|
23
|
+
--arg session_id "${CLAUDE_CODE_SESSION_ID:-cli}" \
|
|
24
|
+
--arg project_path "$PROJECT_PATH" \
|
|
25
|
+
--arg tool_name "$TOOL_NAME" \
|
|
26
|
+
--argjson tool_input "$TOOL_INPUT" \
|
|
27
|
+
--argjson tool_output "$TOOL_OUTPUT" \
|
|
28
|
+
--arg auth "$AUTH_TOKEN" \
|
|
29
|
+
'{hook_type: $hook_type, timestamp: $timestamp, session_id: $session_id,
|
|
30
|
+
project_path: $project_path, tool_name: $tool_name, tool_input: $tool_input,
|
|
31
|
+
tool_output: $tool_output, _auth: $auth}')
|
|
42
32
|
|
|
43
33
|
# 发送到 daemon(PostToolUse 也需要等待响应以支持通知注入)
|
|
44
34
|
RESPONSE=$(echo "$EVENT" | nc -U -w 5 "$SOCKET_PATH" 2>/dev/null || true)
|
|
45
35
|
if [ -n "$RESPONSE" ]; then
|
|
46
|
-
HAS_CONTEXT=$(echo "$RESPONSE" |
|
|
36
|
+
HAS_CONTEXT=$(echo "$RESPONSE" | jq -r 'if .additionalContext and (.additionalContext != "") then "yes" else "no" end')
|
|
47
37
|
if [ "$HAS_CONTEXT" = "yes" ]; then
|
|
48
38
|
echo "$RESPONSE"
|
|
49
39
|
fi
|
|
@@ -7,48 +7,39 @@ AUTH_TOKEN=$(cat "$HOME/.claude-forge/daemon.token" 2>/dev/null || echo '')
|
|
|
7
7
|
# 读取 stdin(Claude Code 传入的 JSON)
|
|
8
8
|
INPUT=$(cat)
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
TOOL_NAME="${CLAUDE_TOOL_NAME:-$(echo "$INPUT" |
|
|
12
|
-
TOOL_INPUT=$(echo "$INPUT" |
|
|
13
|
-
PROJECT_PATH=$(echo "$INPUT" |
|
|
10
|
+
# 提取字段(单次 jq 调用,替代 3 个 python3 进程)
|
|
11
|
+
TOOL_NAME="${CLAUDE_TOOL_NAME:-$(echo "$INPUT" | jq -r '.tool_name // ""')}"
|
|
12
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
13
|
+
PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
14
14
|
PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
|
|
15
15
|
|
|
16
|
-
# 构造事件 JSON
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'session_id': os.environ.get('CLAUDE_CODE_SESSION_ID', 'cli'),
|
|
30
|
-
'project_path': os.environ.get('FORGE_PROJECT_PATH', ''),
|
|
31
|
-
'tool_name': os.environ.get('FORGE_TOOL_NAME', ''),
|
|
32
|
-
'tool_input': tool_input,
|
|
33
|
-
'_auth': os.environ.get('FORGE_AUTH_TOKEN', ''),
|
|
34
|
-
}))
|
|
35
|
-
" 2>/dev/null)
|
|
16
|
+
# 构造事件 JSON(jq -n 替代 python3 heredoc)
|
|
17
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
|
|
18
|
+
EVENT=$(jq -n \
|
|
19
|
+
--arg hook_type "PreToolUse" \
|
|
20
|
+
--arg timestamp "$TIMESTAMP" \
|
|
21
|
+
--arg session_id "${CLAUDE_CODE_SESSION_ID:-cli}" \
|
|
22
|
+
--arg project_path "$PROJECT_PATH" \
|
|
23
|
+
--arg tool_name "$TOOL_NAME" \
|
|
24
|
+
--argjson tool_input "$TOOL_INPUT" \
|
|
25
|
+
--arg auth "$AUTH_TOKEN" \
|
|
26
|
+
'{hook_type: $hook_type, timestamp: $timestamp, session_id: $session_id,
|
|
27
|
+
project_path: $project_path, tool_name: $tool_name, tool_input: $tool_input,
|
|
28
|
+
_auth: $auth}')
|
|
36
29
|
|
|
37
30
|
# 发送事件并等待响应(双向通信,5 秒超时)
|
|
38
31
|
RESPONSE=$(echo "$EVENT" | nc -U -w 5 "$SOCKET_PATH" 2>/dev/null)
|
|
39
32
|
|
|
40
33
|
if [ -n "$RESPONSE" ]; then
|
|
41
|
-
|
|
42
|
-
ALLOW=$(echo "$RESPONSE" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(str(d.get("allow",True)).lower())' 2>/dev/null || echo 'true')
|
|
34
|
+
ALLOW=$(echo "$RESPONSE" | jq -r '.allow // true')
|
|
43
35
|
|
|
44
36
|
if [ "$ALLOW" = "false" ]; then
|
|
45
|
-
# 输出拦截 JSON 到 stdout(Claude Code 读取)
|
|
46
37
|
echo "$RESPONSE"
|
|
47
38
|
exit 2
|
|
48
39
|
fi
|
|
49
40
|
|
|
50
41
|
# 有 additionalContext 时输出
|
|
51
|
-
HAS_CONTEXT=$(echo "$RESPONSE" |
|
|
42
|
+
HAS_CONTEXT=$(echo "$RESPONSE" | jq -r 'if .additionalContext and (.additionalContext != "") then "yes" else "no" end')
|
|
52
43
|
if [ "$HAS_CONTEXT" = "yes" ]; then
|
|
53
44
|
echo "$RESPONSE"
|
|
54
45
|
fi
|
package/dist/hooks/stop.sh
CHANGED
|
@@ -7,25 +7,20 @@ AUTH_TOKEN=$(cat "$HOME/.claude-forge/daemon.token" 2>/dev/null || echo '')
|
|
|
7
7
|
# 读取 stdin
|
|
8
8
|
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
9
9
|
|
|
10
|
-
# 提取 cwd
|
|
11
|
-
PROJECT_PATH=$(echo "$INPUT" |
|
|
10
|
+
# 提取 cwd(jq 替代 python3)
|
|
11
|
+
PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
12
12
|
PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
|
|
13
13
|
|
|
14
|
-
# 构造事件 JSON
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'tool_name': 'stop',
|
|
25
|
-
'tool_input': {},
|
|
26
|
-
'_auth': os.environ.get('FORGE_AUTH_TOKEN', ''),
|
|
27
|
-
}))
|
|
28
|
-
" 2>/dev/null)
|
|
14
|
+
# 构造事件 JSON
|
|
15
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
|
|
16
|
+
EVENT=$(jq -n \
|
|
17
|
+
--arg hook_type "Stop" \
|
|
18
|
+
--arg timestamp "$TIMESTAMP" \
|
|
19
|
+
--arg session_id "${CLAUDE_CODE_SESSION_ID:-cli}" \
|
|
20
|
+
--arg project_path "$PROJECT_PATH" \
|
|
21
|
+
--arg auth "$AUTH_TOKEN" \
|
|
22
|
+
'{hook_type: $hook_type, timestamp: $timestamp, session_id: $session_id,
|
|
23
|
+
project_path: $project_path, tool_name: "stop", tool_input: {}, _auth: $auth}')
|
|
29
24
|
|
|
30
25
|
echo "$EVENT" | nc -U -w 1 "$SOCKET_PATH" 2>/dev/null || true
|
|
31
26
|
exit 0
|
|
@@ -13,9 +13,9 @@ fi
|
|
|
13
13
|
# 读取 stdin(Claude Code 传入的 JSON)
|
|
14
14
|
INPUT=$(cat)
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
USER_PROMPT=$(echo "$INPUT" |
|
|
18
|
-
PROJECT_PATH=$(echo "$INPUT" |
|
|
16
|
+
# 提取字段(jq 替代 python3)
|
|
17
|
+
USER_PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
|
|
18
|
+
PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
19
19
|
PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
|
|
20
20
|
|
|
21
21
|
# 空 prompt 跳过
|
|
@@ -23,21 +23,18 @@ if [ -z "$USER_PROMPT" ]; then
|
|
|
23
23
|
exit 0
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'_auth': os.environ.get('FORGE_AUTH_TOKEN', ''),
|
|
39
|
-
}))
|
|
40
|
-
" 2>/dev/null)
|
|
26
|
+
# 构造事件 JSON(jq -n 替代 python3 heredoc)
|
|
27
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
|
|
28
|
+
EVENT=$(jq -n \
|
|
29
|
+
--arg hook_type "UserPromptSubmit" \
|
|
30
|
+
--arg timestamp "$TIMESTAMP" \
|
|
31
|
+
--arg session_id "${CLAUDE_CODE_SESSION_ID:-cli}" \
|
|
32
|
+
--arg project_path "$PROJECT_PATH" \
|
|
33
|
+
--arg user_prompt "$USER_PROMPT" \
|
|
34
|
+
--arg auth "$AUTH_TOKEN" \
|
|
35
|
+
'{hook_type: $hook_type, timestamp: $timestamp, session_id: $session_id,
|
|
36
|
+
project_path: $project_path, tool_name: "UserPrompt",
|
|
37
|
+
tool_input: {user_prompt: $user_prompt}, _auth: $auth}')
|
|
41
38
|
|
|
42
39
|
if [ -z "$EVENT" ]; then
|
|
43
40
|
exit 0
|
|
@@ -47,25 +44,15 @@ fi
|
|
|
47
44
|
RESPONSE=$(echo "$EVENT" | nc -U -w 10 "$SOCKET_PATH" 2>/dev/null)
|
|
48
45
|
|
|
49
46
|
if [ -n "$RESPONSE" ]; then
|
|
50
|
-
HAS_CONTEXT=$(echo "$RESPONSE" |
|
|
47
|
+
HAS_CONTEXT=$(echo "$RESPONSE" | jq -r 'if .additionalContext and (.additionalContext != "") then "yes" else "no" end')
|
|
51
48
|
if [ "$HAS_CONTEXT" = "yes" ]; then
|
|
52
49
|
# stderr 输出到终端,让用户实时看到决策过程
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
lines = []
|
|
60
|
-
for line in ctx.split("\n"):
|
|
61
|
-
if line.startswith("请在回复"):
|
|
62
|
-
break
|
|
63
|
-
if line.strip():
|
|
64
|
-
lines.append(line)
|
|
65
|
-
if lines:
|
|
66
|
-
os.write(3, ("\033[36m" + "\n".join(lines) + "\033[0m\n").encode())
|
|
67
|
-
' 2>/dev/null
|
|
68
|
-
exec 3>&-
|
|
50
|
+
CONTEXT=$(echo "$RESPONSE" | jq -r '.additionalContext // ""')
|
|
51
|
+
# 提取第一个"请在回复"之前的非空行,输出到终端
|
|
52
|
+
DISPLAY_LINES=$(printf '%s' "$CONTEXT" | awk '/^请在回复/{exit} NF{print}')
|
|
53
|
+
if [ -n "$DISPLAY_LINES" ]; then
|
|
54
|
+
printf '\033[36m%s\033[0m\n' "$DISPLAY_LINES" >&2
|
|
55
|
+
fi
|
|
69
56
|
# stdout 返回给 Claude Code
|
|
70
57
|
echo "$RESPONSE"
|
|
71
58
|
fi
|