@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.
Files changed (3) hide show
  1. package/README.md +55 -0
  2. package/package.json +1 -1
  3. 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.5.2",
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
- for (const msg of msgs) {
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 ID NAME`);
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 shortId = cp.toolId ? cp.toolId.substring(5, 17) : '????????????';
740
- console.log(`${String(i + 1).padStart(2)} ${ts} ${shortId} ${cp.name}`);
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 last 1 --after "checkpoint name" to recall from checkpoint`);
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
- for (const msg of msgs) {
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
- // List all available checkpoints
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
- checkpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
1032
- checkpoints.forEach(cp => {
1033
- const ts = cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : '??:??:??';
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
- filterAfterTs = checkpoint.timestamp ? new Date(checkpoint.timestamp).getTime() : null;
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 msgs = readJsonl(filePath);
1052
- const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
1053
- const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
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 msgs = readJsonl(filePath);
1073
- const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
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 last timestamp descending
1084
- allSessions.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0));
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 = allSessions.length - inactiveSessions.length;
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
- // Output combined rinse (gotime mode)
1148
- let allMsgs = selected.flatMap(s => s.msgs);
1149
- // Sort by timestamp
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
- // Filter by checkpoint timestamp if --after was specified
1153
- if (filterAfterTs) {
1154
- allMsgs = allMsgs.filter(msg => {
1155
- const msgTs = msg.timestamp ? new Date(msg.timestamp).getTime() : 0;
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
- // Build tool result metadata (size, duration)
1161
- const resultMeta = buildToolResultMeta(allMsgs);
1214
+ // Sort by timestamp
1215
+ sessionMsgs.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
1162
1216
 
1163
- for (const msg of allMsgs) {
1164
- if (NOISE_TYPES.includes(msg.type)) continue;
1165
- if (msg.type === 'user') {
1166
- if (isToolResult(msg)) continue;
1167
- const text = getUserText(msg);
1168
- if (text) {
1169
- const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
1170
- console.log(`[${ts}] USER: ${text.substring(0, 500)}`);
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
- } else if (msg.type === 'assistant') {
1173
- const text = getAssistantText(msg);
1174
- const toolSummary = collapseToolCall(msg, resultMeta);
1175
- const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
1176
- if (text) {
1177
- console.log(`[${ts}] ASSISTANT: ${text.substring(0, 500)}`);
1178
- } else if (toolSummary) {
1179
- console.log(`[${ts}] ASSISTANT: ${toolSummary}`);
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(`To get a specific tool result: session-recall tools --show <id>`);
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 (use as standalone flag)
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" # Create checkpoint
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
- console.log(`✓ Checkpoint: ${opts.checkpoint}`);
1293
- console.log(` Time: ${new Date().toISOString()}`);
1294
- console.log(` Use 'session-recall checkpoints' to list all checkpoints`);
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