sweet-search 2.4.2 → 2.5.2

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.
Files changed (46) hide show
  1. package/core/cli.js +43 -5
  2. package/core/embedding/embedding-cache.js +266 -18
  3. package/core/embedding/embedding-service.js +45 -9
  4. package/core/graph/graph-expansion.js +52 -12
  5. package/core/graph/graph-extractor.js +30 -1
  6. package/core/indexing/ast-chunker.js +331 -16
  7. package/core/indexing/chunking/chunk-builder.js +34 -1
  8. package/core/indexing/index-codebase-v21.js +31 -2
  9. package/core/indexing/index.js +6 -3
  10. package/core/indexing/indexer-ann.js +45 -6
  11. package/core/indexing/indexer-build.js +9 -1
  12. package/core/indexing/indexer-phases.js +6 -4
  13. package/core/indexing/indexing-file-policy.js +140 -0
  14. package/core/indexing/li-skip-policy.js +11 -220
  15. package/core/infrastructure/codebase-repository.js +21 -0
  16. package/core/infrastructure/config/embedding.js +20 -1
  17. package/core/infrastructure/config/graph.js +2 -2
  18. package/core/infrastructure/config/ranking.js +10 -0
  19. package/core/infrastructure/config/vector-store.js +1 -1
  20. package/core/infrastructure/coreml-cascade.js +236 -30
  21. package/core/infrastructure/coreml-cascade.json +25 -0
  22. package/core/infrastructure/index.js +17 -0
  23. package/core/infrastructure/init-config.js +216 -0
  24. package/core/infrastructure/language-patterns/registry-core.js +18 -0
  25. package/core/infrastructure/model-registry.js +12 -0
  26. package/core/infrastructure/native-inference.js +143 -51
  27. package/core/infrastructure/tree-sitter-provider.js +92 -2
  28. package/core/ranking/cascaded-scorer.js +6 -2
  29. package/core/ranking/file-kind-ranking.js +264 -0
  30. package/core/ranking/late-interaction-index.js +10 -4
  31. package/core/ranking/late-interaction-policy.js +304 -0
  32. package/core/search/context-expander.js +267 -28
  33. package/core/search/index.js +4 -0
  34. package/core/search/search-cli.js +3 -1
  35. package/core/search/search-pattern.js +4 -3
  36. package/core/search/search-postprocess.js +189 -8
  37. package/core/search/search-read-semantic.js +734 -0
  38. package/core/search/search-read.js +481 -0
  39. package/core/search/search-server.js +153 -5
  40. package/core/search/sweet-search.js +133 -16
  41. package/core/start-server.js +13 -2
  42. package/mcp/server.js +41 -0
  43. package/mcp/tool-handlers.js +117 -6
  44. package/package.json +9 -7
  45. package/scripts/init.js +386 -5
  46. package/scripts/uninstall.js +152 -6
@@ -132,15 +132,23 @@ function getModelCacheDirs(initConfig) {
132
132
  * return an empty array — uninstall doesn't print a "removing 0 B"
133
133
  * line.
134
134
  */
135
- function getCoremlCascadeRemovals() {
135
+ export function getCoremlCascadeRemovals() {
136
136
  const removals = [];
137
137
  try {
138
138
  const root = getCoremlCascadeRoot();
139
139
  if (existsSync(root)) {
140
140
  const state = getCoremlCascadeState();
141
+ // Sum across all advertised families — embed + standard LI + LI-edge.
142
+ // The earlier label only counted embed + standard LI (12 on the
143
+ // shipping spec) which contradicted init's "18 variants ready"
144
+ // (6 embed + 6 LI + 6 LI-edge). `liEdgeTotal` is 0 on hosts whose
145
+ // spec doesn't advertise the edge family, so older specs still
146
+ // collapse to the prior 12-count behaviour without ceremony.
147
+ const totalAll = state.embedTotal + state.liTotal + state.liEdgeTotal;
148
+ const presentAll = state.embedPresent + state.liPresent + state.liEdgePresent;
141
149
  const label = state.complete
142
- ? `coreml cascade (${state.embedTotal + state.liTotal} variants complete)`
143
- : `coreml cascade (${state.embedPresent + state.liPresent}/${state.embedTotal + state.liTotal} variants partial)`;
150
+ ? `coreml cascade (${totalAll} variants complete)`
151
+ : `coreml cascade (${presentAll}/${totalAll} variants partial)`;
144
152
  removals.push({ label, path: root, size: dirSize(root), type: 'coreml-cascade' });
145
153
  }
146
154
  } catch {
@@ -213,6 +221,70 @@ export function stopRunningDaemon({
213
221
  return result;
214
222
  }
215
223
 
224
+ /**
225
+ * Remove the index-maintainer daemon hook init copied into
226
+ * `.claude/hooks/index-maintainer.mjs`. Only removes the file when it
227
+ * matches the bytes init shipped — never deletes a user-modified file
228
+ * we don't own. The marker is the source path: init does
229
+ * `copyFileSync(<pkg>/core/indexing/index-maintainer.mjs, dest)`, so
230
+ * we compare destination bytes to the package source.
231
+ *
232
+ * Returns `{ status, detail }`:
233
+ * not-found — file absent (nothing to do)
234
+ * removed — file removed (matched shipped bytes)
235
+ * skipped — file present but contents differ (user-modified) — left intact
236
+ * dry-run — found the file but skipped the delete
237
+ * error — rm or read failed; uninstall continues
238
+ */
239
+ export function removeIndexMaintainerHook(projectRoot, { dryRun = false } = {}) {
240
+ const hookPath = join(projectRoot, '.claude', 'hooks', 'index-maintainer.mjs');
241
+ if (!existsSync(hookPath)) {
242
+ return { status: 'not-found', detail: 'no .claude/hooks/index-maintainer.mjs' };
243
+ }
244
+
245
+ // Only remove when the bytes match the version init shipped — refuses to
246
+ // delete a hook the user has customized. Failing the byte compare is a
247
+ // soft skip, not an error.
248
+ const shippedPath = join(PACKAGE_ROOT, 'core', 'indexing', 'index-maintainer.mjs');
249
+ let bytesMatch = false;
250
+ try {
251
+ if (existsSync(shippedPath)) {
252
+ const a = readFileSync(hookPath);
253
+ const b = readFileSync(shippedPath);
254
+ bytesMatch = a.length === b.length && a.equals(b);
255
+ }
256
+ } catch {
257
+ // Read errored on either side — treat as "don't remove, surface the
258
+ // file path so the user can clean up manually if they want to".
259
+ return { status: 'skipped', detail: `cannot compare bytes (${hookPath})` };
260
+ }
261
+
262
+ if (!bytesMatch) {
263
+ return {
264
+ status: 'skipped',
265
+ detail: `${hookPath} differs from shipped version — leaving in place (delete manually if intended)`,
266
+ };
267
+ }
268
+
269
+ if (dryRun) {
270
+ return { status: 'dry-run', detail: hookPath };
271
+ }
272
+
273
+ try {
274
+ unlinkSync(hookPath);
275
+ // Best-effort: prune the parent .claude/hooks/ if it's now empty (we
276
+ // own the file, not the directory; only delete if WE made it empty).
277
+ try {
278
+ const parent = dirname(hookPath);
279
+ const entries = readdirSync(parent);
280
+ if (entries.length === 0) rmdirSync(parent);
281
+ } catch { /* ignore — sibling files exist or rmdir failed */ }
282
+ return { status: 'removed', detail: hookPath };
283
+ } catch (err) {
284
+ return { status: 'error', detail: err.message };
285
+ }
286
+ }
287
+
216
288
  /**
217
289
  * Remove the sweet-search /sweet-index skill from `.claude/skills/sweet-index/`.
218
290
  * Only removes the directory we created — leaves `.claude/skills/` and `.claude/`
@@ -342,6 +414,39 @@ export function removePrewarmSessionStartHook(projectRoot, { dryRun = false } =
342
414
  return { status: 'removed', detail: `spliced out ${sessionStart.length - filtered.length} entry` };
343
415
  }
344
416
 
417
+ // ---------------------------------------------------------------------------
418
+ // Optional native package list (derived from package.json)
419
+ // ---------------------------------------------------------------------------
420
+
421
+ /**
422
+ * Return the list of `@sweet-search/native-*` packages declared as
423
+ * `optionalDependencies` in package.json. `--purge` walks this list so
424
+ * additions (e.g. CUDA variants) are picked up automatically without
425
+ * having to keep two hand-maintained lists in sync.
426
+ *
427
+ * Falls back to a hard-coded list if package.json is unreadable, so a
428
+ * partial install still gets best-effort purge coverage.
429
+ */
430
+ export function getOptionalNativePackageNames() {
431
+ try {
432
+ const pkgPath = join(PACKAGE_ROOT, 'package.json');
433
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
434
+ const deps = pkg.optionalDependencies || {};
435
+ const out = Object.keys(deps).filter((n) => n.startsWith('@sweet-search/'));
436
+ if (out.length > 0) return out;
437
+ } catch { /* fall through to baseline */ }
438
+ // Baseline keeps the prior behaviour PLUS the CUDA variants that were
439
+ // missing from the pre-Phase-7 hand-maintained list.
440
+ return [
441
+ '@sweet-search/native-darwin-arm64',
442
+ '@sweet-search/native-darwin-x64',
443
+ '@sweet-search/native-linux-arm64-gnu',
444
+ '@sweet-search/native-linux-arm64-gnu-cuda',
445
+ '@sweet-search/native-linux-x64-gnu',
446
+ '@sweet-search/native-linux-x64-gnu-cuda',
447
+ ];
448
+ }
449
+
345
450
  // ---------------------------------------------------------------------------
346
451
  // Help text
347
452
  // ---------------------------------------------------------------------------
@@ -369,6 +474,8 @@ What gets removed:
369
474
  artifacts AND the sibling .mlmodelc compiled cache files next to
370
475
  each variant. Skipped by --keep-models.
371
476
  - .claude/skills/sweet-index/ (the per-project /sweet-index skill copy)
477
+ - .claude/hooks/index-maintainer.mjs (init-installed). User-modified
478
+ copies are detected via a byte-compare and left in place.
372
479
  - daemon-prewarm SessionStart entry inside .claude/settings.json
373
480
 
374
481
  What is NOT removed:
@@ -436,8 +543,15 @@ export async function runUninstall(args) {
436
543
  const skillPreview = removeSweetIndexSkill(projectRoot, { dryRun: true });
437
544
  const hasSkillEntry = skillPreview.status === 'dry-run';
438
545
 
546
+ // Check for the index-maintainer daemon hook init copies into
547
+ // `.claude/hooks/index-maintainer.mjs`. Same dry-run pattern.
548
+ const indexMaintainerPreview = removeIndexMaintainerHook(projectRoot, { dryRun: true });
549
+ const hasIndexMaintainerHook = indexMaintainerPreview.status === 'dry-run';
550
+ const indexMaintainerSkippedReason =
551
+ indexMaintainerPreview.status === 'skipped' ? indexMaintainerPreview.detail : null;
552
+
439
553
  // Nothing to remove?
440
- if (removals.length === 0 && !hasHookEntry && !hasSkillEntry) {
554
+ if (removals.length === 0 && !hasHookEntry && !hasSkillEntry && !hasIndexMaintainerHook) {
441
555
  console.log('Nothing to remove — Sweet Search is not initialized in this project.');
442
556
  return;
443
557
  }
@@ -457,6 +571,11 @@ export async function runUninstall(args) {
457
571
  if (hasSkillEntry) {
458
572
  console.log(` /sweet-index skill (.claude/skills/sweet-index/)`);
459
573
  }
574
+ if (hasIndexMaintainerHook) {
575
+ console.log(` index-maintainer hook (.claude/hooks/index-maintainer.mjs)`);
576
+ } else if (indexMaintainerSkippedReason) {
577
+ console.log(` [skipped] ${indexMaintainerSkippedReason}`);
578
+ }
460
579
  console.log(` Total: ${formatBytes(totalBytes)}`);
461
580
  if (parsed.keepModels) {
462
581
  console.log(' Model cache: kept (--keep-models)');
@@ -472,6 +591,12 @@ export async function runUninstall(args) {
472
591
  if (drySkill.status === 'dry-run') {
473
592
  console.log(` Would also remove: /sweet-index skill (${drySkill.detail})`);
474
593
  }
594
+ const dryMaintainer = removeIndexMaintainerHook(projectRoot, { dryRun: true });
595
+ if (dryMaintainer.status === 'dry-run') {
596
+ console.log(` Would also remove: index-maintainer hook (${dryMaintainer.detail})`);
597
+ } else if (dryMaintainer.status === 'skipped') {
598
+ console.log(` Would skip: index-maintainer hook — ${dryMaintainer.detail}`);
599
+ }
475
600
  console.log('Dry run — nothing was removed.');
476
601
  return;
477
602
  }
@@ -540,6 +665,21 @@ export async function runUninstall(args) {
540
665
  }
541
666
  // 'not-found' and 'dry-run' are silent in the main output.
542
667
 
668
+ // Reverse the index-maintainer daemon hook init copied into
669
+ // .claude/hooks/index-maintainer.mjs. Bytes-match check inside the
670
+ // helper guarantees we never delete a user-customised file.
671
+ const indexMaintainerResult = removeIndexMaintainerHook(projectRoot, { dryRun: parsed.dryRun });
672
+ if (indexMaintainerResult.status === 'removed') {
673
+ console.log(` Removed: index-maintainer hook (${indexMaintainerResult.detail})`);
674
+ removed++;
675
+ } else if (indexMaintainerResult.status === 'skipped') {
676
+ console.log(` Kept: index-maintainer hook — ${indexMaintainerResult.detail}`);
677
+ kept++;
678
+ } else if (indexMaintainerResult.status === 'error') {
679
+ console.log(` Failed to remove index-maintainer hook: ${indexMaintainerResult.detail}`);
680
+ kept++;
681
+ }
682
+
543
683
  // Stop any daemon that an earlier SessionStart hook spawned. Otherwise the
544
684
  // old daemon keeps running and holding the socket after uninstall, which
545
685
  // surprises users. Never throws — `stopRunningDaemon` swallows every error.
@@ -556,11 +696,17 @@ export async function runUninstall(args) {
556
696
  console.log('');
557
697
  console.log(' Purging npm packages...');
558
698
  try {
559
- execSync('npm uninstall sweet-search @sweet-search/native-darwin-arm64 @sweet-search/native-darwin-x64 @sweet-search/native-linux-x64-gnu @sweet-search/native-linux-arm64-gnu 2>/dev/null || true', {
699
+ const pkgs = ['sweet-search', ...getOptionalNativePackageNames()];
700
+ // Use shell-form so non-installed packages don't abort the whole
701
+ // command (npm exits non-zero per missing pkg). The OR-true keeps
702
+ // the script alive across npm exit codes from a partially-installed
703
+ // host (e.g. a Linux box without the darwin-* packages).
704
+ const cmd = `npm uninstall ${pkgs.join(' ')} 2>/dev/null || true`;
705
+ execSync(cmd, {
560
706
  cwd: projectRoot,
561
707
  stdio: ['pipe', 'pipe', 'pipe'],
562
708
  });
563
- console.log(' npm packages removed.');
709
+ console.log(` npm packages removed (${pkgs.length} candidates).`);
564
710
  } catch {
565
711
  console.log(' npm uninstall failed (packages may not be installed).');
566
712
  }