@takara-ai/miru-code 1.5.3 → 1.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.
package/src/setup.ts CHANGED
@@ -1,13 +1,23 @@
1
- import { divider, fail, hint, info, printBrandBanner, success, writeStdout } from "./cli-ui.ts";
1
+ import { authenticateWithProvider } from "./auth/providers.ts";
2
+ import { credentialAccessToken } from "./auth/types.ts";
3
+ import {
4
+ divider,
5
+ fail,
6
+ hint,
7
+ info,
8
+ printBrandBanner,
9
+ success,
10
+ writeStderr,
11
+ writeStdout,
12
+ } from "./cli-ui.ts";
2
13
  import {
3
14
  beginModeSwitch,
4
15
  clearStoredCredentials,
5
16
  loadStoredCredentials,
6
17
  readStoredCredentials,
7
18
  resolveCredentialsPath,
8
- type StoredSageMakerCredentials,
9
19
  saveStoredCredentials,
10
- saveStoredSageMakerCredentials,
20
+ setStoredCredentialsEnvToken,
11
21
  } from "./credentials.ts";
12
22
  import {
13
23
  isSageMakerConfigured,
@@ -15,29 +25,20 @@ import {
15
25
  type SageMakerEmbeddingConfig,
16
26
  validateSageMakerConnection,
17
27
  } from "./embeddings/sagemaker.ts";
18
- import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
19
28
  import { hasTakaraApiKeyInEnv, resolveEmbeddingApiKey } from "./env.ts";
20
- import { promptHidden, promptText } from "./prompt.ts";
29
+ import { promptText } from "./prompt.ts";
21
30
  import { Spinner } from "./spinner.ts";
22
31
 
23
- async function promptApiKey(): Promise<string> {
24
- let key = "";
25
- while (!key) {
26
- key = await promptHidden("Takara API key (input hidden): ");
27
- if (!key) {
28
- fail("API key cannot be empty.");
29
- }
30
- }
31
- return key;
32
- }
33
-
34
32
  export interface RunSetupOptions {
35
33
  apiKey?: string;
34
+ device?: boolean;
36
35
  force?: boolean;
37
36
  skipValidation?: boolean;
38
37
  sagemaker?: boolean;
39
38
  sagemakerArn?: string;
40
39
  profile?: string;
40
+ allowManualFallback?: boolean;
41
+ interactive?: boolean;
41
42
  }
42
43
 
43
44
  export interface RunSetupResult {
@@ -47,6 +48,7 @@ export interface RunSetupResult {
47
48
 
48
49
  export interface ParsedSetupCliArgs {
49
50
  apiKey?: string;
51
+ device: boolean;
50
52
  force: boolean;
51
53
  clear: boolean;
52
54
  sagemaker: boolean;
@@ -54,7 +56,11 @@ export interface ParsedSetupCliArgs {
54
56
  profile?: string;
55
57
  }
56
58
 
57
- export type SetupCliArgError = "clear_with_key" | "sagemaker_with_key";
59
+ export type SetupCliArgError =
60
+ | "clear_with_key"
61
+ | "sagemaker_with_key"
62
+ | "device_with_key"
63
+ | "device_with_sagemaker";
58
64
 
59
65
  /** Parse `miru setup` argv (everything after the `setup` command). */
60
66
  export function parseSetupCliArgs(rest: string[]): {
@@ -62,6 +68,7 @@ export function parseSetupCliArgs(rest: string[]): {
62
68
  error?: SetupCliArgError;
63
69
  } {
64
70
  let apiKey: string | undefined;
71
+ let device = false;
65
72
  let force = false;
66
73
  let clear = false;
67
74
  let sagemaker = false;
@@ -74,6 +81,8 @@ export function parseSetupCliArgs(rest: string[]): {
74
81
  force = true;
75
82
  } else if (arg === "--clear") {
76
83
  clear = true;
84
+ } else if (arg === "--device") {
85
+ device = true;
77
86
  } else if ((arg === "--key" || arg === "-k") && rest[i + 1]) {
78
87
  apiKey = rest[++i];
79
88
  } else if (arg === "--sagemaker") {
@@ -91,6 +100,7 @@ export function parseSetupCliArgs(rest: string[]): {
91
100
 
92
101
  const args: ParsedSetupCliArgs = {
93
102
  apiKey,
103
+ device,
94
104
  force,
95
105
  clear,
96
106
  sagemaker,
@@ -98,12 +108,18 @@ export function parseSetupCliArgs(rest: string[]): {
98
108
  profile,
99
109
  };
100
110
 
101
- if (clear && apiKey) {
111
+ if (clear && (apiKey || device || sagemaker)) {
102
112
  return { args, error: "clear_with_key" };
103
113
  }
104
114
  if (sagemaker && apiKey) {
105
115
  return { args, error: "sagemaker_with_key" };
106
116
  }
117
+ if (device && apiKey) {
118
+ return { args, error: "device_with_key" };
119
+ }
120
+ if (device && sagemaker) {
121
+ return { args, error: "device_with_sagemaker" };
122
+ }
107
123
  return { args };
108
124
  }
109
125
 
@@ -147,7 +163,9 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
147
163
  writeStdout("Miru will connect directly to your self-hosted SageMaker embedding endpoint.");
148
164
  hint("Miru only inherits AWS credentials from a profile you've already configured —");
149
165
  hint("it never creates or writes to ~/.aws. Run `aws configure --profile <name>` first.");
150
- hint("This replaces any stored Takara API key — only one embedding mode is active at a time.");
166
+ hint(
167
+ "This replaces any stored Takara credentials — only one embedding mode is active at a time.",
168
+ );
151
169
  writeStdout("");
152
170
 
153
171
  const arnInput = options.sagemakerArn ?? (await promptSageMakerArn());
@@ -186,13 +204,12 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
186
204
  spinner.succeed("SageMaker endpoint validated");
187
205
  }
188
206
 
189
- const stored: StoredSageMakerCredentials = { endpoint_arn: arnInput, profile };
190
- const hadTakaraKey = Boolean((await readStoredCredentials())?.takara_api_key);
191
- const path = await saveStoredSageMakerCredentials(stored);
207
+ const hadStoredCredentials = Boolean(await readStoredCredentials());
208
+ const path = await saveStoredCredentials({ kind: "sagemaker", endpointArn: arnInput, profile });
192
209
  writeStdout("");
193
210
  success(`Saved SageMaker config to ${path}`);
194
- if (hadTakaraKey) {
195
- hint("Removed the stored Takara API key — Miru now embeds only via SageMaker.");
211
+ if (hadStoredCredentials) {
212
+ hint("Removed the stored Takara credentials — Miru now embeds only via SageMaker.");
196
213
  } else {
197
214
  hint("Miru will embed via this SageMaker endpoint (Takara is not used).");
198
215
  }
@@ -205,10 +222,11 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
205
222
  return runSageMakerSetup(options);
206
223
  }
207
224
 
208
- if (!options.force && hasTakaraApiKeyInEnv()) {
225
+ const interactive = options.interactive ?? canPromptForCredentials();
226
+ if (!options.force && hasTakaraApiKeyInEnv() && !options.apiKey && !options.device) {
209
227
  const path = resolveCredentialsPath();
210
228
  const stored = await readStoredCredentials();
211
- if (stored?.sagemaker) {
229
+ if (stored?.kind === "sagemaker") {
212
230
  const saved = await saveStoredCredentials(resolveEmbeddingApiKey());
213
231
  writeStdout("");
214
232
  success(`Saved credentials to ${saved}`);
@@ -216,8 +234,10 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
216
234
  writeStdout("");
217
235
  return { path: saved, newlySaved: true };
218
236
  }
219
- if (stored?.takara_api_key) {
220
- info(`API key already configured (env + ${path}). Use --force to replace stored key.`);
237
+ if (stored) {
238
+ info(
239
+ `Credentials already configured (env + ${path}). Use --force to replace stored credentials.`,
240
+ );
221
241
  return { path, newlySaved: false };
222
242
  }
223
243
  info("API key already set via environment variable. Stored credentials unchanged.");
@@ -226,48 +246,39 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
226
246
 
227
247
  if (!options.force) {
228
248
  const stored = await readStoredCredentials();
229
- if (stored?.takara_api_key && !options.apiKey) {
230
- info(`API key already stored at ${resolveCredentialsPath()}. Use --force to replace.`);
231
- process.env.TAKARA_API_KEY = stored.takara_api_key;
249
+ if (stored && stored.kind !== "sagemaker" && !options.apiKey && !options.device) {
250
+ info(`Credentials already stored at ${resolveCredentialsPath()}. Use --force to replace.`);
251
+ setStoredCredentialsEnvToken(credentialAccessToken(stored));
232
252
  return { path: resolveCredentialsPath(), newlySaved: false };
233
253
  }
234
254
  }
235
255
 
236
- writeStdout("");
237
- // stderr banner: setup runs before stdout may be a TTY (e.g. piped miru search).
256
+ writeStderr("");
257
+ // stderr: setup/MCP must not write human auth UI to stdout (JSON-RPC / piped CLI).
238
258
  printBrandBanner(process.stderr);
239
259
  divider("─", 48, process.stderr);
240
- writeStdout("Miru needs a Takara API key for code embeddings.");
241
- hint("Get a bearer token from Takara, then enter it below.");
242
- hint(
243
- "This replaces any stored SageMaker endpoint — only one embedding mode is active at a time.",
244
- );
245
- writeStdout("");
246
-
247
- const apiKey = options.apiKey ?? (await promptApiKey());
260
+ writeStderr("");
248
261
 
249
262
  await beginModeSwitch("takara");
250
- if (!options.skipValidation) {
251
- const spinner = new Spinner("Validating API key");
252
- spinner.start();
253
- const result = await validateEmbeddingApiKey({ apiKey });
254
- if (!result.valid) {
255
- spinner.stop();
256
- throw new Error(result.message);
257
- }
258
- spinner.succeed("API key validated");
259
- }
260
263
 
261
- const hadSageMaker = Boolean((await readStoredCredentials())?.sagemaker);
262
- const path = await saveStoredCredentials(apiKey);
263
- writeStdout("");
264
- success(`Saved credentials to ${path}`);
264
+ const hadSageMaker = (await readStoredCredentials())?.kind === "sagemaker";
265
+ const credentials = await authenticateWithProvider({
266
+ apiKey: options.apiKey,
267
+ device: options.device,
268
+ skipValidation: options.skipValidation,
269
+ allowManualFallback: options.allowManualFallback ?? interactive,
270
+ interactive,
271
+ });
272
+ const path = await saveStoredCredentials(credentials);
273
+ setStoredCredentialsEnvToken(
274
+ credentials.kind === "api_key" ? credentials.apiKey : credentials.accessToken,
275
+ );
276
+ writeStderr("");
277
+ hint("Congratulations! You are now signed in.");
265
278
  if (hadSageMaker) {
266
279
  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
280
  }
270
- writeStdout("");
281
+ writeStderr("");
271
282
  return { path, newlySaved: true };
272
283
  }
273
284
 
@@ -305,28 +316,48 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
305
316
  return;
306
317
  }
307
318
 
308
- await refreshCredentialsFromStore();
319
+ const wantsPrompt = options?.interactive ?? true;
320
+ let refreshError: Error | null = null;
321
+ try {
322
+ await refreshCredentialsFromStore();
323
+ } catch (err) {
324
+ refreshError = err instanceof Error ? err : new Error(String(err));
325
+ }
309
326
  if (hasCredentials()) {
310
327
  return;
311
328
  }
312
329
 
313
- const wantsPrompt = options?.interactive ?? true;
314
- if (wantsPrompt && canPromptForCredentials()) {
315
- writeStdout("");
316
- info("No Takara API key found.");
317
- hint("Miru needs one for embeddings — enter it below (same as `miru setup`).");
318
- await runSetup();
330
+ if (wantsPrompt) {
331
+ writeStderr("");
332
+ if (refreshError) {
333
+ info(`Stored credentials could not be used: ${refreshError.message}`);
334
+ hint("Starting a fresh device-code login.");
335
+ } else {
336
+ info("No Takara credentials found.");
337
+ hint("Starting the same device-code login flow as `miru setup`.");
338
+ }
339
+ await runSetup({
340
+ device: true,
341
+ force: true,
342
+ allowManualFallback: false,
343
+ interactive: true,
344
+ });
319
345
  resolveEmbeddingApiKey();
320
346
  return;
321
347
  }
322
348
 
323
- writeStdout("");
349
+ if (refreshError) {
350
+ throw refreshError;
351
+ }
352
+
353
+ writeStderr("");
324
354
  // Brand on stderr when credentials are missing in non-interactive mode (stdout may not be a TTY).
325
355
  printBrandBanner(process.stderr);
326
- writeStdout("");
356
+ writeStderr("");
327
357
 
328
358
  throw new Error(
329
- "Takara API key required. Run `miru setup` in a terminal, or set TAKARA_API_KEY " +
330
- "in your MCP server env (Cursor mcp.json) or .env.local.",
359
+ "Takara credentials required. If you're an agent with Miru MCP tools available, call the " +
360
+ "`auth` tool to sign in. Otherwise run `miru setup` or `miru setup --key TOKEN` in an " +
361
+ "interactive terminal, or set TAKARA_API_KEY in your environment.",
331
362
  );
332
363
  }