claude-recall 0.25.1 → 0.26.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/README.md +16 -13
- package/dist/cli/claude-recall-cli.js +93 -38
- package/dist/cli/commands/hook-commands.js +121 -86
- package/dist/cli/commands/repair.js +16 -1
- package/dist/hooks/failure-detectors.js +1 -1
- package/dist/hooks/llm-classifier.js +14 -4
- package/dist/hooks/memory-stop-hook.js +87 -15
- package/dist/hooks/post-compact-reload.js +1 -1
- package/dist/hooks/shared.js +21 -1
- package/dist/hooks/tool-outcome-watcher.js +5 -6
- package/dist/mcp/prompts-handler.js +25 -48
- package/dist/mcp/resources-handler.js +1 -1
- package/dist/mcp/server.js +92 -58
- package/dist/mcp/session-manager.js +34 -107
- package/dist/mcp/tools/memory-tools.js +6 -71
- package/dist/mcp/transports/stdio.js +50 -140
- package/dist/memory/storage.js +165 -40
- package/dist/pi/extension.js +7 -24
- package/dist/services/action-pattern-detector.js +4 -4
- package/dist/services/config.js +18 -1
- package/dist/services/database-manager.js +64 -68
- package/dist/services/failure-extractor.js +3 -3
- package/dist/services/memory.js +36 -6
- package/dist/services/preference-extractor.js +4 -7
- package/dist/services/process-manager.js +11 -1
- package/dist/services/promotion-engine.js +1 -2
- package/dist/shared/directives.js +22 -0
- package/dist/shared/event-processors.js +6 -5
- package/package.json +18 -9
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Your preferences, project structure, workflows, corrections, and coding style ar
|
|
|
29
29
|
|
|
30
30
|
| Component | Version | Notes |
|
|
31
31
|
| --------- | ----------------------- | --------------------------- |
|
|
32
|
-
| Node.js | **20+**
|
|
32
|
+
| Node.js | **20.19+** | required for better-sqlite3 (Node 20+) and chalk 5 (ESM `require()` needs ≥20.19) |
|
|
33
33
|
| OS | macOS / Linux / Windows | WSL supported |
|
|
34
34
|
|
|
35
35
|
### Install for Claude Code
|
|
@@ -137,7 +137,7 @@ claude-recall search "preference"
|
|
|
137
137
|
|
|
138
138
|
## How It Works
|
|
139
139
|
|
|
140
|
-
Claude Recall provides
|
|
140
|
+
Claude Recall provides six memory tools (`load_rules`, `store_memory`, `search_memory`, `delete_memory`, `save_checkpoint`, `load_checkpoint`) backed by a local SQLite database with WAL mode, content-hash deduplication, and automatic compaction. The tools are exposed differently depending on the agent:
|
|
141
141
|
|
|
142
142
|
- **Claude Code** — MCP server with four tools and seven prompts, plus file-system hooks for automatic capture
|
|
143
143
|
- **Pi** — native extension with registered tools and event handlers, plus a skill file for behavioral guidance
|
|
@@ -218,9 +218,11 @@ claude-recall monitor # Memory search monitoring stats
|
|
|
218
218
|
```bash
|
|
219
219
|
claude-recall store "content" # Store a memory (default type: preference)
|
|
220
220
|
claude-recall store "content" -t correction # Store with specific type
|
|
221
|
-
claude-recall export backup.json # Export
|
|
221
|
+
claude-recall export backup.json # Export current project's memories to JSON
|
|
222
|
+
claude-recall export backup.json --global # Export ALL projects' memories
|
|
222
223
|
claude-recall import backup.json # Import memories from JSON
|
|
223
|
-
claude-recall clear --force # Delete
|
|
224
|
+
claude-recall clear --force # Delete current project's memories (auto-backup written first)
|
|
225
|
+
claude-recall clear --force --global # Delete ALL projects' memories
|
|
224
226
|
```
|
|
225
227
|
|
|
226
228
|
### Task Checkpoints
|
|
@@ -324,9 +326,11 @@ claude-recall search "query" --json # Output as JSON
|
|
|
324
326
|
claude-recall search "query" --project <id> # Search specific project
|
|
325
327
|
claude-recall store "content" # Store memory directly
|
|
326
328
|
claude-recall store "content" -t <type> # Store with type (preference, correction, failure, devops, project-knowledge)
|
|
327
|
-
claude-recall export backup.json # Export memories to JSON
|
|
329
|
+
claude-recall export backup.json # Export current project's memories to JSON
|
|
330
|
+
claude-recall export backup.json --global # Export all projects
|
|
328
331
|
claude-recall import backup.json # Import memories from JSON
|
|
329
|
-
claude-recall clear --force # Clear
|
|
332
|
+
claude-recall clear --force # Clear current project (auto-backup written first)
|
|
333
|
+
claude-recall clear --force --global # Clear all projects
|
|
330
334
|
claude-recall failures # View failure memories
|
|
331
335
|
claude-recall failures --limit 20 # Limit results
|
|
332
336
|
claude-recall outcomes # Outcome-aware learning status
|
|
@@ -363,7 +367,7 @@ claude-recall mcp status # Current project's server status
|
|
|
363
367
|
claude-recall mcp ps # List all running servers
|
|
364
368
|
claude-recall mcp stop # Stop server
|
|
365
369
|
claude-recall mcp stop --force # Force stop
|
|
366
|
-
claude-recall mcp restart #
|
|
370
|
+
claude-recall mcp restart # Stop server + print start instructions (a stdio server can't self-respawn; Claude Code restarts it on next session)
|
|
367
371
|
claude-recall mcp cleanup # Remove stale PID files
|
|
368
372
|
claude-recall mcp cleanup --all # Stop all servers
|
|
369
373
|
|
|
@@ -374,12 +378,9 @@ claude-recall project register # Register current project
|
|
|
374
378
|
claude-recall project unregister [id] # Unregister a project
|
|
375
379
|
claude-recall project clean # Remove stale registry entries
|
|
376
380
|
|
|
377
|
-
# ── Database
|
|
378
|
-
claude-recall
|
|
379
|
-
claude-recall
|
|
380
|
-
claude-recall migrate export # Export pre-migration backup
|
|
381
|
-
claude-recall migrate import # Import from backup
|
|
382
|
-
claude-recall migrate complete # Run pending migrations
|
|
381
|
+
# ── Database Maintenance ─────────────────────────────────────────────
|
|
382
|
+
claude-recall compact # Dedup + prune retention overflow + VACUUM (also runs automatically on MCP boot)
|
|
383
|
+
claude-recall compact --dry-run # Preview what compaction would remove
|
|
383
384
|
|
|
384
385
|
# ── Auto-Capture Hooks (run automatically, registered via setup --install) ──
|
|
385
386
|
claude-recall hook run correction-detector # UserPromptSubmit hook
|
|
@@ -443,6 +444,8 @@ Runtime behavior can be tuned via environment variables. Defaults are chosen so
|
|
|
443
444
|
| `CLAUDE_RECALL_COMPACT_THRESHOLD` | `10MB` | DB size at which automatic compaction kicks in. |
|
|
444
445
|
| `CLAUDE_RECALL_MAX_MEMORIES` | `10000` | Memory-row soft cap. |
|
|
445
446
|
| `CLAUDE_RECALL_ENFORCE_MODE` | `on` | Set to `off` to bypass the search-enforcer hook. |
|
|
447
|
+
| `CLAUDE_RECALL_LLM_TIMEOUT_MS` | `5000` | Timeout for hook-context LLM calls (classification, hindsight hints). Hooks fall back to regex when it fires. |
|
|
448
|
+
| `CLAUDE_RECALL_STOP_DEBOUNCE_MS` | `300000` | Debounce for the heavy Stop-hook pipeline (episodes, session extraction, promotion). Citations still scan every turn. `0` disables. |
|
|
446
449
|
|
|
447
450
|
---
|
|
448
451
|
|
|
@@ -42,7 +42,6 @@ const fs = __importStar(require("fs"));
|
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const os = __importStar(require("os"));
|
|
44
44
|
const pattern_service_1 = require("../services/pattern-service");
|
|
45
|
-
const migrate_1 = require("./commands/migrate");
|
|
46
45
|
const server_1 = require("../mcp/server");
|
|
47
46
|
const search_monitor_1 = require("../services/search-monitor");
|
|
48
47
|
const skill_generator_1 = require("../services/skill-generator");
|
|
@@ -678,19 +677,22 @@ class ClaudeRecallCLI {
|
|
|
678
677
|
async export(outputPath, options) {
|
|
679
678
|
const format = options.format || 'json';
|
|
680
679
|
try {
|
|
681
|
-
//
|
|
682
|
-
//
|
|
680
|
+
// Default: current project (+ universal/unscoped). --global dumps every
|
|
681
|
+
// project — use that before a global `clear --force --global`.
|
|
683
682
|
const projectId = config_1.ConfigService.getInstance().getProjectId();
|
|
684
|
-
const memories =
|
|
683
|
+
const memories = options.global
|
|
684
|
+
? this.memoryService.getAllMemories()
|
|
685
|
+
: this.memoryService.getAllByProject(projectId);
|
|
685
686
|
if (format === 'json') {
|
|
686
687
|
const exportData = {
|
|
687
688
|
version: '0.2.0',
|
|
688
689
|
exportDate: new Date().toISOString(),
|
|
690
|
+
scope: options.global ? 'all-projects' : projectId,
|
|
689
691
|
count: memories.length,
|
|
690
692
|
memories: memories
|
|
691
693
|
};
|
|
692
694
|
fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2));
|
|
693
|
-
console.log(`✅ Exported ${memories.length} memories to ${outputPath}`);
|
|
695
|
+
console.log(`✅ Exported ${memories.length} memories to ${outputPath}${options.global ? ' (all projects)' : ` (project: ${projectId})`}`);
|
|
694
696
|
}
|
|
695
697
|
else {
|
|
696
698
|
console.error(`❌ Unsupported format: ${format}`);
|
|
@@ -744,25 +746,44 @@ class ClaudeRecallCLI {
|
|
|
744
746
|
}
|
|
745
747
|
}
|
|
746
748
|
/**
|
|
747
|
-
* Clear memories
|
|
749
|
+
* Clear memories. Project-scoped by default; --global wipes ALL projects.
|
|
750
|
+
* Always writes a full-DB backup file first so a mistaken clear is recoverable.
|
|
748
751
|
*/
|
|
749
752
|
async clear(options) {
|
|
753
|
+
const projectId = options.global ? undefined : config_1.ConfigService.getInstance().getProjectId();
|
|
750
754
|
if (!options.force) {
|
|
751
|
-
|
|
755
|
+
if (options.global) {
|
|
756
|
+
console.log('⚠️ This will permanently delete memories from ALL projects.');
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
console.log(`⚠️ This will permanently delete memories for project "${projectId}".`);
|
|
760
|
+
console.log(' (Universal/unscoped memories shared across projects are kept.)');
|
|
761
|
+
console.log(' Use --global to clear every project.');
|
|
762
|
+
}
|
|
752
763
|
console.log('Use --force to confirm.');
|
|
753
764
|
return;
|
|
754
765
|
}
|
|
755
766
|
try {
|
|
767
|
+
// Safety net: copy the DB file before deleting anything
|
|
768
|
+
const dbPath = config_1.ConfigService.getInstance().getDatabasePath();
|
|
769
|
+
let backupPath = null;
|
|
770
|
+
if (fs.existsSync(dbPath)) {
|
|
771
|
+
backupPath = `${dbPath}.backup-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
772
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
773
|
+
}
|
|
756
774
|
const memoryService = memory_1.MemoryService.getInstance();
|
|
757
|
-
|
|
758
|
-
const
|
|
775
|
+
const count = memoryService.clear(options.type, projectId);
|
|
776
|
+
const scopeLabel = options.global ? 'all projects' : `project "${projectId}"`;
|
|
759
777
|
if (options.type) {
|
|
760
|
-
console.log(`✅ Cleared ${count} memories of type
|
|
778
|
+
console.log(`✅ Cleared ${count} memories of type ${options.type} (${scopeLabel})`);
|
|
761
779
|
}
|
|
762
780
|
else {
|
|
763
|
-
console.log(`✅ Cleared ${count} memories`);
|
|
781
|
+
console.log(`✅ Cleared ${count} memories (${scopeLabel})`);
|
|
764
782
|
}
|
|
765
|
-
|
|
783
|
+
if (backupPath) {
|
|
784
|
+
console.log(` Backup written to: ${backupPath}`);
|
|
785
|
+
}
|
|
786
|
+
this.logger.info('CLI', 'Clear completed', { type: options.type, project: projectId || 'all', count });
|
|
766
787
|
}
|
|
767
788
|
catch (error) {
|
|
768
789
|
console.error('❌ Clear failed:', error);
|
|
@@ -1013,7 +1034,8 @@ async function main() {
|
|
|
1013
1034
|
}
|
|
1014
1035
|
}
|
|
1015
1036
|
// Install skills + minimal enforcement hook
|
|
1016
|
-
|
|
1037
|
+
// TODO: honor force — currently `repair --reinstall-hooks` behaves identically to `setup --install`
|
|
1038
|
+
function installSkillsAndHook(_force = false) {
|
|
1017
1039
|
const cwd = process.cwd();
|
|
1018
1040
|
const projectName = path.basename(cwd);
|
|
1019
1041
|
console.log('\n📦 Claude Recall Setup\n');
|
|
@@ -1139,13 +1161,10 @@ async function main() {
|
|
|
1139
1161
|
hooks: [
|
|
1140
1162
|
{
|
|
1141
1163
|
type: "command",
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
type: "command",
|
|
1147
|
-
command: `${hookCmd} rule-injection-resolver`,
|
|
1148
|
-
timeout: 3
|
|
1164
|
+
// Both handlers share one process — each extra node invocation
|
|
1165
|
+
// cold-boots the CLI and reopens the DB on every tool call
|
|
1166
|
+
command: `${hookCmd} tool-outcome-watcher rule-injection-resolver`,
|
|
1167
|
+
timeout: 5
|
|
1149
1168
|
}
|
|
1150
1169
|
]
|
|
1151
1170
|
}
|
|
@@ -1155,13 +1174,8 @@ async function main() {
|
|
|
1155
1174
|
hooks: [
|
|
1156
1175
|
{
|
|
1157
1176
|
type: "command",
|
|
1158
|
-
command: `${hookCmd} tool-failure`,
|
|
1159
|
-
timeout:
|
|
1160
|
-
},
|
|
1161
|
-
{
|
|
1162
|
-
type: "command",
|
|
1163
|
-
command: `${hookCmd} rule-injection-resolver`,
|
|
1164
|
-
timeout: 3
|
|
1177
|
+
command: `${hookCmd} tool-failure rule-injection-resolver`,
|
|
1178
|
+
timeout: 5
|
|
1165
1179
|
}
|
|
1166
1180
|
]
|
|
1167
1181
|
}
|
|
@@ -1187,7 +1201,11 @@ async function main() {
|
|
|
1187
1201
|
hooks: [
|
|
1188
1202
|
{
|
|
1189
1203
|
type: "command",
|
|
1190
|
-
command: `${hookCmd} correction-detector
|
|
1204
|
+
command: `${hookCmd} correction-detector`,
|
|
1205
|
+
// Blocks prompt submission — must stay tight. The in-process
|
|
1206
|
+
// LLM call is capped at 5s (llm-classifier getClient), this
|
|
1207
|
+
// covers process startup on top.
|
|
1208
|
+
timeout: 8
|
|
1191
1209
|
}
|
|
1192
1210
|
]
|
|
1193
1211
|
}
|
|
@@ -1197,13 +1215,10 @@ async function main() {
|
|
|
1197
1215
|
hooks: [
|
|
1198
1216
|
{
|
|
1199
1217
|
type: "command",
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
type: "command",
|
|
1205
|
-
command: `${hookCmd} memory-sync`,
|
|
1206
|
-
timeout: 10
|
|
1218
|
+
// Sequential in one process; memory-sync runs after memory-stop
|
|
1219
|
+
// so it syncs the rules memory-stop just updated
|
|
1220
|
+
command: `${hookCmd} memory-stop memory-sync`,
|
|
1221
|
+
timeout: 40
|
|
1207
1222
|
}
|
|
1208
1223
|
]
|
|
1209
1224
|
}
|
|
@@ -1312,10 +1327,23 @@ async function main() {
|
|
|
1312
1327
|
process.exit(2);
|
|
1313
1328
|
}
|
|
1314
1329
|
try {
|
|
1330
|
+
// Interactive y/N confirmation when attached to a terminal. Without a
|
|
1331
|
+
// TTY (and without --auto), runRepair fails safe and applies nothing.
|
|
1332
|
+
const interactivePrompt = (!options.auto && !options.dryRun && process.stdin.isTTY)
|
|
1333
|
+
? (question) => new Promise((resolve) => {
|
|
1334
|
+
const readline = require('node:readline');
|
|
1335
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1336
|
+
rl.question(question, (answer) => {
|
|
1337
|
+
rl.close();
|
|
1338
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
1339
|
+
});
|
|
1340
|
+
})
|
|
1341
|
+
: undefined;
|
|
1315
1342
|
const result = await (0, repair_1.runRepair)({
|
|
1316
1343
|
auto: !!options.auto,
|
|
1317
1344
|
dryRun: !!options.dryRun,
|
|
1318
1345
|
scope,
|
|
1346
|
+
prompt: interactivePrompt,
|
|
1319
1347
|
});
|
|
1320
1348
|
process.exit(result.exitCode);
|
|
1321
1349
|
}
|
|
@@ -1722,7 +1750,6 @@ async function main() {
|
|
|
1722
1750
|
// Hook commands (automatic memory capture)
|
|
1723
1751
|
hook_commands_1.HookCommands.register(program);
|
|
1724
1752
|
// Migration commands
|
|
1725
|
-
migrate_1.MigrateCommand.register(program);
|
|
1726
1753
|
// Search command
|
|
1727
1754
|
program
|
|
1728
1755
|
.command('search <query>')
|
|
@@ -1849,8 +1876,9 @@ async function main() {
|
|
|
1849
1876
|
// Export command
|
|
1850
1877
|
program
|
|
1851
1878
|
.command('export <output>')
|
|
1852
|
-
.description('Export memories to file')
|
|
1879
|
+
.description('Export memories to file (current project by default)')
|
|
1853
1880
|
.option('-f, --format <format>', 'Export format (json)', 'json')
|
|
1881
|
+
.option('--global', 'Export memories from ALL projects')
|
|
1854
1882
|
.action(async (output, options) => {
|
|
1855
1883
|
const cli = new ClaudeRecallCLI(program.opts());
|
|
1856
1884
|
await cli.export(output, options);
|
|
@@ -1868,14 +1896,41 @@ async function main() {
|
|
|
1868
1896
|
// Clear command
|
|
1869
1897
|
program
|
|
1870
1898
|
.command('clear')
|
|
1871
|
-
.description('Clear memories')
|
|
1899
|
+
.description('Clear memories for the current project (use --global for all projects)')
|
|
1872
1900
|
.option('-t, --type <type>', 'Clear specific memory type')
|
|
1901
|
+
.option('--global', 'Clear memories from ALL projects, not just the current one')
|
|
1873
1902
|
.option('--force', 'Confirm deletion')
|
|
1874
1903
|
.action(async (options) => {
|
|
1875
1904
|
const cli = new ClaudeRecallCLI(program.opts());
|
|
1876
1905
|
await cli.clear(options);
|
|
1877
1906
|
process.exit(0);
|
|
1878
1907
|
});
|
|
1908
|
+
// Compact command — manual trigger for the same compaction that runs
|
|
1909
|
+
// automatically on MCP server boot when thresholds are exceeded.
|
|
1910
|
+
program
|
|
1911
|
+
.command('compact')
|
|
1912
|
+
.description('Compact the database: dedup identical memories, prune retention overflow, VACUUM')
|
|
1913
|
+
.option('--dry-run', 'Report what would be removed without changing anything')
|
|
1914
|
+
.action(async (options) => {
|
|
1915
|
+
try {
|
|
1916
|
+
const { DatabaseManager } = await Promise.resolve().then(() => __importStar(require('../services/database-manager')));
|
|
1917
|
+
const result = await DatabaseManager.getInstance().compact(!!options.dryRun);
|
|
1918
|
+
const savedMB = ((result.beforeSize - result.afterSize) / 1024 / 1024).toFixed(2);
|
|
1919
|
+
if (options.dryRun) {
|
|
1920
|
+
console.log(`Dry run: would deduplicate ${result.deduplicatedCount} and prune ${result.removedCount} memories.`);
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
console.log(`✅ Compaction done: deduplicated ${result.deduplicatedCount}, pruned ${result.removedCount}, saved ${savedMB}MB.`);
|
|
1924
|
+
if (result.backupPath)
|
|
1925
|
+
console.log(` Backup: ${result.backupPath}`);
|
|
1926
|
+
}
|
|
1927
|
+
process.exit(0);
|
|
1928
|
+
}
|
|
1929
|
+
catch (error) {
|
|
1930
|
+
console.error(`❌ Compaction failed: ${error.message}`);
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1879
1934
|
// Checkpoint command group
|
|
1880
1935
|
const checkpoint = program
|
|
1881
1936
|
.command('checkpoint')
|
|
@@ -35,104 +35,139 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.HookCommands = void 0;
|
|
37
37
|
const shared_1 = require("../../hooks/shared");
|
|
38
|
+
const AVAILABLE_HOOKS = [
|
|
39
|
+
'correction-detector',
|
|
40
|
+
'memory-stop',
|
|
41
|
+
'memory-sync',
|
|
42
|
+
'precompact-preserve',
|
|
43
|
+
'post-compact-reload',
|
|
44
|
+
'tool-outcome-watcher',
|
|
45
|
+
'tool-failure',
|
|
46
|
+
'rule-injector',
|
|
47
|
+
'rule-injection-resolver',
|
|
48
|
+
'subagent-start',
|
|
49
|
+
'subagent-stop',
|
|
50
|
+
'session-end-checkpoint',
|
|
51
|
+
'session-end-checkpoint-worker',
|
|
52
|
+
'bash-failure-watcher',
|
|
53
|
+
];
|
|
38
54
|
/**
|
|
39
55
|
* Hook CLI Commands
|
|
40
56
|
*
|
|
41
|
-
* Dispatches `claude-recall hook run <name
|
|
42
|
-
*
|
|
43
|
-
*
|
|
57
|
+
* Dispatches `claude-recall hook run <name...>` to the appropriate hook
|
|
58
|
+
* handler(s). Multiple names run sequentially in ONE process — each node
|
|
59
|
+
* invocation cold-boots the CLI and reopens the database, so hooks that fire
|
|
60
|
+
* on the same event (e.g. PostToolUse) should share an invocation.
|
|
61
|
+
* Reads stdin first (before dynamic import) then passes parsed input to the
|
|
62
|
+
* handlers. Always exits 0.
|
|
44
63
|
*/
|
|
45
64
|
class HookCommands {
|
|
65
|
+
static async runOne(name, input) {
|
|
66
|
+
switch (name) {
|
|
67
|
+
case 'correction-detector': {
|
|
68
|
+
const { handleCorrectionDetector } = await Promise.resolve().then(() => __importStar(require('../../hooks/correction-detector')));
|
|
69
|
+
await handleCorrectionDetector(input);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'memory-stop': {
|
|
73
|
+
const { handleMemoryStop } = await Promise.resolve().then(() => __importStar(require('../../hooks/memory-stop-hook')));
|
|
74
|
+
await handleMemoryStop(input);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case 'precompact-preserve': {
|
|
78
|
+
const { handlePrecompactPreserve } = await Promise.resolve().then(() => __importStar(require('../../hooks/precompact-preserve')));
|
|
79
|
+
await handlePrecompactPreserve(input);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'memory-sync': {
|
|
83
|
+
const { handleMemorySync } = await Promise.resolve().then(() => __importStar(require('../../hooks/memory-sync-hook')));
|
|
84
|
+
await handleMemorySync(input);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'tool-outcome-watcher': {
|
|
88
|
+
const { handleToolOutcomeWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
|
|
89
|
+
await handleToolOutcomeWatcher(input);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'tool-failure': {
|
|
93
|
+
const { handleToolFailure } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
|
|
94
|
+
await handleToolFailure(input);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'post-compact-reload': {
|
|
98
|
+
const { handlePostCompactReload } = await Promise.resolve().then(() => __importStar(require('../../hooks/post-compact-reload')));
|
|
99
|
+
await handlePostCompactReload(input);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'subagent-start': {
|
|
103
|
+
const { handleSubagentStart } = await Promise.resolve().then(() => __importStar(require('../../hooks/subagent-hooks')));
|
|
104
|
+
await handleSubagentStart(input);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'subagent-stop': {
|
|
108
|
+
const { handleSubagentStop } = await Promise.resolve().then(() => __importStar(require('../../hooks/subagent-hooks')));
|
|
109
|
+
await handleSubagentStop(input);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'bash-failure-watcher': {
|
|
113
|
+
// Backward compat alias — routes to tool-outcome-watcher
|
|
114
|
+
const { handleBashFailureWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
|
|
115
|
+
await handleBashFailureWatcher(input);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'session-end-checkpoint': {
|
|
119
|
+
const { handleSessionEndCheckpoint } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint')));
|
|
120
|
+
await handleSessionEndCheckpoint(input);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'session-end-checkpoint-worker': {
|
|
124
|
+
const { handleSessionEndCheckpointWorker } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint-worker')));
|
|
125
|
+
await handleSessionEndCheckpointWorker(input);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'rule-injector': {
|
|
129
|
+
const { handleRuleInjector } = await Promise.resolve().then(() => __importStar(require('../../hooks/rule-injector')));
|
|
130
|
+
await handleRuleInjector(input);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'rule-injection-resolver': {
|
|
134
|
+
const { handleRuleInjectionResolver } = await Promise.resolve().then(() => __importStar(require('../../hooks/rule-injection-resolver')));
|
|
135
|
+
await handleRuleInjectionResolver(input);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
console.error(`Unknown hook: ${name}`);
|
|
140
|
+
console.error(`Available: ${AVAILABLE_HOOKS.join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
46
143
|
static register(program) {
|
|
47
144
|
const hookCmd = program
|
|
48
145
|
.command('hook')
|
|
49
146
|
.description('Hook handlers for automatic memory capture');
|
|
50
147
|
hookCmd
|
|
51
|
-
.command('run <
|
|
52
|
-
.description(
|
|
53
|
-
.action(async (
|
|
148
|
+
.command('run <names...>')
|
|
149
|
+
.description(`Run one or more hook handlers in a single process (${AVAILABLE_HOOKS.slice(0, 3).join(' | ')} | ...)`)
|
|
150
|
+
.action(async (names) => {
|
|
151
|
+
// Read stdin synchronously BEFORE dynamic import to avoid data loss;
|
|
152
|
+
// every handler receives the same event payload.
|
|
153
|
+
let input;
|
|
54
154
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await handleCorrectionDetector(input);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case 'memory-stop': {
|
|
64
|
-
const { handleMemoryStop } = await Promise.resolve().then(() => __importStar(require('../../hooks/memory-stop-hook')));
|
|
65
|
-
await handleMemoryStop(input);
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
case 'precompact-preserve': {
|
|
69
|
-
const { handlePrecompactPreserve } = await Promise.resolve().then(() => __importStar(require('../../hooks/precompact-preserve')));
|
|
70
|
-
await handlePrecompactPreserve(input);
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
case 'memory-sync': {
|
|
74
|
-
const { handleMemorySync } = await Promise.resolve().then(() => __importStar(require('../../hooks/memory-sync-hook')));
|
|
75
|
-
await handleMemorySync(input);
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
case 'tool-outcome-watcher': {
|
|
79
|
-
const { handleToolOutcomeWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
|
|
80
|
-
await handleToolOutcomeWatcher(input);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
case 'tool-failure': {
|
|
84
|
-
const { handleToolFailure } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
|
|
85
|
-
await handleToolFailure(input);
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
case 'post-compact-reload': {
|
|
89
|
-
const { handlePostCompactReload } = await Promise.resolve().then(() => __importStar(require('../../hooks/post-compact-reload')));
|
|
90
|
-
await handlePostCompactReload(input);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
case 'subagent-start': {
|
|
94
|
-
const { handleSubagentStart } = await Promise.resolve().then(() => __importStar(require('../../hooks/subagent-hooks')));
|
|
95
|
-
await handleSubagentStart(input);
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case 'subagent-stop': {
|
|
99
|
-
const { handleSubagentStop } = await Promise.resolve().then(() => __importStar(require('../../hooks/subagent-hooks')));
|
|
100
|
-
await handleSubagentStop(input);
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case 'bash-failure-watcher': {
|
|
104
|
-
// Backward compat alias — routes to tool-outcome-watcher
|
|
105
|
-
const { handleBashFailureWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
|
|
106
|
-
await handleBashFailureWatcher(input);
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
case 'session-end-checkpoint': {
|
|
110
|
-
const { handleSessionEndCheckpoint } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint')));
|
|
111
|
-
await handleSessionEndCheckpoint(input);
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
case 'session-end-checkpoint-worker': {
|
|
115
|
-
const { handleSessionEndCheckpointWorker } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint-worker')));
|
|
116
|
-
await handleSessionEndCheckpointWorker(input);
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
case 'rule-injector': {
|
|
120
|
-
const { handleRuleInjector } = await Promise.resolve().then(() => __importStar(require('../../hooks/rule-injector')));
|
|
121
|
-
await handleRuleInjector(input);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case 'rule-injection-resolver': {
|
|
125
|
-
const { handleRuleInjectionResolver } = await Promise.resolve().then(() => __importStar(require('../../hooks/rule-injection-resolver')));
|
|
126
|
-
await handleRuleInjectionResolver(input);
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
default:
|
|
130
|
-
console.error(`Unknown hook: ${name}`);
|
|
131
|
-
console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, tool-outcome-watcher, session-end-checkpoint');
|
|
132
|
-
}
|
|
155
|
+
input = (0, shared_1.readStdin)();
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
(0, shared_1.hookLog)('hook-dispatcher', `stdin read failed: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
159
|
+
process.exit(0);
|
|
133
160
|
}
|
|
134
|
-
|
|
135
|
-
|
|
161
|
+
for (const name of names) {
|
|
162
|
+
try {
|
|
163
|
+
await HookCommands.runOne(name, input);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// Hooks must never block Claude — always exit 0. But a swallowed
|
|
167
|
+
// error with no log line means a hook can silently stop capturing
|
|
168
|
+
// for weeks, so record it.
|
|
169
|
+
(0, shared_1.hookLog)('hook-dispatcher', `${name}: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
170
|
+
}
|
|
136
171
|
}
|
|
137
172
|
process.exit(0);
|
|
138
173
|
});
|
|
@@ -442,7 +442,22 @@ async function runRepair(options = {}) {
|
|
|
442
442
|
reports,
|
|
443
443
|
};
|
|
444
444
|
}
|
|
445
|
-
if (!options.auto && !options.dryRun
|
|
445
|
+
if (!options.auto && !options.dryRun) {
|
|
446
|
+
// Fail safe: without a way to confirm (non-TTY and no prompt wired),
|
|
447
|
+
// never mutate settings files. Applying fixes requires explicit consent
|
|
448
|
+
// (interactive y/N) or an explicit --auto.
|
|
449
|
+
if (!options.prompt) {
|
|
450
|
+
log.log(`\n${totalFixable} fix(es) available but not applied: confirmation unavailable in non-interactive mode.`);
|
|
451
|
+
log.log('Re-run with --auto to apply safe fixes, or --dry-run to preview.\n');
|
|
452
|
+
return {
|
|
453
|
+
exitCode: 0,
|
|
454
|
+
filesScanned: files.length,
|
|
455
|
+
filesModified: 0,
|
|
456
|
+
fixesApplied: 0,
|
|
457
|
+
unfixable: totalUnfixable,
|
|
458
|
+
reports,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
446
461
|
const proceed = await options.prompt(`\nApply ${totalFixable} fix(es)? [y/N] `);
|
|
447
462
|
if (!proceed) {
|
|
448
463
|
log.log('Aborted. No files changed.\n');
|
|
@@ -298,7 +298,7 @@ function detectRetryLoops(interactions, consumed) {
|
|
|
298
298
|
list.push(ix);
|
|
299
299
|
counts.set(hash, list);
|
|
300
300
|
}
|
|
301
|
-
for (const
|
|
301
|
+
for (const ixList of counts.values()) {
|
|
302
302
|
if (ixList.length < 3)
|
|
303
303
|
continue;
|
|
304
304
|
// All already consumed?
|
|
@@ -58,6 +58,12 @@ Rules:
|
|
|
58
58
|
- If a text is a question, greeting, or code block, classify as "none"`;
|
|
59
59
|
/**
|
|
60
60
|
* Lazy-init the Anthropic client. Returns null if SDK or API key unavailable.
|
|
61
|
+
*
|
|
62
|
+
* Hook context: these calls run inside Claude Code hooks, some of them
|
|
63
|
+
* synchronously blocking the user's prompt (UserPromptSubmit). The SDK's
|
|
64
|
+
* defaults (10-minute timeout, 2 retries) would stall the session on a slow
|
|
65
|
+
* or hung connection, so we cap the timeout and disable retries — a missed
|
|
66
|
+
* classification degrades to the regex fallback, which is the right trade.
|
|
61
67
|
*/
|
|
62
68
|
function getClient() {
|
|
63
69
|
if (clientInstance !== undefined)
|
|
@@ -69,7 +75,11 @@ function getClient() {
|
|
|
69
75
|
try {
|
|
70
76
|
// Dynamic require to avoid hard failure when SDK not installed
|
|
71
77
|
const Anthropic = require('@anthropic-ai/sdk');
|
|
72
|
-
|
|
78
|
+
const timeoutMs = parseInt(process.env.CLAUDE_RECALL_LLM_TIMEOUT_MS || '5000', 10);
|
|
79
|
+
clientInstance = new Anthropic({
|
|
80
|
+
timeout: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 5000,
|
|
81
|
+
maxRetries: 0,
|
|
82
|
+
});
|
|
73
83
|
return clientInstance;
|
|
74
84
|
}
|
|
75
85
|
catch {
|
|
@@ -119,9 +129,9 @@ async function classifyWithLLM(text) {
|
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
/**
|
|
122
|
-
*
|
|
123
|
-
* Returns
|
|
124
|
-
*
|
|
132
|
+
* Extract a failure-specific hindsight lesson from a failure description.
|
|
133
|
+
* Returns null on any failure (no API key, network error, parse error) —
|
|
134
|
+
* callers fall back to grounding the detector's generic remedy text.
|
|
125
135
|
*/
|
|
126
136
|
async function extractHindsightHint(failureDescription, context) {
|
|
127
137
|
const client = getClient();
|