@vibe-x/agent-better-checkpoint 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Agent Better Checkpoint
1
+ # Agent Better Checkpoint (ABC)
2
2
 
3
3
  **One-line install, zero config.** Turns AI agent edits into transparent, queryable Git commits.
4
4
 
@@ -6,6 +6,8 @@
6
6
  npx @vibe-x/agent-better-checkpoint
7
7
  ```
8
8
 
9
+ ![Agent Better Checkpoint](./assets/agent-better-checkpoint.png)
10
+
9
11
  That's it. Your AI coding assistant (Cursor, Claude Code) will now auto-commit every meaningful edit with semantic messages and structured metadata — no more opaque checkpoints.
10
12
 
11
13
  ---
@@ -40,6 +42,19 @@ git log --format="%(trailers:key=Agent,valueonly)" # by agent
40
42
  git log --grep="User-Prompt:.*registration" # by prompt keyword
41
43
  ```
42
44
 
45
+ ### Works with Your Favorite Git Tools
46
+
47
+ Since every checkpoint is a standard Git commit, the entire Git ecosystem is at your disposal — what was once a hidden, platform-specific checkpoint becomes a first-class citizen you can browse, search, diff, and rebase.
48
+
49
+ | Tool | Type | What You Get |
50
+ |------|------|-------------|
51
+ | [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) | VS Code / Cursor Extension | Inline blame, file history, visual commit graph, interactive rebase — see *who* (you or the AI) changed *what* and *when*, right in the editor |
52
+ | [lazygit](https://github.com/jesseduffield/lazygit) | Terminal UI | Fast keyboard-driven staging, diff browsing, cherry-pick, and rebase across checkpoint commits |
53
+ | [tig](https://github.com/jonas/tig) | Terminal UI | Lightweight ncurses Git log viewer, great for quickly scanning checkpoint history |
54
+ | [GitHub / GitLab Web UI](https://github.com) | Web | Browse, compare, and share checkpoint history online after pushing |
55
+
56
+ For example, with **GitLens in Cursor** you can hover any line to see which checkpoint introduced it and the original user prompt, view all checkpoints for a file in a timeline, or search commits by `checkpoint(` prefix and `User-Prompt` trailer content.
57
+
43
58
  ---
44
59
 
45
60
  ## How It Works
@@ -93,11 +108,13 @@ The AI agent will auto-bootstrap the runtime scripts on first use.
93
108
 
94
109
  | Location | Content |
95
110
  |----------|---------|
96
- | `~/.agent-better-checkpoint/scripts/` | Commit script (`checkpoint.sh` / `.ps1`) |
97
- | `~/.agent-better-checkpoint/hooks/stop/` | Stop hook (`check_uncommitted.sh` / `.ps1`) |
111
+ | `~/.vibe-x/agent-better-checkpoint/scripts/` | Commit script (`checkpoint.sh` / `.ps1`) |
112
+ | `~/.vibe-x/agent-better-checkpoint/hooks/stop/` | Stop hook (`check_uncommitted.sh` / `.ps1`) |
98
113
  | Platform skill directory | `SKILL.md` — AI agent instructions |
99
114
  | Platform hook config | Stop hook registration |
100
115
 
116
+ > **Project-local mode**: Projects can also commit `.vibe-x/agent-better-checkpoint/` (config + scripts) for self-contained setup. When present, the global hook delegates to the project-local scripts automatically.
117
+
101
118
  ### Uninstall
102
119
 
103
120
  ```bash
package/bin/cli.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * agent-better-checkpoint 安装器 (Node.js)
4
+ * agent-better-checkpoint installer (Node.js)
5
5
  *
6
- * 通过 npx 一键安装 checkpoint 脚本、stop hook SKILL.md 到用户环境。
7
- * 按平台(macOS/Linux vs Windows)选择性部署对应脚本。
6
+ * One-click install via npx: checkpoint scripts, stop hook, and SKILL.md to user env.
7
+ * Deploys platform-specific scripts (macOS/Linux vs Windows).
8
8
  *
9
9
  * Usage:
10
10
  * npx @vibe-x/agent-better-checkpoint
@@ -18,22 +18,22 @@ import { homedir, platform } from 'node:os';
18
18
  import { fileURLToPath } from 'node:url';
19
19
 
20
20
  // ============================================================
21
- // 路径常量
21
+ // Path constants
22
22
  // ============================================================
23
23
 
24
24
  const __filename = fileURLToPath(import.meta.url);
25
25
  const __dirname = dirname(__filename);
26
26
  const PKG_ROOT = resolve(__dirname, '..');
27
27
 
28
- const INSTALL_BASE = join(homedir(), '.agent-better-checkpoint');
28
+ const INSTALL_BASE = join(homedir(), '.vibe-x', 'agent-better-checkpoint');
29
29
  const SKILL_NAME = 'agent-better-checkpoint';
30
30
 
31
- // 包内源文件路径
31
+ // In-package source paths
32
32
  const PLATFORM_DIR = join(PKG_ROOT, 'platform');
33
33
  const SKILL_SRC = join(PKG_ROOT, 'skill', 'SKILL.md');
34
34
 
35
35
  // ============================================================
36
- // 参数解析
36
+ // Argument parsing
37
37
  // ============================================================
38
38
 
39
39
  function parseArgs(argv) {
@@ -79,12 +79,12 @@ Options:
79
79
  }
80
80
 
81
81
  // ============================================================
82
- // 平台检测
82
+ // Platform detection
83
83
  // ============================================================
84
84
 
85
85
  function detectAIPlatform() {
86
86
  const home = homedir();
87
- // 优先 Claude(如果两者都存在,用户可以用 --platform 覆盖)
87
+ // Prefer Claude (if both exist, user can override with --platform)
88
88
  if (existsSync(join(home, '.claude'))) return 'claude';
89
89
  if (existsSync(join(home, '.cursor'))) return 'cursor';
90
90
  return null;
@@ -97,7 +97,7 @@ function getOSType() {
97
97
  }
98
98
 
99
99
  // ============================================================
100
- // 文件操作辅助
100
+ // File operation helpers
101
101
  // ============================================================
102
102
 
103
103
  function ensureDir(dir) {
@@ -116,7 +116,7 @@ function setExecutable(filepath) {
116
116
  const st = statSync(filepath);
117
117
  chmodSync(filepath, st.mode | 0o111);
118
118
  } catch {
119
- // Windows chmod 可能无效,忽略
119
+ // chmod may be ineffective on Windows, ignore
120
120
  }
121
121
  }
122
122
 
@@ -134,7 +134,7 @@ function writeJsonFile(filepath, data) {
134
134
  }
135
135
 
136
136
  // ============================================================
137
- // 安装逻辑
137
+ // Install logic
138
138
  // ============================================================
139
139
 
140
140
  function installScripts(osType) {
@@ -176,7 +176,7 @@ function installSkill(aiPlatform) {
176
176
  let skillDest;
177
177
 
178
178
  if (aiPlatform === 'cursor') {
179
- // 检查 skills.sh 安装路径
179
+ // Check skills.sh install path
180
180
  const skillsShPath = join(homedir(), '.cursor', 'skills', SKILL_NAME, 'SKILL.md');
181
181
  if (existsSync(skillsShPath)) {
182
182
  console.log(` Skill → already installed at ${skillsShPath} (skipped)`);
@@ -186,7 +186,7 @@ function installSkill(aiPlatform) {
186
186
  skillDir = join(homedir(), '.cursor', 'skills', SKILL_NAME);
187
187
  skillDest = join(skillDir, 'SKILL.md');
188
188
  } else if (aiPlatform === 'claude') {
189
- // Claude Code: 按标准 skills 目录安装
189
+ // Claude Code: install to standard skills directory
190
190
  const skillsRootDir = join(homedir(), '.claude', 'skills');
191
191
  skillDir = join(skillsRootDir, SKILL_NAME);
192
192
  skillDest = join(skillDir, 'SKILL.md');
@@ -208,7 +208,7 @@ function registerCursorHook(osType) {
208
208
  if (!config.hooks) config.hooks = {};
209
209
  if (!config.hooks.stop) config.hooks.stop = [];
210
210
 
211
- // 构建 hook 命令
211
+ // Build hook command
212
212
  let hookCmd;
213
213
  if (osType === 'unix') {
214
214
  hookCmd = `bash ${INSTALL_BASE}/hooks/stop/check_uncommitted.sh`;
@@ -216,12 +216,12 @@ function registerCursorHook(osType) {
216
216
  hookCmd = `powershell -File "${INSTALL_BASE}\\hooks\\stop\\check_uncommitted.ps1"`;
217
217
  }
218
218
 
219
- // 检查是否已注册
219
+ // Check if already registered
220
220
  const alreadyRegistered = config.hooks.stop.some(
221
221
  h => typeof h === 'object' && h.command && h.command.includes(SKILL_NAME.replace(/-/g, ''))
222
222
  );
223
223
 
224
- // 用更精确的检测:检查命令中是否包含 agent-better-checkpoint
224
+ // More precise check: command includes agent-better-checkpoint
225
225
  const registered = config.hooks.stop.some(
226
226
  h => typeof h === 'object' && h.command && h.command.includes('agent-better-checkpoint')
227
227
  );
@@ -265,7 +265,7 @@ function registerClaudeHook(osType) {
265
265
  }
266
266
 
267
267
  // ============================================================
268
- // 卸载逻辑
268
+ // Uninstall logic
269
269
  // ============================================================
270
270
 
271
271
  function uninstallScripts() {
@@ -324,7 +324,7 @@ function unregisterClaudeHook() {
324
324
  }
325
325
 
326
326
  // ============================================================
327
- // 主入口
327
+ // Main entry
328
328
  // ============================================================
329
329
 
330
330
  function main() {
@@ -341,7 +341,7 @@ function main() {
341
341
  }
342
342
 
343
343
  if (args.uninstall) {
344
- // 卸载流程
344
+ // Uninstall flow
345
345
  console.log(`\n[${aiPlatform === 'cursor' ? 'Cursor' : 'Claude Code'}] Uninstalling...`);
346
346
 
347
347
  if (aiPlatform === 'cursor') {
@@ -355,7 +355,7 @@ function main() {
355
355
  uninstallScripts();
356
356
  console.log(`\n✅ Uninstallation complete!`);
357
357
  } else {
358
- // 安装流程
358
+ // Install flow
359
359
  console.log(`\n[${aiPlatform === 'cursor' ? 'Cursor' : 'Claude Code'}] Installing... (OS: ${osType})`);
360
360
 
361
361
  installScripts(osType);
@@ -369,8 +369,8 @@ function main() {
369
369
 
370
370
  console.log(`\n✅ Installation complete!`);
371
371
  console.log(`\nInstalled components:`);
372
- console.log(` 📜 Checkpoint script → ~/.agent-better-checkpoint/scripts/`);
373
- console.log(` 🔒 Stop hook → ~/.agent-better-checkpoint/hooks/stop/`);
372
+ console.log(` 📜 Checkpoint script → ~/.vibe-x/agent-better-checkpoint/scripts/`);
373
+ console.log(` 🔒 Stop hook → ~/.vibe-x/agent-better-checkpoint/hooks/stop/`);
374
374
  console.log(` 📖 SKILL.md → ${aiPlatform === 'cursor' ? '~/.cursor/skills/' : '~/.claude/skills/'}${SKILL_NAME}/`);
375
375
  console.log(`\nThe AI agent will now auto-commit with semantic messages. Happy coding! 🎉`);
376
376
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-x/agent-better-checkpoint",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Semantic Git checkpoint commits for AI coding sessions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,35 +1,37 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # check_uncommitted.sh — Stop Hook: 检查未提交的变更
3
+ # check_uncommitted.sh — Stop Hook: check for uncommitted changes
4
4
  #
5
- # AI 对话结束时触发,检查工作区是否存在未提交的变更。
6
- # 如果存在,输出提醒信息让 AI Agent 执行 fallback checkpoint commit
5
+ # Triggered at AI conversation end. Checks workspace for uncommitted changes.
6
+ # If found, outputs reminder for AI Agent to run fallback checkpoint commit.
7
7
  #
8
- # 支持平台:
9
- # - Cursor: stop hook (stdin JSON workspace_roots)
10
- # - Claude Code: Stop hook (stdin JSON hook_event_name)
8
+ # Supported platforms:
9
+ # - Cursor: stop hook (stdin JSON with workspace_roots)
10
+ # - Claude Code: Stop hook (stdin JSON with hook_event_name)
11
11
  #
12
- # 输出协议:
13
- # - 无问题: {} ( JSON)
14
- # - 有问题 (Cursor): {"followup_message": "..."}
15
- # - 有问题 (Claude Code): {"decision": "block", "reason": "..."}
12
+ # Output protocol:
13
+ # - OK: {} (empty JSON)
14
+ # - Block (Cursor): {"followup_message": "..."}
15
+ # - Block (Claude Code): {"decision": "block", "reason": "..."}
16
16
  #
17
- # JSON 解析使用 grep+sed,不依赖 jq。
17
+ # Config: .vibe-x/agent-better-checkpoint/config.yml (project-level, optional)
18
+ # JSON parsing uses grep+sed, no jq dependency.
18
19
 
19
20
  set -euo pipefail
20
21
 
21
22
  # ============================================================
22
- # 辅助函数:简易 JSON 字段提取(不依赖 jq
23
+ # Helpers: simple JSON field extraction (no jq)
23
24
  # ============================================================
24
25
 
25
- # 输出允许通过的 JSON
26
26
  output_allow() {
27
+ local info="${1:-}"
28
+ if [[ -n "$info" ]]; then
29
+ echo "[checkpoint] $info" >&2
30
+ fi
27
31
  echo '{}'
28
32
  exit 0
29
33
  }
30
34
 
31
- # 从 JSON 中提取布尔字段值
32
- # Usage: json_bool "$json" "field_name" → 输出 "true" 或 "false"
33
35
  json_bool() {
34
36
  local json="$1" field="$2"
35
37
  if echo "$json" | grep -qE "\"${field}\"[[:space:]]*:[[:space:]]*true"; then
@@ -39,8 +41,6 @@ json_bool() {
39
41
  fi
40
42
  }
41
43
 
42
- # 从 JSON 中提取字符串字段值
43
- # Usage: json_string "$json" "field_name" → 输出字符串值(不含引号)
44
44
  json_string() {
45
45
  local json="$1" field="$2"
46
46
  echo "$json" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
@@ -48,13 +48,10 @@ json_string() {
48
48
  | head -1
49
49
  }
50
50
 
51
- # 从 JSON 的 workspace_roots / workspaceRoots 数组中提取第一个路径
52
- # Usage: json_workspace_root "$json" → 输出路径
53
51
  json_workspace_root() {
54
52
  local json="$1"
55
53
  local result=""
56
54
 
57
- # 尝试 workspace_roots 和 workspaceRoots
58
55
  for field in "workspace_roots" "workspaceRoots"; do
59
56
  result=$(echo "$json" \
60
57
  | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\[[^]]*\]" \
@@ -70,7 +67,134 @@ json_workspace_root() {
70
67
  }
71
68
 
72
69
  # ============================================================
73
- # 平台检测
70
+ # Config parsing (.vibe-x/agent-better-checkpoint/config.yml)
71
+ # ============================================================
72
+
73
+ CONFIG_FILE_NAME=".vibe-x/agent-better-checkpoint/config.yml"
74
+
75
+ parse_min_changed_lines() {
76
+ local config="$1"
77
+ [[ -f "$config" ]] || return 0
78
+ local val
79
+ val=$(grep -E '^[[:space:]]+min_changed_lines:[[:space:]]*[0-9]+' "$config" 2>/dev/null \
80
+ | sed -E 's/.*min_changed_lines:[[:space:]]*([0-9]+).*/\1/' \
81
+ | head -1 || true)
82
+ echo "${val:-}"
83
+ }
84
+
85
+ parse_min_changed_files() {
86
+ local config="$1"
87
+ [[ -f "$config" ]] || return 0
88
+ local val
89
+ val=$(grep -E '^[[:space:]]+min_changed_files:[[:space:]]*[0-9]+' "$config" 2>/dev/null \
90
+ | sed -E 's/.*min_changed_files:[[:space:]]*([0-9]+).*/\1/' \
91
+ | head -1 || true)
92
+ echo "${val:-}"
93
+ }
94
+
95
+ parse_passive_patterns() {
96
+ local config="$1"
97
+ [[ -f "$config" ]] || return 0
98
+ local in_section=false
99
+ while IFS= read -r line || [[ -n "$line" ]]; do
100
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
101
+ [[ -z "$line" ]] && continue
102
+ if [[ "$line" =~ ^passive_patterns: ]]; then
103
+ in_section=true
104
+ continue
105
+ fi
106
+ if $in_section; then
107
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+(.*) ]]; then
108
+ local val="${BASH_REMATCH[1]}"
109
+ val="${val#\"}" && val="${val%\"}"
110
+ val="${val#\'}" && val="${val%\'}"
111
+ val=$(echo "$val" | sed 's/[[:space:]]*#.*//;s/[[:space:]]*$//')
112
+ [[ -n "$val" ]] && echo "$val"
113
+ elif [[ ! "$line" =~ ^[[:space:]] ]]; then
114
+ break
115
+ fi
116
+ fi
117
+ done < "$config"
118
+ }
119
+
120
+ # ============================================================
121
+ # Passive file matching
122
+ # ============================================================
123
+
124
+ match_pattern() {
125
+ local file="$1" pattern="$2"
126
+
127
+ # dir/** → match all files under dir/
128
+ if [[ "$pattern" == *"/**" ]]; then
129
+ local prefix="${pattern%/**}/"
130
+ [[ "$file" == "$prefix"* ]] && return 0
131
+ return 1
132
+ fi
133
+
134
+ # *.ext → match suffix
135
+ if [[ "$pattern" == \*.* ]]; then
136
+ local suffix="${pattern#\*}"
137
+ [[ "$file" == *"$suffix" ]] && return 0
138
+ return 1
139
+ fi
140
+
141
+ # Exact match
142
+ [[ "$file" == "$pattern" ]] && return 0
143
+ return 1
144
+ }
145
+
146
+ is_passive_file() {
147
+ local file="$1"
148
+ shift
149
+ local patterns=("$@")
150
+ for pattern in "${patterns[@]}"; do
151
+ if match_pattern "$file" "$pattern"; then
152
+ return 0
153
+ fi
154
+ done
155
+ return 1
156
+ }
157
+
158
+ # ============================================================
159
+ # Changed line count
160
+ # ============================================================
161
+
162
+ count_changed_lines() {
163
+ local workspace="$1"
164
+ shift
165
+ local files=("$@")
166
+ local total=0
167
+
168
+ if [[ ${#files[@]} -eq 0 ]]; then
169
+ echo 0
170
+ return
171
+ fi
172
+
173
+ # Tracked files: staged + unstaged diff lines (batch count)
174
+ local tracked_lines
175
+ tracked_lines=$(
176
+ { git -C "$workspace" diff --numstat -- "${files[@]}" 2>/dev/null || true
177
+ git -C "$workspace" diff --cached --numstat -- "${files[@]}" 2>/dev/null || true
178
+ } | awk '{a=$1; d=$2; if(a!="-") t+=a; if(d!="-") t+=d} END {print t+0}'
179
+ )
180
+ total=$((total + tracked_lines))
181
+
182
+ # Untracked files: total lines count as changes
183
+ for file in "${files[@]}"; do
184
+ if [[ -n "$(git -C "$workspace" ls-files --others --exclude-standard -- "$file" 2>/dev/null)" ]]; then
185
+ if [[ -f "${workspace}/${file}" ]]; then
186
+ local lines
187
+ lines=$(wc -l < "${workspace}/${file}" 2>/dev/null || echo 0)
188
+ total=$((total + ${lines## }))
189
+ fi
190
+ fi
191
+ done
192
+
193
+ echo "$total"
194
+ }
195
+
196
+ # ============================================================
197
+ # Platform detection
74
198
  # ============================================================
75
199
 
76
200
  detect_platform() {
@@ -79,7 +203,6 @@ detect_platform() {
79
203
  echo "unknown"
80
204
  return
81
205
  fi
82
- # Claude Code: 有 hook_event_name 或 tool_name
83
206
  if echo "$json" | grep -qE '"hook_event_name"|"tool_name"'; then
84
207
  echo "claude_code"
85
208
  else
@@ -88,13 +211,12 @@ detect_platform() {
88
211
  }
89
212
 
90
213
  # ============================================================
91
- # Workspace Root 检测
214
+ # Workspace root detection
92
215
  # ============================================================
93
216
 
94
217
  get_workspace_root() {
95
218
  local json="$1"
96
219
 
97
- # 优先从 stdin JSON 获取
98
220
  if [[ -n "$json" ]]; then
99
221
  local ws
100
222
  ws=$(json_workspace_root "$json")
@@ -104,7 +226,6 @@ get_workspace_root() {
104
226
  fi
105
227
  fi
106
228
 
107
- # 回退到环境变量
108
229
  for env_var in CURSOR_PROJECT_DIR CLAUDE_PROJECT_DIR WORKSPACE_ROOT PROJECT_ROOT; do
109
230
  local val="${!env_var:-}"
110
231
  if [[ -n "$val" ]]; then
@@ -113,24 +234,17 @@ get_workspace_root() {
113
234
  fi
114
235
  done
115
236
 
116
- # 最终回退到 PWD / cwd
117
237
  echo "${PWD:-$(pwd)}"
118
238
  }
119
239
 
120
240
  # ============================================================
121
- # Git 操作
241
+ # Git operations
122
242
  # ============================================================
123
243
 
124
244
  is_git_repo() {
125
245
  git -C "$1" rev-parse --is-inside-work-tree &>/dev/null
126
246
  }
127
247
 
128
- has_uncommitted_changes() {
129
- local status
130
- status=$(git -C "$1" status --porcelain 2>/dev/null)
131
- [[ -n "$status" ]]
132
- }
133
-
134
248
  get_change_summary() {
135
249
  local workspace="$1"
136
250
  local max_lines="${2:-20}"
@@ -153,17 +267,16 @@ get_change_summary() {
153
267
  }
154
268
 
155
269
  # ============================================================
156
- # 输出提醒
270
+ # Output reminder
157
271
  # ============================================================
158
272
 
159
- # 转义字符串用于 JSON 值(处理引号、反斜杠、换行等)
160
273
  json_escape() {
161
274
  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
275
+ str="${str//\\/\\\\}"
276
+ str="${str//\"/\\\"}"
277
+ str="${str//$'\n'/\\n}"
278
+ str="${str//$'\r'/\\r}"
279
+ str="${str//$'\t'/\\t}"
167
280
  echo "$str"
168
281
  }
169
282
 
@@ -184,17 +297,15 @@ output_block() {
184
297
  }
185
298
 
186
299
  # ============================================================
187
- # 主逻辑
300
+ # Main logic
188
301
  # ============================================================
189
302
 
190
303
  main() {
191
- # 从 stdin 读取 JSON(非阻塞:如果无输入则为空)
192
304
  local input=""
193
305
  if [[ ! -t 0 ]]; then
194
306
  input=$(cat)
195
307
  fi
196
308
 
197
- # Claude Code 的 stop_hook_active 防止无限循环
198
309
  if [[ -n "$input" ]]; then
199
310
  local stop_active
200
311
  stop_active=$(json_bool "$input" "stop_hook_active")
@@ -203,31 +314,131 @@ main() {
203
314
  fi
204
315
  fi
205
316
 
206
- # 检测平台
207
317
  local platform
208
318
  platform=$(detect_platform "$input")
209
319
 
210
- # 获取 workspace root
211
320
  local workspace
212
321
  workspace=$(get_workspace_root "$input")
213
322
 
214
- # 检查是否为 git 仓库
323
+ # Delegate to project-local script when present (committed with project)
324
+ local project_script="${workspace}/.vibe-x/agent-better-checkpoint/check_uncommitted.sh"
325
+ if [[ -f "$project_script" ]] && [[ -x "$project_script" ]]; then
326
+ echo "$input" | bash "$project_script"
327
+ exit $?
328
+ fi
329
+
215
330
  if ! is_git_repo "$workspace"; then
216
331
  output_allow
217
332
  fi
218
333
 
219
- # 检查是否有未提交变更
220
- if ! has_uncommitted_changes "$workspace"; then
334
+ # Get all changes
335
+ local status_output
336
+ status_output=$(git -C "$workspace" status --porcelain 2>/dev/null)
337
+
338
+ if [[ -z "$status_output" ]]; then
221
339
  output_allow
222
340
  fi
223
341
 
224
- # 获取变更摘要
342
+ # Load config
343
+ local config_file="${workspace}/${CONFIG_FILE_NAME}"
344
+ local -a passive_patterns=()
345
+ local min_changed_lines=""
346
+ local min_changed_files=""
347
+
348
+ if [[ -f "$config_file" ]]; then
349
+ while IFS= read -r p; do
350
+ [[ -n "$p" ]] && passive_patterns+=("$p")
351
+ done < <(parse_passive_patterns "$config_file")
352
+
353
+ min_changed_lines=$(parse_min_changed_lines "$config_file")
354
+ min_changed_files=$(parse_min_changed_files "$config_file")
355
+ fi
356
+
357
+ # ---- 分离主动/被动文件 ----
358
+ local -a active_files=()
359
+ local -a passive_files_list=()
360
+
361
+ while IFS= read -r line; do
362
+ local file="${line:3}"
363
+ # Handle rename: "old -> new"
364
+ if [[ "$file" == *" -> "* ]]; then
365
+ file="${file##* -> }"
366
+ fi
367
+ # Strip possible quotes
368
+ file="${file#\"}" && file="${file%\"}"
369
+
370
+ if [[ ${#passive_patterns[@]} -gt 0 ]] && is_passive_file "$file" "${passive_patterns[@]}"; then
371
+ passive_files_list+=("$file")
372
+ else
373
+ active_files+=("$file")
374
+ fi
375
+ done <<< "$status_output"
376
+
377
+ # ---- No active files → only passive changes, skip ----
378
+ if [[ ${#active_files[@]} -eq 0 ]]; then
379
+ output_allow "Skipped: only passive file changes (${#passive_files_list[@]} files). Patterns: ${passive_patterns[*]}"
380
+ fi
381
+
382
+ # ---- No threshold config → trigger on any active change (backward compat) ----
383
+ if [[ -z "$min_changed_lines" ]] && [[ -z "$min_changed_files" ]]; then
384
+ # Use original reminder logic
385
+ build_and_output_reminder "$workspace" "$platform"
386
+ fi
387
+
388
+ # ---- Check trigger conditions (OR) ----
389
+ local triggered=false
390
+ local active_file_count=${#active_files[@]}
391
+ local active_line_count=0
392
+
393
+ # Check file count first (cheaper)
394
+ if [[ -n "$min_changed_files" ]] && [[ $active_file_count -ge $min_changed_files ]]; then
395
+ triggered=true
396
+ fi
397
+
398
+ # Then check line count (requires diff)
399
+ if [[ "$triggered" != "true" ]] && [[ -n "$min_changed_lines" ]]; then
400
+ active_line_count=$(count_changed_lines "$workspace" "${active_files[@]}")
401
+ if [[ $active_line_count -ge $min_changed_lines ]]; then
402
+ triggered=true
403
+ fi
404
+ fi
405
+
406
+ if [[ "$triggered" != "true" ]]; then
407
+ # Compute line count (if not yet, for output)
408
+ if [[ $active_line_count -eq 0 ]] && [[ -n "$min_changed_lines" ]]; then
409
+ : # Already computed
410
+ elif [[ $active_line_count -eq 0 ]]; then
411
+ active_line_count=$(count_changed_lines "$workspace" "${active_files[@]}")
412
+ fi
413
+ output_allow "Skipped: changes below threshold (${active_file_count} files, ${active_line_count} lines). Config: min_changed_files=${min_changed_files:-unset}, min_changed_lines=${min_changed_lines:-unset}"
414
+ fi
415
+
416
+ # ---- Threshold reached, trigger commit reminder ----
417
+ build_and_output_reminder "$workspace" "$platform"
418
+ }
419
+
420
+ build_and_output_reminder() {
421
+ local workspace="$1"
422
+ local platform="$2"
423
+
225
424
  local changes
226
425
  changes=$(get_change_summary "$workspace")
227
426
  local changes_indented
228
427
  changes_indented=$(echo "$changes" | sed 's/^/ /')
229
428
 
230
- # 构建提醒消息
429
+ # Project-local script; fallback to global
430
+ local checkpoint_cmd_sh checkpoint_cmd_ps1
431
+ if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/checkpoint.sh" ]]; then
432
+ checkpoint_cmd_sh=".vibe-x/agent-better-checkpoint/checkpoint.sh"
433
+ else
434
+ checkpoint_cmd_sh="~/.vibe-x/agent-better-checkpoint/scripts/checkpoint.sh"
435
+ fi
436
+ if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/checkpoint.ps1" ]]; then
437
+ checkpoint_cmd_ps1='.\\.vibe-x\\agent-better-checkpoint\\checkpoint.ps1'
438
+ else
439
+ checkpoint_cmd_ps1='\$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/checkpoint.ps1'
440
+ fi
441
+
231
442
  local reminder
232
443
  reminder="## ⚠️ Uncommitted Changes Detected
233
444
 
@@ -242,12 +453,12 @@ ${changes_indented}
242
453
 
243
454
  **macOS/Linux:**
244
455
  \`\`\`bash
245
- ~/.agent-better-checkpoint/scripts/checkpoint.sh \"checkpoint(<scope>): <description>\" \"<user-prompt>\" --type fallback
456
+ ${checkpoint_cmd_sh} \"checkpoint(<scope>): <description>\" \"<user-prompt>\" --type fallback
246
457
  \`\`\`
247
458
 
248
459
  **Windows (PowerShell):**
249
460
  \`\`\`powershell
250
- powershell -File \"\$env:USERPROFILE/.agent-better-checkpoint/scripts/checkpoint.ps1\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback
461
+ powershell -File \"${checkpoint_cmd_ps1}\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback
251
462
  \`\`\`"
252
463
 
253
464
  output_block "$reminder" "$platform"