claude-recall 0.24.2 → 0.25.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 CHANGED
@@ -291,7 +291,9 @@ tail -20 ~/.claude-recall/hook-logs/memory-stop.log
291
291
  tail -20 ~/.claude-recall/hook-logs/correction-detector.log
292
292
 
293
293
  # "Something is broken, start fresh"
294
- claude-recall repair # Clean up old hooks, reinstall skills
294
+ claude-recall repair # Conservative: fix broken hook paths in settings.json (preserves your customizations)
295
+ claude-recall repair --dry-run # Preview what repair would change
296
+ claude-recall repair --reinstall-hooks # Opinionated: rewrite entire hook block from current template
295
297
  claude-recall setup --install # Reinstall skills + hooks
296
298
  claude-recall mcp cleanup --all # Stop all stale MCP servers
297
299
  ```
@@ -305,7 +307,11 @@ claude-recall setup # Show activation instructions
305
307
  claude-recall setup --install # Install skills + hooks
306
308
  claude-recall upgrade # One-shot upgrade: global binary + clear stale MCP servers
307
309
  claude-recall status # Installation and system status
308
- claude-recall repair # Clean up old hooks, install skills
310
+ claude-recall repair # Fix broken claude-recall hook paths (conservative: preserves user customizations)
311
+ claude-recall repair --auto # Non-interactive; apply safe fixes without prompting (used by postinstall)
312
+ claude-recall repair --dry-run # Report what would change without writing
313
+ claude-recall repair --scope user|project|all # Scope the scan: user (~/.claude), project (closest .claude walking up from cwd), all (user + every nested project under ~). Default: all
314
+ claude-recall repair --reinstall-hooks # Opinionated: rewrite entire hook block from current template
309
315
  claude-recall hooks check # Verify hook files exist and are valid
310
316
  claude-recall hooks test-enforcement # Test if search enforcer hook works
311
317
 
@@ -49,6 +49,7 @@ const skill_generator_1 = require("../services/skill-generator");
49
49
  const mcp_commands_1 = require("./commands/mcp-commands");
50
50
  const project_commands_1 = require("./commands/project-commands");
51
51
  const hook_commands_1 = require("./commands/hook-commands");
52
+ const repair_1 = require("./commands/repair");
52
53
  const program = new commander_1.Command();
53
54
  class ClaudeRecallCLI {
54
55
  constructor(options) {
@@ -1289,14 +1290,41 @@ async function main() {
1289
1290
  }
1290
1291
  process.exit(0);
1291
1292
  });
1292
- // Repair command - cleans up old hooks and installs skills
1293
+ // Repair command conservative by default: fix broken hook paths without
1294
+ // touching user customizations. --reinstall-hooks (or legacy --force) runs
1295
+ // the opinionated installer that rewrites the entire hook block from template.
1293
1296
  program
1294
1297
  .command('repair')
1295
- .description('Clean up old hooks and install skills (v0.9.0+ migration)')
1296
- .option('--force', 'Force overwrite existing configuration')
1297
- .action((options) => {
1298
- installSkillsAndHook(options.force || false);
1299
- process.exit(0);
1298
+ .description('Fix broken claude-recall hook paths in settings.json (conservative)')
1299
+ .option('--auto', 'Non-interactive; apply safe fixes without prompting')
1300
+ .option('--dry-run', 'Report what would change without writing any files')
1301
+ .option('--reinstall-hooks', 'Rewrite the entire hook block from current template (opinionated)')
1302
+ .option('--scope <scope>', 'user | project | all', 'all')
1303
+ .option('--force', '[deprecated alias for --reinstall-hooks]')
1304
+ .action(async (options) => {
1305
+ if (options.reinstallHooks || options.force) {
1306
+ installSkillsAndHook(true);
1307
+ process.exit(0);
1308
+ }
1309
+ const scope = (options.scope || 'all');
1310
+ if (!['user', 'project', 'all'].includes(scope)) {
1311
+ console.error(`Invalid --scope: ${scope}. Use user, project, or all.`);
1312
+ process.exit(2);
1313
+ }
1314
+ try {
1315
+ const result = await (0, repair_1.runRepair)({
1316
+ auto: !!options.auto,
1317
+ dryRun: !!options.dryRun,
1318
+ scope,
1319
+ });
1320
+ process.exit(result.exitCode);
1321
+ }
1322
+ catch (err) {
1323
+ // Never crash postinstall. Log and exit 0 on unexpected errors so
1324
+ // `npm install` continues. Surface via non-zero only for interactive runs.
1325
+ console.error(`repair: unexpected error: ${err.message}`);
1326
+ process.exit(options.auto ? 0 : 1);
1327
+ }
1300
1328
  });
1301
1329
  // Check hooks function
1302
1330
  function checkHooks() {
@@ -0,0 +1,493 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveOnPath = resolveOnPath;
37
+ exports.classifyHook = classifyHook;
38
+ exports.findHomeProjectSettings = findHomeProjectSettings;
39
+ exports.findSettingsFiles = findSettingsFiles;
40
+ exports.scanFile = scanFile;
41
+ exports.applyFixes = applyFixes;
42
+ exports.runRepair = runRepair;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const CLAUDE_RECALL_CLI_RE = /claude[-_]recall[-_]cli(?:\.js)?/i;
47
+ const HOOK_RUN_ID_RE = /hook\s+run\s+(\S+)/i;
48
+ /**
49
+ * Resolve a binary name against PATH. POSIX-first; on Windows tries common
50
+ * extensions. Returns the first matching absolute path, or null.
51
+ */
52
+ function resolveOnPath(binName) {
53
+ const pathEnv = process.env.PATH || '';
54
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
55
+ for (const dir of pathEnv.split(path.delimiter)) {
56
+ if (!dir)
57
+ continue;
58
+ for (const ext of exts) {
59
+ const candidate = path.join(dir, binName + ext);
60
+ try {
61
+ const st = fs.statSync(candidate);
62
+ if (st.isFile())
63
+ return candidate;
64
+ }
65
+ catch {
66
+ // not present — keep looking
67
+ }
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Decide whether a hook command belongs to claude-recall and, if so, whether
74
+ * its invocation target actually resolves on disk / on PATH. Pure function —
75
+ * no I/O except for the filesystem check on absolute script paths and the
76
+ * PATH probe (which the caller can stub via `claudeRecallResolver`).
77
+ */
78
+ function classifyHook(command, claudeRecallResolver) {
79
+ const trimmed = command.trim();
80
+ if (!trimmed)
81
+ return { status: 'non-claude-recall' };
82
+ const tokens = trimmed.split(/\s+/);
83
+ const first = tokens[0] || '';
84
+ const base = path.basename(first);
85
+ const looksLikeCR = CLAUDE_RECALL_CLI_RE.test(trimmed) ||
86
+ base === 'claude-recall' ||
87
+ /\bclaude-recall\b/.test(trimmed);
88
+ if (!looksLikeCR)
89
+ return { status: 'non-claude-recall' };
90
+ const hookIdMatch = trimmed.match(HOOK_RUN_ID_RE);
91
+ const hookId = hookIdMatch ? hookIdMatch[1] : null;
92
+ // Case A: `node /abs/path/to/claude-recall-cli.js hook run ...`
93
+ // or `/abs/path/to/node /abs/path/.../claude-recall-cli.js ...`
94
+ if (base === 'node' || /\/node$/.test(first)) {
95
+ const script = tokens[1];
96
+ if (script && path.isAbsolute(script) && CLAUDE_RECALL_CLI_RE.test(script)) {
97
+ if (!fs.existsSync(script)) {
98
+ return { status: 'broken-absolute', scriptPath: script, hookId };
99
+ }
100
+ return { status: 'ok' };
101
+ }
102
+ // Unusual node invocation we don't recognize — don't touch.
103
+ return { status: 'ok' };
104
+ }
105
+ // Case B: `claude-recall hook run ...` (PATH-resolved form — what we rewrite to)
106
+ if (base === 'claude-recall') {
107
+ if (claudeRecallResolver())
108
+ return { status: 'ok' };
109
+ return { status: 'broken-path', binary: 'claude-recall', hookId };
110
+ }
111
+ // Case C: `npx claude-recall ...` — npx resolves at runtime; treat as OK.
112
+ if (base === 'npx')
113
+ return { status: 'ok' };
114
+ // Anything else mentioning claude-recall — leave alone.
115
+ return { status: 'ok' };
116
+ }
117
+ /**
118
+ * Given the closest settings.json-like file path, return the list of paths
119
+ * to scan (settings.json + settings.local.json if present).
120
+ */
121
+ function pickSiblings(settingsPath) {
122
+ const dir = path.dirname(settingsPath);
123
+ const out = [];
124
+ for (const name of ['settings.json', 'settings.local.json']) {
125
+ const p = path.join(dir, name);
126
+ if (fs.existsSync(p))
127
+ out.push(p);
128
+ }
129
+ return out;
130
+ }
131
+ // Directory names that are guaranteed not to contain a project's `.claude/`
132
+ // dir but tend to be huge. Pruning them keeps the home walk fast even on
133
+ // machines with sprawling node_modules/cache trees.
134
+ const HOME_WALK_PRUNE = new Set([
135
+ 'node_modules',
136
+ '.git',
137
+ '.npm',
138
+ '.nvm',
139
+ '.cache',
140
+ '.pnpm-store',
141
+ '.yarn',
142
+ '.docker',
143
+ '.local',
144
+ '.cargo',
145
+ '.rustup',
146
+ '.gradle',
147
+ '.m2',
148
+ '.vscode-server',
149
+ '.cursor-server',
150
+ 'Library',
151
+ 'AppData',
152
+ 'dist',
153
+ 'build',
154
+ 'target',
155
+ '__pycache__',
156
+ '.venv',
157
+ 'venv',
158
+ '.tox',
159
+ '.mypy_cache',
160
+ '.pytest_cache',
161
+ '.next',
162
+ '.turbo',
163
+ ]);
164
+ const HOME_WALK_MAX_DEPTH = 8;
165
+ /**
166
+ * Walk $HOME (excluding the user-global ~/.claude/ itself) for every nested
167
+ * `.claude/settings.json` and `.claude/settings.local.json`. Conservative on
168
+ * descent — never follows symlinks, prunes well-known bloat dirs, depth-limited.
169
+ * Used by --scope all to find every project under home that may have stale
170
+ * claude-recall hook paths after an install moves.
171
+ */
172
+ function findHomeProjectSettings(home) {
173
+ const results = [];
174
+ const userClaudeDir = path.join(home, '.claude');
175
+ function walk(dir, depth) {
176
+ if (depth > HOME_WALK_MAX_DEPTH)
177
+ return;
178
+ let entries;
179
+ try {
180
+ entries = fs.readdirSync(dir, { withFileTypes: true });
181
+ }
182
+ catch {
183
+ return; // permission denied, gone, etc. — silently skip
184
+ }
185
+ for (const entry of entries) {
186
+ // Don't follow symlinks (loop avoidance, also unlikely to host the
187
+ // canonical project root we want to scan).
188
+ if (entry.isSymbolicLink())
189
+ continue;
190
+ if (entry.isDirectory()) {
191
+ if (HOME_WALK_PRUNE.has(entry.name))
192
+ continue;
193
+ const child = path.join(dir, entry.name);
194
+ if (entry.name === '.claude') {
195
+ // Skip the user-global ~/.claude/ — that's covered by --scope user.
196
+ if (child === userClaudeDir)
197
+ continue;
198
+ for (const f of pickSiblings(path.join(child, 'settings.json'))) {
199
+ if (!results.includes(f))
200
+ results.push(f);
201
+ }
202
+ // Don't descend into .claude/ — settings files live at its root.
203
+ continue;
204
+ }
205
+ walk(child, depth + 1);
206
+ }
207
+ }
208
+ }
209
+ walk(home, 0);
210
+ return results;
211
+ }
212
+ function findSettingsFiles(cwd, home, scope) {
213
+ const results = [];
214
+ if (scope === 'user' || scope === 'all') {
215
+ const userDir = path.join(home, '.claude');
216
+ for (const p of pickSiblings(path.join(userDir, 'settings.json'))) {
217
+ if (!results.includes(p))
218
+ results.push(p);
219
+ }
220
+ }
221
+ if (scope === 'all') {
222
+ // For --scope all we walk the entire home tree to catch every project's
223
+ // .claude/settings.json — important for postinstall, where stale hook paths
224
+ // in ANY project on the machine break Claude Code in that project.
225
+ for (const p of findHomeProjectSettings(home)) {
226
+ if (!results.includes(p))
227
+ results.push(p);
228
+ }
229
+ }
230
+ else if (scope === 'project') {
231
+ // For --scope project we only walk up from cwd looking for the CLOSEST
232
+ // .claude dir (matches Claude Code's own resolution). We don't scan
233
+ // ancestors beyond the first match — those belong to other projects.
234
+ let dir = cwd;
235
+ while (dir !== path.dirname(dir)) {
236
+ const claudeDir = path.join(dir, '.claude');
237
+ const s = path.join(claudeDir, 'settings.json');
238
+ const l = path.join(claudeDir, 'settings.local.json');
239
+ const hasAny = fs.existsSync(s) || fs.existsSync(l);
240
+ if (hasAny) {
241
+ if (fs.existsSync(s) && !results.includes(s))
242
+ results.push(s);
243
+ if (fs.existsSync(l) && !results.includes(l))
244
+ results.push(l);
245
+ break;
246
+ }
247
+ dir = path.dirname(dir);
248
+ }
249
+ }
250
+ return results;
251
+ }
252
+ function scanFile(settingsPath, claudeRecallResolver) {
253
+ const report = { settingsPath, findings: [] };
254
+ let raw;
255
+ try {
256
+ raw = fs.readFileSync(settingsPath, 'utf8');
257
+ }
258
+ catch (e) {
259
+ report.parseError = `cannot read: ${e.message}`;
260
+ return report;
261
+ }
262
+ let parsed;
263
+ try {
264
+ parsed = JSON.parse(raw);
265
+ }
266
+ catch (e) {
267
+ report.parseError = `invalid JSON: ${e.message}`;
268
+ return report;
269
+ }
270
+ if (typeof parsed.hooksVersion === 'string') {
271
+ report.hooksVersion = parsed.hooksVersion;
272
+ }
273
+ const hooks = parsed.hooks;
274
+ if (!hooks || typeof hooks !== 'object')
275
+ return report;
276
+ const hasCR = () => claudeRecallResolver();
277
+ // Cache resolver result within a single scan to avoid hammering stat().
278
+ let cached;
279
+ const cachingResolver = () => {
280
+ if (cached === undefined)
281
+ cached = hasCR();
282
+ return cached ?? null;
283
+ };
284
+ for (const [event, groups] of Object.entries(hooks)) {
285
+ if (!Array.isArray(groups))
286
+ continue;
287
+ groups.forEach((group, groupIndex) => {
288
+ if (!group || !Array.isArray(group.hooks))
289
+ return;
290
+ group.hooks.forEach((hook, hookIndex) => {
291
+ if (!hook || typeof hook.command !== 'string')
292
+ return;
293
+ const classification = classifyHook(hook.command, cachingResolver);
294
+ if (classification.status === 'non-claude-recall')
295
+ return;
296
+ const finding = {
297
+ location: { settingsPath, event, groupIndex, hookIndex },
298
+ originalCommand: hook.command,
299
+ classification,
300
+ };
301
+ if (classification.status === 'broken-absolute') {
302
+ const crPath = cachingResolver();
303
+ if (crPath) {
304
+ const id = classification.hookId;
305
+ if (id) {
306
+ finding.proposedCommand = `claude-recall hook run ${id}`;
307
+ }
308
+ // If we can't extract a hook id we don't know what subcommand to
309
+ // invoke. Leave proposedCommand unset — reported as unfixable.
310
+ }
311
+ }
312
+ report.findings.push(finding);
313
+ });
314
+ });
315
+ }
316
+ return report;
317
+ }
318
+ function applyFixes(report, opts) {
319
+ if (report.parseError)
320
+ return { changed: false, applied: 0, backupPath: null };
321
+ const fixable = report.findings.filter(f => f.proposedCommand);
322
+ if (fixable.length === 0)
323
+ return { changed: false, applied: 0, backupPath: null };
324
+ const raw = fs.readFileSync(report.settingsPath, 'utf8');
325
+ const parsed = JSON.parse(raw);
326
+ if (!parsed.hooks)
327
+ return { changed: false, applied: 0, backupPath: null };
328
+ let applied = 0;
329
+ for (const f of fixable) {
330
+ const { event, groupIndex, hookIndex } = f.location;
331
+ const group = parsed.hooks[event]?.[groupIndex];
332
+ const entry = group?.hooks?.[hookIndex];
333
+ if (!entry)
334
+ continue;
335
+ // Sanity: only rewrite if the command still matches what we scanned.
336
+ if (entry.command !== f.originalCommand)
337
+ continue;
338
+ entry.command = f.proposedCommand;
339
+ applied++;
340
+ }
341
+ if (applied === 0)
342
+ return { changed: false, applied: 0, backupPath: null };
343
+ if (opts.dryRun) {
344
+ return { changed: true, applied, backupPath: null };
345
+ }
346
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
347
+ const backupPath = `${report.settingsPath}.bak.${ts}`;
348
+ fs.writeFileSync(backupPath, raw);
349
+ fs.writeFileSync(report.settingsPath, JSON.stringify(parsed, null, 2));
350
+ return { changed: true, applied, backupPath };
351
+ }
352
+ function classify(finding) {
353
+ const c = finding.classification;
354
+ if (c.status === 'ok' || c.status === 'non-claude-recall')
355
+ return 'ok';
356
+ return finding.proposedCommand ? 'fixable' : 'unfixable';
357
+ }
358
+ function describe(finding) {
359
+ const c = finding.classification;
360
+ const loc = `${finding.location.event}[${finding.location.groupIndex}].hooks[${finding.location.hookIndex}]`;
361
+ if (c.status === 'broken-absolute') {
362
+ return `${loc} missing script: ${c.scriptPath}`;
363
+ }
364
+ if (c.status === 'broken-path') {
365
+ return `${loc} '${c.binary}' not on PATH`;
366
+ }
367
+ return `${loc} ok`;
368
+ }
369
+ async function runRepair(options = {}) {
370
+ const log = options.logger ?? { log: console.log.bind(console), warn: console.warn.bind(console) };
371
+ const cwd = options.cwd ?? process.cwd();
372
+ const home = options.home ?? os.homedir();
373
+ const scope = options.scope ?? 'all';
374
+ const resolver = options.claudeRecallOnPath ?? (() => resolveOnPath('claude-recall'));
375
+ log.log('\n🩺 Claude Recall repair (conservative)\n');
376
+ const files = findSettingsFiles(cwd, home, scope);
377
+ if (files.length === 0) {
378
+ log.log(`No settings files found (scope: ${scope}).`);
379
+ log.log('Nothing to repair. If you meant to install hooks, run:');
380
+ log.log(' claude-recall setup --install\n');
381
+ return { exitCode: 0, filesScanned: 0, filesModified: 0, fixesApplied: 0, unfixable: 0, reports: [] };
382
+ }
383
+ const reports = [];
384
+ let totalFixable = 0;
385
+ let totalUnfixable = 0;
386
+ let totalOk = 0;
387
+ for (const f of files) {
388
+ const report = scanFile(f, resolver);
389
+ reports.push(report);
390
+ if (report.parseError) {
391
+ log.warn(` ⚠ ${f}: ${report.parseError}`);
392
+ continue;
393
+ }
394
+ let fixable = 0, unfixable = 0, ok = 0;
395
+ for (const finding of report.findings) {
396
+ const kind = classify(finding);
397
+ if (kind === 'fixable')
398
+ fixable++;
399
+ else if (kind === 'unfixable')
400
+ unfixable++;
401
+ else
402
+ ok++;
403
+ }
404
+ totalFixable += fixable;
405
+ totalUnfixable += unfixable;
406
+ totalOk += ok;
407
+ const versionTag = report.hooksVersion ? ` (hooksVersion: ${report.hooksVersion})` : '';
408
+ log.log(` ${f}${versionTag}`);
409
+ log.log(` ${ok} OK, ${fixable} fixable, ${unfixable} unfixable`);
410
+ for (const finding of report.findings) {
411
+ if (classify(finding) === 'ok')
412
+ continue;
413
+ log.log(` - ${describe(finding)}`);
414
+ if (finding.proposedCommand) {
415
+ log.log(` proposed: ${finding.proposedCommand}`);
416
+ }
417
+ }
418
+ }
419
+ if (totalFixable === 0) {
420
+ if (totalUnfixable > 0) {
421
+ log.log(`\n${totalUnfixable} broken claude-recall hook(s) found but no safe fix available.`);
422
+ log.log('Install claude-recall on PATH so repair can rewrite the broken paths:');
423
+ log.log(' npm install -g claude-recall\n');
424
+ // Don't fail postinstall — user's current install was fine until their
425
+ // PATH/hook config drifted; this is diagnostic, not an error.
426
+ return {
427
+ exitCode: 0,
428
+ filesScanned: files.length,
429
+ filesModified: 0,
430
+ fixesApplied: 0,
431
+ unfixable: totalUnfixable,
432
+ reports,
433
+ };
434
+ }
435
+ log.log(`\n✅ All ${totalOk} claude-recall hook(s) look healthy. Nothing to do.\n`);
436
+ return {
437
+ exitCode: 0,
438
+ filesScanned: files.length,
439
+ filesModified: 0,
440
+ fixesApplied: 0,
441
+ unfixable: 0,
442
+ reports,
443
+ };
444
+ }
445
+ if (!options.auto && !options.dryRun && options.prompt) {
446
+ const proceed = await options.prompt(`\nApply ${totalFixable} fix(es)? [y/N] `);
447
+ if (!proceed) {
448
+ log.log('Aborted. No files changed.\n');
449
+ return {
450
+ exitCode: 0,
451
+ filesScanned: files.length,
452
+ filesModified: 0,
453
+ fixesApplied: 0,
454
+ unfixable: totalUnfixable,
455
+ reports,
456
+ };
457
+ }
458
+ }
459
+ let filesModified = 0;
460
+ let fixesApplied = 0;
461
+ for (const report of reports) {
462
+ const { changed, applied, backupPath } = applyFixes(report, { dryRun: !!options.dryRun });
463
+ if (changed && !options.dryRun) {
464
+ filesModified++;
465
+ fixesApplied += applied;
466
+ log.log(` ✓ ${report.settingsPath}: applied ${applied} fix(es)`);
467
+ if (backupPath)
468
+ log.log(` backup: ${backupPath}`);
469
+ }
470
+ else if (changed && options.dryRun) {
471
+ fixesApplied += applied;
472
+ log.log(` (dry-run) ${report.settingsPath}: would apply ${applied} fix(es)`);
473
+ }
474
+ }
475
+ if (options.dryRun) {
476
+ log.log(`\nDry run complete. ${fixesApplied} fix(es) would be applied across ${reports.filter(r => r.findings.some(f => f.proposedCommand)).length} file(s).\n`);
477
+ }
478
+ else {
479
+ log.log(`\n✅ Repaired ${fixesApplied} hook(s) across ${filesModified} file(s).`);
480
+ if (totalUnfixable > 0) {
481
+ log.log(` ${totalUnfixable} issue(s) still need manual attention (see above).`);
482
+ }
483
+ log.log('');
484
+ }
485
+ return {
486
+ exitCode: 0,
487
+ filesScanned: files.length,
488
+ filesModified,
489
+ fixesApplied,
490
+ unfixable: totalUnfixable,
491
+ reports,
492
+ };
493
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "description": "Persistent memory for Claude Code and Pi with native Skills integration, automatic capture, failure learning, and project scoping",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -117,6 +117,33 @@ try {
117
117
  // `claude-recall setup` invocation by the user, which is conscious and
118
118
  // produces a diff the user can see.
119
119
 
120
+ // Conservative repair on upgrade: fix broken absolute hook paths in
121
+ // ~/.claude/settings.json AND every project's .claude/settings.json under
122
+ // the user's home. Common when node/nvm versions change, or when the package
123
+ // was reinstalled into a different location (e.g. moving from a root-owned
124
+ // global prefix to ~/.npm-global). The --auto --scope all flags mean:
125
+ // • user-global settings AND every nested project settings file are scanned
126
+ // • only commands pointing at MISSING absolute scripts get rewritten
127
+ // • user customizations (timeouts, matchers, sibling hooks) preserved
128
+ // • writes a .bak.<timestamp> before any change
129
+ // • never installs hooks where none exist — satisfies the "don't clobber"
130
+ // rule above
131
+ // Timeout raised because the home walk can touch many directories on
132
+ // larger machines.
133
+ try {
134
+ const cliPath = path.join(__dirname, '..', 'dist', 'cli', 'claude-recall-cli.js');
135
+ if (fs.existsSync(cliPath)) {
136
+ execSync(`node "${cliPath}" repair --auto --scope all`, {
137
+ stdio: 'inherit',
138
+ timeout: 60000
139
+ });
140
+ }
141
+ } catch (repairError) {
142
+ // Non-fatal: postinstall must never fail the npm install. Any repair
143
+ // problem can be fixed manually with `claude-recall repair`.
144
+ console.log('⚠️ Auto-repair skipped (non-fatal):', repairError.message);
145
+ }
146
+
120
147
  console.log('\n✅ Installation complete!\n');
121
148
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
122
149
  console.log('📌 ACTIVATE CLAUDE RECALL:');