elliot-stack 1.0.18 → 1.0.19

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 (44) hide show
  1. package/README.md +11 -0
  2. package/bin/install.cjs +134 -49
  3. package/package.json +1 -1
  4. package/skills/estack-read-claude-session-history/SKILL.md +196 -0
  5. package/skills/estack-read-claude-session-history/references/jsonl-schema.md +126 -0
  6. package/skills/estack-read-claude-session-history/references/modes.md +366 -0
  7. package/skills/estack-read-claude-session-history/references/recipes.md +237 -0
  8. package/skills/estack-read-claude-session-history/scripts/lib/__init__.py +1 -0
  9. package/skills/estack-read-claude-session-history/scripts/lib/parser.py +460 -0
  10. package/skills/estack-read-claude-session-history/scripts/lib/paths.py +234 -0
  11. package/skills/estack-read-claude-session-history/scripts/lib/search.py +179 -0
  12. package/skills/estack-read-claude-session-history/scripts/lib/subagents.py +88 -0
  13. package/skills/estack-read-claude-session-history/scripts/lib/tools.py +144 -0
  14. package/skills/estack-read-claude-session-history/scripts/read_transcript.py +1448 -0
  15. package/skills/estack-read-claude-session-history/scripts/tests/conftest.py +40 -0
  16. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/README.md +20 -0
  17. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/all-noise.jsonl +4 -0
  18. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/basic-session.jsonl +2 -0
  19. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/interrupted.jsonl +2 -0
  20. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/multi-compact.jsonl +8 -0
  21. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/pending-user.jsonl +2 -0
  22. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta/subagents/agent-aaa.jsonl +2 -0
  23. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta.jsonl +2 -0
  24. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.jsonl +2 -0
  25. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.meta.json +1 -0
  26. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent.jsonl +4 -0
  27. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/time-spread.jsonl +6 -0
  28. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/timeline-day-test.jsonl +5 -0
  29. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-zoo.jsonl +10 -0
  30. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/truncated.jsonl +3 -0
  31. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/unicode.jsonl +2 -0
  32. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-advisor.jsonl +3 -0
  33. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-compact.jsonl +5 -0
  34. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-thinking.jsonl +2 -0
  35. package/skills/estack-read-claude-session-history/scripts/tests/test_backup_roots.py +56 -0
  36. package/skills/estack-read-claude-session-history/scripts/tests/test_json_format.py +201 -0
  37. package/skills/estack-read-claude-session-history/scripts/tests/test_modes.py +199 -0
  38. package/skills/estack-read-claude-session-history/scripts/tests/test_parser.py +195 -0
  39. package/skills/estack-read-claude-session-history/scripts/tests/test_paths.py +133 -0
  40. package/skills/estack-read-claude-session-history/scripts/tests/test_search.py +78 -0
  41. package/skills/estack-read-claude-session-history/scripts/tests/test_subagents.py +43 -0
  42. package/skills/estack-read-claude-session-history/scripts/tests/test_timeline.py +175 -0
  43. package/skills/estack-read-claude-session-history/scripts/tests/test_timezone_and_project.py +212 -0
  44. package/skills/estack-read-claude-session-history/scripts/tests/test_tools.py +80 -0
package/README.md CHANGED
@@ -56,6 +56,17 @@ npx elliot-stack@latest
56
56
 
57
57
  External contributions are welcome via pull request. Direct pushes to `main` are blocked — fork the repo, push your changes to a branch, and open a PR. Only the maintainer (Elliot) can merge to `main` and cut releases. This is intentional: `elliot-stack` is a security-sensitive npm package and the release tag can only be pushed by the maintainer.
58
58
 
59
+ ### Testing locally
60
+
61
+ Run the installer straight from your checkout to preview what a real install would do to your `~/.claude/`:
62
+
63
+ ```bash
64
+ node bin/install.cjs # dry run — previews changes, writes nothing
65
+ node bin/install.cjs --install # actually sync your local edits to ~/.claude/skills/
66
+ ```
67
+
68
+ Run from the repo, the installer **dry-runs by default** so testing never clobbers your live `~/.claude/skills/` install. Pass `--install` once the preview looks right. (`--dry-run` forces a preview even under `npx`.)
69
+
59
70
  See [`docs/publishing.md`](docs/publishing.md) for the release flow and security model.
60
71
 
61
72
  ## License
package/bin/install.cjs CHANGED
@@ -21,6 +21,19 @@ const PACKAGE_HOOKS_DIR = path.join(__dirname, '..', 'hooks');
21
21
  // ── Flags ──────────────────────────────────────────────────────────────────
22
22
  const SILENT = process.argv.includes('--silent');
23
23
  const STARTUP = process.argv.includes('--startup');
24
+ // When run directly from the repo (not via npx/node_modules), default to dry-run
25
+ // so local testing never silently clobbers the live ~/.claude/skills install.
26
+ // Pass --install to actually write files, or --dry-run to force preview mode.
27
+ const IS_LOCAL = !__dirname.includes('node_modules');
28
+ const DRY_RUN = process.argv.includes('--dry-run') ||
29
+ (IS_LOCAL && !process.argv.includes('--install'));
30
+
31
+ // ── Deprecated skills ──────────────────────────────────────────────────────
32
+ // Skills that were renamed or removed. The installer removes these on every
33
+ // run so users don't end up with both the old and new name installed.
34
+ const DEPRECATED_SKILLS = [
35
+ 'estack-prompt-builder', // renamed to estack-prompt-builder-coach
36
+ ];
24
37
 
25
38
  // ── Helpers ────────────────────────────────────────────────────────────────
26
39
 
@@ -44,7 +57,8 @@ function walkDir(dir, base) {
44
57
  function computeFileHash(filePath) {
45
58
  if (!fs.existsSync(filePath)) return null;
46
59
  const hash = crypto.createHash('sha256');
47
- hash.update(fs.readFileSync(filePath));
60
+ const raw = fs.readFileSync(filePath);
61
+ hash.update(Buffer.from(raw.toString('utf8').replace(/\r\n/g, '\n')));
48
62
  return hash.digest('hex');
49
63
  }
50
64
 
@@ -54,9 +68,9 @@ function computeSkillHash(skillDir) {
54
68
  const files = walkDir(skillDir, skillDir);
55
69
  for (const relPath of files) {
56
70
  const fullPath = path.join(skillDir, relPath);
57
- const contents = fs.readFileSync(fullPath);
71
+ const raw = fs.readFileSync(fullPath);
58
72
  hash.update(relPath.replace(/\\/g, '/'));
59
- hash.update(contents);
73
+ hash.update(Buffer.from(raw.toString('utf8').replace(/\r\n/g, '\n')));
60
74
  }
61
75
  return hash.digest('hex');
62
76
  }
@@ -137,7 +151,9 @@ function getSkillDescription(skillDir) {
137
151
 
138
152
  // ── Hook setup ─────────────────────────────────────────────────────────────
139
153
 
140
- function setupStartupHook() {
154
+ // Returns true if the hook was added (or would be added in dryRun), false if
155
+ // it was already configured. In dryRun mode nothing is written to disk.
156
+ function setupStartupHook(dryRun) {
141
157
  let settings = {};
142
158
  if (fs.existsSync(SETTINGS_FILE)) {
143
159
  try {
@@ -160,6 +176,8 @@ function setupStartupHook() {
160
176
  }
161
177
  }
162
178
 
179
+ if (dryRun) return true;
180
+
163
181
  if (!settings.hooks) settings.hooks = {};
164
182
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
165
183
 
@@ -181,7 +199,9 @@ function setupStartupHook() {
181
199
  return true;
182
200
  }
183
201
 
184
- function setupRepoSearchNudgeHook() {
202
+ // Returns true if the hook was added (or would be added in dryRun), false if
203
+ // it was already configured. In dryRun mode nothing is written to disk.
204
+ function setupRepoSearchNudgeHook(dryRun) {
185
205
  let settings = {};
186
206
  if (fs.existsSync(SETTINGS_FILE)) {
187
207
  try {
@@ -203,6 +223,8 @@ function setupRepoSearchNudgeHook() {
203
223
  }
204
224
  }
205
225
 
226
+ if (dryRun) return true;
227
+
206
228
  if (!settings.hooks) settings.hooks = {};
207
229
  if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
208
230
 
@@ -223,6 +245,29 @@ function setupRepoSearchNudgeHook() {
223
245
  // ── Main ────────────────────────────────────────────────────────────────────
224
246
 
225
247
  async function main() {
248
+ // 0. Remove deprecated skills (renamed/deleted from the package)
249
+ if (fs.existsSync(SKILLS_DIR)) {
250
+ const newChecksums0 = fs.existsSync(CHECKSUMS_FILE)
251
+ ? (() => { try { return JSON.parse(fs.readFileSync(CHECKSUMS_FILE, 'utf8')); } catch (_) { return {}; } })()
252
+ : {};
253
+ let changed = false;
254
+ for (const name of DEPRECATED_SKILLS) {
255
+ const dir = path.join(SKILLS_DIR, name);
256
+ if (fs.existsSync(dir)) {
257
+ if (!DRY_RUN) fs.rmSync(dir, { recursive: true, force: true });
258
+ delete newChecksums0[name];
259
+ changed = true;
260
+ if (!SILENT && !STARTUP) {
261
+ console.log((DRY_RUN ? ' [dry run] Would remove deprecated skill: ' : ' Removed deprecated skill: ') + name);
262
+ }
263
+ } else if (newChecksums0[name]) {
264
+ delete newChecksums0[name];
265
+ changed = true;
266
+ }
267
+ }
268
+ if (changed && !DRY_RUN) fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums0, null, 2));
269
+ }
270
+
226
271
  // 1. Scan package skills
227
272
  if (!fs.existsSync(PACKAGE_SKILLS_DIR)) {
228
273
  if (!SILENT && !STARTUP) {
@@ -287,7 +332,7 @@ async function main() {
287
332
  } else if (currentHash !== storedChecksums[name]) {
288
333
  // Stored checksum exists but current doesn't match — user modified it
289
334
  modifiedSkills.push(name);
290
- if (storedChecksums[name] !== packageHashes[name]) {
335
+ if (currentHash !== packageHashes[name]) {
291
336
  needsUpdate.push(name);
292
337
  }
293
338
  } else if (currentHash !== packageHashes[name]) {
@@ -467,32 +512,39 @@ async function main() {
467
512
  console.log(' - ' + filename);
468
513
  }
469
514
  }
470
- console.log('\nChoose an action:');
471
- console.log(' [o] Overwrite all (replace with latest)');
472
- console.log(' [s] Skip all (keep local versions)');
473
- console.log(' [m] Merge (backup local, install new, merge in Claude Code)');
474
- console.log(' [a] Abort (cancel installation)');
475
- console.log('');
476
515
 
477
- const answer = await promptChar('Your choice (o/s/m/a): ');
478
-
479
- if (answer === 'a') {
480
- console.log('Installation aborted.');
481
- process.exit(0);
482
- } else if (answer === 's') {
483
- modifiedAction = 'skip';
484
- } else if (answer === 'm') {
485
- modifiedAction = 'merge';
486
- } else if (answer === 'o') {
516
+ if (DRY_RUN) {
517
+ console.log('\n[dry run] Would prompt: overwrite / skip / merge / abort');
518
+ console.log('[dry run] Showing what would happen with default overwrite...');
487
519
  modifiedAction = 'overwrite';
488
520
  } else {
489
- console.log('Invalid choice. Installation aborted.');
490
- process.exit(1);
521
+ console.log('\nChoose an action:');
522
+ console.log(' [o] Overwrite all (replace with latest)');
523
+ console.log(' [s] Skip all (keep local versions)');
524
+ console.log(' [m] Merge (backup local, install new, merge in Claude Code)');
525
+ console.log(' [a] Abort (cancel installation)');
526
+ console.log('');
527
+
528
+ const answer = await promptChar('Your choice (o/s/m/a): ');
529
+
530
+ if (answer === 'a') {
531
+ console.log('Installation aborted.');
532
+ process.exit(0);
533
+ } else if (answer === 's') {
534
+ modifiedAction = 'skip';
535
+ } else if (answer === 'm') {
536
+ modifiedAction = 'merge';
537
+ } else if (answer === 'o') {
538
+ modifiedAction = 'overwrite';
539
+ } else {
540
+ console.log('Invalid choice. Installation aborted.');
541
+ process.exit(1);
542
+ }
491
543
  }
492
544
  }
493
545
 
494
546
  // 8. Install skills
495
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
547
+ if (!DRY_RUN) fs.mkdirSync(SKILLS_DIR, { recursive: true });
496
548
  const newChecksums = Object.assign({}, storedChecksums);
497
549
  let installedCount = 0;
498
550
  const mergedSkills = [];
@@ -506,23 +558,29 @@ async function main() {
506
558
  continue;
507
559
  }
508
560
  if (modifiedAction === 'merge') {
509
- backupSkill(name);
561
+ if (!DRY_RUN) backupSkill(name);
510
562
  mergedSkills.push(name);
511
- console.log(' Backed up ' + name + ' → ~/.claude/.estack-backup/' + name);
563
+ console.log((DRY_RUN ? ' [dry run] Would back up ' : ' Backed up ') + name + ' → ~/.claude/.estack-backup/' + name);
512
564
  }
513
565
  // overwrite or merge — fall through to install
514
566
  } else if (!needsUpdate.includes(name) && fs.existsSync(path.join(SKILLS_DIR, name))) {
515
567
  // Already installed and up-to-date
568
+ if (DRY_RUN) console.log(' [dry run] Up to date (no change): ' + name);
516
569
  continue;
517
570
  }
518
- copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
571
+ const isUpdate = fs.existsSync(path.join(SKILLS_DIR, name));
572
+ if (!DRY_RUN) copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
519
573
  newChecksums[name] = packageHashes[name];
520
574
  installedCount++;
521
- console.log(' Installed ' + name);
575
+ if (DRY_RUN) {
576
+ console.log(' [dry run] Would ' + (isUpdate ? 'update ' : 'install ') + name);
577
+ } else {
578
+ console.log(' Installed ' + name);
579
+ }
522
580
  }
523
581
 
524
582
  // 8b. Install hooks
525
- fs.mkdirSync(HOOKS_DIR, { recursive: true });
583
+ if (!DRY_RUN) fs.mkdirSync(HOOKS_DIR, { recursive: true });
526
584
  let installedHookCount = 0;
527
585
  const mergedHooks = [];
528
586
 
@@ -535,33 +593,48 @@ async function main() {
535
593
  continue;
536
594
  }
537
595
  if (modifiedAction === 'merge') {
538
- backupHook(filename);
596
+ if (!DRY_RUN) backupHook(filename);
539
597
  mergedHooks.push(filename);
540
- console.log(' Backed up hook ' + filename + ' → ~/.claude/.estack-backup/hooks/' + filename);
598
+ console.log((DRY_RUN ? ' [dry run] Would back up hook ' : ' Backed up hook ') + filename + ' → ~/.claude/.estack-backup/hooks/' + filename);
541
599
  }
542
600
  // overwrite or merge — fall through to install
543
601
  } else if (!hooksNeedingUpdate.includes(filename) && fs.existsSync(path.join(HOOKS_DIR, filename))) {
544
602
  // Already installed and up-to-date
603
+ if (DRY_RUN) console.log(' [dry run] Up to date (no change): hook ' + filename);
545
604
  continue;
546
605
  }
547
- fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
606
+ const isHookUpdate = fs.existsSync(path.join(HOOKS_DIR, filename));
607
+ if (!DRY_RUN) fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
548
608
  newChecksums['hook:' + filename] = packageHookHashes[filename];
549
609
  installedHookCount++;
550
- console.log(' Installed hook ' + filename);
610
+ if (DRY_RUN) {
611
+ console.log(' [dry run] Would ' + (isHookUpdate ? 'update hook ' : 'install hook ') + filename);
612
+ } else {
613
+ console.log(' Installed hook ' + filename);
614
+ }
551
615
  }
552
616
 
553
617
  // 9. Write checksums
554
- fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums, null, 2));
618
+ if (!DRY_RUN) fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums, null, 2));
555
619
 
556
620
  // 10. Setup startup hook and repo-search nudge hook
557
- const hookInstalled = setupStartupHook();
558
- const nudgeHookInstalled = setupRepoSearchNudgeHook();
621
+ // In dry-run these inspect settings.json read-only and report would-be action.
622
+ const hookInstalled = setupStartupHook(DRY_RUN);
623
+ const nudgeHookInstalled = setupRepoSearchNudgeHook(DRY_RUN);
559
624
 
560
625
  // 11. Summary output
561
- console.log('\nestack installed successfully!\n');
562
- console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.claude/skills/');
563
- if (installedHookCount > 0) {
564
- console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' installed to ~/.claude/hooks/');
626
+ if (DRY_RUN) {
627
+ console.log('\n[dry run] No files were changed. Run with --install to apply.\n');
628
+ console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/skills/');
629
+ if (installedHookCount > 0) {
630
+ console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/hooks/');
631
+ }
632
+ } else {
633
+ console.log('\nestack installed successfully!\n');
634
+ console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.claude/skills/');
635
+ if (installedHookCount > 0) {
636
+ console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' installed to ~/.claude/hooks/');
637
+ }
565
638
  }
566
639
  console.log('');
567
640
  console.log('Skills available:');
@@ -582,15 +655,27 @@ async function main() {
582
655
  console.log('Backed up to ~/.claude/.estack-backup/hooks/');
583
656
  }
584
657
 
585
- if (hookInstalled) {
586
- console.log('\nAuto-update hook added to ~/.claude/settings.json');
587
- console.log('Skills will update automatically when you start Claude Code.');
658
+ if (DRY_RUN) {
659
+ if (hookInstalled) {
660
+ console.log('\n[dry run] Would add auto-update hook to ~/.claude/settings.json');
661
+ } else {
662
+ console.log('\nAuto-update hook already configured (no change).');
663
+ }
664
+ if (nudgeHookInstalled) {
665
+ console.log('[dry run] Would register repo-search nudge hook in settings.json.');
666
+ } else {
667
+ console.log('Repo-search nudge hook already configured (no change).');
668
+ }
588
669
  } else {
589
- console.log('\nAuto-update hook already configured.');
590
- }
591
-
592
- if (nudgeHookInstalled) {
593
- console.log('Repo-search nudge hook registered in settings.json.');
670
+ if (hookInstalled) {
671
+ console.log('\nAuto-update hook added to ~/.claude/settings.json');
672
+ console.log('Skills will update automatically when you start Claude Code.');
673
+ } else {
674
+ console.log('\nAuto-update hook already configured.');
675
+ }
676
+ if (nudgeHookInstalled) {
677
+ console.log('Repo-search nudge hook registered in settings.json.');
678
+ }
594
679
  }
595
680
 
596
681
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elliot-stack",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Elliot's skill stack for Claude Code — install via npx elliot-stack@latest",
5
5
  "bin": {
6
6
  "elliot-stack": "bin/install.cjs"
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: estack-read-claude-session-history
3
+ description: (read-claude-session-history) Invoke for ANY task involving Claude Code session history, transcripts, or .jsonl files — this is the only way to read, parse, or search them; do not attempt to use Bash or Read on .jsonl directly. Use for: recovering context after /compact ("what were we doing before compact"), advisor response retrieval ("what did the advisor say"), subagent output collection ("get all subagent finals"), cross-project session search by keyword, session listing and triage, UUID and title lookup, resume-command generation, file-edit and tool-call forensics, session diff between two sessions or subagents, weekly work journal, day timeline of activity blocks and idle gaps, recovering from .claude-backups after data loss, session count queries, and reading the last agent message before a crash or interrupt. Trigger phrases: "session history", "before compact", "what did claude do", "what did I work on", "search my sessions", "find that session", "what did the advisor say", "what did the agent edit", "from the backup", "list my sessions", "subagent outputs", "session journal", "resume previous", "which files did claude touch", "go back and look", "what did I do yesterday", "where did my day go", "timeline of my day", "how much time on".
4
+ ---
5
+
6
+ # Read Claude Session History
7
+
8
+ Search, read, recover, and compare Claude Code session history — across the current session, prior sessions, sibling subagents, all projects, and `.claude-backups` snapshots.
9
+
10
+ Sessions are stored as `.jsonl` files. Reading them raw is hopeless: 1,000–5,000+ lines of dense JSON per session, 33+ project directories, hundreds of historical sessions. This skill wraps a single CLI that knows the entry schema and exposes ~20 modes.
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ PY="C:\Users\2supe\.claude\skills\read-claude-session-history\scripts\read_transcript.py"
16
+
17
+ # What was the last thing the agent said in this session?
18
+ python "$PY" --file <current-session.jsonl> --mode last
19
+
20
+ # Get a 6-line summary of any session (intent, last activity, edits, tool counts, subagent fanout)
21
+ python "$PY" --file <session.jsonl> --mode brief
22
+
23
+ # Recover what got cut off by the most recent /compact
24
+ python "$PY" --file <session.jsonl> --mode pre-compact
25
+
26
+ # Find a session by UUID prefix across all projects
27
+ python "$PY" --mode lookup --uuid abc123de
28
+
29
+ # Search every session in every project for a phrase
30
+ python "$PY" --mode search --all-projects --query "supabase migration"
31
+
32
+ # Pull subagent outputs from a fan-out investigation
33
+ python "$PY" --file <parent.jsonl> --mode subagent-finals
34
+
35
+ # Block-grouped timeline of a whole day across all sessions, with idle gaps
36
+ python "$PY" --mode timeline --date yesterday
37
+
38
+ # Any mode as structured JSON for piping into the next step
39
+ python "$PY" --mode list --project keel --since 7d --format json
40
+ ```
41
+
42
+ ## Time handling — READ THIS before doing anything with time
43
+
44
+ **Every time the CLI displays is already the user's local time.** JSONL files store
45
+ UTC; the script converts on output. Do NOT add or subtract timezone offsets
46
+ yourself, do NOT cross-reference file mtimes to infer the timezone, and do NOT
47
+ treat raw `"timestamp"` fields from a .jsonl (which ARE UTC) as comparable to CLI
48
+ output. If you need a different zone, pass `--tz` (IANA name like
49
+ `America/New_York`, `UTC`, or an offset like `-4`) — never convert manually.
50
+ `--since/--until/--date` specs are interpreted in that same display timezone.
51
+
52
+ ## Decision tree
53
+
54
+ ```
55
+ What are you trying to do?
56
+
57
+ ├─ Read the current session / one specific session
58
+ │ ├─ Last assistant message ──────────────────── --mode last
59
+ │ ├─ All advisor responses ───────────────────── --mode advisor
60
+ │ ├─ Content cut off by /compact ─────────────── --mode pre-compact
61
+ │ ├─ Full human-readable dump ────────────────── --mode dump (size-aware)
62
+ │ ├─ 6-line summary for triage ───────────────── --mode brief
63
+ │ └─ Schema/structural diagnosis ─────────────── --mode debug
64
+
65
+ ├─ Find a session I don't have the path for
66
+ │ ├─ By UUID prefix ──────────────────────────── --mode lookup --uuid <prefix>
67
+ │ ├─ By title or first prompt ────────────────── --mode find --title|--first-prompt
68
+ │ └─ Generate a `claude --resume` command ───── --mode resume-cmd --uuid <prefix>
69
+
70
+ ├─ Search content
71
+ │ ├─ One session ─────────────────────────────── --mode search --file …
72
+ │ ├─ One project ─────────────────────────────── --mode search --cwd …
73
+ │ ├─ All projects ────────────────────────────── --mode search --all-projects
74
+ │ └─ Filter to user msgs / tool-use inputs ──── --role user --in tool_use
75
+
76
+ ├─ Forensics on a session
77
+ │ ├─ Chronological tool-call log ────────────── --mode changelog
78
+ │ ├─ Every file touched ─────────────────────── --mode file-edits
79
+ │ └─ Every tool call (optionally filtered) ──── --mode tool-calls --tool Bash,Edit
80
+
81
+ ├─ Subagent (fan-out) work
82
+ │ ├─ List spawned subagents ─────────────────── --mode subagent-list
83
+ │ ├─ Get every subagent's final message ─────── --mode subagent-finals
84
+ │ └─ Forensics on one subagent ──────────────── --mode subagent-tools|subagent-files --subagent …
85
+
86
+ ├─ Cross-cutting reporting
87
+ │ ├─ "What did I do this week?" ──────────────── --mode journal --since 7d
88
+ │ ├─ "Where did my day go?" / day timeline ───── --mode timeline --date yesterday
89
+ │ ├─ "How much time on <project>?" ───────────── --mode timeline --project <name> --date …
90
+ │ ├─ Count sessions matching a query ─────────── --mode count --query …
91
+ │ └─ Resume where I left off in this project ─── --mode resume-prev --cwd …
92
+
93
+ └─ Compare two sessions or two sibling subagents
94
+ └─ Interleaved diff ──────────────────────── --mode diff --file-a … --file-b … (or --subagents-of …)
95
+ ```
96
+
97
+ ## Quick reference
98
+
99
+ | Mode | Required flags | Returns |
100
+ |---|---|---|
101
+ | `last` | `--file` | Last N assistant text outputs |
102
+ | `advisor` | `--file` | All `advisor_tool_result` payloads |
103
+ | `pre-compact` | `--file` | 40 exchanges before the most recent `/compact` |
104
+ | `dump` | `--file` | Human-readable dump (auto-degrades on transcripts >5MB) |
105
+ | `search` | `--query` + scope | Matches windowed for context (supports `--role`, `--in text|tool_use|thinking|all`) |
106
+ | `debug` | `--file` | Entry/block type distributions + probes |
107
+ | `brief` | `--file` | 6-line summary: uuid·project·mtime·status / intent / last / edits / tools / subagents |
108
+ | `list` | `--cwd` or `--all-projects` | Rich table: mtime, size, uuid, msg count, flags, status, title |
109
+ | `lookup` | `--uuid <prefix>` | Absolute path (exit 1 missing, exit 2 ambiguous) |
110
+ | `find` | `--title` or `--first-prompt` | Sessions ranked by recency |
111
+ | `resume-cmd` | `--uuid <prefix>` | `cd <cwd>; claude --resume <uuid>` snippet |
112
+ | `changelog` | `--file` | `HH:MM:SS TOOL one-line-summary`, day-grouped |
113
+ | `file-edits` | `--file` | Unique paths sorted with op tags |
114
+ | `tool-calls` | `--file` (+ `--tool` filter) | Timestamped per-call blocks |
115
+ | `subagent-list` | `--file` | List sibling subagents with agentType + description |
116
+ | `subagent-finals` | `--file` | Every subagent's final assistant message |
117
+ | `subagent-tools` | `--subagent` | Forensics on one subagent |
118
+ | `subagent-files` | `--subagent` | Files one subagent touched |
119
+ | `resume-prev` | `--cwd` | Banner + dump-style tail of last 10 exchanges |
120
+ | `count` | `--query` (+ scope) | `<N>` to stdout, summary to stderr |
121
+ | `journal` | `--since` (+ scope) | Per-session 5-line block: date·uuid / prompt / ended / edits / tools |
122
+ | `timeline` | `--date` or `--since/--until` (defaults: today, all projects) | Block-grouped day timeline across sessions with idle gaps + active-time totals |
123
+ | `diff` | `--file-a` + `--file-b` OR `--subagents-of` | Timestamp-interleaved A>/B> output |
124
+
125
+ ## Global flags
126
+
127
+ - `--root {live|mirror|snapshot-24h|snapshot-1w|snapshot-1mo|<abs-path>}` — read from a `.claude-backups` mirror or snapshot instead of live. Default `live`.
128
+ - `--cwd <path>` — single-project scope. Use the original working directory (e.g. `"C:\Users\2supe\Other Claude Code"`).
129
+ - `--all-projects` — walk every project under `--root`.
130
+ - `--project <name>` — filter projects by name substring, case-insensitive, matches encoded or decoded form (`--project keel`, `--project "Other Claude Code"`). Works on `list`, `journal`, `search`, `count`, `find`, `timeline`. Use this instead of `--cwd` when you know the project's name but not its exact path.
131
+ - `--file <path>` — single-session scope.
132
+ - `--since <spec>` / `--until <spec>` — accepts ISO date, ISO datetime, relative (`30m`, `24h`, `7d`, `1w`, `1mo`), named (`today`, `yesterday`, `now`).
133
+ - `--date <spec>` — single-day window for `timeline` (`--date yesterday`, `--date 2026-06-01`).
134
+ - `--gap <spec>` — idle-gap threshold for `timeline` blocks (`15m` default, `1h`).
135
+ - `--tz <spec>` — display timezone override (IANA name, `UTC`, or offset like `-4`). Default: system local time.
136
+ - `--format json` (or `--json`) — structured JSON output on every mode (except the legacy `--list`/`--list-subagents` aliases). Pipe-friendly: paths are strings, timestamps ISO.
137
+ - `--exclude-current` — drop the current session (detected via `CLAUDE_SESSION_ID`) from `list`, `journal`, `search`, `count`, and `timeline`.
138
+ - `--include-subagents` — fold subagent finals into `brief`, `last`, `dump` output, each tagged `[subagent <id-short> · <agentType>]`.
139
+ - `--force-dump` — bypass the 5 MB `dump` guard.
140
+ - `-n N` — count modifier (default 5 for `last`, 80 for `dump`, 10 for `resume-prev`).
141
+
142
+ The current session is marked with `[*]` in `list` output. Status glyphs: ✓ clean, ! interrupted, ? pending-user, ● active. Sessions with a compact marker get `[C]`; sessions with subagents get `[S]`.
143
+
144
+ ## Backup-aware reads
145
+
146
+ In March 2026 a Claude Code auto-update deleted live `.jsonl` transcripts (GitHub #41591). To survive that class of incident, this machine maintains four backup roots under `C:\Users\2supe\.claude-backups\`:
147
+
148
+ - `mirror` — continuous mirror
149
+ - `snapshot-24h` — 24-hour-old snapshot
150
+ - `snapshot-1w` — 1-week-old snapshot
151
+ - `snapshot-1mo` — 1-month-old snapshot
152
+
153
+ Any mode accepts `--root <name>`. The resolved root is printed to stderr.
154
+
155
+ ```bash
156
+ # Find a session that was deleted from live but still in yesterday's snapshot
157
+ python "$PY" --root snapshot-24h --mode lookup --uuid <prefix>
158
+
159
+ # Compare today's mirror against a week ago to confirm what was lost
160
+ python "$PY" --root snapshot-1w --cwd "C:\Users\2supe\Other Claude Code" --list
161
+ ```
162
+
163
+ See `references/recipes.md` → "Deletion-incident recovery" for the full playbook.
164
+
165
+ ## Common workflows
166
+
167
+ | Need | Command |
168
+ |---|---|
169
+ | Recover advisor output that scrolled out of context | `--file <session> --mode advisor` |
170
+ | Get back to what you were doing before `/compact` | `--file <session> --mode pre-compact` |
171
+ | Fan-out triage: 14 subagents, want all of their finals | `--file <parent> --mode subagent-finals` (or `--mode brief --include-subagents`) |
172
+ | Find "that session where I asked about supabase rate limits" | `--mode search --all-projects --query "supabase rate limits"` |
173
+ | Resume a project after a few days away | `--mode resume-prev --cwd "<project path>"` |
174
+ | Daily/weekly journal | `--mode journal --since 7d --all-projects` |
175
+ | "Where did yesterday go?" | `--mode timeline --date yesterday` |
176
+ | "How much time on Keel today?" | `--mode timeline --project keel --date today` |
177
+ | Feed session data into a script | any mode + `--format json` |
178
+
179
+ See `references/recipes.md` for fuller multi-step workflows.
180
+
181
+ ## Windows notes
182
+
183
+ - Use `python` (not `python3`) on this Windows setup.
184
+ - The script handles UTF-8 stdout/stderr internally — both PowerShell and Bash work fine for single commands.
185
+ - **Piping `--format json` into another command: use Bash.** PowerShell 5.1 pipes inject a UTF-8 BOM and re-encode through the console codepage, breaking `json.load` (see `references/recipes.md` §5c for the PowerShell workaround).
186
+ - File paths with spaces need quoting: `--cwd "C:\Users\2supe\Other Claude Code"`.
187
+
188
+ ## Reference docs
189
+
190
+ - `references/modes.md` — complete per-mode reference (every flag, every example, exit codes).
191
+ - `references/jsonl-schema.md` — entry/block schema, subagent meta sidecars, compact-marker shape.
192
+ - `references/recipes.md` — multi-step workflows (post-compact recovery, find-then-dump, deletion recovery, week-in-review journal, sibling-agent diff).
193
+
194
+ ## When the modes return empty
195
+
196
+ If a mode returns empty/unexpected output, run `--mode debug` first. It prints the entry-type distribution, content-block types, and probes for advisor + compact markers — useful when the transcript schema has drifted or when a session was truncated.
@@ -0,0 +1,126 @@
1
+ # JSONL schema reference
2
+
3
+ What's actually inside a Claude Code session `.jsonl`. Only relevant when extending the script or debugging an unexpected empty result.
4
+
5
+ ## File location
6
+
7
+ ```
8
+ C:\Users\<user>\.claude\projects\<encoded-cwd>\<session-uuid>.jsonl
9
+ ```
10
+
11
+ The `<encoded-cwd>` is the original working directory with every `:`, `\`, `/`, and whitespace character replaced by `-`. Examples on this machine:
12
+
13
+ | Original CWD | Encoded directory |
14
+ |---|---|
15
+ | `C:\Users\2supe\Other Claude Code` | `C--Users-2supe-Other-Claude-Code` |
16
+ | `C:\Users\2supe\Other Claude Code\Personal Brand Project` | `C--Users-2supe-Other-Claude-Code-Personal-Brand-Project` |
17
+ | `C:\Users\2supe\AppData\Local\Temp` | `C--Users-2supe-AppData-Local-Temp` |
18
+
19
+ The encoding is lossy — single hyphens in the original path collapse into the same hyphens that separate segments. Use `--mode lookup` / `--mode find` to recover the actual session path; `decode_project_name()` produces a display-only approximation.
20
+
21
+ ## Backup roots
22
+
23
+ The same encoded-directory layout exists under each `.claude-backups\<name>\projects\` root:
24
+
25
+ ```
26
+ C:\Users\2supe\.claude-backups\
27
+ ├── mirror\projects\<encoded-cwd>\<uuid>.jsonl
28
+ ├── snapshot-24h\projects\<encoded-cwd>\<uuid>.jsonl
29
+ ├── snapshot-1w\projects\<encoded-cwd>\<uuid>.jsonl
30
+ └── snapshot-1mo\projects\<encoded-cwd>\<uuid>.jsonl
31
+ ```
32
+
33
+ `--root mirror|snapshot-24h|snapshot-1w|snapshot-1mo` rebases all path resolution to that snapshot. An absolute path argument is also accepted.
34
+
35
+ ## Subagent transcripts
36
+
37
+ When a session spawns subagents (via the `Agent` / `Task` tool), each subagent's own transcript is written to:
38
+
39
+ ```
40
+ <project-dir>\<session-uuid>\subagents\agent-<id>.jsonl
41
+ ```
42
+
43
+ A sidecar metadata file lives next to it:
44
+
45
+ ```
46
+ <project-dir>\<session-uuid>\subagents\agent-<id>.meta.json
47
+ ```
48
+
49
+ The meta file contains:
50
+
51
+ ```json
52
+ {
53
+ "agentType": "Explore",
54
+ "description": "Find every reference to X"
55
+ }
56
+ ```
57
+
58
+ When the meta file is missing, `subagents.load_meta` returns `{"agentType": "unknown", "description": ""}`.
59
+
60
+ Subagent entries inside the parent transcript are marked with `isSidechain: true` and carry an `agentId` field.
61
+
62
+ ## Entry types
63
+
64
+ Each line in a `.jsonl` is a JSON object with a `type` field. Entry classifications:
65
+
66
+ | `type` value | Classification | Notes |
67
+ |---|---|---|
68
+ | `user` | signal (user) | `message.content` may be string or array. Compact markers live here. |
69
+ | `assistant` | signal (assistant) | `message.content` is always an array of blocks. |
70
+ | `ai-title` / `custom-title` | title | `aiTitle` / `customTitle` field carries the session title. |
71
+ | `permission-mode` | noise | mode change events |
72
+ | `attachment` | noise | file attachments |
73
+ | `last-prompt` | noise | cached last prompt |
74
+ | `queue-operation` | noise | internal queueing |
75
+ | `file-history-snapshot` | noise | file state snapshots |
76
+ | `system` | noise | system events |
77
+ | `agent-name` | noise | agent name metadata |
78
+ | `pr-link` | noise | PR linkage |
79
+
80
+ The `noise` and `title` entries are skipped by `get_messages()`. `debug` mode prints the full distribution.
81
+
82
+ ## Assistant content block types
83
+
84
+ The `message.content` array for assistant entries can hold:
85
+
86
+ | Block `type` | Field of interest | Meaning |
87
+ |---|---|---|
88
+ | `text` | `text` | The actual assistant text output. |
89
+ | `thinking` | `thinking` (or `text`) | Model internal reasoning. |
90
+ | `tool_use` | `name`, `input`, `id` | A regular tool call. `id` is the matching id for any later `tool_result`. |
91
+ | `server_tool_use` | `name`, `input` | Server-side tool call (e.g., advisor invocation). |
92
+ | `advisor_tool_result` | `content.text` | The advisor's reply. Always nested as `block.content.text`. |
93
+ | `tool_result` | `tool_use_id`, `content` | Result for a prior `tool_use`. `content` is a string or an array of `{type:"text", text:"..."}`. |
94
+
95
+ ## Compact marker
96
+
97
+ A `/compact` event appears as a `type:"user"` entry whose first text content starts with:
98
+
99
+ > `"This session is being continued from a previous conversation"`
100
+
101
+ `classify_entry` returns `"compact"` for these. Everything before the most-recent compact marker is the pre-compact conversation.
102
+
103
+ ## Timestamps
104
+
105
+ Most entries carry a `timestamp` field in ISO-8601 form (`2026-05-01T10:00:05Z`). `_parse_timestamp` accepts ISO strings, naive ISO, and numeric epoch values. Timezone-aware values are stripped for comparison with `--since`/`--until` (which use local naive datetimes).
106
+
107
+ ## Title entries
108
+
109
+ Both `ai-title` and `custom-title` entries surface a `aiTitle` / `customTitle` string. `session_summary()` prefers `aiTitle` when both are present.
110
+
111
+ ## Truncation behavior
112
+
113
+ `iter_lines` drops the final line if it lacks a trailing newline AND fails to parse as JSON, printing `[note: dropped truncated trailing line in <name>]` to stderr. Malformed mid-file lines are dropped silently.
114
+
115
+ ## Status inference
116
+
117
+ `infer_status(lines, mtime, current_session_id, session_uuid)` returns one of:
118
+
119
+ | Status | Glyph | Heuristic |
120
+ |---|---|---|
121
+ | `active` | ● | `current_session_id == session_uuid` AND `mtime` within 5 minutes |
122
+ | `interrupted` | ! | Any `tool_use` block lacks a paired `tool_result` |
123
+ | `pending-user` | ? | Last assistant text message ends with `?` |
124
+ | `clean` | ✓ | none of the above |
125
+
126
+ This is heuristic — it's correct for the majority of real sessions on this machine but can be fooled by, e.g., an assistant message that legitimately ends with a question.