@takara-ai/miru-code 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -48
- 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 +10 -45
- package/src/credentials.ts +8 -13
- 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 +5 -4
- package/src/index/bm25.ts +66 -6
- 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 +8 -8
- package/src/mcp/auth-tool.ts +93 -33
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +42 -22
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +54 -18
- 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 +4 -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 +0 -4
- 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/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
|
@@ -44,8 +44,7 @@ import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
|
|
|
44
44
|
import { MiruMcpServer } from "./runtime.ts";
|
|
45
45
|
|
|
46
46
|
const REPO_DESCRIPTION =
|
|
47
|
-
"
|
|
48
|
-
"Pass the project root for local workspaces. " +
|
|
47
|
+
"`repo` is optional. Set a local path or Git URL for another repo. " +
|
|
49
48
|
"The index is built on the first tool call and cached for the session.";
|
|
50
49
|
|
|
51
50
|
const BENCHMARK_SKIP_NOTES = {
|
|
@@ -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,14 +155,14 @@ 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),
|
|
165
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
150
166
|
include: z
|
|
151
167
|
.array(z.string().min(1))
|
|
152
168
|
.min(1)
|
|
@@ -169,15 +185,16 @@ export function createMcpServer(
|
|
|
169
185
|
},
|
|
170
186
|
async ({ query, repo, include, exclude, dedupe_by_file: dedupeByFile }) => {
|
|
171
187
|
try {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
188
|
+
const source = repo ?? defaultRepo;
|
|
189
|
+
const index = await getIndexForRepo(source, cache);
|
|
190
|
+
const repoRoot = localRepoRoot(source);
|
|
174
191
|
const k = SEARCH_RESULT_COUNT;
|
|
175
192
|
let skip: BenchmarkSkipReason | undefined;
|
|
176
193
|
if (benchmark && (include || exclude)) {
|
|
177
194
|
skip = "filtered_search";
|
|
178
195
|
} else if (benchmark && repoRoot) {
|
|
179
|
-
const comparison = await
|
|
180
|
-
|
|
196
|
+
const comparison = await runWithGrepFallback(() =>
|
|
197
|
+
compareSearch({
|
|
181
198
|
query,
|
|
182
199
|
repoPath: repoRoot,
|
|
183
200
|
index,
|
|
@@ -245,7 +262,7 @@ export function createMcpServer(
|
|
|
245
262
|
.describe(
|
|
246
263
|
"Alias for `literal` — accepted so a call shaped like `search(query=...)` still resolves as an exact-substring lookup.",
|
|
247
264
|
),
|
|
248
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
265
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
249
266
|
mode: z
|
|
250
267
|
.enum(["count", "locations", "lines"])
|
|
251
268
|
.optional()
|
|
@@ -291,19 +308,20 @@ export function createMcpServer(
|
|
|
291
308
|
return toolErrorText(new Error("locate requires `literal` (exact substring to find)."));
|
|
292
309
|
}
|
|
293
310
|
try {
|
|
294
|
-
const
|
|
311
|
+
const source = repo ?? defaultRepo;
|
|
312
|
+
const index = await getIndexForRepo(source, cache);
|
|
295
313
|
|
|
296
314
|
let skip: BenchmarkSkipReason | undefined;
|
|
297
|
-
const repoRoot = localRepoRoot(
|
|
315
|
+
const repoRoot = localRepoRoot(source);
|
|
298
316
|
if (benchmark && repoRoot && typeof lit === "string") {
|
|
299
317
|
if (locateOpts.limit != null) {
|
|
300
318
|
skip = "limited_locate";
|
|
301
|
-
} else if (!
|
|
319
|
+
} else if (!selectLiteralBaseline()) {
|
|
302
320
|
skip = "incompatible_literal_baseline";
|
|
303
321
|
}
|
|
304
322
|
if (!skip) {
|
|
305
|
-
const comparison = await
|
|
306
|
-
|
|
323
|
+
const comparison = await runWithGrepFallback(() =>
|
|
324
|
+
compareLocate({
|
|
307
325
|
literal: lit,
|
|
308
326
|
repoPath: repoRoot,
|
|
309
327
|
index,
|
|
@@ -343,7 +361,7 @@ export function createMcpServer(
|
|
|
343
361
|
.number()
|
|
344
362
|
.int()
|
|
345
363
|
.describe("Line from the search hit (`anchor_line` when truncated, else `start_line`)."),
|
|
346
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
364
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
347
365
|
before: z
|
|
348
366
|
.number()
|
|
349
367
|
.int()
|
|
@@ -360,8 +378,9 @@ export function createMcpServer(
|
|
|
360
378
|
},
|
|
361
379
|
async ({ file_path: filePath, anchor_line: anchorLine, repo, before, after }) => {
|
|
362
380
|
try {
|
|
363
|
-
const
|
|
364
|
-
const
|
|
381
|
+
const source = repo ?? defaultRepo;
|
|
382
|
+
const index = await getIndexForRepo(source, cache);
|
|
383
|
+
const repoRoot = localRepoRoot(source);
|
|
365
384
|
const beforeCount = before ?? DEFAULT_EXPAND_BEFORE;
|
|
366
385
|
const afterCount = after ?? DEFAULT_EXPAND_AFTER;
|
|
367
386
|
const { anchor, chunks: expanded } = expandChunksAtLine(
|
|
@@ -404,13 +423,14 @@ export function createMcpServer(
|
|
|
404
423
|
.number()
|
|
405
424
|
.int()
|
|
406
425
|
.describe("Line from the search hit (`anchor_line` when truncated, else `start_line`)."),
|
|
407
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
426
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
408
427
|
},
|
|
409
428
|
},
|
|
410
429
|
async ({ file_path: filePath, anchor_line: anchorLine, repo }) => {
|
|
411
430
|
try {
|
|
412
|
-
const
|
|
413
|
-
const
|
|
431
|
+
const source = repo ?? defaultRepo;
|
|
432
|
+
const index = await getIndexForRepo(source, cache);
|
|
433
|
+
const repoRoot = localRepoRoot(source);
|
|
414
434
|
const chunk = resolveChunk(index.chunks, filePath, anchorLine, repoRoot);
|
|
415
435
|
if (!chunk) {
|
|
416
436
|
return toolText(
|
|
@@ -452,7 +472,7 @@ export function createMcpServer(
|
|
|
452
472
|
},
|
|
453
473
|
async ({ repo }) => {
|
|
454
474
|
try {
|
|
455
|
-
const rollup = await
|
|
475
|
+
const rollup = await readRollup({ repo });
|
|
456
476
|
return toolText(JSON.stringify(rollup));
|
|
457
477
|
} catch (err) {
|
|
458
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> {
|
package/src/miru-index.ts
CHANGED
|
@@ -26,6 +26,31 @@ import { chunkKey, chunkToDict, defaultContentTypes } from "./types.ts";
|
|
|
26
26
|
import { computeSourceCacheKey, isGitUrl } from "./utils.ts";
|
|
27
27
|
import { indexCacheEpoch } from "./version.ts";
|
|
28
28
|
|
|
29
|
+
interface FactoryDependencies {
|
|
30
|
+
getValidatedCache: typeof getValidatedCache;
|
|
31
|
+
resolveEmbeddingModel: typeof resolveEmbeddingModel;
|
|
32
|
+
getEmbeddingBackend: (model: string) => EmbeddingBackend;
|
|
33
|
+
createIndexFromPath: typeof createIndexFromPath;
|
|
34
|
+
cloneGitRepository: typeof cloneGitRepository;
|
|
35
|
+
findIndexCachePath: typeof findIndexCachePath;
|
|
36
|
+
loadCachedIndex: typeof loadCachedIndex;
|
|
37
|
+
saveBuiltIndex: (index: MiruIndex, path: string) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function factoryDependencies(overrides: Partial<FactoryDependencies> = {}): FactoryDependencies {
|
|
41
|
+
return {
|
|
42
|
+
getValidatedCache,
|
|
43
|
+
resolveEmbeddingModel,
|
|
44
|
+
getEmbeddingBackend,
|
|
45
|
+
createIndexFromPath,
|
|
46
|
+
cloneGitRepository,
|
|
47
|
+
findIndexCachePath,
|
|
48
|
+
loadCachedIndex,
|
|
49
|
+
saveBuiltIndex: (index, path) => index.save(path),
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
/**
|
|
30
55
|
* In-memory search index over a codebase: BM25 keyword scores plus semantic
|
|
31
56
|
* embeddings, with optional disk cache for reuse across runs.
|
|
@@ -171,18 +196,20 @@ export class MiruIndex {
|
|
|
171
196
|
content: ContentType[] = defaultContentTypes(),
|
|
172
197
|
embeddingModel?: string,
|
|
173
198
|
ref?: string | null,
|
|
199
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
174
200
|
): Promise<MiruIndex> {
|
|
201
|
+
const deps = factoryDependencies(dependencies);
|
|
175
202
|
const cacheKey = computeSourceCacheKey(url, ref);
|
|
176
|
-
const model = embeddingModel ?? resolveEmbeddingModel();
|
|
177
|
-
const cached = await getValidatedCache(cacheKey, model, content);
|
|
203
|
+
const model = embeddingModel ?? deps.resolveEmbeddingModel();
|
|
204
|
+
const cached = await deps.getValidatedCache(cacheKey, model, content);
|
|
178
205
|
if (cached) {
|
|
179
|
-
return MiruIndex.loadFromDisk(cached, model);
|
|
206
|
+
return MiruIndex.loadFromDisk(cached, model, deps);
|
|
180
207
|
}
|
|
181
208
|
|
|
182
|
-
const cloneDir = await cloneGitRepository(url, ref);
|
|
209
|
+
const cloneDir = await deps.cloneGitRepository(url, ref);
|
|
183
210
|
try {
|
|
184
|
-
const embeddings = getEmbeddingBackend(model);
|
|
185
|
-
const { bm25, semantic, chunks } = await createIndexFromPath(
|
|
211
|
+
const embeddings = deps.getEmbeddingBackend(model);
|
|
212
|
+
const { bm25, semantic, chunks } = await deps.createIndexFromPath(
|
|
186
213
|
cloneDir,
|
|
187
214
|
embeddings,
|
|
188
215
|
content,
|
|
@@ -198,7 +225,7 @@ export class MiruIndex {
|
|
|
198
225
|
root: null,
|
|
199
226
|
content,
|
|
200
227
|
});
|
|
201
|
-
await index.
|
|
228
|
+
await deps.saveBuiltIndex(index, deps.findIndexCachePath(cacheKey));
|
|
202
229
|
return index;
|
|
203
230
|
} finally {
|
|
204
231
|
await rm(cloneDir, { recursive: true, force: true });
|
|
@@ -211,11 +238,12 @@ export class MiruIndex {
|
|
|
211
238
|
content: ContentType[] = defaultContentTypes(),
|
|
212
239
|
embeddingModel?: string,
|
|
213
240
|
ref?: string | null,
|
|
241
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
214
242
|
): Promise<MiruIndex> {
|
|
215
243
|
if (isGitUrl(source)) {
|
|
216
|
-
return MiruIndex.fromGit(source, content, embeddingModel, ref);
|
|
244
|
+
return MiruIndex.fromGit(source, content, embeddingModel, ref, dependencies);
|
|
217
245
|
}
|
|
218
|
-
return MiruIndex.fromPath(source, content, embeddingModel);
|
|
246
|
+
return MiruIndex.fromPath(source, content, embeddingModel, dependencies);
|
|
219
247
|
}
|
|
220
248
|
|
|
221
249
|
/**
|
|
@@ -229,7 +257,9 @@ export class MiruIndex {
|
|
|
229
257
|
path: string,
|
|
230
258
|
content: ContentType[] = defaultContentTypes(),
|
|
231
259
|
embeddingModel?: string,
|
|
260
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
232
261
|
): Promise<MiruIndex> {
|
|
262
|
+
const deps = factoryDependencies(dependencies);
|
|
233
263
|
const resolved = resolve(path);
|
|
234
264
|
try {
|
|
235
265
|
const st = await Bun.file(resolved).stat();
|
|
@@ -243,14 +273,14 @@ export class MiruIndex {
|
|
|
243
273
|
throw new Error(`Path does not exist: ${path}`);
|
|
244
274
|
}
|
|
245
275
|
|
|
246
|
-
const model = embeddingModel ?? resolveEmbeddingModel();
|
|
247
|
-
const cached = await getValidatedCache(resolved, model, content);
|
|
276
|
+
const model = embeddingModel ?? deps.resolveEmbeddingModel();
|
|
277
|
+
const cached = await deps.getValidatedCache(resolved, model, content);
|
|
248
278
|
if (cached) {
|
|
249
|
-
return MiruIndex.loadFromDisk(cached, model);
|
|
279
|
+
return MiruIndex.loadFromDisk(cached, model, deps);
|
|
250
280
|
}
|
|
251
281
|
|
|
252
|
-
const embeddings = getEmbeddingBackend(model);
|
|
253
|
-
const { bm25, semantic, chunks } = await createIndexFromPath(
|
|
282
|
+
const embeddings = deps.getEmbeddingBackend(model);
|
|
283
|
+
const { bm25, semantic, chunks } = await deps.createIndexFromPath(
|
|
254
284
|
resolved,
|
|
255
285
|
embeddings,
|
|
256
286
|
content,
|
|
@@ -274,15 +304,20 @@ export class MiruIndex {
|
|
|
274
304
|
* Validates embedding model compatibility via cache metadata. Sets
|
|
275
305
|
* `loadedFromDisk` so callers can avoid redundant cache writes.
|
|
276
306
|
*/
|
|
277
|
-
static async loadFromDisk(
|
|
278
|
-
|
|
279
|
-
|
|
307
|
+
static async loadFromDisk(
|
|
308
|
+
path: string,
|
|
309
|
+
embeddingModel?: string,
|
|
310
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
311
|
+
): Promise<MiruIndex> {
|
|
312
|
+
const deps = factoryDependencies(dependencies);
|
|
313
|
+
const model = embeddingModel ?? deps.resolveEmbeddingModel();
|
|
314
|
+
const { bm25, semantic, chunks, metadata } = await deps.loadCachedIndex(path);
|
|
280
315
|
const content = (metadata.content_type as ContentType[]) ?? defaultContentTypes();
|
|
281
316
|
const root = metadata.root_path ? String(metadata.root_path) : null;
|
|
282
317
|
const storedFileMtimes = (metadata.file_mtimes as Record<string, number>) ?? {};
|
|
283
318
|
|
|
284
319
|
return new MiruIndex({
|
|
285
|
-
embeddings: getEmbeddingBackend(model),
|
|
320
|
+
embeddings: deps.getEmbeddingBackend(model),
|
|
286
321
|
bm25Index: bm25,
|
|
287
322
|
semanticIndex: semantic,
|
|
288
323
|
chunks,
|
|
@@ -387,6 +422,7 @@ export class MiruIndex {
|
|
|
387
422
|
embeddings: this.embeddings,
|
|
388
423
|
chunks: this.chunksInternal,
|
|
389
424
|
semanticIndex: this.semanticIndex,
|
|
425
|
+
bm25: this.bm25Index,
|
|
390
426
|
relativePaths,
|
|
391
427
|
});
|
|
392
428
|
|
package/src/prompt.ts
CHANGED
|
@@ -62,22 +62,49 @@ export function applyHiddenPromptChar(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Visible-input prompt (not for secrets — use promptHidden for those). */
|
|
65
|
-
export async function promptText(
|
|
66
|
-
|
|
65
|
+
export async function promptText(
|
|
66
|
+
message: string,
|
|
67
|
+
defaultValue = "",
|
|
68
|
+
createInterface: typeof createPromptInterface = createPromptInterface,
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const rl = createInterface({ input, output });
|
|
71
|
+
return promptTextWith(
|
|
72
|
+
message,
|
|
73
|
+
defaultValue,
|
|
74
|
+
(question) => rl.question(question),
|
|
75
|
+
() => rl.close(),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createPromptInterface(
|
|
80
|
+
options: { input: typeof input; output: typeof output },
|
|
81
|
+
factory: typeof readline.createInterface = readline.createInterface,
|
|
82
|
+
): Pick<readline.Interface, "question" | "close"> {
|
|
83
|
+
return factory(options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function promptTextWith(
|
|
87
|
+
message: string,
|
|
88
|
+
defaultValue: string,
|
|
89
|
+
ask: (question: string) => Promise<string>,
|
|
90
|
+
close: () => void = () => {},
|
|
91
|
+
): Promise<string> {
|
|
67
92
|
try {
|
|
68
93
|
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
69
|
-
const answer = (await
|
|
94
|
+
const answer = (await ask(`${message}${suffix}: `)).trim();
|
|
70
95
|
return answer || defaultValue;
|
|
71
96
|
} finally {
|
|
72
|
-
|
|
97
|
+
close();
|
|
73
98
|
}
|
|
74
99
|
}
|
|
75
100
|
|
|
76
101
|
export async function promptHidden(
|
|
77
102
|
message: string,
|
|
78
|
-
stream:
|
|
103
|
+
stream: { write(text: string): unknown } = output,
|
|
104
|
+
io: { input?: HiddenPromptInput; exit?: (code: number) => void } = {},
|
|
79
105
|
): Promise<string> {
|
|
80
|
-
|
|
106
|
+
const inputStream = io.input ?? input;
|
|
107
|
+
if (!inputStream.isTTY) {
|
|
81
108
|
throw new Error(
|
|
82
109
|
"Cannot prompt for API key: stdin is not a TTY. Run `miru setup --key YOUR_KEY` or set TAKARA_API_KEY.",
|
|
83
110
|
);
|
|
@@ -85,9 +112,9 @@ export async function promptHidden(
|
|
|
85
112
|
|
|
86
113
|
return new Promise((resolve, reject) => {
|
|
87
114
|
stream.write(message);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
115
|
+
inputStream.setRawMode?.(true);
|
|
116
|
+
inputStream.resume();
|
|
117
|
+
inputStream.setEncoding("utf8");
|
|
91
118
|
|
|
92
119
|
let state = createHiddenPromptState();
|
|
93
120
|
|
|
@@ -100,7 +127,7 @@ export async function promptHidden(
|
|
|
100
127
|
cleanup();
|
|
101
128
|
stream.write("\n");
|
|
102
129
|
reject(new Error("Setup cancelled."));
|
|
103
|
-
process.exit(130);
|
|
130
|
+
(io.exit ?? process.exit)(130);
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
if (result.submit) {
|
|
@@ -117,11 +144,21 @@ export async function promptHidden(
|
|
|
117
144
|
};
|
|
118
145
|
|
|
119
146
|
const cleanup = () => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
inputStream.setRawMode?.(false);
|
|
148
|
+
inputStream.pause();
|
|
149
|
+
inputStream.removeListener("data", onData);
|
|
123
150
|
};
|
|
124
151
|
|
|
125
|
-
|
|
152
|
+
inputStream.on("data", onData);
|
|
126
153
|
});
|
|
127
154
|
}
|
|
155
|
+
|
|
156
|
+
interface HiddenPromptInput {
|
|
157
|
+
isTTY?: boolean;
|
|
158
|
+
setRawMode?: (mode: boolean) => void;
|
|
159
|
+
resume(): void;
|
|
160
|
+
pause(): void;
|
|
161
|
+
setEncoding(encoding: "utf8"): unknown;
|
|
162
|
+
on(event: "data", listener: (chunk: string) => void): unknown;
|
|
163
|
+
removeListener(event: "data", listener: (chunk: string) => void): unknown;
|
|
164
|
+
}
|
package/src/ranking/boosting.ts
CHANGED
|
@@ -209,12 +209,11 @@ function boostSymbolDefinitions(
|
|
|
209
209
|
const stem = basename(chunk.file_path)
|
|
210
210
|
.replace(/\.[^.]+$/, "")
|
|
211
211
|
.toLowerCase();
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
boosted.set(key, tier);
|
|
212
|
+
if ([...names].some((n) => stemMatches(stem, n.toLowerCase()))) {
|
|
213
|
+
const tier = definitionTier(chunk, names, boostUnit);
|
|
214
|
+
if (tier) {
|
|
215
|
+
boosted.set(key, tier);
|
|
216
|
+
}
|
|
218
217
|
}
|
|
219
218
|
}
|
|
220
219
|
}
|
package/src/ranking/location.ts
CHANGED
|
@@ -222,16 +222,12 @@ export function boostExactStemMatches(
|
|
|
222
222
|
if (GENERIC_STEM_DENY.has(keyword)) {
|
|
223
223
|
continue;
|
|
224
224
|
}
|
|
225
|
-
if (preferImplementation && stem === keyword && !IMPLEMENTATION_HINTS.has(stem)) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
const parts = keyword.split("-").filter((p) => p.length > 2);
|
|
234
|
-
if (parts.length > 1 && parts.some((p) => p === stem)) {
|
|
225
|
+
if (!(preferImplementation && stem === keyword && !IMPLEMENTATION_HINTS.has(stem))) {
|
|
226
|
+
const keywordNorm = keyword.replace(/[-_]/g, "");
|
|
227
|
+
if (stem === keyword || stemNorm === keywordNorm) {
|
|
228
|
+
boosted.set(key, (boosted.get(key) ?? 0) + boost);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
235
231
|
}
|
|
236
232
|
}
|
|
237
233
|
}
|
package/src/ranking/penalties.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function rerankTopk(
|
|
|
72
72
|
const ranked = [...penalised.entries()].sort((a, b) => b[1] - a[1]);
|
|
73
73
|
|
|
74
74
|
const fileSelected = new Map<string, number>();
|
|
75
|
-
const selected: { score: number; key: string }[] = [];
|
|
75
|
+
const selected: { score: number; key: string; chunk: Chunk }[] = [];
|
|
76
76
|
let minSelected = Infinity;
|
|
77
77
|
|
|
78
78
|
for (const [key, penScore] of ranked) {
|
|
@@ -81,31 +81,23 @@ export function rerankTopk(
|
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
if (selected.length
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const excess = already - FILE_SATURATION_THRESHOLD + 1;
|
|
92
|
-
effScore *= tuning.fileSaturationDecay ** excess;
|
|
93
|
-
}
|
|
84
|
+
if (selected.length < topK || penScore > minSelected) {
|
|
85
|
+
const already = fileSelected.get(chunk.file_path) ?? 0;
|
|
86
|
+
let effScore = penScore;
|
|
87
|
+
if (already >= FILE_SATURATION_THRESHOLD) {
|
|
88
|
+
const excess = already - FILE_SATURATION_THRESHOLD + 1;
|
|
89
|
+
effScore *= tuning.fileSaturationDecay ** excess;
|
|
90
|
+
}
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
selected.push({ score: effScore, key, chunk });
|
|
93
|
+
fileSelected.set(chunk.file_path, already + 1);
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
if (selected.length >= topK) {
|
|
96
|
+
minSelected = Math.min(...selected.map((s) => s.score));
|
|
97
|
+
}
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
selected.sort((a, b) => b.score - a.score);
|
|
104
|
-
return selected.slice(0, topK).
|
|
105
|
-
const chunk = chunksByKey.get(key);
|
|
106
|
-
if (!chunk) {
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
109
|
-
return [[chunk, score] as [Chunk, number]];
|
|
110
|
-
});
|
|
102
|
+
return selected.slice(0, topK).map(({ chunk, score }) => [chunk, score]);
|
|
111
103
|
}
|
package/src/search.ts
CHANGED
|
@@ -137,11 +137,9 @@ export async function hybridSearch(options: {
|
|
|
137
137
|
|
|
138
138
|
const allKeys = new Set([...normalizedSemantic.keys(), ...normalizedBm25.keys()]);
|
|
139
139
|
const sortedKeys = [...allKeys].sort((a, b) => {
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
return 0;
|
|
144
|
-
}
|
|
140
|
+
// Every score key was produced from a chunk in this query.
|
|
141
|
+
const ca = chunksByKey.get(a) as Chunk;
|
|
142
|
+
const cb = chunksByKey.get(b) as Chunk;
|
|
145
143
|
return ca.start_line - cb.start_line;
|
|
146
144
|
});
|
|
147
145
|
|
|
@@ -164,13 +162,7 @@ export async function hybridSearch(options: {
|
|
|
164
162
|
return [...combinedScores.entries()]
|
|
165
163
|
.sort((a, b) => b[1] - a[1])
|
|
166
164
|
.slice(0, topK)
|
|
167
|
-
.
|
|
168
|
-
const chunk = chunksByKey.get(key);
|
|
169
|
-
if (!chunk) {
|
|
170
|
-
return [];
|
|
171
|
-
}
|
|
172
|
-
return [{ chunk, score }];
|
|
173
|
-
});
|
|
165
|
+
.map(([key, score]) => ({ chunk: chunksByKey.get(key) as Chunk, score }));
|
|
174
166
|
}
|
|
175
167
|
|
|
176
168
|
export async function searchSemanticOnly(options: {
|