sweet-search 2.5.10 β†’ 2.5.12

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
@@ -4,9 +4,10 @@
4
4
 
5
5
  ### *Maybe grep isn't all you need…* 🍬
6
6
 
7
- **A local-first hybrid code-search engine built for AI coding agents.**
8
- Semantic + lexical + structural search over your working tree, GPU-accelerated local inference,
9
- and an evolved system prompt that teaches your agent to use it all β€” even on plain CPU.
7
+
8
+ Every AI coding agent of today is stuck believing grep+Read is the way... ***sweet-search*** challenges the narrative 😎
9
+
10
+ A 100% local code-search engine for *Claude Code*, *Codex*, *Cursor* & friends with six blazing and purpose-built tools that hand your agent ranked, ready-to-use answers instead of walls of grep output. Up to 34% cheaper, 56% fewer tool calls, more useful answers, SOTA retrieval quality, zero API keys.
10
11
 
11
12
  [![npm](https://img.shields.io/npm/v/sweet-search?color=cb3837&label=npm)](https://www.npmjs.com/package/sweet-search)
12
13
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
@@ -18,29 +19,16 @@ and an evolved system prompt that teaches your agent to use it all β€” even on p
18
19
 
19
20
  ---
20
21
 
21
- Your AI agent burns most of its tokens *looking* for code: grep, read, grep again, read more.
22
- **sweet-search** replaces that loop with six purpose-built tools that return ranked, self-contained answers β€”
23
- backed by a Rust/WASM engine, ColBERT-style late interaction, a code knowledge graph, and an index that
24
- updates itself as you type.
25
-
26
- <div align="center">
27
-
28
- **10.2Γ—** ripgrep's median grep speed &nbsp;Β·&nbsp; **2.9 ms** warm queries &nbsp;Β·&nbsp; **47Γ—** faster reranking kernels &nbsp;Β·&nbsp; **0** API keys
29
-
30
- <sub>measured in-repo β€” sources in [Benchmarks](#-benchmarks)</sub>
31
-
32
- </div>
33
-
34
22
  ## ✨ Highlights
35
23
 
36
- - **Hybrid retrieval** β€” BM25F lexical + dense semantic + structural graph signals, fused per query by a CatBoost router running in WASM
37
- - **Agent-native by design** β€” token-budgeted output tiers, an MCP server, and a GEPA-evolved system prompt installed into Claude Code, Codex, Gemini CLI, and Cursor with one command
38
- - **Indexed grep, ~10Γ— ripgrep** β€” a sparse n-gram prefilter skips the files that provably can't match
24
+ - **Hybrid retrieval** β€” one of the six tools uses BM25F lexical + dense semantic + structural graph signals, fused per query and reranked by late-interaction
25
+ - **Agent-native by design** β€” token-budgeted output tiers, an optional MCP server (and default zero-overhead CLI), and a GEPA-evolved system prompt installed into Claude Code, Codex, Gemini CLI, and Cursor with one command
26
+ - **Indexed grep, ~10Γ— faster than ripgrep** β€” a sparse n-gram prefilter skips the files that provably can't match
39
27
  - **ColBERT-style reranking, locally** β€” per-token MaxSim late interaction on hand-written SIMD kernels
40
- - **Runs on anything** β€” Apple Metal, CUDA, CoreML Neural Engine, or plain CPU via INT8 ONNX; same engine, auto-selected
41
- - **Never stale** β€” a reconcile daemon keeps the index converged with your *working tree*, uncommitted edits included
42
- - **Fits in RAM** β€” INT4-quantized binary index segments and memory-mapped HNSW
43
- - **Local-first** β€” all models run on-device; nothing is sent anywhere, ever
28
+ - **GPU-accelerated indexing** β€” Apple Metal, CUDA, CoreML Neural Engine, or plain CPU via ORT; same engine, auto-selected
29
+ - **Never stale** β€” incremental indexing keeps the index aligned with your *working tree*, uncommitted edits included
30
+ - **No storage hassle** β€” indexed artifacts maximally optimized without any accuracy tradeoff; up to INT4 quantization
31
+ - **Local-first** β€” all models run on-device; nothing is sent anywhere, ever. CPU-inference supported for all models
44
32
 
45
33
  ## πŸ“š Table of Contents
46
34
 
@@ -146,13 +134,13 @@ We measure sweet-search four ways β€” from how much it helps a real agent down t
146
134
  <tr>
147
135
  <td width="50%" valign="top">
148
136
 
149
- πŸ€– **[β‘  Code-retrieval](#bench-code-retrieval)** *(agent-in-the-loop)*<br>
137
+ πŸ€– **β‘  [Code-retrieval](#bench-code-retrieval)** *(agent-in-the-loop)*<br>
150
138
  <sub>Does it make a real coding agent **cheaper and more useful** when it searches your repo? Paired against each model's own grep-and-read loop.</sub>
151
139
 
152
140
  </td>
153
141
  <td width="50%" valign="top">
154
142
 
155
- 🚧 **[β‘‘ Task-completion](#bench-task-completion)** *(coming soon)*<br>
143
+ 🚧 **β‘‘ [Task-completion](#bench-task-completion)** *(coming soon)*<br>
156
144
  <sub>Does cheaper, denser context **compound** into a higher resolve-rate on multi-step engineering tasks? Harness in progress.</sub>
157
145
 
158
146
  </td>
@@ -160,13 +148,13 @@ We measure sweet-search four ways β€” from how much it helps a real agent down t
160
148
  <tr>
161
149
  <td width="50%" valign="top">
162
150
 
163
- πŸ“„ **[β‘’ Paper-type IR](#bench-paper-type)** *(academic)*<br>
151
+ πŸ“„ **β‘’ [Paper-type IR](#bench-paper-type)** *(academic)*<br>
164
152
  <sub>The standard NLβ†’code retrieval suites (GCSN, M2CRB, CoSQA…), full-corpus MRR@10.</sub>
165
153
 
166
154
  </td>
167
155
  <td width="50%" valign="top">
168
156
 
169
- ⚑ **[β‘£ Engine speed](#bench-engine-speed)**<br>
157
+ ⚑ **β‘£ [Engine speed](#bench-engine-speed)**<br>
170
158
  <sub>Raw systems numbers β€” grep throughput, query latency, rerank kernels, HNSW.</sub>
171
159
 
172
160
  </td>
@@ -233,43 +221,52 @@ The win is **harness-adaptive**: where the native loop is disciplined (Claude Co
233
221
  <a id="bench-paper-type"></a>
234
222
  ### πŸ“„ 3. Paper-type retrieval benchmarks β€” *academic NLβ†’code IR*
235
223
 
236
- > [!NOTE]
237
- > πŸ”„ **Refreshed on the current engine (June 2026).** AdvTest, CoIR, CoSQA, and M2CRB were just
238
- > re-run on the latest build β€” the one with the late-interaction correctness fixes, HNSW tuning,
239
- > and the May 2026 ranking overhaul β€” and every one of them moved **up**. GCSN, CoSQA+, and CLARC
240
- > were already current. Reproduction artifacts are in [`eval/results/`](eval/results/).
241
-
242
- Every number below is the **`ss-search` pipeline end-to-end** β€” the same binary you install, querying
243
- against the **full corpus** (no 99-distractor shortcuts), on an M3 Max.
244
-
245
- | πŸ“š Benchmark | πŸ” What it tests | # Queries | 🎯 MRR@10 |
246
- |-----------|---------------|--------:|-------:|
247
- | 🌐 **GenCodeSearchNet** | NLβ†’code, 6 languages | 6,000 | **86.6** |
248
- | πŸ—ΊοΈ **M2CRB** | multilingual NLβ†’code (ES/PT/DE/FR β†’ Py/Java/JS) | 2,814 | **65.9** |
249
- | 🐍 CoSQA (test split) | web queries β†’ Python | 500 | 98.8 |
250
- | 🐍 CoSQA+ | web queries β†’ Python, multi-match | 20,604 | 72.1 |
251
- | βš™οΈ CLARC | NLβ†’C/C++ (systems code) | 1,245 | 67.4 |
252
- | πŸ›‘οΈ AdvTest | adversarially renamed Python | 1,000 | **99.1** |
253
- | 🌍 CoIR | 10 datasets, 14 languages | 4,500 | **72.4** |
254
-
255
- **GenCodeSearchNet: the strongest result published anywhere, as far as we can tell.** The benchmark's
256
- own paper tops out at MRR ≀ 0.42 for its fine-tuned baselines (and ≀ 0.10 on the cross-lingual subsets),
257
- with zero-shot OpenAI Ada-2 at 0.79–0.94 β€” and those are measured against **99 random distractors per
258
- query**. sweet-search scores **0.866**, retrieving from the entire 6,000-document corpus.
259
-
260
- **M2CRB: best published number, no fine-tuning.** The benchmark paper's best model β€” a CodeBERT
261
- *fine-tuned on the task's training mix* β€” reaches 52.7 (auMRRc, a metric averaged over smaller retrieval
262
- pools). sweet-search reaches **65.9 full-corpus MRR@10 out of the box**, on Spanish, Portuguese, German,
263
- and French queries.
224
+ Every number below is the **`ss-search` pipeline end-to-end** β€” the same binary you install β€” run
225
+ against the **full benchmark corpus** (no 99-distractor shortcuts), **zero-shot** (we never
226
+ fine-tune on these tasks). Where a benchmark's queries are docstrings, we strip the docstring out of the
227
+ indexed code so the query can't trivially match itself β€” the standard retrieval protocol.
228
+
229
+ We're SOTA in June 2026 on 3/4 attempted benchmarks at HARDER settings (running on full pool) than most other attempts!
230
+
231
+ | πŸ“š Benchmark | πŸ” What it tests | # Queries | πŸ“‚ Pool | 🎯 MRR@10 | πŸ† SOTA? |
232
+ |-----------|---------------|---------:|---------:|--------:|--------:|
233
+ | 🌐 **GenCodeSearchNet** | NLβ†’code, 6 languages | 6,000 | full 6,000 | **86.6** | YES βœ… |
234
+ | 🐍 **CoSQA** | web queries β†’ Python | 500 | full 6,267 | **65.5** | βœ… (zero-shot) |
235
+ | πŸ—ΊοΈ M2CRB | multilingual NLβ†’code (ES/PT/DE/FR β†’ Py/Java/JS) | 5,795 | full 5,795 | 54.0 | YES βœ… |
236
+ | πŸ›‘οΈ AdvTest | adversarial, identifier-obfuscated Python | 19,210 | full 19,210 | 51.4 | NO ❌ |
237
+
238
+ <sub>SOTA = best result we can find in the published literature as of June 2026; cross-metric/protocol comparisons are spelled out per benchmark below.</sub>
239
+
240
+ #### 🌐 GenCodeSearchNet β†’ `86.6` &nbsp;Β·&nbsp; πŸ† SOTA in June 2026
241
+ - **The BEST PUBLISHED number we can find, anywhere**
242
+ - The benchmark's own paper caps at **MRR ≀ 0.42** for fine-tuned baselines (≀ 0.10 cross-lingual); even zero-shot OpenAI Ada-2 reaches 0.79–0.94 β€” but **all of it against a tiny 99-distractor pool**.
243
+ - We score **0.866 against the entire 6,000-document corpus** β€” *a strictly harder setting* β€” and **zero-shot**. πŸ”₯
244
+
245
+ #### 🐍 CoSQA β†’ `65.5` &nbsp;Β·&nbsp; πŸ₯‡ Zero-shot SOTA in June 2026
246
+ - **Beats EVERY PUBLISHED zero-shot model**
247
+ - Canonical setup: 500 real web queries β†’ the fixed **6,267-code database**, no fine-tuning.
248
+ - Clears the strongest zero-shot results out there β€” CodeSage-Large `47.5` Β· OpenAI text-embedding-3-large `55.4` Β· OASIS `55.8` β€” and goes **toe-to-toe with *fine-tuned* CodeBERT / GraphCodeBERT** (64.7 / 67.5). πŸ’ͺ
249
+ - <sub>CoSQA has known label noise, so we read the absolute height with a pinch of salt.</sub>
250
+
251
+ #### πŸ—ΊοΈ M2CRB β†’ `54.0` &nbsp;Β·&nbsp; πŸ† SOTA in June 2026
252
+ - **the BEST PUBLISHED number we can find, anywhere** β€” and zero-shot
253
+ - πŸ‡ͺπŸ‡Έ Spanish Β· πŸ‡΅πŸ‡Ή Portuguese Β· πŸ‡©πŸ‡ͺ German Β· πŸ‡«πŸ‡· French β†’ Python / Java / JavaScript.
254
+ - The paper's best β€” a CodeBERT **fine-tuned on the task** β€” reaches **52.7 auMRRc**, a metric that *averages over easier, smaller pools* (so `auMRRc β‰₯ full-pool MRR` for any model). Our **54.0 is full-pool MRR@10** over all 5,795 functions in one pool β€” a **strictly harder** measure, cleared with **no fine-tuning**. πŸ”₯
255
+
256
+ #### πŸ›‘οΈ AdvTest β†’ `51.4` &nbsp;Β·&nbsp; πŸ§ͺ **our honest worst case β€” and we publish it anyway**
257
+ - Adversarial obfuscation (`def Func(arg_0):`) deletes the lexical + graph signals our hybrid feeds on β€” yet we still **beat the classic fine-tuned baselines** (CodeBERT `27` Β· GraphCodeBERT `35` Β· UniXcoder `41`), and our stack *still lifts our own encoder ~3pp even here*.
258
+ - πŸ” **Full transparency:** we could **not** reproduce the often-cited `59.5` for the bare CodeRankEmbed encoder β€” the *reference FP32 model* scores **54.7** on our leak-free corpus, our shipped INT8 build **51.4**. The gap is stricter preprocessing + INT8 quantization, **not** the retrieval pipeline. We report exactly what we measured.
264
259
 
265
260
  <details>
266
- <summary><b>Methodology & build dates</b></summary>
261
+ <summary><b>Methodology, protocol & honesty notes</b></summary>
267
262
 
268
- - **Reproduction:** result artifacts live in `eval/results/`; rerun via `eval/run_all.js`.
269
- - **Protocol note:** published baselines for GCSN and CoSQA-style benchmarks typically rank the gold snippet against 99 sampled distractors. All sweet-search numbers rank against the full benchmark corpus β€” strictly harder.
270
- - **Build dates:** AdvTest, CoIR, CoSQA, and M2CRB were re-run on the **June 2026** engine (0 errors on each); GCSN, CoSQA+, and CLARC are from the May 2026 build. All numbers reflect the current late-interaction pipeline β€” the correctness fixes, HNSW tuning, and May ranking overhaul. The June re-runs all improved over their earlier builds (AdvTest 91.5β†’99.1, CoIR 57.3β†’72.4, CoSQA 97.0β†’98.8, M2CRB 60.2β†’65.9).
271
- - **Honesty corner:** CrossCodeEval β€” cross-file *completion context* retrieval, a different task than NL search β€” sits at 0.12. We don't optimize for it and report it anyway.
272
- - Dates and per-language breakdowns: [`docs/BENCHMARKS_EXPLAINED.md`](docs/BENCHMARKS_EXPLAINED.md).
263
+ - **Reproduction:** result artifacts live in [`eval/results/`](eval/results/); rerun via `eval/run_all.js`. The canonical full-pool loaders are in `eval/download_data.py`.
264
+ - **Full corpus, not distractors.** Published baselines for GCSN- and CoSQA-style benchmarks typically rank the gold against 99 sampled distractors; every number here ranks against the benchmark's *full* corpus (6k–19k candidates) β€” strictly harder.
265
+ - **Zero-shot + docstring-stripped.** We never fine-tune on these tasks. For docstring-derived benchmarks (AdvTest, M2CRB) we strip the docstring from the indexed code β€” otherwise the NL query matches itself verbatim (a no-strip AdvTest run scores a meaningless 0.98). This is the standard protocol; it is also why our AdvTest is lower than naΓ―ve setups that leave the docstring in.
266
+ - **What we deliberately don't claim yet.** CoIR (official metric NDCG@10 over per-subtask corpora up to ~1M docs), CoSQA+ (multi-positive, MAP-primary), and CLARC (per-group pools) use protocols and metrics our single-pool MRR@10 harness doesn't currently match. Rather than publish apples-to-oranges numbers, we omit them; faithful per-subtask CoIR (NDCG@10) runs are queued.
267
+ - **M2CRB** β€” the paper's metric is *auMRRc* (area under the MRR-vs-pool-size curve; best published **52.7**, fine-tuned). Because that area averages over easier small pools, `auMRRc β‰₯ full-pool MRR` for any model β€” so our **54.0 full-pool MRR@10** (all 5,795 functions, zero-shot) clears their best on a strictly harder measure. No one publishes a plain full-corpus MRR@10 on M2CRB, so ours is the best available.
268
+ - **AdvTest honesty note.** We could not reproduce the commonly-cited 59.5 for the bare CodeRankEmbed encoder on our corpus: the reference FP32 model scores 54.7 on our leak-free, docstring-stripped, full-19,210 setup, and our shipped INT8 build 51.4. We report our measured numbers and the reference check rather than the leaderboard figure.
269
+ - **Honesty corner:** CrossCodeEval β€” cross-file *completion-context* retrieval, a different task than NL search β€” sits at 0.12. We don't optimize for it and report it anyway.
273
270
 
274
271
  </details>
275
272
 
@@ -314,7 +311,8 @@ to be *consumed by an agent* β€” a useful answer, not a wall of matches to scrol
314
311
 
315
312
  A hybrid search pipeline with late interaction reranking that returns actual code blocks.
316
313
 
317
- SOTA in several published [`benchmarks`](#-benchmarks).
314
+ Leading published-benchmark results β€” strongest we can find on GenCodeSearchNet, and above every published
315
+ zero-shot model on CoSQA. See [`benchmarks`](#-benchmarks).
318
316
 
319
317
  ```mermaid
320
318
  flowchart TD
@@ -733,12 +731,13 @@ the three-stage retrieval it feeds at query time.
733
731
  sweet-search meets your agent wherever it is β€” shell tools, MCP, or injected instructions:
734
732
 
735
733
  ```jsonc
736
- // .claude/mcp.json β€” that's the whole integration
734
+ // .mcp.json (project root) β€” that's the whole integration
735
+ // or just run: sweet-search init --mcp
737
736
  {
738
737
  "mcpServers": {
739
738
  "sweet-search": {
740
739
  "command": "npx",
741
- "args": ["sweet-search-mcp", "--project-root", "/absolute/path/to/your/repo"]
740
+ "args": ["-y", "sweet-search-mcp", "--project-root", "/absolute/path/to/your/repo"]
742
741
  }
743
742
  }
744
743
  }
@@ -797,6 +796,16 @@ sweet-search stands on a lot of shoulders, and we'd rather name them than preten
797
796
 
798
797
  <div align="center">
799
798
 
800
- **If sweet-search saves your agent's tokens, a ⭐ helps other agents' humans find it.**
799
+ ### Found it useful?
800
+
801
+ If sweet-search saves your agent's tokens, a ⭐ helps other agents' humans find it.
802
+
803
+ <a href="https://github.com/mrsladoje/sweet-search">
804
+ <img src="https://img.shields.io/badge/⭐%20Star%20sweet--search%20on%20GitHub-181717?style=for-the-badge&logo=github&logoColor=white" alt="Star sweet-search on GitHub" />
805
+ </a>
806
+
807
+ <br/><br/>
808
+
809
+ [![GitHub stars](https://img.shields.io/github/stars/mrsladoje/sweet-search?style=social)](https://github.com/mrsladoje/sweet-search/stargazers)
801
810
 
802
811
  </div>
@@ -86,6 +86,10 @@ export async function showBanner(opts = {}) {
86
86
  try {
87
87
  if (!shouldRender(out, env)) return false;
88
88
 
89
+ // Clear the screen first so a tall banner can't half-overflow / strand the
90
+ // shell prompt mid-image (home + erase screen + erase scrollback, like `clear`).
91
+ if (opts.clear) out.write('\x1b[H\x1b[2J\x1b[3J');
92
+
89
93
  const sharp = require('sharp');
90
94
  const man = JSON.parse(readFileSync(join(ASSET_DIR, 'banner-manifest.json'), 'utf8'));
91
95
  const { gridCols: GC, cellW: CW, cellH: CH, count: N, frameMs } = man;
@@ -123,7 +123,7 @@ async function main() {
123
123
 
124
124
  // Animated banner (best-effort; interactive TTY only, never in CI / quiet / stdin-fed runs).
125
125
  if (!quiet && !help && !filesFromStdin && process.stdout.isTTY && !process.env.CI && !process.env.NO_BANNER && !process.env.SWEET_SEARCH_NO_BANNER) {
126
- try { const { showBanner } = await import('../banner/render-banner.js'); await showBanner(); } catch { /* non-fatal */ }
126
+ try { const { showBanner } = await import('../banner/render-banner.js'); await showBanner({ clear: true }); } catch { /* non-fatal */ }
127
127
  }
128
128
 
129
129
  // Apply late interaction model overrides before any model code runs.
@@ -0,0 +1,51 @@
1
+ ---
2
+ variant: mcp
3
+ derived_from: p7-v1-mpp
4
+ source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/Mpp.md
5
+ benchmarked: false
6
+ note: >-
7
+ Hand-derived MCP-tool variant of the frozen M++ champion (p7-v1-mpp). The
8
+ STRATEGY core β€” routing by what-you-hold, trust-the-top-hit, sufficiency
9
+ stops, the two-probe absence rule, the <state_summary> gate, and the output
10
+ discipline β€” is preserved (semantics intact; the <state_summary> gate and
11
+ Output section are byte-identical, the rest carries only the tool-mechanics
12
+ rename). Only the tool-mechanics layer is remapped: the six ss-* CLI commands collapse onto the sweet-search MCP
13
+ tool surface (a UNIFIED `search` that subsumes ss-search/ss-find/ss-grep, plus
14
+ `trace` / `read-semantic` / `read` / `repo-map`). The anti-raw-scan discipline
15
+ is KEPT and retargeted from "ss-* vs raw grep" to "sweet-search tools vs native
16
+ Grep/Read/Bash", because an MCP host (e.g. Claude Code) still exposes native
17
+ file tools alongside the server.
18
+ NOT yet validated on held-out / OOD / vault against the MCP transport. Do NOT
19
+ treat its quality as equivalent to the CLI champion until a P7 eval is run
20
+ through MCP. Until then this ships only behind `init --mcp --no-cli`.
21
+ ---
22
+ # Sweet-search β€” code search tool guide
23
+
24
+ Sweet-search indexes the working tree (uncommitted edits too) and searches it faster and cheaper than scanning files by hand. Use the sweet-search tools for all code search and navigation, in preference to native Grep/Read or shell `grep`/`find`/`cat`/`ls` β€” the index covers every file, so a manual scan only re-confirms a sweet-search result at higher cost, never beats it. Reach for a raw read only for an edit too recent to be reconciled (seconds old). Never re-run a sweet-search hit as a native Grep. Any sub-agent you delegate to must use these sweet-search tools, with this system prompt verbatim.
25
+
26
+ ## Tools
27
+ The sweet-search MCP server exposes these (call each by the name your client lists it under):
28
+ - **search** β€” hybrid code search; returns ranked, self-contained code blocks. Your primary tool: it subsumes semantic search, exact-literal search, and semantic-reranked regex. Give it a natural-language query for a concept, or pass a `regex` (or lexical mode) for an exact token.
29
+ - **trace** β€” a symbol's callers, callees and impact in one call.
30
+ - **read-semantic** β€” the top ranked spans of one known file for a query.
31
+ - **read** β€” a narrow file range when you already know the file and lines.
32
+ - **repo-map** β€” a PageRank-scored symbol overview when you need orientation, not a specific answer.
33
+
34
+ ## Open with the cheapest tool for what you hold
35
+ - **An exact token** (identifier, function/class/constant, error string, config key, path you could copy-paste): ONE `search` with that literal as a `regex` (rarest token, escaped). Trust the top hit and stop β€” no natural-language search first, no confirming re-search. One exception: if the top hit is an autogenerated file (a "do not edit" or "@generated" header, or a name like `schema11`/`validateN`), it is a generated copy, not where the value is authored β€” follow it to the real source it is generated from.
36
+ - **Only a behavior or concept**: one `search` in natural language for what you're looking for, then anchor on the symbol that surfaces. Shape it lightly by the target language β€” short and interrogative for JS/TS/Dart, a touch longer with a domain keyword otherwise.
37
+ - **How something flows / dispatches / is called / what a change impacts**: anchor one symbol (a literal, or a `search`), then `trace` it β€” one call returns callers, callees and impact. Prefer callees over impact (especially Python/Ruby/PHP). If a trace is sparse or empty, anchor the downstream symbol with `search` rather than retrying or hand-crawling; never make `trace` the spine of a multi-file search.
38
+
39
+ Trust the top ranked result; confirm with at most one narrow `read`, never a re-run of a matching hit.
40
+
41
+ ## Multi-file
42
+ Chain inside the tools: land the entry file, `read-semantic` it for the import or handoff symbol, then `search` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them β€” or dropping to a native Grep/Read to "just look" β€” is the main multi-file cost trap.
43
+
44
+ ## A confirmed absence is a complete answer
45
+ When what you're looking for may not exist, absence is settled once TWO complementary `search` probes come back empty for the same concept: one in natural language and one as a broad `regex` on its likeliest identifier (a short substring/prefix). A search that returns plausible-but-off-target code is the decoy, not a lead β€” do not chase it. Two empty index probes over the whole codebase are more conclusive than any native scan or file listing, so state the negative and stop: no third synonym, no native `grep`/`ls`/`cat` enumeration.
46
+
47
+ ## Before the third probe
48
+ Before your third sweet-search probe in the current search iteration β€” or before your final answer, whichever comes first β€” output a `<state_summary>` block with exactly: (1) one sentence on what you've established, (2) one sentence on your current blind spot.
49
+
50
+ ## Output
51
+ Stop the instant your evidence answers what you're looking for β€” one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match`.
package/mcp/server.js CHANGED
@@ -115,10 +115,28 @@ const vocabDeps = { coreDir };
115
115
  // MCP Server
116
116
  // ---------------------------------------------------------------------------
117
117
 
118
+ // MCP `instructions` β€” the agent-routing policy delivered to hosts that connect
119
+ // to this server WITHOUT a project file to inject into (Claude Desktop, a remote
120
+ // endpoint). This is the SECONDARY carrier; the primary is the MCP-variant prompt
121
+ // that `sweet-search init --mcp --no-cli` injects into CLAUDE.md/AGENTS.md (the
122
+ // high-salience slot). Best-effort: if the ship-file is missing the server still
123
+ // starts, just without instructions.
124
+ const MCP_INSTRUCTIONS = (() => {
125
+ try {
126
+ const p = path.join(__dirname, '..', 'core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md');
127
+ const raw = readFileSync(p, 'utf8');
128
+ return raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim() || undefined;
129
+ } catch (err) {
130
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] mcp instructions: ${err?.message || err}\n`);
131
+ return undefined;
132
+ }
133
+ })();
134
+
118
135
  const server = new McpServer({
119
136
  name: 'sweet-search',
120
137
  version: PKG_VERSION,
121
138
  }, {
139
+ ...(MCP_INSTRUCTIONS ? { instructions: MCP_INSTRUCTIONS } : {}),
122
140
  capabilities: {
123
141
  tools: { listChanged: false },
124
142
  resources: { subscribe: false, listChanged: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sweet-search",
3
- "version": "2.5.10",
3
+ "version": "2.5.12",
4
4
  "description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
5
5
  "type": "module",
6
6
  "main": "core/search/sweet-search.js",
@@ -61,6 +61,7 @@
61
61
  "scripts/smoke-test.js",
62
62
  "scripts/inject-agent-instructions.js",
63
63
  "scripts/write-claude-rules.js",
64
+ "scripts/install-mcp-server.js",
64
65
  "scripts/install-prompt-reminders.js",
65
66
  "scripts/install-tool-enforcement.js",
66
67
  "scripts/hooks/",
@@ -163,12 +164,12 @@
163
164
  },
164
165
  "optionalDependencies": {
165
166
  "usearch": "^2.21.4",
166
- "@sweet-search/native-darwin-arm64": "2.5.10",
167
- "@sweet-search/native-darwin-x64": "2.5.10",
168
- "@sweet-search/native-linux-arm64-gnu": "2.5.10",
169
- "@sweet-search/native-linux-arm64-gnu-cuda": "2.5.10",
170
- "@sweet-search/native-linux-x64-gnu": "2.5.10",
171
- "@sweet-search/native-linux-x64-gnu-cuda": "2.5.10"
167
+ "@sweet-search/native-darwin-arm64": "2.5.12",
168
+ "@sweet-search/native-darwin-x64": "2.5.12",
169
+ "@sweet-search/native-linux-arm64-gnu": "2.5.12",
170
+ "@sweet-search/native-linux-arm64-gnu-cuda": "2.5.12",
171
+ "@sweet-search/native-linux-x64-gnu": "2.5.12",
172
+ "@sweet-search/native-linux-x64-gnu-cuda": "2.5.12"
172
173
  },
173
174
  "engines": {
174
175
  "node": ">=18.0.0"
package/scripts/init.js CHANGED
@@ -38,9 +38,10 @@ import {
38
38
  import { describeDedupConfig } from '../core/infrastructure/index.js';
39
39
  import { verifyRuntime, getMaxsimTier, getRouterType } from './verify-runtime.js';
40
40
  import { ALL_HARNESSES, injectAgentInstructions } from './inject-agent-instructions.js';
41
- import { writeClaudeRules } from './write-claude-rules.js';
42
- import { installPromptReminderHook } from './install-prompt-reminders.js';
43
- import { installToolEnforcement } from './install-tool-enforcement.js';
41
+ import { writeClaudeRules, removeClaudeRules } from './write-claude-rules.js';
42
+ import { installMcpServer } from './install-mcp-server.js';
43
+ import { installPromptReminderHook, removePromptReminderHook } from './install-prompt-reminders.js';
44
+ import { installToolEnforcement, removeToolEnforcement } from './install-tool-enforcement.js';
44
45
  import { isNativeInferenceAvailable } from '../core/infrastructure/native-inference.js';
45
46
 
46
47
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -83,6 +84,15 @@ export function parseInitArgs(args) {
83
84
  enforceTools: false, // P3: --enforce-tools (default OFF β€” opt-in strict mode)
84
85
  codex: false, // --codex: wire the Codex CLI SessionStart hook
85
86
  codexEnableGlobalHooks: false, // --codex-enable-global-hooks: also enable the flag in ~/.codex/config.toml
87
+ // Contact-surface flags (additive at install, exclusive at consumption):
88
+ // --mcp registers the sweet-search MCP server in the project .mcp.json
89
+ // (additive β€” the CLI stays). Harness-agnostic, root-level.
90
+ // --no-cli makes MCP the agent's *contact surface*: inject the MCP-tool
91
+ // prompt variant instead of the ss-* CLI one, and skip the
92
+ // CLI-surface-specific supplements (rules file, ss-* reminder).
93
+ // Indexing still runs through the CLI/engine. Requires --mcp.
94
+ mcp: false,
95
+ noCli: false,
86
96
  };
87
97
 
88
98
  for (let i = 0; i < args.length; i++) {
@@ -188,12 +198,40 @@ export function parseInitArgs(args) {
188
198
  // P3: opt-in strict mode β€” denies native Grep + installs a Read
189
199
  // hint hook. Opinionated and Claude-specific (per Β§4D).
190
200
  result.enforceTools = true;
201
+ } else if (arg === '--mcp') {
202
+ // Register the sweet-search MCP server in the project root .mcp.json.
203
+ // Additive: the CLI surface stays. Independent of --no-claude.
204
+ result.mcp = true;
205
+ } else if (arg === '--no-cli') {
206
+ // Make MCP the agent's contact surface: inject the MCP-tool prompt
207
+ // variant and skip the CLI-surface supplements. Requires --mcp (the
208
+ // agent would otherwise have no way to reach sweet-search). Indexing
209
+ // still uses the CLI/engine.
210
+ result.noCli = true;
191
211
  }
192
212
  }
193
213
 
194
214
  return result;
195
215
  }
196
216
 
217
+ /**
218
+ * Cross-flag validation for init args. Currently the only rule: `--no-cli`
219
+ * (suppress the CLI contact surface) is meaningless without `--mcp` (the
220
+ * replacement contact surface). Returns `{ ok, error }`.
221
+ */
222
+ export function validateInitArgs(parsed) {
223
+ if (parsed.noCli && !parsed.mcp) {
224
+ return {
225
+ ok: false,
226
+ error:
227
+ '--no-cli requires --mcp. Suppressing the CLI contact surface leaves the agent '
228
+ + 'with no way to reach sweet-search unless the MCP server is registered. '
229
+ + 'Re-run as `sweet-search init --mcp --no-cli`, or drop --no-cli.',
230
+ };
231
+ }
232
+ return { ok: true, error: null };
233
+ }
234
+
197
235
  /**
198
236
  * Resolve the active harness list. Default is `claude-code` only;
199
237
  * `--agents` / `--gemini` / `--cursor` add to that set; `--no-claude`
@@ -713,7 +751,7 @@ function printReport(report) {
713
751
  profile, maxsimTier, routerType, models, verification, runtimeDownloads,
714
752
  capability, cascadeReport, dedupReport, prewarmHookReport, skillReport,
715
753
  liChoices, agentInstructionsReport, claudeRulesReport,
716
- promptReminderReport, toolEnforcementReport,
754
+ promptReminderReport, toolEnforcementReport, mcpServerReport,
717
755
  } = report;
718
756
 
719
757
  console.log('');
@@ -833,6 +871,9 @@ function printReport(report) {
833
871
  if (toolEnforcementReport && toolEnforcementReport.status !== 'skipped') {
834
872
  console.log(` Tool enforcement: ${toolEnforcementReport.status} (Grep deny + Read hint)`);
835
873
  }
874
+ if (mcpServerReport && mcpServerReport.status) {
875
+ console.log(` MCP server (.mcp.json): ${mcpServerReport.status}${mcpServerReport.detail ? ` β€” ${mcpServerReport.detail}` : ''}`);
876
+ }
836
877
 
837
878
  console.log(` Runtime downloads: ${runtimeDownloads}`);
838
879
 
@@ -1429,7 +1470,24 @@ Options:
1429
1470
  native Read suggesting ss-read / ss-semantic.
1430
1471
  Read is hinted, not blocked, because edit
1431
1472
  workflows legitimately need Read. Always
1432
- implied off when --no-claude is set.
1473
+ implied off when --no-claude or --no-cli is set.
1474
+ --mcp Register the sweet-search MCP server in the project
1475
+ root .mcp.json (an "npx -y sweet-search-mcp" entry
1476
+ under mcpServers.sweet-search). Additive and
1477
+ idempotent β€” the CLI surface stays, other servers
1478
+ and JSON keys are preserved. Root-level and
1479
+ harness-agnostic (independent of --no-claude). The
1480
+ MCP server is a thin adapter over the same engine
1481
+ the CLI wraps.
1482
+ --no-cli Make MCP the agent's CONTACT SURFACE: inject the
1483
+ MCP-tool prompt variant instead of the ss-* CLI
1484
+ one, and skip the CLI-surface supplements (the
1485
+ .claude/rules file, the ss-* prompt reminder, tool
1486
+ enforcement). Indexing still runs through the CLI/
1487
+ engine β€” this only changes how the agent searches.
1488
+ Requires --mcp. NOTE: the MCP prompt variant is
1489
+ hand-derived from the frozen CLI champion and is not
1490
+ yet benchmarked on the MCP transport.
1433
1491
  --verbose, -v Enable verbose output
1434
1492
  --help, -h Show this help
1435
1493
 
@@ -1448,9 +1506,11 @@ CoreML cascade (M3+ Apple Silicon only):
1448
1506
  strategy.
1449
1507
 
1450
1508
  Examples:
1451
- sweet-search init # Full profile (default)
1509
+ sweet-search init # Full profile (default); CLI contact surface
1452
1510
  sweet-search init --profile core # Core profile (no model downloads)
1453
1511
  sweet-search init --force # Re-download all models
1512
+ sweet-search init --mcp # Also register the MCP server (CLI stays)
1513
+ sweet-search init --mcp --no-cli # MCP-only contact surface (MCP-variant prompt)
1454
1514
  sweet-search init --build-coreml-cascade # Trace the cascade locally (dev only)
1455
1515
  `);
1456
1516
  }
@@ -1467,10 +1527,17 @@ export async function runInit(args) {
1467
1527
  return;
1468
1528
  }
1469
1529
 
1530
+ const validation = validateInitArgs(parsed);
1531
+ if (!validation.ok) {
1532
+ console.error(`sweet-search init: ${validation.error}`);
1533
+ process.exitCode = 1;
1534
+ return;
1535
+ }
1536
+
1470
1537
  // 0. Animated banner (best-effort; only on an interactive TTY, never in CI/pipes).
1471
1538
  if (process.stdout.isTTY && !process.env.CI && !process.env.NO_BANNER && !process.env.SWEET_SEARCH_NO_BANNER) {
1472
1539
  // query:false β€” init is interactive (readline); avoid any stdin contention with the terminal capability probe.
1473
- try { const { showBanner } = await import('../core/banner/render-banner.js'); await showBanner({ query: false }); } catch { /* non-fatal */ }
1540
+ try { const { showBanner } = await import('../core/banner/render-banner.js'); await showBanner({ query: false, clear: true }); } catch { /* non-fatal */ }
1474
1541
  }
1475
1542
 
1476
1543
  // 1. Node.js version check
@@ -1918,6 +1985,30 @@ export async function runInit(args) {
1918
1985
  // Idempotent marker block so re-init never duplicates content.
1919
1986
  // `--no-agent-instructions` is the umbrella that skips the
1920
1987
  // instruction-file injection layer entirely.
1988
+ //
1989
+ // 11.5 MCP server registration (`--mcp`). Writes the project-root `.mcp.json`
1990
+ // entry for `sweet-search-mcp`. Additive + idempotent + harness-agnostic
1991
+ // (root-level, independent of --no-claude). The MCP server is a thin
1992
+ // adapter over the same engine the CLI wraps β€” `--mcp` adds it, it never
1993
+ // replaces the CLI. `--no-cli` (below) only switches the agent's contact
1994
+ // surface to MCP; indexing keeps running through the CLI/engine.
1995
+ let mcpServerReport = null;
1996
+ if (parsed.mcp) {
1997
+ try {
1998
+ mcpServerReport = installMcpServer({ projectRoot });
1999
+ process.stderr.write(
2000
+ `[init] MCP server (.mcp.json): ${mcpServerReport.status}`
2001
+ + (mcpServerReport.detail ? ` β€” ${mcpServerReport.detail}` : '') + '\n',
2002
+ );
2003
+ } catch (err) {
2004
+ process.stderr.write(`[init] Warning: MCP server registration failed: ${err.message}\n`);
2005
+ }
2006
+ }
2007
+
2008
+ // Contact-surface variant: --no-cli makes MCP the agent's surface, so we
2009
+ // inject the MCP-tool prompt variant instead of the ss-* CLI champion.
2010
+ const promptVariant = parsed.noCli ? 'mcp' : 'cli';
2011
+
1921
2012
  let agentInstructionsReport = null;
1922
2013
  let claudeRulesReport = null;
1923
2014
  if (!parsed.skipAgentInstructions) {
@@ -1936,25 +2027,40 @@ export async function runInit(args) {
1936
2027
  projectRoot,
1937
2028
  harnesses: activeHarnesses,
1938
2029
  useSymlinks: parsed.symlinkInstructionFiles,
2030
+ variant: promptVariant,
1939
2031
  });
1940
2032
  const summary = Object.entries(agentInstructionsReport.harnesses)
1941
2033
  .map(([k, v]) => `${k}=${v}`).join(' ');
1942
2034
  const canonical = agentInstructionsReport.canonical
1943
2035
  ? ` (canonical=${agentInstructionsReport.canonical})` : '';
1944
- process.stderr.write(`[init] Agent instructions: ${summary || '(none)'}${canonical}\n`);
2036
+ const variantTag = promptVariant === 'mcp' ? ' [mcp variant]' : '';
2037
+ process.stderr.write(`[init] Agent instructions: ${summary || '(none)'}${canonical}${variantTag}\n`);
1945
2038
  } catch (err) {
1946
2039
  process.stderr.write(`[init] Warning: Agent-instruction injection failed: ${err.message}\n`);
1947
2040
  }
1948
- // Claude rules file is only useful when claude-code is enabled β€” the
1949
- // sole load path is the @.claude/rules/sweet-search.md import line that
1950
- // injectAgentInstructions writes into CLAUDE.md.
2041
+ // Claude rules file is only useful when claude-code is enabled AND the
2042
+ // CLI is the contact surface β€” its sole load path is the
2043
+ // @.claude/rules/sweet-search.md import line that injectAgentInstructions
2044
+ // writes into CLAUDE.md (omitted in the --no-cli MCP variant), and its
2045
+ // body is written in ss-* CLI terms. Under --no-cli we TEAR DOWN any rules
2046
+ // file a prior CLI init wrote (idempotent: not-found when absent) so a
2047
+ // cli→mcp re-init never leaves a stale ss-* supplement contradicting the
2048
+ // injected MCP prompt.
1951
2049
  if (activeHarnesses.includes('claude-code')) {
1952
2050
  try {
1953
- const status = writeClaudeRules({ projectRoot });
1954
- claudeRulesReport = { status };
1955
- process.stderr.write(`[init] Claude rules: ${status}\n`);
2051
+ if (parsed.noCli) {
2052
+ const status = removeClaudeRules({ projectRoot });
2053
+ claudeRulesReport = { status };
2054
+ if (status === 'removed' || parsed.verbose) {
2055
+ process.stderr.write(`[init] Claude rules: ${status}${status === 'removed' ? ' (--no-cli β€” stale ss-* CLI supplement torn down)' : ' (--no-cli)'}\n`);
2056
+ }
2057
+ } else {
2058
+ const status = writeClaudeRules({ projectRoot });
2059
+ claudeRulesReport = { status };
2060
+ process.stderr.write(`[init] Claude rules: ${status}\n`);
2061
+ }
1956
2062
  } catch (err) {
1957
- process.stderr.write(`[init] Warning: Could not write Claude rules: ${err.message}\n`);
2063
+ process.stderr.write(`[init] Warning: Claude rules ${parsed.noCli ? 'teardown' : 'write'} failed: ${err.message}\n`);
1958
2064
  }
1959
2065
  }
1960
2066
  }
@@ -1968,15 +2074,26 @@ export async function runInit(args) {
1968
2074
  // `.claude/hooks/sweet-search-remind-tools.mjs` with a
1969
2075
  // `hooks.UserPromptSubmit` entry in `.claude/settings.json` keyed by
1970
2076
  // filename so re-init updates rather than duplicates.
2077
+ // Under --no-cli the reminder body (ss-* CLI Bash commands) contradicts
2078
+ // the injected MCP-variant prompt, so we TEAR DOWN any reminder hook a
2079
+ // prior CLI init installed (idempotent: not-found when absent) rather
2080
+ // than merely skipping the install. An MCP-variant reminder is a follow-up.
1971
2081
  let promptReminderReport = null;
1972
2082
  if (!parsed.noClaude) {
1973
- promptReminderReport = installPromptReminderHook({
1974
- projectRoot,
1975
- packageRoot: PACKAGE_ROOT,
1976
- skipped: parsed.skipPromptReminders,
1977
- });
1978
- if (parsed.verbose || promptReminderReport.status === 'error') {
1979
- process.stderr.write(`[init] Prompt reminder hook: ${promptReminderReport.status} β€” ${promptReminderReport.detail}\n`);
2083
+ if (parsed.noCli) {
2084
+ promptReminderReport = removePromptReminderHook({ projectRoot });
2085
+ if (parsed.verbose || promptReminderReport.status === 'error') {
2086
+ process.stderr.write(`[init] Prompt reminder hook: ${promptReminderReport.status} (--no-cli) β€” ${promptReminderReport.detail}\n`);
2087
+ }
2088
+ } else {
2089
+ promptReminderReport = installPromptReminderHook({
2090
+ projectRoot,
2091
+ packageRoot: PACKAGE_ROOT,
2092
+ skipped: parsed.skipPromptReminders,
2093
+ });
2094
+ if (parsed.verbose || promptReminderReport.status === 'error') {
2095
+ process.stderr.write(`[init] Prompt reminder hook: ${promptReminderReport.status} β€” ${promptReminderReport.detail}\n`);
2096
+ }
1980
2097
  }
1981
2098
  }
1982
2099
 
@@ -1984,15 +2101,26 @@ export async function runInit(args) {
1984
2101
  // `--enforce-tools`; universal `--no-claude` gate above. Adds
1985
2102
  // `permissions.deny: ["Grep"]` and a PreToolUse hint hook for `Read`
1986
2103
  // in `.claude/settings.json`. Strict + opinionated; off by default.
2104
+ // Under --no-cli the Read hint points at ss-read / ss-semantic (CLI
2105
+ // surface) and denying native Grep is moot when MCP `search` is the
2106
+ // contact surface β€” so we TEAR DOWN any enforcement a prior CLI init
2107
+ // wrote (idempotent: not-found when absent) instead of merely skipping.
1987
2108
  let toolEnforcementReport = null;
1988
2109
  if (!parsed.noClaude) {
1989
- toolEnforcementReport = installToolEnforcement({
1990
- projectRoot,
1991
- packageRoot: PACKAGE_ROOT,
1992
- skipped: !parsed.enforceTools,
1993
- });
1994
- if (parsed.verbose || toolEnforcementReport.status === 'error') {
1995
- process.stderr.write(`[init] Tool enforcement: ${toolEnforcementReport.status} β€” ${toolEnforcementReport.detail}\n`);
2110
+ if (parsed.noCli) {
2111
+ toolEnforcementReport = removeToolEnforcement({ projectRoot });
2112
+ if (parsed.verbose || toolEnforcementReport.status === 'error') {
2113
+ process.stderr.write(`[init] Tool enforcement: ${toolEnforcementReport.status} (--no-cli)${toolEnforcementReport.detail ? ` β€” ${toolEnforcementReport.detail}` : ''}\n`);
2114
+ }
2115
+ } else {
2116
+ toolEnforcementReport = installToolEnforcement({
2117
+ projectRoot,
2118
+ packageRoot: PACKAGE_ROOT,
2119
+ skipped: !parsed.enforceTools,
2120
+ });
2121
+ if (parsed.verbose || toolEnforcementReport.status === 'error') {
2122
+ process.stderr.write(`[init] Tool enforcement: ${toolEnforcementReport.status} β€” ${toolEnforcementReport.detail}\n`);
2123
+ }
1996
2124
  }
1997
2125
  }
1998
2126
 
@@ -2014,6 +2142,7 @@ export async function runInit(args) {
2014
2142
  claudeRulesReport,
2015
2143
  promptReminderReport,
2016
2144
  toolEnforcementReport,
2145
+ mcpServerReport,
2017
2146
  });
2018
2147
  }
2019
2148
 
@@ -63,20 +63,27 @@ function escapeRegex(s) {
63
63
 
64
64
  const SHIP_FILE_REL = 'core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md';
65
65
 
66
+ // MCP-tool variant of the policy (init --mcp --no-cli). Same strategy core; the
67
+ // tool-mechanics layer is remapped from the ss-* CLI surface onto the
68
+ // sweet-search MCP tool surface. Read lazily β€” only the variant actually
69
+ // requested needs to exist, so importing this module never requires the MCP
70
+ // ship-file to be present.
71
+ const MCP_SHIP_FILE_REL = 'core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md';
72
+
66
73
  /** Strip a leading YAML front-matter block (`---\n … \n---\n`) if present. */
67
74
  export function stripFrontMatter(text) {
68
75
  return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
69
76
  }
70
77
 
71
- function readShippedPolicy() {
78
+ function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M++' } = {}) {
72
79
  const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/scripts
73
- const shipPath = join(here, '..', SHIP_FILE_REL);
80
+ const shipPath = join(here, '..', rel);
74
81
  let raw;
75
82
  try {
76
83
  raw = readFileSync(shipPath, 'utf8');
77
84
  } catch (err) {
78
85
  throw new Error(
79
- `inject-agent-instructions: cannot read the M++ ship-file at ${shipPath}. ` +
86
+ `inject-agent-instructions: cannot read the ${label} ship-file at ${shipPath}. ` +
80
87
  'It MUST be present (packaged via package.json "files"). Regenerate with ' +
81
88
  '`node core/prompt-optimization/sweep/finalize-mpp.mjs`. ' +
82
89
  `Cause: ${err.message}`,
@@ -84,13 +91,31 @@ function readShippedPolicy() {
84
91
  }
85
92
  const body = stripFrontMatter(raw).trimEnd();
86
93
  if (!body) {
87
- throw new Error(`inject-agent-instructions: M++ ship-file at ${shipPath} has an empty body.`);
94
+ throw new Error(`inject-agent-instructions: ${label} ship-file at ${shipPath} has an empty body.`);
88
95
  }
89
96
  return body;
90
97
  }
91
98
 
92
99
  export const CANONICAL_POLICY_BODY = readShippedPolicy();
93
100
 
101
+ let _mcpPolicyBody = null;
102
+ /** Lazily read + cache the MCP-variant policy body. */
103
+ export function getMcpPolicyBody() {
104
+ if (_mcpPolicyBody == null) {
105
+ _mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M++ (MCP variant)' });
106
+ }
107
+ return _mcpPolicyBody;
108
+ }
109
+
110
+ /**
111
+ * Resolve the policy body for a contact-surface variant.
112
+ * 'cli' (default) β†’ the frozen ss-* CLI champion (CANONICAL_POLICY_BODY)
113
+ * 'mcp' β†’ the MCP-tool variant (init --mcp --no-cli)
114
+ */
115
+ export function getPolicyBody(variant = 'cli') {
116
+ return variant === 'mcp' ? getMcpPolicyBody() : CANONICAL_POLICY_BODY;
117
+ }
118
+
94
119
  const CURSOR_FRONTMATTER = `---
95
120
  description: Sweet Search tool-routing, stopping, and citation policy
96
121
  alwaysApply: false
@@ -111,12 +136,12 @@ function wrapMarker(body) {
111
136
  * full policy plus, for CLAUDE.md, an extra `@.claude/rules/sweet-search.md`
112
137
  * import line so the Claude-specific shim is loaded.
113
138
  */
114
- export function buildCanonicalBlock({ extraImports = [] } = {}) {
139
+ export function buildCanonicalBlock({ extraImports = [], policyBody = CANONICAL_POLICY_BODY } = {}) {
115
140
  if (extraImports.length === 0) {
116
- return wrapMarker(CANONICAL_POLICY_BODY);
141
+ return wrapMarker(policyBody);
117
142
  }
118
143
  const importLines = extraImports.map(t => `@${t}`).join('\n');
119
- return wrapMarker(`${CANONICAL_POLICY_BODY}\n${importLines}\n`);
144
+ return wrapMarker(`${policyBody}\n${importLines}\n`);
120
145
  }
121
146
 
122
147
  /**
@@ -133,8 +158,8 @@ export function buildImportBlock({ importTargets }) {
133
158
  }
134
159
 
135
160
  /** Body for the cursor .mdc (frontmatter + inlined canonical body). */
136
- export function buildCursorFile() {
137
- return CURSOR_FRONTMATTER + wrapMarker(CANONICAL_POLICY_BODY);
161
+ export function buildCursorFile(policyBody = CANONICAL_POLICY_BODY) {
162
+ return CURSOR_FRONTMATTER + wrapMarker(policyBody);
138
163
  }
139
164
 
140
165
  // ─── Marker injection ───────────────────────────────────────────────────────
@@ -265,26 +290,36 @@ export function injectAgentInstructions({
265
290
  projectRoot,
266
291
  harnesses = ALL_HARNESSES,
267
292
  useSymlinks = true,
293
+ variant = 'cli',
268
294
  } = {}) {
269
295
  if (!projectRoot) throw new TypeError('inject-agent-instructions: projectRoot is required');
270
296
  const enabled = new Set(harnesses);
271
- const report = { harnesses: {}, canonical: null };
297
+ const report = { harnesses: {}, canonical: null, variant };
272
298
 
273
299
  if (enabled.size === 0) return report;
274
300
 
301
+ // Variant selects the policy body. The MCP variant retargets every ss-* CLI
302
+ // reference onto the sweet-search MCP tool surface; it also drops the Claude
303
+ // `@.claude/rules/sweet-search.md` import because that supplement is written
304
+ // in ss-* CLI terms and would contradict the MCP body (the CLI rules file is
305
+ // skipped under --no-cli in init too).
306
+ const policyBody = getPolicyBody(variant);
307
+ const claudeExtraImports = variant === 'mcp' ? [] : ['.claude/rules/sweet-search.md'];
308
+
275
309
  // 1. Canonical file: CLAUDE.md when Claude Code is enabled, else AGENTS.md.
276
- // Body is the full policy plus (Claude-only) the @.claude/rules import.
310
+ // Body is the full policy plus (Claude-only, CLI variant) the @.claude/rules import.
277
311
  let canonicalFile;
278
312
  let canonicalBlock;
279
313
  if (enabled.has('claude-code')) {
280
314
  canonicalFile = CLAUDE_FILE;
281
315
  canonicalBlock = buildCanonicalBlock({
282
- extraImports: ['.claude/rules/sweet-search.md'],
316
+ extraImports: claudeExtraImports,
317
+ policyBody,
283
318
  });
284
319
  report.canonical = 'claude-code';
285
320
  } else if (enabled.has('agents') || enabled.has('gemini') || enabled.has('cursor')) {
286
321
  canonicalFile = AGENTS_FILE;
287
- canonicalBlock = buildCanonicalBlock();
322
+ canonicalBlock = buildCanonicalBlock({ policyBody });
288
323
  report.canonical = 'agents'; // AGENTS.md is the multi-harness convention (Codex, OpenCode, …)
289
324
  } else {
290
325
  return report; // no canonical, nothing to write
@@ -343,12 +378,12 @@ export function injectAgentInstructions({
343
378
  // and any user notes outside the markers.
344
379
  report.harnesses.cursor = injectMarkerBlock({
345
380
  filePath: cursorPath,
346
- block: buildCanonicalBlock(),
381
+ block: buildCanonicalBlock({ policyBody }),
347
382
  });
348
383
  } else {
349
384
  // Fresh file β€” write frontmatter + canonical body in marker block.
350
385
  mkdirSync(dirname(cursorPath), { recursive: true });
351
- writeFileSync(cursorPath, buildCursorFile());
386
+ writeFileSync(cursorPath, buildCursorFile(policyBody));
352
387
  report.harnesses.cursor = 'created';
353
388
  }
354
389
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Project-local MCP server registration for sweet-search.
3
+ *
4
+ * `sweet-search init --mcp` writes a `sweet-search` entry into the project's
5
+ * root `.mcp.json` β€” the project-scoped MCP config read by Claude Code and other
6
+ * MCP hosts (see docs/search/MCP_INTEGRATION.md Β§`.mcp.json`). Additive and
7
+ * idempotent: existing servers and any other top-level keys are preserved; only
8
+ * `mcpServers.sweet-search` is created/updated.
9
+ *
10
+ * Design notes:
11
+ * - This is ROOT-level and harness-agnostic. It is independent of `--no-claude`
12
+ * (which gates `.claude/*` writes only). `.mcp.json` lives at the repo root.
13
+ * - The MCP server is a thin adapter over the SAME engine the CLI wraps. `--mcp`
14
+ * ADDS it; it never replaces the CLI. `--no-cli` only swaps the agent's
15
+ * *contact surface* to MCP β€” indexing still runs through the CLI/engine.
16
+ * - We never clobber an unparseable user `.mcp.json`; we fail loudly instead.
17
+ */
18
+
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+
22
+ export const MCP_CONFIG_FILE = '.mcp.json';
23
+ export const MCP_SERVER_KEY = 'sweet-search';
24
+
25
+ /**
26
+ * The canonical server entry. Uses `npx -y sweet-search-mcp` (the published bin)
27
+ * so the registration keeps working after a global/local install without a
28
+ * hard-coded path, and pins the target repo via `--project-root`.
29
+ */
30
+ export function buildServerEntry({ projectRoot }) {
31
+ return {
32
+ command: 'npx',
33
+ args: ['-y', 'sweet-search-mcp', '--project-root', projectRoot],
34
+ };
35
+ }
36
+
37
+ function isPlainObject(v) {
38
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
39
+ }
40
+
41
+ function deepEqual(a, b) {
42
+ return JSON.stringify(a) === JSON.stringify(b);
43
+ }
44
+
45
+ /**
46
+ * Install/update the sweet-search MCP server registration.
47
+ * Idempotent. Returns `{ status, path, detail? }` where status is one of:
48
+ * 'created' β€” wrote a fresh .mcp.json
49
+ * 'added' β€” file existed, added our server entry
50
+ * 'updated' β€” our entry existed but differed; rewritten
51
+ * 'unchanged' β€” our entry already matches
52
+ * 'error' β€” existing file is not a usable JSON object (left untouched)
53
+ */
54
+ export function installMcpServer({ projectRoot, configFile = MCP_CONFIG_FILE } = {}) {
55
+ if (!projectRoot) throw new TypeError('install-mcp-server: projectRoot is required');
56
+ const configPath = join(projectRoot, configFile);
57
+ const entry = buildServerEntry({ projectRoot });
58
+
59
+ let config = {};
60
+ const existed = existsSync(configPath);
61
+ if (existed) {
62
+ let raw;
63
+ try {
64
+ raw = readFileSync(configPath, 'utf8');
65
+ } catch (err) {
66
+ return { status: 'error', path: configPath, detail: `cannot read ${configFile}: ${err.message}` };
67
+ }
68
+ try {
69
+ config = JSON.parse(raw);
70
+ } catch (err) {
71
+ return { status: 'error', path: configPath, detail: `existing ${configFile} is not valid JSON: ${err.message}` };
72
+ }
73
+ if (!isPlainObject(config)) {
74
+ return { status: 'error', path: configPath, detail: `existing ${configFile} is not a JSON object` };
75
+ }
76
+ }
77
+
78
+ if (!isPlainObject(config.mcpServers)) config.mcpServers = {};
79
+
80
+ const prev = config.mcpServers[MCP_SERVER_KEY];
81
+ if (prev && deepEqual(prev, entry)) {
82
+ return { status: 'unchanged', path: configPath };
83
+ }
84
+ const hadEntry = prev !== undefined;
85
+ config.mcpServers[MCP_SERVER_KEY] = entry;
86
+
87
+ mkdirSync(dirname(configPath), { recursive: true });
88
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
89
+ return { status: existed ? (hadEntry ? 'updated' : 'added') : 'created', path: configPath };
90
+ }
91
+
92
+ /**
93
+ * Reverse `installMcpServer`. Removes only our `mcpServers.sweet-search` entry,
94
+ * preserving any other servers / top-level keys. Deletes the file outright only
95
+ * when it becomes wholly empty (no other servers, no other top-level keys).
96
+ * @returns 'removed' | 'file-deleted' | 'not-found' | 'dry-run'
97
+ */
98
+ export function removeMcpServer({ projectRoot, configFile = MCP_CONFIG_FILE, dryRun = false } = {}) {
99
+ if (!projectRoot) throw new TypeError('remove-mcp-server: projectRoot is required');
100
+ const configPath = join(projectRoot, configFile);
101
+ if (!existsSync(configPath)) return 'not-found';
102
+ let config;
103
+ try {
104
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
105
+ } catch {
106
+ return 'not-found'; // unparseable / not ours β€” never touch it
107
+ }
108
+ if (!isPlainObject(config) || !isPlainObject(config.mcpServers) || !(MCP_SERVER_KEY in config.mcpServers)) {
109
+ return 'not-found';
110
+ }
111
+ if (dryRun) return 'dry-run';
112
+
113
+ delete config.mcpServers[MCP_SERVER_KEY];
114
+ const hasOtherServers = Object.keys(config.mcpServers).length > 0;
115
+ const hasOtherKeys = Object.keys(config).some((k) => k !== 'mcpServers');
116
+ if (!hasOtherServers && !hasOtherKeys) {
117
+ unlinkSync(configPath);
118
+ return 'file-deleted';
119
+ }
120
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
121
+ return 'removed';
122
+ }
@@ -34,8 +34,8 @@ function run() {
34
34
  const L2 = 'β–„β–„β–ˆ β–€β–„β–ˆβ–„β–€ β–ˆβ–ˆβ–„ β–ˆβ–ˆβ–„ β–ˆ β–„β–„β–ˆ β–ˆβ–ˆβ–„ β–ˆβ–€β–ˆ β–ˆβ–ˆβ–„ β–ˆβ–„β–„ β–ˆβ–€β–ˆ';
35
35
  const msg = [
36
36
  '',
37
- ` ${c('1;38;5;135', L1)}`,
38
- ` ${c('1;38;5;135', L2)}`,
37
+ ` ${c('1;38;2;90;115;220', L1)}`,
38
+ ` ${c('1;38;2;90;115;220', L2)}`,
39
39
  '',
40
40
  ` ${c('1', 'Get started:')}`,
41
41
  ` ${c('36', 'sweet-search init')} set up the current project`,
@@ -20,6 +20,7 @@ import { getCoremlCascadeRoot, getCoremlCascadeState } from '../core/infrastruct
20
20
  import { PREWARM_HOOK_FILENAME } from './init.js';
21
21
  import { removeAgentInstructions } from './inject-agent-instructions.js';
22
22
  import { removeClaudeRules } from './write-claude-rules.js';
23
+ import { removeMcpServer } from './install-mcp-server.js';
23
24
  import { removePromptReminderHook } from './install-prompt-reminders.js';
24
25
  import { removeToolEnforcement } from './install-tool-enforcement.js';
25
26
  import { projectSocketPath, projectPidFile } from '../core/search/server-identity.js';
@@ -711,11 +712,16 @@ export async function runUninstall(args) {
711
712
  const codexHookPreview = removeCodexSessionStartHook(projectRoot, { dryRun: true });
712
713
  const hasCodexHook = codexHookPreview.status === 'dry-run';
713
714
 
715
+ // MCP server registration (.mcp.json mcpServers.sweet-search), written by
716
+ // `init --mcp`.
717
+ const mcpServerPreview = removeMcpServer({ projectRoot, dryRun: true });
718
+ const hasMcpServer = mcpServerPreview === 'dry-run';
719
+
714
720
  // Nothing to remove?
715
721
  if (
716
722
  removals.length === 0 && !hasHookEntry && !hasSkillEntry && !hasIndexMaintainerHook
717
723
  && !agentInstructionsTouched && !hasClaudeRules
718
- && !hasPromptReminder && !hasToolEnforcement && !hasCodexHook
724
+ && !hasPromptReminder && !hasToolEnforcement && !hasCodexHook && !hasMcpServer
719
725
  ) {
720
726
  console.log('Nothing to remove β€” Sweet Search is not initialized in this project.');
721
727
  return;
@@ -758,6 +764,9 @@ export async function runUninstall(args) {
758
764
  if (hasCodexHook) {
759
765
  console.log(` Codex SessionStart hook (.codex/hooks.json)`);
760
766
  }
767
+ if (hasMcpServer) {
768
+ console.log(` MCP server registration (.mcp.json β€” mcpServers.sweet-search)`);
769
+ }
761
770
  console.log(` Total: ${formatBytes(totalBytes)}`);
762
771
  if (parsed.keepModels) {
763
772
  console.log(' Model cache: kept (--keep-models)');
@@ -783,6 +792,10 @@ export async function runUninstall(args) {
783
792
  if (dryCodex.status === 'dry-run') {
784
793
  console.log(` Would also remove: Codex SessionStart hook (.codex/hooks.json β€” ${dryCodex.detail})`);
785
794
  }
795
+ const dryMcp = removeMcpServer({ projectRoot, dryRun: true });
796
+ if (dryMcp === 'dry-run') {
797
+ console.log(` Would also remove: MCP server registration (.mcp.json β€” mcpServers.sweet-search)`);
798
+ }
786
799
  console.log('Dry run β€” nothing was removed.');
787
800
  return;
788
801
  }
@@ -949,6 +962,18 @@ export async function runUninstall(args) {
949
962
  kept++;
950
963
  }
951
964
 
965
+ // MCP server registration (.mcp.json mcpServers.sweet-search). Only our entry
966
+ // is removed; other servers and JSON keys are preserved.
967
+ const mcpServerResult = removeMcpServer({ projectRoot, dryRun: parsed.dryRun });
968
+ if (mcpServerResult === 'removed') {
969
+ console.log(` Removed: MCP server registration (.mcp.json β€” mcpServers.sweet-search)`);
970
+ removed++;
971
+ } else if (mcpServerResult === 'file-deleted') {
972
+ console.log(` Removed: .mcp.json (wholly sweet-search-managed)`);
973
+ removed++;
974
+ }
975
+ // 'not-found' / 'dry-run' are silent.
976
+
952
977
  // Purge npm packages
953
978
  if (parsed.purge) {
954
979
  console.log('');