claude-recall 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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+** | required for better-sqlite3 |
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 four memory tools backed by a local SQLite database with WAL mode, content-hash deduplication, and automatic compaction. The tools are exposed differently depending on the agent:
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 all memories to JSON
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 all memories (irreversible)
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 all memories
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 # Restart server
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 Migration ──────────────────────────────────────────────
378
- claude-recall migrate check # Check if migration needed
379
- claude-recall migrate schema # Show current schema version
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
- // Export all memories from the current project (+ universal). For a
682
- // true cross-project dump, the user can run `--project` per project.
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 = this.memoryService.getAllByProject(projectId);
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
- console.log('⚠️ This will permanently delete memories.');
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
- // Actually clear the memories
758
- const count = memoryService.clear(options.type);
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: ${options.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
- this.logger.info('CLI', 'Clear completed', { type: options.type, count });
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
- function installSkillsAndHook(force = false) {
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
- command: `${hookCmd} tool-outcome-watcher`,
1143
- timeout: 3
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: 3
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
- command: `${hookCmd} memory-stop`,
1201
- timeout: 30
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>` to the appropriate hook handler.
42
- * Reads stdin first (before dynamic import) then passes parsed input to the handler.
43
- * Always exits 0.
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 <name>')
52
- .description('Run a hook handler (correction-detector | memory-stop | precompact-preserve)')
53
- .action(async (name) => {
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
- // Read stdin synchronously BEFORE dynamic import to avoid data loss
56
- const input = (0, shared_1.readStdin)();
57
- switch (name) {
58
- case 'correction-detector': {
59
- const { handleCorrectionDetector } = await Promise.resolve().then(() => __importStar(require('../../hooks/correction-detector')));
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
- catch {
135
- // Hooks must never block Claude — always exit 0
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 && options.prompt) {
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 [hash, ixList] of counts) {
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
- clientInstance = new Anthropic();
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
- * Classify multiple texts in a single Haiku API call.
123
- * Returns an array of results (null for unclassifiable items).
124
- * Falls back to null array on total failure (caller should use regex).
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();