claude-recall 0.24.1 โ†’ 0.25.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
@@ -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 (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) {
@@ -391,43 +392,51 @@ class ClaudeRecallCLI {
391
392
  }
392
393
  console.log(`Installed: ${current}`);
393
394
  console.log(`Latest: ${latest}`);
394
- if (current === latest) {
395
- console.log('\nโœ“ Already up to date.');
396
- return;
395
+ const needsInstall = current !== latest;
396
+ if (needsInstall) {
397
+ console.log(`\n๐Ÿ“ฆ Upgrading ${current} โ†’ ${latest}...\n`);
398
+ // Run npm install -g, streaming output so the user sees progress / errors live
399
+ const install = spawnSync('npm', ['install', '-g', 'claude-recall@latest'], {
400
+ stdio: 'inherit',
401
+ });
402
+ if (install.status !== 0) {
403
+ // npm prints its own error โ€” add the practical remediation on top
404
+ console.error('\nโŒ Install failed.');
405
+ console.error('\nMost common cause: your global npm prefix is owned by root (EACCES).');
406
+ console.error('\nQuick fix:');
407
+ console.error(' sudo npm install -g claude-recall');
408
+ console.error('\nPermanent fix (no more sudo for any global install on this machine):');
409
+ console.error(' mkdir -p ~/.npm-global');
410
+ console.error(" npm config set prefix ~/.npm-global");
411
+ console.error(" echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc");
412
+ console.error(' source ~/.bashrc');
413
+ console.error('\nThen re-run: claude-recall upgrade');
414
+ process.exit(install.status ?? 1);
415
+ }
416
+ // Kill any running MCP servers so Claude Code respawns them with the new binary
417
+ console.log('\n๐Ÿงน Cleaning up running MCP servers (Claude Code respawns them on next tool call)...');
418
+ try {
419
+ spawnSync('claude-recall', ['mcp', 'cleanup', '--all'], { stdio: 'inherit' });
420
+ }
421
+ catch {
422
+ // Non-fatal โ€” the user can restart Claude Code manually if this fails
423
+ }
397
424
  }
398
- console.log(`\n๐Ÿ“ฆ Upgrading ${current} โ†’ ${latest}...\n`);
399
- // Run npm install -g, streaming output so the user sees progress / errors live
400
- const install = spawnSync('npm', ['install', '-g', 'claude-recall@latest'], {
401
- stdio: 'inherit',
402
- });
403
- if (install.status !== 0) {
404
- // npm prints its own error โ€” add the practical remediation on top
405
- console.error('\nโŒ Install failed.');
406
- console.error('\nMost common cause: your global npm prefix is owned by root (EACCES).');
407
- console.error('\nQuick fix:');
408
- console.error(' sudo npm install -g claude-recall');
409
- console.error('\nPermanent fix (no more sudo for any global install on this machine):');
410
- console.error(' mkdir -p ~/.npm-global');
411
- console.error(" npm config set prefix ~/.npm-global");
412
- console.error(" echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc");
413
- console.error(' source ~/.bashrc');
414
- console.error('\nThen re-run: claude-recall upgrade');
415
- process.exit(install.status ?? 1);
416
- }
417
- // Kill any running MCP servers so Claude Code respawns them with the new binary
418
- console.log('\n๐Ÿงน Cleaning up running MCP servers (Claude Code respawns them on next tool call)...');
419
- try {
420
- spawnSync('claude-recall', ['mcp', 'cleanup', '--all'], { stdio: 'inherit' });
425
+ // Detect stale project-local installs that would shadow the global binary
426
+ // when invoked via `npx claude-recall`. Runs on every upgrade invocation,
427
+ // including when already up-to-date โ€” so `claude-recall upgrade` doubles
428
+ // as a diagnostic for the npx-walks-up-and-finds-stale-local trap, and
429
+ // `claude-recall upgrade --clean-locals` works as a one-shot cleanup
430
+ // command even when no version change is pending.
431
+ this.warnOrCleanStaleLocals(latest, opts.cleanLocals === true);
432
+ if (needsInstall) {
433
+ console.log(`\nโœ“ Upgraded to ${latest}. No need to re-run \`claude mcp add\` โ€” existing`);
434
+ console.log(' registrations point at the `claude-recall` command and pick up the new');
435
+ console.log(' binary automatically. Just run any tool in Claude Code.');
421
436
  }
422
- catch {
423
- // Non-fatal โ€” the user can restart Claude Code manually if this fails
437
+ else {
438
+ console.log('\nโœ“ Already up to date.');
424
439
  }
425
- // Detect stale project-local installs that would shadow the just-installed
426
- // global binary when invoked via `npx claude-recall`.
427
- this.warnOrCleanStaleLocals(latest, opts.cleanLocals === true);
428
- console.log(`\nโœ“ Upgraded to ${latest}. No need to re-run \`claude mcp add\` โ€” existing`);
429
- console.log(' registrations point at the `claude-recall` command and pick up the new');
430
- console.log(' binary automatically. Just run any tool in Claude Code.');
431
440
  }
432
441
  /**
433
442
  * Demote rules loaded often but never cited โ€” excludes them from future load_rules payloads.
@@ -1281,14 +1290,41 @@ async function main() {
1281
1290
  }
1282
1291
  process.exit(0);
1283
1292
  });
1284
- // 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.
1285
1296
  program
1286
1297
  .command('repair')
1287
- .description('Clean up old hooks and install skills (v0.9.0+ migration)')
1288
- .option('--force', 'Force overwrite existing configuration')
1289
- .action((options) => {
1290
- installSkillsAndHook(options.force || false);
1291
- 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
+ }
1292
1328
  });
1293
1329
  // Check hooks function
1294
1330
  function checkHooks() {
@@ -0,0 +1,402 @@
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.findSettingsFiles = findSettingsFiles;
39
+ exports.scanFile = scanFile;
40
+ exports.applyFixes = applyFixes;
41
+ exports.runRepair = runRepair;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
45
+ const CLAUDE_RECALL_CLI_RE = /claude[-_]recall[-_]cli(?:\.js)?/i;
46
+ const HOOK_RUN_ID_RE = /hook\s+run\s+(\S+)/i;
47
+ /**
48
+ * Resolve a binary name against PATH. POSIX-first; on Windows tries common
49
+ * extensions. Returns the first matching absolute path, or null.
50
+ */
51
+ function resolveOnPath(binName) {
52
+ const pathEnv = process.env.PATH || '';
53
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
54
+ for (const dir of pathEnv.split(path.delimiter)) {
55
+ if (!dir)
56
+ continue;
57
+ for (const ext of exts) {
58
+ const candidate = path.join(dir, binName + ext);
59
+ try {
60
+ const st = fs.statSync(candidate);
61
+ if (st.isFile())
62
+ return candidate;
63
+ }
64
+ catch {
65
+ // not present โ€” keep looking
66
+ }
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Decide whether a hook command belongs to claude-recall and, if so, whether
73
+ * its invocation target actually resolves on disk / on PATH. Pure function โ€”
74
+ * no I/O except for the filesystem check on absolute script paths and the
75
+ * PATH probe (which the caller can stub via `claudeRecallResolver`).
76
+ */
77
+ function classifyHook(command, claudeRecallResolver) {
78
+ const trimmed = command.trim();
79
+ if (!trimmed)
80
+ return { status: 'non-claude-recall' };
81
+ const tokens = trimmed.split(/\s+/);
82
+ const first = tokens[0] || '';
83
+ const base = path.basename(first);
84
+ const looksLikeCR = CLAUDE_RECALL_CLI_RE.test(trimmed) ||
85
+ base === 'claude-recall' ||
86
+ /\bclaude-recall\b/.test(trimmed);
87
+ if (!looksLikeCR)
88
+ return { status: 'non-claude-recall' };
89
+ const hookIdMatch = trimmed.match(HOOK_RUN_ID_RE);
90
+ const hookId = hookIdMatch ? hookIdMatch[1] : null;
91
+ // Case A: `node /abs/path/to/claude-recall-cli.js hook run ...`
92
+ // or `/abs/path/to/node /abs/path/.../claude-recall-cli.js ...`
93
+ if (base === 'node' || /\/node$/.test(first)) {
94
+ const script = tokens[1];
95
+ if (script && path.isAbsolute(script) && CLAUDE_RECALL_CLI_RE.test(script)) {
96
+ if (!fs.existsSync(script)) {
97
+ return { status: 'broken-absolute', scriptPath: script, hookId };
98
+ }
99
+ return { status: 'ok' };
100
+ }
101
+ // Unusual node invocation we don't recognize โ€” don't touch.
102
+ return { status: 'ok' };
103
+ }
104
+ // Case B: `claude-recall hook run ...` (PATH-resolved form โ€” what we rewrite to)
105
+ if (base === 'claude-recall') {
106
+ if (claudeRecallResolver())
107
+ return { status: 'ok' };
108
+ return { status: 'broken-path', binary: 'claude-recall', hookId };
109
+ }
110
+ // Case C: `npx claude-recall ...` โ€” npx resolves at runtime; treat as OK.
111
+ if (base === 'npx')
112
+ return { status: 'ok' };
113
+ // Anything else mentioning claude-recall โ€” leave alone.
114
+ return { status: 'ok' };
115
+ }
116
+ /**
117
+ * Given the closest settings.json-like file path, return the list of paths
118
+ * to scan (settings.json + settings.local.json if present).
119
+ */
120
+ function pickSiblings(settingsPath) {
121
+ const dir = path.dirname(settingsPath);
122
+ const out = [];
123
+ for (const name of ['settings.json', 'settings.local.json']) {
124
+ const p = path.join(dir, name);
125
+ if (fs.existsSync(p))
126
+ out.push(p);
127
+ }
128
+ return out;
129
+ }
130
+ function findSettingsFiles(cwd, home, scope) {
131
+ const results = [];
132
+ if (scope === 'user' || scope === 'all') {
133
+ const userDir = path.join(home, '.claude');
134
+ for (const p of pickSiblings(path.join(userDir, 'settings.json'))) {
135
+ if (!results.includes(p))
136
+ results.push(p);
137
+ }
138
+ }
139
+ if (scope === 'project' || scope === 'all') {
140
+ // Walk up looking for the CLOSEST .claude dir containing a settings file
141
+ // (matches Claude Code's own resolution). We don't scan ancestors beyond
142
+ // the first match โ€” those belong to other projects.
143
+ let dir = cwd;
144
+ while (dir !== path.dirname(dir)) {
145
+ const claudeDir = path.join(dir, '.claude');
146
+ const s = path.join(claudeDir, 'settings.json');
147
+ const l = path.join(claudeDir, 'settings.local.json');
148
+ const hasAny = fs.existsSync(s) || fs.existsSync(l);
149
+ if (hasAny) {
150
+ if (fs.existsSync(s) && !results.includes(s))
151
+ results.push(s);
152
+ if (fs.existsSync(l) && !results.includes(l))
153
+ results.push(l);
154
+ break;
155
+ }
156
+ dir = path.dirname(dir);
157
+ }
158
+ }
159
+ return results;
160
+ }
161
+ function scanFile(settingsPath, claudeRecallResolver) {
162
+ const report = { settingsPath, findings: [] };
163
+ let raw;
164
+ try {
165
+ raw = fs.readFileSync(settingsPath, 'utf8');
166
+ }
167
+ catch (e) {
168
+ report.parseError = `cannot read: ${e.message}`;
169
+ return report;
170
+ }
171
+ let parsed;
172
+ try {
173
+ parsed = JSON.parse(raw);
174
+ }
175
+ catch (e) {
176
+ report.parseError = `invalid JSON: ${e.message}`;
177
+ return report;
178
+ }
179
+ if (typeof parsed.hooksVersion === 'string') {
180
+ report.hooksVersion = parsed.hooksVersion;
181
+ }
182
+ const hooks = parsed.hooks;
183
+ if (!hooks || typeof hooks !== 'object')
184
+ return report;
185
+ const hasCR = () => claudeRecallResolver();
186
+ // Cache resolver result within a single scan to avoid hammering stat().
187
+ let cached;
188
+ const cachingResolver = () => {
189
+ if (cached === undefined)
190
+ cached = hasCR();
191
+ return cached ?? null;
192
+ };
193
+ for (const [event, groups] of Object.entries(hooks)) {
194
+ if (!Array.isArray(groups))
195
+ continue;
196
+ groups.forEach((group, groupIndex) => {
197
+ if (!group || !Array.isArray(group.hooks))
198
+ return;
199
+ group.hooks.forEach((hook, hookIndex) => {
200
+ if (!hook || typeof hook.command !== 'string')
201
+ return;
202
+ const classification = classifyHook(hook.command, cachingResolver);
203
+ if (classification.status === 'non-claude-recall')
204
+ return;
205
+ const finding = {
206
+ location: { settingsPath, event, groupIndex, hookIndex },
207
+ originalCommand: hook.command,
208
+ classification,
209
+ };
210
+ if (classification.status === 'broken-absolute') {
211
+ const crPath = cachingResolver();
212
+ if (crPath) {
213
+ const id = classification.hookId;
214
+ if (id) {
215
+ finding.proposedCommand = `claude-recall hook run ${id}`;
216
+ }
217
+ // If we can't extract a hook id we don't know what subcommand to
218
+ // invoke. Leave proposedCommand unset โ€” reported as unfixable.
219
+ }
220
+ }
221
+ report.findings.push(finding);
222
+ });
223
+ });
224
+ }
225
+ return report;
226
+ }
227
+ function applyFixes(report, opts) {
228
+ if (report.parseError)
229
+ return { changed: false, applied: 0, backupPath: null };
230
+ const fixable = report.findings.filter(f => f.proposedCommand);
231
+ if (fixable.length === 0)
232
+ return { changed: false, applied: 0, backupPath: null };
233
+ const raw = fs.readFileSync(report.settingsPath, 'utf8');
234
+ const parsed = JSON.parse(raw);
235
+ if (!parsed.hooks)
236
+ return { changed: false, applied: 0, backupPath: null };
237
+ let applied = 0;
238
+ for (const f of fixable) {
239
+ const { event, groupIndex, hookIndex } = f.location;
240
+ const group = parsed.hooks[event]?.[groupIndex];
241
+ const entry = group?.hooks?.[hookIndex];
242
+ if (!entry)
243
+ continue;
244
+ // Sanity: only rewrite if the command still matches what we scanned.
245
+ if (entry.command !== f.originalCommand)
246
+ continue;
247
+ entry.command = f.proposedCommand;
248
+ applied++;
249
+ }
250
+ if (applied === 0)
251
+ return { changed: false, applied: 0, backupPath: null };
252
+ if (opts.dryRun) {
253
+ return { changed: true, applied, backupPath: null };
254
+ }
255
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
256
+ const backupPath = `${report.settingsPath}.bak.${ts}`;
257
+ fs.writeFileSync(backupPath, raw);
258
+ fs.writeFileSync(report.settingsPath, JSON.stringify(parsed, null, 2));
259
+ return { changed: true, applied, backupPath };
260
+ }
261
+ function classify(finding) {
262
+ const c = finding.classification;
263
+ if (c.status === 'ok' || c.status === 'non-claude-recall')
264
+ return 'ok';
265
+ return finding.proposedCommand ? 'fixable' : 'unfixable';
266
+ }
267
+ function describe(finding) {
268
+ const c = finding.classification;
269
+ const loc = `${finding.location.event}[${finding.location.groupIndex}].hooks[${finding.location.hookIndex}]`;
270
+ if (c.status === 'broken-absolute') {
271
+ return `${loc} missing script: ${c.scriptPath}`;
272
+ }
273
+ if (c.status === 'broken-path') {
274
+ return `${loc} '${c.binary}' not on PATH`;
275
+ }
276
+ return `${loc} ok`;
277
+ }
278
+ async function runRepair(options = {}) {
279
+ const log = options.logger ?? { log: console.log.bind(console), warn: console.warn.bind(console) };
280
+ const cwd = options.cwd ?? process.cwd();
281
+ const home = options.home ?? os.homedir();
282
+ const scope = options.scope ?? 'all';
283
+ const resolver = options.claudeRecallOnPath ?? (() => resolveOnPath('claude-recall'));
284
+ log.log('\n๐Ÿฉบ Claude Recall repair (conservative)\n');
285
+ const files = findSettingsFiles(cwd, home, scope);
286
+ if (files.length === 0) {
287
+ log.log(`No settings files found (scope: ${scope}).`);
288
+ log.log('Nothing to repair. If you meant to install hooks, run:');
289
+ log.log(' claude-recall setup --install\n');
290
+ return { exitCode: 0, filesScanned: 0, filesModified: 0, fixesApplied: 0, unfixable: 0, reports: [] };
291
+ }
292
+ const reports = [];
293
+ let totalFixable = 0;
294
+ let totalUnfixable = 0;
295
+ let totalOk = 0;
296
+ for (const f of files) {
297
+ const report = scanFile(f, resolver);
298
+ reports.push(report);
299
+ if (report.parseError) {
300
+ log.warn(` โš  ${f}: ${report.parseError}`);
301
+ continue;
302
+ }
303
+ let fixable = 0, unfixable = 0, ok = 0;
304
+ for (const finding of report.findings) {
305
+ const kind = classify(finding);
306
+ if (kind === 'fixable')
307
+ fixable++;
308
+ else if (kind === 'unfixable')
309
+ unfixable++;
310
+ else
311
+ ok++;
312
+ }
313
+ totalFixable += fixable;
314
+ totalUnfixable += unfixable;
315
+ totalOk += ok;
316
+ const versionTag = report.hooksVersion ? ` (hooksVersion: ${report.hooksVersion})` : '';
317
+ log.log(` ${f}${versionTag}`);
318
+ log.log(` ${ok} OK, ${fixable} fixable, ${unfixable} unfixable`);
319
+ for (const finding of report.findings) {
320
+ if (classify(finding) === 'ok')
321
+ continue;
322
+ log.log(` - ${describe(finding)}`);
323
+ if (finding.proposedCommand) {
324
+ log.log(` proposed: ${finding.proposedCommand}`);
325
+ }
326
+ }
327
+ }
328
+ if (totalFixable === 0) {
329
+ if (totalUnfixable > 0) {
330
+ log.log(`\n${totalUnfixable} broken claude-recall hook(s) found but no safe fix available.`);
331
+ log.log('Install claude-recall on PATH so repair can rewrite the broken paths:');
332
+ log.log(' npm install -g claude-recall\n');
333
+ // Don't fail postinstall โ€” user's current install was fine until their
334
+ // PATH/hook config drifted; this is diagnostic, not an error.
335
+ return {
336
+ exitCode: 0,
337
+ filesScanned: files.length,
338
+ filesModified: 0,
339
+ fixesApplied: 0,
340
+ unfixable: totalUnfixable,
341
+ reports,
342
+ };
343
+ }
344
+ log.log(`\nโœ… All ${totalOk} claude-recall hook(s) look healthy. Nothing to do.\n`);
345
+ return {
346
+ exitCode: 0,
347
+ filesScanned: files.length,
348
+ filesModified: 0,
349
+ fixesApplied: 0,
350
+ unfixable: 0,
351
+ reports,
352
+ };
353
+ }
354
+ if (!options.auto && !options.dryRun && options.prompt) {
355
+ const proceed = await options.prompt(`\nApply ${totalFixable} fix(es)? [y/N] `);
356
+ if (!proceed) {
357
+ log.log('Aborted. No files changed.\n');
358
+ return {
359
+ exitCode: 0,
360
+ filesScanned: files.length,
361
+ filesModified: 0,
362
+ fixesApplied: 0,
363
+ unfixable: totalUnfixable,
364
+ reports,
365
+ };
366
+ }
367
+ }
368
+ let filesModified = 0;
369
+ let fixesApplied = 0;
370
+ for (const report of reports) {
371
+ const { changed, applied, backupPath } = applyFixes(report, { dryRun: !!options.dryRun });
372
+ if (changed && !options.dryRun) {
373
+ filesModified++;
374
+ fixesApplied += applied;
375
+ log.log(` โœ“ ${report.settingsPath}: applied ${applied} fix(es)`);
376
+ if (backupPath)
377
+ log.log(` backup: ${backupPath}`);
378
+ }
379
+ else if (changed && options.dryRun) {
380
+ fixesApplied += applied;
381
+ log.log(` (dry-run) ${report.settingsPath}: would apply ${applied} fix(es)`);
382
+ }
383
+ }
384
+ if (options.dryRun) {
385
+ 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`);
386
+ }
387
+ else {
388
+ log.log(`\nโœ… Repaired ${fixesApplied} hook(s) across ${filesModified} file(s).`);
389
+ if (totalUnfixable > 0) {
390
+ log.log(` ${totalUnfixable} issue(s) still need manual attention (see above).`);
391
+ }
392
+ log.log('');
393
+ }
394
+ return {
395
+ exitCode: 0,
396
+ filesScanned: files.length,
397
+ filesModified,
398
+ fixesApplied,
399
+ unfixable: totalUnfixable,
400
+ reports,
401
+ };
402
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
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,30 @@ 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 that reference a defunct claude-recall install
122
+ // (common when node/nvm versions change, or when the package was reinstalled
123
+ // into a different location). The --auto --scope user flags mean:
124
+ // โ€ข only user-global settings are touched (no project files)
125
+ // โ€ข only commands pointing at MISSING absolute scripts get rewritten
126
+ // โ€ข user customizations (timeouts, matchers, sibling hooks) preserved
127
+ // โ€ข writes a .bak.<timestamp> before any change
128
+ // โ€ข never installs hooks where none exist โ€” satisfies the "don't clobber"
129
+ // rule above
130
+ try {
131
+ const cliPath = path.join(__dirname, '..', 'dist', 'cli', 'claude-recall-cli.js');
132
+ if (fs.existsSync(cliPath)) {
133
+ execSync(`node "${cliPath}" repair --auto --scope user`, {
134
+ stdio: 'inherit',
135
+ timeout: 15000
136
+ });
137
+ }
138
+ } catch (repairError) {
139
+ // Non-fatal: postinstall must never fail the npm install. Any repair
140
+ // problem can be fixed manually with `claude-recall repair`.
141
+ console.log('โš ๏ธ Auto-repair skipped (non-fatal):', repairError.message);
142
+ }
143
+
120
144
  console.log('\nโœ… Installation complete!\n');
121
145
  console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
122
146
  console.log('๐Ÿ“Œ ACTIVATE CLAUDE RECALL:');