neander-checkpoints 0.1.0__tar.gz
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.
- neander_checkpoints-0.1.0/LICENSE +21 -0
- neander_checkpoints-0.1.0/PKG-INFO +8 -0
- neander_checkpoints-0.1.0/README.md +199 -0
- neander_checkpoints-0.1.0/pyproject.toml +23 -0
- neander_checkpoints-0.1.0/setup.cfg +4 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/__init__.py +3 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/hooks/hooks_config.json +26 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/hooks/project_claude_snippet.md +36 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/checkpoint.sh +173 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/detect_commit.sh +55 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/link_commit.sh +29 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/on_stop.sh +45 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/parse_jsonl.py +1103 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/persist_summary.sh +28 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/redact.py +239 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/restore.sh +111 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/scripts/save_summary.sh +112 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/skills/neander-redact/SKILL.md +39 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/skills/neander-search/SKILL.md +42 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/skills/neander-session-stats/SKILL.md +28 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/skills/neander-status/SKILL.md +22 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/skills/neander-summarize/SKILL.md +94 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/bundled/skills/neander-transcript/SKILL.md +37 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/cli.py +80 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/install.py +295 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints/resume.py +195 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints.egg-info/PKG-INFO +8 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints.egg-info/SOURCES.txt +30 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints.egg-info/dependency_links.txt +1 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints.egg-info/entry_points.txt +2 -0
- neander_checkpoints-0.1.0/src/neander_checkpoints.egg-info/top_level.txt +1 -0
- neander_checkpoints-0.1.0/tests/test_parse_jsonl.py +1013 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 neander.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# neander_checkpoints
|
|
2
|
+
|
|
3
|
+
Capture Claude Code sessions alongside your Git history. Understand *why* code changed, not just *what*. Resume where you left off — even on a different machine.
|
|
4
|
+
|
|
5
|
+
Built natively for Claude Code using hooks, skills, and scripts. No external binaries. Install once, works automatically.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Your git log shows what code changed. But when AI writes your code, the *how* and *why* live in the conversation — prompts, reasoning, tool calls, dead ends, decisions. Without capturing that, you lose context the moment a session ends.
|
|
10
|
+
|
|
11
|
+
neander_checkpoints solves this:
|
|
12
|
+
|
|
13
|
+
- **Understand why code changed** — see the full prompt/response transcript and files touched for any checkpoint
|
|
14
|
+
- **Keep git history clean** — checkpoint data lives on a separate orphan branch, never pollutes your working tree
|
|
15
|
+
- **Onboard faster** — show teammates the path from prompt to change to commit
|
|
16
|
+
- **Search across checkpoints** — find the checkpoint where you fixed the auth bug, by keyword, branch, file, or just asking in natural language
|
|
17
|
+
- **Remote checkpoint support** — fetch checkpoints from remote with `--fetch`, search and browse across machines
|
|
18
|
+
- **Cross-machine resume** — push checkpoints to remote, pull them on another machine, continue exactly where you left off
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
Once installed, everything is automatic:
|
|
23
|
+
|
|
24
|
+
1. **You code with Claude** — business as usual
|
|
25
|
+
2. **On every commit**, a hook captures the session transcript and metadata as a checkpoint on the `neander/checkpoints/v1` orphan branch
|
|
26
|
+
3. **On session end**, a final checkpoint captures everything even if no commits were made
|
|
27
|
+
4. **Commits get linked** — a `Claude-Session` trailer is added so you can trace any commit back to the conversation that produced it
|
|
28
|
+
5. **All reads go through the checkpoint branch** — commands read directly from git, not local files. Use `--fetch` to pull remote checkpoint data first
|
|
29
|
+
|
|
30
|
+
When you need context, just ask naturally:
|
|
31
|
+
|
|
32
|
+
| You say | What happens |
|
|
33
|
+
|---|---|
|
|
34
|
+
| "What did I do yesterday?" | Claude searches checkpoints, shows relevant results |
|
|
35
|
+
| "Why did we make this change?" | Claude finds the checkpoint that touched the file, reads the transcript |
|
|
36
|
+
|
|
37
|
+
| "Go back to before that change" | Claude lists checkpoints and offers to restore |
|
|
38
|
+
| "How much did that checkpoint cost?" | Claude shows token usage and cost estimate |
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
All commands work as Claude Code skills — Claude auto-invokes them based on conversation context. You can also use them explicitly as slash commands.
|
|
43
|
+
|
|
44
|
+
All commands accept checkpoint IDs (16-char hex like `a3f8b9c1d2e4`), session IDs (UUIDs), or partial IDs.
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `/neander-status` | Overview of active sessions and recent checkpoints |
|
|
49
|
+
| `/neander-search` | Search checkpoints by keyword, branch, file, date, commit, or natural language |
|
|
50
|
+
| `/neander-transcript` | View the condensed conversation transcript for a checkpoint |
|
|
51
|
+
| `/neander-summarize` | Generate an AI summary (intent, outcome, learnings, friction, open items) and persist it |
|
|
52
|
+
| `/neander-session-stats` | Token usage, cost estimate, duration, files modified for a checkpoint |
|
|
53
|
+
| `/neander-redact` | Scan a transcript for secrets and PII before sharing |
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
### Status
|
|
58
|
+
|
|
59
|
+
Shows the current session and recent checkpoints at a glance.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Current: 17e8f125 · opus · feat/impl-tasks-from-td · 0.4k tokens · 0 files
|
|
63
|
+
(not yet checkpointed)
|
|
64
|
+
|
|
65
|
+
== Checkpoints (18 total) ==
|
|
66
|
+
|
|
67
|
+
Checkpoint Commit Session Date Files Topic
|
|
68
|
+
------------ -------- -------- ---------------- ----- -----
|
|
69
|
+
dfe7c7132205 70e684cf 37252de3 2026-03-28 13:20 9 Simplify the generate_tasks flow...
|
|
70
|
+
b4d88d5d4fe9 70e684cf 37252de3 2026-03-28 13:20 9
|
|
71
|
+
7b02e43d74db 67ff5c5c 37252de3 2026-03-28 13:18 9
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Search
|
|
75
|
+
|
|
76
|
+
Find any checkpoint by keyword, branch, file, date, or commit — or just describe what you're looking for.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
> "find the checkpoint where I fixed the WebSocket reconnection bugs"
|
|
80
|
+
> /neander-search OAuth on feat/auth
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Transcripts
|
|
84
|
+
|
|
85
|
+
Clean, readable conversation flow — strips IDE noise, tool results, and thinking blocks.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
--- 2026-03-22 ---
|
|
89
|
+
|
|
90
|
+
12:21 [User] Implement the following plan...
|
|
91
|
+
|
|
92
|
+
12:21 [Assistant] I'll read both files in parallel.
|
|
93
|
+
|
|
94
|
+
[Tool] Read: modules/chat/chat_websocket_handler.py
|
|
95
|
+
|
|
96
|
+
[Tool] Edit: modules/chat/chat_websocket_handler.py
|
|
97
|
+
|
|
98
|
+
[Tool] Bash: Run chat module tests
|
|
99
|
+
|
|
100
|
+
12:22 [Assistant] Both fixes are done.
|
|
101
|
+
|
|
102
|
+
12:30 [User] Can we also handle the edge case for...
|
|
103
|
+
|
|
104
|
+
12:30 [Assistant] Good catch, I'll add validation.
|
|
105
|
+
|
|
106
|
+
[Tool] Edit: modules/chat/chat_websocket_handler.py
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### AI summaries
|
|
110
|
+
|
|
111
|
+
Structured summaries generated once, persisted to the checkpoint branch, cached across sessions and machines.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
### Intent
|
|
115
|
+
Fix two reliability bugs in chat streaming: replay time window and upsert atomicity.
|
|
116
|
+
|
|
117
|
+
### Outcome
|
|
118
|
+
Both fixes implemented and tested.
|
|
119
|
+
|
|
120
|
+
### Learnings
|
|
121
|
+
**Code**:
|
|
122
|
+
- `message_repository.py:94-121` — arrayFilters with $set is the right pattern for atomic MongoDB array updates
|
|
123
|
+
|
|
124
|
+
### Friction
|
|
125
|
+
- Unused datetime import needed a second cleanup pass
|
|
126
|
+
|
|
127
|
+
### Open Items
|
|
128
|
+
- Pre-existing test failure needs investigation
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Cross-machine resume
|
|
132
|
+
|
|
133
|
+
Checkpoints are pushed to the remote automatically. On another machine:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
# Fetches transcript from remote, prints: claude --resume <session-id>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### Secret redaction
|
|
141
|
+
|
|
142
|
+
Three-layer detection before transcripts leave your machine:
|
|
143
|
+
1. **Shannon entropy** — high-entropy strings (API keys, tokens)
|
|
144
|
+
2. **Pattern matching** — 15+ known formats (AWS keys, GitHub PATs, JWTs, connection strings)
|
|
145
|
+
3. **PII detection** — emails, phone numbers, SSNs
|
|
146
|
+
|
|
147
|
+
See [EXAMPLES.md](EXAMPLES.md) for full output examples of every command.
|
|
148
|
+
|
|
149
|
+
## Checkpoint format
|
|
150
|
+
|
|
151
|
+
Stored on `neander/checkpoints/v1` — a versioned orphan branch that never touches your code history.
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
neander/checkpoints/v1/
|
|
155
|
+
├── index.log # fast lookup index
|
|
156
|
+
├── a3/
|
|
157
|
+
│ └── f8b9c1d2e4567890/
|
|
158
|
+
│ ├── metadata.json # checkpoint ID, session IDs, commit, files, AI summary
|
|
159
|
+
│ ├── transcript-<session-1>.jsonl
|
|
160
|
+
│ └── transcript-<session-2>.jsonl
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Multi-session** — concurrent sessions on the same commit don't collide
|
|
164
|
+
- **Persisted summaries** — AI summaries cached in metadata.json
|
|
165
|
+
- **Auto-push** — checkpoints pushed to remote after creation
|
|
166
|
+
|
|
167
|
+
## Install
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pip install neander-checkpoints
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Then in any project:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
cd /path/to/project
|
|
177
|
+
neander-checkpoints install
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
That's it. The installer validates prerequisites (git repo, Claude Code, Python 3.10+), then copies scripts, skills, hooks, and permissions into the project's `.claude/` directory. Everything is self-contained — anyone who clones the repo gets it working out of the box.
|
|
181
|
+
|
|
182
|
+
### What gets installed
|
|
183
|
+
|
|
184
|
+
- **Scripts** — JSONL parser, checkpoint creator, secret redaction, session restore
|
|
185
|
+
- **Skills** — 6 auto-invoked skills that Claude triggers based on conversation context
|
|
186
|
+
- **Hooks** — `Stop` and `PostToolUse:Bash` hooks for automatic checkpointing and commit linking
|
|
187
|
+
- **Permissions** — auto-allow rules so scripts run without approval prompts
|
|
188
|
+
- **CLAUDE.md** — instructions telling Claude when to proactively use checkpoint tools
|
|
189
|
+
- **Pre-push hook** — redacts secrets from transcripts before they leave your machine
|
|
190
|
+
|
|
191
|
+
## Requirements
|
|
192
|
+
|
|
193
|
+
- Python 3.10+
|
|
194
|
+
- Claude Code 2.x
|
|
195
|
+
- git
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "neander-checkpoints"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Checkpoint management for Claude Code sessions"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
neander-checkpoints = "neander_checkpoints.cli:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["setuptools>=68.0"]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.package-data]
|
|
19
|
+
neander_checkpoints = [
|
|
20
|
+
"bundled/scripts/*",
|
|
21
|
+
"bundled/skills/**/*",
|
|
22
|
+
"bundled/hooks/*",
|
|
23
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "bash __SCRIPTS_DIR__/on_stop.sh 2>/dev/null || true"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bash __SCRIPTS_DIR__/detect_commit.sh 2>/dev/null || true"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
## Checkpoint Management (neander_checkpoints)
|
|
2
|
+
|
|
3
|
+
This project has checkpoint management tools installed in `.claude/scripts/` and `.claude/skills/`.
|
|
4
|
+
|
|
5
|
+
### IMPORTANT: Always use skills, not raw scripts
|
|
6
|
+
|
|
7
|
+
When the user asks about checkpoints, sessions, summaries, transcripts, or history, **invoke the corresponding skill using the Skill tool** — do NOT try to do it yourself with raw git commands or scripts. The skills handle persistence and formatting correctly.
|
|
8
|
+
|
|
9
|
+
| User says | Invoke this skill |
|
|
10
|
+
|---|---|
|
|
11
|
+
| "summarize checkpoint ..." | `/neander-summarize` |
|
|
12
|
+
| "show transcript ...", "what happened in ..." | `/neander-transcript` |
|
|
13
|
+
| "search checkpoints ...", "find the checkpoint where ..." | `/neander-search` |
|
|
14
|
+
| "what did I do yesterday/last week" | `/neander-search` |
|
|
15
|
+
| "checkpoint stats", "how much did it cost" | `/neander-session-stats` |
|
|
16
|
+
| "recent checkpoints", "what's been going on" | `/neander-status` |
|
|
17
|
+
|
|
18
|
+
### When to use these tools proactively
|
|
19
|
+
|
|
20
|
+
You don't need to wait for the user to run a slash command. Use the skills naturally when the context calls for it:
|
|
21
|
+
|
|
22
|
+
- **User asks about previous work** → invoke `/neander-search`
|
|
23
|
+
- **User asks about code history beyond git** → invoke `/neander-search`, then `/neander-transcript`
|
|
24
|
+
- **User seems lost or is re-doing work** → invoke `/neander-search` to check if a previous checkpoint solved this
|
|
25
|
+
|
|
26
|
+
### Available scripts (for direct use only when skills don't cover the case)
|
|
27
|
+
|
|
28
|
+
All commands read from the git checkpoint branch. Use `--fetch` to pull remote data first. The primary flag is `--checkpoint`/`-c` (`--session`/`-s` still works as alias).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python3 __SCRIPTS_DIR__/parse_jsonl.py list --project <cwd>
|
|
32
|
+
python3 __SCRIPTS_DIR__/parse_jsonl.py search --project <cwd> --keyword "text" --branch "name" --fetch
|
|
33
|
+
python3 __SCRIPTS_DIR__/parse_jsonl.py stats --checkpoint <checkpoint-id>
|
|
34
|
+
python3 __SCRIPTS_DIR__/parse_jsonl.py transcript --checkpoint <checkpoint-id>
|
|
35
|
+
bash __SCRIPTS_DIR__/restore.sh <session-id> <cwd>
|
|
36
|
+
```
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# checkpoint.sh — Save current session transcript to a git orphan branch.
|
|
4
|
+
#
|
|
5
|
+
# Creates/updates a neander/checkpoints/v1 orphan branch with session
|
|
6
|
+
# transcripts and metadata. Supports multiple sessions per checkpoint.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# checkpoint.sh <session_jsonl_path> [commit_sha]
|
|
10
|
+
# checkpoint.sh --commit <sha> <path1> [path2] ...
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
CHECKPOINT_BRANCH="neander/checkpoints/v1"
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
PARSER="$SCRIPT_DIR/parse_jsonl.py"
|
|
18
|
+
|
|
19
|
+
# Parse args
|
|
20
|
+
SESSION_FILES=()
|
|
21
|
+
COMMIT_SHA=""
|
|
22
|
+
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--commit)
|
|
26
|
+
COMMIT_SHA="$2"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
*)
|
|
30
|
+
SESSION_FILES+=("$1")
|
|
31
|
+
shift
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
if [ ${#SESSION_FILES[@]} -eq 0 ]; then
|
|
37
|
+
echo "Usage: checkpoint.sh <session_jsonl_path> [commit_sha]" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Backward compat: if two positional args and second isn't a file, it's commit sha
|
|
42
|
+
if [ ${#SESSION_FILES[@]} -ge 2 ] && [ -z "$COMMIT_SHA" ]; then
|
|
43
|
+
LAST_IDX=$(( ${#SESSION_FILES[@]} - 1 ))
|
|
44
|
+
LAST="${SESSION_FILES[$LAST_IDX]}"
|
|
45
|
+
if [ ! -f "$LAST" ]; then
|
|
46
|
+
COMMIT_SHA="$LAST"
|
|
47
|
+
unset "SESSION_FILES[$LAST_IDX]"
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
[ -z "$COMMIT_SHA" ] && COMMIT_SHA="$(git rev-parse HEAD 2>/dev/null || echo 'none')"
|
|
52
|
+
|
|
53
|
+
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
54
|
+
|
|
55
|
+
# Collect valid session IDs and files
|
|
56
|
+
VALID_FILES=()
|
|
57
|
+
SESSION_IDS=()
|
|
58
|
+
for f in "${SESSION_FILES[@]}"; do
|
|
59
|
+
if [ ! -f "$f" ]; then
|
|
60
|
+
echo "Warning: Session file not found, skipping: $f" >&2
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
VALID_FILES+=("$f")
|
|
64
|
+
SESSION_IDS+=("$(basename "$f" .jsonl)")
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
if [ ${#VALID_FILES[@]} -eq 0 ]; then
|
|
68
|
+
echo "Error: No valid session files provided" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Generate checkpoint ID
|
|
73
|
+
CHECKPOINT_INPUT=""
|
|
74
|
+
for sid in "${SESSION_IDS[@]}"; do
|
|
75
|
+
CHECKPOINT_INPUT="${CHECKPOINT_INPUT}${sid}"
|
|
76
|
+
done
|
|
77
|
+
CHECKPOINT_INPUT="${CHECKPOINT_INPUT}${TIMESTAMP}"
|
|
78
|
+
CHECKPOINT_ID="$(echo "$CHECKPOINT_INPUT" | shasum -a 256 | cut -c1-16)"
|
|
79
|
+
|
|
80
|
+
# Shard directory
|
|
81
|
+
SHARD_DIR="${CHECKPOINT_ID:0:2}/${CHECKPOINT_ID:2}"
|
|
82
|
+
|
|
83
|
+
# Collect modified files from all sessions
|
|
84
|
+
ALL_FILES_JSON="[]"
|
|
85
|
+
for f in "${VALID_FILES[@]}"; do
|
|
86
|
+
stats="$(python3 "$PARSER" stats --session "$f" --json 2>/dev/null || echo '{}')"
|
|
87
|
+
files="$(echo "$stats" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('modified_files',[])))" 2>/dev/null || echo '[]')"
|
|
88
|
+
ALL_FILES_JSON="$(python3 -c "
|
|
89
|
+
import json
|
|
90
|
+
a = json.loads('$ALL_FILES_JSON')
|
|
91
|
+
b = json.loads('$files')
|
|
92
|
+
print(json.dumps(sorted(set(a + b))))
|
|
93
|
+
")"
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
# Build session IDs JSON
|
|
97
|
+
SESSION_IDS_JSON="$(python3 -c "import json; print(json.dumps($(printf '"%s",' "${SESSION_IDS[@]}" | sed 's/,$//' | sed 's/^/[/;s/$/]/')))")"
|
|
98
|
+
|
|
99
|
+
# Build metadata
|
|
100
|
+
METADATA="$(python3 -c "
|
|
101
|
+
import json
|
|
102
|
+
metadata = {
|
|
103
|
+
'id': '$CHECKPOINT_ID',
|
|
104
|
+
'session_ids': $SESSION_IDS_JSON,
|
|
105
|
+
'commit_sha': '$COMMIT_SHA',
|
|
106
|
+
'created_at': '$TIMESTAMP',
|
|
107
|
+
'merged_files': $ALL_FILES_JSON,
|
|
108
|
+
'summary': None
|
|
109
|
+
}
|
|
110
|
+
print(json.dumps(metadata, indent=2))
|
|
111
|
+
")"
|
|
112
|
+
|
|
113
|
+
# Save current branch
|
|
114
|
+
ORIGINAL_BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse HEAD)"
|
|
115
|
+
STASH_NEEDED=false
|
|
116
|
+
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
|
117
|
+
STASH_NEEDED=true
|
|
118
|
+
git stash push -m "checkpoint-auto-stash" --quiet
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
cleanup() {
|
|
122
|
+
git checkout "$ORIGINAL_BRANCH" --quiet 2>/dev/null || true
|
|
123
|
+
if [ "$STASH_NEEDED" = true ]; then
|
|
124
|
+
git stash pop --quiet 2>/dev/null || true
|
|
125
|
+
fi
|
|
126
|
+
}
|
|
127
|
+
trap cleanup EXIT
|
|
128
|
+
|
|
129
|
+
# Create orphan branch if it doesn't exist
|
|
130
|
+
if ! git rev-parse --verify "$CHECKPOINT_BRANCH" >/dev/null 2>&1; then
|
|
131
|
+
git checkout --orphan "$CHECKPOINT_BRANCH" --quiet
|
|
132
|
+
git rm -rf . --quiet 2>/dev/null || true
|
|
133
|
+
echo "# Claude Code Session Checkpoints (v1)" > README.md
|
|
134
|
+
git add README.md
|
|
135
|
+
git commit -m "Initialize checkpoint branch" --quiet
|
|
136
|
+
else
|
|
137
|
+
git checkout "$CHECKPOINT_BRANCH" --quiet
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Create checkpoint directory
|
|
141
|
+
mkdir -p "$SHARD_DIR"
|
|
142
|
+
|
|
143
|
+
# Copy transcripts — one per session
|
|
144
|
+
for i in "${!VALID_FILES[@]}"; do
|
|
145
|
+
f="${VALID_FILES[$i]}"
|
|
146
|
+
sid="${SESSION_IDS[$i]}"
|
|
147
|
+
cp "$f" "$SHARD_DIR/transcript-${sid}.jsonl"
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
# Write metadata
|
|
151
|
+
echo "$METADATA" > "$SHARD_DIR/metadata.json"
|
|
152
|
+
|
|
153
|
+
# Update index
|
|
154
|
+
for sid in "${SESSION_IDS[@]}"; do
|
|
155
|
+
echo "$CHECKPOINT_ID|$sid|$COMMIT_SHA|$TIMESTAMP" >> index.log
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
# Commit
|
|
159
|
+
git add "$SHARD_DIR" index.log
|
|
160
|
+
git commit -m "checkpoint: ${CHECKPOINT_ID} sessions=${#SESSION_IDS[@]} commit=${COMMIT_SHA:0:8}" --quiet
|
|
161
|
+
|
|
162
|
+
CHECKPOINT_REF="$(git rev-parse HEAD)"
|
|
163
|
+
|
|
164
|
+
# Push to remote if one exists
|
|
165
|
+
if git remote get-url origin >/dev/null 2>&1; then
|
|
166
|
+
git push origin "$CHECKPOINT_BRANCH" --quiet 2>/dev/null || true
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
echo "Checkpoint created: $CHECKPOINT_ID"
|
|
170
|
+
echo " Sessions: ${SESSION_IDS[*]}"
|
|
171
|
+
echo " Commit: $COMMIT_SHA"
|
|
172
|
+
echo " Branch: $CHECKPOINT_BRANCH"
|
|
173
|
+
echo " Ref: $CHECKPOINT_REF"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# detect_commit.sh — Hook script for PostToolUse:Bash
|
|
4
|
+
#
|
|
5
|
+
# Receives JSON on stdin from Claude Code hook system.
|
|
6
|
+
# Checks if the Bash command was a git commit on a user branch
|
|
7
|
+
# (not our checkpoint branch or internal scripts).
|
|
8
|
+
# If so, triggers checkpoint.sh and link_commit.sh.
|
|
9
|
+
#
|
|
10
|
+
# Usage: detect_commit.sh (reads JSON from stdin)
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
16
|
+
|
|
17
|
+
# Read hook input from stdin
|
|
18
|
+
INPUT="$(cat)"
|
|
19
|
+
|
|
20
|
+
# Extract fields from JSON
|
|
21
|
+
COMMAND="$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))" 2>/dev/null || echo "")"
|
|
22
|
+
SESSION_ID="$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('session_id',''))" 2>/dev/null || echo "")"
|
|
23
|
+
TRANSCRIPT_PATH="$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('transcript_path',''))" 2>/dev/null || echo "")"
|
|
24
|
+
|
|
25
|
+
# Only trigger on user git commits, not our internal scripts
|
|
26
|
+
# Skip if:
|
|
27
|
+
# - command doesn't contain "git commit"
|
|
28
|
+
# - command is from checkpoint.sh, save_summary.sh, or link_commit.sh
|
|
29
|
+
# - current branch is the checkpoint branch
|
|
30
|
+
echo "$COMMAND" | grep -qE "git commit" || exit 0
|
|
31
|
+
echo "$COMMAND" | grep -qE "checkpoint|save_summary|persist_summary" && exit 0
|
|
32
|
+
[ -z "$SESSION_ID" ] && exit 0
|
|
33
|
+
|
|
34
|
+
CURRENT_BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || echo "")"
|
|
35
|
+
echo "$CURRENT_BRANCH" | grep -q "neander/checkpoints" && exit 0
|
|
36
|
+
|
|
37
|
+
# Link the commit to this session
|
|
38
|
+
"$SCRIPT_DIR/link_commit.sh" "$SESSION_ID"
|
|
39
|
+
|
|
40
|
+
# Find the session JSONL and checkpoint it at this commit
|
|
41
|
+
COMMIT_SHA="$(git rev-parse HEAD 2>/dev/null || echo 'none')"
|
|
42
|
+
|
|
43
|
+
SESSION_FILE=""
|
|
44
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
45
|
+
SESSION_FILE="$TRANSCRIPT_PATH"
|
|
46
|
+
else
|
|
47
|
+
PROJECTS_DIR="$HOME/.claude/projects"
|
|
48
|
+
if [ -d "$PROJECTS_DIR" ]; then
|
|
49
|
+
SESSION_FILE="$(find "$PROJECTS_DIR" -name "${SESSION_ID}.jsonl" -type f 2>/dev/null | head -1)"
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [ -n "$SESSION_FILE" ]; then
|
|
54
|
+
"$SCRIPT_DIR/checkpoint.sh" "$SESSION_FILE" "$COMMIT_SHA" &
|
|
55
|
+
fi
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# link_commit.sh — Add session metadata trailer to the most recent commit.
|
|
4
|
+
#
|
|
5
|
+
# Called by PostToolUse:Bash hook when a git commit is detected.
|
|
6
|
+
# Adds a "Claude-Session" trailer linking the commit to the active session.
|
|
7
|
+
#
|
|
8
|
+
# Usage: link_commit.sh <session_id>
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
SESSION_ID="${1:?Usage: link_commit.sh <session_id>}"
|
|
14
|
+
|
|
15
|
+
# Only proceed if we're in a git repo
|
|
16
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Get the latest commit message
|
|
21
|
+
CURRENT_MSG="$(git log -1 --format=%B)"
|
|
22
|
+
|
|
23
|
+
# Don't add trailer if already present
|
|
24
|
+
if echo "$CURRENT_MSG" | grep -q "Claude-Session:"; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Amend the commit with the trailer
|
|
29
|
+
git commit --amend --no-edit --trailer "Claude-Session: $SESSION_ID" --quiet 2>/dev/null || true
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# on_stop.sh — Hook script for Stop event
|
|
4
|
+
#
|
|
5
|
+
# Receives JSON on stdin from Claude Code hook system.
|
|
6
|
+
# Only creates a checkpoint if the session actually modified files
|
|
7
|
+
# (i.e., used Write/Edit tools). Skips read-only sessions like
|
|
8
|
+
# running /neander-summarize or asking questions.
|
|
9
|
+
#
|
|
10
|
+
# Usage: on_stop.sh (reads JSON from stdin)
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
16
|
+
PARSER="$SCRIPT_DIR/parse_jsonl.py"
|
|
17
|
+
|
|
18
|
+
# Read hook input from stdin
|
|
19
|
+
INPUT="$(cat)"
|
|
20
|
+
|
|
21
|
+
# Extract fields from JSON
|
|
22
|
+
SESSION_ID="$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('session_id',''))" 2>/dev/null || echo "")"
|
|
23
|
+
TRANSCRIPT_PATH="$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('transcript_path',''))" 2>/dev/null || echo "")"
|
|
24
|
+
|
|
25
|
+
# Find session file
|
|
26
|
+
SESSION_FILE=""
|
|
27
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
28
|
+
SESSION_FILE="$TRANSCRIPT_PATH"
|
|
29
|
+
elif [ -n "$SESSION_ID" ]; then
|
|
30
|
+
PROJECTS_DIR="$HOME/.claude/projects"
|
|
31
|
+
if [ -d "$PROJECTS_DIR" ]; then
|
|
32
|
+
SESSION_FILE="$(find "$PROJECTS_DIR" -name "${SESSION_ID}.jsonl" -type f 2>/dev/null | head -1)"
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
[ -z "$SESSION_FILE" ] && exit 0
|
|
37
|
+
|
|
38
|
+
# Only checkpoint if the session modified files
|
|
39
|
+
FILE_COUNT="$(python3 "$PARSER" files --session "$SESSION_FILE" 2>/dev/null | wc -l | tr -d ' ')"
|
|
40
|
+
if [ "$FILE_COUNT" -eq 0 ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
COMMIT_SHA="$(git rev-parse HEAD 2>/dev/null || echo 'none')"
|
|
45
|
+
"$SCRIPT_DIR/checkpoint.sh" "$SESSION_FILE" "$COMMIT_SHA"
|