claude-recall 0.26.1 → 0.27.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.
@@ -36,8 +36,13 @@ PASSTHROUGH_TOOLS = [
36
36
  'AskUserQuestion', # User interaction
37
37
  ]
38
38
 
39
- # Read-only bash commands that don't need memory search
40
- READ_ONLY_BASH = [
39
+ # Bash command prefixes exempt from the load-rules gate. NOT all read-only:
40
+ # alongside inspection commands, routine dev-workflow commands (git commit,
41
+ # npm test, ...) are deliberately exempt — the gate exists to get rules loaded
42
+ # before Claude starts shaping code, and blocking mid-workflow git/npm calls
43
+ # adds friction without a safety benefit. This is an advisory nudge, not a
44
+ # security boundary.
45
+ EXEMPT_BASH_PREFIXES = [
41
46
  'ls', 'cat', 'head', 'tail', 'less', 'more', 'file', 'stat', 'wc',
42
47
  'find', 'locate', 'which', 'whereis', 'type', 'pwd', 'whoami',
43
48
  'git status', 'git log', 'git diff', 'git show', 'git branch',
@@ -67,7 +72,7 @@ def load_state(session_id: str) -> dict:
67
72
  if state_file.exists():
68
73
  try:
69
74
  return json.load(open(state_file))
70
- except:
75
+ except Exception:
71
76
  pass
72
77
  return {'lastSearchAt': None, 'searchQuery': None}
73
78
 
@@ -76,7 +81,7 @@ def save_state(session_id: str, state: dict):
76
81
  STATE_DIR.mkdir(parents=True, exist_ok=True)
77
82
  try:
78
83
  json.dump(state, open(get_state_file(session_id), 'w'), indent=2)
79
- except:
84
+ except Exception:
80
85
  pass
81
86
 
82
87
 
@@ -84,18 +89,26 @@ def is_search_tool(tool_name: str) -> bool:
84
89
  return any(s in tool_name for s in SEARCH_TOOLS)
85
90
 
86
91
 
87
- def is_read_only_bash(command: str) -> bool:
92
+ def _has_command_prefix(cmd: str, prefix: str) -> bool:
93
+ """Word-boundary prefix match: 'cat foo' matches 'cat', but
94
+ 'catastrophic-script.sh' and 'envsubst' do not match 'cat'/'env'."""
95
+ if not cmd.startswith(prefix):
96
+ return False
97
+ rest = cmd[len(prefix):]
98
+ return rest == '' or rest[0] in ' \t;|&'
99
+
100
+
101
+ def is_exempt_bash(command: str) -> bool:
88
102
  if not command:
89
103
  return False
90
104
  cmd = command.strip().lower()
91
- # Check direct match or pipe starting with read-only
92
- for ro in READ_ONLY_BASH:
93
- if cmd.startswith(ro):
94
- return True
105
+ # Check direct match, or the first segment of a pipeline
106
+ candidates = [cmd]
95
107
  if '|' in cmd:
96
- first = cmd.split('|')[0].strip()
97
- for ro in READ_ONLY_BASH:
98
- if first.startswith(ro):
108
+ candidates.append(cmd.split('|')[0].strip())
109
+ for candidate in candidates:
110
+ for prefix in EXEMPT_BASH_PREFIXES:
111
+ if _has_command_prefix(candidate, prefix):
99
112
  return True
100
113
  return False
101
114
 
@@ -106,7 +119,7 @@ def main():
106
119
 
107
120
  try:
108
121
  data = json.load(sys.stdin)
109
- except:
122
+ except Exception:
110
123
  sys.exit(0)
111
124
 
112
125
  tool_name = data.get('tool_name', '')
@@ -139,14 +152,20 @@ def main():
139
152
  if not last_search:
140
153
  if tool_name not in ENFORCE_TOOLS:
141
154
  sys.exit(0)
142
- # Mutation tool on first call fall through to blocking logic below.
155
+ # Exempt bash passes even before rules are loaded previously this
156
+ # exemption only existed on the post-load branch, so the very first
157
+ # `git status`/`ls` of a session was hard-blocked up to MAX_BLOCKS
158
+ # times, contradicting the "read-only exploration passes freely" rule.
159
+ if tool_name == 'Bash' and is_exempt_bash(tool_input.get('command', '')):
160
+ sys.exit(0)
161
+ # Other mutation tool on first call — fall through to blocking logic below.
143
162
  else:
144
163
  # Rules loaded at least once — only enforce on mutation tools
145
164
  if tool_name not in ENFORCE_TOOLS:
146
165
  sys.exit(0)
147
166
 
148
- # Skip read-only bash
149
- if tool_name == 'Bash' and is_read_only_bash(tool_input.get('command', '')):
167
+ # Skip exempt bash
168
+ if tool_name == 'Bash' and is_exempt_bash(tool_input.get('command', '')):
150
169
  sys.exit(0)
151
170
 
152
171
  if last_search:
@@ -131,9 +131,9 @@ memories to form a skill (3+ for most types, 5+ for preferences). If so, it writ
131
131
  a SKILL.md file that Claude Code loads automatically.
132
132
 
133
133
  **CLI commands:**
134
- - `npx claude-recall skills list` — see generated skills
135
- - `npx claude-recall skills generate --force` — force regeneration
136
- - `npx claude-recall skills clean --force` — remove all auto-generated skills
134
+ - `claude-recall skills list` — see generated skills
135
+ - `claude-recall skills generate --force` — force regeneration
136
+ - `claude-recall skills clean --force` — remove all auto-generated skills
137
137
 
138
138
  ## Automatic Capture Hooks
139
139
 
@@ -158,7 +158,7 @@ Claude Recall registers hooks on six Claude Code events for automatic capture, j
158
158
  - Auto-checkpoint quality gate: refuses to save when the LLM detects the task was already complete — manual checkpoints stay sticky
159
159
  - Always exits 0 — hooks never block Claude
160
160
 
161
- **Setup:** Run `npx claude-recall setup --install` to register hooks in `.claude/settings.json`. After any upgrade, re-run `setup --install` in each project so newly-added hook events get registered (claude-recall uses a `hooksVersion` field to signal when registration has changed).
161
+ **Setup:** Run `claude-recall setup --install` to register hooks in `.claude/settings.json`. After an upgrade whose release notes mention new or changed hooks (a `hooksVersion` bump), re-run it in each active project it's idempotent, so when hooks are already current it's a no-op and touches nothing.
162
162
 
163
163
  ## Example Workflows
164
164
 
package/README.md CHANGED
@@ -49,6 +49,16 @@ claude mcp add claude-recall -- claude-recall mcp start
49
49
 
50
50
  Restart Claude Code. Ask *"Load my rules"* to verify — Claude should call `load_rules`.
51
51
 
52
+ Prefer it active in **every** project? Register the MCP server once at user scope instead of per project (memories stay isolated per project either way — scoping comes from the working directory, not the install):
53
+
54
+ ```bash
55
+ claude mcp add --scope user claude-recall -- claude-recall mcp start
56
+ ```
57
+
58
+ Hook-based auto-capture remains a per-project opt-in via `claude-recall setup --install` (it writes to that project's `.claude/settings.json`).
59
+
60
+ > **Do NOT add claude-recall as a project dependency** (`npm install claude-recall` inside a project). All projects share one database at `~/.claude-recall/` and whatever binary touches it runs schema migrations — multiple project-local copies at different versions fight over the same file. Worse, `npx claude-recall` prefers a project-local copy over your up-to-date global one, so a stale local install silently shadows every upgrade. One global binary; per-project *activation* only.
61
+
52
62
  > **Hit `EACCES: permission denied`?** Your global npm is owned by root. Either `sudo npm install -g claude-recall` once, or do the permanent fix described in [Upgrading](#upgrading) below.
53
63
 
54
64
  ### Install for Pi
@@ -71,6 +81,15 @@ claude-recall upgrade
71
81
 
72
82
  One command. Checks the registry, refreshes the global binary, clears any running MCP servers — Claude Code respawns them on the next tool call, picking up the new version. **No `claude mcp add` re-run needed** — existing registrations point at the `claude-recall` command, not a pinned path.
73
83
 
84
+ If the release notes mention new or changed hooks (a `hooksVersion` bump), also re-run `claude-recall setup --install` in each active project. It's safe to run any time: when your hooks are already current it's a no-op and touches nothing.
85
+
86
+ > **Registered before v0.27.x?** Older versions auto-registered the MCP server with an `npx`-based command, which re-resolves the package on every server start and can be shadowed by stale project-local installs. Switch to the direct binary form (run in each affected project):
87
+ >
88
+ > ```bash
89
+ > claude mcp remove claude-recall
90
+ > claude mcp add claude-recall -- claude-recall mcp start
91
+ > ```
92
+
74
93
  For Pi, run `pi update npm:claude-recall` and restart Pi.
75
94
 
76
95
  > **Seeing `error: unknown command 'upgrade'`?** Your installed version predates 0.23.2 (the release that added the `upgrade` command). Bootstrap once with `npm install -g claude-recall@latest`, then all future upgrades use `claude-recall upgrade`.
@@ -49,6 +49,11 @@ const mcp_commands_1 = require("./commands/mcp-commands");
49
49
  const project_commands_1 = require("./commands/project-commands");
50
50
  const hook_commands_1 = require("./commands/hook-commands");
51
51
  const repair_1 = require("./commands/repair");
52
+ // v14 = add PreToolUse rule-injector + Post resolver for JITRI.
53
+ // Bump when the hook block template changes — setup skips the settings
54
+ // rewrite when the installed hooksVersion already matches.
55
+ const HOOKS_VERSION = '14.0.0';
56
+ const parse_utils_1 = require("./parse-utils");
52
57
  const program = new commander_1.Command();
53
58
  class ClaudeRecallCLI {
54
59
  constructor(options) {
@@ -232,7 +237,7 @@ class ClaudeRecallCLI {
232
237
  // Sort by timestamp (newest first)
233
238
  failures.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
234
239
  // Limit results
235
- const limit = options.limit || 10;
240
+ const limit = (0, parse_utils_1.parsePositiveInt)(options.limit, 'limit', 10);
236
241
  const displayFailures = failures.slice(0, limit);
237
242
  console.log('\n❌ Failure Memories (Counterfactual Learning)\n');
238
243
  console.log(`Found ${failures.length} failures (showing ${displayFailures.length})\n`);
@@ -241,9 +246,20 @@ class ClaudeRecallCLI {
241
246
  return;
242
247
  }
243
248
  displayFailures.forEach((failure, index) => {
244
- const value = typeof failure.value === 'string'
245
- ? JSON.parse(failure.value)
246
- : failure.value;
249
+ // One malformed row must not crash the whole listing — fall back to the
250
+ // raw string as content
251
+ let value;
252
+ if (typeof failure.value === 'string') {
253
+ try {
254
+ value = JSON.parse(failure.value);
255
+ }
256
+ catch {
257
+ value = { content: failure.value };
258
+ }
259
+ }
260
+ else {
261
+ value = failure.value;
262
+ }
247
263
  const content = value.content || value;
248
264
  console.log(`${index + 1}. ${value.title || 'Untitled Failure'}`);
249
265
  console.log(` What Failed: ${content.what_failed || 'Unknown'}`);
@@ -394,10 +410,21 @@ class ClaudeRecallCLI {
394
410
  const needsInstall = current !== latest;
395
411
  if (needsInstall) {
396
412
  console.log(`\n📦 Upgrading ${current} → ${latest}...\n`);
397
- // Run npm install -g, streaming output so the user sees progress / errors live
413
+ // Run npm install -g, streaming output so the user sees progress / errors live.
414
+ // shell: true on Windows — npm is npm.cmd there and a bare spawnSync ENOENTs.
398
415
  const install = spawnSync('npm', ['install', '-g', 'claude-recall@latest'], {
399
416
  stdio: 'inherit',
417
+ shell: process.platform === 'win32',
400
418
  });
419
+ if (install.error && install.error.code === 'ENOENT') {
420
+ // spawn failed before npm ever ran — the EACCES advice below would be
421
+ // misleading here (previous behavior: status === null !== 0 fell
422
+ // through to the "sudo npm install" remediation for a missing npm)
423
+ console.error('\n❌ npm not found on PATH.');
424
+ console.error('\nInstall Node.js (which includes npm) ≥ 20.19, or fix your PATH, then re-run:');
425
+ console.error(' claude-recall upgrade');
426
+ process.exit(1);
427
+ }
401
428
  if (install.status !== 0) {
402
429
  // npm prints its own error — add the practical remediation on top
403
430
  console.error('\n❌ Install failed.');
@@ -534,7 +561,7 @@ class ClaudeRecallCLI {
534
561
  * Show outcome-aware learning status: episodes, outcome events, candidate lessons, memory stats
535
562
  */
536
563
  showOutcomes(options) {
537
- const limit = options.limit || 10;
564
+ const limit = (0, parse_utils_1.parsePositiveInt)(options.limit, 'limit', 10);
538
565
  const section = options.section;
539
566
  const db = this.memoryService.getDatabase();
540
567
  // Check if outcome tables exist
@@ -619,7 +646,7 @@ class ClaudeRecallCLI {
619
646
  * Search memories by query
620
647
  */
621
648
  search(query, options) {
622
- const limit = options.limit || 10;
649
+ const limit = (0, parse_utils_1.parsePositiveInt)(options.limit, 'limit', 10);
623
650
  // Determine search scope
624
651
  let results;
625
652
  if (options.global) {
@@ -709,7 +736,7 @@ class ClaudeRecallCLI {
709
736
  /**
710
737
  * Import memories from a file
711
738
  */
712
- async import(inputPath) {
739
+ async import(inputPath, options = {}) {
713
740
  try {
714
741
  if (!fs.existsSync(inputPath)) {
715
742
  console.error(`❌ File not found: ${inputPath}`);
@@ -724,11 +751,19 @@ class ClaudeRecallCLI {
724
751
  let imported = 0;
725
752
  for (const memory of data.memories) {
726
753
  try {
754
+ // Preserve each memory's original scoping. Exported rows carry
755
+ // project_id/scope as columns (not inside context) — the previous
756
+ // `context: memory.context || {}` dropped them, silently rescoping
757
+ // every imported memory to the CURRENT project. --project overrides.
727
758
  this.memoryService.store({
728
759
  key: memory.key || `imported_${Date.now()}_${Math.random()}`,
729
760
  value: memory.value,
730
761
  type: memory.type || 'imported',
731
- context: memory.context || {}
762
+ context: {
763
+ ...(memory.context || {}),
764
+ projectId: options.project ?? memory.project_id ?? memory.context?.projectId,
765
+ scope: memory.scope ?? memory.context?.scope ?? null,
766
+ }
732
767
  });
733
768
  imported++;
734
769
  }
@@ -736,8 +771,8 @@ class ClaudeRecallCLI {
736
771
  console.warn(`⚠️ Failed to import memory: ${error}`);
737
772
  }
738
773
  }
739
- console.log(`✅ Imported ${imported}/${data.memories.length} memories`);
740
- this.logger.info('CLI', 'Import completed', { imported, total: data.memories.length });
774
+ console.log(`✅ Imported ${imported}/${data.memories.length} memories${options.project ? ` (rescoped to project: ${options.project})` : ' (original project scoping preserved)'}`);
775
+ this.logger.info('CLI', 'Import completed', { imported, total: data.memories.length, project: options.project });
741
776
  }
742
777
  catch (error) {
743
778
  console.error('❌ Import failed:', error);
@@ -908,7 +943,7 @@ class ClaudeRecallCLI {
908
943
  else if (!hasPi) {
909
944
  console.log('Claude Code MCP:');
910
945
  console.log(' Status: Not registered');
911
- console.log(' Command: claude mcp add claude-recall claude-recall mcp start');
946
+ console.log(' Command: claude mcp add claude-recall -- claude-recall mcp start');
912
947
  }
913
948
  else {
914
949
  // Pi is present; show Claude Code MCP as optional
@@ -948,7 +983,7 @@ class ClaudeRecallCLI {
948
983
  async store(content, options) {
949
984
  try {
950
985
  const type = options.type || 'preference';
951
- const confidence = options.confidence || 0.8;
986
+ const confidence = (0, parse_utils_1.parseUnitFloat)(options.confidence, 'confidence', 0.8);
952
987
  // Parse metadata if provided
953
988
  let metadata = {};
954
989
  if (options.metadata) {
@@ -1034,8 +1069,7 @@ async function main() {
1034
1069
  }
1035
1070
  }
1036
1071
  // Install skills + minimal enforcement hook
1037
- // TODO: honor force currently `repair --reinstall-hooks` behaves identically to `setup --install`
1038
- function installSkillsAndHook(_force = false) {
1072
+ function installSkillsAndHook(force = false) {
1039
1073
  const cwd = process.cwd();
1040
1074
  const projectName = path.basename(cwd);
1041
1075
  console.log('\n📦 Claude Recall Setup\n');
@@ -1103,6 +1137,18 @@ async function main() {
1103
1137
  catch (e) {
1104
1138
  settings = {};
1105
1139
  }
1140
+ // Idempotent re-run: hook block already at the current version → leave
1141
+ // settings.json untouched (no rewrite, no backup churn) but still
1142
+ // refresh skills below. `force` (repair --reinstall-hooks) rewrites
1143
+ // unconditionally — the recovery path for a corrupted or hand-edited
1144
+ // block. Previously force was accepted and ignored, so the two
1145
+ // commands behaved identically.
1146
+ if (!force && settings.hooksVersion === HOOKS_VERSION) {
1147
+ console.log(`✅ Hooks already at v${HOOKS_VERSION} — settings.json untouched (use \`repair --reinstall-hooks\` to force a rewrite)`);
1148
+ installSkills(claudeDir, packageSkillsDir);
1149
+ console.log('\n✅ Setup complete!\n');
1150
+ return;
1151
+ }
1106
1152
  const existingHooks = settings.hooks;
1107
1153
  const hasExistingHooks = existingHooks
1108
1154
  && typeof existingHooks === 'object'
@@ -1116,11 +1162,18 @@ async function main() {
1116
1162
  console.log(' from other tools that need to coexist.');
1117
1163
  }
1118
1164
  }
1119
- // Resolve the CLI script path so hooks use the local install instead of npx.
1120
- // This avoids registry lookups on every hook invocation.
1165
+ // Prefer the portable `claude-recall` form when the binary is on PATH
1166
+ // absolute dist paths break on every nvm switch or prefix move, the exact
1167
+ // failure class `repair` exists to fix (repair already treats the PATH
1168
+ // form as canonical). Absolute path only as fallback.
1169
+ const onPath = (0, repair_1.resolveOnPath)('claude-recall');
1121
1170
  const cliScript = path.join(packageDir, 'dist', 'cli', 'claude-recall-cli.js');
1122
- const hookCmd = `node ${cliScript} hook run`;
1123
- settings.hooksVersion = '14.0.0'; // v14 = add PreToolUse rule-injector + Post resolver for JITRI
1171
+ const hookCmd = onPath ? 'claude-recall hook run' : `node ${cliScript} hook run`;
1172
+ if (!onPath) {
1173
+ console.log('⚠️ claude-recall not on PATH — hooks will use an absolute path (breaks if the install moves).');
1174
+ console.log(' `npm install -g claude-recall` gives move-proof hooks; `claude-recall repair` can convert later.');
1175
+ }
1176
+ settings.hooksVersion = HOOKS_VERSION;
1124
1177
  settings.hooks = {
1125
1178
  SubagentStart: [
1126
1179
  {
@@ -1259,7 +1312,13 @@ async function main() {
1259
1312
  }
1260
1313
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1261
1314
  console.log('✅ Configured search enforcement hook');
1262
- // === INSTALL: Copy skills directory ===
1315
+ installSkills(claudeDir, packageSkillsDir);
1316
+ console.log('\n✅ Setup complete!\n');
1317
+ console.log('ℹ️ Uses Skills (guidance) + hooks (auto-capture with LLM classification).');
1318
+ console.log('Restart Claude Code to activate.\n');
1319
+ }
1320
+ // === INSTALL: Copy skills directory (idempotent overwrite) ===
1321
+ function installSkills(claudeDir, packageSkillsDir) {
1263
1322
  if (fs.existsSync(packageSkillsDir)) {
1264
1323
  const skillsDir = path.join(claudeDir, 'skills');
1265
1324
  copyDirRecursive(packageSkillsDir, skillsDir);
@@ -1268,9 +1327,6 @@ async function main() {
1268
1327
  else {
1269
1328
  console.log(`⚠️ Skills not found at: ${packageSkillsDir}`);
1270
1329
  }
1271
- console.log('\n✅ Setup complete!\n');
1272
- console.log('ℹ️ Uses Skills (guidance) + hooks (auto-capture with LLM classification).');
1273
- console.log('Restart Claude Code to activate.\n');
1274
1330
  }
1275
1331
  // Setup command - shows activation instructions or installs skills
1276
1332
  program
@@ -1283,24 +1339,32 @@ async function main() {
1283
1339
  installSkillsAndHook();
1284
1340
  }
1285
1341
  else {
1286
- // Show activation instructions
1342
+ // Show activation instructions. Registration uses the global
1343
+ // `claude-recall` binary, NOT `npx ... @latest`: npx resolves through
1344
+ // any stale project-local install (shadow trap) and @latest hits the
1345
+ // registry on every server spawn. Consecutive commands are printed
1346
+ // flush-left as one contiguous block so they can be copy-pasted in
1347
+ // one go.
1287
1348
  console.log('\n✅ Claude Recall Setup\n');
1288
1349
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1289
- console.log('📌 ACTIVATE CLAUDE RECALL:');
1350
+ console.log('📌 ACTIVATE CLAUDE RECALL (run in each project):');
1290
1351
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1291
1352
  console.log('');
1292
- console.log(' claude mcp add claude-recall -- npx -y claude-recall@latest mcp start');
1353
+ console.log('claude-recall setup --install');
1354
+ console.log('claude mcp add claude-recall -- claude-recall mcp start');
1293
1355
  console.log('');
1294
1356
  console.log(' Then restart Claude Code (exit and re-enter the session).');
1295
1357
  console.log('');
1296
1358
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1297
1359
  console.log('');
1298
- console.log('🔄 Already registered? Remove and re-add:');
1299
- console.log(' claude mcp remove claude-recall');
1300
- console.log(' claude mcp add claude-recall -- npx -y claude-recall@latest mcp start');
1360
+ console.log('🔄 Registered under an old npx-based command? Remove and re-add:');
1361
+ console.log('');
1362
+ console.log('claude mcp remove claude-recall');
1363
+ console.log('claude mcp add claude-recall -- claude-recall mcp start');
1364
+ console.log('');
1365
+ console.log('🛑 Stop a running old instance:');
1301
1366
  console.log('');
1302
- console.log('🛑 Stop old instance:');
1303
- console.log(' npx claude-recall mcp stop');
1367
+ console.log('claude-recall mcp stop');
1304
1368
  console.log('');
1305
1369
  }
1306
1370
  process.exit(0);
@@ -1372,7 +1436,7 @@ async function main() {
1372
1436
  }
1373
1437
  if (!settingsPath) {
1374
1438
  console.log('❌ No .claude/settings.json found in directory tree');
1375
- console.log(' Run: npx claude-recall repair\n');
1439
+ console.log(' Run: claude-recall repair\n');
1376
1440
  return;
1377
1441
  }
1378
1442
  console.log(`✅ Found settings: ${settingsPath}`);
@@ -1429,7 +1493,7 @@ async function main() {
1429
1493
  }
1430
1494
  }
1431
1495
  if (hasIssues) {
1432
- console.log('\n⚠️ Issues found. Run: npx claude-recall repair\n');
1496
+ console.log('\n⚠️ Issues found. Run: claude-recall repair\n');
1433
1497
  }
1434
1498
  else {
1435
1499
  console.log('\n✅ All hooks OK!\n');
@@ -1460,7 +1524,7 @@ async function main() {
1460
1524
  }
1461
1525
  if (!enforcerPath) {
1462
1526
  console.log('❌ Could not find search_enforcer.py');
1463
- console.log(' Run: npx claude-recall repair\n');
1527
+ console.log(' Run: claude-recall repair\n');
1464
1528
  return;
1465
1529
  }
1466
1530
  console.log(`📍 Enforcer: ${enforcerPath}`);
@@ -1544,7 +1608,7 @@ async function main() {
1544
1608
  }
1545
1609
  else {
1546
1610
  console.log('❌ Some tests failed. Check hook configuration.\n');
1547
- console.log('Run: npx claude-recall repair\n');
1611
+ console.log('Run: claude-recall repair\n');
1548
1612
  }
1549
1613
  }
1550
1614
  // Hooks command group
@@ -1644,7 +1708,7 @@ async function main() {
1644
1708
  console.log('\n📋 Auto-Generated Skills\n');
1645
1709
  if (skills.length === 0) {
1646
1710
  console.log('No auto-generated skills found.\n');
1647
- console.log('Run `npx claude-recall skills generate` to create skills from memories.\n');
1711
+ console.log('Run `claude-recall skills generate` to create skills from memories.\n');
1648
1712
  }
1649
1713
  else {
1650
1714
  for (const skill of skills) {
@@ -1761,7 +1825,7 @@ async function main() {
1761
1825
  .action((query, options) => {
1762
1826
  const cli = new ClaudeRecallCLI(program.opts());
1763
1827
  cli.search(query, {
1764
- limit: parseInt(options.limit),
1828
+ limit: (0, parse_utils_1.parsePositiveInt)(options.limit, 'limit', 10),
1765
1829
  json: options.json,
1766
1830
  project: options.project,
1767
1831
  global: options.global
@@ -1791,7 +1855,7 @@ async function main() {
1791
1855
  .action((options) => {
1792
1856
  const cli = new ClaudeRecallCLI(program.opts());
1793
1857
  cli.showFailures({
1794
- limit: parseInt(options.limit),
1858
+ limit: (0, parse_utils_1.parsePositiveInt)(options.limit, 'limit', 10),
1795
1859
  project: options.project
1796
1860
  });
1797
1861
  process.exit(0);
@@ -1805,7 +1869,7 @@ async function main() {
1805
1869
  .action((options) => {
1806
1870
  const cli = new ClaudeRecallCLI(program.opts());
1807
1871
  cli.showOutcomes({
1808
- limit: parseInt(options.limit),
1872
+ limit: (0, parse_utils_1.parsePositiveInt)(options.limit, 'limit', 10),
1809
1873
  section: options.section
1810
1874
  });
1811
1875
  process.exit(0);
@@ -1824,8 +1888,8 @@ async function main() {
1824
1888
  const cli = new ClaudeRecallCLI(program.opts());
1825
1889
  cli.demoteRules({
1826
1890
  dryRun: options.dryRun,
1827
- minLoads: parseInt(options.minLoads),
1828
- minAgeDays: parseInt(options.minAgeDays),
1891
+ minLoads: (0, parse_utils_1.parsePositiveInt)(options.minLoads, 'min-loads', 20),
1892
+ minAgeDays: (0, parse_utils_1.parsePositiveInt)(options.minAgeDays, 'min-age-days', 7),
1829
1893
  });
1830
1894
  process.exit(0);
1831
1895
  });
@@ -1846,7 +1910,7 @@ async function main() {
1846
1910
  const cli = new ClaudeRecallCLI(program.opts());
1847
1911
  cli.dedupSimilarRules({
1848
1912
  dryRun: options.dryRun,
1849
- threshold: parseFloat(options.threshold),
1913
+ threshold: (0, parse_utils_1.parseUnitFloat)(options.threshold, 'threshold', 0.65),
1850
1914
  });
1851
1915
  process.exit(0);
1852
1916
  });
@@ -1887,10 +1951,11 @@ async function main() {
1887
1951
  // Import command
1888
1952
  program
1889
1953
  .command('import <input>')
1890
- .description('Import memories from file')
1891
- .action(async (input) => {
1954
+ .description('Import memories from file (original project scoping preserved)')
1955
+ .option('--project <id>', 'Rescope all imported memories to this project ID')
1956
+ .action(async (input, options) => {
1892
1957
  const cli = new ClaudeRecallCLI(program.opts());
1893
- await cli.import(input);
1958
+ await cli.import(input, options);
1894
1959
  process.exit(0);
1895
1960
  });
1896
1961
  // Clear command
@@ -1993,7 +2058,7 @@ async function main() {
1993
2058
  const cli = new ClaudeRecallCLI(program.opts());
1994
2059
  await cli.store(content, {
1995
2060
  type: options.type,
1996
- confidence: parseFloat(options.confidence),
2061
+ confidence: (0, parse_utils_1.parseUnitFloat)(options.confidence, 'confidence', 0.8),
1997
2062
  metadata: options.metadata
1998
2063
  });
1999
2064
  process.exit(0);
@@ -102,7 +102,7 @@ class MCPCommands {
102
102
  else {
103
103
  console.log();
104
104
  console.log(chalk_1.default.yellow('⚠ Project not registered in registry'));
105
- console.log(chalk_1.default.gray(' Run `npx claude-recall project register` to register'));
105
+ console.log(chalk_1.default.gray(' Run `claude-recall project register` to register'));
106
106
  }
107
107
  console.log();
108
108
  }
@@ -135,7 +135,7 @@ class MCPCommands {
135
135
  console.log(` ${chalk_1.default.gray(server.projectId.padEnd(40))} PID: ${chalk_1.default.gray(server.pid)} (not running)`);
136
136
  }
137
137
  console.log();
138
- console.log(chalk_1.default.yellow(`💡 Run 'npx claude-recall mcp cleanup' to remove stale PID files`));
138
+ console.log(chalk_1.default.yellow(`💡 Run 'claude-recall mcp cleanup' to remove stale PID files`));
139
139
  console.log();
140
140
  }
141
141
  // Show registered projects that don't have running servers
@@ -147,7 +147,7 @@ class MCPCommands {
147
147
  console.log(` ${chalk_1.default.gray(projectId.padEnd(40))} v${entry.version}`);
148
148
  }
149
149
  console.log();
150
- console.log(chalk_1.default.gray(`💡 Run 'npx claude-recall project list' for detailed registry info`));
150
+ console.log(chalk_1.default.gray(`💡 Run 'claude-recall project list' for detailed registry info`));
151
151
  console.log();
152
152
  }
153
153
  }
@@ -173,19 +173,25 @@ class MCPCommands {
173
173
  return;
174
174
  }
175
175
  const signal = options.force ? 'SIGKILL' : 'SIGTERM';
176
- console.log(`Sending ${signal} to PID ${chalk_1.default.yellow(status.pid)}...`);
176
+ const pid = status.pid;
177
+ console.log(`Sending ${signal} to PID ${chalk_1.default.yellow(pid)}...`);
177
178
  try {
178
- this.processManager.killProcess(status.pid, options.force || false);
179
- this.processManager.removePidFile(projectId);
180
- // Give it a moment to shut down
181
- await new Promise(resolve => setTimeout(resolve, 1000));
182
- // Verify it stopped
183
- const newStatus = this.processManager.getServerStatus(projectId);
184
- if (!newStatus.isRunning) {
179
+ this.processManager.killProcess(pid, options.force || false);
180
+ // Verify death against the SAVED pid, polling up to 5s. The previous
181
+ // code removed the PID file first and then "verified" by re-reading it,
182
+ // so isRunning was always false and "stopped successfully" printed even
183
+ // if the process ignored SIGTERM.
184
+ const deadline = Date.now() + 5000;
185
+ while (this.processManager.isProcessRunning(pid) && Date.now() < deadline) {
186
+ await new Promise(resolve => setTimeout(resolve, 100));
187
+ }
188
+ if (!this.processManager.isProcessRunning(pid)) {
189
+ this.processManager.removePidFile(projectId);
185
190
  console.log(chalk_1.default.green('✓ Server stopped successfully'));
186
191
  }
187
192
  else {
188
- console.log(chalk_1.default.yellow('⚠ Server may still be running. Try --force flag.'));
193
+ // Keep the PID file the server is still alive and tracked
194
+ console.log(chalk_1.default.yellow(`⚠ PID ${pid} is still running after ${signal}. Try --force flag.`));
189
195
  }
190
196
  }
191
197
  catch (error) {
@@ -208,7 +214,7 @@ class MCPCommands {
208
214
  await new Promise(resolve => setTimeout(resolve, 2000));
209
215
  }
210
216
  console.log('\nTo start the server, run:');
211
- console.log(chalk_1.default.cyan(' npx claude-recall mcp start'));
217
+ console.log(chalk_1.default.cyan(' claude-recall mcp start'));
212
218
  console.log();
213
219
  console.log(chalk_1.default.gray('Note: The MCP server is normally started automatically by Claude Code.'));
214
220
  console.log(chalk_1.default.gray(' You only need to run this manually for debugging purposes.'));