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.
- package/.claude/skills/auto-preferences/SKILL.md +5 -5
- package/.claude/skills/auto-preferences/manifest.json +7 -7
- package/README.md +23 -0
- package/dist/cli/claude-recall-cli.js +116 -0
- package/dist/mcp/tools/memory-tools.js +147 -9
- package/dist/memory/storage.js +45 -0
- package/dist/pi/extension.js +81 -2
- package/dist/services/memory.js +18 -0
- package/package.json +1 -1
|
@@ -8,14 +8,14 @@ source: claude-recall
|
|
|
8
8
|
|
|
9
9
|
# Preferences
|
|
10
10
|
|
|
11
|
-
Auto-generated from 5 memories. Last updated: 2026-04-
|
|
11
|
+
Auto-generated from 5 memories. Last updated: 2026-04-11.
|
|
12
12
|
|
|
13
13
|
## Rules
|
|
14
14
|
|
|
15
|
-
- Session test preference
|
|
16
|
-
- Test preference
|
|
17
|
-
- Test preference
|
|
18
|
-
- Test preference
|
|
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": "
|
|
3
|
+
"sourceHash": "47589a75902fabe04a4aab7d8c89e3cdd26c4c1cf7b6e3086a7ea3186413abe4",
|
|
4
4
|
"memoryCount": 5,
|
|
5
|
-
"generatedAt": "2026-04-
|
|
5
|
+
"generatedAt": "2026-04-11T08:40:07.182Z",
|
|
6
6
|
"memoryKeys": [
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/dist/memory/storage.js
CHANGED
|
@@ -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);
|
package/dist/pi/extension.js
CHANGED
|
@@ -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',
|
package/dist/services/memory.js
CHANGED
|
@@ -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