@takara-ai/miru-code 1.3.1 → 1.5.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/src/mcp/server.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import * as z from "zod";
2
2
  import packageJson from "../../package.json";
3
- import { attachSearchBenchmark, benchmarkSearchComparison } from "../benchmark/compare.ts";
3
+ import { benchmarkSearchComparison, toAgentBenchmarkSummary } from "../benchmark/compare.ts";
4
4
  import {
5
5
  appendBenchmarkQuery,
6
6
  readBenchmarkRollup,
7
7
  recordFromAgentSummary,
8
8
  recordFromBenchmark,
9
9
  } from "../benchmark/history.ts";
10
- import { attachLocateBenchmark, benchmarkLocateComparison } from "../benchmark/locate-compare.ts";
10
+ import { benchmarkLocateComparison } from "../benchmark/locate-compare.ts";
11
+ import { appendAgentBenchmark } from "../benchmark/summary.ts";
11
12
  import {
12
13
  MCP_BENCHMARK_SERVER_INSTRUCTIONS,
13
14
  MCP_EXPAND_TOOL_DESCRIPTION,
@@ -32,6 +33,11 @@ import {
32
33
  MAX_MCP_TOP_K,
33
34
  resolveChunk,
34
35
  } from "../utils.ts";
36
+ import {
37
+ formatExpandResultsText,
38
+ formatLiteralLocateText,
39
+ formatResultsText,
40
+ } from "./format-text.ts";
35
41
  import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
36
42
  import { MiruMcpServer } from "./runtime.ts";
37
43
 
@@ -43,14 +49,11 @@ const REPO_DESCRIPTION =
43
49
  const BENCHMARK_LOCAL_ONLY_NOTE =
44
50
  "Benchmark comparisons require a local repo path; git URL repos are skipped.";
45
51
 
46
- function withBenchmarkSkipped<T extends Record<string, unknown>>(
47
- payload: T,
48
- ): T & { benchmark_skipped: "local_repo_only"; note: string } {
49
- return {
50
- ...payload,
52
+ function withBenchmarkSkippedNote(body: string): string {
53
+ return `${body}\n\n${JSON.stringify({
51
54
  benchmark_skipped: "local_repo_only",
52
55
  note: BENCHMARK_LOCAL_ONLY_NOTE,
53
- };
56
+ })}`;
54
57
  }
55
58
 
56
59
  export function createMcpServer(
@@ -108,20 +111,18 @@ export function createMcpServer(
108
111
  });
109
112
  const results = comparison.results;
110
113
  if (results.length === 0) {
111
- return toolText(JSON.stringify({ error: "No results found." }));
114
+ return toolText("No results found.");
112
115
  }
113
116
  try {
114
117
  await appendBenchmarkQuery(recordFromBenchmark(repoRoot, comparison.benchmark));
115
118
  } catch {
116
119
  // History is best-effort; never fail the search on persist errors.
117
120
  }
121
+ const body = formatResultsText(
122
+ formatResults(query, results, { repoRoot, snippet: true }),
123
+ );
118
124
  return toolText(
119
- JSON.stringify(
120
- attachSearchBenchmark(
121
- formatResults(query, results, { repoRoot, snippet: true }),
122
- comparison.benchmark,
123
- ),
124
- ),
125
+ appendAgentBenchmark(body, toAgentBenchmarkSummary(comparison.benchmark)),
125
126
  );
126
127
  }
127
128
 
@@ -130,13 +131,14 @@ export function createMcpServer(
130
131
  results = dedupeResultsByFile(results);
131
132
  }
132
133
  if (results.length === 0) {
133
- return toolText(JSON.stringify({ error: "No results found." }));
134
+ return toolText("No results found.");
134
135
  }
135
136
  const payload = formatResults(query, results, { repoRoot, snippet: true });
137
+ const body = formatResultsText(payload);
136
138
  if (benchmark && !repoRoot) {
137
- return toolText(JSON.stringify(withBenchmarkSkipped(payload)));
139
+ return toolText(withBenchmarkSkippedNote(body));
138
140
  }
139
- return toolText(JSON.stringify(payload));
141
+ return toolText(body);
140
142
  } catch (err) {
141
143
  return toolText(err instanceof Error ? err.message : String(err));
142
144
  }
@@ -212,18 +214,18 @@ export function createMcpServer(
212
214
  } catch {
213
215
  // History is best-effort; never fail locate on persist errors.
214
216
  }
215
- return toolText(
216
- JSON.stringify(attachLocateBenchmark(comparison.payload, comparison.benchmark)),
217
- );
217
+ const body = formatLiteralLocateText(comparison.payload);
218
+ return toolText(appendAgentBenchmark(body, comparison.benchmark));
218
219
  }
219
220
  }
220
221
 
221
222
  const result = index.locateLiteral(literal, locateOpts);
222
223
  const payload = formatLiteralLocate(result);
224
+ const body = formatLiteralLocateText(payload);
223
225
  if (benchmark && !localRepoRoot(repo)) {
224
- return toolText(JSON.stringify(withBenchmarkSkipped(payload)));
226
+ return toolText(withBenchmarkSkippedNote(body));
225
227
  }
226
- return toolText(JSON.stringify(payload));
228
+ return toolText(body);
227
229
  } catch (err) {
228
230
  return toolText(err instanceof Error ? err.message : String(err));
229
231
  }
@@ -277,7 +279,7 @@ export function createMcpServer(
277
279
  );
278
280
  }
279
281
  return toolText(
280
- JSON.stringify(
282
+ formatExpandResultsText(
281
283
  formatExpandResults(filePath, anchorLine, anchor, expanded, {
282
284
  repoRoot,
283
285
  before: beforeCount,
@@ -327,12 +329,10 @@ export function createMcpServer(
327
329
  }
328
330
  const results = await index.findRelated(chunk, clampMcpTopK(topK));
329
331
  if (results.length === 0) {
330
- return toolText(
331
- JSON.stringify({ error: `No related chunks found for ${filePath}:${anchorLine}.` }),
332
- );
332
+ return toolText(`No related chunks found for ${filePath}:${anchorLine}.`);
333
333
  }
334
334
  return toolText(
335
- JSON.stringify(
335
+ formatResultsText(
336
336
  formatResults(`Chunks related to ${filePath}:${anchorLine}`, results, {
337
337
  repoRoot,
338
338
  snippet: true,
package/src/prompt.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
+ import * as readline from "node:readline/promises";
2
3
 
3
4
  export interface HiddenPromptState {
4
5
  value: string;
@@ -60,6 +61,18 @@ export function applyHiddenPromptChar(
60
61
  };
61
62
  }
62
63
 
64
+ /** Visible-input prompt (not for secrets — use promptHidden for those). */
65
+ export async function promptText(message: string, defaultValue = ""): Promise<string> {
66
+ const rl = readline.createInterface({ input, output });
67
+ try {
68
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
69
+ const answer = (await rl.question(`${message}${suffix}: `)).trim();
70
+ return answer || defaultValue;
71
+ } finally {
72
+ rl.close();
73
+ }
74
+ }
75
+
63
76
  export async function promptHidden(message: string): Promise<string> {
64
77
  if (!input.isTTY) {
65
78
  throw new Error(
package/src/setup.ts CHANGED
@@ -1,14 +1,22 @@
1
- import { divider, fail, hint, info, printBrandBanner, success, writeStderr } from "./cli-ui.ts";
1
+ import { divider, fail, hint, info, printBrandBanner, success, writeStdout } from "./cli-ui.ts";
2
2
  import {
3
3
  clearStoredCredentials,
4
4
  loadStoredCredentials,
5
5
  readStoredCredentials,
6
6
  resolveCredentialsPath,
7
+ type StoredSageMakerCredentials,
7
8
  saveStoredCredentials,
9
+ saveStoredSageMakerCredentials,
8
10
  } from "./credentials.ts";
11
+ import {
12
+ isSageMakerConfigured,
13
+ parseSageMakerEndpointArn,
14
+ type SageMakerEmbeddingConfig,
15
+ validateSageMakerConnection,
16
+ } from "./embeddings/sagemaker.ts";
9
17
  import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
10
18
  import { hasTakaraApiKeyInEnv, resolveEmbeddingApiKey } from "./env.ts";
11
- import { promptHidden } from "./prompt.ts";
19
+ import { promptHidden, promptText } from "./prompt.ts";
12
20
  import { Spinner } from "./spinner.ts";
13
21
 
14
22
  async function promptApiKey(): Promise<string> {
@@ -26,6 +34,9 @@ export interface RunSetupOptions {
26
34
  apiKey?: string;
27
35
  force?: boolean;
28
36
  skipValidation?: boolean;
37
+ sagemaker?: boolean;
38
+ sagemakerArn?: string;
39
+ profile?: string;
29
40
  }
30
41
 
31
42
  export interface RunSetupResult {
@@ -33,7 +44,165 @@ export interface RunSetupResult {
33
44
  newlySaved: boolean;
34
45
  }
35
46
 
47
+ export interface ParsedSetupCliArgs {
48
+ apiKey?: string;
49
+ force: boolean;
50
+ clear: boolean;
51
+ sagemaker: boolean;
52
+ sagemakerArn?: string;
53
+ profile?: string;
54
+ }
55
+
56
+ export type SetupCliArgError = "clear_with_key" | "sagemaker_with_key";
57
+
58
+ /** Parse `miru setup` argv (everything after the `setup` command). */
59
+ export function parseSetupCliArgs(rest: string[]): {
60
+ args: ParsedSetupCliArgs;
61
+ error?: SetupCliArgError;
62
+ } {
63
+ let apiKey: string | undefined;
64
+ let force = false;
65
+ let clear = false;
66
+ let sagemaker = false;
67
+ let sagemakerArn: string | undefined;
68
+ let profile: string | undefined;
69
+
70
+ for (let i = 0; i < rest.length; i++) {
71
+ const arg = rest[i];
72
+ if (arg === "--force") {
73
+ force = true;
74
+ } else if (arg === "--clear") {
75
+ clear = true;
76
+ } else if ((arg === "--key" || arg === "-k") && rest[i + 1]) {
77
+ apiKey = rest[++i];
78
+ } else if (arg === "--sagemaker") {
79
+ sagemaker = true;
80
+ } else if (arg === "--arn" && rest[i + 1]) {
81
+ sagemakerArn = rest[++i];
82
+ } else if (arg === "--profile" && rest[i + 1]) {
83
+ profile = rest[++i];
84
+ }
85
+ }
86
+
87
+ if (sagemakerArn) {
88
+ sagemaker = true;
89
+ }
90
+
91
+ const args: ParsedSetupCliArgs = {
92
+ apiKey,
93
+ force,
94
+ clear,
95
+ sagemaker,
96
+ sagemakerArn,
97
+ profile,
98
+ };
99
+
100
+ if (clear && apiKey) {
101
+ return { args, error: "clear_with_key" };
102
+ }
103
+ if (sagemaker && apiKey) {
104
+ return { args, error: "sagemaker_with_key" };
105
+ }
106
+ return { args };
107
+ }
108
+
109
+ async function promptSageMakerArn(): Promise<string> {
110
+ while (true) {
111
+ const arn = await promptText("SageMaker endpoint ARN");
112
+ if (!arn) {
113
+ fail("Endpoint ARN cannot be empty.");
114
+ continue;
115
+ }
116
+ try {
117
+ parseSageMakerEndpointArn(arn);
118
+ return arn;
119
+ } catch (err) {
120
+ fail(err instanceof Error ? err.message : String(err));
121
+ }
122
+ }
123
+ }
124
+
125
+ async function promptAwsProfile(): Promise<string> {
126
+ while (true) {
127
+ const profile = await promptText(
128
+ "AWS profile name (must already exist in ~/.aws)",
129
+ process.env.AWS_PROFILE || "miru",
130
+ );
131
+ if (profile) {
132
+ return profile;
133
+ }
134
+ fail("AWS profile name cannot be empty.");
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Miru only ever inherits AWS credentials — it never creates, writes, or rotates an
140
+ * AWS profile. Set one up yourself first (e.g. `aws configure --profile miru`).
141
+ */
142
+ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<RunSetupResult> {
143
+ writeStdout("");
144
+ printBrandBanner(process.stderr);
145
+ divider("─", 48, process.stderr);
146
+ writeStdout("Miru will connect directly to your self-hosted SageMaker embedding endpoint.");
147
+ hint("Miru only inherits AWS credentials from a profile you've already configured —");
148
+ hint("it never creates or writes to ~/.aws. Run `aws configure --profile <name>` first.");
149
+ hint("This replaces any stored Takara API key — only one embedding mode is active at a time.");
150
+ writeStdout("");
151
+
152
+ const arnInput = options.sagemakerArn ?? (await promptSageMakerArn());
153
+ const parsed = parseSageMakerEndpointArn(arnInput);
154
+
155
+ let profile = options.profile;
156
+ if (!profile) {
157
+ if (!canPromptForCredentials()) {
158
+ throw new Error(
159
+ "An AWS profile name is required. Pass --profile <name>, or run " +
160
+ "`miru setup --sagemaker` interactively.",
161
+ );
162
+ }
163
+ profile = await promptAwsProfile();
164
+ }
165
+
166
+ process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = arnInput;
167
+ process.env.AWS_PROFILE = profile;
168
+
169
+ if (!options.skipValidation) {
170
+ const spinner = new Spinner(`Validating SageMaker endpoint (profile "${profile}")`);
171
+ spinner.start();
172
+ const config: SageMakerEmbeddingConfig = {
173
+ endpointName: parsed.endpointName,
174
+ region: parsed.region,
175
+ normalize: true,
176
+ truncate: true,
177
+ truncationDirection: "Right",
178
+ };
179
+ const result = await validateSageMakerConnection(config);
180
+ if (!result.valid) {
181
+ spinner.stop();
182
+ throw new Error(result.message);
183
+ }
184
+ spinner.succeed("SageMaker endpoint validated");
185
+ }
186
+
187
+ const stored: StoredSageMakerCredentials = { endpoint_arn: arnInput, profile };
188
+ const hadTakaraKey = Boolean((await readStoredCredentials())?.takara_api_key);
189
+ const path = await saveStoredSageMakerCredentials(stored);
190
+ writeStdout("");
191
+ success(`Saved SageMaker config to ${path}`);
192
+ if (hadTakaraKey) {
193
+ hint("Removed the stored Takara API key — Miru now embeds only via SageMaker.");
194
+ } else {
195
+ hint("Miru will embed via this SageMaker endpoint (Takara is not used).");
196
+ }
197
+ writeStdout("");
198
+ return { path, newlySaved: true };
199
+ }
200
+
36
201
  export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupResult> {
202
+ if (options.sagemaker || options.sagemakerArn) {
203
+ return runSageMakerSetup(options);
204
+ }
205
+
37
206
  if (!options.force && hasTakaraApiKeyInEnv()) {
38
207
  const path = resolveCredentialsPath();
39
208
  const stored = await readStoredCredentials();
@@ -47,20 +216,23 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
47
216
 
48
217
  if (!options.force) {
49
218
  const stored = await readStoredCredentials();
50
- if (stored && !options.apiKey) {
219
+ if (stored?.takara_api_key && !options.apiKey) {
51
220
  info(`API key already stored at ${resolveCredentialsPath()}. Use --force to replace.`);
52
221
  process.env.TAKARA_API_KEY = stored.takara_api_key;
53
222
  return { path: resolveCredentialsPath(), newlySaved: false };
54
223
  }
55
224
  }
56
225
 
57
- writeStderr("");
226
+ writeStdout("");
58
227
  // stderr banner: setup runs before stdout may be a TTY (e.g. piped miru search).
59
228
  printBrandBanner(process.stderr);
60
229
  divider("─", 48, process.stderr);
61
- writeStderr("Miru needs a Takara API key for code embeddings.");
230
+ writeStdout("Miru needs a Takara API key for code embeddings.");
62
231
  hint("Get a bearer token from Takara, then enter it below.");
63
- writeStderr("");
232
+ hint(
233
+ "This replaces any stored SageMaker endpoint — only one embedding mode is active at a time.",
234
+ );
235
+ writeStdout("");
64
236
 
65
237
  const apiKey = options.apiKey ?? (await promptApiKey());
66
238
 
@@ -75,22 +247,27 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
75
247
  spinner.succeed("API key validated");
76
248
  }
77
249
 
250
+ const hadSageMaker = Boolean((await readStoredCredentials())?.sagemaker);
78
251
  const path = await saveStoredCredentials(apiKey);
79
252
  process.env.TAKARA_API_KEY = apiKey;
80
- writeStderr("");
253
+ writeStdout("");
81
254
  success(`Saved credentials to ${path}`);
82
- hint("MCP loads this key from credentials.json automatically.");
83
- writeStderr("");
255
+ if (hadSageMaker) {
256
+ hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
257
+ } else {
258
+ hint("MCP loads this key from credentials.json automatically.");
259
+ }
260
+ writeStdout("");
84
261
  return { path, newlySaved: true };
85
262
  }
86
263
 
87
264
  export async function runClearCredentials(): Promise<void> {
88
265
  const { cleared, path } = await clearStoredCredentials();
89
266
  if (cleared) {
90
- success(`Removed stored API key from ${path}`);
267
+ success(`Removed stored credentials from ${path}`);
91
268
  return;
92
269
  }
93
- info(`No stored API key at ${path}`);
270
+ info(`No stored credentials at ${path}`);
94
271
  }
95
272
 
96
273
  export function canPromptForCredentials(): boolean {
@@ -98,6 +275,9 @@ export function canPromptForCredentials(): boolean {
98
275
  }
99
276
 
100
277
  export function hasCredentials(): boolean {
278
+ if (isSageMakerConfigured()) {
279
+ return true;
280
+ }
101
281
  try {
102
282
  resolveEmbeddingApiKey();
103
283
  return true;
@@ -122,7 +302,7 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
122
302
 
123
303
  const wantsPrompt = options?.interactive ?? true;
124
304
  if (wantsPrompt && canPromptForCredentials()) {
125
- writeStderr("");
305
+ writeStdout("");
126
306
  info("No Takara API key found.");
127
307
  hint("Miru needs one for embeddings — enter it below (same as `miru setup`).");
128
308
  await runSetup();
@@ -130,10 +310,10 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
130
310
  return;
131
311
  }
132
312
 
133
- writeStderr("");
313
+ writeStdout("");
134
314
  // Brand on stderr when credentials are missing in non-interactive mode (stdout may not be a TTY).
135
315
  printBrandBanner(process.stderr);
136
- writeStderr("");
316
+ writeStdout("");
137
317
 
138
318
  throw new Error(
139
319
  "Takara API key required. Run `miru setup` in a terminal, or set TAKARA_API_KEY " +