@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 +20 -3
- package/bin/cli.mjs +23 -23
- package/package.json +1 -1
- package/platform/unix/check_uncommitted.sh +263 -52
- package/platform/unix/checkpoint.sh +13 -13
- package/platform/win/check_uncommitted.ps1 +278 -57
- package/platform/win/checkpoint.ps1 +13 -13
- package/skill/SKILL.md +23 -13
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
|
+

|
|
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
|
|
4
|
+
* agent-better-checkpoint installer (Node.js)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,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
|
-
#
|
|
6
|
-
#
|
|
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
|
|
10
|
-
# - Claude Code: Stop hook (stdin JSON
|
|
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
|
-
# -
|
|
14
|
-
# -
|
|
15
|
-
# -
|
|
12
|
+
# Output protocol:
|
|
13
|
+
# - OK: {} (empty JSON)
|
|
14
|
+
# - Block (Cursor): {"followup_message": "..."}
|
|
15
|
+
# - Block (Claude Code): {"decision": "block", "reason": "..."}
|
|
16
16
|
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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}"
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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 \"
|
|
461
|
+
powershell -File \"${checkpoint_cmd_ps1}\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback
|
|
251
462
|
\`\`\`"
|
|
252
463
|
|
|
253
464
|
output_block "$reminder" "$platform"
|