claude-code-replay 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/dist/main.mjs +3150 -0
  4. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gleb Mishchenko
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.
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # claude-code-replay
2
+
3
+ [![CI](https://github.com/glebmish/claude-code-replay/actions/workflows/ci.yml/badge.svg)](https://github.com/glebmish/claude-code-replay/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](./package.json)
6
+
7
+ Replay Claude Code session logs (`*.jsonl`) to reconstruct the lost project
8
+ state — file by file, commit by commit, in the order events happened.
9
+ The tool of last resort when a destructive command wiped the tree.
10
+
11
+ ## How it works
12
+
13
+ There are two replay layers, and the second is opt-in:
14
+
15
+ 1. **Deterministic replay** — walks every `*.jsonl` under `--logs-dir`
16
+ (including subagent JSONLs under `<session>/subagents/`) in strict
17
+ chronological order and applies file writes.
18
+ 2. **Claude classifier** (opt-in, `--enable-llm-classifier`) — every Bash
19
+ event would otherwise be skipped. With the classifier on, each Bash
20
+ event is sent to Claude (Sonnet 4.6) which decides
21
+ `execute` or `skip` per event, with reasons. See
22
+ [How the classifier works](#how-the-classifier-works).
23
+
24
+ ## Install
25
+
26
+ Prerequisites:
27
+
28
+ - **Node 20 or newer.**
29
+ - **Claude Code CLI installed and authenticated** (`claude login`) — only
30
+ needed for the classifier (`--enable-llm-classifier`). The classifier
31
+ reuses that auth via the Claude Agent SDK, so no separate Anthropic API
32
+ key is needed.
33
+
34
+ Run without installing:
35
+
36
+ ```bash
37
+ npx claude-code-replay --target … --source-root … [flags]
38
+ ```
39
+
40
+ Or install globally:
41
+
42
+ ```bash
43
+ npm install -g claude-code-replay
44
+ claude-code-replay --target … --source-root … [flags]
45
+ ```
46
+
47
+ Or, build from source — clone, `npm install`, then either
48
+ `npm run replay -- <flags>` directly or `npm link` to expose it as
49
+ `claude-code-replay` on your `PATH`.
50
+
51
+ ## Quick start
52
+
53
+ ```bash
54
+ claude-code-replay \
55
+ --target /tmp/myrepo-recovered/ \
56
+ --source-root /Users/you/projects/myrepo \
57
+ --enable-llm-classifier
58
+ ```
59
+
60
+ What you'll see on stdout (from a real 304-event replay):
61
+
62
+ ```
63
+ INFO collecting events from /Users/you/.claude/projects/-Users-you-projects-myrepo
64
+ INFO collected 304 events
65
+ INFO building snapshot index from file-history-snapshot entries
66
+ INFO snapshot index covers 43 paths
67
+ INFO classifier: 4 batch(es) over 208 payload events (117 Bash, 91 context); sizes=[71,64,59,14]
68
+ INFO classifier model=claude-sonnet-4-6, mode=base, source-roots=1
69
+ INFO classifier batch 1/4 cache hit
70
+ INFO classifier batch 2/4 cache hit
71
+ INFO classifier batch 3/4 cache hit
72
+ INFO classifier batch 4/4 cache hit
73
+ INFO classifier returned 208 decisions
74
+ === claude-code-replay summary ===
75
+ events total: 304
76
+ replayed: 64 (of 64)
77
+ skipped: 240
78
+ bash executed: 25 of 25
79
+ classifier batches: 4 (4 cached, 0 live)
80
+ halted: no
81
+ elapsed: 3.70s
82
+ target files: 732 (8519381 bytes total)
83
+ ```
84
+
85
+ The summary omits rows that would be zero on a typical run (overrides,
86
+ cwd-filtered Bash, snapshot heals, lenient-read skips). Per-event
87
+ `CLASSIFY` / `APPLY` / `CHECK` traces and detailed classifier
88
+ diagnostics are gated behind `--debug`. Real errors (argv parse
89
+ failures, classifier API errors) go to stderr; this run log goes to
90
+ stdout so you can pipe it without losing diagnostics.
91
+
92
+ Exit codes: `0` success, `2` argv error, `10` halted on command failure.
93
+
94
+ ## Flags
95
+
96
+ ### Required
97
+ - `--target <path>` — directory the replay writes into. Must be distinct
98
+ from every logs dir (in either direction); replayed `rm -rf .` could
99
+ otherwise destroy the logs mid-run.
100
+ - `--source-root <path>` — original absolute `cwd` from the session.
101
+ Compared verbatim against `event.cwd` in the logs, so it must match
102
+ character-for-character (no relative paths, no symlink-resolved paths).
103
+ Repeatable for sessions that moved across roots.
104
+
105
+ ### Replay window
106
+ - `--logs-dir <path>` — directory containing the session `*.jsonl` files
107
+ (and any `<session>/subagents/` JSONLs). Optional, repeatable. By
108
+ default, one logs dir is inferred from each `--source-root` as
109
+ `~/.claude/projects/<encoded-source-root>` (every `/` in the absolute
110
+ source-root is replaced with `-`). Inferred dirs that don't exist on
111
+ disk are silently skipped; explicit `--logs-dir` values are added on
112
+ top of the inferred set and must exist.
113
+ - `--cutoff <iso-ts>` — drop events at or after this ISO 8601 timestamp
114
+ at parse time. Use when the session's later events include the
115
+ destructive operation you're recovering from.
116
+ - `--start <iso-ts>` — start replay at the first event whose timestamp
117
+ is at or after this. Composes with `--cutoff` to define a window. The
118
+ target dir is trusted to already reflect the state events before
119
+ `--start` would have produced.
120
+ - `--from-index <N>` — start replay at event index `N` (events `0..N-1`
121
+ are not classified or applied). Composes with `--start`; whichever
122
+ lands later wins. The halt-and-resume primitive: on a halt at `K`, fix
123
+ the cause and resume with `--from-index K`.
124
+
125
+ ### Verification
126
+ - `--strict` — disable both heal layers (snapshot heal *and* apply-reads
127
+ heal). Any Read mismatch or missing target halts immediately. Useful
128
+ when measuring how much of a replay needs healing (e.g. when
129
+ evaluating a classifier — heal counts in default mode signal what the
130
+ classifier left on the table).
131
+ - `--strict-reads` — halt on the first failed Read checkpoint instead of
132
+ the default (log + continue). Useful for debugging which event
133
+ triggered a missing-file scenario; the default-on lenient behaviour is
134
+ what keeps long replays from stopping every time the classifier
135
+ correctly omits a producing Bash chain (see the cascade rule in
136
+ [`docs/classifier-prompt.md`](./docs/classifier-prompt.md)).
137
+
138
+ ### Bash classifier (opt-in)
139
+ - `--enable-llm-classifier` — opt in to LLM calls. Required to use the
140
+ classifier at all. The base prompt always includes a git-focused
141
+ supplement that calls out `git add` / `git commit` / `git branch` /
142
+ `git checkout` / `git merge` / `git rebase` / `git revert` /
143
+ `git reset` / `git tag` / `git filter-repo` (non-exhaustive; the same
144
+ logic extends to any equivalent state-mutating command, and to
145
+ heredoc/sed writes whose content a later commit captures). Restoring
146
+ the original git history is the dominant real-world use case for
147
+ replay, so it ships as the default rather than an opt-in flag.
148
+ - `--custom-intent "<intent>"` — append a natural-language intent
149
+ describing what the replay should accomplish. Use for behaviour
150
+ beyond the built-in git focus, e.g.
151
+ `"keep all dependency installs (npm/pip) so node_modules ends up populated"`
152
+ or `"skip any docker/podman commands; this replay runs without a daemon"`.
153
+ Repeatable; each value is joined with a newline.
154
+ - `--override-classifier-cache` — skip reading from the classifier cache
155
+ and force a fresh LLM call, but still write the new response back to
156
+ the cache (overwriting any existing entry).
157
+ - `--skip-uncached-tail` — if the cached run's `last_event_ts` falls
158
+ inside the current logs, drop every event with a later timestamp
159
+ before the classifier sees them. The classifier then full-hits the
160
+ cache and the runtime replays only what was already cached.
161
+ Intended for "re-run yesterday's replay against today's slightly
162
+ grown logs without paying for the new tail." If no cache exists, the
163
+ flag warns and proceeds without truncation. **Caveat:** events
164
+ past the cap go unclassified — if the appended tail contains a
165
+ destructive command, you won't see it.
166
+
167
+ ### Per-event overrides
168
+ - `--override-skip <INDEX>` — repeatable. Force event `INDEX` to skip,
169
+ regardless of any rule-based or LLM classification. Works on any event
170
+ type (`Bash`, `Read`, `Edit`, `Write`, checkpoint).
171
+ - `--override-execute <INDEX>[=CMD]` — repeatable. Force event `INDEX`
172
+ (`Bash` only) to execute. Bare form runs the event's original command;
173
+ `=CMD` runs the substring `CMD` instead (must be a literal substring
174
+ of the event's original command — same constraint as the LLM
175
+ classifier's `decision.command`). Subject to the same
176
+ `cwd`-inside-source-roots check as classifier-approved executes.
177
+
178
+ ### Diagnostics
179
+ - `--dry-run` — classify only, no execution. Walks the event stream,
180
+ prints the summary, but does not apply Writes/Edits, verify Read
181
+ checkpoints, or execute approved Bash. Combine with `--debug` to see
182
+ the per-event `CLASSIFY` line for every event.
183
+ - `--debug` — turn on the per-event `CLASSIFY` / `APPLY` / `CHECK`
184
+ trace (one line per event) plus verbose classifier instrumentation.
185
+ Off by default because the default run keeps to a handful of `INFO`
186
+ setup lines and the final summary.
187
+
188
+ ## How the classifier works
189
+
190
+ All requests go through the Claude Agent SDK (`claude-sonnet-4-6`,
191
+ multi-turn streaming, no tool use). It reuses Claude Code's existing
192
+ auth — no separate API key needed. The default model id targets the
193
+ 200k-context variant; switching to the 1M-context variant (`[1m]`
194
+ suffix) requires Anthropic "Usage credits" opt-in and is currently a
195
+ source-level toggle in `src/llm-classifier/sdk.ts`.
196
+
197
+ The Bash payload is split into batches of 50–100 events, cut at the first
198
+ `git commit` past the threshold. Each batch becomes one user turn in a
199
+ single conversation, so the system prompt and earlier batches are
200
+ cache-served on subsequent turns.
201
+
202
+ Per-batch responses are cached at
203
+ `$XDG_CACHE_HOME/claude-code-replay/<encoded-target>/batch-NNNN.json`
204
+ plus a single `meta.json`. The encoding mirrors Claude Code's own
205
+ project-dir scheme: every `/` in the absolute `--target` path becomes
206
+ `-`, so `/tmp/myrepo-recovered` lives at
207
+ `$XDG_CACHE_HOME/claude-code-replay/-tmp-myrepo-recovered/`. The cache
208
+ directory is intentionally outside `--target` so a replayed
209
+ `git add .` can't sweep it in.
210
+
211
+ **Cache invalidation.** All-or-nothing: one shared key covers every
212
+ batch, so any input change invalidates the entire run at once. On a
213
+ mismatch the stale entries are wiped, the classifier recomputes from
214
+ scratch, and the run log states *which* input changed (e.g.
215
+ `INFO classifier cache miss: inputs changed (session logs); wiping
216
+ stale entries and recomputing 4 batch(es)`). The following changes
217
+ invalidate:
218
+
219
+ - **Editing the system prompt** (`src/llm-classifier/prompts.ts`).
220
+ - **Adding, removing, or changing any `--custom-intent`**.
221
+ - **Changing the `--source-root` set**.
222
+ - **Claude Code logs set changes** — a new session JSONL appears, an
223
+ existing one grows, or `--cutoff` (applied at parse time) crops the
224
+ set. Resuming the same logs with a different `--from-index` /
225
+ `--start` does NOT invalidate.
226
+ - **`--override-classifier-cache`** — forces a fresh call without
227
+ consulting the cache; results are still written back.
228
+ - **Pointing at a different `--target`** — the cache subdir is the
229
+ encoded target path, so a different target is a different cache
230
+ namespace (the old one is left orphaned, not deleted).
231
+
232
+ The literal system prompt — including the file-dependency cascade
233
+ rule — lives in
234
+ [`src/llm-classifier/prompts.ts`](./src/llm-classifier/prompts.ts).
235
+ [`docs/classifier-prompt.md`](./docs/classifier-prompt.md) explains its
236
+ rules and the speculation/cache machinery with worked examples; the
237
+ README does not duplicate the prompt itself.
238
+
239
+ ## Limitations
240
+
241
+ - **Only `Write`, `Edit`, `Read` are deterministic.** Anything else
242
+ (`Bash`, `Task`, `TodoWrite`, `WebFetch`, MCP tools, …) is skipped in
243
+ the default path. The classifier closes the gap for `Bash` only; the
244
+ rest stays skipped.
245
+ - **The classifier is an assistant, not an oracle.** With
246
+ `--enable-llm-classifier`, every approved `execute` runs a real shell
247
+ command in `--target`. Run `--dry-run` and review the
248
+ `CLASSIFY` / `APPLY` stream before trusting it on a fresh tree.
249
+ - **Default-lenient Read checkpoints** mean a misclassified Bash chain
250
+ can silently produce missing-file Reads that get skipped rather than
251
+ halted. Use `--strict-reads` (or the broader `--strict`) when
252
+ debugging suspected cascade misses.
253
+
254
+ ## Architecture
255
+
256
+ A contributor-facing module map of `src/` lives in
257
+ [`docs/architecture.md`](./docs/architecture.md). The system prompt the
258
+ classifier ships with is in
259
+ [`src/llm-classifier/prompts.ts`](./src/llm-classifier/prompts.ts), with
260
+ behavioural explanation in
261
+ [`docs/classifier-prompt.md`](./docs/classifier-prompt.md). The
262
+ empirically-derived Claude Code session log format the replayer reads
263
+ is documented in [`docs/log-format.md`](./docs/log-format.md).
264
+
265
+ ## Tests
266
+
267
+ ```bash
268
+ npm test # vitest run
269
+ npm run typecheck # tsc --noEmit
270
+ ```
271
+
272
+ ## Changelog
273
+
274
+ See [GitHub Releases](https://github.com/glebmish/claude-code-replay/releases)
275
+ for per-version release notes.
276
+
277
+ ## License
278
+
279
+ MIT — see [LICENSE](./LICENSE).