@vibe-x/agent-better-checkpoint 0.1.0
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/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/cli.mjs +380 -0
- package/package.json +34 -0
- package/platform/unix/check_uncommitted.sh +256 -0
- package/platform/unix/checkpoint.sh +124 -0
- package/platform/win/check_uncommitted.ps1 +209 -0
- package/platform/win/checkpoint.ps1 +123 -0
- package/skill/SKILL.md +170 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# check_uncommitted.sh — Stop Hook: 检查未提交的变更
|
|
4
|
+
#
|
|
5
|
+
# 在 AI 对话结束时触发,检查工作区是否存在未提交的变更。
|
|
6
|
+
# 如果存在,输出提醒信息让 AI Agent 执行 fallback checkpoint commit。
|
|
7
|
+
#
|
|
8
|
+
# 支持平台:
|
|
9
|
+
# - Cursor: stop hook (stdin JSON 含 workspace_roots)
|
|
10
|
+
# - Claude Code: Stop hook (stdin JSON 含 hook_event_name)
|
|
11
|
+
#
|
|
12
|
+
# 输出协议:
|
|
13
|
+
# - 无问题: {} (空 JSON)
|
|
14
|
+
# - 有问题 (Cursor): {"followup_message": "..."}
|
|
15
|
+
# - 有问题 (Claude Code): {"decision": "block", "reason": "..."}
|
|
16
|
+
#
|
|
17
|
+
# JSON 解析使用 grep+sed,不依赖 jq。
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# ============================================================
|
|
22
|
+
# 辅助函数:简易 JSON 字段提取(不依赖 jq)
|
|
23
|
+
# ============================================================
|
|
24
|
+
|
|
25
|
+
# 输出允许通过的 JSON
|
|
26
|
+
output_allow() {
|
|
27
|
+
echo '{}'
|
|
28
|
+
exit 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 从 JSON 中提取布尔字段值
|
|
32
|
+
# Usage: json_bool "$json" "field_name" → 输出 "true" 或 "false"
|
|
33
|
+
json_bool() {
|
|
34
|
+
local json="$1" field="$2"
|
|
35
|
+
if echo "$json" | grep -qE "\"${field}\"[[:space:]]*:[[:space:]]*true"; then
|
|
36
|
+
echo "true"
|
|
37
|
+
else
|
|
38
|
+
echo "false"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# 从 JSON 中提取字符串字段值
|
|
43
|
+
# Usage: json_string "$json" "field_name" → 输出字符串值(不含引号)
|
|
44
|
+
json_string() {
|
|
45
|
+
local json="$1" field="$2"
|
|
46
|
+
echo "$json" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
|
|
47
|
+
| sed -E "s/\"${field}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"/\1/" \
|
|
48
|
+
| head -1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# 从 JSON 的 workspace_roots / workspaceRoots 数组中提取第一个路径
|
|
52
|
+
# Usage: json_workspace_root "$json" → 输出路径
|
|
53
|
+
json_workspace_root() {
|
|
54
|
+
local json="$1"
|
|
55
|
+
local result=""
|
|
56
|
+
|
|
57
|
+
# 尝试 workspace_roots 和 workspaceRoots
|
|
58
|
+
for field in "workspace_roots" "workspaceRoots"; do
|
|
59
|
+
result=$(echo "$json" \
|
|
60
|
+
| grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\[[^]]*\]" \
|
|
61
|
+
| grep -oE '"[^"]*"' \
|
|
62
|
+
| tail -n +2 \
|
|
63
|
+
| head -1 \
|
|
64
|
+
| tr -d '"' || true)
|
|
65
|
+
if [[ -n "$result" ]]; then
|
|
66
|
+
echo "$result"
|
|
67
|
+
return
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# ============================================================
|
|
73
|
+
# 平台检测
|
|
74
|
+
# ============================================================
|
|
75
|
+
|
|
76
|
+
detect_platform() {
|
|
77
|
+
local json="$1"
|
|
78
|
+
if [[ -z "$json" ]]; then
|
|
79
|
+
echo "unknown"
|
|
80
|
+
return
|
|
81
|
+
fi
|
|
82
|
+
# Claude Code: 有 hook_event_name 或 tool_name
|
|
83
|
+
if echo "$json" | grep -qE '"hook_event_name"|"tool_name"'; then
|
|
84
|
+
echo "claude_code"
|
|
85
|
+
else
|
|
86
|
+
echo "cursor"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ============================================================
|
|
91
|
+
# Workspace Root 检测
|
|
92
|
+
# ============================================================
|
|
93
|
+
|
|
94
|
+
get_workspace_root() {
|
|
95
|
+
local json="$1"
|
|
96
|
+
|
|
97
|
+
# 优先从 stdin JSON 获取
|
|
98
|
+
if [[ -n "$json" ]]; then
|
|
99
|
+
local ws
|
|
100
|
+
ws=$(json_workspace_root "$json")
|
|
101
|
+
if [[ -n "$ws" ]]; then
|
|
102
|
+
echo "$ws"
|
|
103
|
+
return
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# 回退到环境变量
|
|
108
|
+
for env_var in CURSOR_PROJECT_DIR CLAUDE_PROJECT_DIR WORKSPACE_ROOT PROJECT_ROOT; do
|
|
109
|
+
local val="${!env_var:-}"
|
|
110
|
+
if [[ -n "$val" ]]; then
|
|
111
|
+
echo "$val"
|
|
112
|
+
return
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
# 最终回退到 PWD / cwd
|
|
117
|
+
echo "${PWD:-$(pwd)}"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# ============================================================
|
|
121
|
+
# Git 操作
|
|
122
|
+
# ============================================================
|
|
123
|
+
|
|
124
|
+
is_git_repo() {
|
|
125
|
+
git -C "$1" rev-parse --is-inside-work-tree &>/dev/null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
has_uncommitted_changes() {
|
|
129
|
+
local status
|
|
130
|
+
status=$(git -C "$1" status --porcelain 2>/dev/null)
|
|
131
|
+
[[ -n "$status" ]]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get_change_summary() {
|
|
135
|
+
local workspace="$1"
|
|
136
|
+
local max_lines="${2:-20}"
|
|
137
|
+
local output
|
|
138
|
+
output=$(git -C "$workspace" status --short 2>/dev/null || true)
|
|
139
|
+
|
|
140
|
+
if [[ -z "$output" ]]; then
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
local total
|
|
145
|
+
total=$(echo "$output" | wc -l)
|
|
146
|
+
|
|
147
|
+
if [[ "$total" -gt "$max_lines" ]]; then
|
|
148
|
+
echo "$output" | head -n "$max_lines"
|
|
149
|
+
echo " ... and $((total - max_lines)) more files"
|
|
150
|
+
else
|
|
151
|
+
echo "$output"
|
|
152
|
+
fi
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# ============================================================
|
|
156
|
+
# 输出提醒
|
|
157
|
+
# ============================================================
|
|
158
|
+
|
|
159
|
+
# 转义字符串用于 JSON 值(处理引号、反斜杠、换行等)
|
|
160
|
+
json_escape() {
|
|
161
|
+
local str="$1"
|
|
162
|
+
str="${str//\\/\\\\}" # 反斜杠
|
|
163
|
+
str="${str//\"/\\\"}" # 双引号
|
|
164
|
+
str="${str//$'\n'/\\n}" # 换行
|
|
165
|
+
str="${str//$'\r'/\\r}" # 回车
|
|
166
|
+
str="${str//$'\t'/\\t}" # Tab
|
|
167
|
+
echo "$str"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
output_block() {
|
|
171
|
+
local message="$1"
|
|
172
|
+
local platform="$2"
|
|
173
|
+
local escaped
|
|
174
|
+
escaped=$(json_escape "$message")
|
|
175
|
+
|
|
176
|
+
if [[ "$platform" == "cursor" ]]; then
|
|
177
|
+
echo "{\"followup_message\":\"${escaped}\"}"
|
|
178
|
+
elif [[ "$platform" == "claude_code" ]]; then
|
|
179
|
+
echo "{\"decision\":\"block\",\"reason\":\"${escaped}\"}"
|
|
180
|
+
else
|
|
181
|
+
echo "{\"message\":\"${escaped}\"}"
|
|
182
|
+
fi
|
|
183
|
+
exit 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# ============================================================
|
|
187
|
+
# 主逻辑
|
|
188
|
+
# ============================================================
|
|
189
|
+
|
|
190
|
+
main() {
|
|
191
|
+
# 从 stdin 读取 JSON(非阻塞:如果无输入则为空)
|
|
192
|
+
local input=""
|
|
193
|
+
if [[ ! -t 0 ]]; then
|
|
194
|
+
input=$(cat)
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Claude Code 的 stop_hook_active 防止无限循环
|
|
198
|
+
if [[ -n "$input" ]]; then
|
|
199
|
+
local stop_active
|
|
200
|
+
stop_active=$(json_bool "$input" "stop_hook_active")
|
|
201
|
+
if [[ "$stop_active" == "true" ]]; then
|
|
202
|
+
output_allow
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# 检测平台
|
|
207
|
+
local platform
|
|
208
|
+
platform=$(detect_platform "$input")
|
|
209
|
+
|
|
210
|
+
# 获取 workspace root
|
|
211
|
+
local workspace
|
|
212
|
+
workspace=$(get_workspace_root "$input")
|
|
213
|
+
|
|
214
|
+
# 检查是否为 git 仓库
|
|
215
|
+
if ! is_git_repo "$workspace"; then
|
|
216
|
+
output_allow
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# 检查是否有未提交变更
|
|
220
|
+
if ! has_uncommitted_changes "$workspace"; then
|
|
221
|
+
output_allow
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# 获取变更摘要
|
|
225
|
+
local changes
|
|
226
|
+
changes=$(get_change_summary "$workspace")
|
|
227
|
+
local changes_indented
|
|
228
|
+
changes_indented=$(echo "$changes" | sed 's/^/ /')
|
|
229
|
+
|
|
230
|
+
# 构建提醒消息
|
|
231
|
+
local reminder
|
|
232
|
+
reminder="## ⚠️ Uncommitted Changes Detected
|
|
233
|
+
|
|
234
|
+
There are uncommitted changes in the workspace. Please create a checkpoint commit before ending the conversation.
|
|
235
|
+
|
|
236
|
+
**Changed files:**
|
|
237
|
+
\`\`\`
|
|
238
|
+
${changes_indented}
|
|
239
|
+
\`\`\`
|
|
240
|
+
|
|
241
|
+
**Action Required**: Run the checkpoint script to commit these changes:
|
|
242
|
+
|
|
243
|
+
**macOS/Linux:**
|
|
244
|
+
\`\`\`bash
|
|
245
|
+
~/.agent-better-checkpoint/scripts/checkpoint.sh \"checkpoint(<scope>): <description>\" \"<user-prompt>\" --type fallback
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
**Windows (PowerShell):**
|
|
249
|
+
\`\`\`powershell
|
|
250
|
+
powershell -File \"\$env:USERPROFILE/.agent-better-checkpoint/scripts/checkpoint.ps1\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback
|
|
251
|
+
\`\`\`"
|
|
252
|
+
|
|
253
|
+
output_block "$reminder" "$platform"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
main
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# checkpoint.sh — 创建语义化的 Git checkpoint commit
|
|
4
|
+
#
|
|
5
|
+
# AI 生成描述性内容(subject + body),本脚本负责:
|
|
6
|
+
# 1. 截断 user-prompt(≤60字符,头尾截断)
|
|
7
|
+
# 2. 通过 git interpret-trailers 追加元信息
|
|
8
|
+
# 3. 执行 git add -A && git commit
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# checkpoint.sh <message> [user-prompt] [--type auto|fallback]
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
# ============================================================
|
|
16
|
+
# 参数解析
|
|
17
|
+
# ============================================================
|
|
18
|
+
MESSAGE="${1:-}"
|
|
19
|
+
USER_PROMPT="${2:-}"
|
|
20
|
+
CHECKPOINT_TYPE="auto"
|
|
21
|
+
|
|
22
|
+
shift 2 2>/dev/null || true
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--type)
|
|
26
|
+
CHECKPOINT_TYPE="${2:-auto}"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
*)
|
|
30
|
+
shift
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
if [[ -z "$MESSAGE" ]]; then
|
|
36
|
+
echo "Error: commit message is required" >&2
|
|
37
|
+
echo "Usage: checkpoint.sh <message> [user-prompt] [--type auto|fallback]" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# ============================================================
|
|
42
|
+
# 平台检测
|
|
43
|
+
# ============================================================
|
|
44
|
+
detect_platform() {
|
|
45
|
+
if [[ -n "${CLAUDE_CODE:-}" ]] || command -v claude &>/dev/null; then
|
|
46
|
+
echo "claude-code"
|
|
47
|
+
elif [[ -n "${CURSOR_VERSION:-}" ]] || [[ -n "${CURSOR_TRACE_ID:-}" ]]; then
|
|
48
|
+
echo "cursor"
|
|
49
|
+
else
|
|
50
|
+
echo "unknown"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AGENT_PLATFORM=$(detect_platform)
|
|
55
|
+
|
|
56
|
+
# ============================================================
|
|
57
|
+
# User-Prompt 截断(≤60 字符,头尾保留 + 中间省略号)
|
|
58
|
+
# ============================================================
|
|
59
|
+
truncate_prompt() {
|
|
60
|
+
local prompt="$1"
|
|
61
|
+
local max_len=60
|
|
62
|
+
local len=${#prompt}
|
|
63
|
+
|
|
64
|
+
if [[ $len -le $max_len ]]; then
|
|
65
|
+
echo "$prompt"
|
|
66
|
+
return
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
local head_len=$(( (max_len - 3) / 2 ))
|
|
70
|
+
local tail_len=$(( max_len - 3 - head_len ))
|
|
71
|
+
local head="${prompt:0:$head_len}"
|
|
72
|
+
local tail="${prompt:$((len - tail_len)):$tail_len}"
|
|
73
|
+
echo "${head}...${tail}"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
TRUNCATED_PROMPT=""
|
|
77
|
+
if [[ -n "$USER_PROMPT" ]]; then
|
|
78
|
+
TRUNCATED_PROMPT=$(truncate_prompt "$USER_PROMPT")
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# ============================================================
|
|
82
|
+
# 检查是否有变更
|
|
83
|
+
# ============================================================
|
|
84
|
+
has_changes() {
|
|
85
|
+
# staged 变更
|
|
86
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
# unstaged 变更
|
|
90
|
+
if ! git diff --quiet 2>/dev/null; then
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
# untracked 文件
|
|
94
|
+
if [[ -n "$(git ls-files --others --exclude-standard 2>/dev/null)" ]]; then
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ! has_changes; then
|
|
101
|
+
echo "No changes to commit."
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# ============================================================
|
|
106
|
+
# git add -A
|
|
107
|
+
# ============================================================
|
|
108
|
+
git add -A
|
|
109
|
+
|
|
110
|
+
# ============================================================
|
|
111
|
+
# 构建 trailer 并提交
|
|
112
|
+
# ============================================================
|
|
113
|
+
TRAILER_ARGS=(
|
|
114
|
+
--trailer "Agent: ${AGENT_PLATFORM}"
|
|
115
|
+
--trailer "Checkpoint-Type: ${CHECKPOINT_TYPE}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if [[ -n "$TRUNCATED_PROMPT" ]]; then
|
|
119
|
+
TRAILER_ARGS+=(--trailer "User-Prompt: ${TRUNCATED_PROMPT}")
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
echo "$MESSAGE" | git interpret-trailers "${TRAILER_ARGS[@]}" | git commit -F -
|
|
123
|
+
|
|
124
|
+
echo "Checkpoint committed successfully."
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
check_uncommitted.ps1 — Stop Hook: 检查未提交的变更 (Windows PowerShell)
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
在 AI 对话结束时触发,检查工作区是否存在未提交的变更。
|
|
7
|
+
如果存在,输出提醒信息让 AI Agent 执行 fallback checkpoint commit。
|
|
8
|
+
|
|
9
|
+
支持平台:
|
|
10
|
+
- Cursor: stop hook (stdin JSON 含 workspace_roots)
|
|
11
|
+
- Claude Code: Stop hook (stdin JSON 含 hook_event_name)
|
|
12
|
+
|
|
13
|
+
输出协议:
|
|
14
|
+
- 无问题: {} (空 JSON)
|
|
15
|
+
- 有问题 (Cursor): {"followup_message": "..."}
|
|
16
|
+
- 有问题 (Claude Code): {"decision": "block", "reason": "..."}
|
|
17
|
+
#>
|
|
18
|
+
|
|
19
|
+
$ErrorActionPreference = "Stop"
|
|
20
|
+
|
|
21
|
+
# ============================================================
|
|
22
|
+
# 辅助函数
|
|
23
|
+
# ============================================================
|
|
24
|
+
|
|
25
|
+
function Output-Allow {
|
|
26
|
+
Write-Output '{}'
|
|
27
|
+
exit 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Output-Block {
|
|
31
|
+
param(
|
|
32
|
+
[string]$Message,
|
|
33
|
+
[string]$Platform
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# 转义 JSON 字符串
|
|
37
|
+
$Escaped = $Message -replace '\\', '\\\\' `
|
|
38
|
+
-replace '"', '\"' `
|
|
39
|
+
-replace "`r`n", '\n' `
|
|
40
|
+
-replace "`n", '\n' `
|
|
41
|
+
-replace "`r", '\r' `
|
|
42
|
+
-replace "`t", '\t'
|
|
43
|
+
|
|
44
|
+
switch ($Platform) {
|
|
45
|
+
"cursor" {
|
|
46
|
+
Write-Output "{`"followup_message`":`"$Escaped`"}"
|
|
47
|
+
}
|
|
48
|
+
"claude_code" {
|
|
49
|
+
Write-Output "{`"decision`":`"block`",`"reason`":`"$Escaped`"}"
|
|
50
|
+
}
|
|
51
|
+
default {
|
|
52
|
+
Write-Output "{`"message`":`"$Escaped`"}"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exit 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ============================================================
|
|
59
|
+
# 平台检测
|
|
60
|
+
# ============================================================
|
|
61
|
+
|
|
62
|
+
function Detect-Platform {
|
|
63
|
+
param($InputData)
|
|
64
|
+
|
|
65
|
+
if (-not $InputData) {
|
|
66
|
+
return "unknown"
|
|
67
|
+
}
|
|
68
|
+
if ($InputData.PSObject.Properties["hook_event_name"] -or
|
|
69
|
+
$InputData.PSObject.Properties["tool_name"]) {
|
|
70
|
+
return "claude_code"
|
|
71
|
+
}
|
|
72
|
+
return "cursor"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# ============================================================
|
|
76
|
+
# Workspace Root 检测
|
|
77
|
+
# ============================================================
|
|
78
|
+
|
|
79
|
+
function Get-WorkspaceRoot {
|
|
80
|
+
param($InputData)
|
|
81
|
+
|
|
82
|
+
# 优先从 stdin JSON 获取
|
|
83
|
+
if ($InputData) {
|
|
84
|
+
foreach ($field in @("workspace_roots", "workspaceRoots")) {
|
|
85
|
+
$roots = $null
|
|
86
|
+
try { $roots = $InputData.$field } catch {}
|
|
87
|
+
if ($roots -and $roots.Count -gt 0) {
|
|
88
|
+
return $roots[0]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# 回退到环境变量
|
|
94
|
+
foreach ($envVar in @("CURSOR_PROJECT_DIR", "CLAUDE_PROJECT_DIR", "WORKSPACE_ROOT", "PROJECT_ROOT")) {
|
|
95
|
+
$val = [Environment]::GetEnvironmentVariable($envVar)
|
|
96
|
+
if ($val) { return $val }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# 最终回退到当前目录
|
|
100
|
+
return (Get-Location).Path
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# ============================================================
|
|
104
|
+
# Git 操作
|
|
105
|
+
# ============================================================
|
|
106
|
+
|
|
107
|
+
function Test-GitRepo {
|
|
108
|
+
param([string]$Path)
|
|
109
|
+
try {
|
|
110
|
+
$null = git -C $Path rev-parse --is-inside-work-tree 2>$null
|
|
111
|
+
return $LASTEXITCODE -eq 0
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return $false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Test-UncommittedChanges {
|
|
119
|
+
param([string]$Path)
|
|
120
|
+
$status = git -C $Path status --porcelain 2>$null
|
|
121
|
+
return [bool]$status
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Get-ChangeSummary {
|
|
125
|
+
param(
|
|
126
|
+
[string]$Path,
|
|
127
|
+
[int]$MaxLines = 20
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
$output = git -C $Path status --short 2>$null
|
|
131
|
+
if (-not $output) { return "" }
|
|
132
|
+
|
|
133
|
+
$lines = $output -split "`n"
|
|
134
|
+
if ($lines.Count -gt $MaxLines) {
|
|
135
|
+
$shown = $lines[0..($MaxLines - 1)]
|
|
136
|
+
$shown += " ... and $($lines.Count - $MaxLines) more files"
|
|
137
|
+
return ($shown -join "`n")
|
|
138
|
+
}
|
|
139
|
+
return ($lines -join "`n")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ============================================================
|
|
143
|
+
# 主逻辑
|
|
144
|
+
# ============================================================
|
|
145
|
+
|
|
146
|
+
# 从 stdin 读取 JSON
|
|
147
|
+
$InputData = $null
|
|
148
|
+
try {
|
|
149
|
+
$rawInput = [Console]::In.ReadToEnd()
|
|
150
|
+
if ($rawInput.Trim()) {
|
|
151
|
+
$InputData = $rawInput | ConvertFrom-Json
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
# 忽略 JSON 解析错误
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Claude Code 的 stop_hook_active 防止无限循环
|
|
159
|
+
if ($InputData -and $InputData.PSObject.Properties["stop_hook_active"]) {
|
|
160
|
+
if ($InputData.stop_hook_active -eq $true) {
|
|
161
|
+
Output-Allow
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# 检测平台
|
|
166
|
+
$Platform = Detect-Platform -InputData $InputData
|
|
167
|
+
|
|
168
|
+
# 获取 workspace root
|
|
169
|
+
$Workspace = Get-WorkspaceRoot -InputData $InputData
|
|
170
|
+
|
|
171
|
+
# 检查是否为 git 仓库
|
|
172
|
+
if (-not (Test-GitRepo -Path $Workspace)) {
|
|
173
|
+
Output-Allow
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# 检查是否有未提交变更
|
|
177
|
+
if (-not (Test-UncommittedChanges -Path $Workspace)) {
|
|
178
|
+
Output-Allow
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# 获取变更摘要
|
|
182
|
+
$Changes = Get-ChangeSummary -Path $Workspace
|
|
183
|
+
$ChangesIndented = ($Changes -split "`n" | ForEach-Object { " $_" }) -join "`n"
|
|
184
|
+
|
|
185
|
+
# 构建提醒消息
|
|
186
|
+
$Reminder = @"
|
|
187
|
+
## ⚠️ Uncommitted Changes Detected
|
|
188
|
+
|
|
189
|
+
There are uncommitted changes in the workspace. Please create a checkpoint commit before ending the conversation.
|
|
190
|
+
|
|
191
|
+
**Changed files:**
|
|
192
|
+
``````
|
|
193
|
+
$ChangesIndented
|
|
194
|
+
``````
|
|
195
|
+
|
|
196
|
+
**Action Required**: Run the checkpoint script to commit these changes:
|
|
197
|
+
|
|
198
|
+
**macOS/Linux:**
|
|
199
|
+
``````bash
|
|
200
|
+
~/.agent-better-checkpoint/scripts/checkpoint.sh "checkpoint(<scope>): <description>" "<user-prompt>" --type fallback
|
|
201
|
+
``````
|
|
202
|
+
|
|
203
|
+
**Windows (PowerShell):**
|
|
204
|
+
``````powershell
|
|
205
|
+
powershell -File "`$env:USERPROFILE/.agent-better-checkpoint/scripts/checkpoint.ps1" "checkpoint(<scope>): <description>" "<user-prompt>" -Type fallback
|
|
206
|
+
``````
|
|
207
|
+
"@
|
|
208
|
+
|
|
209
|
+
Output-Block -Message $Reminder -Platform $Platform
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
checkpoint.ps1 — 创建语义化的 Git checkpoint commit (Windows PowerShell)
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
AI 生成描述性内容(subject + body),本脚本负责:
|
|
7
|
+
1. 截断 user-prompt(≤60字符,头尾截断)
|
|
8
|
+
2. 通过 git interpret-trailers 追加元信息
|
|
9
|
+
3. 执行 git add -A && git commit
|
|
10
|
+
|
|
11
|
+
.PARAMETER Message
|
|
12
|
+
Full commit message (subject + blank line + body). Required.
|
|
13
|
+
|
|
14
|
+
.PARAMETER UserPrompt
|
|
15
|
+
The user's original prompt/request. Optional.
|
|
16
|
+
|
|
17
|
+
.PARAMETER Type
|
|
18
|
+
Checkpoint type: "auto" (default) or "fallback".
|
|
19
|
+
|
|
20
|
+
.EXAMPLE
|
|
21
|
+
.\checkpoint.ps1 "checkpoint(auth): add JWT refresh" "implement token refresh"
|
|
22
|
+
#>
|
|
23
|
+
|
|
24
|
+
param(
|
|
25
|
+
[Parameter(Mandatory = $true, Position = 0)]
|
|
26
|
+
[string]$Message,
|
|
27
|
+
|
|
28
|
+
[Parameter(Position = 1)]
|
|
29
|
+
[string]$UserPrompt = "",
|
|
30
|
+
|
|
31
|
+
[string]$Type = "auto"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
$ErrorActionPreference = "Stop"
|
|
35
|
+
|
|
36
|
+
# ============================================================
|
|
37
|
+
# 平台检测
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
function Detect-Platform {
|
|
41
|
+
if ($env:CLAUDE_CODE -or (Get-Command claude -ErrorAction SilentlyContinue)) {
|
|
42
|
+
return "claude-code"
|
|
43
|
+
}
|
|
44
|
+
if ($env:CURSOR_VERSION -or $env:CURSOR_TRACE_ID) {
|
|
45
|
+
return "cursor"
|
|
46
|
+
}
|
|
47
|
+
return "unknown"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$AgentPlatform = Detect-Platform
|
|
51
|
+
|
|
52
|
+
# ============================================================
|
|
53
|
+
# User-Prompt 截断(≤60 字符,头尾保留 + 中间省略号)
|
|
54
|
+
# ============================================================
|
|
55
|
+
|
|
56
|
+
function Truncate-Prompt {
|
|
57
|
+
param([string]$Prompt)
|
|
58
|
+
|
|
59
|
+
$MaxLen = 60
|
|
60
|
+
if ($Prompt.Length -le $MaxLen) {
|
|
61
|
+
return $Prompt
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$HeadLen = [math]::Floor(($MaxLen - 3) / 2)
|
|
65
|
+
$TailLen = $MaxLen - 3 - $HeadLen
|
|
66
|
+
$Head = $Prompt.Substring(0, $HeadLen)
|
|
67
|
+
$Tail = $Prompt.Substring($Prompt.Length - $TailLen, $TailLen)
|
|
68
|
+
return "${Head}...${Tail}"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$TruncatedPrompt = ""
|
|
72
|
+
if ($UserPrompt) {
|
|
73
|
+
$TruncatedPrompt = Truncate-Prompt -Prompt $UserPrompt
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ============================================================
|
|
77
|
+
# 检查是否有变更
|
|
78
|
+
# ============================================================
|
|
79
|
+
|
|
80
|
+
function Test-HasChanges {
|
|
81
|
+
# staged 变更
|
|
82
|
+
$diffCached = git diff --cached --quiet 2>$null
|
|
83
|
+
if ($LASTEXITCODE -ne 0) { return $true }
|
|
84
|
+
|
|
85
|
+
# unstaged 变更
|
|
86
|
+
$diffWorking = git diff --quiet 2>$null
|
|
87
|
+
if ($LASTEXITCODE -ne 0) { return $true }
|
|
88
|
+
|
|
89
|
+
# untracked 文件
|
|
90
|
+
$untracked = git ls-files --others --exclude-standard 2>$null
|
|
91
|
+
if ($untracked) { return $true }
|
|
92
|
+
|
|
93
|
+
return $false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (-not (Test-HasChanges)) {
|
|
97
|
+
Write-Host "No changes to commit."
|
|
98
|
+
exit 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ============================================================
|
|
102
|
+
# git add -A
|
|
103
|
+
# ============================================================
|
|
104
|
+
|
|
105
|
+
git add -A
|
|
106
|
+
|
|
107
|
+
# ============================================================
|
|
108
|
+
# 构建 trailer 并提交
|
|
109
|
+
# ============================================================
|
|
110
|
+
|
|
111
|
+
$TrailerArgs = @(
|
|
112
|
+
"--trailer", "Agent: $AgentPlatform",
|
|
113
|
+
"--trailer", "Checkpoint-Type: $Type"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if ($TruncatedPrompt) {
|
|
117
|
+
$TrailerArgs += @("--trailer", "User-Prompt: $TruncatedPrompt")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# 通过管道传递消息 → git interpret-trailers → git commit
|
|
121
|
+
$Message | git interpret-trailers @TrailerArgs | git commit -F -
|
|
122
|
+
|
|
123
|
+
Write-Host "Checkpoint committed successfully."
|