@aiplumber/session-recall 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -1
- package/package.json +1 -1
- package/session-recall +707 -98
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @aiplumber/session-recall
|
|
2
2
|
|
|
3
|
-
Pull context from previous Claude Code sessions. Sessions end, context resets - this tool lets you continue where you left off.
|
|
3
|
+
Pull context from previous **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** sessions. Sessions end, context resets - this tool lets you continue where you left off.
|
|
4
|
+
|
|
5
|
+
> **What is Claude Code?** Anthropic's official CLI for Claude - an agentic coding assistant that runs in your terminal. This tool parses Claude Code's conversation logs stored in `~/.claude/projects/`.
|
|
4
6
|
|
|
5
7
|
## The Problem
|
|
6
8
|
|
|
@@ -120,6 +122,40 @@ session-recall checkpoints
|
|
|
120
122
|
session-recall last 1 --after "finished research"
|
|
121
123
|
```
|
|
122
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
|
+
|
|
123
159
|
### rinse - Compress Session Data
|
|
124
160
|
|
|
125
161
|
```bash
|
|
@@ -188,6 +224,27 @@ session-recall tools --show 014opBVN
|
|
|
188
224
|
session-recall last 3 -f text > context.txt
|
|
189
225
|
```
|
|
190
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
|
+
|
|
191
248
|
## Output Modes
|
|
192
249
|
|
|
193
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.0",
|
|
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
|
@@ -333,6 +333,25 @@ function getToolResultText(msg) {
|
|
|
333
333
|
return typeof result.content === 'string' ? result.content : JSON.stringify(result.content);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
// Extract tool_result content from user message (for marker-based search)
|
|
337
|
+
function getToolResultContent(msg) {
|
|
338
|
+
if (msg.type !== 'user') return null;
|
|
339
|
+
|
|
340
|
+
// Check toolUseResult.stdout
|
|
341
|
+
if (msg.toolUseResult?.stdout) return msg.toolUseResult.stdout;
|
|
342
|
+
|
|
343
|
+
// Check message.content array
|
|
344
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
345
|
+
for (const block of msg.message.content) {
|
|
346
|
+
if (block.type === 'tool_result' && block.content) {
|
|
347
|
+
return typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
336
355
|
function cmdRinse(inputPath, opts) {
|
|
337
356
|
// Check if input is a directory or file
|
|
338
357
|
const stat = fs.statSync(inputPath);
|
|
@@ -679,6 +698,42 @@ function cmdTools(opts) {
|
|
|
679
698
|
console.log(`Use: session-recall tools --show ${exampleId} to see specific result`);
|
|
680
699
|
}
|
|
681
700
|
|
|
701
|
+
// Checkpoint marker pattern for discovery (search tool_result content, not command strings)
|
|
702
|
+
const CHECKPOINT_MARKER_PATTERN = /@@SESSION-CHECKPOINT@@\s+"([^"]+)"(?:\s+uuid="([^"]*)")?(?:\s+stopped_at="([^"]*)")?(?:\s+next="([^"]*)")?(?:\s+blocker="([^"]*)")?(?:\s+decision="([^"]*)")?(?:\s+gotcha="([^"]*)")?\s+ts=(\S+)/;
|
|
703
|
+
// Groups: [1]=name, [2]=uuid, [3]=stopped_at, [4]=next, [5]=blocker, [6]=decision, [7]=gotcha, [8]=timestamp
|
|
704
|
+
|
|
705
|
+
// Scan session messages for checkpoint markers in tool_result content
|
|
706
|
+
function scanSessionCheckpoints(messages, sessionFile) {
|
|
707
|
+
const checkpoints = [];
|
|
708
|
+
|
|
709
|
+
for (const msg of messages) {
|
|
710
|
+
// Look for checkpoint markers in tool_result content (user messages)
|
|
711
|
+
const resultContent = getToolResultContent(msg);
|
|
712
|
+
if (!resultContent) continue;
|
|
713
|
+
|
|
714
|
+
const match = resultContent.match(CHECKPOINT_MARKER_PATTERN);
|
|
715
|
+
if (match) {
|
|
716
|
+
const fields = {};
|
|
717
|
+
if (match[3]) fields.stopped_at = match[3];
|
|
718
|
+
if (match[4]) fields.next = match[4];
|
|
719
|
+
if (match[5]) fields.blocker = match[5];
|
|
720
|
+
if (match[6]) fields.decision = match[6];
|
|
721
|
+
if (match[7]) fields.gotcha = match[7];
|
|
722
|
+
|
|
723
|
+
checkpoints.push({
|
|
724
|
+
name: match[1],
|
|
725
|
+
uuid: match[2] || sessionFile.replace('.jsonl', ''), // Use embedded uuid or fallback to session file
|
|
726
|
+
fields,
|
|
727
|
+
timestamp: match[8], // Use timestamp from marker
|
|
728
|
+
msgTimestamp: msg.timestamp, // Also keep message timestamp
|
|
729
|
+
session: sessionFile
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return checkpoints;
|
|
735
|
+
}
|
|
736
|
+
|
|
682
737
|
function cmdCheckpoints(opts) {
|
|
683
738
|
const dir = cwdToProjectFolder();
|
|
684
739
|
if (!dir) {
|
|
@@ -698,26 +753,8 @@ function cmdCheckpoints(opts) {
|
|
|
698
753
|
for (const file of jsonlFiles) {
|
|
699
754
|
const filePath = path.join(dir, file);
|
|
700
755
|
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
|
-
}
|
|
756
|
+
const sessionCheckpoints = scanSessionCheckpoints(msgs, file);
|
|
757
|
+
checkpoints.push(...sessionCheckpoints);
|
|
721
758
|
}
|
|
722
759
|
|
|
723
760
|
if (checkpoints.length === 0) {
|
|
@@ -729,19 +766,51 @@ function cmdCheckpoints(opts) {
|
|
|
729
766
|
// Sort by timestamp
|
|
730
767
|
checkpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
731
768
|
|
|
769
|
+
// If --show, display specific checkpoint details
|
|
770
|
+
if (opts.show) {
|
|
771
|
+
const cp = checkpoints.find(c => c.name.toLowerCase().includes(opts.show.toLowerCase()));
|
|
772
|
+
if (!cp) {
|
|
773
|
+
console.error(`Checkpoint not found: "${opts.show}"`);
|
|
774
|
+
console.error(`\nAvailable checkpoints:`);
|
|
775
|
+
checkpoints.forEach(c => console.error(` "${c.name}"`));
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
console.log(`=== CHECKPOINT: ${cp.name} ===`);
|
|
780
|
+
console.log(`UUID: ${cp.uuid}`);
|
|
781
|
+
console.log(`Time: ${cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : 'unknown'}`);
|
|
782
|
+
console.log(``);
|
|
783
|
+
|
|
784
|
+
const fieldNames = ['stopped_at', 'next', 'blocker', 'decision', 'gotcha'];
|
|
785
|
+
let hasFields = false;
|
|
786
|
+
for (const field of fieldNames) {
|
|
787
|
+
if (cp.fields[field]) {
|
|
788
|
+
console.log(`${field}: ${cp.fields[field]}`);
|
|
789
|
+
hasFields = true;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (!hasFields) {
|
|
793
|
+
console.log(`(no structured fields)`);
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// List all checkpoints
|
|
732
799
|
console.log(`=== CHECKPOINTS (${checkpoints.length} total) ===`);
|
|
733
800
|
console.log(``);
|
|
734
|
-
console.log(`# TIME
|
|
801
|
+
console.log(`# TIME NAME FIELDS`);
|
|
735
802
|
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
736
803
|
|
|
737
804
|
checkpoints.forEach((cp, i) => {
|
|
738
805
|
const ts = cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
739
|
-
const
|
|
740
|
-
|
|
806
|
+
const name = cp.name.substring(0, 32).padEnd(32);
|
|
807
|
+
const fieldList = Object.keys(cp.fields).filter(k => cp.fields[k]).join(', ') || '-';
|
|
808
|
+
console.log(`${String(i + 1).padStart(2)} ${ts} ${name} ${fieldList}`);
|
|
741
809
|
});
|
|
742
810
|
|
|
743
811
|
console.log(``);
|
|
744
|
-
console.log(`Use: session-recall
|
|
812
|
+
console.log(`Use: session-recall checkpoints --show "name" for details`);
|
|
813
|
+
console.log(`Use: session-recall last 1 --after "name" to recall from checkpoint`);
|
|
745
814
|
}
|
|
746
815
|
|
|
747
816
|
function calcSessionCosts(messages) {
|
|
@@ -970,74 +1039,54 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
970
1039
|
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
971
1040
|
let checkpoint = null;
|
|
972
1041
|
|
|
1042
|
+
// Collect all checkpoints using marker-based search
|
|
1043
|
+
const allCheckpoints = [];
|
|
973
1044
|
for (const file of jsonlFiles) {
|
|
974
1045
|
const filePath = path.join(dir, file);
|
|
975
1046
|
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;
|
|
1047
|
+
const sessionCheckpoints = scanSessionCheckpoints(msgs, file);
|
|
1048
|
+
allCheckpoints.push(...sessionCheckpoints);
|
|
998
1049
|
}
|
|
999
1050
|
|
|
1051
|
+
// Find the matching checkpoint
|
|
1052
|
+
checkpoint = allCheckpoints.find(cp => cp.name === opts.after);
|
|
1053
|
+
|
|
1000
1054
|
if (!checkpoint) {
|
|
1001
1055
|
console.error(`Checkpoint not found: "${opts.after}"`);
|
|
1002
1056
|
console.error(`Available checkpoints:`);
|
|
1003
1057
|
|
|
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) {
|
|
1058
|
+
if (allCheckpoints.length === 0) {
|
|
1029
1059
|
console.error(' (none)');
|
|
1030
1060
|
} else {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const ts = cp.timestamp ?
|
|
1061
|
+
allCheckpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
1062
|
+
allCheckpoints.forEach(cp => {
|
|
1063
|
+
const ts = cp.timestamp ? cp.timestamp.substring(11, 19) : '??:??:??';
|
|
1034
1064
|
console.error(` "${cp.name}" (${ts})`);
|
|
1035
1065
|
});
|
|
1036
1066
|
}
|
|
1037
1067
|
process.exit(1);
|
|
1038
1068
|
}
|
|
1039
1069
|
|
|
1040
|
-
|
|
1070
|
+
// Use msgTimestamp for filtering (timestamp from jsonl message, not marker)
|
|
1071
|
+
filterAfterTs = checkpoint.msgTimestamp ? new Date(checkpoint.msgTimestamp).getTime() : new Date(checkpoint.timestamp).getTime();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Helper: get session start timestamp from first line of jsonl
|
|
1075
|
+
function getSessionStartTs(filePath) {
|
|
1076
|
+
try {
|
|
1077
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1078
|
+
const firstLine = content.split('\n')[0];
|
|
1079
|
+
if (!firstLine) return null;
|
|
1080
|
+
const obj = JSON.parse(firstLine);
|
|
1081
|
+
// Check snapshot.timestamp first (common in first line), then top-level timestamp
|
|
1082
|
+
let ts = obj.snapshot?.timestamp || obj.timestamp;
|
|
1083
|
+
if (ts) return new Date(ts);
|
|
1084
|
+
// Fallback: find first timestamp in file
|
|
1085
|
+
const match = content.match(/"timestamp":"([^"]+)"/);
|
|
1086
|
+
return match ? new Date(match[1]) : null;
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1041
1090
|
}
|
|
1042
1091
|
|
|
1043
1092
|
// Collect all sessions
|
|
@@ -1048,10 +1097,9 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1048
1097
|
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1049
1098
|
for (const file of jsonlFiles) {
|
|
1050
1099
|
const filePath = path.join(dir, file);
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
allSessions.push({ filePath, msgs, lastTs, project: path.basename(dir) });
|
|
1100
|
+
const startTs = getSessionStartTs(filePath);
|
|
1101
|
+
// Defer full read until needed
|
|
1102
|
+
allSessions.push({ filePath, startTs, project: path.basename(dir), _msgs: null });
|
|
1055
1103
|
}
|
|
1056
1104
|
} else {
|
|
1057
1105
|
// Scan all projects
|
|
@@ -1069,10 +1117,8 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1069
1117
|
for (const file of jsonlFiles) {
|
|
1070
1118
|
const filePath = path.join(projPath, file);
|
|
1071
1119
|
try {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
|
|
1075
|
-
allSessions.push({ filePath, msgs, lastTs, project: proj });
|
|
1120
|
+
const startTs = getSessionStartTs(filePath);
|
|
1121
|
+
allSessions.push({ filePath, startTs, project: proj, _msgs: null });
|
|
1076
1122
|
} catch (e) {
|
|
1077
1123
|
// Skip unreadable files
|
|
1078
1124
|
}
|
|
@@ -1080,12 +1126,22 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1080
1126
|
}
|
|
1081
1127
|
}
|
|
1082
1128
|
|
|
1083
|
-
// Sort by
|
|
1084
|
-
allSessions.sort((a, b) => (b.
|
|
1129
|
+
// Sort by session start timestamp descending (most recent first)
|
|
1130
|
+
allSessions.sort((a, b) => (b.startTs || 0) - (a.startTs || 0));
|
|
1131
|
+
|
|
1132
|
+
// Now load messages for top candidates and compute lastTs for active filter
|
|
1133
|
+
// We check more than n to account for filtering out active sessions
|
|
1134
|
+
const candidateCount = Math.min(allSessions.length, n + 5);
|
|
1135
|
+
for (let i = 0; i < candidateCount; i++) {
|
|
1136
|
+
const s = allSessions[i];
|
|
1137
|
+
s.msgs = readJsonl(s.filePath);
|
|
1138
|
+
const timestamps = s.msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
1139
|
+
s.lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : s.startTs;
|
|
1140
|
+
}
|
|
1085
1141
|
|
|
1086
1142
|
// Exclude currently active sessions (activity within threshold)
|
|
1087
1143
|
const now = Date.now();
|
|
1088
|
-
const inactiveSessions = allSessions.filter(s => {
|
|
1144
|
+
const inactiveSessions = allSessions.slice(0, candidateCount).filter(s => {
|
|
1089
1145
|
if (!s.lastTs) return true; // Include sessions with no timestamp
|
|
1090
1146
|
const ageMs = now - s.lastTs.getTime();
|
|
1091
1147
|
return ageMs > ACTIVE_SESSION_THRESHOLD_MS;
|
|
@@ -1099,8 +1155,8 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1099
1155
|
process.exit(1);
|
|
1100
1156
|
}
|
|
1101
1157
|
|
|
1102
|
-
// Count excluded active sessions for reporting
|
|
1103
|
-
const excludedActive =
|
|
1158
|
+
// Count excluded active sessions for reporting (only from candidates we checked)
|
|
1159
|
+
const excludedActive = candidateCount - inactiveSessions.length;
|
|
1104
1160
|
|
|
1105
1161
|
if (opts.dryRun) {
|
|
1106
1162
|
console.log(`--- DRY RUN: last ${n} ---`);
|
|
@@ -1160,23 +1216,68 @@ function cmdLast(arg1, arg2, opts) {
|
|
|
1160
1216
|
// Build tool result metadata (size, duration)
|
|
1161
1217
|
const resultMeta = buildToolResultMeta(allMsgs);
|
|
1162
1218
|
|
|
1219
|
+
// Build tag map for injection
|
|
1220
|
+
// Tag map: { uuid -> { type, level, reason } }
|
|
1221
|
+
const tagMap = {};
|
|
1222
|
+
for (const session of selected) {
|
|
1223
|
+
const sessionTags = scanSessionTags(session.filePath);
|
|
1224
|
+
for (const tag of sessionTags) {
|
|
1225
|
+
if (tag.targetUuid) {
|
|
1226
|
+
tagMap[tag.targetUuid] = {
|
|
1227
|
+
type: tag.type,
|
|
1228
|
+
level: tag.level,
|
|
1229
|
+
reason: tag.reason
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1163
1235
|
for (const msg of allMsgs) {
|
|
1164
1236
|
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
1237
|
+
|
|
1238
|
+
// Check if this message has a tag
|
|
1239
|
+
const toolId = getToolUseId(msg);
|
|
1240
|
+
const msgUuid = toolId || msg.uuid;
|
|
1241
|
+
const tag = msgUuid ? tagMap[msgUuid] : null;
|
|
1242
|
+
|
|
1243
|
+
// Determine if we should show this tag
|
|
1244
|
+
const showTag = tag && (
|
|
1245
|
+
tag.level === 'critical' ||
|
|
1246
|
+
(tag.level === 'important' && opts.showImportant)
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1165
1249
|
if (msg.type === 'user') {
|
|
1166
1250
|
if (isToolResult(msg)) continue;
|
|
1167
1251
|
const text = getUserText(msg);
|
|
1168
1252
|
if (text) {
|
|
1169
1253
|
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
1170
|
-
|
|
1254
|
+
if (showTag) {
|
|
1255
|
+
const marker = tag.level === 'critical' ? '\u26A0' : '\u2139';
|
|
1256
|
+
console.log(`[${ts}] ${marker} ${tag.level.toUpperCase()} [${tag.type}] USER: ${text.substring(0, 500)}`);
|
|
1257
|
+
console.log(` Reason: "${tag.reason}"`);
|
|
1258
|
+
} else {
|
|
1259
|
+
console.log(`[${ts}] USER: ${text.substring(0, 500)}`);
|
|
1260
|
+
}
|
|
1171
1261
|
}
|
|
1172
1262
|
} else if (msg.type === 'assistant') {
|
|
1173
1263
|
const text = getAssistantText(msg);
|
|
1174
1264
|
const toolSummary = collapseToolCall(msg, resultMeta);
|
|
1175
1265
|
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1266
|
+
|
|
1267
|
+
if (showTag) {
|
|
1268
|
+
const marker = tag.level === 'critical' ? '\u26A0' : '\u2139';
|
|
1269
|
+
if (text) {
|
|
1270
|
+
console.log(`[${ts}] ${marker} ${tag.level.toUpperCase()} [${tag.type}] ASSISTANT: ${text.substring(0, 500)}`);
|
|
1271
|
+
} else if (toolSummary) {
|
|
1272
|
+
console.log(`[${ts}] ${marker} ${tag.level.toUpperCase()} [${tag.type}] ${toolSummary}`);
|
|
1273
|
+
}
|
|
1274
|
+
console.log(` Reason: "${tag.reason}"`);
|
|
1275
|
+
} else {
|
|
1276
|
+
if (text) {
|
|
1277
|
+
console.log(`[${ts}] ASSISTANT: ${text.substring(0, 500)}`);
|
|
1278
|
+
} else if (toolSummary) {
|
|
1279
|
+
console.log(`[${ts}] ASSISTANT: ${toolSummary}`);
|
|
1280
|
+
}
|
|
1180
1281
|
}
|
|
1181
1282
|
}
|
|
1182
1283
|
}
|
|
@@ -1219,16 +1320,35 @@ COMMANDS:
|
|
|
1219
1320
|
last [N] [folder] Get last N sessions (default: 1, CWD project only)
|
|
1220
1321
|
-a, --all Scan all projects (not just CWD)
|
|
1221
1322
|
-d, --dry-run Show what would be pulled without output
|
|
1323
|
+
--show-important Include important-level tags in output
|
|
1222
1324
|
|
|
1223
1325
|
tools List tool calls in most recent session (CWD project)
|
|
1224
1326
|
--show <id> Show specific tool result by ID (partial match)
|
|
1225
1327
|
|
|
1226
1328
|
checkpoints List all checkpoints in current project
|
|
1329
|
+
--show "name" Show checkpoint details (partial match)
|
|
1227
1330
|
|
|
1228
|
-
--checkpoint "name" Create a checkpoint marker
|
|
1331
|
+
--checkpoint "name" Create a checkpoint marker
|
|
1332
|
+
--stopped_at "point" Where work stopped (file:line, step N of M)
|
|
1333
|
+
--next "action" Single next action to take
|
|
1334
|
+
--blocker "issue" What's blocking progress (optional)
|
|
1335
|
+
--decision "choice" Decision made + reasoning (optional)
|
|
1336
|
+
--gotcha "trap" Gotcha for next session (optional)
|
|
1229
1337
|
|
|
1230
1338
|
last [N] --after "name" Recall from after a checkpoint
|
|
1231
1339
|
|
|
1340
|
+
TAGGING:
|
|
1341
|
+
--back N [type] Preview message N positions back (for tagging)
|
|
1342
|
+
Types: toolcall, agentcall, discourse, checkpoint
|
|
1343
|
+
|
|
1344
|
+
tag <type> [--back N] <level> "reason"
|
|
1345
|
+
Tag a message for recall surfacing
|
|
1346
|
+
Types: toolcall, agentcall, discourse, checkpoint
|
|
1347
|
+
Levels: critical (auto-show), important (opt-in), bookmark (lookup)
|
|
1348
|
+
|
|
1349
|
+
tags [filter] List all tags in current project
|
|
1350
|
+
Filter by type or level
|
|
1351
|
+
|
|
1232
1352
|
EXAMPLES:
|
|
1233
1353
|
session-recall last 1 -d # Check what's available
|
|
1234
1354
|
session-recall last 1 -f text # Pull last session
|
|
@@ -1236,12 +1356,401 @@ EXAMPLES:
|
|
|
1236
1356
|
session-recall tools --show 014opBVN # Get specific tool result
|
|
1237
1357
|
|
|
1238
1358
|
# Checkpoints - mark progress, recall from any point
|
|
1239
|
-
session-recall --checkpoint "finished research" #
|
|
1359
|
+
session-recall --checkpoint "finished research" # Simple checkpoint
|
|
1360
|
+
session-recall --checkpoint "hit blocker" --stopped_at "auth.ts:42" --blocker "API 403"
|
|
1240
1361
|
session-recall checkpoints # List all checkpoints
|
|
1362
|
+
session-recall checkpoints --show "blocker" # Show checkpoint details
|
|
1241
1363
|
session-recall last 1 --after "finished research" # Recall from checkpoint
|
|
1364
|
+
|
|
1365
|
+
# Tagging - mark important moments
|
|
1366
|
+
session-recall --back 3 toolcall # Preview 3rd previous tool call
|
|
1367
|
+
session-recall tag toolcall critical "found the bug" # Tag most recent
|
|
1368
|
+
session-recall tag toolcall --back 3 important "useful data" # Tag 3 back
|
|
1369
|
+
session-recall tags # List all tags
|
|
1370
|
+
session-recall tags critical # List only critical tags
|
|
1371
|
+
session-recall last 1 # Critical tags auto-surface
|
|
1372
|
+
session-recall last 1 --show-important # Include important tags
|
|
1242
1373
|
`);
|
|
1243
1374
|
}
|
|
1244
1375
|
|
|
1376
|
+
// ========== MESSAGE TYPE CLASSIFICATION ==========
|
|
1377
|
+
|
|
1378
|
+
// Classify a message by its tagging type: toolcall, agentcall, discourse, checkpoint
|
|
1379
|
+
function classifyMessageType(msg, checkpointToolIds = new Set()) {
|
|
1380
|
+
if (!msg) return null;
|
|
1381
|
+
|
|
1382
|
+
// Check for checkpoint (Bash tool calls with session-recall --checkpoint)
|
|
1383
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1384
|
+
for (const block of msg.message.content) {
|
|
1385
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
1386
|
+
const cmd = block.input?.command || '';
|
|
1387
|
+
if (cmd.match(/session-recall\s+--checkpoint/)) {
|
|
1388
|
+
return 'checkpoint';
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Check for tool_use in assistant messages
|
|
1395
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1396
|
+
const toolUses = msg.message.content.filter(c => c.type === 'tool_use');
|
|
1397
|
+
if (toolUses.length > 0) {
|
|
1398
|
+
// Check if any are Task tools (agentcall)
|
|
1399
|
+
const hasTask = toolUses.some(t => t.name === 'Task');
|
|
1400
|
+
if (hasTask) return 'agentcall';
|
|
1401
|
+
return 'toolcall';
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Check for discourse (user messages without tool_result, or assistant with text)
|
|
1406
|
+
if (msg.type === 'user') {
|
|
1407
|
+
if (!isToolResult(msg)) {
|
|
1408
|
+
const text = getUserText(msg);
|
|
1409
|
+
if (text && text.length > 0) return 'discourse';
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if (msg.type === 'assistant') {
|
|
1415
|
+
const text = getAssistantText(msg);
|
|
1416
|
+
if (text && text.length > 0) return 'discourse';
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Get message preview for --back display
|
|
1423
|
+
function getMessagePreview(msg, msgType) {
|
|
1424
|
+
if (msgType === 'toolcall' || msgType === 'agentcall') {
|
|
1425
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1426
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1427
|
+
if (toolUse) {
|
|
1428
|
+
const name = toolUse.name;
|
|
1429
|
+
const input = toolUse.input || {};
|
|
1430
|
+
if (name === 'Task') {
|
|
1431
|
+
return `Task: ${input.description?.substring(0, 60) || 'subagent task'}`;
|
|
1432
|
+
} else if (name === 'Bash') {
|
|
1433
|
+
return `Bash: ${input.command?.substring(0, 60) || 'command'}`;
|
|
1434
|
+
} else if (name === 'Read') {
|
|
1435
|
+
return `Read: ${input.file_path}`;
|
|
1436
|
+
} else if (name === 'Edit' || name === 'Write') {
|
|
1437
|
+
return `${name}: ${input.file_path}`;
|
|
1438
|
+
} else if (name === 'Grep') {
|
|
1439
|
+
return `Grep: ${input.pattern?.substring(0, 40)}`;
|
|
1440
|
+
} else if (name === 'Glob') {
|
|
1441
|
+
return `Glob: ${input.pattern}`;
|
|
1442
|
+
} else {
|
|
1443
|
+
return `${name}`;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (msgType === 'checkpoint') {
|
|
1450
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1451
|
+
for (const block of msg.message.content) {
|
|
1452
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
1453
|
+
const cmd = block.input?.command || '';
|
|
1454
|
+
const match = cmd.match(/session-recall\s+--checkpoint\s+["']([^"']+)["']/);
|
|
1455
|
+
if (match) return `Checkpoint: "${match[1]}"`;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (msgType === 'discourse') {
|
|
1462
|
+
if (msg.type === 'user') {
|
|
1463
|
+
const text = getUserText(msg);
|
|
1464
|
+
return `User: ${text?.substring(0, 60) || '(empty)'}`;
|
|
1465
|
+
} else {
|
|
1466
|
+
const text = getAssistantText(msg);
|
|
1467
|
+
return `Assistant: ${text?.substring(0, 60) || '(empty)'}`;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return '(unknown)';
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Get the tool_use ID from a message (for toolcall/agentcall)
|
|
1475
|
+
function getToolUseId(msg) {
|
|
1476
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1477
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1478
|
+
if (toolUse) return toolUse.id;
|
|
1479
|
+
}
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// ========== --back PREVIEW COMMAND ==========
|
|
1484
|
+
|
|
1485
|
+
function cmdBack(backN, filterType, opts) {
|
|
1486
|
+
const dir = cwdToProjectFolder();
|
|
1487
|
+
if (!dir) {
|
|
1488
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Get most recent session (INCLUDING active)
|
|
1493
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1494
|
+
if (jsonlFiles.length === 0) {
|
|
1495
|
+
console.error('No sessions found');
|
|
1496
|
+
process.exit(1);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Sort by modification time to get most recent
|
|
1500
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
1501
|
+
const filePath = path.join(dir, f);
|
|
1502
|
+
const stat = fs.statSync(filePath);
|
|
1503
|
+
return { filePath, mtime: stat.mtime };
|
|
1504
|
+
});
|
|
1505
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
1506
|
+
|
|
1507
|
+
// Use the most recent session (even if active)
|
|
1508
|
+
const session = filesWithStats[0];
|
|
1509
|
+
const messages = readJsonl(session.filePath);
|
|
1510
|
+
|
|
1511
|
+
// Filter messages by type
|
|
1512
|
+
const validTypes = ['toolcall', 'agentcall', 'discourse', 'checkpoint'];
|
|
1513
|
+
if (filterType && !validTypes.includes(filterType)) {
|
|
1514
|
+
console.error(`Invalid type: ${filterType}`);
|
|
1515
|
+
console.error(`Valid types: ${validTypes.join(', ')}`);
|
|
1516
|
+
process.exit(1);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Classify all messages and filter
|
|
1520
|
+
const classified = [];
|
|
1521
|
+
for (const msg of messages) {
|
|
1522
|
+
const msgType = classifyMessageType(msg);
|
|
1523
|
+
if (msgType && (!filterType || msgType === filterType)) {
|
|
1524
|
+
// Skip session-recall tag commands (they're toolcalls but shouldn't be counted)
|
|
1525
|
+
if (msgType === 'toolcall' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1526
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1527
|
+
if (toolUse && toolUse.name === 'Bash') {
|
|
1528
|
+
const cmd = toolUse.input?.command || '';
|
|
1529
|
+
if (cmd.match(/session-recall\s+tag\s+/)) continue;
|
|
1530
|
+
if (cmd.match(/session-recall\s+--back\s+/)) continue;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
classified.push({ msg, msgType });
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (classified.length === 0) {
|
|
1538
|
+
console.error(`No ${filterType || 'messages'} found in session`);
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Count back N from end
|
|
1543
|
+
const targetIndex = classified.length - backN;
|
|
1544
|
+
if (targetIndex < 0) {
|
|
1545
|
+
console.error(`Only ${classified.length} ${filterType || 'messages'} found, cannot go back ${backN}`);
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const target = classified[targetIndex];
|
|
1550
|
+
const toolId = getToolUseId(target.msg);
|
|
1551
|
+
const uuid = toolId || target.msg.uuid || '(no uuid)';
|
|
1552
|
+
const ts = target.msg.timestamp ? new Date(target.msg.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
1553
|
+
const preview = getMessagePreview(target.msg, target.msgType);
|
|
1554
|
+
|
|
1555
|
+
console.log(`UUID: ${uuid}`);
|
|
1556
|
+
console.log(`Type: ${target.msgType}`);
|
|
1557
|
+
console.log(`Time: ${ts}`);
|
|
1558
|
+
console.log(`Content: ${preview}`);
|
|
1559
|
+
console.log(``);
|
|
1560
|
+
console.log(`To tag this: session-recall tag ${target.msgType}${backN > 1 ? ' --back ' + backN : ''} <level> "reason"`);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// ========== TAG COMMAND ==========
|
|
1564
|
+
|
|
1565
|
+
function cmdTag(tagType, level, reason, backN, opts) {
|
|
1566
|
+
const validTypes = ['toolcall', 'agentcall', 'discourse', 'checkpoint'];
|
|
1567
|
+
const validLevels = ['critical', 'important', 'bookmark'];
|
|
1568
|
+
|
|
1569
|
+
if (!validTypes.includes(tagType)) {
|
|
1570
|
+
console.error(`Invalid type: ${tagType}`);
|
|
1571
|
+
console.error(`Valid types: ${validTypes.join(', ')}`);
|
|
1572
|
+
process.exit(1);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (!validLevels.includes(level)) {
|
|
1576
|
+
console.error(`Invalid level: ${level}`);
|
|
1577
|
+
console.error(`Valid levels: ${validLevels.join(', ')}`);
|
|
1578
|
+
process.exit(1);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if (level === 'critical' && !reason) {
|
|
1582
|
+
console.error(`Critical tags require a reason`);
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const dir = cwdToProjectFolder();
|
|
1587
|
+
if (!dir) {
|
|
1588
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
1589
|
+
process.exit(1);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Get most recent session (INCLUDING active - this is the current session)
|
|
1593
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1594
|
+
if (jsonlFiles.length === 0) {
|
|
1595
|
+
console.error('No sessions found');
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Sort by modification time to get most recent
|
|
1600
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
1601
|
+
const filePath = path.join(dir, f);
|
|
1602
|
+
const stat = fs.statSync(filePath);
|
|
1603
|
+
return { filePath, mtime: stat.mtime };
|
|
1604
|
+
});
|
|
1605
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
1606
|
+
|
|
1607
|
+
// Use the most recent session
|
|
1608
|
+
const session = filesWithStats[0];
|
|
1609
|
+
const messages = readJsonl(session.filePath);
|
|
1610
|
+
|
|
1611
|
+
// Classify all messages and filter by type
|
|
1612
|
+
const classified = [];
|
|
1613
|
+
for (const msg of messages) {
|
|
1614
|
+
const msgType = classifyMessageType(msg);
|
|
1615
|
+
if (msgType === tagType) {
|
|
1616
|
+
// Skip session-recall tag/back commands
|
|
1617
|
+
if (msgType === 'toolcall' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1618
|
+
const toolUse = msg.message.content.find(c => c.type === 'tool_use');
|
|
1619
|
+
if (toolUse && toolUse.name === 'Bash') {
|
|
1620
|
+
const cmd = toolUse.input?.command || '';
|
|
1621
|
+
if (cmd.match(/session-recall\s+tag\s+/)) continue;
|
|
1622
|
+
if (cmd.match(/session-recall\s+--back\s+/)) continue;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
classified.push({ msg, msgType });
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (classified.length === 0) {
|
|
1630
|
+
console.error(`No ${tagType} found in session`);
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Count back N from end
|
|
1635
|
+
const targetIndex = classified.length - backN;
|
|
1636
|
+
if (targetIndex < 0) {
|
|
1637
|
+
console.error(`Only ${classified.length} ${tagType} found, cannot go back ${backN}`);
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const target = classified[targetIndex];
|
|
1642
|
+
const toolId = getToolUseId(target.msg);
|
|
1643
|
+
const uuid = toolId || target.msg.uuid || '(no uuid)';
|
|
1644
|
+
const isoTimestamp = target.msg.timestamp ? new Date(target.msg.timestamp).toISOString().replace(/\.\d{3}Z$/, 'Z') : new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1645
|
+
|
|
1646
|
+
// Output marker format - this IS the tag storage (searched in tool_result)
|
|
1647
|
+
// Format: @@SESSION-TAG@@ <type> <level> "<reason>" uuid=<uuid> ts=<timestamp>
|
|
1648
|
+
console.log(`@@SESSION-TAG@@ ${tagType} ${level} "${reason || ''}" uuid=${uuid} ts=${isoTimestamp}`);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// ========== TAGS LISTING COMMAND ==========
|
|
1652
|
+
|
|
1653
|
+
// Marker regex patterns for discovery (search tool_result content, not command strings)
|
|
1654
|
+
const TAG_MARKER_PATTERN = /@@SESSION-TAG@@\s+(\S+)\s+(\S+)\s+"([^"]*)"\s+uuid=(\S+)\s+ts=(\S+)/;
|
|
1655
|
+
// Groups: [1]=type, [2]=level, [3]=reason, [4]=uuid, [5]=timestamp
|
|
1656
|
+
|
|
1657
|
+
// Scan a session for tags by searching tool_result content for @@SESSION-TAG@@ markers
|
|
1658
|
+
function scanSessionTags(filePath) {
|
|
1659
|
+
const messages = readJsonl(filePath);
|
|
1660
|
+
const tags = [];
|
|
1661
|
+
|
|
1662
|
+
for (const msg of messages) {
|
|
1663
|
+
// Look for tag markers in tool_result content (user messages)
|
|
1664
|
+
const resultContent = getToolResultContent(msg);
|
|
1665
|
+
if (!resultContent) continue;
|
|
1666
|
+
|
|
1667
|
+
const match = resultContent.match(TAG_MARKER_PATTERN);
|
|
1668
|
+
if (match) {
|
|
1669
|
+
const tagType = match[1];
|
|
1670
|
+
const level = match[2];
|
|
1671
|
+
const reason = match[3];
|
|
1672
|
+
const uuid = match[4];
|
|
1673
|
+
const timestamp = match[5];
|
|
1674
|
+
|
|
1675
|
+
tags.push({
|
|
1676
|
+
type: tagType,
|
|
1677
|
+
level,
|
|
1678
|
+
reason,
|
|
1679
|
+
targetUuid: uuid,
|
|
1680
|
+
targetTimestamp: timestamp,
|
|
1681
|
+
tagTimestamp: msg.timestamp,
|
|
1682
|
+
session: path.basename(filePath)
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return tags;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function cmdTags(filterArg, opts) {
|
|
1691
|
+
const dir = cwdToProjectFolder();
|
|
1692
|
+
if (!dir) {
|
|
1693
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
1694
|
+
process.exit(1);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1698
|
+
if (jsonlFiles.length === 0) {
|
|
1699
|
+
console.error('No sessions found');
|
|
1700
|
+
process.exit(1);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Collect all tags from all sessions
|
|
1704
|
+
const allTags = [];
|
|
1705
|
+
for (const file of jsonlFiles) {
|
|
1706
|
+
const filePath = path.join(dir, file);
|
|
1707
|
+
const sessionTags = scanSessionTags(filePath);
|
|
1708
|
+
allTags.push(...sessionTags);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Apply filter if provided
|
|
1712
|
+
let filteredTags = allTags;
|
|
1713
|
+
const validTypes = ['toolcall', 'agentcall', 'discourse', 'checkpoint'];
|
|
1714
|
+
const validLevels = ['critical', 'important', 'bookmark'];
|
|
1715
|
+
|
|
1716
|
+
if (filterArg) {
|
|
1717
|
+
if (validTypes.includes(filterArg)) {
|
|
1718
|
+
filteredTags = allTags.filter(t => t.type === filterArg);
|
|
1719
|
+
} else if (validLevels.includes(filterArg)) {
|
|
1720
|
+
filteredTags = allTags.filter(t => t.level === filterArg);
|
|
1721
|
+
} else {
|
|
1722
|
+
console.error(`Invalid filter: ${filterArg}`);
|
|
1723
|
+
console.error(`Valid filters: ${[...validTypes, ...validLevels].join(', ')}`);
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (filteredTags.length === 0) {
|
|
1729
|
+
console.log('No tags found.');
|
|
1730
|
+
console.log('Create one with: session-recall tag <type> <level> "reason"');
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Sort by target timestamp
|
|
1735
|
+
filteredTags.sort((a, b) => new Date(a.targetTimestamp) - new Date(b.targetTimestamp));
|
|
1736
|
+
|
|
1737
|
+
console.log(`=== TAGS (${filteredTags.length} total) ===`);
|
|
1738
|
+
console.log(``);
|
|
1739
|
+
console.log(`TYPE LEVEL TIME REASON`);
|
|
1740
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
1741
|
+
|
|
1742
|
+
for (const tag of filteredTags) {
|
|
1743
|
+
const ts = tag.targetTimestamp ? new Date(tag.targetTimestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
1744
|
+
const typePad = tag.type.padEnd(10);
|
|
1745
|
+
const levelPad = tag.level.padEnd(10);
|
|
1746
|
+
const reason = tag.reason.substring(0, 40);
|
|
1747
|
+
console.log(`${typePad} ${levelPad} ${ts} ${reason}`);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
console.log(``);
|
|
1751
|
+
console.log(`Use: session-recall last 1 to see critical tags in context`);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1245
1754
|
// ========== ARGUMENT PARSING ==========
|
|
1246
1755
|
|
|
1247
1756
|
function parseArgs(args) {
|
|
@@ -1272,6 +1781,20 @@ function parseArgs(args) {
|
|
|
1272
1781
|
opts.checkpoint = args[++i];
|
|
1273
1782
|
} else if (arg === '--after') {
|
|
1274
1783
|
opts.after = args[++i];
|
|
1784
|
+
} else if (arg === '--stopped_at') {
|
|
1785
|
+
opts.stopped_at = args[++i];
|
|
1786
|
+
} else if (arg === '--next') {
|
|
1787
|
+
opts.next = args[++i];
|
|
1788
|
+
} else if (arg === '--blocker') {
|
|
1789
|
+
opts.blocker = args[++i];
|
|
1790
|
+
} else if (arg === '--decision') {
|
|
1791
|
+
opts.decision = args[++i];
|
|
1792
|
+
} else if (arg === '--gotcha') {
|
|
1793
|
+
opts.gotcha = args[++i];
|
|
1794
|
+
} else if (arg === '--back') {
|
|
1795
|
+
opts.back = parseInt(args[++i]);
|
|
1796
|
+
} else if (arg === '--show-important') {
|
|
1797
|
+
opts.showImportant = true;
|
|
1275
1798
|
} else if (arg === '-h' || arg === '--help') {
|
|
1276
1799
|
opts.help = true;
|
|
1277
1800
|
} else if (!arg.startsWith('-')) {
|
|
@@ -1289,9 +1812,95 @@ const { opts, positional } = parseArgs(args);
|
|
|
1289
1812
|
|
|
1290
1813
|
// Handle --checkpoint flag first (no-op marker that gets logged)
|
|
1291
1814
|
if (opts.checkpoint) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1815
|
+
// Output marker format - this IS the checkpoint storage (searched in tool_result)
|
|
1816
|
+
// Format: @@SESSION-CHECKPOINT@@ "<name>" uuid="<uuid>" stopped_at="<val>" next="<val>" blocker="<val>" decision="<val>" gotcha="<val>" ts=<timestamp>
|
|
1817
|
+
const isoTimestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1818
|
+
|
|
1819
|
+
// Detect current session UUID by finding most recently modified .jsonl
|
|
1820
|
+
let currentUuid = 'unknown';
|
|
1821
|
+
const claudeProjectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
1822
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
1823
|
+
let newestFile = null;
|
|
1824
|
+
let newestTime = 0;
|
|
1825
|
+
const scanDir = (dir) => {
|
|
1826
|
+
try {
|
|
1827
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1828
|
+
for (const entry of entries) {
|
|
1829
|
+
const fullPath = path.join(dir, entry.name);
|
|
1830
|
+
if (entry.isDirectory()) {
|
|
1831
|
+
scanDir(fullPath);
|
|
1832
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
1833
|
+
const stat = fs.statSync(fullPath);
|
|
1834
|
+
if (stat.mtimeMs > newestTime) {
|
|
1835
|
+
newestTime = stat.mtimeMs;
|
|
1836
|
+
newestFile = entry.name;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
} catch (e) { /* ignore permission errors */ }
|
|
1841
|
+
};
|
|
1842
|
+
scanDir(claudeProjectsDir);
|
|
1843
|
+
if (newestFile) {
|
|
1844
|
+
currentUuid = newestFile.replace('.jsonl', '');
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
let marker = `@@SESSION-CHECKPOINT@@ "${opts.checkpoint}" uuid="${currentUuid}"`;
|
|
1849
|
+
if (opts.stopped_at) marker += ` stopped_at="${opts.stopped_at}"`;
|
|
1850
|
+
if (opts.next) marker += ` next="${opts.next}"`;
|
|
1851
|
+
if (opts.blocker) marker += ` blocker="${opts.blocker}"`;
|
|
1852
|
+
if (opts.decision) marker += ` decision="${opts.decision}"`;
|
|
1853
|
+
if (opts.gotcha) marker += ` gotcha="${opts.gotcha}"`;
|
|
1854
|
+
marker += ` ts=${isoTimestamp}`;
|
|
1855
|
+
console.log(marker);
|
|
1856
|
+
process.exit(0);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// Handle 'tag' command: session-recall tag <type> [--back N] <level> "reason"
|
|
1860
|
+
// Must come before --back handler since tag command can have --back option
|
|
1861
|
+
if (positional[0] === 'tag') {
|
|
1862
|
+
// Parse tag command arguments
|
|
1863
|
+
// Format: tag <type> [--back N] <level> "reason"
|
|
1864
|
+
// Note: --back is parsed by parseArgs into opts.back
|
|
1865
|
+
const tagArgs = positional.slice(1);
|
|
1866
|
+
let tagType = null;
|
|
1867
|
+
let level = null;
|
|
1868
|
+
let reason = null;
|
|
1869
|
+
let backN = opts.back || 1; // Use opts.back if available
|
|
1870
|
+
|
|
1871
|
+
for (let i = 0; i < tagArgs.length; i++) {
|
|
1872
|
+
const arg = tagArgs[i];
|
|
1873
|
+
if (!tagType) {
|
|
1874
|
+
tagType = arg;
|
|
1875
|
+
} else if (!level) {
|
|
1876
|
+
level = arg;
|
|
1877
|
+
} else if (!reason) {
|
|
1878
|
+
reason = arg;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (!tagType || !level) {
|
|
1883
|
+
console.error('Usage: session-recall tag <type> [--back N] <level> "reason"');
|
|
1884
|
+
console.error('Types: toolcall, agentcall, discourse, checkpoint');
|
|
1885
|
+
console.error('Levels: critical, important, bookmark');
|
|
1886
|
+
process.exit(1);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
cmdTag(tagType, level, reason, backN, opts);
|
|
1890
|
+
process.exit(0);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Handle 'tags' command: session-recall tags [filter]
|
|
1894
|
+
if (positional[0] === 'tags') {
|
|
1895
|
+
const filterArg = positional[1] || null;
|
|
1896
|
+
cmdTags(filterArg, opts);
|
|
1897
|
+
process.exit(0);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Handle --back N [type] preview command
|
|
1901
|
+
if (opts.back) {
|
|
1902
|
+
const filterType = positional[0] || null;
|
|
1903
|
+
cmdBack(opts.back, filterType, opts);
|
|
1295
1904
|
process.exit(0);
|
|
1296
1905
|
}
|
|
1297
1906
|
|