@takara-ai/miru-code 1.1.2 → 1.2.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 +178 -107
- package/package.json +9 -5
- package/src/benchmark/compare.ts +278 -0
- package/src/benchmark/grep.ts +352 -0
- package/src/benchmark/history.ts +206 -0
- package/src/benchmark/locate-compare.ts +65 -0
- package/src/benchmark/rg-literal.ts +143 -0
- package/src/benchmark/summary.ts +29 -0
- package/src/benchmark/types.ts +65 -0
- package/src/cli.ts +185 -4
- package/src/credentials.ts +8 -0
- package/src/help.ts +45 -2
- package/src/index/create.ts +2 -2
- package/src/index/incremental.ts +4 -3
- package/src/index.ts +3 -1
- package/src/installer/benchmark-mode.ts +248 -0
- package/src/installer/config.ts +14 -2
- package/src/installer/hooks/opencode-plugin.ts +6 -20
- package/src/installer/hooks/search-guard.ts +4 -5
- package/src/installer/installer.ts +59 -6
- package/src/installer/search-policy.ts +28 -9
- package/src/literal.ts +139 -0
- package/src/mcp/index-cache.ts +2 -2
- package/src/mcp/serve.ts +4 -3
- package/src/mcp/server.ts +166 -9
- package/src/miru-index.ts +15 -6
- package/src/snippet.ts +6 -3
- package/src/token-count.ts +62 -0
- package/src/tokenizer/bert-wordpiece.ts +219 -0
- package/src/tokenizer/load.ts +14 -0
- package/src/types.ts +8 -0
- package/src/utils.ts +8 -1
- package/tokenizer/tokenizer.json +1 -0
package/src/literal.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** Exact literal location over already-indexed Miru chunks (no disk re-walk). */
|
|
2
|
+
|
|
3
|
+
import type { Chunk } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export type LiteralMode = "count" | "locations" | "lines";
|
|
6
|
+
|
|
7
|
+
export interface LiteralHit {
|
|
8
|
+
file_path: string;
|
|
9
|
+
line: number;
|
|
10
|
+
/** Present only in `lines` mode. */
|
|
11
|
+
text?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LiteralLocateResult {
|
|
15
|
+
literal: string;
|
|
16
|
+
mode: LiteralMode;
|
|
17
|
+
/** Total unique file:line matches. */
|
|
18
|
+
n: number;
|
|
19
|
+
/** Unique files with at least one match. */
|
|
20
|
+
files: number;
|
|
21
|
+
/** True only when an explicit `limit` cut the hit list short. */
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
hits: LiteralHit[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LiteralLocateOptions {
|
|
27
|
+
mode?: LiteralMode;
|
|
28
|
+
/**
|
|
29
|
+
* Optional cap on returned hits (ignored for `count`).
|
|
30
|
+
* Omit for all matches — agents must not fall back to Grep when truncated.
|
|
31
|
+
*/
|
|
32
|
+
limit?: number;
|
|
33
|
+
ignore_case?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveLimit(limit: number | undefined): number | null {
|
|
37
|
+
if (limit == null || !Number.isFinite(limit)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return Math.max(1, Math.floor(limit));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function includesLiteral(haystack: string, needle: string, ignoreCase: boolean): boolean {
|
|
44
|
+
if (!ignoreCase) {
|
|
45
|
+
return haystack.includes(needle);
|
|
46
|
+
}
|
|
47
|
+
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scan indexed chunks for an exact substring. Dedupes by file:line when chunks overlap.
|
|
52
|
+
* Only covers indexed content (same corpus as `search` / `expand`).
|
|
53
|
+
* Returns every match unless `limit` is set.
|
|
54
|
+
*/
|
|
55
|
+
export function locateLiteral(
|
|
56
|
+
chunks: readonly Chunk[],
|
|
57
|
+
literal: string,
|
|
58
|
+
options: LiteralLocateOptions = {},
|
|
59
|
+
): LiteralLocateResult {
|
|
60
|
+
const needle = literal;
|
|
61
|
+
const mode = options.mode ?? "lines";
|
|
62
|
+
const ignoreCase = options.ignore_case === true;
|
|
63
|
+
const limit = resolveLimit(options.limit);
|
|
64
|
+
|
|
65
|
+
if (!needle) {
|
|
66
|
+
return { literal: needle, mode, n: 0, files: 0, truncated: false, hits: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
const fileSet = new Set<string>();
|
|
71
|
+
const hits: LiteralHit[] = [];
|
|
72
|
+
let n = 0;
|
|
73
|
+
|
|
74
|
+
for (const chunk of chunks) {
|
|
75
|
+
if (!includesLiteral(chunk.content, needle, ignoreCase)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const lines = chunk.content.split("\n");
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const lineText = lines[i] ?? "";
|
|
81
|
+
if (!includesLiteral(lineText, needle, ignoreCase)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const line = chunk.start_line + i;
|
|
85
|
+
const key = `${chunk.file_path}:${line}`;
|
|
86
|
+
if (seen.has(key)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(key);
|
|
90
|
+
fileSet.add(chunk.file_path);
|
|
91
|
+
n += 1;
|
|
92
|
+
if (mode === "count") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (limit != null && hits.length >= limit) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const hit: LiteralHit = { file_path: chunk.file_path, line };
|
|
99
|
+
if (mode === "lines") {
|
|
100
|
+
hit.text = lineText;
|
|
101
|
+
}
|
|
102
|
+
hits.push(hit);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
literal: needle,
|
|
108
|
+
mode,
|
|
109
|
+
n,
|
|
110
|
+
files: fileSet.size,
|
|
111
|
+
truncated: mode !== "count" && limit != null && n > hits.length,
|
|
112
|
+
hits: mode === "count" ? [] : hits,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Compact agent-facing JSON (minimal keys). */
|
|
117
|
+
export function formatLiteralLocate(result: LiteralLocateResult): Record<string, unknown> {
|
|
118
|
+
const base: Record<string, unknown> = {
|
|
119
|
+
literal: result.literal,
|
|
120
|
+
mode: result.mode,
|
|
121
|
+
n: result.n,
|
|
122
|
+
files: result.files,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (result.mode === "count") {
|
|
126
|
+
return base;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (result.truncated) {
|
|
130
|
+
base.truncated = true;
|
|
131
|
+
}
|
|
132
|
+
base.hits = result.hits.map((hit) => {
|
|
133
|
+
if (hit.text !== undefined) {
|
|
134
|
+
return { f: hit.file_path, l: hit.line, t: hit.text };
|
|
135
|
+
}
|
|
136
|
+
return { f: hit.file_path, l: hit.line };
|
|
137
|
+
});
|
|
138
|
+
return base;
|
|
139
|
+
}
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { walkFiles } from "../index/file-walker.ts";
|
|
|
4
4
|
import { getExtensions } from "../index/files.ts";
|
|
5
5
|
import { normalizeRelativePath, relativePathFromRoot } from "../index/incremental.ts";
|
|
6
6
|
import { MiruIndex } from "../miru-index.ts";
|
|
7
|
-
import type
|
|
7
|
+
import { type ContentType, defaultContentTypes } from "../types.ts";
|
|
8
8
|
import {
|
|
9
9
|
computeSourceCacheKey,
|
|
10
10
|
isAllowedRepoSource,
|
|
@@ -71,7 +71,7 @@ export class IndexCache {
|
|
|
71
71
|
private readonly entries = new Map<string, CacheEntry>();
|
|
72
72
|
readonly watchers = new Map<string, WatcherHandle>();
|
|
73
73
|
|
|
74
|
-
constructor(content: ContentType[] =
|
|
74
|
+
constructor(content: ContentType[] = defaultContentTypes(), defaultRef: string | null = null) {
|
|
75
75
|
this.content = content;
|
|
76
76
|
this.defaultRef = defaultRef;
|
|
77
77
|
}
|
package/src/mcp/serve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ContentType, defaultContentTypes } from "../types.ts";
|
|
2
2
|
import { IndexCache } from "./index-cache.ts";
|
|
3
3
|
import { createMcpServer } from "./server.ts";
|
|
4
4
|
import { StdioTransport } from "./stdio.ts";
|
|
@@ -6,9 +6,10 @@ import { StdioTransport } from "./stdio.ts";
|
|
|
6
6
|
export async function serveMcp(options: {
|
|
7
7
|
ref?: string | null;
|
|
8
8
|
content?: ContentType[];
|
|
9
|
+
benchmark?: boolean;
|
|
9
10
|
}): Promise<void> {
|
|
10
|
-
const cache = new IndexCache(options.content ??
|
|
11
|
-
const server = createMcpServer(cache);
|
|
11
|
+
const cache = new IndexCache(options.content ?? defaultContentTypes(), options.ref ?? null);
|
|
12
|
+
const server = createMcpServer(cache, { benchmark: options.benchmark ?? false });
|
|
12
13
|
const transport = new StdioTransport();
|
|
13
14
|
await server.connect(transport);
|
|
14
15
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import packageJson from "../../package.json";
|
|
3
|
+
import { attachSearchBenchmark, benchmarkSearchComparison } from "../benchmark/compare.ts";
|
|
3
4
|
import {
|
|
5
|
+
appendBenchmarkQuery,
|
|
6
|
+
readBenchmarkRollup,
|
|
7
|
+
recordFromAgentSummary,
|
|
8
|
+
recordFromBenchmark,
|
|
9
|
+
} from "../benchmark/history.ts";
|
|
10
|
+
import { attachLocateBenchmark, benchmarkLocateComparison } from "../benchmark/locate-compare.ts";
|
|
11
|
+
import {
|
|
12
|
+
MCP_BENCHMARK_SERVER_INSTRUCTIONS,
|
|
4
13
|
MCP_EXPAND_TOOL_DESCRIPTION,
|
|
5
14
|
MCP_FIND_RELATED_TOOL_DESCRIPTION,
|
|
15
|
+
MCP_LOCATE_TOOL_DESCRIPTION,
|
|
16
|
+
MCP_READ_BENCHMARK_TOOL_DESCRIPTION,
|
|
6
17
|
MCP_SEARCH_TOOL_DESCRIPTION,
|
|
7
18
|
MCP_SERVER_INSTRUCTIONS,
|
|
8
19
|
} from "../installer/search-policy.ts";
|
|
20
|
+
import { formatLiteralLocate, type LiteralMode } from "../literal.ts";
|
|
9
21
|
import type { ContentType } from "../types.ts";
|
|
10
22
|
import {
|
|
11
23
|
clampMcpTopK,
|
|
24
|
+
DEFAULT_EXPAND_AFTER,
|
|
25
|
+
DEFAULT_EXPAND_BEFORE,
|
|
12
26
|
DEFAULT_MCP_TOP_K,
|
|
13
27
|
dedupeResultsByFile,
|
|
14
28
|
expandChunksAtLine,
|
|
@@ -26,14 +40,31 @@ const REPO_DESCRIPTION =
|
|
|
26
40
|
"Pass the project root for local workspaces. " +
|
|
27
41
|
"The index is built on the first tool call and cached for the session.";
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
const BENCHMARK_LOCAL_ONLY_NOTE =
|
|
44
|
+
"Benchmark comparisons require a local repo path; git URL repos are skipped.";
|
|
45
|
+
|
|
46
|
+
function withBenchmarkSkipped<T extends Record<string, unknown>>(
|
|
47
|
+
payload: T,
|
|
48
|
+
): T & { benchmark_skipped: "local_repo_only"; note: string } {
|
|
49
|
+
return {
|
|
50
|
+
...payload,
|
|
51
|
+
benchmark_skipped: "local_repo_only",
|
|
52
|
+
note: BENCHMARK_LOCAL_ONLY_NOTE,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createMcpServer(
|
|
57
|
+
cache: IndexCache,
|
|
58
|
+
options?: { benchmark?: boolean },
|
|
59
|
+
): MiruMcpServer {
|
|
60
|
+
const benchmark = options?.benchmark ?? false;
|
|
30
61
|
const server = new MiruMcpServer(
|
|
31
62
|
{
|
|
32
63
|
name: "miru",
|
|
33
64
|
version: packageJson.version,
|
|
34
65
|
},
|
|
35
66
|
{
|
|
36
|
-
instructions: MCP_SERVER_INSTRUCTIONS,
|
|
67
|
+
instructions: benchmark ? MCP_BENCHMARK_SERVER_INSTRUCTIONS : MCP_SERVER_INSTRUCTIONS,
|
|
37
68
|
},
|
|
38
69
|
);
|
|
39
70
|
|
|
@@ -64,15 +95,116 @@ export function createMcpServer(cache: IndexCache): MiruMcpServer {
|
|
|
64
95
|
async ({ query, repo, top_k: topK, dedupe_by_file: dedupeByFile }) => {
|
|
65
96
|
try {
|
|
66
97
|
const index = await getIndexForRepo(repo, cache);
|
|
67
|
-
|
|
98
|
+
const repoRoot = localRepoRoot(repo);
|
|
99
|
+
const k = clampMcpTopK(topK);
|
|
100
|
+
|
|
101
|
+
if (benchmark && repoRoot) {
|
|
102
|
+
const comparison = await benchmarkSearchComparison({
|
|
103
|
+
query,
|
|
104
|
+
repoPath: repoRoot,
|
|
105
|
+
index,
|
|
106
|
+
topK: k,
|
|
107
|
+
dedupeByFile: dedupeByFile !== false,
|
|
108
|
+
});
|
|
109
|
+
const results = comparison.results;
|
|
110
|
+
if (results.length === 0) {
|
|
111
|
+
return toolText(JSON.stringify({ error: "No results found." }));
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await appendBenchmarkQuery(recordFromBenchmark(repoRoot, comparison.benchmark));
|
|
115
|
+
} catch {
|
|
116
|
+
// History is best-effort; never fail the search on persist errors.
|
|
117
|
+
}
|
|
118
|
+
return toolText(
|
|
119
|
+
JSON.stringify(
|
|
120
|
+
attachSearchBenchmark(
|
|
121
|
+
formatResults(query, results, { repoRoot, snippet: true }),
|
|
122
|
+
comparison.benchmark,
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let results = await index.search({ query, topK: k });
|
|
68
129
|
if (dedupeByFile !== false) {
|
|
69
130
|
results = dedupeResultsByFile(results);
|
|
70
131
|
}
|
|
71
132
|
if (results.length === 0) {
|
|
72
133
|
return toolText(JSON.stringify({ error: "No results found." }));
|
|
73
134
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
135
|
+
const payload = formatResults(query, results, { repoRoot, snippet: true });
|
|
136
|
+
if (benchmark && !repoRoot) {
|
|
137
|
+
return toolText(JSON.stringify(withBenchmarkSkipped(payload)));
|
|
138
|
+
}
|
|
139
|
+
return toolText(JSON.stringify(payload));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return toolText(err instanceof Error ? err.message : String(err));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
server.registerTool(
|
|
147
|
+
"locate",
|
|
148
|
+
{
|
|
149
|
+
description: MCP_LOCATE_TOOL_DESCRIPTION,
|
|
150
|
+
inputSchema: {
|
|
151
|
+
literal: z
|
|
152
|
+
.string()
|
|
153
|
+
.min(1)
|
|
154
|
+
.describe("Exact substring to find (env var, symbol, error code, quoted text)."),
|
|
155
|
+
repo: z.string().describe(REPO_DESCRIPTION),
|
|
156
|
+
mode: z
|
|
157
|
+
.enum(["count", "locations", "lines"])
|
|
158
|
+
.optional()
|
|
159
|
+
.describe(
|
|
160
|
+
"count=totals only; locations=path:line; lines=path:line+text (default). Prefer count/locations when possible.",
|
|
161
|
+
),
|
|
162
|
+
limit: z
|
|
163
|
+
.number()
|
|
164
|
+
.int()
|
|
165
|
+
.min(1)
|
|
166
|
+
.optional()
|
|
167
|
+
.describe(
|
|
168
|
+
"Optional cap on returned hits. Omit to return ALL matches (recommended). Do not fall back to Grep if n is large — use mode=count or locations instead.",
|
|
169
|
+
),
|
|
170
|
+
ignore_case: z.boolean().optional().describe("Case-insensitive match (default false)."),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
async ({ literal, repo, mode, limit, ignore_case: ignoreCase }) => {
|
|
174
|
+
try {
|
|
175
|
+
const index = await getIndexForRepo(repo, cache);
|
|
176
|
+
const locateOpts = {
|
|
177
|
+
mode: (mode as LiteralMode | undefined) ?? "lines",
|
|
178
|
+
...(limit != null ? { limit } : {}),
|
|
179
|
+
ignore_case: ignoreCase,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (benchmark) {
|
|
183
|
+
const repoRoot = localRepoRoot(repo);
|
|
184
|
+
if (repoRoot) {
|
|
185
|
+
const comparison = await benchmarkLocateComparison({
|
|
186
|
+
literal,
|
|
187
|
+
repoPath: repoRoot,
|
|
188
|
+
index,
|
|
189
|
+
locate: locateOpts,
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
await appendBenchmarkQuery(recordFromAgentSummary(repoRoot, comparison.benchmark));
|
|
193
|
+
} catch {
|
|
194
|
+
// History is best-effort; never fail locate on persist errors.
|
|
195
|
+
}
|
|
196
|
+
return toolText(
|
|
197
|
+
JSON.stringify(attachLocateBenchmark(comparison.payload, comparison.benchmark)),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = index.locateLiteral(literal, locateOpts);
|
|
203
|
+
const payload = formatLiteralLocate(result);
|
|
204
|
+
if (benchmark && !localRepoRoot(repo)) {
|
|
205
|
+
return toolText(JSON.stringify(withBenchmarkSkipped(payload)));
|
|
206
|
+
}
|
|
207
|
+
return toolText(JSON.stringify(payload));
|
|
76
208
|
} catch (err) {
|
|
77
209
|
return toolText(err instanceof Error ? err.message : String(err));
|
|
78
210
|
}
|
|
@@ -97,21 +229,21 @@ export function createMcpServer(cache: IndexCache): MiruMcpServer {
|
|
|
97
229
|
.int()
|
|
98
230
|
.min(0)
|
|
99
231
|
.optional()
|
|
100
|
-
.describe(
|
|
232
|
+
.describe(`Extra chunks before the anchor (default ${DEFAULT_EXPAND_BEFORE}).`),
|
|
101
233
|
after: z
|
|
102
234
|
.number()
|
|
103
235
|
.int()
|
|
104
236
|
.min(0)
|
|
105
237
|
.optional()
|
|
106
|
-
.describe(
|
|
238
|
+
.describe(`Extra chunks after the anchor (default ${DEFAULT_EXPAND_AFTER}).`),
|
|
107
239
|
},
|
|
108
240
|
},
|
|
109
241
|
async ({ file_path: filePath, anchor_line: anchorLine, repo, before, after }) => {
|
|
110
242
|
try {
|
|
111
243
|
const index = await getIndexForRepo(repo, cache);
|
|
112
244
|
const repoRoot = localRepoRoot(repo);
|
|
113
|
-
const beforeCount = before ??
|
|
114
|
-
const afterCount = after ??
|
|
245
|
+
const beforeCount = before ?? DEFAULT_EXPAND_BEFORE;
|
|
246
|
+
const afterCount = after ?? DEFAULT_EXPAND_AFTER;
|
|
115
247
|
const { anchor, chunks: expanded } = expandChunksAtLine(
|
|
116
248
|
index.chunks,
|
|
117
249
|
filePath,
|
|
@@ -194,6 +326,31 @@ export function createMcpServer(cache: IndexCache): MiruMcpServer {
|
|
|
194
326
|
},
|
|
195
327
|
);
|
|
196
328
|
|
|
329
|
+
if (benchmark) {
|
|
330
|
+
server.registerTool(
|
|
331
|
+
"read_benchmark",
|
|
332
|
+
{
|
|
333
|
+
description: MCP_READ_BENCHMARK_TOOL_DESCRIPTION,
|
|
334
|
+
inputSchema: {
|
|
335
|
+
repo: z
|
|
336
|
+
.string()
|
|
337
|
+
.optional()
|
|
338
|
+
.describe(
|
|
339
|
+
"Optional local repo path or git URL to filter the rollup. Omit for overall totals.",
|
|
340
|
+
),
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
async ({ repo }) => {
|
|
344
|
+
try {
|
|
345
|
+
const rollup = await readBenchmarkRollup({ repo });
|
|
346
|
+
return toolText(JSON.stringify(rollup));
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return toolText(err instanceof Error ? err.message : String(err));
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
197
354
|
return server;
|
|
198
355
|
}
|
|
199
356
|
|
package/src/miru-index.ts
CHANGED
|
@@ -14,9 +14,10 @@ import { createIndexFromPath } from "./index/create.ts";
|
|
|
14
14
|
import { applyIncrementalFileChanges } 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 { type LiteralLocateOptions, type LiteralLocateResult, locateLiteral } from "./literal.ts";
|
|
17
18
|
import { hybridSearch, searchSemanticOnly } from "./search.ts";
|
|
18
19
|
import type { Chunk, ContentType, SearchResult } from "./types.ts";
|
|
19
|
-
import { chunkKey, chunkToDict } from "./types.ts";
|
|
20
|
+
import { chunkKey, chunkToDict, defaultContentTypes } from "./types.ts";
|
|
20
21
|
import { computeSourceCacheKey, isGitUrl } from "./utils.ts";
|
|
21
22
|
import { indexCacheEpoch } from "./version.ts";
|
|
22
23
|
|
|
@@ -120,7 +121,7 @@ export class MiruIndex {
|
|
|
120
121
|
this.chunksInternal = options.chunks;
|
|
121
122
|
this.embeddingModel = options.embeddingModel;
|
|
122
123
|
this._root = options.root ?? null;
|
|
123
|
-
this.content = options.content ??
|
|
124
|
+
this.content = options.content ?? defaultContentTypes();
|
|
124
125
|
this.loadedFromDiskFlag = options.loadedFromDisk ?? false;
|
|
125
126
|
this.storedFileMtimes = new Map(Object.entries(options.storedFileMtimes ?? {}));
|
|
126
127
|
this.fileMapping = new Map();
|
|
@@ -162,7 +163,7 @@ export class MiruIndex {
|
|
|
162
163
|
*/
|
|
163
164
|
static async fromGit(
|
|
164
165
|
url: string,
|
|
165
|
-
content: ContentType[] =
|
|
166
|
+
content: ContentType[] = defaultContentTypes(),
|
|
166
167
|
embeddingModel?: string,
|
|
167
168
|
ref?: string | null,
|
|
168
169
|
): Promise<MiruIndex> {
|
|
@@ -202,7 +203,7 @@ export class MiruIndex {
|
|
|
202
203
|
/** Resolve a local path or git URL and delegate to `fromPath` or `fromGit`. */
|
|
203
204
|
static async fromSource(
|
|
204
205
|
source: string,
|
|
205
|
-
content: ContentType[] =
|
|
206
|
+
content: ContentType[] = defaultContentTypes(),
|
|
206
207
|
embeddingModel?: string,
|
|
207
208
|
ref?: string | null,
|
|
208
209
|
): Promise<MiruIndex> {
|
|
@@ -221,7 +222,7 @@ export class MiruIndex {
|
|
|
221
222
|
*/
|
|
222
223
|
static async fromPath(
|
|
223
224
|
path: string,
|
|
224
|
-
content: ContentType[] =
|
|
225
|
+
content: ContentType[] = defaultContentTypes(),
|
|
225
226
|
embeddingModel?: string,
|
|
226
227
|
): Promise<MiruIndex> {
|
|
227
228
|
const resolved = resolve(path);
|
|
@@ -271,7 +272,7 @@ export class MiruIndex {
|
|
|
271
272
|
static async loadFromDisk(path: string, embeddingModel?: string): Promise<MiruIndex> {
|
|
272
273
|
const model = embeddingModel ?? resolveEmbeddingModel();
|
|
273
274
|
const { bm25, semantic, chunks, metadata } = await loadCachedIndex(path);
|
|
274
|
-
const content = (metadata.content_type as ContentType[]) ??
|
|
275
|
+
const content = (metadata.content_type as ContentType[]) ?? defaultContentTypes();
|
|
275
276
|
const root = metadata.root_path ? String(metadata.root_path) : null;
|
|
276
277
|
const storedFileMtimes = (metadata.file_mtimes as Record<string, number>) ?? {};
|
|
277
278
|
|
|
@@ -444,4 +445,12 @@ export class MiruIndex {
|
|
|
444
445
|
|
|
445
446
|
return results.filter((r) => chunkKey(r.chunk) !== chunkKey(target)).slice(0, topK);
|
|
446
447
|
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Exact substring location over indexed chunks (no disk re-walk).
|
|
451
|
+
* Prefer for known literals; use `search` for meaning-based questions.
|
|
452
|
+
*/
|
|
453
|
+
locateLiteral(literal: string, options?: LiteralLocateOptions): LiteralLocateResult {
|
|
454
|
+
return locateLiteral(this.chunksInternal, literal, options);
|
|
455
|
+
}
|
|
447
456
|
}
|
package/src/snippet.ts
CHANGED
|
@@ -149,6 +149,9 @@ export function applySnippetsToResults(
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
export
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
export {
|
|
153
|
+
countTokens,
|
|
154
|
+
estimateResultTokens,
|
|
155
|
+
tokenCountMethod,
|
|
156
|
+
tokenizerJsonPath,
|
|
157
|
+
} from "./token-count.ts";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { BertWordPieceTokenizer } from "./tokenizer/bert-wordpiece.ts";
|
|
5
|
+
import { loadTokenizerFromFile } from "./tokenizer/load.ts";
|
|
6
|
+
import type { SearchResult } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const TOKENIZER_JSON_ENV = "MIRU_TOKENIZER_JSON";
|
|
9
|
+
const PACKAGE_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
10
|
+
const DEFAULT_TOKENIZER_JSON = join(PACKAGE_ROOT, "tokenizer", "tokenizer.json");
|
|
11
|
+
|
|
12
|
+
let cached: BertWordPieceTokenizer | undefined;
|
|
13
|
+
let cachedPath: string | null = null;
|
|
14
|
+
|
|
15
|
+
function resolveTokenizerJsonPath(): string {
|
|
16
|
+
const fromEnv = process.env[TOKENIZER_JSON_ENV]?.trim();
|
|
17
|
+
if (fromEnv) {
|
|
18
|
+
return resolve(fromEnv);
|
|
19
|
+
}
|
|
20
|
+
return DEFAULT_TOKENIZER_JSON;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadTokenizer(): BertWordPieceTokenizer {
|
|
24
|
+
if (cached) {
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const path = resolveTokenizerJsonPath();
|
|
29
|
+
if (!existsSync(path)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Tokenizer not found at ${path}. Set ${TOKENIZER_JSON_ENV} or add tokenizer/tokenizer.json to the package.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cached = loadTokenizerFromFile(path);
|
|
36
|
+
cachedPath = path;
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Reset cached tokenizer (for tests). */
|
|
41
|
+
export function resetTokenizerCache(): void {
|
|
42
|
+
cached = undefined;
|
|
43
|
+
cachedPath = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function tokenizerJsonPath(): string {
|
|
47
|
+
loadTokenizer();
|
|
48
|
+
return cachedPath ?? resolveTokenizerJsonPath();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @deprecated Use tokenCountMethod() — kept for benchmark response compatibility. */
|
|
52
|
+
export function tokenCountMethod(): "wordpiece" {
|
|
53
|
+
return "wordpiece";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function countTokens(text: string): number {
|
|
57
|
+
return loadTokenizer().count(text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function estimateResultTokens(results: SearchResult[]): number {
|
|
61
|
+
return results.reduce((sum, result) => sum + countTokens(result.chunk.content), 0);
|
|
62
|
+
}
|