@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.
Files changed (3) hide show
  1. package/README.md +58 -1
  2. package/package.json +1 -1
  3. 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.5.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
- 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
- }
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 ID NAME`);
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 shortId = cp.toolId ? cp.toolId.substring(5, 17) : '????????????';
740
- console.log(`${String(i + 1).padStart(2)} ${ts} ${shortId} ${cp.name}`);
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 last 1 --after "checkpoint name" to recall from checkpoint`);
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
- 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;
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
- // 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) {
1058
+ if (allCheckpoints.length === 0) {
1029
1059
  console.error(' (none)');
1030
1060
  } 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) : '??:??:??';
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
- filterAfterTs = checkpoint.timestamp ? new Date(checkpoint.timestamp).getTime() : null;
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 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) });
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 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 });
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 last timestamp descending
1084
- allSessions.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0));
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 = allSessions.length - inactiveSessions.length;
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
- console.log(`[${ts}] USER: ${text.substring(0, 500)}`);
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
- if (text) {
1177
- console.log(`[${ts}] ASSISTANT: ${text.substring(0, 500)}`);
1178
- } else if (toolSummary) {
1179
- console.log(`[${ts}] ASSISTANT: ${toolSummary}`);
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 (use as standalone flag)
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" # Create checkpoint
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
- console.log(`✓ Checkpoint: ${opts.checkpoint}`);
1293
- console.log(` Time: ${new Date().toISOString()}`);
1294
- console.log(` Use 'session-recall checkpoints' to list all checkpoints`);
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