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.
- package/README.md +11 -0
- package/bin/install.cjs +134 -49
- package/package.json +1 -1
- package/skills/estack-read-claude-session-history/SKILL.md +196 -0
- package/skills/estack-read-claude-session-history/references/jsonl-schema.md +126 -0
- package/skills/estack-read-claude-session-history/references/modes.md +366 -0
- package/skills/estack-read-claude-session-history/references/recipes.md +237 -0
- package/skills/estack-read-claude-session-history/scripts/lib/__init__.py +1 -0
- package/skills/estack-read-claude-session-history/scripts/lib/parser.py +460 -0
- package/skills/estack-read-claude-session-history/scripts/lib/paths.py +234 -0
- package/skills/estack-read-claude-session-history/scripts/lib/search.py +179 -0
- package/skills/estack-read-claude-session-history/scripts/lib/subagents.py +88 -0
- package/skills/estack-read-claude-session-history/scripts/lib/tools.py +144 -0
- package/skills/estack-read-claude-session-history/scripts/read_transcript.py +1448 -0
- package/skills/estack-read-claude-session-history/scripts/tests/conftest.py +40 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/README.md +20 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/all-noise.jsonl +4 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/basic-session.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/interrupted.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/multi-compact.jsonl +8 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/pending-user.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta/subagents/agent-aaa.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.meta.json +1 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent.jsonl +4 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/time-spread.jsonl +6 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/timeline-day-test.jsonl +5 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-zoo.jsonl +10 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/truncated.jsonl +3 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/unicode.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-advisor.jsonl +3 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-compact.jsonl +5 -0
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-thinking.jsonl +2 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_backup_roots.py +56 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_json_format.py +201 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_modes.py +199 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_parser.py +195 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_paths.py +133 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_search.py +78 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_subagents.py +43 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_timeline.py +175 -0
- package/skills/estack-read-claude-session-history/scripts/tests/test_timezone_and_project.py +212 -0
- 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
|
-
|
|
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
|
|
71
|
+
const raw = fs.readFileSync(fullPath);
|
|
58
72
|
hash.update(relPath.replace(/\\/g, '/'));
|
|
59
|
-
hash.update(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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('
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
const
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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 (
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
@@ -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.
|