@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/miru-index.ts
CHANGED
|
@@ -14,13 +14,43 @@ import { createIndexFromPath } from "./index/create.ts";
|
|
|
14
14
|
import { applyIncrementalFileChanges, normalizeRelativePath } from "./index/incremental.ts";
|
|
15
15
|
import { persistencePaths, saveIndexBundle } from "./index/persistence.ts";
|
|
16
16
|
import type { SemanticIndex } from "./index/semantic-index.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
filterChunksByGlob,
|
|
19
|
+
type LiteralLocateOptions,
|
|
20
|
+
type LiteralLocateResult,
|
|
21
|
+
locateLiteral,
|
|
22
|
+
} from "./literal.ts";
|
|
18
23
|
import { hybridSearch, searchSemanticOnly } from "./search.ts";
|
|
19
24
|
import type { Chunk, ContentType, SearchResult } from "./types.ts";
|
|
20
25
|
import { chunkKey, chunkToDict, defaultContentTypes } from "./types.ts";
|
|
21
26
|
import { computeSourceCacheKey, isGitUrl } from "./utils.ts";
|
|
22
27
|
import { indexCacheEpoch } from "./version.ts";
|
|
23
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
|
+
|
|
24
54
|
/**
|
|
25
55
|
* In-memory search index over a codebase: BM25 keyword scores plus semantic
|
|
26
56
|
* embeddings, with optional disk cache for reuse across runs.
|
|
@@ -166,18 +196,20 @@ export class MiruIndex {
|
|
|
166
196
|
content: ContentType[] = defaultContentTypes(),
|
|
167
197
|
embeddingModel?: string,
|
|
168
198
|
ref?: string | null,
|
|
199
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
169
200
|
): Promise<MiruIndex> {
|
|
201
|
+
const deps = factoryDependencies(dependencies);
|
|
170
202
|
const cacheKey = computeSourceCacheKey(url, ref);
|
|
171
|
-
const model = embeddingModel ?? resolveEmbeddingModel();
|
|
172
|
-
const cached = await getValidatedCache(cacheKey, model, content);
|
|
203
|
+
const model = embeddingModel ?? deps.resolveEmbeddingModel();
|
|
204
|
+
const cached = await deps.getValidatedCache(cacheKey, model, content);
|
|
173
205
|
if (cached) {
|
|
174
|
-
return MiruIndex.loadFromDisk(cached, model);
|
|
206
|
+
return MiruIndex.loadFromDisk(cached, model, deps);
|
|
175
207
|
}
|
|
176
208
|
|
|
177
|
-
const cloneDir = await cloneGitRepository(url, ref);
|
|
209
|
+
const cloneDir = await deps.cloneGitRepository(url, ref);
|
|
178
210
|
try {
|
|
179
|
-
const embeddings = getEmbeddingBackend(model);
|
|
180
|
-
const { bm25, semantic, chunks } = await createIndexFromPath(
|
|
211
|
+
const embeddings = deps.getEmbeddingBackend(model);
|
|
212
|
+
const { bm25, semantic, chunks } = await deps.createIndexFromPath(
|
|
181
213
|
cloneDir,
|
|
182
214
|
embeddings,
|
|
183
215
|
content,
|
|
@@ -193,7 +225,7 @@ export class MiruIndex {
|
|
|
193
225
|
root: null,
|
|
194
226
|
content,
|
|
195
227
|
});
|
|
196
|
-
await index.
|
|
228
|
+
await deps.saveBuiltIndex(index, deps.findIndexCachePath(cacheKey));
|
|
197
229
|
return index;
|
|
198
230
|
} finally {
|
|
199
231
|
await rm(cloneDir, { recursive: true, force: true });
|
|
@@ -206,11 +238,12 @@ export class MiruIndex {
|
|
|
206
238
|
content: ContentType[] = defaultContentTypes(),
|
|
207
239
|
embeddingModel?: string,
|
|
208
240
|
ref?: string | null,
|
|
241
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
209
242
|
): Promise<MiruIndex> {
|
|
210
243
|
if (isGitUrl(source)) {
|
|
211
|
-
return MiruIndex.fromGit(source, content, embeddingModel, ref);
|
|
244
|
+
return MiruIndex.fromGit(source, content, embeddingModel, ref, dependencies);
|
|
212
245
|
}
|
|
213
|
-
return MiruIndex.fromPath(source, content, embeddingModel);
|
|
246
|
+
return MiruIndex.fromPath(source, content, embeddingModel, dependencies);
|
|
214
247
|
}
|
|
215
248
|
|
|
216
249
|
/**
|
|
@@ -224,7 +257,9 @@ export class MiruIndex {
|
|
|
224
257
|
path: string,
|
|
225
258
|
content: ContentType[] = defaultContentTypes(),
|
|
226
259
|
embeddingModel?: string,
|
|
260
|
+
dependencies?: Partial<FactoryDependencies>,
|
|
227
261
|
): Promise<MiruIndex> {
|
|
262
|
+
const deps = factoryDependencies(dependencies);
|
|
228
263
|
const resolved = resolve(path);
|
|
229
264
|
try {
|
|
230
265
|
const st = await Bun.file(resolved).stat();
|
|
@@ -238,14 +273,14 @@ export class MiruIndex {
|
|
|
238
273
|
throw new Error(`Path does not exist: ${path}`);
|
|
239
274
|
}
|
|
240
275
|
|
|
241
|
-
const model = embeddingModel ?? resolveEmbeddingModel();
|
|
242
|
-
const cached = await getValidatedCache(resolved, model, content);
|
|
276
|
+
const model = embeddingModel ?? deps.resolveEmbeddingModel();
|
|
277
|
+
const cached = await deps.getValidatedCache(resolved, model, content);
|
|
243
278
|
if (cached) {
|
|
244
|
-
return MiruIndex.loadFromDisk(cached, model);
|
|
279
|
+
return MiruIndex.loadFromDisk(cached, model, deps);
|
|
245
280
|
}
|
|
246
281
|
|
|
247
|
-
const embeddings = getEmbeddingBackend(model);
|
|
248
|
-
const { bm25, semantic, chunks } = await createIndexFromPath(
|
|
282
|
+
const embeddings = deps.getEmbeddingBackend(model);
|
|
283
|
+
const { bm25, semantic, chunks } = await deps.createIndexFromPath(
|
|
249
284
|
resolved,
|
|
250
285
|
embeddings,
|
|
251
286
|
content,
|
|
@@ -269,15 +304,20 @@ export class MiruIndex {
|
|
|
269
304
|
* Validates embedding model compatibility via cache metadata. Sets
|
|
270
305
|
* `loadedFromDisk` so callers can avoid redundant cache writes.
|
|
271
306
|
*/
|
|
272
|
-
static async loadFromDisk(
|
|
273
|
-
|
|
274
|
-
|
|
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);
|
|
275
315
|
const content = (metadata.content_type as ContentType[]) ?? defaultContentTypes();
|
|
276
316
|
const root = metadata.root_path ? String(metadata.root_path) : null;
|
|
277
317
|
const storedFileMtimes = (metadata.file_mtimes as Record<string, number>) ?? {};
|
|
278
318
|
|
|
279
319
|
return new MiruIndex({
|
|
280
|
-
embeddings: getEmbeddingBackend(model),
|
|
320
|
+
embeddings: deps.getEmbeddingBackend(model),
|
|
281
321
|
bm25Index: bm25,
|
|
282
322
|
semanticIndex: semantic,
|
|
283
323
|
chunks,
|
|
@@ -382,6 +422,7 @@ export class MiruIndex {
|
|
|
382
422
|
embeddings: this.embeddings,
|
|
383
423
|
chunks: this.chunksInternal,
|
|
384
424
|
semanticIndex: this.semanticIndex,
|
|
425
|
+
bm25: this.bm25Index,
|
|
385
426
|
relativePaths,
|
|
386
427
|
});
|
|
387
428
|
|
|
@@ -407,11 +448,19 @@ export class MiruIndex {
|
|
|
407
448
|
);
|
|
408
449
|
}
|
|
409
450
|
|
|
451
|
+
private getPathSelector(include?: string[], exclude?: string[]): readonly number[] | undefined {
|
|
452
|
+
if (!include?.length && !exclude?.length) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
const included = new Set(filterChunksByGlob(this.chunks, include, exclude).map(chunkKey));
|
|
456
|
+
return this.chunks.flatMap((chunk, index) => (included.has(chunkKey(chunk)) ? [index] : []));
|
|
457
|
+
}
|
|
458
|
+
|
|
410
459
|
/**
|
|
411
460
|
* Hybrid BM25 + semantic search over indexed chunks.
|
|
412
461
|
*
|
|
413
462
|
* `alpha` blends keyword vs vector scores (default depends on content type).
|
|
414
|
-
* `
|
|
463
|
+
* `include` / `exclude` glob patterns restrict candidate files before ranking.
|
|
415
464
|
* `rerank` defaults to on for code indexes (cross-encoder style reranking).
|
|
416
465
|
*/
|
|
417
466
|
async search(options: {
|
|
@@ -422,15 +471,34 @@ export class MiruIndex {
|
|
|
422
471
|
alpha?: number | null;
|
|
423
472
|
filterLanguages?: string[];
|
|
424
473
|
filterPaths?: string[];
|
|
474
|
+
include?: string[];
|
|
475
|
+
exclude?: string[];
|
|
425
476
|
rerank?: boolean;
|
|
426
477
|
}): Promise<SearchResult[]> {
|
|
427
|
-
const {
|
|
478
|
+
const {
|
|
479
|
+
query,
|
|
480
|
+
queryVector,
|
|
481
|
+
topK = 10,
|
|
482
|
+
alpha,
|
|
483
|
+
filterLanguages,
|
|
484
|
+
filterPaths,
|
|
485
|
+
include,
|
|
486
|
+
exclude,
|
|
487
|
+
rerank,
|
|
488
|
+
} = options;
|
|
428
489
|
|
|
429
490
|
if (!this.chunks.length || !query.trim()) {
|
|
430
491
|
return [];
|
|
431
492
|
}
|
|
432
493
|
|
|
433
494
|
const resolvedRerank = rerank ?? this.content.includes("code");
|
|
495
|
+
const selector = this.getSelector(filterLanguages, filterPaths);
|
|
496
|
+
const pathSelector = this.getPathSelector(include, exclude);
|
|
497
|
+
let effectiveSelector = pathSelector ?? selector;
|
|
498
|
+
if (selector && pathSelector) {
|
|
499
|
+
const allowedIndices = new Set(selector);
|
|
500
|
+
effectiveSelector = pathSelector.filter((index) => allowedIndices.has(index));
|
|
501
|
+
}
|
|
434
502
|
|
|
435
503
|
return hybridSearch({
|
|
436
504
|
query,
|
|
@@ -441,7 +509,7 @@ export class MiruIndex {
|
|
|
441
509
|
chunks: this.chunks,
|
|
442
510
|
topK,
|
|
443
511
|
alpha,
|
|
444
|
-
selector:
|
|
512
|
+
selector: effectiveSelector,
|
|
445
513
|
rerank: resolvedRerank,
|
|
446
514
|
});
|
|
447
515
|
}
|
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
|
@@ -96,6 +96,10 @@ export async function hybridSearch(options: {
|
|
|
96
96
|
rerank = true,
|
|
97
97
|
} = options;
|
|
98
98
|
|
|
99
|
+
if (selector?.length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
99
103
|
const alphaWeight = resolveAlpha(query, alpha);
|
|
100
104
|
const tuning = rankingConfig();
|
|
101
105
|
const candidateCount =
|
|
@@ -133,11 +137,9 @@ export async function hybridSearch(options: {
|
|
|
133
137
|
|
|
134
138
|
const allKeys = new Set([...normalizedSemantic.keys(), ...normalizedBm25.keys()]);
|
|
135
139
|
const sortedKeys = [...allKeys].sort((a, b) => {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
return 0;
|
|
140
|
-
}
|
|
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;
|
|
141
143
|
return ca.start_line - cb.start_line;
|
|
142
144
|
});
|
|
143
145
|
|
|
@@ -160,13 +162,7 @@ export async function hybridSearch(options: {
|
|
|
160
162
|
return [...combinedScores.entries()]
|
|
161
163
|
.sort((a, b) => b[1] - a[1])
|
|
162
164
|
.slice(0, topK)
|
|
163
|
-
.
|
|
164
|
-
const chunk = chunksByKey.get(key);
|
|
165
|
-
if (!chunk) {
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
return [{ chunk, score }];
|
|
169
|
-
});
|
|
165
|
+
.map(([key, score]) => ({ chunk: chunksByKey.get(key) as Chunk, score }));
|
|
170
166
|
}
|
|
171
167
|
|
|
172
168
|
export async function searchSemanticOnly(options: {
|
package/src/setup.ts
CHANGED
|
@@ -123,25 +123,27 @@ export function parseSetupCliArgs(rest: string[]): {
|
|
|
123
123
|
return { args };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async function promptSageMakerArn(): Promise<string> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
async function promptSageMakerArn(promptTextImpl: typeof promptText = promptText): Promise<string> {
|
|
127
|
+
let arn = "";
|
|
128
|
+
while (!arn) {
|
|
129
|
+
const candidate = await promptTextImpl("SageMaker endpoint ARN");
|
|
130
|
+
if (!candidate) {
|
|
130
131
|
fail("Endpoint ARN cannot be empty.");
|
|
131
132
|
continue;
|
|
132
133
|
}
|
|
133
134
|
try {
|
|
134
|
-
parseSageMakerEndpointArn(
|
|
135
|
-
|
|
135
|
+
parseSageMakerEndpointArn(candidate);
|
|
136
|
+
arn = candidate;
|
|
136
137
|
} catch (err) {
|
|
137
138
|
fail(err instanceof Error ? err.message : String(err));
|
|
138
139
|
}
|
|
139
140
|
}
|
|
141
|
+
return arn;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
async function promptAwsProfile(): Promise<string> {
|
|
144
|
+
async function promptAwsProfile(promptTextImpl: typeof promptText = promptText): Promise<string> {
|
|
143
145
|
while (true) {
|
|
144
|
-
const profile = await
|
|
146
|
+
const profile = await promptTextImpl(
|
|
145
147
|
"AWS profile name (must already exist in ~/.aws)",
|
|
146
148
|
process.env.AWS_PROFILE || "miru",
|
|
147
149
|
);
|
|
@@ -152,11 +154,20 @@ async function promptAwsProfile(): Promise<string> {
|
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
export interface SageMakerSetupDependencies {
|
|
158
|
+
promptText?: typeof promptText;
|
|
159
|
+
canPromptForCredentials?: typeof canPromptForCredentials;
|
|
160
|
+
validateConnection?: typeof validateSageMakerConnection;
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
/**
|
|
156
164
|
* Miru only ever inherits AWS credentials — it never creates, writes, or rotates an
|
|
157
165
|
* AWS profile. Set one up yourself first (e.g. `aws configure --profile miru`).
|
|
158
166
|
*/
|
|
159
|
-
export async function runSageMakerSetup(
|
|
167
|
+
export async function runSageMakerSetup(
|
|
168
|
+
options: RunSetupOptions = {},
|
|
169
|
+
dependencies: SageMakerSetupDependencies = {},
|
|
170
|
+
): Promise<RunSetupResult> {
|
|
160
171
|
writeStdout("");
|
|
161
172
|
printBrandBanner(process.stderr);
|
|
162
173
|
divider("─", 48, process.stderr);
|
|
@@ -168,18 +179,19 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
|
|
|
168
179
|
);
|
|
169
180
|
writeStdout("");
|
|
170
181
|
|
|
171
|
-
const
|
|
182
|
+
const promptTextImpl = dependencies.promptText ?? promptText;
|
|
183
|
+
const arnInput = options.sagemakerArn ?? (await promptSageMakerArn(promptTextImpl));
|
|
172
184
|
const parsed = parseSageMakerEndpointArn(arnInput);
|
|
173
185
|
|
|
174
186
|
let profile = options.profile;
|
|
175
187
|
if (!profile) {
|
|
176
|
-
if (!canPromptForCredentials()) {
|
|
188
|
+
if (!(dependencies.canPromptForCredentials ?? canPromptForCredentials)()) {
|
|
177
189
|
throw new Error(
|
|
178
190
|
"An AWS profile name is required. Pass --profile <name>, or run " +
|
|
179
191
|
"`miru setup --sagemaker` interactively.",
|
|
180
192
|
);
|
|
181
193
|
}
|
|
182
|
-
profile = await promptAwsProfile();
|
|
194
|
+
profile = await promptAwsProfile(promptTextImpl);
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
await beginModeSwitch("sagemaker");
|
|
@@ -197,7 +209,7 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
|
|
|
197
209
|
truncate: true,
|
|
198
210
|
truncationDirection: "Right",
|
|
199
211
|
};
|
|
200
|
-
const result = await validateSageMakerConnection(config);
|
|
212
|
+
const result = await (dependencies.validateConnection ?? validateSageMakerConnection)(config);
|
|
201
213
|
if (!result.valid) {
|
|
202
214
|
spinner.stop();
|
|
203
215
|
throw new Error(result.message);
|
package/src/snippet.ts
CHANGED
|
@@ -79,7 +79,7 @@ const RUBY_OR_ELIXIR_BODY_RE = /^\s*def\w*\b|^\s*defmodule\b/;
|
|
|
79
79
|
const MAX_DECLARATION_LOOKBACK = 40;
|
|
80
80
|
const INDENTED_BLOCK_LANGUAGES = new Set(["python", "haskell", "ocaml"]);
|
|
81
81
|
const END_BLOCK_LANGUAGES = new Set(["ruby", "elixir"]);
|
|
82
|
-
const TAG_LANGUAGES = new Set(["html", "embeddedtemplate", "xml", "vue", "svelte"]);
|
|
82
|
+
const TAG_LANGUAGES = new Set(["html", "embeddedtemplate", "xml", "vue", "svelte", "astro"]);
|
|
83
83
|
const BRACE_BLOCK_LANGUAGES = new Set([
|
|
84
84
|
"bash",
|
|
85
85
|
"c",
|
|
@@ -192,16 +192,14 @@ function completeHtmlTag(
|
|
|
192
192
|
for (let i = Math.max(0, start - MAX_DECLARATION_LOOKBACK); i <= anchor; i++) {
|
|
193
193
|
const line = lines[i] ?? "";
|
|
194
194
|
for (const match of line.matchAll(HTML_TAG_RE)) {
|
|
195
|
-
const name = match[1]
|
|
196
|
-
if (!name) {
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
195
|
+
const name = (match[1] as string).toLowerCase();
|
|
199
196
|
if (match[0].startsWith("</")) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
197
|
+
const matchingIndex = openTags.reduce(
|
|
198
|
+
(last, tag, index) => (tag.name === name ? index : last),
|
|
199
|
+
-1,
|
|
200
|
+
);
|
|
201
|
+
if (matchingIndex >= 0) {
|
|
202
|
+
openTags.splice(matchingIndex, 1);
|
|
205
203
|
}
|
|
206
204
|
} else if (!match[0].endsWith("/>") && !HTML_VOID_TAGS.has(name)) {
|
|
207
205
|
openTags.push({ name, start: i });
|
|
@@ -374,10 +372,6 @@ function compactSupportingSnippet(snippet: SnippetResult, maxLines: number): Sni
|
|
|
374
372
|
/** Pick the 0-based line index inside `content` that best matches the query. */
|
|
375
373
|
export function anchorLineOffset(content: string, query: string): number {
|
|
376
374
|
const lines = content.split("\n");
|
|
377
|
-
if (lines.length === 0) {
|
|
378
|
-
return 0;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
375
|
const terms = queryMatchTerms(query);
|
|
382
376
|
let bestIndex = Math.floor(lines.length / 2);
|
|
383
377
|
let bestScore = -1;
|
|
@@ -401,18 +395,6 @@ export function trimChunkToSnippet(
|
|
|
401
395
|
anchorLine?: number,
|
|
402
396
|
): SnippetResult {
|
|
403
397
|
const lines = chunk.content.split("\n");
|
|
404
|
-
if (lines.length === 0) {
|
|
405
|
-
return {
|
|
406
|
-
chunk,
|
|
407
|
-
meta: {
|
|
408
|
-
truncated: false,
|
|
409
|
-
anchor_line: chunk.start_line,
|
|
410
|
-
full_start_line: chunk.start_line,
|
|
411
|
-
full_end_line: chunk.end_line,
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
398
|
const queryAnchorOffset = anchorLineOffset(chunk.content, query);
|
|
417
399
|
const anchorOffset =
|
|
418
400
|
anchorLine == null
|