code-session-memory 0.4.3 → 0.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 (38) hide show
  1. package/README.md +75 -58
  2. package/dist/mcp/index.js +23 -2
  3. package/dist/mcp/index.js.map +1 -1
  4. package/dist/mcp/server.d.ts +4 -2
  5. package/dist/mcp/server.d.ts.map +1 -1
  6. package/dist/mcp/server.js +11 -3
  7. package/dist/mcp/server.js.map +1 -1
  8. package/dist/src/cli-sessions.d.ts +6 -7
  9. package/dist/src/cli-sessions.d.ts.map +1 -1
  10. package/dist/src/cli-sessions.js +238 -178
  11. package/dist/src/cli-sessions.js.map +1 -1
  12. package/dist/src/cli.js +272 -12
  13. package/dist/src/cli.js.map +1 -1
  14. package/dist/src/cursor-to-messages.d.ts +64 -0
  15. package/dist/src/cursor-to-messages.d.ts.map +1 -0
  16. package/dist/src/cursor-to-messages.js +243 -0
  17. package/dist/src/cursor-to-messages.js.map +1 -0
  18. package/dist/src/cursor-transcript-to-messages.d.ts +22 -0
  19. package/dist/src/cursor-transcript-to-messages.d.ts.map +1 -0
  20. package/dist/src/cursor-transcript-to-messages.js +79 -0
  21. package/dist/src/cursor-transcript-to-messages.js.map +1 -0
  22. package/dist/src/database.d.ts +13 -2
  23. package/dist/src/database.d.ts.map +1 -1
  24. package/dist/src/database.js +42 -8
  25. package/dist/src/database.js.map +1 -1
  26. package/dist/src/indexer-cli-claude.js +25 -3
  27. package/dist/src/indexer-cli-claude.js.map +1 -1
  28. package/dist/src/indexer-cli-cursor.d.ts +25 -0
  29. package/dist/src/indexer-cli-cursor.d.ts.map +1 -0
  30. package/dist/src/indexer-cli-cursor.js +118 -0
  31. package/dist/src/indexer-cli-cursor.js.map +1 -0
  32. package/dist/src/indexer.d.ts.map +1 -1
  33. package/dist/src/indexer.js +46 -9
  34. package/dist/src/indexer.js.map +1 -1
  35. package/dist/src/types.d.ts +6 -1
  36. package/dist/src/types.d.ts.map +1 -1
  37. package/package.json +3 -2
  38. package/skill/memory.md +7 -2
@@ -3,12 +3,10 @@
3
3
  /**
4
4
  * sessions sub-commands for code-session-memory CLI
5
5
  *
6
- * sessions [list] Browse sessions (interactive TUI)
7
- * sessions list --filter Browse with filter step first
6
+ * sessions [list] Browse sessions (3-level tree: source → date → session)
8
7
  * sessions print [id] Print all chunks of a session to stdout
9
- * sessions print --filter Pick session interactively with filter, then print
10
8
  * sessions delete [id] Delete a session from the DB
11
- * sessions delete --filter Pick session interactively with filter, then delete
9
+ * sessions purge [--days <n>] [--yes] Delete all sessions older than N days
12
10
  */
13
11
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
12
  if (k2 === undefined) k2 = k;
@@ -47,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
47
45
  exports.cmdSessionsList = cmdSessionsList;
48
46
  exports.cmdSessionsPrint = cmdSessionsPrint;
49
47
  exports.cmdSessionsDelete = cmdSessionsDelete;
48
+ exports.cmdSessionsPurge = cmdSessionsPurge;
50
49
  exports.cmdSessions = cmdSessions;
51
50
  const clack = __importStar(require("@clack/prompts"));
52
51
  const database_1 = require("./database");
@@ -66,7 +65,11 @@ function fmtDate(unixMs) {
66
65
  return new Date(unixMs).toISOString().slice(0, 10);
67
66
  }
68
67
  function fmtSource(source) {
69
- return source === "opencode" ? cyan("opencode") : yellow("claude-code");
68
+ if (source === "opencode")
69
+ return cyan("opencode");
70
+ if (source === "cursor")
71
+ return green("cursor");
72
+ return yellow("claude-code");
70
73
  }
71
74
  function fmtTitle(title) {
72
75
  return title || dim("(untitled)");
@@ -77,11 +80,14 @@ function fmtProject(project) {
77
80
  const parts = project.replace(/\\/g, "/").split("/").filter(Boolean);
78
81
  return parts.length > 2 ? dim("…/" + parts.slice(-2).join("/")) : dim(project);
79
82
  }
83
+ function fmtChunks(n) {
84
+ return `${n} chunk${n !== 1 ? "s" : ""}`;
85
+ }
80
86
  function hr(char = "─", width = 72) {
81
87
  return char.repeat(width);
82
88
  }
83
89
  // ---------------------------------------------------------------------------
84
- // DB helpers (open/close around each command)
90
+ // DB helpers
85
91
  // ---------------------------------------------------------------------------
86
92
  function withDb(fn) {
87
93
  const dbPath = (0, database_1.resolveDbPath)();
@@ -94,129 +100,131 @@ function withDb(fn) {
94
100
  }
95
101
  }
96
102
  // ---------------------------------------------------------------------------
97
- // Label builder for the session picker
103
+ // Label builders
98
104
  // ---------------------------------------------------------------------------
99
105
  function sessionLabel(s) {
100
- const date = fmtDate(s.updated_at);
101
- const chunks = `${s.chunk_count} chunk${s.chunk_count !== 1 ? "s" : ""}`;
106
+ const chunks = fmtChunks(s.chunk_count);
102
107
  const title = fmtTitle(s.session_title).padEnd(40);
103
- return `${title} ${date} ${chunks}`;
108
+ return `${title} ${chunks}`;
104
109
  }
105
110
  function sessionHint(s) {
106
111
  return `${fmtSource(s.source)} ${fmtProject(s.project)}`;
107
112
  }
108
- // ---------------------------------------------------------------------------
109
- // Filter step (shown when --filter flag is present)
110
- // ---------------------------------------------------------------------------
111
- async function runFilterStep() {
112
- clack.intro(bold("Filter sessions"));
113
- const sourceAnswer = await clack.select({
114
- message: "Source",
115
- options: [
116
- { value: "all", label: "All tools" },
117
- { value: "opencode", label: "OpenCode" },
118
- { value: "claude-code", label: "Claude Code" },
119
- ],
120
- initialValue: "all",
121
- });
122
- if (clack.isCancel(sourceAnswer)) {
123
- clack.cancel("Cancelled.");
124
- process.exit(0);
113
+ async function pickSessionTree(allSessions) {
114
+ if (allSessions.length === 0)
115
+ return null;
116
+ // ── Level 1: choose source ──────────────────────────────────────────────
117
+ // Group by source, count sessions + chunks
118
+ const sourceMap = new Map();
119
+ for (const s of allSessions) {
120
+ if (!sourceMap.has(s.source))
121
+ sourceMap.set(s.source, []);
122
+ sourceMap.get(s.source).push(s);
125
123
  }
126
- const dateAnswer = await clack.select({
127
- message: "Date range",
128
- options: [
129
- { value: "all", label: "All time" },
130
- { value: "7d", label: "Last 7 days" },
131
- { value: "30d", label: "Last 30 days" },
132
- { value: "90d", label: "Last 90 days" },
133
- { value: "recent", label: "Last N days", hint: "enter a number" },
134
- { value: "older", label: "Older than N days", hint: "enter a number" },
135
- ],
136
- initialValue: "all",
124
+ // Sort sources by session count descending
125
+ const sources = [...sourceMap.entries()].sort((a, b) => b[1].length - a[1].length);
126
+ const sourceOptions = [
127
+ ...sources.map(([src, rows]) => ({
128
+ value: src,
129
+ label: sourceLabelText(src),
130
+ hint: `${rows.length} session${rows.length !== 1 ? "s" : ""} ${fmtChunks(rows.reduce((n, r) => n + r.chunk_count, 0))}`,
131
+ })),
132
+ { value: "__all__", label: dim("All sessions"), hint: `${allSessions.length} total` },
133
+ { value: "__exit__", label: dim("Exit") },
134
+ ];
135
+ const srcChoice = await clack.select({
136
+ message: "Source",
137
+ options: sourceOptions,
138
+ maxItems: 8,
137
139
  });
138
- if (clack.isCancel(dateAnswer)) {
139
- clack.cancel("Cancelled.");
140
- process.exit(0);
141
- }
142
- const filter = {};
143
- if (sourceAnswer !== "all") {
144
- filter.source = sourceAnswer;
145
- }
146
- const nowMs = Date.now();
147
- const DAY_MS = 86400 * 1000;
148
- if (dateAnswer === "7d") {
149
- filter.fromDate = nowMs - 7 * DAY_MS;
150
- }
151
- else if (dateAnswer === "30d") {
152
- filter.fromDate = nowMs - 30 * DAY_MS;
153
- }
154
- else if (dateAnswer === "90d") {
155
- filter.fromDate = nowMs - 90 * DAY_MS;
140
+ if (clack.isCancel(srcChoice) || srcChoice === "__exit__")
141
+ return null;
142
+ const filteredBySrc = srcChoice === "__all__" ? allSessions : sourceMap.get(srcChoice);
143
+ // ── Level 2: choose date (skip if only one date or "All sessions") ──────
144
+ let filteredByDate;
145
+ if (srcChoice === "__all__") {
146
+ // Skip date level — go straight to all sessions
147
+ filteredByDate = filteredBySrc;
156
148
  }
157
- else if (dateAnswer === "recent") {
158
- const nAnswer = await clack.text({
159
- message: "Show sessions from the last how many days?",
160
- placeholder: "14",
161
- validate(v) {
162
- if (!v || isNaN(Number(v)) || Number(v) <= 0)
163
- return "Please enter a positive number.";
164
- },
165
- });
166
- if (clack.isCancel(nAnswer)) {
167
- clack.cancel("Cancelled.");
168
- process.exit(0);
149
+ else {
150
+ // Group by YYYY-MM-DD
151
+ const dateMap = new Map();
152
+ for (const s of filteredBySrc) {
153
+ const d = fmtDate(s.updated_at);
154
+ if (!dateMap.has(d))
155
+ dateMap.set(d, []);
156
+ dateMap.get(d).push(s);
169
157
  }
170
- filter.fromDate = nowMs - Number(nAnswer) * DAY_MS;
171
- }
172
- else if (dateAnswer === "older") {
173
- const nAnswer = await clack.text({
174
- message: "Show sessions older than how many days?",
175
- placeholder: "30",
176
- validate(v) {
177
- if (!v || isNaN(Number(v)) || Number(v) <= 0)
178
- return "Please enter a positive number.";
179
- },
180
- });
181
- if (clack.isCancel(nAnswer)) {
182
- clack.cancel("Cancelled.");
183
- process.exit(0);
158
+ if (dateMap.size === 1) {
159
+ // Only one date — skip level 2
160
+ filteredByDate = filteredBySrc;
161
+ }
162
+ else {
163
+ const dates = [...dateMap.entries()].sort((a, b) => b[0].localeCompare(a[0]));
164
+ const dateOptions = [
165
+ ...dates.map(([date, rows]) => ({
166
+ value: date,
167
+ label: date,
168
+ hint: `${rows.length} session${rows.length !== 1 ? "s" : ""}`,
169
+ })),
170
+ { value: "__back__", label: dim("Back") },
171
+ ];
172
+ const dateChoice = await clack.select({
173
+ message: `Date ${dim("(" + sourceLabelText(srcChoice) + ")")}`,
174
+ options: dateOptions,
175
+ maxItems: 10,
176
+ });
177
+ if (clack.isCancel(dateChoice) || dateChoice === "__back__") {
178
+ // Go back to level 1
179
+ return pickSessionTree(allSessions);
180
+ }
181
+ filteredByDate = dateMap.get(dateChoice);
184
182
  }
185
- filter.toDate = nowMs - Number(nAnswer) * DAY_MS;
186
- }
187
- return filter;
188
- }
189
- // ---------------------------------------------------------------------------
190
- // Shared session picker
191
- // Returns the chosen SessionRow, or null if the user cancelled / exited.
192
- // When withFilter is true, runs the filter step first.
193
- // ---------------------------------------------------------------------------
194
- async function pickSession(withFilter) {
195
- let filter = {};
196
- if (withFilter) {
197
- filter = await runFilterStep();
198
- console.log();
199
- }
200
- const sessions = withDb((db) => (0, database_2.listSessions)(db, filter));
201
- if (sessions.length === 0) {
202
- clack.log.warn("No sessions match the current filter.");
203
- return null;
204
183
  }
205
- const chosen = await clack.select({
206
- message: `${sessions.length} session${sessions.length !== 1 ? "s" : ""}${withFilter ? dim(" (filtered)") : ""} — pick one`,
207
- options: [
208
- ...sessions.map((s) => ({
209
- value: s.session_id,
210
- label: sessionLabel(s),
211
- hint: sessionHint(s),
212
- })),
213
- { value: "__cancel__", label: dim("Cancel") },
214
- ],
184
+ // ── Level 3: choose session ──────────────────────────────────────────────
185
+ const sessionOptions = [
186
+ ...filteredByDate.map((s) => ({
187
+ value: s.session_id,
188
+ label: sessionLabel(s),
189
+ hint: sessionHint(s),
190
+ })),
191
+ { value: "__back__", label: dim("Back") },
192
+ ];
193
+ const backLabel = srcChoice === "__all__"
194
+ ? "Source"
195
+ : dateMap_size(filteredBySrc) === 1 ? "Source" : "Date";
196
+ const sessionOptions2 = sessionOptions.map((o) => o.value === "__back__" ? { ...o, hint: `back to ${backLabel}` } : o);
197
+ const sessionChoice = await clack.select({
198
+ message: srcChoice === "__all__"
199
+ ? `Session ${dim("(all sources)")}`
200
+ : `Session ${dim("(" + sourceLabelText(srcChoice) + ")")}`,
201
+ options: sessionOptions2,
215
202
  maxItems: 12,
216
203
  });
217
- if (clack.isCancel(chosen) || chosen === "__cancel__")
218
- return null;
219
- return sessions.find((s) => s.session_id === chosen) ?? null;
204
+ if (clack.isCancel(sessionChoice) || sessionChoice === "__back__") {
205
+ if (srcChoice === "__all__" || dateMap_size(filteredBySrc) === 1) {
206
+ // Back to level 1
207
+ return pickSessionTree(allSessions);
208
+ }
209
+ // Back to level 2 — re-run from source choice but pre-select the date level
210
+ // Simplest: restart from level 1 (preserves the loop invariant)
211
+ return pickSessionTree(allSessions);
212
+ }
213
+ return filteredByDate.find((s) => s.session_id === sessionChoice) ?? null;
214
+ }
215
+ // Helper: plain text source label (no ANSI) for use in hints/messages
216
+ function sourceLabelText(source) {
217
+ if (source === "opencode")
218
+ return "OpenCode";
219
+ if (source === "cursor")
220
+ return "Cursor";
221
+ if (source === "claude-code")
222
+ return "Claude Code";
223
+ return source;
224
+ }
225
+ // Helper to count unique dates in a session list without capturing dateMap
226
+ function dateMap_size(sessions) {
227
+ return new Set(sessions.map((s) => fmtDate(s.updated_at))).size;
220
228
  }
221
229
  // ---------------------------------------------------------------------------
222
230
  // Action loop for a selected session (used by sessions list)
@@ -225,7 +233,7 @@ async function sessionActionLoop(session) {
225
233
  while (true) {
226
234
  clack.log.message([
227
235
  `${bold(fmtTitle(session.session_title))}`,
228
- `${fmtSource(session.source)} ${fmtDate(session.updated_at)} ${session.chunk_count} chunks`,
236
+ `${fmtSource(session.source)} ${fmtDate(session.updated_at)} ${fmtChunks(session.chunk_count)}`,
229
237
  `Project: ${session.project || dim("—")}`,
230
238
  `ID: ${dim(session.session_id)}`,
231
239
  ].join("\n"), { symbol: "○" });
@@ -246,7 +254,7 @@ async function sessionActionLoop(session) {
246
254
  }
247
255
  if (action === "delete") {
248
256
  const confirmed = await clack.confirm({
249
- message: `Delete "${fmtTitle(session.session_title)}" (${session.chunk_count} chunks)?`,
257
+ message: `Delete "${fmtTitle(session.session_title)}" (${fmtChunks(session.chunk_count)})?`,
250
258
  initialValue: false,
251
259
  });
252
260
  if (clack.isCancel(confirmed) || !confirmed) {
@@ -263,59 +271,44 @@ async function sessionActionLoop(session) {
263
271
  // ---------------------------------------------------------------------------
264
272
  // sessions list
265
273
  // ---------------------------------------------------------------------------
266
- async function cmdSessionsList(args) {
267
- const withFilter = args.includes("--filter");
268
- if (!withFilter) {
269
- clack.intro(bold("Sessions"));
270
- }
271
- // Main browse loop — re-shown after back/delete so the user can keep browsing
272
- let filter = {};
273
- let filterResolved = false;
274
+ async function cmdSessionsList() {
275
+ clack.intro(bold("Sessions"));
274
276
  while (true) {
275
- // Run filter step once (first iteration only, if --filter was passed)
276
- if (withFilter && !filterResolved) {
277
- filter = await runFilterStep();
278
- filterResolved = true;
279
- console.log();
280
- }
281
- const sessions = withDb((db) => (0, database_2.listSessions)(db, filter));
277
+ const sessions = withDb((db) => (0, database_2.listSessions)(db));
282
278
  if (sessions.length === 0) {
283
- clack.log.warn("No sessions match the current filter.");
279
+ clack.log.warn("No sessions indexed yet.");
284
280
  clack.outro("Done.");
285
281
  return;
286
282
  }
287
- const chosen = await clack.select({
288
- message: `${sessions.length} session${sessions.length !== 1 ? "s" : ""} ${withFilter ? dim("(filtered)") : ""}`,
289
- options: [
290
- ...sessions.map((s) => ({
291
- value: s.session_id,
292
- label: sessionLabel(s),
293
- hint: sessionHint(s),
294
- })),
295
- { value: "__exit__", label: dim("Exit") },
296
- ],
297
- maxItems: 12,
298
- });
299
- if (clack.isCancel(chosen) || chosen === "__exit__") {
283
+ const session = await pickSessionTree(sessions);
284
+ if (!session) {
300
285
  clack.outro("Done.");
301
286
  return;
302
287
  }
303
- const session = sessions.find((s) => s.session_id === chosen);
304
288
  const result = await sessionActionLoop(session);
305
289
  if (result === "exit")
306
290
  return;
307
- // result === "back" → loop again, refresh session list
291
+ // result === "back" → loop, refresh session list and restart tree
292
+ }
293
+ }
294
+ // ---------------------------------------------------------------------------
295
+ // Shared session picker (for print / delete sub-commands)
296
+ // ---------------------------------------------------------------------------
297
+ async function pickSession() {
298
+ const sessions = withDb((db) => (0, database_2.listSessions)(db));
299
+ if (sessions.length === 0) {
300
+ clack.log.warn("No sessions indexed yet.");
301
+ return null;
308
302
  }
303
+ return pickSessionTree(sessions);
309
304
  }
310
305
  // ---------------------------------------------------------------------------
311
306
  // sessions print [id]
312
307
  // ---------------------------------------------------------------------------
313
- async function cmdSessionsPrint(sessionId, args = []) {
314
- const withFilter = args.includes("--filter");
308
+ async function cmdSessionsPrint(sessionId) {
315
309
  if (!sessionId) {
316
- // No ID given — launch interactive picker
317
310
  clack.intro(bold("Print session"));
318
- const session = await pickSession(withFilter);
311
+ const session = await pickSession();
319
312
  if (!session) {
320
313
  clack.outro("Cancelled.");
321
314
  return;
@@ -340,15 +333,18 @@ function printSession(sessionId) {
340
333
  const useTty = process.stdout.isTTY;
341
334
  const b = (s) => useTty ? bold(s) : s;
342
335
  const d = (s) => useTty ? dim(s) : s;
343
- const c = (s) => useTty ? cyan(s) : s;
344
- const y = (s) => useTty ? yellow(s) : s;
336
+ const fmtSrc = (src) => {
337
+ if (!useTty)
338
+ return src;
339
+ return fmtSource(src);
340
+ };
345
341
  const title = session?.session_title || "(untitled)";
346
342
  const source = session?.source || "unknown";
347
343
  const date = session ? fmtDate(session.updated_at) : "unknown";
348
344
  const project = session?.project || "—";
349
345
  console.log(hr());
350
346
  console.log(`${b("Session:")} ${title}`);
351
- console.log(`${b("Source:")} ${source === "opencode" ? c(source) : y(source)} ${d(date)}`);
347
+ console.log(`${b("Source:")} ${fmtSrc(source)} ${d(date)}`);
352
348
  console.log(`${b("Project:")} ${project}`);
353
349
  console.log(`${b("ID:")} ${d(sessionId)}`);
354
350
  console.log(`${b("Chunks:")} ${chunks.length}`);
@@ -367,13 +363,11 @@ function printSession(sessionId) {
367
363
  // ---------------------------------------------------------------------------
368
364
  // sessions delete [id]
369
365
  // ---------------------------------------------------------------------------
370
- async function cmdSessionsDelete(sessionId, args = []) {
371
- const withFilter = args.includes("--filter");
366
+ async function cmdSessionsDelete(sessionId) {
372
367
  clack.intro(bold("Delete session"));
373
368
  let session = null;
374
369
  if (!sessionId) {
375
- // No ID given — launch interactive picker
376
- session = await pickSession(withFilter);
370
+ session = await pickSession();
377
371
  if (!session) {
378
372
  clack.outro("Cancelled.");
379
373
  return;
@@ -391,12 +385,12 @@ async function cmdSessionsDelete(sessionId, args = []) {
391
385
  }
392
386
  clack.log.message([
393
387
  `${bold(fmtTitle(session.session_title))}`,
394
- `${fmtSource(session.source)} ${fmtDate(session.updated_at)} ${session.chunk_count} chunks`,
388
+ `${fmtSource(session.source)} ${fmtDate(session.updated_at)} ${fmtChunks(session.chunk_count)}`,
395
389
  `Project: ${session.project || dim("—")}`,
396
390
  `ID: ${dim(session.session_id)}`,
397
391
  ].join("\n"), { symbol: "○" });
398
392
  const confirmed = await clack.confirm({
399
- message: `Delete this session (${session.chunk_count} chunks)?`,
393
+ message: `Delete this session (${fmtChunks(session.chunk_count)})?`,
400
394
  initialValue: false,
401
395
  });
402
396
  if (clack.isCancel(confirmed) || !confirmed) {
@@ -409,6 +403,78 @@ async function cmdSessionsDelete(sessionId, args = []) {
409
403
  clack.outro("Done.");
410
404
  }
411
405
  // ---------------------------------------------------------------------------
406
+ // sessions purge [--days <n>] [--yes]
407
+ // ---------------------------------------------------------------------------
408
+ async function cmdSessionsPurge(args) {
409
+ const DAY_MS = 86400 * 1000;
410
+ // Parse --days <n>
411
+ let days;
412
+ const daysIdx = args.indexOf("--days");
413
+ if (daysIdx !== -1 && args[daysIdx + 1]) {
414
+ const parsed = Number(args[daysIdx + 1]);
415
+ if (!Number.isInteger(parsed) || parsed <= 0) {
416
+ console.error("--days must be a positive integer");
417
+ process.exit(1);
418
+ }
419
+ days = parsed;
420
+ }
421
+ const skipConfirm = args.includes("--yes");
422
+ if (!skipConfirm) {
423
+ clack.intro(bold("Purge old sessions"));
424
+ }
425
+ // Prompt for days if not provided
426
+ if (days === undefined) {
427
+ const answer = await clack.text({
428
+ message: "Delete sessions older than how many days?",
429
+ placeholder: "30",
430
+ validate(v) {
431
+ const n = Number(v);
432
+ if (!v || !Number.isInteger(n) || n <= 0)
433
+ return "Please enter a positive integer.";
434
+ },
435
+ });
436
+ if (clack.isCancel(answer)) {
437
+ clack.cancel("Cancelled.");
438
+ return;
439
+ }
440
+ days = Number(answer);
441
+ }
442
+ const cutoff = Date.now() - days * DAY_MS;
443
+ const candidates = withDb((db) => (0, database_2.listSessions)(db, { toDate: cutoff }));
444
+ if (candidates.length === 0) {
445
+ const msg = `No sessions older than ${days} day${days !== 1 ? "s" : ""} found.`;
446
+ if (skipConfirm) {
447
+ console.log(msg);
448
+ }
449
+ else {
450
+ clack.log.info(msg);
451
+ clack.outro("Nothing to purge.");
452
+ }
453
+ return;
454
+ }
455
+ const totalChunks = candidates.reduce((n, s) => n + s.chunk_count, 0);
456
+ const summary = `${candidates.length} session${candidates.length !== 1 ? "s" : ""} (${totalChunks} chunks) older than ${days} day${days !== 1 ? "s" : ""}`;
457
+ if (skipConfirm) {
458
+ // Non-interactive: just do it
459
+ const result = withDb((db) => (0, database_2.deleteSessionsOlderThan)(db, cutoff));
460
+ console.log(`Deleted ${result.sessions} sessions (${result.chunks} chunks).`);
461
+ return;
462
+ }
463
+ clack.log.warn(`Found ${summary}.`);
464
+ const confirmed = await clack.confirm({
465
+ message: `Permanently delete ${summary}?`,
466
+ initialValue: false,
467
+ });
468
+ if (clack.isCancel(confirmed) || !confirmed) {
469
+ clack.cancel("Purge cancelled — database was not modified.");
470
+ return;
471
+ }
472
+ const result = withDb((db) => (0, database_2.deleteSessionsOlderThan)(db, cutoff));
473
+ clack.log.success(`Deleted ${result.sessions} sessions (${result.chunks} chunks).`);
474
+ clack.log.warn("Note: sessions will be re-indexed on the next agent turn if source files still exist.");
475
+ clack.outro("Done.");
476
+ }
477
+ // ---------------------------------------------------------------------------
412
478
  // Help
413
479
  // ---------------------------------------------------------------------------
414
480
  function sessionsHelp() {
@@ -417,22 +483,18 @@ function sessionsHelp() {
417
483
  ${b("sessions")} — Browse, inspect, and delete indexed sessions
418
484
 
419
485
  ${b("Usage:")}
420
- npx code-session-memory sessions Browse sessions interactively
421
- npx code-session-memory sessions --filter Apply source/date filters before browsing
486
+ npx code-session-memory sessions Browse sessions (tree: source → date → session)
422
487
  npx code-session-memory sessions list Same as above (explicit sub-command)
423
- npx code-session-memory sessions list --filter Same with filter step
424
488
 
425
489
  npx code-session-memory sessions print Pick a session interactively, then print
426
- npx code-session-memory sessions print --filter Pick with filter, then print
427
490
  npx code-session-memory sessions print <id> Print all chunks of a session directly
428
491
 
429
492
  npx code-session-memory sessions delete Pick a session interactively, then delete
430
- npx code-session-memory sessions delete --filter Pick with filter, then delete
431
493
  npx code-session-memory sessions delete <id> Delete a session directly
432
494
 
433
- ${b("Filter options")} (with --filter):
434
- Source: All tools / OpenCode / Claude Code
435
- Date range: Last 7 / 30 / 90 days, last N days (custom), older than N days (custom)
495
+ npx code-session-memory sessions purge Delete sessions older than N days (interactive)
496
+ npx code-session-memory sessions purge --days <n> Non-interactive, prompts for confirmation
497
+ npx code-session-memory sessions purge --days <n> --yes Fully non-interactive (no confirmation)
436
498
 
437
499
  ${b("Notes:")}
438
500
  - Deleting a session only removes it from the DB. If the source files still
@@ -441,36 +503,34 @@ ${b("Notes:")}
441
503
  `);
442
504
  }
443
505
  // ---------------------------------------------------------------------------
444
- // Entry point: dispatch sessions sub-commands
506
+ // Entry point
445
507
  // ---------------------------------------------------------------------------
446
508
  async function cmdSessions(argv) {
447
509
  const sub = argv[0] ?? "list";
448
- // Help flag anywhere in argv
449
510
  if (argv.includes("--help") || argv.includes("-h")) {
450
511
  sessionsHelp();
451
512
  return;
452
513
  }
453
514
  switch (sub) {
454
515
  case "list":
455
- await cmdSessionsList(argv.slice(1));
516
+ await cmdSessionsList();
456
517
  break;
457
518
  case "print": {
458
- // argv[1] is either an ID or a flag (--filter) or absent
459
519
  const maybeId = argv[1] && !argv[1].startsWith("-") ? argv[1] : undefined;
460
- const restArgs = maybeId ? argv.slice(2) : argv.slice(1);
461
- await cmdSessionsPrint(maybeId, restArgs);
520
+ await cmdSessionsPrint(maybeId);
462
521
  break;
463
522
  }
464
523
  case "delete": {
465
524
  const maybeId = argv[1] && !argv[1].startsWith("-") ? argv[1] : undefined;
466
- const restArgs = maybeId ? argv.slice(2) : argv.slice(1);
467
- await cmdSessionsDelete(maybeId, restArgs);
525
+ await cmdSessionsDelete(maybeId);
468
526
  break;
469
527
  }
528
+ case "purge":
529
+ await cmdSessionsPurge(argv.slice(1));
530
+ break;
470
531
  default:
471
- // Treat unknown first arg as implicit "list" (e.g. `sessions --filter`)
472
532
  if (sub.startsWith("-")) {
473
- await cmdSessionsList(argv);
533
+ await cmdSessionsList();
474
534
  }
475
535
  else {
476
536
  console.error(`Unknown sessions sub-command: ${sub}`);