@takara-ai/miru-code 1.4.0 → 1.5.2

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/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,11 +44,179 @@ 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();
40
- if (stored) {
209
+ if (stored?.sagemaker) {
210
+ const apiKey = resolveEmbeddingApiKey();
211
+ await saveStoredCredentials(apiKey);
212
+ process.env.TAKARA_API_KEY = apiKey;
213
+ writeStdout("");
214
+ success(`Saved credentials to ${path}`);
215
+ hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
216
+ writeStdout("");
217
+ return { path, newlySaved: true };
218
+ }
219
+ if (stored?.takara_api_key) {
41
220
  info(`API key already configured (env + ${path}). Use --force to replace stored key.`);
42
221
  return { path, newlySaved: false };
43
222
  }
@@ -47,20 +226,23 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
47
226
 
48
227
  if (!options.force) {
49
228
  const stored = await readStoredCredentials();
50
- if (stored && !options.apiKey) {
229
+ if (stored?.takara_api_key && !options.apiKey) {
51
230
  info(`API key already stored at ${resolveCredentialsPath()}. Use --force to replace.`);
52
231
  process.env.TAKARA_API_KEY = stored.takara_api_key;
53
232
  return { path: resolveCredentialsPath(), newlySaved: false };
54
233
  }
55
234
  }
56
235
 
57
- writeStderr("");
236
+ writeStdout("");
58
237
  // stderr banner: setup runs before stdout may be a TTY (e.g. piped miru search).
59
238
  printBrandBanner(process.stderr);
60
239
  divider("─", 48, process.stderr);
61
- writeStderr("Miru needs a Takara API key for code embeddings.");
240
+ writeStdout("Miru needs a Takara API key for code embeddings.");
62
241
  hint("Get a bearer token from Takara, then enter it below.");
63
- writeStderr("");
242
+ hint(
243
+ "This replaces any stored SageMaker endpoint — only one embedding mode is active at a time.",
244
+ );
245
+ writeStdout("");
64
246
 
65
247
  const apiKey = options.apiKey ?? (await promptApiKey());
66
248
 
@@ -75,22 +257,27 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
75
257
  spinner.succeed("API key validated");
76
258
  }
77
259
 
260
+ const hadSageMaker = Boolean((await readStoredCredentials())?.sagemaker);
78
261
  const path = await saveStoredCredentials(apiKey);
79
262
  process.env.TAKARA_API_KEY = apiKey;
80
- writeStderr("");
263
+ writeStdout("");
81
264
  success(`Saved credentials to ${path}`);
82
- hint("MCP loads this key from credentials.json automatically.");
83
- writeStderr("");
265
+ if (hadSageMaker) {
266
+ hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
267
+ } else {
268
+ hint("MCP loads this key from credentials.json automatically.");
269
+ }
270
+ writeStdout("");
84
271
  return { path, newlySaved: true };
85
272
  }
86
273
 
87
274
  export async function runClearCredentials(): Promise<void> {
88
275
  const { cleared, path } = await clearStoredCredentials();
89
276
  if (cleared) {
90
- success(`Removed stored API key from ${path}`);
277
+ success(`Removed stored credentials from ${path}`);
91
278
  return;
92
279
  }
93
- info(`No stored API key at ${path}`);
280
+ info(`No stored credentials at ${path}`);
94
281
  }
95
282
 
96
283
  export function canPromptForCredentials(): boolean {
@@ -98,6 +285,9 @@ export function canPromptForCredentials(): boolean {
98
285
  }
99
286
 
100
287
  export function hasCredentials(): boolean {
288
+ if (isSageMakerConfigured()) {
289
+ return true;
290
+ }
101
291
  try {
102
292
  resolveEmbeddingApiKey();
103
293
  return true;
@@ -122,7 +312,7 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
122
312
 
123
313
  const wantsPrompt = options?.interactive ?? true;
124
314
  if (wantsPrompt && canPromptForCredentials()) {
125
- writeStderr("");
315
+ writeStdout("");
126
316
  info("No Takara API key found.");
127
317
  hint("Miru needs one for embeddings — enter it below (same as `miru setup`).");
128
318
  await runSetup();
@@ -130,10 +320,10 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
130
320
  return;
131
321
  }
132
322
 
133
- writeStderr("");
323
+ writeStdout("");
134
324
  // Brand on stderr when credentials are missing in non-interactive mode (stdout may not be a TTY).
135
325
  printBrandBanner(process.stderr);
136
- writeStderr("");
326
+ writeStdout("");
137
327
 
138
328
  throw new Error(
139
329
  "Takara API key required. Run `miru setup` in a terminal, or set TAKARA_API_KEY " +