@aiplumber/session-recall 1.5.2 → 1.6.6
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 +55 -0
- package/package.json +1 -1
- package/session-recall +758 -123
package/README.md
CHANGED
|
@@ -122,6 +122,40 @@ session-recall checkpoints
|
|
|
122
122
|
session-recall last 1 --after "finished research"
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### tags - Mark Important Content
|
|
126
|
+
|
|
127
|
+
Tag any session content for later recall. Critical items auto-surface.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Preview what would be tagged (without tagging)
|
|
131
|
+
session-recall --back 3 toolcall
|
|
132
|
+
|
|
133
|
+
# Tag most recent tool call as critical
|
|
134
|
+
session-recall tag toolcall critical "found the root cause"
|
|
135
|
+
|
|
136
|
+
# Tag tool call from 3 calls ago
|
|
137
|
+
session-recall tag toolcall --back 3 important "useful data here"
|
|
138
|
+
|
|
139
|
+
# Tag other content types
|
|
140
|
+
session-recall tag agentcall critical "minion discovered pattern"
|
|
141
|
+
session-recall tag discourse important "user clarified requirement"
|
|
142
|
+
session-recall tag checkpoint critical "breakthrough moment"
|
|
143
|
+
|
|
144
|
+
# List all tags
|
|
145
|
+
session-recall tags
|
|
146
|
+
|
|
147
|
+
# Filter tags
|
|
148
|
+
session-recall tags critical # Only critical level
|
|
149
|
+
session-recall tags toolcall # Only tool calls
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Types:** `toolcall`, `agentcall`, `discourse`, `checkpoint`
|
|
153
|
+
|
|
154
|
+
**Levels:**
|
|
155
|
+
- `critical` - Auto-injected in recall output (always shown)
|
|
156
|
+
- `important` - Shown with `--show-important` flag
|
|
157
|
+
- `bookmark` - Lookup only, never injected
|
|
158
|
+
|
|
125
159
|
### rinse - Compress Session Data
|
|
126
160
|
|
|
127
161
|
```bash
|
|
@@ -190,6 +224,27 @@ session-recall tools --show 014opBVN
|
|
|
190
224
|
session-recall last 3 -f text > context.txt
|
|
191
225
|
```
|
|
192
226
|
|
|
227
|
+
### 5. Tag Critical Discoveries
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Run commands, discover something important
|
|
231
|
+
Bash: grep "API_KEY" .env
|
|
232
|
+
# Output: API_KEY=sk-live-abc123...
|
|
233
|
+
|
|
234
|
+
# Realize that was important (2 tool calls ago)
|
|
235
|
+
session-recall --back 2 toolcall
|
|
236
|
+
# Shows: UUID abc123 | grep "API_KEY" .env | API_KEY=sk-...
|
|
237
|
+
|
|
238
|
+
# Tag it
|
|
239
|
+
session-recall tag toolcall --back 2 critical "found exposed API key"
|
|
240
|
+
|
|
241
|
+
# Next session - critical items auto-surface in recall
|
|
242
|
+
session-recall last 2
|
|
243
|
+
# [16:42:15] ⚠ CRITICAL [toolcall] grep "API_KEY" .env
|
|
244
|
+
# Reason: "found exposed API key"
|
|
245
|
+
# Result: API_KEY=sk-live-abc123...
|
|
246
|
+
```
|
|
247
|
+
|
|
193
248
|
## Output Modes
|
|
194
249
|
|
|
195
250
|
| Flag | Tool Results | Use Case |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiplumber/session-recall",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"description": "Pull context from previous Claude Code sessions. Sessions end, context resets - this tool lets you continue where you left off.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"session-recall": "./session-recall"
|
package/session-recall
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const VERSION = '1.6.6';
|
|
4
|
+
|
|
3
5
|
const fs = require('fs');
|
|
4
6
|
const path = require('path');
|
|
5
7
|
|
|
@@ -333,6 +335,25 @@ function getToolResultText(msg) {
|
|
|
333
335
|
return typeof result.content === 'string' ? result.content : JSON.stringify(result.content);
|
|
334
336
|
}
|
|
335
337
|
|
|
338
|
+
// Extract tool_result content from user message (for marker-based search)
|
|
339
|
+
function getToolResultContent(msg) {
|
|
340
|
+
if (msg.type !== 'user') return null;
|
|
341
|
+
|
|
342
|
+
// Check toolUseResult.stdout
|
|
343
|
+
if (msg.toolUseResult?.stdout) return msg.toolUseResult.stdout;
|
|
344
|
+
|
|
345
|
+
// Check message.content array
|
|
346
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
347
|
+
for (const block of msg.message.content) {
|
|
348
|
+
if (block.type === 'tool_result' && block.content) {
|
|
349
|
+
return typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
336
357
|
function cmdRinse(inputPath, opts) {
|
|
337
358
|
// Check if input is a directory or file
|
|
338
359
|
const stat = fs.statSync(inputPath);
|
|
@@ -679,6 +700,42 @@ function cmdTools(opts) {
|
|
|
679
700
|
console.log(`Use: session-recall tools --show ${exampleId} to see specific result`);
|
|
680
701
|
}
|
|
681
702
|
|
|
703
|
+
// Checkpoint marker pattern for discovery (search tool_result content, not command strings)
|
|
704
|
+
const CHECKPOINT_MARKER_PATTERN = /@@SESSION-CHECKPOINT@@\s+"([^"]+)"(?:\s+uuid="([^"]*)")?(?:\s+stopped_at="([^"]*)")?(?:\s+next="([^"]*)")?(?:\s+blocker="([^"]*)")?(?:\s+decision="([^"]*)")?(?:\s+gotcha="([^"]*)")?\s+ts=(\S+)/;
|
|
705
|
+
// Groups: [1]=name, [2]=uuid, [3]=stopped_at, [4]=next, [5]=blocker, [6]=decision, [7]=gotcha, [8]=timestamp
|
|
706
|
+
|
|
707
|
+
// Scan session messages for checkpoint markers in tool_result content
|
|
708
|
+
function scanSessionCheckpoints(messages, sessionFile) {
|
|
709
|
+
const checkpoints = [];
|
|
710
|
+
|
|
711
|
+
for (const msg of messages) {
|
|
712
|
+
// Look for checkpoint markers in tool_result content (user messages)
|
|
713
|
+
const resultContent = getToolResultContent(msg);
|
|
714
|
+
if (!resultContent) continue;
|
|
715
|
+
|
|
716
|
+
const match = resultContent.match(CHECKPOINT_MARKER_PATTERN);
|
|
717
|
+
if (match) {
|
|
718
|
+
const fields = {};
|
|
719
|
+
if (match[3]) fields.stopped_at = match[3];
|
|
720
|
+
if (match[4]) fields.next = match[4];
|
|
721
|
+
if (match[5]) fields.blocker = match[5];
|
|
722
|
+
if (match[6]) fields.decision = match[6];
|
|
723
|
+
if (match[7]) fields.gotcha = match[7];
|
|
724
|
+
|
|
725
|
+
checkpoints.push({
|
|
726
|
+
name: match[1],
|
|
727
|
+
uuid: match[2] || sessionFile.replace('.jsonl', ''), // Use embedded uuid or fallback to session file
|
|
728
|
+
fields,
|
|
729
|
+
timestamp: match[8], // Use timestamp from marker
|
|
730
|
+
msgTimestamp: msg.timestamp, // Also keep message timestamp
|
|
731
|
+
session: sessionFile
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return checkpoints;
|
|
737
|
+
}
|
|
738
|
+
|
|
682
739
|
function cmdCheckpoints(opts) {
|
|
683
740
|
const dir = cwdToProjectFolder();
|
|
684
741
|
if (!dir) {
|
|
@@ -698,26 +755,8 @@ function cmdCheckpoints(opts) {
|
|
|
698
755
|
for (const file of jsonlFiles) {
|
|
699
756
|
const filePath = path.join(dir, file);
|
|
700
757
|
const msgs = readJsonl(filePath);
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
// Look for Bash tool calls with session-recall --checkpoint
|
|
704
|
-
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
705
|
-
for (const block of msg.message.content) {
|
|
706
|
-
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
707
|
-
const cmd = block.input?.command || '';
|
|
708
|
-
const match = cmd.match(/session-recall\s+--checkpoint\s+["']?([^"'\n]+)["']?/);
|
|
709
|
-
if (match) {
|
|
710
|
-
checkpoints.push({
|
|
711
|
-
name: match[1].trim(),
|
|
712
|
-
timestamp: msg.timestamp,
|
|
713
|
-
toolId: block.id,
|
|
714
|
-
session: file
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
758
|
+
const sessionCheckpoints = scanSessionCheckpoints(msgs, file);
|
|
759
|
+
checkpoints.push(...sessionCheckpoints);
|
|
721
760
|
}
|
|
722
761
|
|
|
723
762
|
if (checkpoints.length === 0) {
|
|
@@ -729,19 +768,51 @@ function cmdCheckpoints(opts) {
|
|
|
729
768
|
// Sort by timestamp
|
|
730
769
|
checkpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
731
770
|
|
|
771
|
+
// If --show, display specific checkpoint details
|
|
772
|
+
if (opts.show) {
|
|
773
|
+
const cp = checkpoints.find(c => c.name.toLowerCase().includes(opts.show.toLowerCase()));
|
|
774
|
+
if (!cp) {
|
|
775
|
+
console.error(`Checkpoint not found: "${opts.show}"`);
|
|
776
|
+
console.error(`\nAvailable checkpoints:`);
|
|
777
|
+
checkpoints.forEach(c => console.error(` "${c.name}"`));
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log(`=== CHECKPOINT: ${cp.name} ===`);
|
|
782
|
+
console.log(`UUID: ${cp.uuid}`);
|
|
783
|
+
console.log(`Time: ${cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : 'unknown'}`);
|
|
784
|
+
console.log(``);
|
|
785
|
+
|
|
786
|
+
const fieldNames = ['stopped_at', 'next', 'blocker', 'decision', 'gotcha'];
|
|
787
|
+
let hasFields = false;
|
|
788
|
+
for (const field of fieldNames) {
|
|
789
|
+
if (cp.fields[field]) {
|
|
790
|
+
console.log(`${field}: ${cp.fields[field]}`);
|
|
791
|
+
hasFields = true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (!hasFields) {
|
|
795
|
+
console.log(`(no structured fields)`);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// List all checkpoints
|
|
732
801
|
console.log(`=== CHECKPOINTS (${checkpoints.length} total) ===`);
|
|
733
802
|
console.log(``);
|
|
734
|
-
console.log(`# TIME
|
|
803
|
+
console.log(`# TIME NAME FIELDS`);
|
|
735
804
|
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
736
805
|
|
|
737
806
|
checkpoints.forEach((cp, i) => {
|
|
738
807
|
const ts = cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
739
|
-
const
|
|
740
|
-
|
|
808
|
+
const name = cp.name.substring(0, 32).padEnd(32);
|
|
809
|
+
const fieldList = Object.keys(cp.fields).filter(k => cp.fields[k]).join(', ') || '-';
|
|
810
|
+
console.log(`${String(i + 1).padStart(2)} ${ts} ${name} ${fieldList}`);
|
|
741
811
|
});
|
|
742
812
|
|
|
743
813
|
console.log(``);
|
|
744
|
-
console.log(`Use: session-recall
|
|
814
|
+
console.log(`Use: session-recall checkpoints --show "name" for details`);
|
|
815
|
+
console.log(`Use: session-recall last 1 --after "name" to recall from checkpoint`);
|
|
745
816
|
}
|
|
746
817
|
|
|
747
818
|
function calcSessionCosts(messages) {
|
|
@@ -970,74 +1041,54 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
970
1041
|
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
971
1042
|
let checkpoint = null;
|
|
972
1043
|
|
|
1044
|
+
// Collect all checkpoints using marker-based search
|
|
1045
|
+
const allCheckpoints = [];
|
|
973
1046
|
for (const file of jsonlFiles) {
|
|
974
1047
|
const filePath = path.join(dir, file);
|
|
975
1048
|
const msgs = readJsonl(filePath);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
// Look for Bash tool calls with session-recall --checkpoint
|
|
979
|
-
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
980
|
-
for (const block of msg.message.content) {
|
|
981
|
-
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
982
|
-
const cmd = block.input?.command || '';
|
|
983
|
-
const match = cmd.match(/session-recall\s+--checkpoint\s+["']?([^"'\n]+)["']?/);
|
|
984
|
-
if (match && match[1].trim() === opts.after) {
|
|
985
|
-
checkpoint = {
|
|
986
|
-
name: match[1].trim(),
|
|
987
|
-
timestamp: msg.timestamp,
|
|
988
|
-
session: file
|
|
989
|
-
};
|
|
990
|
-
break;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
if (checkpoint) break;
|
|
996
|
-
}
|
|
997
|
-
if (checkpoint) break;
|
|
1049
|
+
const sessionCheckpoints = scanSessionCheckpoints(msgs, file);
|
|
1050
|
+
allCheckpoints.push(...sessionCheckpoints);
|
|
998
1051
|
}
|
|
999
1052
|
|
|
1053
|
+
// Find the matching checkpoint
|
|
1054
|
+
checkpoint = allCheckpoints.find(cp => cp.name === opts.after);
|
|
1055
|
+
|
|
1000
1056
|
if (!checkpoint) {
|
|
1001
1057
|
console.error(`Checkpoint not found: "${opts.after}"`);
|
|
1002
1058
|
console.error(`Available checkpoints:`);
|
|
1003
1059
|
|
|
1004
|
-
|
|
1005
|
-
const checkpoints = [];
|
|
1006
|
-
for (const file of jsonlFiles) {
|
|
1007
|
-
const filePath = path.join(dir, file);
|
|
1008
|
-
const msgs = readJsonl(filePath);
|
|
1009
|
-
|
|
1010
|
-
for (const msg of msgs) {
|
|
1011
|
-
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1012
|
-
for (const block of msg.message.content) {
|
|
1013
|
-
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
1014
|
-
const cmd = block.input?.command || '';
|
|
1015
|
-
const match = cmd.match(/session-recall\s+--checkpoint\s+["']?([^"'\n]+)["']?/);
|
|
1016
|
-
if (match) {
|
|
1017
|
-
checkpoints.push({
|
|
1018
|
-
name: match[1].trim(),
|
|
1019
|
-
timestamp: msg.timestamp
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (checkpoints.length === 0) {
|
|
1060
|
+
if (allCheckpoints.length === 0) {
|
|
1029
1061
|
console.error(' (none)');
|
|
1030
1062
|
} else {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const ts = cp.timestamp ?
|
|
1063
|
+
allCheckpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
1064
|
+
allCheckpoints.forEach(cp => {
|
|
1065
|
+
const ts = cp.timestamp ? cp.timestamp.substring(11, 19) : '??:??:??';
|
|
1034
1066
|
console.error(` "${cp.name}" (${ts})`);
|
|
1035
1067
|
});
|
|
1036
1068
|
}
|
|
1037
1069
|
process.exit(1);
|
|
1038
1070
|
}
|
|
1039
1071
|
|
|
1040
|
-
|
|
1072
|
+
// Use msgTimestamp for filtering (timestamp from jsonl message, not marker)
|
|
1073
|
+
filterAfterTs = checkpoint.msgTimestamp ? new Date(checkpoint.msgTimestamp).getTime() : new Date(checkpoint.timestamp).getTime();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Helper: get session start timestamp from first line of jsonl
|
|
1077
|
+
function getSessionStartTs(filePath) {
|
|
1078
|
+
try {
|
|
1079
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1080
|
+
const firstLine = content.split('\n')[0];
|
|
1081
|
+
if (!firstLine) return null;
|
|
1082
|
+
const obj = JSON.parse(firstLine);
|
|
1083
|
+
// Check snapshot.timestamp first (common in first line), then top-level timestamp
|
|
1084
|
+
let ts = obj.snapshot?.timestamp || obj.timestamp;
|
|
1085
|
+
if (ts) return new Date(ts);
|
|
1086
|
+
// Fallback: find first timestamp in file
|
|
1087
|
+
const match = content.match(/"timestamp":"([^"]+)"/);
|
|
1088
|
+
return match ? new Date(match[1]) : null;
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1041
1092
|
}
|
|
1042
1093
|
|
|
1043
1094
|
// Collect all sessions
|
|
@@ -1048,10 +1099,9 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1048
1099
|
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1049
1100
|
for (const file of jsonlFiles) {
|
|
1050
1101
|
const filePath = path.join(dir, file);
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
allSessions.push({ filePath, msgs, lastTs, project: path.basename(dir) });
|
|
1102
|
+
const startTs = getSessionStartTs(filePath);
|
|
1103
|
+
// Defer full read until needed
|
|
1104
|
+
allSessions.push({ filePath, startTs, project: path.basename(dir), _msgs: null });
|
|
1055
1105
|
}
|
|
1056
1106
|
} else {
|
|
1057
1107
|
// Scan all projects
|
|
@@ -1069,10 +1119,8 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1069
1119
|
for (const file of jsonlFiles) {
|
|
1070
1120
|
const filePath = path.join(projPath, file);
|
|
1071
1121
|
try {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
|
|
1075
|
-
allSessions.push({ filePath, msgs, lastTs, project: proj });
|
|
1122
|
+
const startTs = getSessionStartTs(filePath);
|
|
1123
|
+
allSessions.push({ filePath, startTs, project: proj, _msgs: null });
|
|
1076
1124
|
} catch (e) {
|
|
1077
1125
|
// Skip unreadable files
|
|
1078
1126
|
}
|
|
@@ -1080,12 +1128,22 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1080
1128
|
}
|
|
1081
1129
|
}
|
|
1082
1130
|
|
|
1083
|
-
// Sort by
|
|
1084
|
-
allSessions.sort((a, b) => (b.
|
|
1131
|
+
// Sort by session start timestamp descending (most recent first)
|
|
1132
|
+
allSessions.sort((a, b) => (b.startTs || 0) - (a.startTs || 0));
|
|
1133
|
+
|
|
1134
|
+
// Now load messages for top candidates and compute lastTs for active filter
|
|
1135
|
+
// We check more than n to account for filtering out active sessions
|
|
1136
|
+
const candidateCount = Math.min(allSessions.length, n + 5);
|
|
1137
|
+
for (let i = 0; i < candidateCount; i++) {
|
|
1138
|
+
const s = allSessions[i];
|
|
1139
|
+
s.msgs = readJsonl(s.filePath);
|
|
1140
|
+
const timestamps = s.msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
1141
|
+
s.lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : s.startTs;
|
|
1142
|
+
}
|
|
1085
1143
|
|
|
1086
1144
|
// Exclude currently active sessions (activity within threshold)
|
|
1087
1145
|
const now = Date.now();
|
|
1088
|
-
const inactiveSessions = allSessions.filter(s => {
|
|
1146
|
+
const inactiveSessions = allSessions.slice(0, candidateCount).filter(s => {
|
|
1089
1147
|
if (!s.lastTs) return true; // Include sessions with no timestamp
|
|
1090
1148
|
const ageMs = now - s.lastTs.getTime();
|
|
1091
1149
|
return ageMs > ACTIVE_SESSION_THRESHOLD_MS;
|
|
@@ -1099,8 +1157,8 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1099
1157
|
process.exit(1);
|
|
1100
1158
|
}
|
|
1101
1159
|
|
|
1102
|
-
// Count excluded active sessions for reporting
|
|
1103
|
-
const excludedActive =
|
|
1160
|
+
// Count excluded active sessions for reporting (only from candidates we checked)
|
|
1161
|
+
const excludedActive = candidateCount - inactiveSessions.length;
|
|
1104
1162
|
|
|
1105
1163
|
if (opts.dryRun) {
|
|
1106
1164
|
console.log(`--- DRY RUN: last ${n} ---`);
|
|
@@ -1144,47 +1202,104 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1144
1202
|
return;
|
|
1145
1203
|
}
|
|
1146
1204
|
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
allMsgs.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1205
|
+
// Print header
|
|
1206
|
+
console.log(`[session-recall v${VERSION}] This context has been stripped of most tool results to reduce noise.`);
|
|
1207
|
+
console.log(`---`);
|
|
1151
1208
|
|
|
1152
|
-
//
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
return msgTs > filterAfterTs;
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1209
|
+
// Process each session separately to add separators
|
|
1210
|
+
for (let sessionIdx = 0; sessionIdx < selected.length; sessionIdx++) {
|
|
1211
|
+
const session = selected[sessionIdx];
|
|
1212
|
+
let sessionMsgs = session.msgs;
|
|
1159
1213
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1214
|
+
// Sort by timestamp
|
|
1215
|
+
sessionMsgs.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1162
1216
|
|
|
1163
|
-
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1217
|
+
// Filter by checkpoint timestamp if --after was specified
|
|
1218
|
+
if (filterAfterTs) {
|
|
1219
|
+
sessionMsgs = sessionMsgs.filter(msg => {
|
|
1220
|
+
const msgTs = msg.timestamp ? new Date(msg.timestamp).getTime() : 0;
|
|
1221
|
+
return msgTs > filterAfterTs;
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Build tool result metadata (size, duration)
|
|
1226
|
+
const resultMeta = buildToolResultMeta(sessionMsgs);
|
|
1227
|
+
|
|
1228
|
+
// Build tag map for this session
|
|
1229
|
+
const tagMap = {};
|
|
1230
|
+
const sessionTags = scanSessionTags(session.filePath);
|
|
1231
|
+
for (const tag of sessionTags) {
|
|
1232
|
+
if (tag.targetUuid) {
|
|
1233
|
+
tagMap[tag.targetUuid] = {
|
|
1234
|
+
type: tag.type,
|
|
1235
|
+
level: tag.level,
|
|
1236
|
+
reason: tag.reason
|
|
1237
|
+
};
|
|
1171
1238
|
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
for (const msg of sessionMsgs) {
|
|
1242
|
+
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
1243
|
+
|
|
1244
|
+
// Check if this message has a tag
|
|
1245
|
+
const toolId = getToolUseId(msg);
|
|
1246
|
+
const msgUuid = toolId || msg.uuid;
|
|
1247
|
+
const tag = msgUuid ? tagMap[msgUuid] : null;
|
|
1248
|
+
|
|
1249
|
+
// Determine if we should show this tag
|
|
1250
|
+
const showTag = tag && (
|
|
1251
|
+
tag.level === 'critical' ||
|
|
1252
|
+
(tag.level === 'important' && opts.showImportant)
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
if (msg.type === 'user') {
|
|
1256
|
+
if (isToolResult(msg)) continue;
|
|
1257
|
+
const text = getUserText(msg);
|
|
1258
|
+
if (text) {
|
|
1259
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
1260
|
+
if (showTag) {
|
|
1261
|
+
const marker = tag.level === 'critical' ? '\u26A0' : '\u2139';
|
|
1262
|
+
console.log(`[${ts}] ${marker} ${tag.level.toUpperCase()} [${tag.type}] USER: ${text.substring(0, 500)}`);
|
|
1263
|
+
console.log(` Reason: "${tag.reason}"`);
|
|
1264
|
+
} else {
|
|
1265
|
+
console.log(`[${ts}] USER: ${text.substring(0, 500)}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
} else if (msg.type === 'assistant') {
|
|
1269
|
+
const text = getAssistantText(msg);
|
|
1270
|
+
const toolSummary = collapseToolCall(msg, resultMeta);
|
|
1271
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
1272
|
+
|
|
1273
|
+
if (showTag) {
|
|
1274
|
+
const marker = tag.level === 'critical' ? '\u26A0' : '\u2139';
|
|
1275
|
+
if (text) {
|
|
1276
|
+
console.log(`[${ts}] ${marker} ${tag.level.toUpperCase()} [${tag.type}] ASSISTANT: ${text.substring(0, 500)}`);
|
|
1277
|
+
} else if (toolSummary) {
|
|
1278
|
+
console.log(`[${ts}] ${marker} ${tag.level.toUpperCase()} [${tag.type}] ${toolSummary}`);
|
|
1279
|
+
}
|
|
1280
|
+
console.log(` Reason: "${tag.reason}"`);
|
|
1281
|
+
} else {
|
|
1282
|
+
if (text) {
|
|
1283
|
+
console.log(`[${ts}] ASSISTANT: ${text.substring(0, 500)}`);
|
|
1284
|
+
} else if (toolSummary) {
|
|
1285
|
+
console.log(`[${ts}] ASSISTANT: ${toolSummary}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1180
1288
|
}
|
|
1181
1289
|
}
|
|
1290
|
+
|
|
1291
|
+
// Print session separator if not the last session
|
|
1292
|
+
if (sessionIdx < selected.length - 1) {
|
|
1293
|
+
const nextSession = selected[sessionIdx + 1];
|
|
1294
|
+
const nextTs = nextSession.lastTs ? nextSession.lastTs.toISOString().replace('T', ' ').substring(0, 16) : 'unknown';
|
|
1295
|
+
console.log(`\n=== END SESSION ${sessionIdx + 1} === NEXT SESSION: ${nextSession.project} (${nextTs}) ===\n`);
|
|
1296
|
+
}
|
|
1182
1297
|
}
|
|
1183
1298
|
|
|
1184
1299
|
// Print instructions for Claude
|
|
1185
1300
|
console.log(``);
|
|
1186
1301
|
console.log(`---`);
|
|
1187
|
-
console.log(`
|
|
1302
|
+
console.log(`Tool results: Most are noise. Only fetch with "session-recall tools --show <id>" when you see something you actually need.`);
|
|
1188
1303
|
console.log(`Session file: ${selected[0]?.filePath || 'unknown'}`);
|
|
1189
1304
|
}
|
|
1190
1305
|
|
|
@@ -1219,16 +1334,39 @@ COMMANDS:
|
|
|
1219
1334
|
last [N] [folder] Get last N sessions (default: 1, CWD project only)
|
|
1220
1335
|
-a, --all Scan all projects (not just CWD)
|
|
1221
1336
|
-d, --dry-run Show what would be pulled without output
|
|
1337
|
+
--show-important Include important-level tags in output
|
|
1222
1338
|
|
|
1223
1339
|
tools List tool calls in most recent session (CWD project)
|
|
1224
1340
|
--show <id> Show specific tool result by ID (partial match)
|
|
1225
1341
|
|
|
1226
1342
|
checkpoints List all checkpoints in current project
|
|
1343
|
+
--show "name" Show checkpoint details (partial match)
|
|
1227
1344
|
|
|
1228
|
-
--checkpoint "name" Create a checkpoint marker
|
|
1345
|
+
--checkpoint "name" Create a checkpoint marker
|
|
1346
|
+
--stopped_at "point" Where work stopped (file:line, step N of M)
|
|
1347
|
+
--next "action" Single next action to take
|
|
1348
|
+
--blocker "issue" What's blocking progress (optional)
|
|
1349
|
+
--decision "choice" Decision made + reasoning (optional)
|
|
1350
|
+
--gotcha "trap" Gotcha for next session (optional)
|
|
1229
1351
|
|
|
1230
1352
|
last [N] --after "name" Recall from after a checkpoint
|
|
1231
1353
|
|
|
1354
|
+
TAGGING:
|
|
1355
|
+
--back N [type] Preview message N positions back (for tagging)
|
|
1356
|
+
Types: toolcall, agentcall, discourse, checkpoint
|
|
1357
|
+
|
|
1358
|
+
tag <type> [--back N] <level> "reason"
|
|
1359
|
+
Tag a message for recall surfacing
|
|
1360
|
+
Types: toolcall, agentcall, discourse, checkpoint
|
|
1361
|
+
Levels: critical (auto-show), important (opt-in), bookmark (lookup)
|
|
1362
|
+
|
|
1363
|
+
tags [filter] List all tags in current project
|
|
1364
|
+
Filter by type or level
|
|
1365
|
+
|
|
1366
|
+
OPTIONS:
|
|
1367
|
+
-v, --version Show version number
|
|
1368
|
+
-h, --help Show this help
|
|
1369
|
+
|
|
1232
1370
|
EXAMPLES:
|
|
1233
1371
|
session-recall last 1 -d # Check what's available
|
|
1234
1372
|
session-recall last 1 -f text # Pull last session
|
|
@@ -1236,12 +1374,401 @@ EXAMPLES:
|
|
|
1236
1374
|
session-recall tools --show 014opBVN # Get specific tool result
|
|
1237
1375
|
|
|
1238
1376
|
# Checkpoints - mark progress, recall from any point
|
|
1239
|
-
session-recall --checkpoint "finished research" #
|
|
1377
|
+
session-recall --checkpoint "finished research" # Simple checkpoint
|
|
1378
|
+
session-recall --checkpoint "hit blocker" --stopped_at "auth.ts:42" --blocker "API 403"
|
|
1240
1379
|
session-recall checkpoints # List all checkpoints
|
|
1380
|
+
session-recall checkpoints --show "blocker" # Show checkpoint details
|
|
1241
1381
|
session-recall last 1 --after "finished research" # Recall from checkpoint
|
|
1382
|
+
|
|
1383
|
+
# Tagging - mark important moments
|
|
1384
|
+
session-recall --back 3 toolcall # Preview 3rd previous tool call
|
|
1385
|
+
session-recall tag toolcall critical "found the bug" # Tag most recent
|
|
1386
|
+
session-recall tag toolcall --back 3 important "useful data" # Tag 3 back
|
|
1387
|
+
session-recall tags # List all tags
|
|
1388
|
+
session-recall tags critical # List only critical tags
|
|
1389
|
+
session-recall last 1 # Critical tags auto-surface
|
|
1390
|
+
session-recall last 1 --show-important # Include important tags
|
|
1242
1391
|
`);
|
|
1243
1392
|
}
|
|
1244
1393
|
|
|
1394
|
+
// ========== MESSAGE TYPE CLASSIFICATION ==========
|
|
1395
|
+
|
|
1396
|
+
// Classify a message by its tagging type: toolcall, agentcall, discourse, checkpoint
|
|
1397
|
+
function classifyMessageType(msg, checkpointToolIds = new Set()) {
|
|
1398
|
+
if (!msg) return null;
|
|
1399
|
+
|
|
1400
|
+
// Check for checkpoint (Bash tool calls with session-recall --checkpoint)
|
|
1401
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1402
|
+
for (const block of msg.message.content) {
|
|
1403
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
1404
|
+
const cmd = block.input?.command || '';
|
|
1405
|
+
if (cmd.match(/session-recall\s+--checkpoint/)) {
|
|
1406
|
+
return 'checkpoint';
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Check for tool_use in assistant messages
|
|
1413
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1414
|
+
const toolUses = msg.message.content.filter(c => c.type === 'tool_use');
|
|
1415
|
+
if (toolUses.length > 0) {
|
|
1416
|
+
// Check if any are Task tools (agentcall)
|
|
1417
|
+
const hasTask = toolUses.some(t => t.name === 'Task');
|
|
1418
|
+
if (hasTask) return 'agentcall';
|
|
1419
|
+
return 'toolcall';
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Check for discourse (user messages without tool_result, or assistant with text)
|
|
1424
|
+
if (msg.type === 'user') {
|
|
1425
|
+
if (!isToolResult(msg)) {
|
|
1426
|
+
const text = getUserText(msg);
|
|
1427
|
+
if (text && text.length > 0) return 'discourse';
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (msg.type === 'assistant') {
|
|
1433
|
+
const text = getAssistantText(msg);
|
|
1434
|
+
if (text && text.length > 0) return 'discourse';
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Get message preview for --back display
|
|
1441
|
+
function getMessagePreview(msg, msgType) {
|
|
1442
|
+
if (msgType === 'toolcall' || msgType === 'agentcall') {
|
|
1443
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1444
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1445
|
+
if (toolUse) {
|
|
1446
|
+
const name = toolUse.name;
|
|
1447
|
+
const input = toolUse.input || {};
|
|
1448
|
+
if (name === 'Task') {
|
|
1449
|
+
return `Task: ${input.description?.substring(0, 60) || 'subagent task'}`;
|
|
1450
|
+
} else if (name === 'Bash') {
|
|
1451
|
+
return `Bash: ${input.command?.substring(0, 60) || 'command'}`;
|
|
1452
|
+
} else if (name === 'Read') {
|
|
1453
|
+
return `Read: ${input.file_path}`;
|
|
1454
|
+
} else if (name === 'Edit' || name === 'Write') {
|
|
1455
|
+
return `${name}: ${input.file_path}`;
|
|
1456
|
+
} else if (name === 'Grep') {
|
|
1457
|
+
return `Grep: ${input.pattern?.substring(0, 40)}`;
|
|
1458
|
+
} else if (name === 'Glob') {
|
|
1459
|
+
return `Glob: ${input.pattern}`;
|
|
1460
|
+
} else {
|
|
1461
|
+
return `${name}`;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (msgType === 'checkpoint') {
|
|
1468
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1469
|
+
for (const block of msg.message.content) {
|
|
1470
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
1471
|
+
const cmd = block.input?.command || '';
|
|
1472
|
+
const match = cmd.match(/session-recall\s+--checkpoint\s+["']([^"']+)["']/);
|
|
1473
|
+
if (match) return `Checkpoint: "${match[1]}"`;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (msgType === 'discourse') {
|
|
1480
|
+
if (msg.type === 'user') {
|
|
1481
|
+
const text = getUserText(msg);
|
|
1482
|
+
return `User: ${text?.substring(0, 60) || '(empty)'}`;
|
|
1483
|
+
} else {
|
|
1484
|
+
const text = getAssistantText(msg);
|
|
1485
|
+
return `Assistant: ${text?.substring(0, 60) || '(empty)'}`;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
return '(unknown)';
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Get the tool_use ID from a message (for toolcall/agentcall)
|
|
1493
|
+
function getToolUseId(msg) {
|
|
1494
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1495
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1496
|
+
if (toolUse) return toolUse.id;
|
|
1497
|
+
}
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// ========== --back PREVIEW COMMAND ==========
|
|
1502
|
+
|
|
1503
|
+
function cmdBack(backN, filterType, opts) {
|
|
1504
|
+
const dir = cwdToProjectFolder();
|
|
1505
|
+
if (!dir) {
|
|
1506
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
1507
|
+
process.exit(1);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Get most recent session (INCLUDING active)
|
|
1511
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1512
|
+
if (jsonlFiles.length === 0) {
|
|
1513
|
+
console.error('No sessions found');
|
|
1514
|
+
process.exit(1);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Sort by modification time to get most recent
|
|
1518
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
1519
|
+
const filePath = path.join(dir, f);
|
|
1520
|
+
const stat = fs.statSync(filePath);
|
|
1521
|
+
return { filePath, mtime: stat.mtime };
|
|
1522
|
+
});
|
|
1523
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
1524
|
+
|
|
1525
|
+
// Use the most recent session (even if active)
|
|
1526
|
+
const session = filesWithStats[0];
|
|
1527
|
+
const messages = readJsonl(session.filePath);
|
|
1528
|
+
|
|
1529
|
+
// Filter messages by type
|
|
1530
|
+
const validTypes = ['toolcall', 'agentcall', 'discourse', 'checkpoint'];
|
|
1531
|
+
if (filterType && !validTypes.includes(filterType)) {
|
|
1532
|
+
console.error(`Invalid type: ${filterType}`);
|
|
1533
|
+
console.error(`Valid types: ${validTypes.join(', ')}`);
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Classify all messages and filter
|
|
1538
|
+
const classified = [];
|
|
1539
|
+
for (const msg of messages) {
|
|
1540
|
+
const msgType = classifyMessageType(msg);
|
|
1541
|
+
if (msgType && (!filterType || msgType === filterType)) {
|
|
1542
|
+
// Skip session-recall tag commands (they're toolcalls but shouldn't be counted)
|
|
1543
|
+
if (msgType === 'toolcall' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1544
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1545
|
+
if (toolUse && toolUse.name === 'Bash') {
|
|
1546
|
+
const cmd = toolUse.input?.command || '';
|
|
1547
|
+
if (cmd.match(/session-recall\s+tag\s+/)) continue;
|
|
1548
|
+
if (cmd.match(/session-recall\s+--back\s+/)) continue;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
classified.push({ msg, msgType });
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (classified.length === 0) {
|
|
1556
|
+
console.error(`No ${filterType || 'messages'} found in session`);
|
|
1557
|
+
process.exit(1);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Count back N from end
|
|
1561
|
+
const targetIndex = classified.length - backN;
|
|
1562
|
+
if (targetIndex < 0) {
|
|
1563
|
+
console.error(`Only ${classified.length} ${filterType || 'messages'} found, cannot go back ${backN}`);
|
|
1564
|
+
process.exit(1);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const target = classified[targetIndex];
|
|
1568
|
+
const toolId = getToolUseId(target.msg);
|
|
1569
|
+
const uuid = toolId || target.msg.uuid || '(no uuid)';
|
|
1570
|
+
const ts = target.msg.timestamp ? new Date(target.msg.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
1571
|
+
const preview = getMessagePreview(target.msg, target.msgType);
|
|
1572
|
+
|
|
1573
|
+
console.log(`UUID: ${uuid}`);
|
|
1574
|
+
console.log(`Type: ${target.msgType}`);
|
|
1575
|
+
console.log(`Time: ${ts}`);
|
|
1576
|
+
console.log(`Content: ${preview}`);
|
|
1577
|
+
console.log(``);
|
|
1578
|
+
console.log(`To tag this: session-recall tag ${target.msgType}${backN > 1 ? ' --back ' + backN : ''} <level> "reason"`);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// ========== TAG COMMAND ==========
|
|
1582
|
+
|
|
1583
|
+
function cmdTag(tagType, level, reason, backN, opts) {
|
|
1584
|
+
const validTypes = ['toolcall', 'agentcall', 'discourse', 'checkpoint'];
|
|
1585
|
+
const validLevels = ['critical', 'important', 'bookmark'];
|
|
1586
|
+
|
|
1587
|
+
if (!validTypes.includes(tagType)) {
|
|
1588
|
+
console.error(`Invalid type: ${tagType}`);
|
|
1589
|
+
console.error(`Valid types: ${validTypes.join(', ')}`);
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (!validLevels.includes(level)) {
|
|
1594
|
+
console.error(`Invalid level: ${level}`);
|
|
1595
|
+
console.error(`Valid levels: ${validLevels.join(', ')}`);
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (level === 'critical' && !reason) {
|
|
1600
|
+
console.error(`Critical tags require a reason`);
|
|
1601
|
+
process.exit(1);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const dir = cwdToProjectFolder();
|
|
1605
|
+
if (!dir) {
|
|
1606
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Get most recent session (INCLUDING active - this is the current session)
|
|
1611
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1612
|
+
if (jsonlFiles.length === 0) {
|
|
1613
|
+
console.error('No sessions found');
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Sort by modification time to get most recent
|
|
1618
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
1619
|
+
const filePath = path.join(dir, f);
|
|
1620
|
+
const stat = fs.statSync(filePath);
|
|
1621
|
+
return { filePath, mtime: stat.mtime };
|
|
1622
|
+
});
|
|
1623
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
1624
|
+
|
|
1625
|
+
// Use the most recent session
|
|
1626
|
+
const session = filesWithStats[0];
|
|
1627
|
+
const messages = readJsonl(session.filePath);
|
|
1628
|
+
|
|
1629
|
+
// Classify all messages and filter by type
|
|
1630
|
+
const classified = [];
|
|
1631
|
+
for (const msg of messages) {
|
|
1632
|
+
const msgType = classifyMessageType(msg);
|
|
1633
|
+
if (msgType === tagType) {
|
|
1634
|
+
// Skip session-recall tag/back commands
|
|
1635
|
+
if (msgType === 'toolcall' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1636
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1637
|
+
if (toolUse && toolUse.name === 'Bash') {
|
|
1638
|
+
const cmd = toolUse.input?.command || '';
|
|
1639
|
+
if (cmd.match(/session-recall\s+tag\s+/)) continue;
|
|
1640
|
+
if (cmd.match(/session-recall\s+--back\s+/)) continue;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
classified.push({ msg, msgType });
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (classified.length === 0) {
|
|
1648
|
+
console.error(`No ${tagType} found in session`);
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Count back N from end
|
|
1653
|
+
const targetIndex = classified.length - backN;
|
|
1654
|
+
if (targetIndex < 0) {
|
|
1655
|
+
console.error(`Only ${classified.length} ${tagType} found, cannot go back ${backN}`);
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const target = classified[targetIndex];
|
|
1660
|
+
const toolId = getToolUseId(target.msg);
|
|
1661
|
+
const uuid = toolId || target.msg.uuid || '(no uuid)';
|
|
1662
|
+
const isoTimestamp = target.msg.timestamp ? new Date(target.msg.timestamp).toISOString().replace(/\.\d{3}Z$/, 'Z') : new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1663
|
+
|
|
1664
|
+
// Output marker format - this IS the tag storage (searched in tool_result)
|
|
1665
|
+
// Format: @@SESSION-TAG@@ <type> <level> "<reason>" uuid=<uuid> ts=<timestamp>
|
|
1666
|
+
console.log(`@@SESSION-TAG@@ ${tagType} ${level} "${reason || ''}" uuid=${uuid} ts=${isoTimestamp}`);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// ========== TAGS LISTING COMMAND ==========
|
|
1670
|
+
|
|
1671
|
+
// Marker regex patterns for discovery (search tool_result content, not command strings)
|
|
1672
|
+
const TAG_MARKER_PATTERN = /@@SESSION-TAG@@\s+(\S+)\s+(\S+)\s+"([^"]*)"\s+uuid=(\S+)\s+ts=(\S+)/;
|
|
1673
|
+
// Groups: [1]=type, [2]=level, [3]=reason, [4]=uuid, [5]=timestamp
|
|
1674
|
+
|
|
1675
|
+
// Scan a session for tags by searching tool_result content for @@SESSION-TAG@@ markers
|
|
1676
|
+
function scanSessionTags(filePath) {
|
|
1677
|
+
const messages = readJsonl(filePath);
|
|
1678
|
+
const tags = [];
|
|
1679
|
+
|
|
1680
|
+
for (const msg of messages) {
|
|
1681
|
+
// Look for tag markers in tool_result content (user messages)
|
|
1682
|
+
const resultContent = getToolResultContent(msg);
|
|
1683
|
+
if (!resultContent) continue;
|
|
1684
|
+
|
|
1685
|
+
const match = resultContent.match(TAG_MARKER_PATTERN);
|
|
1686
|
+
if (match) {
|
|
1687
|
+
const tagType = match[1];
|
|
1688
|
+
const level = match[2];
|
|
1689
|
+
const reason = match[3];
|
|
1690
|
+
const uuid = match[4];
|
|
1691
|
+
const timestamp = match[5];
|
|
1692
|
+
|
|
1693
|
+
tags.push({
|
|
1694
|
+
type: tagType,
|
|
1695
|
+
level,
|
|
1696
|
+
reason,
|
|
1697
|
+
targetUuid: uuid,
|
|
1698
|
+
targetTimestamp: timestamp,
|
|
1699
|
+
tagTimestamp: msg.timestamp,
|
|
1700
|
+
session: path.basename(filePath)
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return tags;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function cmdTags(filterArg, opts) {
|
|
1709
|
+
const dir = cwdToProjectFolder();
|
|
1710
|
+
if (!dir) {
|
|
1711
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1716
|
+
if (jsonlFiles.length === 0) {
|
|
1717
|
+
console.error('No sessions found');
|
|
1718
|
+
process.exit(1);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Collect all tags from all sessions
|
|
1722
|
+
const allTags = [];
|
|
1723
|
+
for (const file of jsonlFiles) {
|
|
1724
|
+
const filePath = path.join(dir, file);
|
|
1725
|
+
const sessionTags = scanSessionTags(filePath);
|
|
1726
|
+
allTags.push(...sessionTags);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Apply filter if provided
|
|
1730
|
+
let filteredTags = allTags;
|
|
1731
|
+
const validTypes = ['toolcall', 'agentcall', 'discourse', 'checkpoint'];
|
|
1732
|
+
const validLevels = ['critical', 'important', 'bookmark'];
|
|
1733
|
+
|
|
1734
|
+
if (filterArg) {
|
|
1735
|
+
if (validTypes.includes(filterArg)) {
|
|
1736
|
+
filteredTags = allTags.filter(t => t.type === filterArg);
|
|
1737
|
+
} else if (validLevels.includes(filterArg)) {
|
|
1738
|
+
filteredTags = allTags.filter(t => t.level === filterArg);
|
|
1739
|
+
} else {
|
|
1740
|
+
console.error(`Invalid filter: ${filterArg}`);
|
|
1741
|
+
console.error(`Valid filters: ${[...validTypes, ...validLevels].join(', ')}`);
|
|
1742
|
+
process.exit(1);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (filteredTags.length === 0) {
|
|
1747
|
+
console.log('No tags found.');
|
|
1748
|
+
console.log('Create one with: session-recall tag <type> <level> "reason"');
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Sort by target timestamp
|
|
1753
|
+
filteredTags.sort((a, b) => new Date(a.targetTimestamp) - new Date(b.targetTimestamp));
|
|
1754
|
+
|
|
1755
|
+
console.log(`=== TAGS (${filteredTags.length} total) ===`);
|
|
1756
|
+
console.log(``);
|
|
1757
|
+
console.log(`TYPE LEVEL TIME REASON`);
|
|
1758
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
1759
|
+
|
|
1760
|
+
for (const tag of filteredTags) {
|
|
1761
|
+
const ts = tag.targetTimestamp ? new Date(tag.targetTimestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
1762
|
+
const typePad = tag.type.padEnd(10);
|
|
1763
|
+
const levelPad = tag.level.padEnd(10);
|
|
1764
|
+
const reason = tag.reason.substring(0, 40);
|
|
1765
|
+
console.log(`${typePad} ${levelPad} ${ts} ${reason}`);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
console.log(``);
|
|
1769
|
+
console.log(`Use: session-recall last 1 to see critical tags in context`);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1245
1772
|
// ========== ARGUMENT PARSING ==========
|
|
1246
1773
|
|
|
1247
1774
|
function parseArgs(args) {
|
|
@@ -1272,8 +1799,24 @@ function parseArgs(args) {
|
|
|
1272
1799
|
opts.checkpoint = args[++i];
|
|
1273
1800
|
} else if (arg === '--after') {
|
|
1274
1801
|
opts.after = args[++i];
|
|
1802
|
+
} else if (arg === '--stopped_at') {
|
|
1803
|
+
opts.stopped_at = args[++i];
|
|
1804
|
+
} else if (arg === '--next') {
|
|
1805
|
+
opts.next = args[++i];
|
|
1806
|
+
} else if (arg === '--blocker') {
|
|
1807
|
+
opts.blocker = args[++i];
|
|
1808
|
+
} else if (arg === '--decision') {
|
|
1809
|
+
opts.decision = args[++i];
|
|
1810
|
+
} else if (arg === '--gotcha') {
|
|
1811
|
+
opts.gotcha = args[++i];
|
|
1812
|
+
} else if (arg === '--back') {
|
|
1813
|
+
opts.back = parseInt(args[++i]);
|
|
1814
|
+
} else if (arg === '--show-important') {
|
|
1815
|
+
opts.showImportant = true;
|
|
1275
1816
|
} else if (arg === '-h' || arg === '--help') {
|
|
1276
1817
|
opts.help = true;
|
|
1818
|
+
} else if (arg === '-v' || arg === '--version') {
|
|
1819
|
+
opts.version = true;
|
|
1277
1820
|
} else if (!arg.startsWith('-')) {
|
|
1278
1821
|
positional.push(arg);
|
|
1279
1822
|
}
|
|
@@ -1287,11 +1830,103 @@ function parseArgs(args) {
|
|
|
1287
1830
|
const args = process.argv.slice(2);
|
|
1288
1831
|
const { opts, positional } = parseArgs(args);
|
|
1289
1832
|
|
|
1833
|
+
// Handle --version flag
|
|
1834
|
+
if (opts.version) {
|
|
1835
|
+
console.log(`session-recall v${VERSION}`);
|
|
1836
|
+
process.exit(0);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1290
1839
|
// Handle --checkpoint flag first (no-op marker that gets logged)
|
|
1291
1840
|
if (opts.checkpoint) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1841
|
+
// Output marker format - this IS the checkpoint storage (searched in tool_result)
|
|
1842
|
+
// Format: @@SESSION-CHECKPOINT@@ "<name>" uuid="<uuid>" stopped_at="<val>" next="<val>" blocker="<val>" decision="<val>" gotcha="<val>" ts=<timestamp>
|
|
1843
|
+
const isoTimestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1844
|
+
|
|
1845
|
+
// Detect current session UUID by finding most recently modified .jsonl
|
|
1846
|
+
let currentUuid = 'unknown';
|
|
1847
|
+
const claudeProjectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
1848
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
1849
|
+
let newestFile = null;
|
|
1850
|
+
let newestTime = 0;
|
|
1851
|
+
const scanDir = (dir) => {
|
|
1852
|
+
try {
|
|
1853
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1854
|
+
for (const entry of entries) {
|
|
1855
|
+
const fullPath = path.join(dir, entry.name);
|
|
1856
|
+
if (entry.isDirectory()) {
|
|
1857
|
+
scanDir(fullPath);
|
|
1858
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
1859
|
+
const stat = fs.statSync(fullPath);
|
|
1860
|
+
if (stat.mtimeMs > newestTime) {
|
|
1861
|
+
newestTime = stat.mtimeMs;
|
|
1862
|
+
newestFile = entry.name;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
} catch (e) { /* ignore permission errors */ }
|
|
1867
|
+
};
|
|
1868
|
+
scanDir(claudeProjectsDir);
|
|
1869
|
+
if (newestFile) {
|
|
1870
|
+
currentUuid = newestFile.replace('.jsonl', '');
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
let marker = `@@SESSION-CHECKPOINT@@ "${opts.checkpoint}" uuid="${currentUuid}"`;
|
|
1875
|
+
if (opts.stopped_at) marker += ` stopped_at="${opts.stopped_at}"`;
|
|
1876
|
+
if (opts.next) marker += ` next="${opts.next}"`;
|
|
1877
|
+
if (opts.blocker) marker += ` blocker="${opts.blocker}"`;
|
|
1878
|
+
if (opts.decision) marker += ` decision="${opts.decision}"`;
|
|
1879
|
+
if (opts.gotcha) marker += ` gotcha="${opts.gotcha}"`;
|
|
1880
|
+
marker += ` ts=${isoTimestamp}`;
|
|
1881
|
+
console.log(marker);
|
|
1882
|
+
process.exit(0);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Handle 'tag' command: session-recall tag <type> [--back N] <level> "reason"
|
|
1886
|
+
// Must come before --back handler since tag command can have --back option
|
|
1887
|
+
if (positional[0] === 'tag') {
|
|
1888
|
+
// Parse tag command arguments
|
|
1889
|
+
// Format: tag <type> [--back N] <level> "reason"
|
|
1890
|
+
// Note: --back is parsed by parseArgs into opts.back
|
|
1891
|
+
const tagArgs = positional.slice(1);
|
|
1892
|
+
let tagType = null;
|
|
1893
|
+
let level = null;
|
|
1894
|
+
let reason = null;
|
|
1895
|
+
let backN = opts.back || 1; // Use opts.back if available
|
|
1896
|
+
|
|
1897
|
+
for (let i = 0; i < tagArgs.length; i++) {
|
|
1898
|
+
const arg = tagArgs[i];
|
|
1899
|
+
if (!tagType) {
|
|
1900
|
+
tagType = arg;
|
|
1901
|
+
} else if (!level) {
|
|
1902
|
+
level = arg;
|
|
1903
|
+
} else if (!reason) {
|
|
1904
|
+
reason = arg;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
if (!tagType || !level) {
|
|
1909
|
+
console.error('Usage: session-recall tag <type> [--back N] <level> "reason"');
|
|
1910
|
+
console.error('Types: toolcall, agentcall, discourse, checkpoint');
|
|
1911
|
+
console.error('Levels: critical, important, bookmark');
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
cmdTag(tagType, level, reason, backN, opts);
|
|
1916
|
+
process.exit(0);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// Handle 'tags' command: session-recall tags [filter]
|
|
1920
|
+
if (positional[0] === 'tags') {
|
|
1921
|
+
const filterArg = positional[1] || null;
|
|
1922
|
+
cmdTags(filterArg, opts);
|
|
1923
|
+
process.exit(0);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// Handle --back N [type] preview command
|
|
1927
|
+
if (opts.back) {
|
|
1928
|
+
const filterType = positional[0] || null;
|
|
1929
|
+
cmdBack(opts.back, filterType, opts);
|
|
1295
1930
|
process.exit(0);
|
|
1296
1931
|
}
|
|
1297
1932
|
|