@takara-ai/miru-code 1.8.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -53
- package/docs/coverage-badge.json +6 -0
- package/grammars/manifest.json +10 -2
- package/grammars/tree-sitter-astro.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-php_only.wasm +0 -0
- package/grammars/tree-sitter-sql.wasm +0 -0
- package/grammars/tree-sitter-svelte.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/package.json +10 -2
- package/src/agents.ts +4 -4
- package/src/auth/device.ts +26 -21
- package/src/auth/providers.ts +26 -11
- package/src/benchmark/compare.ts +3 -10
- package/src/benchmark/grep.ts +64 -37
- package/src/benchmark/locate-compare.ts +8 -1
- package/src/benchmark/rg-literal.ts +9 -4
- package/src/cache.ts +3 -3
- package/src/chunking/ast.ts +21 -3
- package/src/chunking/grammars.ts +8 -3
- package/src/chunking/structural.ts +13 -24
- package/src/cli-mode.ts +18 -0
- package/src/cli.ts +54 -98
- package/src/credentials.ts +14 -14
- package/src/embeddings/openai.ts +6 -3
- package/src/embeddings/sagemaker.ts +18 -3
- package/src/embeddings/validate.ts +10 -7
- package/src/git.ts +6 -2
- package/src/help.ts +7 -7
- package/src/index/bm25.ts +66 -6
- package/src/index/chunk-selector.ts +39 -11
- package/src/index/create.ts +5 -9
- package/src/index/files.ts +5 -1
- package/src/index/incremental.ts +48 -8
- package/src/index/sparse.ts +14 -2
- package/src/installer/agents.ts +85 -65
- package/src/installer/benchmark-mode.ts +40 -11
- package/src/installer/config.ts +105 -42
- package/src/installer/installer.ts +59 -34
- package/src/installer/legacy-hooks.ts +131 -0
- package/src/installer/prompt.ts +20 -22
- package/src/installer/search-policy.ts +10 -10
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +65 -46
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +90 -22
- package/src/prompt.ts +51 -14
- package/src/ranking/boosting.ts +5 -6
- package/src/ranking/location.ts +6 -10
- package/src/ranking/penalties.ts +14 -22
- package/src/search.ts +8 -12
- package/src/setup.ts +25 -13
- package/src/snippet.ts +8 -26
- package/src/spinner.ts +26 -16
- package/src/tokenizer/bert-wordpiece.ts +0 -6
- package/src/utils.ts +1 -15
- package/src/version.ts +4 -4
- package/src/installer/hooks/install.ts +0 -549
- package/src/installer/hooks/opencode-plugin.ts +0 -54
- package/src/installer/hooks/search-guard.ts +0 -272
package/src/installer/prompt.ts
CHANGED
|
@@ -113,18 +113,9 @@ function parseArrowKey(chunk: Buffer, text: string): string | null {
|
|
|
113
113
|
const introducer = text.charAt(1);
|
|
114
114
|
if (introducer === "[" || introducer === "O") {
|
|
115
115
|
const arrow = text.charAt(text.length - 1);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (arrow === "B") {
|
|
120
|
-
return "down";
|
|
121
|
-
}
|
|
122
|
-
if (arrow === "C") {
|
|
123
|
-
return "right";
|
|
124
|
-
}
|
|
125
|
-
if (arrow === "D") {
|
|
126
|
-
return "left";
|
|
127
|
-
}
|
|
116
|
+
return (
|
|
117
|
+
({ A: "up", B: "down", C: "right", D: "left" } as Record<string, string>)[arrow] ?? null
|
|
118
|
+
);
|
|
128
119
|
}
|
|
129
120
|
}
|
|
130
121
|
|
|
@@ -308,9 +299,13 @@ function updateConfirmPromptLine(question: string, yesSelected: boolean): void {
|
|
|
308
299
|
);
|
|
309
300
|
}
|
|
310
301
|
|
|
311
|
-
export async function promptConfirm(
|
|
302
|
+
export async function promptConfirm(
|
|
303
|
+
question: string,
|
|
304
|
+
defaultYes = true,
|
|
305
|
+
io: { input?: typeof input; output?: typeof output } = {},
|
|
306
|
+
): Promise<boolean> {
|
|
312
307
|
if (!input.isTTY) {
|
|
313
|
-
const rl = readline.createInterface({ input, output });
|
|
308
|
+
const rl = readline.createInterface({ input: io.input ?? input, output: io.output ?? output });
|
|
314
309
|
try {
|
|
315
310
|
const hintText = defaultYes ? dim("[Y/n]") : dim("[y/N]");
|
|
316
311
|
const answer = (await rl.question(`${question} ${hintText} `)).trim().toLowerCase();
|
|
@@ -354,10 +349,13 @@ export async function promptConfirm(question: string, defaultYes = true): Promis
|
|
|
354
349
|
export async function promptMultiSelectLegacy<T>(
|
|
355
350
|
title: string,
|
|
356
351
|
items: Array<{ label: string; value: T; checked: boolean }>,
|
|
352
|
+
io: { input?: typeof input; output?: typeof output } = {},
|
|
357
353
|
): Promise<T[] | null> {
|
|
358
|
-
|
|
359
|
-
output
|
|
360
|
-
|
|
354
|
+
const promptInput = io.input ?? input;
|
|
355
|
+
const promptOutput = io.output ?? output;
|
|
356
|
+
promptOutput.write("\n");
|
|
357
|
+
promptOutput.write(`${bold(title)}\n`);
|
|
358
|
+
divider("─", 48, promptOutput);
|
|
361
359
|
|
|
362
360
|
for (let i = 0; i < items.length; i++) {
|
|
363
361
|
const item = items[i];
|
|
@@ -365,13 +363,13 @@ export async function promptMultiSelectLegacy<T>(
|
|
|
365
363
|
continue;
|
|
366
364
|
}
|
|
367
365
|
const mark = item.checked ? cyan("[x]") : dim("[ ]");
|
|
368
|
-
|
|
366
|
+
promptOutput.write(` ${dim(String(i + 1).padStart(2))}. ${mark} ${item.label}\n`);
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
|
|
372
|
-
hint("Enter numbers (1,3), 'all', or press Enter for defaults",
|
|
369
|
+
promptOutput.write("\n");
|
|
370
|
+
hint("Enter numbers (1,3), 'all', or press Enter for defaults", promptOutput);
|
|
373
371
|
|
|
374
|
-
const rl = readline.createInterface({ input, output });
|
|
372
|
+
const rl = readline.createInterface({ input: promptInput, output: promptOutput });
|
|
375
373
|
try {
|
|
376
374
|
const answer = (await rl.question(`${dim("> ")}`)).toLowerCase();
|
|
377
375
|
if (!answer) {
|
|
@@ -390,7 +388,7 @@ export async function promptMultiSelectLegacy<T>(
|
|
|
390
388
|
}
|
|
391
389
|
|
|
392
390
|
if (indices.size === 0) {
|
|
393
|
-
warn("No valid selection.",
|
|
391
|
+
warn("No valid selection.", promptOutput);
|
|
394
392
|
return null;
|
|
395
393
|
}
|
|
396
394
|
|
|
@@ -28,15 +28,17 @@ export function buildSearchPolicyBody(native: NativeToolNames): string {
|
|
|
28
28
|
|
|
29
29
|
${SNIPPET_GUIDANCE}
|
|
30
30
|
|
|
31
|
+
\`repo\` is optional, set it for another repo.
|
|
32
|
+
|
|
31
33
|
Use Miru MCP tools:
|
|
32
34
|
- \`locate\` — exact substring (env var, symbol, error code); prefer \`mode=count\` or \`locations\`
|
|
33
|
-
- \`search\` — one call per conceptual question;
|
|
35
|
+
- \`search\` — one call per conceptual question; use \`include\` / \`exclude\` Gitignore-style globs to scope files (same syntax as \`locate\`)
|
|
34
36
|
- \`expand\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\`)
|
|
35
37
|
- \`find_related\` — similar code in other files (hits may also be snippets; use \`expand\` if truncated)
|
|
36
38
|
|
|
37
39
|
Call shapes:
|
|
38
|
-
- \`locate(literal="<exact substring>"
|
|
39
|
-
- \`search(query="<natural language>",
|
|
40
|
+
- \`locate(literal="<exact substring>")\` — prefer \`mode="locations"\`/\`"count"\`
|
|
41
|
+
- \`search(query="<natural language>", include=["src/auth/**"])\` — use the same \`include\` / \`exclude\` globs on \`locate\` when scoping an exact-string search
|
|
40
42
|
|
|
41
43
|
Stop rules:
|
|
42
44
|
- Literal in the request → \`locate\`, not \`search\`
|
|
@@ -54,7 +56,7 @@ ${buildSearchPolicyTable(native)}`;
|
|
|
54
56
|
export function buildMcpWorkflow(native: NativeToolNames): string {
|
|
55
57
|
return `### MCP workflow
|
|
56
58
|
|
|
57
|
-
1. Call \`search
|
|
59
|
+
1. Call \`search\`; add \`repo\` only for another repo.
|
|
58
60
|
2. For exact literals, call \`locate\` (prefer \`mode=count\` or \`locations\`).
|
|
59
61
|
3. On \`truncated: true\`, call \`expand\` — only if the snippet doesn't already answer.
|
|
60
62
|
4. Use \`find_related\` to trace similar code in other files — not for more context in the same file.
|
|
@@ -71,7 +73,7 @@ miru find-related src/auth.ts 42 .
|
|
|
71
73
|
\`\`\`
|
|
72
74
|
|
|
73
75
|
After large refactors or CLI-only use, run \`miru clear <path>\` to rebuild the disk cache.
|
|
74
|
-
If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;
|
|
76
|
+
If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code@latest\`.`;
|
|
75
77
|
|
|
76
78
|
export function buildSubagentBody(native: NativeToolNames): string {
|
|
77
79
|
return `When Miru MCP is available, use MCP \`search\`, \`locate\`, \`expand\`, and \`find_related\` — not ${native.explorationDenied} for exploration.
|
|
@@ -118,7 +120,7 @@ export const MCP_SERVER_INSTRUCTIONS =
|
|
|
118
120
|
"Exact token (identifier, quoted string, env var, error code) → `locate`; everything else → `search` once, no paraphrase retries. " +
|
|
119
121
|
`${SNIPPET_GUIDANCE} ` +
|
|
120
122
|
"`find_related` traces similar code elsewhere, not more context in the same file. " +
|
|
121
|
-
"
|
|
123
|
+
"`repo` is optional, good for multi repo workflows. " +
|
|
122
124
|
"Native Grep/Glob only outside the indexed repo or for non-code tasks. " +
|
|
123
125
|
"Default indexed scope is code, config, and docs; an empty result can mean out-of-scope, not nonexistent. " +
|
|
124
126
|
"On credential errors, call this server's `auth` tool — not the host `mcp_auth` tool.";
|
|
@@ -134,7 +136,7 @@ export const MCP_BENCHMARK_SERVER_INSTRUCTIONS =
|
|
|
134
136
|
|
|
135
137
|
export const MCP_SEARCH_TOOL_DESCRIPTION =
|
|
136
138
|
"Your default search for all code search queries in this indexed repo — the best, fastest, and cheapest way to find code; better than any other tool. " +
|
|
137
|
-
"Returns compact snippets (~±15 lines). One call per question. " +
|
|
139
|
+
"Returns up to 5 compact snippets (~±15 lines). One call per question. Use `include` / `exclude` Gitignore-style globs to scope files; these work the same way on `locate`. " +
|
|
138
140
|
"Exact literal (env var, symbol, error code, quoted text)? Use `locate` instead. " +
|
|
139
141
|
"On `truncated: true`, call `expand` — only if the snippet doesn't already answer the question.";
|
|
140
142
|
|
|
@@ -155,11 +157,9 @@ export const MCP_EXPAND_TOOL_DESCRIPTION =
|
|
|
155
157
|
"Use only if `truncated: true` and the snippet doesn't already answer the question — not for similar code elsewhere (use find_related).";
|
|
156
158
|
|
|
157
159
|
export const MCP_FIND_RELATED_TOOL_DESCRIPTION =
|
|
158
|
-
"Find
|
|
160
|
+
"Find up to 5 chunks similar to a file:line in OTHER parts of the codebase. Results may be snippets; use `expand` when `truncated: true`. " +
|
|
159
161
|
"For more context in the same file, use `expand` instead.";
|
|
160
162
|
|
|
161
163
|
export const MCP_READ_BENCHMARK_TOOL_DESCRIPTION =
|
|
162
164
|
"Cumulative Miru vs Grep token savings from saved `search` and `locate` calls. Returns compact totals {n,saved,save_pct,miru,grep}. " +
|
|
163
165
|
"Do not call unless the user asks about savings.";
|
|
164
|
-
|
|
165
|
-
export const SEARCH_GUARD_EXPAND_HINT = SNIPPET_GUIDANCE;
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -49,6 +49,11 @@ type CacheEntry = {
|
|
|
49
49
|
lastError: Error | null;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
export interface IndexCacheDependencies {
|
|
53
|
+
fromSource?: typeof MiruIndex.fromSource;
|
|
54
|
+
watch?: typeof watch;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
export function mcpWatchEnabled(): boolean {
|
|
53
58
|
const raw = process.env.MIRU_MCP_WATCH;
|
|
54
59
|
return raw !== "0" && raw !== "false";
|
|
@@ -76,12 +81,20 @@ export function shouldIgnoreWatchPath(relativePath: string | null | undefined):
|
|
|
76
81
|
export class IndexCache {
|
|
77
82
|
private readonly content: ContentType[];
|
|
78
83
|
private readonly defaultRef: string | null;
|
|
84
|
+
private readonly fromSource: typeof MiruIndex.fromSource;
|
|
85
|
+
private readonly watchSource: typeof watch;
|
|
79
86
|
private readonly entries = new Map<string, CacheEntry>();
|
|
80
87
|
readonly watchers = new Map<string, WatcherHandle>();
|
|
81
88
|
|
|
82
|
-
constructor(
|
|
89
|
+
constructor(
|
|
90
|
+
content: ContentType[] = defaultContentTypes(),
|
|
91
|
+
defaultRef: string | null = null,
|
|
92
|
+
dependencies: IndexCacheDependencies = {},
|
|
93
|
+
) {
|
|
83
94
|
this.content = content;
|
|
84
95
|
this.defaultRef = defaultRef;
|
|
96
|
+
this.fromSource = dependencies.fromSource ?? MiruIndex.fromSource;
|
|
97
|
+
this.watchSource = dependencies.watch ?? watch;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
private ensureEntry(cacheKey: string, source: string): CacheEntry {
|
|
@@ -128,7 +141,7 @@ export class IndexCache {
|
|
|
128
141
|
): Promise<MiruIndex> {
|
|
129
142
|
const entry = this.ensureEntry(cacheKey, source);
|
|
130
143
|
const task = (async () => {
|
|
131
|
-
const index = await
|
|
144
|
+
const index = await this.fromSource(source, this.content, undefined, ref);
|
|
132
145
|
if (!isGitUrl(source)) {
|
|
133
146
|
await index.saveToCache(resolve(source));
|
|
134
147
|
}
|
|
@@ -342,7 +355,7 @@ export class IndexCache {
|
|
|
342
355
|
|
|
343
356
|
let nativeWatcher: ReturnType<typeof watch> | null = null;
|
|
344
357
|
try {
|
|
345
|
-
nativeWatcher =
|
|
358
|
+
nativeWatcher = this.watchSource(resolved, { recursive: true }, (_event, filename) => {
|
|
346
359
|
this.noteFileChange(path, filename);
|
|
347
360
|
});
|
|
348
361
|
} catch {
|
package/src/mcp/server.ts
CHANGED
|
@@ -24,17 +24,15 @@ import {
|
|
|
24
24
|
import { formatLiteralLocate } from "../literal.ts";
|
|
25
25
|
import type { Chunk, ContentType, SearchResult } from "../types.ts";
|
|
26
26
|
import {
|
|
27
|
-
clampMcpTopK,
|
|
28
27
|
DEFAULT_EXPAND_AFTER,
|
|
29
28
|
DEFAULT_EXPAND_BEFORE,
|
|
30
|
-
DEFAULT_MCP_TOP_K,
|
|
31
29
|
dedupeResultsByFile,
|
|
32
30
|
expandChunksAtLine,
|
|
33
31
|
formatExpandResults,
|
|
34
32
|
formatResults,
|
|
35
33
|
localRepoRoot,
|
|
36
|
-
MAX_MCP_TOP_K,
|
|
37
34
|
resolveChunk,
|
|
35
|
+
SEARCH_RESULT_COUNT,
|
|
38
36
|
} from "../utils.ts";
|
|
39
37
|
import { registerAuthTool, toolErrorText } from "./auth-tool.ts";
|
|
40
38
|
import {
|
|
@@ -46,12 +44,13 @@ import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
|
|
|
46
44
|
import { MiruMcpServer } from "./runtime.ts";
|
|
47
45
|
|
|
48
46
|
const REPO_DESCRIPTION =
|
|
49
|
-
"
|
|
50
|
-
"Pass the project root for local workspaces. " +
|
|
47
|
+
"`repo` is optional. Set a local path or Git URL for another repo. " +
|
|
51
48
|
"The index is built on the first tool call and cached for the session.";
|
|
52
49
|
|
|
53
50
|
const BENCHMARK_SKIP_NOTES = {
|
|
54
51
|
local_repo_only: "Benchmark comparisons require a local repo path; git URL repos are skipped.",
|
|
52
|
+
filtered_search:
|
|
53
|
+
"Benchmark comparisons are skipped when search is restricted by include/exclude patterns.",
|
|
55
54
|
limited_locate:
|
|
56
55
|
"Benchmark comparison skipped: locate.limit is global, while rg/grep limits are per file. Omit limit for a valid token and recall comparison.",
|
|
57
56
|
incompatible_literal_baseline:
|
|
@@ -121,9 +120,26 @@ async function persistBenchmarkQuery(
|
|
|
121
120
|
|
|
122
121
|
export function createMcpServer(
|
|
123
122
|
cache: IndexCache,
|
|
124
|
-
options?: {
|
|
123
|
+
options?: {
|
|
124
|
+
benchmark?: boolean;
|
|
125
|
+
dependencies?: {
|
|
126
|
+
withGrepTimeoutFallback?: typeof withGrepTimeoutFallback;
|
|
127
|
+
benchmarkSearchComparison?: typeof benchmarkSearchComparison;
|
|
128
|
+
benchmarkLocateComparison?: typeof benchmarkLocateComparison;
|
|
129
|
+
selectComparableLiteralSearchTool?: typeof selectComparableLiteralSearchTool;
|
|
130
|
+
readBenchmarkRollup?: typeof readBenchmarkRollup;
|
|
131
|
+
};
|
|
132
|
+
},
|
|
125
133
|
): MiruMcpServer {
|
|
134
|
+
const defaultRepo = process.cwd();
|
|
126
135
|
const benchmark = options?.benchmark ?? false;
|
|
136
|
+
const dependencies = options?.dependencies;
|
|
137
|
+
const runWithGrepFallback = dependencies?.withGrepTimeoutFallback ?? withGrepTimeoutFallback;
|
|
138
|
+
const compareSearch = dependencies?.benchmarkSearchComparison ?? benchmarkSearchComparison;
|
|
139
|
+
const compareLocate = dependencies?.benchmarkLocateComparison ?? benchmarkLocateComparison;
|
|
140
|
+
const selectLiteralBaseline =
|
|
141
|
+
dependencies?.selectComparableLiteralSearchTool ?? selectComparableLiteralSearchTool;
|
|
142
|
+
const readRollup = dependencies?.readBenchmarkRollup ?? readBenchmarkRollup;
|
|
127
143
|
const server = new MiruMcpServer(
|
|
128
144
|
{
|
|
129
145
|
name: "miru",
|
|
@@ -139,37 +155,46 @@ export function createMcpServer(
|
|
|
139
155
|
server.registerTool(
|
|
140
156
|
"search",
|
|
141
157
|
{
|
|
142
|
-
description: `${MCP_SEARCH_TOOL_DESCRIPTION} Indexes
|
|
158
|
+
description: `${MCP_SEARCH_TOOL_DESCRIPTION} Indexes the selected directory or URL on first call; later calls reuse the session cache.`,
|
|
143
159
|
inputSchema: {
|
|
144
160
|
query: z
|
|
145
161
|
.string()
|
|
146
162
|
.describe(
|
|
147
163
|
"Natural language or code query — your default for all code search in this repo.",
|
|
148
164
|
),
|
|
149
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
165
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
166
|
+
include: z
|
|
167
|
+
.array(z.string().min(1))
|
|
168
|
+
.min(1)
|
|
169
|
+
.optional()
|
|
170
|
+
.describe(
|
|
171
|
+
'Gitignore-style glob patterns; only matching files are searched (e.g. ["src/auth/**", "docs/**/*.md"]). Same semantics as `locate.include`.',
|
|
172
|
+
),
|
|
173
|
+
exclude: z
|
|
174
|
+
.array(z.string().min(1))
|
|
153
175
|
.min(1)
|
|
154
|
-
.max(MAX_MCP_TOP_K)
|
|
155
176
|
.optional()
|
|
156
|
-
.describe(
|
|
177
|
+
.describe(
|
|
178
|
+
"Gitignore-style glob patterns; matching files are skipped. Same semantics as `locate.exclude`.",
|
|
179
|
+
),
|
|
157
180
|
dedupe_by_file: z
|
|
158
181
|
.boolean()
|
|
159
182
|
.optional()
|
|
160
183
|
.describe("Keep only the best hit per file (default true)."),
|
|
161
184
|
},
|
|
162
185
|
},
|
|
163
|
-
async ({ query, repo,
|
|
186
|
+
async ({ query, repo, include, exclude, dedupe_by_file: dedupeByFile }) => {
|
|
164
187
|
try {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
188
|
+
const source = repo ?? defaultRepo;
|
|
189
|
+
const index = await getIndexForRepo(source, cache);
|
|
190
|
+
const repoRoot = localRepoRoot(source);
|
|
191
|
+
const k = SEARCH_RESULT_COUNT;
|
|
169
192
|
let skip: BenchmarkSkipReason | undefined;
|
|
170
|
-
if (benchmark &&
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
if (benchmark && (include || exclude)) {
|
|
194
|
+
skip = "filtered_search";
|
|
195
|
+
} else if (benchmark && repoRoot) {
|
|
196
|
+
const comparison = await runWithGrepFallback(() =>
|
|
197
|
+
compareSearch({
|
|
173
198
|
query,
|
|
174
199
|
repoPath: repoRoot,
|
|
175
200
|
index,
|
|
@@ -199,7 +224,7 @@ export function createMcpServer(
|
|
|
199
224
|
skip = "local_repo_only";
|
|
200
225
|
}
|
|
201
226
|
|
|
202
|
-
let results = await index.search({ query, topK: k });
|
|
227
|
+
let results = await index.search({ query, topK: k, include, exclude });
|
|
203
228
|
if (dedupeByFile !== false) {
|
|
204
229
|
results = dedupeResultsByFile(results);
|
|
205
230
|
}
|
|
@@ -237,7 +262,7 @@ export function createMcpServer(
|
|
|
237
262
|
.describe(
|
|
238
263
|
"Alias for `literal` — accepted so a call shaped like `search(query=...)` still resolves as an exact-substring lookup.",
|
|
239
264
|
),
|
|
240
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
265
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
241
266
|
mode: z
|
|
242
267
|
.enum(["count", "locations", "lines"])
|
|
243
268
|
.optional()
|
|
@@ -283,19 +308,20 @@ export function createMcpServer(
|
|
|
283
308
|
return toolErrorText(new Error("locate requires `literal` (exact substring to find)."));
|
|
284
309
|
}
|
|
285
310
|
try {
|
|
286
|
-
const
|
|
311
|
+
const source = repo ?? defaultRepo;
|
|
312
|
+
const index = await getIndexForRepo(source, cache);
|
|
287
313
|
|
|
288
314
|
let skip: BenchmarkSkipReason | undefined;
|
|
289
|
-
const repoRoot = localRepoRoot(
|
|
315
|
+
const repoRoot = localRepoRoot(source);
|
|
290
316
|
if (benchmark && repoRoot && typeof lit === "string") {
|
|
291
317
|
if (locateOpts.limit != null) {
|
|
292
318
|
skip = "limited_locate";
|
|
293
|
-
} else if (!
|
|
319
|
+
} else if (!selectLiteralBaseline()) {
|
|
294
320
|
skip = "incompatible_literal_baseline";
|
|
295
321
|
}
|
|
296
322
|
if (!skip) {
|
|
297
|
-
const comparison = await
|
|
298
|
-
|
|
323
|
+
const comparison = await runWithGrepFallback(() =>
|
|
324
|
+
compareLocate({
|
|
299
325
|
literal: lit,
|
|
300
326
|
repoPath: repoRoot,
|
|
301
327
|
index,
|
|
@@ -335,7 +361,7 @@ export function createMcpServer(
|
|
|
335
361
|
.number()
|
|
336
362
|
.int()
|
|
337
363
|
.describe("Line from the search hit (`anchor_line` when truncated, else `start_line`)."),
|
|
338
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
364
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
339
365
|
before: z
|
|
340
366
|
.number()
|
|
341
367
|
.int()
|
|
@@ -352,8 +378,9 @@ export function createMcpServer(
|
|
|
352
378
|
},
|
|
353
379
|
async ({ file_path: filePath, anchor_line: anchorLine, repo, before, after }) => {
|
|
354
380
|
try {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
381
|
+
const source = repo ?? defaultRepo;
|
|
382
|
+
const index = await getIndexForRepo(source, cache);
|
|
383
|
+
const repoRoot = localRepoRoot(source);
|
|
357
384
|
const beforeCount = before ?? DEFAULT_EXPAND_BEFORE;
|
|
358
385
|
const afterCount = after ?? DEFAULT_EXPAND_AFTER;
|
|
359
386
|
const { anchor, chunks: expanded } = expandChunksAtLine(
|
|
@@ -396,29 +423,21 @@ export function createMcpServer(
|
|
|
396
423
|
.number()
|
|
397
424
|
.int()
|
|
398
425
|
.describe("Line from the search hit (`anchor_line` when truncated, else `start_line`)."),
|
|
399
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
400
|
-
top_k: z
|
|
401
|
-
.number()
|
|
402
|
-
.int()
|
|
403
|
-
.min(1)
|
|
404
|
-
.max(MAX_MCP_TOP_K)
|
|
405
|
-
.optional()
|
|
406
|
-
.describe(
|
|
407
|
-
`Number of similar chunks to return (default ${DEFAULT_MCP_TOP_K}, max ${MAX_MCP_TOP_K}).`,
|
|
408
|
-
),
|
|
426
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
409
427
|
},
|
|
410
428
|
},
|
|
411
|
-
async ({ file_path: filePath, anchor_line: anchorLine, repo
|
|
429
|
+
async ({ file_path: filePath, anchor_line: anchorLine, repo }) => {
|
|
412
430
|
try {
|
|
413
|
-
const
|
|
414
|
-
const
|
|
431
|
+
const source = repo ?? defaultRepo;
|
|
432
|
+
const index = await getIndexForRepo(source, cache);
|
|
433
|
+
const repoRoot = localRepoRoot(source);
|
|
415
434
|
const chunk = resolveChunk(index.chunks, filePath, anchorLine, repoRoot);
|
|
416
435
|
if (!chunk) {
|
|
417
436
|
return toolText(
|
|
418
437
|
`No chunk found at ${filePath}:${anchorLine}. Make sure the file is indexed and the line number is within a known chunk.`,
|
|
419
438
|
);
|
|
420
439
|
}
|
|
421
|
-
const results = await index.findRelated(chunk,
|
|
440
|
+
const results = await index.findRelated(chunk, SEARCH_RESULT_COUNT);
|
|
422
441
|
if (results.length === 0) {
|
|
423
442
|
return toolText(`No related chunks found for ${filePath}:${anchorLine}.`);
|
|
424
443
|
}
|
|
@@ -453,7 +472,7 @@ export function createMcpServer(
|
|
|
453
472
|
},
|
|
454
473
|
async ({ repo }) => {
|
|
455
474
|
try {
|
|
456
|
-
const rollup = await
|
|
475
|
+
const rollup = await readRollup({ repo });
|
|
457
476
|
return toolText(JSON.stringify(rollup));
|
|
458
477
|
} catch (err) {
|
|
459
478
|
return toolText(err instanceof Error ? err.message : String(err));
|
package/src/mcp/stdio.ts
CHANGED
|
@@ -45,6 +45,21 @@ export class StdioTransport {
|
|
|
45
45
|
private buffer = "";
|
|
46
46
|
private started = false;
|
|
47
47
|
private abortController: AbortController | null = null;
|
|
48
|
+
private readonly inputStream: () => ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
49
|
+
private readonly writeText: (text: string) => Promise<unknown>;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
options: {
|
|
53
|
+
inputStream?: () => ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
54
|
+
writeText?: (text: string) => Promise<unknown>;
|
|
55
|
+
stdin?: { stream: () => ReadableStream<Uint8Array<ArrayBufferLike>> };
|
|
56
|
+
bunWrite?: (destination: typeof Bun.stdout, text: string) => Promise<unknown>;
|
|
57
|
+
} = {},
|
|
58
|
+
) {
|
|
59
|
+
this.inputStream = options.inputStream ?? (() => (options.stdin ?? Bun.stdin).stream());
|
|
60
|
+
this.writeText =
|
|
61
|
+
options.writeText ?? ((text) => (options.bunWrite ?? Bun.write)(Bun.stdout, text));
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
onmessage?: (message: JsonRpcMessage) => void | Promise<void>;
|
|
50
65
|
onerror?: (error: Error) => void;
|
|
@@ -56,7 +71,7 @@ export class StdioTransport {
|
|
|
56
71
|
}
|
|
57
72
|
this.started = true;
|
|
58
73
|
this.abortController = new AbortController();
|
|
59
|
-
const reader =
|
|
74
|
+
const reader = this.inputStream().getReader();
|
|
60
75
|
const decoder = new TextDecoder();
|
|
61
76
|
|
|
62
77
|
try {
|
|
@@ -85,7 +100,7 @@ export class StdioTransport {
|
|
|
85
100
|
|
|
86
101
|
async send(message: JsonRpcMessage): Promise<void> {
|
|
87
102
|
const line = `${JSON.stringify(message)}\n`;
|
|
88
|
-
await
|
|
103
|
+
await this.writeText(line);
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
private async append(chunk: string): Promise<void> {
|