@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.
Files changed (60) hide show
  1. package/README.md +43 -48
  2. package/docs/coverage-badge.json +6 -0
  3. package/grammars/manifest.json +10 -2
  4. package/grammars/tree-sitter-astro.wasm +0 -0
  5. package/grammars/tree-sitter-php.wasm +0 -0
  6. package/grammars/tree-sitter-php_only.wasm +0 -0
  7. package/grammars/tree-sitter-sql.wasm +0 -0
  8. package/grammars/tree-sitter-svelte.wasm +0 -0
  9. package/grammars/tree-sitter-vue.wasm +0 -0
  10. package/package.json +10 -2
  11. package/src/agents.ts +4 -4
  12. package/src/auth/device.ts +26 -21
  13. package/src/auth/providers.ts +26 -11
  14. package/src/benchmark/compare.ts +3 -10
  15. package/src/benchmark/grep.ts +64 -37
  16. package/src/benchmark/locate-compare.ts +8 -1
  17. package/src/benchmark/rg-literal.ts +9 -4
  18. package/src/cache.ts +3 -3
  19. package/src/chunking/ast.ts +21 -3
  20. package/src/chunking/grammars.ts +8 -3
  21. package/src/chunking/structural.ts +13 -24
  22. package/src/cli-mode.ts +18 -0
  23. package/src/cli.ts +10 -45
  24. package/src/credentials.ts +8 -13
  25. package/src/embeddings/openai.ts +6 -3
  26. package/src/embeddings/sagemaker.ts +18 -3
  27. package/src/embeddings/validate.ts +10 -7
  28. package/src/git.ts +6 -2
  29. package/src/help.ts +5 -4
  30. package/src/index/bm25.ts +66 -6
  31. package/src/index/create.ts +5 -9
  32. package/src/index/files.ts +5 -1
  33. package/src/index/incremental.ts +48 -8
  34. package/src/index/sparse.ts +14 -2
  35. package/src/installer/agents.ts +85 -65
  36. package/src/installer/benchmark-mode.ts +40 -11
  37. package/src/installer/config.ts +105 -42
  38. package/src/installer/installer.ts +59 -34
  39. package/src/installer/legacy-hooks.ts +131 -0
  40. package/src/installer/prompt.ts +20 -22
  41. package/src/installer/search-policy.ts +8 -8
  42. package/src/mcp/auth-tool.ts +93 -33
  43. package/src/mcp/index-cache.ts +16 -3
  44. package/src/mcp/server.ts +42 -22
  45. package/src/mcp/stdio.ts +17 -2
  46. package/src/miru-index.ts +54 -18
  47. package/src/prompt.ts +51 -14
  48. package/src/ranking/boosting.ts +5 -6
  49. package/src/ranking/location.ts +6 -10
  50. package/src/ranking/penalties.ts +14 -22
  51. package/src/search.ts +4 -12
  52. package/src/setup.ts +25 -13
  53. package/src/snippet.ts +8 -26
  54. package/src/spinner.ts +26 -16
  55. package/src/tokenizer/bert-wordpiece.ts +0 -6
  56. package/src/utils.ts +0 -4
  57. package/src/version.ts +4 -4
  58. package/src/installer/hooks/install.ts +0 -549
  59. package/src/installer/hooks/opencode-plugin.ts +0 -54
  60. package/src/installer/hooks/search-guard.ts +0 -272
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
- while (true) {
128
- const arn = await promptText("SageMaker endpoint ARN");
129
- if (!arn) {
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(arn);
135
- return arn;
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 promptText(
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(options: RunSetupOptions = {}): Promise<RunSetupResult> {
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 arnInput = options.sagemakerArn ?? (await promptSageMakerArn());
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]?.toLowerCase();
196
- if (!name) {
197
- continue;
198
- }
195
+ const name = (match[1] as string).toLowerCase();
199
196
  if (match[0].startsWith("</")) {
200
- for (let j = openTags.length - 1; j >= 0; j--) {
201
- if (openTags[j]?.name === name) {
202
- openTags.splice(j, 1);
203
- break;
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
package/src/spinner.ts CHANGED
@@ -8,11 +8,14 @@ export class Spinner {
8
8
  private frame = 0;
9
9
  private belowLines = 0;
10
10
 
11
- constructor(private readonly message: string) {}
11
+ constructor(
12
+ private readonly message: string,
13
+ private readonly stderr: SpinnerOutput = process.stderr,
14
+ ) {}
12
15
 
13
16
  start(): void {
14
- if (!process.stderr.isTTY) {
15
- process.stderr.write(`${this.message}...\n`);
17
+ if (!this.stderr.isTTY) {
18
+ this.stderr.write(`${this.message}...\n`);
16
19
  return;
17
20
  }
18
21
  this.draw();
@@ -20,17 +23,17 @@ export class Spinner {
20
23
  }
21
24
 
22
25
  follow(line: string): void {
23
- if (!process.stderr.isTTY) {
24
- process.stderr.write(`${line}\n`);
26
+ if (!this.stderr.isTTY) {
27
+ this.stderr.write(`${line}\n`);
25
28
  return;
26
29
  }
27
- const cols = process.stderr.columns || 80;
30
+ const cols = this.stderr.columns || 80;
28
31
  const rows = line.split("\n").reduce((count, part) => {
29
32
  return count + Math.max(1, Math.ceil(displayWidth(part) / cols));
30
33
  }, 0);
31
- process.stderr.write(`\n${line}`);
34
+ this.stderr.write(`\n${line}`);
32
35
  this.belowLines += rows;
33
- process.stderr.write(`\x1b[${this.belowLines}A`);
36
+ this.stderr.write(`\x1b[${this.belowLines}A`);
34
37
  }
35
38
 
36
39
  stop(finalMessage?: string): void {
@@ -38,19 +41,19 @@ export class Spinner {
38
41
  clearInterval(this.timer);
39
42
  this.timer = null;
40
43
  }
41
- if (process.stderr.isTTY) {
42
- process.stderr.write("\r\x1b[K");
44
+ if (this.stderr.isTTY) {
45
+ this.stderr.write("\r\x1b[K");
43
46
  if (finalMessage) {
44
- process.stderr.write(finalMessage);
47
+ this.stderr.write(finalMessage);
45
48
  }
46
49
  if (this.belowLines > 0) {
47
- process.stderr.write(`\x1b[${this.belowLines}B`);
50
+ this.stderr.write(`\x1b[${this.belowLines}B`);
48
51
  }
49
52
  if (finalMessage || this.belowLines > 0) {
50
- process.stderr.write("\n");
53
+ this.stderr.write("\n");
51
54
  }
52
55
  } else if (finalMessage) {
53
- process.stderr.write(`${finalMessage}\n`);
56
+ this.stderr.write(`${finalMessage}\n`);
54
57
  }
55
58
  }
56
59
 
@@ -68,16 +71,23 @@ export class Spinner {
68
71
 
69
72
  private draw(): void {
70
73
  const glyph = FRAMES[this.frame++ % FRAMES.length];
71
- process.stderr.write(`\r${dim(glyph)} ${this.message}`);
74
+ this.stderr.write(`\r${dim(glyph)} ${this.message}`);
72
75
  }
73
76
  }
74
77
 
78
+ interface SpinnerOutput {
79
+ isTTY?: boolean;
80
+ columns?: number;
81
+ write(text: string): unknown;
82
+ }
83
+
75
84
  export async function withSpinner<T>(
76
85
  message: string,
77
86
  fn: () => Promise<T>,
78
87
  options?: { successMessage?: string; failMessage?: string },
88
+ output: SpinnerOutput = process.stderr,
79
89
  ): Promise<T> {
80
- const spinner = new Spinner(message);
90
+ const spinner = new Spinner(message, output);
81
91
  spinner.start();
82
92
  try {
83
93
  const result = await fn();
@@ -99,9 +99,6 @@ function whitespaceTokenize(text: string): string[] {
99
99
 
100
100
  function splitOnPunctuation(text: string): string[] {
101
101
  const chars = [...text];
102
- if (chars.length === 0) {
103
- return [];
104
- }
105
102
 
106
103
  const output: string[][] = [];
107
104
  let startNewWord = true;
@@ -138,9 +135,6 @@ function wordpieceTokenize(
138
135
  prefix: string,
139
136
  maxInputCharsPerWord: number,
140
137
  ): string[] {
141
- if (token.length === 0) {
142
- return [];
143
- }
144
138
  if (token.length > maxInputCharsPerWord) {
145
139
  return [unkToken];
146
140
  }
package/src/utils.ts CHANGED
@@ -165,10 +165,6 @@ export function expandChunksAtLine(
165
165
  const anchorIndex = fileChunks.findIndex(
166
166
  (chunk) => chunk.start_line === anchor.start_line && chunk.end_line === anchor.end_line,
167
167
  );
168
- if (anchorIndex < 0) {
169
- return { anchor, chunks: [anchor] };
170
- }
171
-
172
168
  const start = Math.max(0, anchorIndex - before);
173
169
  const end = Math.min(fileChunks.length, anchorIndex + after + 1);
174
170
  return { anchor, chunks: fileChunks.slice(start, end) };
package/src/version.ts CHANGED
@@ -26,8 +26,8 @@ export function miruVersion(): string {
26
26
  * - 0.x releases: `0.{minor}` (chunking may break on minor bumps pre-1.0).
27
27
  * - 1.x+: major version only.
28
28
  */
29
- export function indexCacheEpoch(): string {
30
- const parts = packageJson.version.split(".");
29
+ export function indexCacheEpoch(version: string = packageJson.version): string {
30
+ const parts = version.split(".");
31
31
  const major = parts[0] ?? "0";
32
32
  const minor = parts[1] ?? "0";
33
33
  if (major === "0") {
@@ -118,7 +118,7 @@ export async function fetchLatestPublishedVersion(fetchImpl: Fetcher = fetch): P
118
118
  }
119
119
 
120
120
  /** Print update hint to stderr when a newer package is on npm (at most once per day). */
121
- export async function maybeNotifyUpdate(): Promise<void> {
121
+ export async function maybeNotifyUpdate(fetchImpl: Fetcher = fetch): Promise<void> {
122
122
  if (process.env.MIRU_NO_UPDATE_CHECK === "1") {
123
123
  return;
124
124
  }
@@ -133,7 +133,7 @@ export async function maybeNotifyUpdate(): Promise<void> {
133
133
  }
134
134
 
135
135
  try {
136
- const latest = await fetchLatestPublishedVersion();
136
+ const latest = await fetchLatestPublishedVersion(fetchImpl);
137
137
  await writeUpdateCheckCache(latest);
138
138
  if (isVersionNewer(latest, current)) {
139
139
  writeUpdateNotice(latest, current);