@takara-ai/miru-code 0.4.12 → 0.6.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.
@@ -2,6 +2,12 @@ import { basename, dirname } from "node:path";
2
2
  import { splitIdentifier } from "../tokens.ts";
3
3
  import type { Chunk } from "../types.ts";
4
4
  import { chunkKey } from "../types.ts";
5
+ import { searchImprovementsEnabled } from "./features.ts";
6
+ import {
7
+ boostExactStemMatches,
8
+ boostLocationSignals,
9
+ penalizeInstallerForLocation,
10
+ } from "./location.ts";
5
11
 
6
12
  const SYMBOL_QUERY_RE =
7
13
  /^(?:(?:[A-Za-z_][A-Za-z0-9_]*(?:(?:::|\\|->|\.)[A-Za-z_][A-Za-z0-9_]*)+)|_(?:[A-Za-z0-9_]*)|(?:[A-Za-z][A-Za-z0-9]*[A-Z_][A-Za-z0-9_]*)|(?:[A-Z][A-Za-z0-9]*))$/;
@@ -165,6 +171,11 @@ export function applyQueryBoost(
165
171
  } else {
166
172
  boostStemMatches(combinedScores, query, maxScore, chunksByKey);
167
173
  boostEmbeddedSymbols(combinedScores, query, maxScore, allChunks, chunksByKey);
174
+ if (searchImprovementsEnabled()) {
175
+ boostExactStemMatches(combinedScores, query, maxScore, chunksByKey);
176
+ boostLocationSignals(combinedScores, query, maxScore, allChunks, chunksByKey);
177
+ penalizeInstallerForLocation(combinedScores, query, chunksByKey);
178
+ }
168
179
  }
169
180
  return combinedScores;
170
181
  }
@@ -0,0 +1,11 @@
1
+ /** Toggle search ranking/indexing improvements (A/B via MIRU_SEARCH_V2=0|1). */
2
+ export function searchImprovementsEnabled(): boolean {
3
+ const value = process.env.MIRU_SEARCH_V2;
4
+ if (value === "0" || value === "false") {
5
+ return false;
6
+ }
7
+ if (value === "1" || value === "true") {
8
+ return true;
9
+ }
10
+ return true;
11
+ }
@@ -0,0 +1,237 @@
1
+ import { basename } from "node:path";
2
+ import type { Chunk } from "../types.ts";
3
+ import { chunkKey } from "../types.ts";
4
+
5
+ const LOCATION_QUERY_RE =
6
+ /\b(where(?:'s| is)?|entry\s*point|bootstrap|starts?|live[s]?|located|defined|wiring)\b/i;
7
+
8
+ const ENTRY_POINT_QUERY_RE = /\b(entry\s*point|bootstrap|starts?|wiring|command\s*line)\b/i;
9
+
10
+ const INTEGRATION_QUERY_RE = /\b(install|uninstall|hook|agent|configure|setup)\b/i;
11
+
12
+ const INSTALLER_PATH_RE = /(?:^|\/)installer(?:\/|$)/;
13
+
14
+ const ENTRY_POINT_CONTENT_RE =
15
+ /#!\/usr\/bin|(?:^|\n)\s*(?:async\s+)?function\s+main\s*\(|(?:^|\n)\s*if\s*\(\s*require\.main\s*===\s*module/;
16
+
17
+ const PACKAGE_ENTRY_RE = /^\[package entry\]/;
18
+
19
+ const LOCATION_BOOST_MULTIPLIER = 2.5;
20
+ const INSTALLER_LOCATION_PENALTY = 0.35;
21
+
22
+ /** Too common as filenames to get exact-stem boosts from incidental query tokens. */
23
+ const IMPLEMENTATION_HINTS = new Set(
24
+ [
25
+ "command",
26
+ "handler",
27
+ "router",
28
+ "routing",
29
+ "middleware",
30
+ "model",
31
+ "channel",
32
+ "request",
33
+ "response",
34
+ "dispatch",
35
+ "pipeline",
36
+ ].map((w) => w.toLowerCase()),
37
+ );
38
+
39
+ const GENERIC_STEM_DENY = new Set(
40
+ [
41
+ "route",
42
+ "routes",
43
+ "router",
44
+ "routing",
45
+ "handler",
46
+ "middleware",
47
+ "channel",
48
+ "model",
49
+ "models",
50
+ "request",
51
+ "response",
52
+ "utils",
53
+ "util",
54
+ "server",
55
+ "client",
56
+ "application",
57
+ "dispatch",
58
+ "core",
59
+ "lib",
60
+ "main",
61
+ "mod",
62
+ "test",
63
+ "tests",
64
+ "schema",
65
+ "config",
66
+ "helper",
67
+ "helpers",
68
+ "engine",
69
+ "pipeline",
70
+ "plug",
71
+ "plugs",
72
+ "command",
73
+ "commands",
74
+ "index",
75
+ ].map((w) => w.toLowerCase()),
76
+ );
77
+
78
+ export function isLocationQuery(query: string): boolean {
79
+ return LOCATION_QUERY_RE.test(query);
80
+ }
81
+
82
+ export function isEntryPointQuery(query: string): boolean {
83
+ return ENTRY_POINT_QUERY_RE.test(query);
84
+ }
85
+
86
+ function fileStemMatchesQuery(filePath: string, query: string): boolean {
87
+ const stem = basename(filePath)
88
+ .replace(/\.[^.]+$/, "")
89
+ .toLowerCase();
90
+ const rawTokens = (query.match(/[a-zA-Z_][a-zA-Z0-9_-]*/g) ?? []).map((t) => t.toLowerCase());
91
+
92
+ if (rawTokens.includes(stem)) {
93
+ return true;
94
+ }
95
+
96
+ for (const token of rawTokens) {
97
+ if (token.includes("-") && token.split("-").includes(stem)) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ const stemNorm = stem.replace(/[-_]/g, "");
103
+ return rawTokens.some((t) => t.replace(/[-_]/g, "") === stemNorm);
104
+ }
105
+
106
+ export function isIntegrationQuery(query: string): boolean {
107
+ return INTEGRATION_QUERY_RE.test(query);
108
+ }
109
+
110
+ function chunkLooksLikeEntryPoint(chunk: Chunk): boolean {
111
+ if (PACKAGE_ENTRY_RE.test(chunk.content)) {
112
+ return true;
113
+ }
114
+ return ENTRY_POINT_CONTENT_RE.test(chunk.content);
115
+ }
116
+
117
+ export function boostLocationSignals(
118
+ boosted: Map<string, number>,
119
+ query: string,
120
+ maxScore: number,
121
+ allChunks: Chunk[],
122
+ chunksByKey: Map<string, Chunk>,
123
+ ): void {
124
+ if (!isLocationQuery(query)) {
125
+ return;
126
+ }
127
+
128
+ const entryPointQuery = isEntryPointQuery(query);
129
+ const packageBoost = maxScore * (entryPointQuery ? LOCATION_BOOST_MULTIPLIER : 0.75);
130
+ const codeBoost = maxScore * LOCATION_BOOST_MULTIPLIER;
131
+
132
+ for (const chunk of allChunks) {
133
+ if (!chunkLooksLikeEntryPoint(chunk)) {
134
+ continue;
135
+ }
136
+
137
+ const isPackageEntry = PACKAGE_ENTRY_RE.test(chunk.content);
138
+ if (isPackageEntry) {
139
+ const key = chunkKey(chunk);
140
+ boosted.set(key, (boosted.get(key) ?? 0) + packageBoost);
141
+ chunksByKey.set(key, chunk);
142
+ continue;
143
+ }
144
+
145
+ if (!entryPointQuery && !fileStemMatchesQuery(chunk.file_path, query)) {
146
+ continue;
147
+ }
148
+
149
+ const key = chunkKey(chunk);
150
+ boosted.set(key, (boosted.get(key) ?? 0) + codeBoost);
151
+ chunksByKey.set(key, chunk);
152
+ }
153
+ }
154
+
155
+ export function penalizeInstallerForLocation(
156
+ boosted: Map<string, number>,
157
+ query: string,
158
+ chunksByKey: Map<string, Chunk>,
159
+ ): void {
160
+ if (!isLocationQuery(query) || isIntegrationQuery(query)) {
161
+ return;
162
+ }
163
+
164
+ for (const [key, score] of boosted) {
165
+ const chunk = chunksByKey.get(key);
166
+ if (!chunk) {
167
+ continue;
168
+ }
169
+ if (INSTALLER_PATH_RE.test(chunk.file_path.replace(/\\/g, "/"))) {
170
+ boosted.set(key, score * INSTALLER_LOCATION_PENALTY);
171
+ }
172
+ }
173
+ }
174
+
175
+ function hasSpecificImplementationTarget(query: string, chunksByKey: Map<string, Chunk>): boolean {
176
+ const lowered = query.toLowerCase();
177
+ for (const hint of IMPLEMENTATION_HINTS) {
178
+ if (!lowered.includes(hint)) {
179
+ continue;
180
+ }
181
+ for (const chunk of chunksByKey.values()) {
182
+ const stem = basename(chunk.file_path)
183
+ .replace(/\.[^.]+$/, "")
184
+ .toLowerCase();
185
+ if (stem === hint) {
186
+ return true;
187
+ }
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+
193
+ export function boostExactStemMatches(
194
+ boosted: Map<string, number>,
195
+ query: string,
196
+ maxScore: number,
197
+ chunksByKey: Map<string, Chunk>,
198
+ ): void {
199
+ const keywords = new Set(
200
+ (query.match(/[a-zA-Z_][a-zA-Z0-9_-]*/g) ?? [])
201
+ .filter((w) => w.length > 2)
202
+ .map((w) => w.toLowerCase()),
203
+ );
204
+ if (keywords.size === 0) {
205
+ return;
206
+ }
207
+
208
+ const boost = maxScore * 0.75;
209
+ const preferImplementation = hasSpecificImplementationTarget(query, chunksByKey);
210
+
211
+ for (const key of [...boosted.keys()]) {
212
+ const chunk = chunksByKey.get(key);
213
+ if (!chunk) {
214
+ continue;
215
+ }
216
+ const stem = basename(chunk.file_path)
217
+ .replace(/\.[^.]+$/, "")
218
+ .toLowerCase();
219
+ const stemNorm = stem.replace(/[-_]/g, "");
220
+ for (const keyword of keywords) {
221
+ if (GENERIC_STEM_DENY.has(keyword)) {
222
+ continue;
223
+ }
224
+ if (preferImplementation && stem === keyword && !IMPLEMENTATION_HINTS.has(stem)) {
225
+ continue;
226
+ }
227
+ const keywordNorm = keyword.replace(/[-_]/g, "");
228
+ if (stem === keyword || stemNorm === keywordNorm) {
229
+ boosted.set(key, (boosted.get(key) ?? 0) + boost);
230
+ break;
231
+ }
232
+ const parts = keyword.split("-").filter((p) => p.length > 2);
233
+ if (parts.length > 1 && parts.some((p) => p === stem)) {
234
+ }
235
+ }
236
+ }
237
+ }
package/src/search.ts CHANGED
@@ -4,6 +4,8 @@ import type { SemanticIndex } from "./index/semantic-index.ts";
4
4
  import { selectorToMask } from "./index/sparse.ts";
5
5
  import { selectTopKScoreIndices } from "./index/topk.ts";
6
6
  import { applyQueryBoost, boostMultiChunkFiles } from "./ranking/boosting.ts";
7
+ import { searchImprovementsEnabled } from "./ranking/features.ts";
8
+ import { isLocationQuery } from "./ranking/location.ts";
7
9
  import { rerankTopk } from "./ranking/penalties.ts";
8
10
  import { resolveAlpha } from "./ranking/weighting.ts";
9
11
  import { tokenize } from "./tokens.ts";
@@ -93,7 +95,8 @@ export async function hybridSearch(options: {
93
95
  } = options;
94
96
 
95
97
  const alphaWeight = resolveAlpha(query, alpha);
96
- const candidateCount = topK * 5;
98
+ const candidateCount =
99
+ searchImprovementsEnabled() && isLocationQuery(query) ? topK * 10 : topK * 5;
97
100
  const chunksByKey = new Map(chunks.map((c) => [chunkKey(c), c]));
98
101
 
99
102
  const queryVecPromise = embeddings.embedQuery(query);
package/src/setup.ts CHANGED
@@ -63,7 +63,7 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<string> {
63
63
  spinner.start();
64
64
  const result = await validateEmbeddingApiKey({ apiKey });
65
65
  if (!result.valid) {
66
- spinner.fail(result.message);
66
+ spinner.stop();
67
67
  throw new Error(result.message);
68
68
  }
69
69
  spinner.succeed("API key validated");
package/src/snippet.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { envOptionalInt } from "./env.ts";
2
+ import { tokenize } from "./tokens.ts";
3
+ import type { Chunk, SearchResult } from "./types.ts";
4
+
5
+ const SNIPPET_STOPWORDS = new Set(
6
+ "a an and are as at be by do does for from has have how if in is it not of on or the to was what when where which who why with".split(
7
+ " ",
8
+ ),
9
+ );
10
+
11
+ export interface SnippetMeta {
12
+ truncated: boolean;
13
+ anchor_line: number;
14
+ full_start_line: number;
15
+ full_end_line: number;
16
+ }
17
+
18
+ export interface SnippetResult {
19
+ chunk: Chunk;
20
+ meta: SnippetMeta;
21
+ }
22
+
23
+ export function resolveSnippetLines(): number {
24
+ return envOptionalInt(["MIRU_SNIPPET_LINES"], 3) ?? 15;
25
+ }
26
+
27
+ export function searchSnippetsEnabled(): boolean {
28
+ const value = process.env.MIRU_SEARCH_SNIPPETS;
29
+ if (value === "0" || value === "false") {
30
+ return false;
31
+ }
32
+ if (value === "1" || value === "true") {
33
+ return true;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ function queryMatchTerms(query: string): Set<string> {
39
+ const terms = new Set<string>();
40
+ for (const tok of tokenize(query)) {
41
+ if (tok.length >= 3 && !SNIPPET_STOPWORDS.has(tok)) {
42
+ terms.add(tok);
43
+ }
44
+ }
45
+ for (const word of query.match(/[a-zA-Z_][a-zA-Z0-9_-]*/g) ?? []) {
46
+ const lower = word.toLowerCase();
47
+ if (lower.length >= 3 && !SNIPPET_STOPWORDS.has(lower)) {
48
+ terms.add(lower);
49
+ }
50
+ }
51
+ return terms;
52
+ }
53
+
54
+ function scoreLine(line: string, terms: Set<string>): number {
55
+ const lower = line.toLowerCase();
56
+ let score = 0;
57
+ for (const term of terms) {
58
+ if (lower.includes(term)) {
59
+ score++;
60
+ }
61
+ }
62
+ return score;
63
+ }
64
+
65
+ /** Pick the 0-based line index inside `content` that best matches the query. */
66
+ export function anchorLineOffset(content: string, query: string): number {
67
+ const lines = content.split("\n");
68
+ if (lines.length === 0) {
69
+ return 0;
70
+ }
71
+
72
+ const terms = queryMatchTerms(query);
73
+ let bestIndex = Math.floor(lines.length / 2);
74
+ let bestScore = -1;
75
+
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i] ?? "";
78
+ const score = scoreLine(line, terms);
79
+ if (score > bestScore) {
80
+ bestScore = score;
81
+ bestIndex = i;
82
+ }
83
+ }
84
+
85
+ return bestIndex;
86
+ }
87
+
88
+ export function trimChunkToSnippet(
89
+ chunk: Chunk,
90
+ query: string,
91
+ linesEachSide = resolveSnippetLines(),
92
+ ): SnippetResult {
93
+ const lines = chunk.content.split("\n");
94
+ if (lines.length === 0) {
95
+ return {
96
+ chunk,
97
+ meta: {
98
+ truncated: false,
99
+ anchor_line: chunk.start_line,
100
+ full_start_line: chunk.start_line,
101
+ full_end_line: chunk.end_line,
102
+ },
103
+ };
104
+ }
105
+
106
+ const anchorOffset = anchorLineOffset(chunk.content, query);
107
+ const startOffset = Math.max(0, anchorOffset - linesEachSide);
108
+ const endOffset = Math.min(lines.length, anchorOffset + linesEachSide + 1);
109
+ const truncated = startOffset > 0 || endOffset < lines.length;
110
+
111
+ if (!truncated) {
112
+ return {
113
+ chunk,
114
+ meta: {
115
+ truncated: false,
116
+ anchor_line: chunk.start_line + anchorOffset,
117
+ full_start_line: chunk.start_line,
118
+ full_end_line: chunk.end_line,
119
+ },
120
+ };
121
+ }
122
+
123
+ const snippetContent = lines.slice(startOffset, endOffset).join("\n");
124
+ return {
125
+ chunk: {
126
+ ...chunk,
127
+ content: snippetContent,
128
+ start_line: chunk.start_line + startOffset,
129
+ end_line: chunk.start_line + endOffset - 1,
130
+ },
131
+ meta: {
132
+ truncated: true,
133
+ anchor_line: chunk.start_line + anchorOffset,
134
+ full_start_line: chunk.start_line,
135
+ full_end_line: chunk.end_line,
136
+ },
137
+ };
138
+ }
139
+
140
+ export function applySnippetsToResults(
141
+ results: SearchResult[],
142
+ query: string,
143
+ linesEachSide?: number,
144
+ ): Array<{ result: SearchResult; meta: SnippetMeta }> {
145
+ const radius = linesEachSide ?? resolveSnippetLines();
146
+ return results.map((result) => {
147
+ const { chunk, meta } = trimChunkToSnippet(result.chunk, query, radius);
148
+ return { result: { chunk, score: result.score }, meta };
149
+ });
150
+ }
151
+
152
+ export function estimateResultTokens(results: SearchResult[]): number {
153
+ return results.reduce((sum, r) => sum + Math.floor(r.chunk.content.length / 4), 0);
154
+ }
package/src/spinner.ts CHANGED
@@ -57,7 +57,7 @@ export async function withSpinner<T>(
57
57
  spinner.succeed(options?.successMessage ?? "");
58
58
  return result;
59
59
  } catch (err) {
60
- spinner.fail(options?.failMessage);
60
+ spinner.stop();
61
61
  throw err;
62
62
  }
63
63
  }
package/src/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { join, relative, resolve, sep } from "node:path";
2
+ import { applySnippetsToResults, searchSnippetsEnabled } from "./snippet.ts";
2
3
  import type { Chunk, ContentType, SearchResult } from "./types.ts";
3
4
  import { chunkToDict } from "./types.ts";
4
5
 
@@ -16,11 +17,22 @@ export function isGitUrl(path: string): boolean {
16
17
  return GIT_URL_SCHEMES.some((scheme) => path.startsWith(scheme)) || SCP_GIT_URL_RE.test(path);
17
18
  }
18
19
 
20
+ export function httpGitCloneAllowed(): boolean {
21
+ const raw = process.env.MIRU_ALLOW_HTTP_GIT;
22
+ return raw === "1" || raw === "true";
23
+ }
24
+
19
25
  export function isAllowedRepoSource(repo: string): boolean {
20
26
  if (!isGitUrl(repo)) {
21
27
  return true;
22
28
  }
23
- return repo.startsWith("https://") || repo.startsWith("http://");
29
+ if (repo.startsWith("https://")) {
30
+ return true;
31
+ }
32
+ if (repo.startsWith("http://")) {
33
+ return httpGitCloneAllowed();
34
+ }
35
+ return false;
24
36
  }
25
37
 
26
38
  /** Resolved local repo root for MCP output; null for remote git URLs. */
@@ -28,6 +40,20 @@ export function localRepoRoot(repo: string): string | null {
28
40
  return isGitUrl(repo) ? null : resolve(repo);
29
41
  }
30
42
 
43
+ /** When MIRU_WORKSPACE_ROOT is set, local repo paths must stay inside it. */
44
+ export function validateLocalRepoPath(repo: string): void {
45
+ const workspaceRoot = process.env.MIRU_WORKSPACE_ROOT?.trim();
46
+ if (!workspaceRoot || isGitUrl(repo)) {
47
+ return;
48
+ }
49
+ const resolvedRepo = resolve(repo);
50
+ const resolvedWorkspace = resolve(workspaceRoot);
51
+ const prefix = resolvedWorkspace.endsWith(sep) ? resolvedWorkspace : `${resolvedWorkspace}${sep}`;
52
+ if (resolvedRepo !== resolvedWorkspace && !resolvedRepo.startsWith(prefix)) {
53
+ throw new Error(`Local repo path is outside workspace: ${repo}`);
54
+ }
55
+ }
56
+
31
57
  /** Map an absolute or repo-relative path to the index-relative form. */
32
58
  export function toIndexedFilePath(filePath: string, repoRoot?: string | null): string {
33
59
  if (!repoRoot) {
@@ -154,18 +180,49 @@ export function expandChunksAtLine(
154
180
  return { anchor, chunks: fileChunks.slice(start, end) };
155
181
  }
156
182
 
183
+ /** Relative relevance for MCP/CLI output (top hit in a batch is always 100%). */
184
+ export function formatRelevanceScore(score: number, maxScore: number): string {
185
+ if (maxScore <= 0) {
186
+ return "0%";
187
+ }
188
+ const pct = Math.round((score / maxScore) * 100);
189
+ return `${pct}%`;
190
+ }
191
+
157
192
  export function formatResults(
158
193
  query: string,
159
194
  results: SearchResult[],
160
- options?: { repoRoot?: string | null },
195
+ options?: { repoRoot?: string | null; snippet?: boolean },
161
196
  ): { query: string; results: Record<string, unknown>[] } {
162
197
  const repoRoot = options?.repoRoot ?? null;
198
+ const useSnippet = options?.snippet ?? searchSnippetsEnabled();
199
+ const payload = useSnippet ? applySnippetsToResults(results, query) : null;
200
+ const maxScore = results.reduce((max, result) => Math.max(max, result.score), 0);
201
+
163
202
  return {
164
203
  query,
165
- results: results.map((result) => ({
166
- chunk: chunkToResponseDict(result.chunk, repoRoot),
167
- score: result.score,
168
- })),
204
+ results: results.map((result, index) => {
205
+ const entry = payload?.[index];
206
+ const chunk = entry?.result.chunk ?? result.chunk;
207
+ const dict = chunkToResponseDict(chunk, repoRoot);
208
+ const score = formatRelevanceScore(result.score, maxScore);
209
+ if (entry?.meta.truncated) {
210
+ return {
211
+ chunk: {
212
+ ...dict,
213
+ truncated: true,
214
+ anchor_line: entry.meta.anchor_line,
215
+ full_start_line: entry.meta.full_start_line,
216
+ full_end_line: entry.meta.full_end_line,
217
+ },
218
+ score,
219
+ };
220
+ }
221
+ return {
222
+ chunk: dict,
223
+ score,
224
+ };
225
+ }),
169
226
  };
170
227
  }
171
228