@takara-ai/miru-code 0.4.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/server.ts CHANGED
@@ -2,14 +2,22 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import type { ContentType } from "../types.ts";
4
4
  import {
5
+ clampMcpTopK,
5
6
  DEFAULT_MCP_TOP_K,
6
7
  dedupeResultsByFile,
7
8
  expandChunksAtLine,
8
9
  formatExpandResults,
9
10
  formatResults,
10
11
  localRepoRoot,
12
+ MAX_MCP_TOP_K,
11
13
  resolveChunk,
12
14
  } from "../utils.ts";
15
+ import {
16
+ MCP_EXPAND_TOOL_DESCRIPTION,
17
+ MCP_FIND_RELATED_TOOL_DESCRIPTION,
18
+ MCP_SEARCH_TOOL_DESCRIPTION,
19
+ MCP_SERVER_INSTRUCTIONS,
20
+ } from "../installer/search-policy.ts";
13
21
  import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
14
22
 
15
23
  const REPO_DESCRIPTION =
@@ -17,14 +25,6 @@ const REPO_DESCRIPTION =
17
25
  "Pass the project root for local workspaces. " +
18
26
  "The index is built on the first tool call and cached for the session.";
19
27
 
20
- const SERVER_INSTRUCTIONS =
21
- "DO NOT use Grep, Glob, SemanticSearch, or Read to explore code when this server is available. " +
22
- "Workflow: (1) `search` once per question — check `guidance.sufficient` and `suggested_next` in the response. " +
23
- "(2) If context looks cut off, call `expand` on the hit's file_path + line — do NOT re-search with paraphrases or read the whole file. " +
24
- "(3) `find_related` traces similar code in other files, not more context in the same file. " +
25
- "Always pass the project root as `repo`. Local repos return `absolute_path` on each hit — use Read only to edit. " +
26
- "Native search is only for exact literal confirmation or files outside the indexed repo.";
27
-
28
28
  export function createMcpServer(cache: IndexCache): McpServer {
29
29
  const server = new McpServer(
30
30
  {
@@ -32,7 +32,7 @@ export function createMcpServer(cache: IndexCache): McpServer {
32
32
  version: "0.1.0",
33
33
  },
34
34
  {
35
- instructions: SERVER_INSTRUCTIONS,
35
+ instructions: MCP_SERVER_INSTRUCTIONS,
36
36
  },
37
37
  );
38
38
 
@@ -40,9 +40,7 @@ export function createMcpServer(cache: IndexCache): McpServer {
40
40
  "search",
41
41
  {
42
42
  description:
43
- "DEFAULT code exploration tool. One call per question is usually enough — read `guidance` in the response. " +
44
- "If a hit looks truncated, call `expand` (same file) instead of re-searching or Read. " +
45
- "Indexes `repo` on first call; later calls reuse the session cache.",
43
+ `${MCP_SEARCH_TOOL_DESCRIPTION} Indexes \`repo\` on first call; later calls reuse the session cache.`,
46
44
  inputSchema: {
47
45
  query: z.string().describe("Natural language or code query."),
48
46
  repo: z.string().describe(REPO_DESCRIPTION),
@@ -50,8 +48,9 @@ export function createMcpServer(cache: IndexCache): McpServer {
50
48
  .number()
51
49
  .int()
52
50
  .min(1)
51
+ .max(MAX_MCP_TOP_K)
53
52
  .optional()
54
- .describe(`Number of results (default ${DEFAULT_MCP_TOP_K}).`),
53
+ .describe(`Number of results (default ${DEFAULT_MCP_TOP_K}, max ${MAX_MCP_TOP_K}).`),
55
54
  dedupe_by_file: z
56
55
  .boolean()
57
56
  .optional()
@@ -61,7 +60,7 @@ export function createMcpServer(cache: IndexCache): McpServer {
61
60
  async ({ query, repo, top_k: topK, dedupe_by_file: dedupeByFile }) => {
62
61
  try {
63
62
  const index = await getIndexForRepo(repo, cache);
64
- let results = await index.search({ query, topK: topK ?? DEFAULT_MCP_TOP_K });
63
+ let results = await index.search({ query, topK: clampMcpTopK(topK) });
65
64
  if (dedupeByFile !== false) {
66
65
  results = dedupeResultsByFile(results);
67
66
  }
@@ -69,7 +68,7 @@ export function createMcpServer(cache: IndexCache): McpServer {
69
68
  return toolText(JSON.stringify({ error: "No results found." }));
70
69
  }
71
70
  const repoRoot = localRepoRoot(repo);
72
- return toolText(JSON.stringify(formatResults(query, results, { repoRoot })));
71
+ return toolText(JSON.stringify(formatResults(query, results, { repoRoot, snippet: true })));
73
72
  } catch (err) {
74
73
  return toolText(err instanceof Error ? err.message : String(err));
75
74
  }
@@ -79,15 +78,15 @@ export function createMcpServer(cache: IndexCache): McpServer {
79
78
  server.registerTool(
80
79
  "expand",
81
80
  {
82
- description:
83
- "More context in the SAME file as a search hit. Pass file_path + line from a hit; returns adjacent indexed chunks. " +
84
- "Use when a chunk looks cut off — NOT for finding similar code in other files (use find_related). " +
85
- "Prefer this over re-searching or reading the whole file.",
81
+ description: MCP_EXPAND_TOOL_DESCRIPTION,
86
82
  inputSchema: {
87
83
  file_path: z
88
84
  .string()
89
85
  .describe("Path from a search hit (`file_path` or `absolute_path` for local repos)."),
90
- line: z.number().int().describe("Line number (1-indexed) from the hit."),
86
+ line: z
87
+ .number()
88
+ .int()
89
+ .describe("Line from the hit: `anchor_line` when truncated, otherwise `start_line` (1-indexed)."),
91
90
  repo: z.string().describe(REPO_DESCRIPTION),
92
91
  before: z
93
92
  .number()
@@ -140,18 +139,25 @@ export function createMcpServer(cache: IndexCache): McpServer {
140
139
  server.registerTool(
141
140
  "find_related",
142
141
  {
143
- description:
144
- "Find code similar to a file:line in OTHER parts of the codebase. " +
145
- "For more context in the same file, use `expand` instead.",
142
+ description: MCP_FIND_RELATED_TOOL_DESCRIPTION,
146
143
  inputSchema: {
147
144
  file_path: z
148
145
  .string()
149
146
  .describe(
150
147
  "Path from a search hit (`file_path` or `absolute_path` for local repos).",
151
148
  ),
152
- line: z.number().int().describe("Line number (1-indexed)."),
149
+ line: z
150
+ .number()
151
+ .int()
152
+ .describe("Line from the hit: `anchor_line` when truncated, otherwise `start_line` (1-indexed)."),
153
153
  repo: z.string().describe(REPO_DESCRIPTION),
154
- top_k: z.number().int().min(1).optional().describe("Number of similar chunks to return."),
154
+ top_k: z
155
+ .number()
156
+ .int()
157
+ .min(1)
158
+ .max(MAX_MCP_TOP_K)
159
+ .optional()
160
+ .describe(`Number of similar chunks to return (default ${DEFAULT_MCP_TOP_K}, max ${MAX_MCP_TOP_K}).`),
155
161
  },
156
162
  },
157
163
  async ({ file_path: filePath, line, repo, top_k: topK }) => {
@@ -164,7 +170,7 @@ export function createMcpServer(cache: IndexCache): McpServer {
164
170
  `No chunk found at ${filePath}:${line}. Make sure the file is indexed and the line number is within a known chunk.`,
165
171
  );
166
172
  }
167
- const results = await index.findRelated(chunk, topK ?? DEFAULT_MCP_TOP_K);
173
+ const results = await index.findRelated(chunk, clampMcpTopK(topK));
168
174
  if (results.length === 0) {
169
175
  return toolText(
170
176
  JSON.stringify({ error: `No related chunks found for ${filePath}:${line}.` }),
@@ -172,7 +178,10 @@ export function createMcpServer(cache: IndexCache): McpServer {
172
178
  }
173
179
  return toolText(
174
180
  JSON.stringify(
175
- formatResults(`Chunks related to ${filePath}:${line}`, results, { repoRoot }),
181
+ formatResults(`Chunks related to ${filePath}:${line}`, results, {
182
+ repoRoot,
183
+ snippet: true,
184
+ }),
176
185
  ),
177
186
  );
178
187
  } catch (err) {
@@ -2,6 +2,12 @@ import { basename, dirname } from "node:path";
2
2
  import { splitIdentifier } from "../tokens.ts";
3
3
  import type { Chunk } from "../types.ts";
4
4
  import { chunkKey } from "../types.ts";
5
+ import { searchImprovementsEnabled } from "./features.ts";
6
+ import {
7
+ boostExactStemMatches,
8
+ boostLocationSignals,
9
+ penalizeInstallerForLocation,
10
+ } from "./location.ts";
5
11
 
6
12
  const SYMBOL_QUERY_RE =
7
13
  /^(?:(?:[A-Za-z_][A-Za-z0-9_]*(?:(?:::|\\|->|\.)[A-Za-z_][A-Za-z0-9_]*)+)|_(?:[A-Za-z0-9_]*)|(?:[A-Za-z][A-Za-z0-9]*[A-Z_][A-Za-z0-9_]*)|(?:[A-Z][A-Za-z0-9]*))$/;
@@ -165,6 +171,11 @@ export function applyQueryBoost(
165
171
  } else {
166
172
  boostStemMatches(combinedScores, query, maxScore, chunksByKey);
167
173
  boostEmbeddedSymbols(combinedScores, query, maxScore, allChunks, chunksByKey);
174
+ if (searchImprovementsEnabled()) {
175
+ boostExactStemMatches(combinedScores, query, maxScore, chunksByKey);
176
+ boostLocationSignals(combinedScores, query, maxScore, allChunks, chunksByKey);
177
+ penalizeInstallerForLocation(combinedScores, query, chunksByKey);
178
+ }
168
179
  }
169
180
  return combinedScores;
170
181
  }
@@ -0,0 +1,11 @@
1
+ /** Toggle search ranking/indexing improvements (A/B via MIRU_SEARCH_V2=0|1). */
2
+ export function searchImprovementsEnabled(): boolean {
3
+ const value = process.env.MIRU_SEARCH_V2;
4
+ if (value === "0" || value === "false") {
5
+ return false;
6
+ }
7
+ if (value === "1" || value === "true") {
8
+ return true;
9
+ }
10
+ return true;
11
+ }
@@ -0,0 +1,241 @@
1
+ import { basename } from "node:path";
2
+ import type { Chunk } from "../types.ts";
3
+ import { chunkKey } from "../types.ts";
4
+
5
+ const LOCATION_QUERY_RE =
6
+ /\b(where(?:'s| is)?|entry\s*point|bootstrap|starts?|live[s]?|located|defined|wiring)\b/i;
7
+
8
+ const ENTRY_POINT_QUERY_RE = /\b(entry\s*point|bootstrap|starts?|wiring|command\s*line)\b/i;
9
+
10
+ const INTEGRATION_QUERY_RE = /\b(install|uninstall|hook|agent|configure|setup)\b/i;
11
+
12
+ const INSTALLER_PATH_RE = /(?:^|\/)installer(?:\/|$)/;
13
+
14
+ const ENTRY_POINT_CONTENT_RE =
15
+ /#!\/usr\/bin|(?:^|\n)\s*(?:async\s+)?function\s+main\s*\(|(?:^|\n)\s*if\s*\(\s*require\.main\s*===\s*module/;
16
+
17
+ const PACKAGE_ENTRY_RE = /^\[package entry\]/;
18
+
19
+ const LOCATION_BOOST_MULTIPLIER = 2.5;
20
+ const INSTALLER_LOCATION_PENALTY = 0.35;
21
+
22
+ /** Too common as filenames to get exact-stem boosts from incidental query tokens. */
23
+ const IMPLEMENTATION_HINTS = new Set(
24
+ [
25
+ "command",
26
+ "handler",
27
+ "router",
28
+ "routing",
29
+ "middleware",
30
+ "model",
31
+ "channel",
32
+ "request",
33
+ "response",
34
+ "dispatch",
35
+ "pipeline",
36
+ ].map((w) => w.toLowerCase()),
37
+ );
38
+
39
+ const GENERIC_STEM_DENY = new Set(
40
+ [
41
+ "route",
42
+ "routes",
43
+ "router",
44
+ "routing",
45
+ "handler",
46
+ "middleware",
47
+ "channel",
48
+ "model",
49
+ "models",
50
+ "request",
51
+ "response",
52
+ "utils",
53
+ "util",
54
+ "server",
55
+ "client",
56
+ "application",
57
+ "dispatch",
58
+ "core",
59
+ "lib",
60
+ "main",
61
+ "mod",
62
+ "test",
63
+ "tests",
64
+ "schema",
65
+ "config",
66
+ "helper",
67
+ "helpers",
68
+ "engine",
69
+ "pipeline",
70
+ "plug",
71
+ "plugs",
72
+ "command",
73
+ "commands",
74
+ "index",
75
+ ].map((w) => w.toLowerCase()),
76
+ );
77
+
78
+ export function isLocationQuery(query: string): boolean {
79
+ return LOCATION_QUERY_RE.test(query);
80
+ }
81
+
82
+ export function isEntryPointQuery(query: string): boolean {
83
+ return ENTRY_POINT_QUERY_RE.test(query);
84
+ }
85
+
86
+ function fileStemMatchesQuery(filePath: string, query: string): boolean {
87
+ const stem = basename(filePath)
88
+ .replace(/\.[^.]+$/, "")
89
+ .toLowerCase();
90
+ const rawTokens = (query.match(/[a-zA-Z_][a-zA-Z0-9_-]*/g) ?? []).map((t) => t.toLowerCase());
91
+
92
+ if (rawTokens.includes(stem)) {
93
+ return true;
94
+ }
95
+
96
+ for (const token of rawTokens) {
97
+ if (token.includes("-") && token.split("-").includes(stem)) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ const stemNorm = stem.replace(/[-_]/g, "");
103
+ return rawTokens.some((t) => t.replace(/[-_]/g, "") === stemNorm);
104
+ }
105
+
106
+ export function isIntegrationQuery(query: string): boolean {
107
+ return INTEGRATION_QUERY_RE.test(query);
108
+ }
109
+
110
+ function chunkLooksLikeEntryPoint(chunk: Chunk): boolean {
111
+ if (PACKAGE_ENTRY_RE.test(chunk.content)) {
112
+ return true;
113
+ }
114
+ return ENTRY_POINT_CONTENT_RE.test(chunk.content);
115
+ }
116
+
117
+ export function boostLocationSignals(
118
+ boosted: Map<string, number>,
119
+ query: string,
120
+ maxScore: number,
121
+ allChunks: Chunk[],
122
+ chunksByKey: Map<string, Chunk>,
123
+ ): void {
124
+ if (!isLocationQuery(query)) {
125
+ return;
126
+ }
127
+
128
+ const entryPointQuery = isEntryPointQuery(query);
129
+ const packageBoost = maxScore * (entryPointQuery ? LOCATION_BOOST_MULTIPLIER : 0.75);
130
+ const codeBoost = maxScore * LOCATION_BOOST_MULTIPLIER;
131
+
132
+ for (const chunk of allChunks) {
133
+ if (!chunkLooksLikeEntryPoint(chunk)) {
134
+ continue;
135
+ }
136
+
137
+ const isPackageEntry = PACKAGE_ENTRY_RE.test(chunk.content);
138
+ if (isPackageEntry) {
139
+ const key = chunkKey(chunk);
140
+ boosted.set(key, (boosted.get(key) ?? 0) + packageBoost);
141
+ chunksByKey.set(key, chunk);
142
+ continue;
143
+ }
144
+
145
+ if (!entryPointQuery && !fileStemMatchesQuery(chunk.file_path, query)) {
146
+ continue;
147
+ }
148
+
149
+ const key = chunkKey(chunk);
150
+ boosted.set(key, (boosted.get(key) ?? 0) + codeBoost);
151
+ chunksByKey.set(key, chunk);
152
+ }
153
+ }
154
+
155
+ export function penalizeInstallerForLocation(
156
+ boosted: Map<string, number>,
157
+ query: string,
158
+ chunksByKey: Map<string, Chunk>,
159
+ ): void {
160
+ if (!isLocationQuery(query) || isIntegrationQuery(query)) {
161
+ return;
162
+ }
163
+
164
+ for (const [key, score] of boosted) {
165
+ const chunk = chunksByKey.get(key);
166
+ if (!chunk) {
167
+ continue;
168
+ }
169
+ if (INSTALLER_PATH_RE.test(chunk.file_path.replace(/\\/g, "/"))) {
170
+ boosted.set(key, score * INSTALLER_LOCATION_PENALTY);
171
+ }
172
+ }
173
+ }
174
+
175
+ function hasSpecificImplementationTarget(
176
+ query: string,
177
+ chunksByKey: Map<string, Chunk>,
178
+ ): boolean {
179
+ const lowered = query.toLowerCase();
180
+ for (const hint of IMPLEMENTATION_HINTS) {
181
+ if (!lowered.includes(hint)) {
182
+ continue;
183
+ }
184
+ for (const chunk of chunksByKey.values()) {
185
+ const stem = basename(chunk.file_path)
186
+ .replace(/\.[^.]+$/, "")
187
+ .toLowerCase();
188
+ if (stem === hint) {
189
+ return true;
190
+ }
191
+ }
192
+ }
193
+ return false;
194
+ }
195
+
196
+ export function boostExactStemMatches(
197
+ boosted: Map<string, number>,
198
+ query: string,
199
+ maxScore: number,
200
+ chunksByKey: Map<string, Chunk>,
201
+ ): void {
202
+ const keywords = new Set(
203
+ (query.match(/[a-zA-Z_][a-zA-Z0-9_-]*/g) ?? [])
204
+ .filter((w) => w.length > 2)
205
+ .map((w) => w.toLowerCase()),
206
+ );
207
+ if (keywords.size === 0) {
208
+ return;
209
+ }
210
+
211
+ const boost = maxScore * 0.75;
212
+ const preferImplementation = hasSpecificImplementationTarget(query, chunksByKey);
213
+
214
+ for (const key of [...boosted.keys()]) {
215
+ const chunk = chunksByKey.get(key);
216
+ if (!chunk) {
217
+ continue;
218
+ }
219
+ const stem = basename(chunk.file_path)
220
+ .replace(/\.[^.]+$/, "")
221
+ .toLowerCase();
222
+ const stemNorm = stem.replace(/[-_]/g, "");
223
+ for (const keyword of keywords) {
224
+ if (GENERIC_STEM_DENY.has(keyword)) {
225
+ continue;
226
+ }
227
+ if (preferImplementation && stem === keyword && !IMPLEMENTATION_HINTS.has(stem)) {
228
+ continue;
229
+ }
230
+ const keywordNorm = keyword.replace(/[-_]/g, "");
231
+ if (stem === keyword || stemNorm === keywordNorm) {
232
+ boosted.set(key, (boosted.get(key) ?? 0) + boost);
233
+ break;
234
+ }
235
+ const parts = keyword.split("-").filter((p) => p.length > 2);
236
+ if (parts.length > 1 && parts.some((p) => p === stem)) {
237
+ continue;
238
+ }
239
+ }
240
+ }
241
+ }
package/src/search.ts CHANGED
@@ -4,6 +4,8 @@ import type { SemanticIndex } from "./index/semantic-index.ts";
4
4
  import { selectorToMask } from "./index/sparse.ts";
5
5
  import { selectTopKScoreIndices } from "./index/topk.ts";
6
6
  import { applyQueryBoost, boostMultiChunkFiles } from "./ranking/boosting.ts";
7
+ import { searchImprovementsEnabled } from "./ranking/features.ts";
8
+ import { isLocationQuery } from "./ranking/location.ts";
7
9
  import { rerankTopk } from "./ranking/penalties.ts";
8
10
  import { resolveAlpha } from "./ranking/weighting.ts";
9
11
  import { tokenize } from "./tokens.ts";
@@ -93,7 +95,8 @@ export async function hybridSearch(options: {
93
95
  } = options;
94
96
 
95
97
  const alphaWeight = resolveAlpha(query, alpha);
96
- const candidateCount = topK * 5;
98
+ const candidateCount =
99
+ searchImprovementsEnabled() && isLocationQuery(query) ? topK * 10 : topK * 5;
97
100
  const chunksByKey = new Map(chunks.map((c) => [chunkKey(c), c]));
98
101
 
99
102
  const queryVecPromise = embeddings.embedQuery(query);
package/src/snippet.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { envOptionalInt } from "./env.ts";
2
+ import { tokenize } from "./tokens.ts";
3
+ import type { Chunk, SearchResult } from "./types.ts";
4
+
5
+ const SNIPPET_STOPWORDS = new Set(
6
+ "a an and are as at be by do does for from has have how if in is it not of on or the to was what when where which who why with".split(
7
+ " ",
8
+ ),
9
+ );
10
+
11
+ export interface SnippetMeta {
12
+ truncated: boolean;
13
+ anchor_line: number;
14
+ full_start_line: number;
15
+ full_end_line: number;
16
+ }
17
+
18
+ export interface SnippetResult {
19
+ chunk: Chunk;
20
+ meta: SnippetMeta;
21
+ }
22
+
23
+ export function resolveSnippetLines(): number {
24
+ return envOptionalInt(["MIRU_SNIPPET_LINES"], 3) ?? 15;
25
+ }
26
+
27
+ export function searchSnippetsEnabled(): boolean {
28
+ const value = process.env.MIRU_SEARCH_SNIPPETS;
29
+ if (value === "0" || value === "false") {
30
+ return false;
31
+ }
32
+ if (value === "1" || value === "true") {
33
+ return true;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ function queryMatchTerms(query: string): Set<string> {
39
+ const terms = new Set<string>();
40
+ for (const tok of tokenize(query)) {
41
+ if (tok.length >= 3 && !SNIPPET_STOPWORDS.has(tok)) {
42
+ terms.add(tok);
43
+ }
44
+ }
45
+ for (const word of query.match(/[a-zA-Z_][a-zA-Z0-9_-]*/g) ?? []) {
46
+ const lower = word.toLowerCase();
47
+ if (lower.length >= 3 && !SNIPPET_STOPWORDS.has(lower)) {
48
+ terms.add(lower);
49
+ }
50
+ }
51
+ return terms;
52
+ }
53
+
54
+ function scoreLine(line: string, terms: Set<string>): number {
55
+ const lower = line.toLowerCase();
56
+ let score = 0;
57
+ for (const term of terms) {
58
+ if (lower.includes(term)) {
59
+ score++;
60
+ }
61
+ }
62
+ return score;
63
+ }
64
+
65
+ /** Pick the 0-based line index inside `content` that best matches the query. */
66
+ export function anchorLineOffset(content: string, query: string): number {
67
+ const lines = content.split("\n");
68
+ if (lines.length === 0) {
69
+ return 0;
70
+ }
71
+
72
+ const terms = queryMatchTerms(query);
73
+ let bestIndex = Math.floor(lines.length / 2);
74
+ let bestScore = -1;
75
+
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i] ?? "";
78
+ const score = scoreLine(line, terms);
79
+ if (score > bestScore) {
80
+ bestScore = score;
81
+ bestIndex = i;
82
+ }
83
+ }
84
+
85
+ return bestIndex;
86
+ }
87
+
88
+ export function trimChunkToSnippet(
89
+ chunk: Chunk,
90
+ query: string,
91
+ linesEachSide = resolveSnippetLines(),
92
+ ): SnippetResult {
93
+ const lines = chunk.content.split("\n");
94
+ if (lines.length === 0) {
95
+ return {
96
+ chunk,
97
+ meta: {
98
+ truncated: false,
99
+ anchor_line: chunk.start_line,
100
+ full_start_line: chunk.start_line,
101
+ full_end_line: chunk.end_line,
102
+ },
103
+ };
104
+ }
105
+
106
+ const anchorOffset = anchorLineOffset(chunk.content, query);
107
+ const startOffset = Math.max(0, anchorOffset - linesEachSide);
108
+ const endOffset = Math.min(lines.length, anchorOffset + linesEachSide + 1);
109
+ const truncated = startOffset > 0 || endOffset < lines.length;
110
+
111
+ if (!truncated) {
112
+ return {
113
+ chunk,
114
+ meta: {
115
+ truncated: false,
116
+ anchor_line: chunk.start_line + anchorOffset,
117
+ full_start_line: chunk.start_line,
118
+ full_end_line: chunk.end_line,
119
+ },
120
+ };
121
+ }
122
+
123
+ const snippetContent = lines.slice(startOffset, endOffset).join("\n");
124
+ return {
125
+ chunk: {
126
+ ...chunk,
127
+ content: snippetContent,
128
+ start_line: chunk.start_line + startOffset,
129
+ end_line: chunk.start_line + endOffset - 1,
130
+ },
131
+ meta: {
132
+ truncated: true,
133
+ anchor_line: chunk.start_line + anchorOffset,
134
+ full_start_line: chunk.start_line,
135
+ full_end_line: chunk.end_line,
136
+ },
137
+ };
138
+ }
139
+
140
+ export function applySnippetsToResults(
141
+ results: SearchResult[],
142
+ query: string,
143
+ linesEachSide?: number,
144
+ ): Array<{ result: SearchResult; meta: SnippetMeta }> {
145
+ const radius = linesEachSide ?? resolveSnippetLines();
146
+ return results.map((result) => {
147
+ const { chunk, meta } = trimChunkToSnippet(result.chunk, query, radius);
148
+ return { result: { chunk, score: result.score }, meta };
149
+ });
150
+ }
151
+
152
+ export function estimateResultTokens(results: SearchResult[]): number {
153
+ return results.reduce((sum, r) => sum + Math.floor(r.chunk.content.length / 4), 0);
154
+ }