claude-recall 0.20.16 → 0.21.1

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.
@@ -8,14 +8,14 @@ source: claude-recall
8
8
 
9
9
  # Preferences
10
10
 
11
- Auto-generated from 5 memories. Last updated: 2026-04-10.
11
+ Auto-generated from 5 memories. Last updated: 2026-04-11.
12
12
 
13
13
  ## Rules
14
14
 
15
- - Session test preference 1775841028541
16
- - Test preference 1775841028479-2
17
- - Test preference 1775841028479-1
18
- - Test preference 1775841028479-0
15
+ - Session test preference 1775896807164
16
+ - Test preference 1775896807117-2
17
+ - Test preference 1775896807117-1
18
+ - Test preference 1775896807117-0
19
19
  - Test memory content
20
20
 
21
21
  ---
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "topicId": "preferences",
3
- "sourceHash": "ffe036415b05cb18e9c3e65a38a2e7dedbf02dfd5463c14a03184a9c93c75799",
3
+ "sourceHash": "47589a75902fabe04a4aab7d8c89e3cdd26c4c1cf7b6e3086a7ea3186413abe4",
4
4
  "memoryCount": 5,
5
- "generatedAt": "2026-04-10T17:10:28.567Z",
5
+ "generatedAt": "2026-04-11T08:40:07.182Z",
6
6
  "memoryKeys": [
7
- "memory_1775841028543_yc9163qfd",
8
- "memory_1775841028515_8bjn9exsc",
9
- "memory_1775841028500_gch5kvqxl",
10
- "memory_1775841028480_n2evtiegj",
11
- "memory_1775841028436_w06waet05"
7
+ "memory_1775896807165_8gx2muuz6",
8
+ "memory_1775896807142_2zmd3oc2x",
9
+ "memory_1775896807130_nsc3d9wky",
10
+ "memory_1775896807118_3qvm34ozn",
11
+ "memory_1775896807081_saak1fkqp"
12
12
  ]
13
13
  }
package/README.md CHANGED
@@ -180,6 +180,24 @@ claude-recall import backup.json # Import memories from JSON
180
180
  claude-recall clear --force # Delete all memories (irreversible)
181
181
  ```
182
182
 
183
+ ### Task Checkpoints
184
+
185
+ Persistent "where I left off" snapshots — one per project, replaces previous on save. Not loaded as a rule; `load_rules` only hints that one exists.
186
+
187
+ ```bash
188
+ claude-recall checkpoint save \
189
+ --completed "inference layer, domain layer" \
190
+ --remaining "wire server.js, strip 3GPP URNs" \
191
+ --blockers "none" \
192
+ --notes "see inference/README.md"
193
+
194
+ claude-recall checkpoint load # Show the latest checkpoint
195
+ claude-recall checkpoint load --json # Machine-readable
196
+ claude-recall checkpoint clear # Delete the checkpoint
197
+ ```
198
+
199
+ Agents can also save/load checkpoints via MCP tools (`mcp__claude-recall__save_checkpoint` / `mcp__claude-recall__load_checkpoint`) or Pi tools (`recall_save_checkpoint` / `recall_load_checkpoint`).
200
+
183
201
  ### Troubleshooting
184
202
 
185
203
  ```bash
@@ -246,6 +264,11 @@ claude-recall outcomes --section stats # Retrieval/helpfulness stats
246
264
  claude-recall outcomes --limit 20 # More items per section
247
265
  claude-recall monitor # Memory search monitoring stats
248
266
 
267
+ # ── Task Checkpoints ────────────────────────────────────────────────
268
+ claude-recall checkpoint save --completed <text> --remaining <text> [--blockers <text>] [--notes <text>] [--project <id>]
269
+ claude-recall checkpoint load [--project <id>] [--json]
270
+ claude-recall checkpoint clear [--project <id>]
271
+
249
272
  # ── Skills ───────────────────────────────────────────────────────────
250
273
  claude-recall skills generate # Generate skills from memories
251
274
  claude-recall skills generate --dry-run # Preview without writing
@@ -500,6 +500,80 @@ class ClaudeRecallCLI {
500
500
  process.exit(1);
501
501
  }
502
502
  }
503
+ /**
504
+ * Save a task checkpoint for the current (or specified) project.
505
+ */
506
+ checkpointSave(opts) {
507
+ try {
508
+ const projectId = opts.project || config_1.ConfigService.getInstance().getProjectId();
509
+ this.memoryService.saveCheckpoint(projectId, {
510
+ completed: opts.completed,
511
+ remaining: opts.remaining,
512
+ blockers: opts.blockers || 'none',
513
+ notes: opts.notes,
514
+ });
515
+ console.log(`\n✅ Checkpoint saved for project: ${projectId}\n`);
516
+ console.log(` Completed: ${opts.completed}`);
517
+ console.log(` Remaining: ${opts.remaining}`);
518
+ console.log(` Blockers: ${opts.blockers || 'none'}`);
519
+ if (opts.notes)
520
+ console.log(` Notes: ${opts.notes}`);
521
+ console.log('');
522
+ }
523
+ catch (error) {
524
+ console.error('❌ Checkpoint save failed:', error);
525
+ process.exit(1);
526
+ }
527
+ }
528
+ /**
529
+ * Load the latest task checkpoint for the current (or specified) project.
530
+ */
531
+ checkpointLoad(opts) {
532
+ try {
533
+ const projectId = opts.project || config_1.ConfigService.getInstance().getProjectId();
534
+ const checkpoint = this.memoryService.loadCheckpoint(projectId);
535
+ if (!checkpoint) {
536
+ console.log(`\nNo checkpoint found for project: ${projectId}\n`);
537
+ return;
538
+ }
539
+ if (opts.json) {
540
+ console.log(JSON.stringify({ projectId, ...checkpoint }, null, 2));
541
+ return;
542
+ }
543
+ const updatedDate = new Date(checkpoint.updated_at).toLocaleString();
544
+ console.log(`\n📌 Checkpoint for project: ${projectId}`);
545
+ console.log(` Updated: ${updatedDate}\n`);
546
+ console.log(` Completed: ${checkpoint.completed}`);
547
+ console.log(` Remaining: ${checkpoint.remaining}`);
548
+ console.log(` Blockers: ${checkpoint.blockers}`);
549
+ if (checkpoint.notes)
550
+ console.log(` Notes: ${checkpoint.notes}`);
551
+ console.log('');
552
+ }
553
+ catch (error) {
554
+ console.error('❌ Checkpoint load failed:', error);
555
+ process.exit(1);
556
+ }
557
+ }
558
+ /**
559
+ * Delete the task checkpoint for the current (or specified) project.
560
+ */
561
+ checkpointClear(opts) {
562
+ try {
563
+ const projectId = opts.project || config_1.ConfigService.getInstance().getProjectId();
564
+ const deleted = this.memoryService.deleteCheckpoint(projectId);
565
+ if (deleted) {
566
+ console.log(`\n✅ Checkpoint cleared for project: ${projectId}\n`);
567
+ }
568
+ else {
569
+ console.log(`\nNo checkpoint to clear for project: ${projectId}\n`);
570
+ }
571
+ }
572
+ catch (error) {
573
+ console.error('❌ Checkpoint clear failed:', error);
574
+ process.exit(1);
575
+ }
576
+ }
503
577
  /**
504
578
  * Show system status
505
579
  */
@@ -1394,6 +1468,48 @@ async function main() {
1394
1468
  await cli.clear(options);
1395
1469
  process.exit(0);
1396
1470
  });
1471
+ // Checkpoint command group
1472
+ const checkpoint = program
1473
+ .command('checkpoint')
1474
+ .description('Save and load task checkpoints (work-in-progress snapshots)');
1475
+ checkpoint
1476
+ .command('save')
1477
+ .description('Save a task checkpoint for the current project (replaces previous)')
1478
+ .requiredOption('--completed <text>', 'What is done')
1479
+ .requiredOption('--remaining <text>', 'What is left to do')
1480
+ .option('--blockers <text>', 'Current blockers (default: "none")')
1481
+ .option('--notes <text>', 'Free-form notes, file refs, etc.')
1482
+ .option('--project <id>', 'Project ID (default: current)')
1483
+ .action((options) => {
1484
+ const cli = new ClaudeRecallCLI(program.opts());
1485
+ cli.checkpointSave({
1486
+ project: options.project,
1487
+ completed: options.completed,
1488
+ remaining: options.remaining,
1489
+ blockers: options.blockers,
1490
+ notes: options.notes,
1491
+ });
1492
+ process.exit(0);
1493
+ });
1494
+ checkpoint
1495
+ .command('load')
1496
+ .description('Load the latest task checkpoint for the current project')
1497
+ .option('--project <id>', 'Project ID (default: current)')
1498
+ .option('--json', 'Output as JSON')
1499
+ .action((options) => {
1500
+ const cli = new ClaudeRecallCLI(program.opts());
1501
+ cli.checkpointLoad({ project: options.project, json: options.json });
1502
+ process.exit(0);
1503
+ });
1504
+ checkpoint
1505
+ .command('clear')
1506
+ .description('Delete the task checkpoint for the current project')
1507
+ .option('--project <id>', 'Project ID (default: current)')
1508
+ .action((options) => {
1509
+ const cli = new ClaudeRecallCLI(program.opts());
1510
+ cli.checkpointClear({ project: options.project });
1511
+ process.exit(0);
1512
+ });
1397
1513
  // Status command
1398
1514
  program
1399
1515
  .command('status')
@@ -1,10 +1,67 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MemoryTools = void 0;
4
+ exports.formatRuleValue = formatRuleValue;
4
5
  const config_1 = require("../../services/config");
5
6
  const search_monitor_1 = require("../../services/search-monitor");
6
7
  const skill_generator_1 = require("../../services/skill-generator");
7
8
  const outcome_storage_1 = require("../../services/outcome-storage");
9
+ /**
10
+ * Render any memory.value shape as a readable string for load_rules output.
11
+ *
12
+ * Memory values land in the DB in several historical shapes. The previous
13
+ * rendering used `m.value.content || m.value.value || JSON.stringify(m.value)`
14
+ * which short-circuited on truthy non-string objects, producing "[object Object]"
15
+ * when string interpolation eventually called toString() on the returned object.
16
+ *
17
+ * Rules:
18
+ * 1. strings/numbers pass through (or coerce)
19
+ * 2. null/undefined → empty string
20
+ * 3. objects: prefer the first STRING field in order: content, value, title, description
21
+ * (only string — non-string `content` falls through to title)
22
+ * 4. nested failure shapes: extract `what_failed` (top-level or under `content`)
23
+ * 5. last resort: truncated JSON.stringify (never raw object)
24
+ *
25
+ * Exported for direct unit testing in tests/unit/format-rule-value.test.ts.
26
+ */
27
+ function formatRuleValue(value) {
28
+ if (value == null)
29
+ return '';
30
+ if (typeof value === 'string')
31
+ return value;
32
+ if (typeof value !== 'object')
33
+ return String(value);
34
+ const v = value;
35
+ // Prefer the first non-empty string field. Order matters:
36
+ // - `content` covers legacy hook failures and promoted lessons (lesson text)
37
+ // - `value` covers preference shape
38
+ // - `title` covers tool-outcome-watcher failures whose `content` is a nested object
39
+ // - `description` is a last-ditch human label
40
+ for (const field of ['content', 'value', 'title', 'description']) {
41
+ const candidate = v[field];
42
+ if (typeof candidate === 'string' && candidate.trim()) {
43
+ return candidate;
44
+ }
45
+ }
46
+ // Nested failure object — extract what_failed if present
47
+ if (typeof v.what_failed === 'string' && v.what_failed.trim()) {
48
+ return v.what_failed;
49
+ }
50
+ if (v.content && typeof v.content === 'object') {
51
+ const inner = v.content;
52
+ if (typeof inner.what_failed === 'string' && inner.what_failed.trim()) {
53
+ return inner.what_failed;
54
+ }
55
+ }
56
+ // Last resort: truncated JSON. Never return a raw object.
57
+ try {
58
+ const json = JSON.stringify(value);
59
+ return json.length > 200 ? json.substring(0, 200) + '…' : json;
60
+ }
61
+ catch {
62
+ return String(value);
63
+ }
64
+ }
8
65
  class MemoryTools {
9
66
  constructor(memoryService, logger, onMemoryChanged) {
10
67
  this.memoryService = memoryService;
@@ -162,7 +219,34 @@ class MemoryTools {
162
219
  required: ['id']
163
220
  },
164
221
  handler: this.handleDeleteMemory.bind(this)
165
- }
222
+ },
223
+ {
224
+ name: 'mcp__claude-recall__save_checkpoint',
225
+ description: 'Save a task checkpoint — a structured snapshot of work in progress (completed/remaining/blockers/notes). Replaces any previous checkpoint for this project. Call when ending a work session or pausing on a task.',
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ completed: { type: 'string', description: 'What has been finished in this work stream' },
230
+ remaining: { type: 'string', description: 'What is left to do' },
231
+ blockers: { type: 'string', description: 'Current blockers (use "none" if none)' },
232
+ notes: { type: 'string', description: 'Optional free-form notes, file references, etc.' },
233
+ projectId: { type: 'string', description: 'Optional project ID override. Defaults to current project.' },
234
+ },
235
+ required: ['completed', 'remaining', 'blockers'],
236
+ },
237
+ handler: this.handleSaveCheckpoint.bind(this),
238
+ },
239
+ {
240
+ name: 'mcp__claude-recall__load_checkpoint',
241
+ description: 'Load the latest task checkpoint for the current project. Returns null if none exists. Call at the start of a session to recall where you left off — load_rules will hint when one exists.',
242
+ inputSchema: {
243
+ type: 'object',
244
+ properties: {
245
+ projectId: { type: 'string', description: 'Optional project ID override. Defaults to current project.' },
246
+ },
247
+ },
248
+ handler: this.handleLoadCheckpoint.bind(this),
249
+ },
166
250
  ];
167
251
  }
168
252
  /**
@@ -260,7 +344,7 @@ class MemoryTools {
260
344
  const sections = [];
261
345
  if (rules.preferences.length > 0) {
262
346
  sections.push('## Preferences\n' + rules.preferences.map(m => {
263
- const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
347
+ const val = formatRuleValue(m.value);
264
348
  // Only show key prefix if it's a meaningful name (not auto-generated)
265
349
  const key = m.preference_key || m.key || '';
266
350
  const isAutoKey = key.startsWith('memory_') || key.startsWith('auto_') || key.startsWith('pref_');
@@ -269,7 +353,7 @@ class MemoryTools {
269
353
  }
270
354
  if (rules.corrections.length > 0) {
271
355
  sections.push('## Corrections\n' + rules.corrections.map(m => {
272
- const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
356
+ const val = formatRuleValue(m.value);
273
357
  const isPromoted = m.key.startsWith('promoted_') || m.value?.source === 'promotion-engine';
274
358
  const evidence = isPromoted && m.value?.evidence_count ? ` (learned from ${m.value.evidence_count} observations)` : '';
275
359
  return isPromoted ? `- [promoted lesson] ${val}${evidence}` : `- ${val}`;
@@ -281,21 +365,21 @@ class MemoryTools {
281
365
  const regularFailures = rules.failures.filter(m => !m.key.startsWith('promoted_') && m.value?.source !== 'promotion-engine');
282
366
  if (promotedLessons.length > 0) {
283
367
  sections.push('## Promoted Lessons (learned from repeated outcomes)\n' + promotedLessons.map(m => {
284
- const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
368
+ const val = formatRuleValue(m.value);
285
369
  const evidence = m.value?.evidence_count ? ` (seen ${m.value.evidence_count}x)` : '';
286
370
  return `- ${val}${evidence}`;
287
371
  }).join('\n'));
288
372
  }
289
373
  if (regularFailures.length > 0) {
290
374
  sections.push('## Failures\n' + regularFailures.map(m => {
291
- const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
375
+ const val = formatRuleValue(m.value);
292
376
  return `- ${val}`;
293
377
  }).join('\n'));
294
378
  }
295
379
  }
296
380
  if (rules.devops.length > 0) {
297
381
  sections.push('## DevOps Rules\n' + rules.devops.map(m => {
298
- const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
382
+ const val = formatRuleValue(m.value);
299
383
  return `- ${val}`;
300
384
  }).join('\n'));
301
385
  }
@@ -349,15 +433,30 @@ class MemoryTools {
349
433
  catch {
350
434
  // Non-critical
351
435
  }
436
+ // Checkpoint hint — surface existence without dumping content
437
+ let checkpointHint = '';
438
+ try {
439
+ const project = projectId || context.projectId || config_1.ConfigService.getInstance().getProjectId();
440
+ if (this.memoryService.hasCheckpoint(project)) {
441
+ const cp = this.memoryService.loadCheckpoint(project);
442
+ if (cp) {
443
+ const updatedDate = new Date(cp.updated_at).toLocaleString();
444
+ checkpointHint = `📌 You have an unfinished task checkpoint from ${updatedDate} — call \`load_checkpoint\` to see completed/remaining/blockers before starting work.\n\n`;
445
+ }
446
+ }
447
+ }
448
+ catch {
449
+ // Non-critical
450
+ }
352
451
  let rulesText;
353
452
  if (sections.length > 0) {
354
453
  const body = sections.join('\n\n');
355
454
  rulesText = directive
356
- ? `${directive}\n\n---\n\n${body}`
357
- : body;
455
+ ? `${directive}\n\n${checkpointHint}---\n\n${body}`
456
+ : `${checkpointHint}${body}`;
358
457
  }
359
458
  else {
360
- rulesText = 'No active rules found. This may be a new project.';
459
+ rulesText = checkpointHint || 'No active rules found. This may be a new project.';
361
460
  }
362
461
  return {
363
462
  rules: rulesText,
@@ -462,6 +561,45 @@ class MemoryTools {
462
561
  throw error;
463
562
  }
464
563
  }
564
+ async handleSaveCheckpoint(input, context) {
565
+ try {
566
+ const { completed, remaining, blockers, notes, projectId } = input;
567
+ if (typeof completed !== 'string' || typeof remaining !== 'string' || typeof blockers !== 'string') {
568
+ throw new Error('completed, remaining, and blockers are required string fields');
569
+ }
570
+ const project = projectId || config_1.ConfigService.getInstance().getProjectId();
571
+ this.memoryService.saveCheckpoint(project, { completed, remaining, blockers, notes });
572
+ return {
573
+ success: true,
574
+ projectId: project,
575
+ message: `Checkpoint saved for project: ${project}`,
576
+ };
577
+ }
578
+ catch (error) {
579
+ this.logger.error('MemoryTools', 'Failed to save checkpoint', error);
580
+ throw error;
581
+ }
582
+ }
583
+ async handleLoadCheckpoint(input, context) {
584
+ try {
585
+ const { projectId } = input || {};
586
+ const project = projectId || config_1.ConfigService.getInstance().getProjectId();
587
+ const checkpoint = this.memoryService.loadCheckpoint(project);
588
+ if (!checkpoint) {
589
+ return { found: false, projectId: project };
590
+ }
591
+ return {
592
+ found: true,
593
+ projectId: project,
594
+ ...checkpoint,
595
+ updated_at_iso: new Date(checkpoint.updated_at).toISOString(),
596
+ };
597
+ }
598
+ catch (error) {
599
+ this.logger.error('MemoryTools', 'Failed to load checkpoint', error);
600
+ throw error;
601
+ }
602
+ }
465
603
  getTools() {
466
604
  return this.tools;
467
605
  }
@@ -329,6 +329,51 @@ class MemoryStorage {
329
329
  // This ensures that other processes (like CLI) can see the changes immediately
330
330
  this.db.pragma('wal_checkpoint(TRUNCATE)');
331
331
  }
332
+ // --- Task checkpoint methods ---
333
+ checkpointKey(projectId) {
334
+ return `task-checkpoint:${projectId}`;
335
+ }
336
+ saveCheckpoint(projectId, input) {
337
+ const checkpoint = {
338
+ completed: input.completed,
339
+ remaining: input.remaining,
340
+ blockers: input.blockers,
341
+ notes: input.notes,
342
+ updated_at: Date.now(),
343
+ };
344
+ // Use a deterministic key per project so INSERT OR REPLACE replaces the previous one.
345
+ // Direct INSERT OR REPLACE bypasses fuzzy/hash dedup since they check `key != ?`.
346
+ const stmt = this.db.prepare(`
347
+ INSERT OR REPLACE INTO memories
348
+ (key, value, type, project_id, file_path, timestamp, relevance_score, access_count,
349
+ preference_key, is_active, superseded_by, superseded_at, confidence_score, sophistication_level, scope, content_hash)
350
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
351
+ `);
352
+ const value = JSON.stringify(checkpoint);
353
+ const contentHash = this.computeContentHash(checkpoint, 'task-checkpoint');
354
+ stmt.run(this.checkpointKey(projectId), value, 'task-checkpoint', projectId, null, checkpoint.updated_at, 1.0, 0, null, 1, null, null, null, 1, null, contentHash);
355
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
356
+ }
357
+ loadCheckpoint(projectId) {
358
+ const row = this.db.prepare('SELECT value FROM memories WHERE key = ? AND type = ?').get(this.checkpointKey(projectId), 'task-checkpoint');
359
+ if (!row)
360
+ return null;
361
+ try {
362
+ return JSON.parse(row.value);
363
+ }
364
+ catch {
365
+ return null;
366
+ }
367
+ }
368
+ hasCheckpoint(projectId) {
369
+ const row = this.db.prepare('SELECT 1 FROM memories WHERE key = ? AND type = ?').get(this.checkpointKey(projectId), 'task-checkpoint');
370
+ return !!row;
371
+ }
372
+ deleteCheckpoint(projectId) {
373
+ const result = this.db.prepare('DELETE FROM memories WHERE key = ? AND type = ?').run(this.checkpointKey(projectId), 'task-checkpoint');
374
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
375
+ return result.changes > 0;
376
+ }
332
377
  retrieve(key) {
333
378
  const stmt = this.db.prepare('SELECT * FROM memories WHERE key = ?');
334
379
  const row = stmt.get(key);
@@ -217,9 +217,21 @@ function default_1(pi) {
217
217
  os.recordRetrieval(m.key);
218
218
  }
219
219
  catch { /* non-critical */ }
220
+ // Checkpoint hint — surface existence without dumping content
221
+ let checkpointHint = '';
222
+ try {
223
+ if (ms.hasCheckpoint(projectId)) {
224
+ const cp = ms.loadCheckpoint(projectId);
225
+ if (cp) {
226
+ const updatedDate = new Date(cp.updated_at).toLocaleString();
227
+ checkpointHint = `📌 You have an unfinished task checkpoint from ${updatedDate} — call \`recall_load_checkpoint\` to see completed/remaining/blockers before starting work.\n\n`;
228
+ }
229
+ }
230
+ }
231
+ catch { /* non-critical */ }
220
232
  const text = body
221
- ? `${LOAD_RULES_DIRECTIVE}\n\n---\n\n${body}`
222
- : 'No active rules found. This may be a new project.';
233
+ ? `${LOAD_RULES_DIRECTIVE}\n\n${checkpointHint}---\n\n${body}`
234
+ : (checkpointHint || 'No active rules found. This may be a new project.');
223
235
  return {
224
236
  content: [{ type: 'text', text }],
225
237
  };
@@ -333,6 +345,73 @@ function default_1(pi) {
333
345
  }
334
346
  },
335
347
  });
348
+ // --- Tool: recall_save_checkpoint ---
349
+ pi.registerTool({
350
+ name: 'recall_save_checkpoint',
351
+ label: 'Save Task Checkpoint',
352
+ description: 'Save a structured snapshot of work in progress (completed/remaining/blockers/notes). Replaces any previous checkpoint for this project. Call when ending a session or pausing on a task.',
353
+ promptSnippet: 'Save a task checkpoint with what is done, what remains, and any blockers',
354
+ parameters: {},
355
+ async execute(_id, params, _signal, _onUpdate, _ctx) {
356
+ try {
357
+ const { completed, remaining, blockers, notes } = params || {};
358
+ if (typeof completed !== 'string' || typeof remaining !== 'string' || typeof blockers !== 'string') {
359
+ return {
360
+ content: [{ type: 'text', text: 'Error: completed, remaining, and blockers are required string fields' }],
361
+ isError: true,
362
+ };
363
+ }
364
+ const ms = memory_1.MemoryService.getInstance();
365
+ ms.saveCheckpoint(projectId, { completed, remaining, blockers, notes });
366
+ return {
367
+ content: [{ type: 'text', text: JSON.stringify({
368
+ success: true,
369
+ projectId,
370
+ message: `Checkpoint saved for project: ${projectId}`,
371
+ }) }],
372
+ };
373
+ }
374
+ catch (err) {
375
+ return {
376
+ content: [{ type: 'text', text: `Failed to save checkpoint: ${err.message}` }],
377
+ isError: true,
378
+ };
379
+ }
380
+ },
381
+ });
382
+ // --- Tool: recall_load_checkpoint ---
383
+ pi.registerTool({
384
+ name: 'recall_load_checkpoint',
385
+ label: 'Load Task Checkpoint',
386
+ description: 'Load the latest task checkpoint for the current project. Returns null if none exists. Call at session start to recall where you left off.',
387
+ promptSnippet: 'Load the saved task checkpoint to see what was in progress',
388
+ parameters: {},
389
+ async execute(_id, _params, _signal, _onUpdate, _ctx) {
390
+ try {
391
+ const ms = memory_1.MemoryService.getInstance();
392
+ const checkpoint = ms.loadCheckpoint(projectId);
393
+ if (!checkpoint) {
394
+ return {
395
+ content: [{ type: 'text', text: JSON.stringify({ found: false, projectId }) }],
396
+ };
397
+ }
398
+ return {
399
+ content: [{ type: 'text', text: JSON.stringify({
400
+ found: true,
401
+ projectId,
402
+ ...checkpoint,
403
+ updated_at_iso: new Date(checkpoint.updated_at).toISOString(),
404
+ }) }],
405
+ };
406
+ }
407
+ catch (err) {
408
+ return {
409
+ content: [{ type: 'text', text: `Failed to load checkpoint: ${err.message}` }],
410
+ isError: true,
411
+ };
412
+ }
413
+ },
414
+ });
336
415
  // --- Tool: recall_delete_memory ---
337
416
  pi.registerTool({
338
417
  name: 'recall_delete_memory',
@@ -527,6 +527,24 @@ class MemoryService {
527
527
  getDatabase() {
528
528
  return this.storage.getDatabase();
529
529
  }
530
+ // --- Task checkpoint methods ---
531
+ saveCheckpoint(projectId, input) {
532
+ this.storage.saveCheckpoint(projectId, input);
533
+ this.logger.info('MemoryService', `Checkpoint saved for project: ${projectId}`);
534
+ }
535
+ loadCheckpoint(projectId) {
536
+ return this.storage.loadCheckpoint(projectId);
537
+ }
538
+ hasCheckpoint(projectId) {
539
+ return this.storage.hasCheckpoint(projectId);
540
+ }
541
+ deleteCheckpoint(projectId) {
542
+ const deleted = this.storage.deleteCheckpoint(projectId);
543
+ if (deleted) {
544
+ this.logger.info('MemoryService', `Checkpoint deleted for project: ${projectId}`);
545
+ }
546
+ return deleted;
547
+ }
530
548
  /**
531
549
  * Check if database is connected
532
550
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.20.16",
3
+ "version": "0.21.1",
4
4
  "description": "Persistent memory for Claude Code and Pi with native Skills integration, automatic capture, failure learning, and project scoping",
5
5
  "main": "dist/index.js",
6
6
  "bin": {