claude-recall 0.28.2 → 0.28.4
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/README.md
CHANGED
|
@@ -129,6 +129,8 @@ With Option B the agent has the memory tools (`load_rules`, `store_memory`, `sea
|
|
|
129
129
|
|
|
130
130
|
**Not available under Kiro** with either option (Kiro's hooks expose no transcript): transcript-based failure detection and session-end auto-checkpoints.
|
|
131
131
|
|
|
132
|
+
> **Project scoping under Kiro.** Memories scope to the **working directory Kiro reports for the session**, not your shell's current directory. These are usually the same — but `kiro --resume` carries the *original* session's directory, so a resumed session captures into the project it was first started in (even if your shell has since moved). Start Kiro fresh from a project directory to scope there. `claude-recall kiro doctor` prints the resolved project so you can confirm where memories are landing.
|
|
133
|
+
|
|
132
134
|
### Shared Database
|
|
133
135
|
|
|
134
136
|
All runtimes (Claude Code, Pi, Kiro CLI) use the same database at `~/.claude-recall/claude-recall.db`, scoped per project by working directory. A correction learned in one agent is available in the others.
|
|
@@ -425,6 +427,7 @@ claude-recall store "content" -t <type> # Store with type (preference, correcti
|
|
|
425
427
|
claude-recall export backup.json # Export current project's memories to JSON
|
|
426
428
|
claude-recall export backup.json --global # Export all projects
|
|
427
429
|
claude-recall import backup.json # Import memories from JSON
|
|
430
|
+
claude-recall delete <key> # Delete one memory by key (get keys from `search`)
|
|
428
431
|
claude-recall clear --force # Clear current project (auto-backup written first)
|
|
429
432
|
claude-recall clear --force --global # Clear all projects
|
|
430
433
|
claude-recall failures # View failure memories
|
|
@@ -1961,6 +1961,26 @@ async function main() {
|
|
|
1961
1961
|
await cli.import(input, options);
|
|
1962
1962
|
process.exit(0);
|
|
1963
1963
|
});
|
|
1964
|
+
// Delete a single memory by key
|
|
1965
|
+
program
|
|
1966
|
+
.command('delete <key>')
|
|
1967
|
+
.description('Delete a single memory by its key (find keys with `search`)')
|
|
1968
|
+
.action((key) => {
|
|
1969
|
+
try {
|
|
1970
|
+
const deleted = memory_1.MemoryService.getInstance().delete(key);
|
|
1971
|
+
if (deleted) {
|
|
1972
|
+
console.log(`✅ Deleted memory: ${key}`);
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
console.log(`⚠️ No memory found with key: ${key}`);
|
|
1976
|
+
}
|
|
1977
|
+
process.exit(deleted ? 0 : 1);
|
|
1978
|
+
}
|
|
1979
|
+
catch (error) {
|
|
1980
|
+
console.error(`❌ Delete failed: ${error.message}`);
|
|
1981
|
+
process.exit(1);
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1964
1984
|
// Clear command
|
|
1965
1985
|
program
|
|
1966
1986
|
.command('clear')
|
|
@@ -180,6 +180,24 @@ class HookCommands {
|
|
|
180
180
|
(0, shared_1.hookLog)('hook-dispatcher', `stdin read failed (continuing with empty payload): ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
181
181
|
input = {};
|
|
182
182
|
}
|
|
183
|
+
// Deterministic project scoping. Resolve the project from the cwd the
|
|
184
|
+
// RUNTIME declares in the hook payload — NOT the cwd this subprocess
|
|
185
|
+
// happened to inherit. Both Claude Code and Kiro include `cwd`; for CC
|
|
186
|
+
// it equals process.cwd() so this is a no-op, but for Kiro it pins
|
|
187
|
+
// scoping to the session's working directory. Without this a memory
|
|
188
|
+
// captured while working on project A could land in project B (e.g. a
|
|
189
|
+
// `kiro --resume`d session whose cwd differs from the shell's), and
|
|
190
|
+
// capture (userPromptSubmit) and injection (agentSpawn) could even
|
|
191
|
+
// disagree. Project memories must scope to ONE deterministic project.
|
|
192
|
+
if (input && typeof input.cwd === 'string' && input.cwd.trim()) {
|
|
193
|
+
try {
|
|
194
|
+
const { ConfigService } = await Promise.resolve().then(() => __importStar(require('../../services/config')));
|
|
195
|
+
ConfigService.getInstance().updateConfig({ project: { rootDir: input.cwd } });
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
(0, shared_1.hookLog)('hook-dispatcher', `cwd scoping failed (using inherited cwd): ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
183
201
|
for (const name of names) {
|
|
184
202
|
try {
|
|
185
203
|
await HookCommands.runOne(name, input);
|
|
@@ -265,10 +265,165 @@ class KiroCommands {
|
|
|
265
265
|
console.log('session-end checkpoints (Kiro exposes no transcript to hooks).');
|
|
266
266
|
process.exit(0);
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Read a Kiro agent config and report whether claude-recall is wired
|
|
270
|
+
* (MCP server + which lifecycle hooks). Returns null if the file is missing
|
|
271
|
+
* or unparseable.
|
|
272
|
+
*/
|
|
273
|
+
static inspectAgent(agentPath) {
|
|
274
|
+
let config;
|
|
275
|
+
try {
|
|
276
|
+
config = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const mcp = !!(config.mcpServers && config.mcpServers['claude-recall']);
|
|
282
|
+
const hooks = [];
|
|
283
|
+
if (config.hooks && typeof config.hooks === 'object') {
|
|
284
|
+
for (const [event, entries] of Object.entries(config.hooks)) {
|
|
285
|
+
if (Array.isArray(entries) && entries.some((h) => typeof h?.command === 'string' && h.command.includes('claude-recall'))) {
|
|
286
|
+
hooks.push(event);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { name: config.name || path.basename(agentPath, '.json'), mcp, hooks };
|
|
291
|
+
}
|
|
292
|
+
/** Last non-empty line of a hook log + how long ago, or null if none. */
|
|
293
|
+
static lastLogLine(logPath) {
|
|
294
|
+
try {
|
|
295
|
+
const lines = fs.readFileSync(logPath, 'utf8').split('\n').filter(l => l.trim());
|
|
296
|
+
if (lines.length === 0)
|
|
297
|
+
return null;
|
|
298
|
+
const line = lines[lines.length - 1];
|
|
299
|
+
const m = line.match(/^\[([^\]]+)\]/);
|
|
300
|
+
let ageMs = null;
|
|
301
|
+
if (m) {
|
|
302
|
+
const t = Date.parse(m[1]);
|
|
303
|
+
if (!Number.isNaN(t))
|
|
304
|
+
ageMs = Date.now() - t;
|
|
305
|
+
}
|
|
306
|
+
return { line, ageMs };
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
static fmtAge(ageMs) {
|
|
313
|
+
if (ageMs === null)
|
|
314
|
+
return '';
|
|
315
|
+
const min = Math.round(ageMs / 60000);
|
|
316
|
+
if (min < 1)
|
|
317
|
+
return ' (just now)';
|
|
318
|
+
if (min < 60)
|
|
319
|
+
return ` (${min}m ago)`;
|
|
320
|
+
const h = Math.round(min / 60);
|
|
321
|
+
if (h < 48)
|
|
322
|
+
return ` (${h}h ago)`;
|
|
323
|
+
return ` (${Math.round(h / 24)}d ago)`;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* `claude-recall kiro doctor` — read-only diagnostic. Answers the questions
|
|
327
|
+
* that otherwise take a support round-trip: is the binary current and on
|
|
328
|
+
* PATH, is a key available for LLM capture, does the DB have memories, which
|
|
329
|
+
* Kiro agents have claude-recall wired, and are the hooks actually firing.
|
|
330
|
+
*/
|
|
331
|
+
static runDoctor() {
|
|
332
|
+
const line = (marker, text) => console.log(` ${marker} ${text}`);
|
|
333
|
+
console.log('\n🩺 Claude Recall — Kiro diagnostic\n');
|
|
334
|
+
// --- Install ---
|
|
335
|
+
console.log('Install');
|
|
336
|
+
let version = 'unknown';
|
|
337
|
+
try {
|
|
338
|
+
version = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'package.json'), 'utf8')).version;
|
|
339
|
+
}
|
|
340
|
+
catch { /* leave unknown */ }
|
|
341
|
+
line('•', `version: ${version}`);
|
|
342
|
+
const onPath = (0, repair_1.resolveOnPath)('claude-recall');
|
|
343
|
+
line(onPath ? '✓' : '⚠', onPath
|
|
344
|
+
? `on PATH: ${onPath}`
|
|
345
|
+
: 'not on PATH — hooks/agent commands will use absolute paths (break if the install moves)');
|
|
346
|
+
line(process.env.ANTHROPIC_API_KEY ? '✓' : '•', process.env.ANTHROPIC_API_KEY
|
|
347
|
+
? 'ANTHROPIC_API_KEY set — hooks use Claude Haiku for classification'
|
|
348
|
+
: 'ANTHROPIC_API_KEY not set — hooks use the regex fallback (explicit "remember/recall/always/never/I prefer …" still captured)');
|
|
349
|
+
// --- Database ---
|
|
350
|
+
console.log('\nDatabase (this project)');
|
|
351
|
+
try {
|
|
352
|
+
// Lazy require so a broken DB can't stop the earlier sections printing
|
|
353
|
+
const { MemoryService } = require('../../services/memory');
|
|
354
|
+
const { ConfigService } = require('../../services/config');
|
|
355
|
+
const ms = MemoryService.getInstance();
|
|
356
|
+
const projectId = ConfigService.getInstance().getProjectId();
|
|
357
|
+
const stats = ms.getStats();
|
|
358
|
+
line('✓', `project: ${projectId}`);
|
|
359
|
+
line('•', `total memories (all projects): ${stats.total}`);
|
|
360
|
+
const rules = ms.loadActiveRules(projectId);
|
|
361
|
+
const ruleCount = rules.preferences.length + rules.corrections.length + rules.failures.length + rules.devops.length;
|
|
362
|
+
line(ruleCount > 0 ? '✓' : '•', `active rules for this project: ${ruleCount}`);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
line('⚠', `could not open database: ${err.message}`);
|
|
366
|
+
}
|
|
367
|
+
// --- Kiro agents ---
|
|
368
|
+
console.log('\nKiro agents with Claude Recall wired');
|
|
369
|
+
const agentDirs = [
|
|
370
|
+
{ label: 'workspace', dir: path.join(process.cwd(), '.kiro', 'agents') },
|
|
371
|
+
{ label: 'global', dir: path.join(os.homedir(), '.kiro', 'agents') },
|
|
372
|
+
];
|
|
373
|
+
let anyWired = false;
|
|
374
|
+
for (const { label, dir } of agentDirs) {
|
|
375
|
+
if (!fs.existsSync(dir))
|
|
376
|
+
continue;
|
|
377
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
378
|
+
for (const f of files) {
|
|
379
|
+
const info = KiroCommands.inspectAgent(path.join(dir, f));
|
|
380
|
+
if (!info || (!info.mcp && info.hooks.length === 0))
|
|
381
|
+
continue;
|
|
382
|
+
anyWired = true;
|
|
383
|
+
const bits = [
|
|
384
|
+
info.mcp ? 'MCP' : null,
|
|
385
|
+
info.hooks.length ? `hooks: ${info.hooks.join(', ')}` : null,
|
|
386
|
+
].filter(Boolean).join(' | ');
|
|
387
|
+
line('✓', `${info.name} (${label}) — ${bits}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!anyWired) {
|
|
391
|
+
line('⚠', 'none found. Run `claude-recall kiro setup` (new agent) or');
|
|
392
|
+
line(' ', ' `claude-recall kiro setup --merge-into <agent>` (existing agent).');
|
|
393
|
+
}
|
|
394
|
+
// --- Hook activity ---
|
|
395
|
+
console.log('\nRecent hook activity');
|
|
396
|
+
const dir = process.env.CLAUDE_RECALL_DB_PATH || path.join(os.homedir(), '.claude-recall');
|
|
397
|
+
const logDir = path.join(dir, 'hook-logs');
|
|
398
|
+
const kiro = KiroCommands.lastLogLine(path.join(logDir, 'kiro.log'));
|
|
399
|
+
const cd = KiroCommands.lastLogLine(path.join(logDir, 'correction-detector.log'));
|
|
400
|
+
if (kiro) {
|
|
401
|
+
line('✓', `kiro hooks last ran${KiroCommands.fmtAge(kiro.ageMs)}: ${kiro.line.replace(/^\[[^\]]+\]\s*/, '')}`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
line('⚠', 'no kiro.log — Kiro hooks have never fired. After `kiro setup`/`--merge-into`, RESTART Kiro (hooks bind at agent activation).');
|
|
405
|
+
}
|
|
406
|
+
if (cd) {
|
|
407
|
+
line('✓', `capture hook last ran${KiroCommands.fmtAge(cd.ageMs)}: ${cd.line.replace(/^\[[^\]]+\]\s*/, '')}`);
|
|
408
|
+
}
|
|
409
|
+
// --- Governance note ---
|
|
410
|
+
console.log('\nNotes');
|
|
411
|
+
line('•', 'If Kiro\'s startup banner omits "claude-recall" from loaded servers, your org likely');
|
|
412
|
+
line(' ', ' restricts MCP to a trusted registry. Hooks still work (capture + injection go straight');
|
|
413
|
+
line(' ', ' to the local DB) — only the interactive MCP tools need an admin to allowlist claude-recall.');
|
|
414
|
+
console.log('');
|
|
415
|
+
process.exit(0);
|
|
416
|
+
}
|
|
268
417
|
static register(program) {
|
|
269
418
|
const kiroCmd = program
|
|
270
419
|
.command('kiro')
|
|
271
420
|
.description('Kiro CLI integration');
|
|
421
|
+
kiroCmd
|
|
422
|
+
.command('doctor')
|
|
423
|
+
.description('Diagnose the Kiro integration: install, database, wired agents, and whether hooks are firing')
|
|
424
|
+
.action(() => {
|
|
425
|
+
KiroCommands.runDoctor();
|
|
426
|
+
});
|
|
272
427
|
kiroCmd
|
|
273
428
|
.command('setup')
|
|
274
429
|
.description('Write a Kiro custom agent (.kiro/agents/recall.json) with Claude Recall memory wired in, or merge into an existing agent')
|
|
@@ -43,8 +43,14 @@ async function handleCorrectionDetector(input) {
|
|
|
43
43
|
(0, shared_1.hookLog)('correction-detector', `Reask signal detection error: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
44
44
|
}
|
|
45
45
|
const result = await (0, shared_1.classifyContent)(prompt);
|
|
46
|
-
if (!result)
|
|
46
|
+
if (!result) {
|
|
47
|
+
// Terse trace so "did the hook fire?" is answerable from the log alone:
|
|
48
|
+
// absence of any line = hook never ran; "no rule detected" = ran but the
|
|
49
|
+
// prompt wasn't a storable rule; "Captured X" = ran and stored. Prompt
|
|
50
|
+
// text is NOT logged (privacy) — only its length.
|
|
51
|
+
(0, shared_1.hookLog)('correction-detector', `no rule detected in prompt (len=${prompt.length})`);
|
|
47
52
|
return;
|
|
53
|
+
}
|
|
48
54
|
// Reject short/garbage extracts and raw dumps (not clean rules)
|
|
49
55
|
if (result.extract.length < 10 || result.extract.length > 200)
|
|
50
56
|
return;
|
package/dist/hooks/shared.js
CHANGED
|
@@ -73,11 +73,13 @@ const CORRECTION_PATTERNS = [
|
|
|
73
73
|
{ regex: /\bstop\s+(doing|using|adding)\s+(.+)/i, confidence: 0.75 },
|
|
74
74
|
];
|
|
75
75
|
const PREFERENCE_PATTERNS = [
|
|
76
|
-
// "remember ..."
|
|
77
|
-
// that X", "remember to X",
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
76
|
+
// "remember ..." / "recall ..." are explicit store requests in any phrasing —
|
|
77
|
+
// "remember that X", "remember to X", "remember my favourite color is green",
|
|
78
|
+
// "recall my favourite color is green". This is the resilient (no-LLM,
|
|
79
|
+
// no-MCP) capture path, which matters under enterprise Kiro governance that
|
|
80
|
+
// blocks the MCP tools. The interrogative/question-mark guards below keep
|
|
81
|
+
// "do you remember that config file?" / "do you recall X?" out.
|
|
82
|
+
{ regex: /\b(?:remember|recall)\s+(?:that\s+|this\s+|to\s+)?(.+)/i, confidence: 0.8 },
|
|
81
83
|
{ regex: /\bfrom\s+now\s+on[,.]?\s+(.+)/i, confidence: 0.8 },
|
|
82
84
|
{ regex: /\bgoing\s+forward[,.]?\s+(.+)/i, confidence: 0.8 },
|
|
83
85
|
{ regex: /\balways\s+(.+)/i, confidence: 0.75 },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-recall",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
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": {
|
|
@@ -105,6 +105,6 @@
|
|
|
105
105
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
106
106
|
"better-sqlite3": "^12.2.0",
|
|
107
107
|
"chalk": "^5.5.0",
|
|
108
|
-
"commander": "^
|
|
108
|
+
"commander": "^14.0.3"
|
|
109
109
|
}
|
|
110
110
|
}
|