@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.
@@ -0,0 +1,119 @@
1
+ import { hint, info, warn, writeStderr } from "../cli-ui.ts";
2
+ import { validateEmbeddingApiKey } from "../embeddings/validate.ts";
3
+ import { promptConfirm } from "../installer/prompt.ts";
4
+ import { promptHidden } from "../prompt.ts";
5
+ import { Spinner } from "../spinner.ts";
6
+ import {
7
+ openBrowserForDeviceLogin,
8
+ pollDeviceAuthorization,
9
+ startDeviceAuthorization,
10
+ } from "./device.ts";
11
+ import type { AuthenticatedCredentials } from "./types.ts";
12
+
13
+ export type AuthMode = "api_key" | "device_code";
14
+
15
+ export interface AuthenticateOptions {
16
+ apiKey?: string;
17
+ device?: boolean;
18
+ skipValidation?: boolean;
19
+ allowManualFallback?: boolean;
20
+ interactive?: boolean;
21
+ }
22
+
23
+ async function promptApiKey(): Promise<string> {
24
+ let key = "";
25
+ while (!key) {
26
+ key = await promptHidden("Takara API key (input hidden): ", process.stderr);
27
+ if (!key) {
28
+ warn("API key cannot be empty.");
29
+ }
30
+ }
31
+ return key;
32
+ }
33
+
34
+ async function resolveAuthMode(options: AuthenticateOptions): Promise<AuthMode> {
35
+ if (options.apiKey) {
36
+ return "api_key";
37
+ }
38
+ if (options.device) {
39
+ return "device_code";
40
+ }
41
+ if (!options.interactive) {
42
+ throw new Error("Choose an auth mode with `miru setup --device` or `miru setup --key TOKEN`.");
43
+ }
44
+ writeStderr("");
45
+ info("Choose how to authenticate.");
46
+ const useDevice = await promptConfirm("Use device code login?", true);
47
+ return useDevice ? "device_code" : "api_key";
48
+ }
49
+
50
+ async function authenticateWithApiKey(
51
+ options: AuthenticateOptions,
52
+ ): Promise<AuthenticatedCredentials> {
53
+ const apiKey = options.apiKey ?? (await promptApiKey());
54
+ if (!options.skipValidation) {
55
+ const spinner = new Spinner("Validating API key");
56
+ spinner.start();
57
+ const result = await validateEmbeddingApiKey({ apiKey });
58
+ if (!result.valid) {
59
+ spinner.stop();
60
+ throw new Error(result.message);
61
+ }
62
+ spinner.succeed("API key validated");
63
+ }
64
+ return { kind: "api_key", apiKey };
65
+ }
66
+
67
+ async function authenticateWithDeviceCode(
68
+ options: AuthenticateOptions,
69
+ ): Promise<AuthenticatedCredentials> {
70
+ const start = await startDeviceAuthorization();
71
+ const verificationUrl = start.verificationUriComplete ?? start.verificationUri;
72
+ writeStderr("");
73
+ hint(`Visit ${verificationUrl}`);
74
+ if (!start.verificationUriComplete) {
75
+ hint(`Code: ${start.userCode}`);
76
+ }
77
+
78
+ const shouldOpenBrowser =
79
+ options.interactive &&
80
+ (process.env.MIRU_OPEN_BROWSER === undefined || process.env.MIRU_OPEN_BROWSER === "1");
81
+ if (shouldOpenBrowser) {
82
+ openBrowserForDeviceLogin(verificationUrl);
83
+ }
84
+
85
+ const spinner = new Spinner("Waiting for authentication");
86
+ spinner.start();
87
+ try {
88
+ const tokens = await pollDeviceAuthorization(start);
89
+ spinner.succeed("");
90
+ return {
91
+ kind: "device_code",
92
+ accessToken: tokens.accessToken,
93
+ refreshToken: tokens.refreshToken,
94
+ expiresAt: tokens.expiresAt,
95
+ tokenType: tokens.tokenType,
96
+ scope: tokens.scope,
97
+ };
98
+ } catch (err) {
99
+ spinner.stop();
100
+ if (options.allowManualFallback && options.interactive) {
101
+ warn(err instanceof Error ? err.message : String(err));
102
+ const fallback = await promptConfirm("Enter an API key instead?", true);
103
+ if (fallback) {
104
+ return authenticateWithApiKey(options);
105
+ }
106
+ }
107
+ throw err;
108
+ }
109
+ }
110
+
111
+ export async function authenticateWithProvider(
112
+ options: AuthenticateOptions,
113
+ ): Promise<AuthenticatedCredentials> {
114
+ const mode = await resolveAuthMode(options);
115
+ if (mode === "device_code") {
116
+ return authenticateWithDeviceCode(options);
117
+ }
118
+ return authenticateWithApiKey(options);
119
+ }
@@ -0,0 +1,65 @@
1
+ export const CREDENTIALS_VERSION = 2;
2
+ export const LEGACY_CREDENTIALS_VERSION = 1;
3
+
4
+ export type StoredCredentialKind = "api_key" | "device_code" | "sagemaker";
5
+
6
+ export interface StoredApiKeyCredentials {
7
+ version: typeof CREDENTIALS_VERSION;
8
+ kind: "api_key";
9
+ api_key: string;
10
+ }
11
+
12
+ export interface StoredDeviceCodeCredentials {
13
+ version: typeof CREDENTIALS_VERSION;
14
+ kind: "device_code";
15
+ access_token: string;
16
+ refresh_token?: string;
17
+ expires_at?: string;
18
+ token_type?: string;
19
+ scope?: string;
20
+ }
21
+
22
+ /** Self-hosted SageMaker embedding endpoint — mutually exclusive with Takara token auth. */
23
+ export interface StoredSageMakerCredentials {
24
+ version: typeof CREDENTIALS_VERSION;
25
+ kind: "sagemaker";
26
+ endpoint_arn: string;
27
+ profile?: string;
28
+ }
29
+
30
+ export type StoredCredentials =
31
+ | StoredApiKeyCredentials
32
+ | StoredDeviceCodeCredentials
33
+ | StoredSageMakerCredentials;
34
+
35
+ /** Pre-v2 credentials.json shape, from before device-code auth and the `kind` discriminator existed. */
36
+ export interface LegacyStoredCredentials {
37
+ version: typeof LEGACY_CREDENTIALS_VERSION;
38
+ takara_api_key?: string;
39
+ sagemaker?: { endpoint_arn: string; profile?: string };
40
+ }
41
+
42
+ export type SaveStoredCredentialsInput =
43
+ | string
44
+ | { kind: "api_key"; apiKey: string }
45
+ | {
46
+ kind: "device_code";
47
+ accessToken: string;
48
+ refreshToken?: string;
49
+ expiresAt?: string;
50
+ tokenType?: string;
51
+ scope?: string;
52
+ }
53
+ | { kind: "sagemaker"; endpointArn: string; profile?: string };
54
+
55
+ export type AuthenticatedCredentials = Exclude<
56
+ SaveStoredCredentialsInput,
57
+ string | { kind: "sagemaker"; endpointArn: string; profile?: string }
58
+ >;
59
+
60
+ /** Token to hydrate TAKARA_API_KEY with. Not defined for SageMaker (AWS-profile auth, no bearer token). */
61
+ export function credentialAccessToken(
62
+ credentials: StoredApiKeyCredentials | StoredDeviceCodeCredentials,
63
+ ): string {
64
+ return credentials.kind === "api_key" ? credentials.api_key : credentials.access_token;
65
+ }
package/src/cli.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  info,
12
12
  prefersJsonOutput,
13
13
  success,
14
+ warn,
14
15
  writeStdout,
15
16
  } from "./cli-ui.ts";
16
17
  import { loadStoredCredentials } from "./credentials.ts";
@@ -61,7 +62,13 @@ process.title = "miru";
61
62
 
62
63
  await loadEnvFiles();
63
64
  normalizeTakaraApiKeyEnv();
64
- await loadStoredCredentials();
65
+ // Soft-fail: expired/revoked device refresh must not brick `miru -v`, `setup --clear`, etc.
66
+ // Command paths call ensureCredentials() which recovers interactively when allowed.
67
+ try {
68
+ await loadStoredCredentials();
69
+ } catch {
70
+ // Leave recovery to ensureCredentials / setup.
71
+ }
65
72
 
66
73
  const CLI_COMMANDS = new Set([
67
74
  "search",
@@ -474,26 +481,35 @@ async function runCli(argv: string[]): Promise<void> {
474
481
  if (command === "setup") {
475
482
  const { args, error } = parseSetupCliArgs(rest);
476
483
  if (error === "clear_with_key") {
477
- fail("miru setup --clear cannot be combined with --key.");
484
+ fail("miru setup --clear cannot be combined with --key, --device, or --sagemaker.");
478
485
  process.exit(1);
479
486
  }
480
487
  if (error === "sagemaker_with_key") {
481
488
  fail("miru setup --sagemaker cannot be combined with --key.");
482
489
  process.exit(1);
483
490
  }
491
+ if (error === "device_with_key") {
492
+ fail("miru setup accepts either --device or --key TOKEN, not both.");
493
+ process.exit(1);
494
+ }
495
+ if (error === "device_with_sagemaker") {
496
+ fail("miru setup --device cannot be combined with --sagemaker.");
497
+ process.exit(1);
498
+ }
484
499
  if (args.clear) {
485
500
  await runClearCredentials();
486
501
  return;
487
502
  }
488
503
  const { newlySaved } = await runSetup({
489
504
  apiKey: args.apiKey,
505
+ device: args.device,
490
506
  force: args.force,
491
507
  sagemaker: args.sagemaker,
492
508
  sagemakerArn: args.sagemakerArn,
493
509
  profile: args.profile,
494
510
  });
495
511
  if (newlySaved) {
496
- const offerInstall = canPromptForCredentials() && !args.apiKey && !args.force;
512
+ const offerInstall = canPromptForCredentials() && !args.apiKey && !args.device && !args.force;
497
513
  if (offerInstall) {
498
514
  const install = await promptConfirm("Configure Miru in your coding agent now?");
499
515
  if (install) {
@@ -501,7 +517,7 @@ async function runCli(argv: string[]): Promise<void> {
501
517
  return;
502
518
  }
503
519
  }
504
- hint("Run `miru install` to add Miru to your IDE.");
520
+ hint("To add Miru to your IDE, run `miru install`.");
505
521
  }
506
522
  return;
507
523
  }
@@ -675,7 +691,20 @@ async function runMcp(argv: string[]): Promise<void> {
675
691
  }
676
692
 
677
693
  async function runMcpWithCredentials(argv: string[]): Promise<void> {
678
- await ensureCredentials({ interactive: false });
694
+ // The MCP server is spawned as a headless stdio subprocess, never a real
695
+ // login terminal, so it must never crash the whole process over missing
696
+ // credentials — that would kill every tool, not just the ones needing
697
+ // embeddings. Best-effort load/refresh whatever's already stored; leave it
698
+ // to individual tool calls to report a missing-credentials error, and to
699
+ // the `auth` tool (src/mcp/auth-tool.ts) to complete device-code login
700
+ // in-band without a terminal.
701
+ await loadStoredCredentials().catch((err: unknown) => {
702
+ // Swallow so a stale/revoked refresh token etc. can't crash the server — but
703
+ // don't swallow silently, or the only symptom is a later "credentials
704
+ // required" tool error with no clue why stored credentials didn't work.
705
+ warn(`Could not load stored credentials: ${err instanceof Error ? err.message : String(err)}`);
706
+ return false;
707
+ });
679
708
  await runMcp(argv);
680
709
  }
681
710
 
@@ -1,19 +1,23 @@
1
- import { chmod, mkdir } from "node:fs/promises";
1
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { deviceCredentialsNeedRefresh, refreshDeviceAuthorization } from "./auth/device.ts";
4
+ import {
5
+ CREDENTIALS_VERSION,
6
+ credentialAccessToken,
7
+ LEGACY_CREDENTIALS_VERSION,
8
+ type LegacyStoredCredentials,
9
+ type SaveStoredCredentialsInput,
10
+ type StoredCredentials,
11
+ type StoredSageMakerCredentials,
12
+ } from "./auth/types.ts";
3
13
  import { hasTakaraApiKeyInEnv, normalizeTakaraApiKeyEnv } from "./env.ts";
4
14
 
5
15
  const CREDENTIALS_FILENAME = "credentials.json";
6
- const CREDENTIALS_VERSION = 1;
16
+ let activeStoredToken: string | null = null;
7
17
 
8
- export interface StoredSageMakerCredentials {
9
- endpoint_arn: string;
10
- profile?: string;
11
- }
12
-
13
- interface StoredCredentials {
14
- version: number;
15
- takara_api_key?: string;
16
- sagemaker?: StoredSageMakerCredentials;
18
+ export function setStoredCredentialsEnvToken(token: string): void {
19
+ activeStoredToken = token;
20
+ process.env.TAKARA_API_KEY = token;
17
21
  }
18
22
 
19
23
  export function resolveCredentialsDir(): string {
@@ -51,11 +55,44 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
51
55
  return null;
52
56
  }
53
57
  try {
54
- const parsed = JSON.parse(await Bun.file(path).text()) as StoredCredentials;
55
- if (parsed.version !== CREDENTIALS_VERSION || (!parsed.takara_api_key && !parsed.sagemaker)) {
58
+ const parsed = JSON.parse(await readFile(path, "utf-8")) as
59
+ | StoredCredentials
60
+ | LegacyStoredCredentials;
61
+ if (parsed.version === LEGACY_CREDENTIALS_VERSION) {
62
+ if (parsed.sagemaker) {
63
+ return {
64
+ version: CREDENTIALS_VERSION,
65
+ kind: "sagemaker",
66
+ endpoint_arn: parsed.sagemaker.endpoint_arn,
67
+ profile: parsed.sagemaker.profile,
68
+ };
69
+ }
70
+ if (parsed.takara_api_key?.trim()) {
71
+ return {
72
+ version: CREDENTIALS_VERSION,
73
+ kind: "api_key",
74
+ api_key: parsed.takara_api_key,
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+ if (parsed.version !== CREDENTIALS_VERSION || !("kind" in parsed)) {
56
80
  return null;
57
81
  }
58
- return parsed;
82
+ if (parsed.kind === "api_key" && parsed.api_key?.trim()) {
83
+ return parsed;
84
+ }
85
+ if (parsed.kind === "sagemaker" && parsed.endpoint_arn?.trim()) {
86
+ return parsed;
87
+ }
88
+ if (parsed.kind === "device_code") {
89
+ if (!parsed.access_token?.trim()) {
90
+ // Treat incomplete device files as absent so `setup --clear` and recovery can proceed.
91
+ return null;
92
+ }
93
+ return parsed;
94
+ }
95
+ return null;
59
96
  } catch {
60
97
  return null;
61
98
  }
@@ -85,26 +122,21 @@ function hydrateSageMakerEnv(sagemaker: StoredSageMakerCredentials): void {
85
122
  export async function beginModeSwitch(to: "takara" | "sagemaker"): Promise<void> {
86
123
  const stored = await readStoredCredentials();
87
124
  if (to === "takara") {
88
- clearSageMakerEnv(stored?.sagemaker);
125
+ clearSageMakerEnv(stored?.kind === "sagemaker" ? stored : undefined);
89
126
  } else {
90
127
  clearTakaraEnv();
91
128
  }
92
129
  }
93
130
 
94
- async function writeExclusive(payload: StoredCredentials): Promise<string> {
95
- const dir = resolveCredentialsDir();
96
- const path = resolveCredentialsPath();
97
- await mkdir(dir, { recursive: true, mode: 0o700 });
98
- await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
99
- try {
100
- await chmod(path, 0o600);
101
- } catch {
102
- // Windows may not support Unix mode bits on all filesystems.
103
- }
104
- return path;
131
+ function envUsesStoredToken(): boolean {
132
+ return activeStoredToken !== null && process.env.TAKARA_API_KEY === activeStoredToken;
133
+ }
134
+
135
+ function markLoadedToken(token: string): void {
136
+ setStoredCredentialsEnvToken(token);
105
137
  }
106
138
 
107
- /** Apply credentials.json as the sole mode — never leave Takara and SageMaker both active. */
139
+ /** Hydrate TAKARA_API_KEY / SageMaker env vars from the credentials file when env is unset. */
108
140
  export async function loadStoredCredentials(): Promise<boolean> {
109
141
  normalizeTakaraApiKeyEnv();
110
142
  const stored = await readStoredCredentials();
@@ -112,23 +144,10 @@ export async function loadStoredCredentials(): Promise<boolean> {
112
144
  return false;
113
145
  }
114
146
 
115
- if (stored.takara_api_key) {
116
- let changed = false;
117
- if (!hasTakaraApiKeyInEnv()) {
118
- process.env.TAKARA_API_KEY = stored.takara_api_key;
119
- changed = true;
120
- }
121
- if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN || process.env.MIRU_SAGEMAKER_ENDPOINT_NAME) {
122
- clearSageMakerEnv(stored.sagemaker);
123
- changed = true;
124
- }
125
- return changed;
126
- }
127
-
128
- if (stored.sagemaker) {
147
+ if (stored.kind === "sagemaker") {
129
148
  let changed = false;
130
149
  if (!process.env.MIRU_SAGEMAKER_ENDPOINT_ARN?.trim()) {
131
- hydrateSageMakerEnv(stored.sagemaker);
150
+ hydrateSageMakerEnv(stored);
132
151
  changed = true;
133
152
  }
134
153
  if (hasTakaraApiKeyInEnv()) {
@@ -137,26 +156,102 @@ export async function loadStoredCredentials(): Promise<boolean> {
137
156
  }
138
157
  return changed;
139
158
  }
140
- return false;
141
- }
142
159
 
143
- export async function saveStoredCredentials(apiKey: string): Promise<string> {
144
- const previous = await readStoredCredentials();
145
- const path = await writeExclusive({ version: CREDENTIALS_VERSION, takara_api_key: apiKey });
146
- process.env.TAKARA_API_KEY = apiKey;
147
- clearSageMakerEnv(previous?.sagemaker);
148
- return path;
160
+ // Takara token modes (api_key / device_code) are mutually exclusive with SageMaker.
161
+ const needsSageMakerClear = Boolean(
162
+ process.env.MIRU_SAGEMAKER_ENDPOINT_ARN || process.env.MIRU_SAGEMAKER_ENDPOINT_NAME,
163
+ );
164
+ const needsToken = !hasTakaraApiKeyInEnv() || envUsesStoredToken();
165
+
166
+ if (!needsToken && !needsSageMakerClear) {
167
+ return false;
168
+ }
169
+
170
+ if (needsSageMakerClear) {
171
+ clearSageMakerEnv();
172
+ }
173
+
174
+ if (!needsToken) {
175
+ return true;
176
+ }
177
+
178
+ if (stored.kind === "device_code" && deviceCredentialsNeedRefresh(stored)) {
179
+ const refreshed = await refreshDeviceAuthorization(stored);
180
+ await saveStoredCredentials({
181
+ kind: "device_code",
182
+ accessToken: refreshed.accessToken,
183
+ refreshToken: refreshed.refreshToken,
184
+ expiresAt: refreshed.expiresAt,
185
+ tokenType: refreshed.tokenType,
186
+ scope: refreshed.scope,
187
+ });
188
+ markLoadedToken(refreshed.accessToken);
189
+ return true;
190
+ }
191
+ markLoadedToken(credentialAccessToken(stored));
192
+ return true;
149
193
  }
150
194
 
151
- export async function saveStoredSageMakerCredentials(
152
- sagemaker: StoredSageMakerCredentials,
153
- ): Promise<string> {
154
- const path = await writeExclusive({ version: CREDENTIALS_VERSION, sagemaker });
155
- clearTakaraEnv();
156
- process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = sagemaker.endpoint_arn;
157
- if (sagemaker.profile) {
158
- process.env.AWS_PROFILE = sagemaker.profile;
195
+ export async function saveStoredCredentials(input: SaveStoredCredentialsInput): Promise<string> {
196
+ const dir = resolveCredentialsDir();
197
+ const path = resolveCredentialsPath();
198
+ await mkdir(dir, { recursive: true, mode: 0o700 });
199
+
200
+ const payload: StoredCredentials =
201
+ typeof input === "string" || input.kind === "api_key"
202
+ ? {
203
+ version: CREDENTIALS_VERSION,
204
+ kind: "api_key",
205
+ api_key: typeof input === "string" ? input : input.apiKey,
206
+ }
207
+ : input.kind === "sagemaker"
208
+ ? {
209
+ version: CREDENTIALS_VERSION,
210
+ kind: "sagemaker",
211
+ endpoint_arn: input.endpointArn,
212
+ profile: input.profile,
213
+ }
214
+ : {
215
+ version: CREDENTIALS_VERSION,
216
+ kind: "device_code",
217
+ access_token: input.accessToken,
218
+ refresh_token: input.refreshToken,
219
+ expires_at: input.expiresAt,
220
+ token_type: input.tokenType,
221
+ scope: input.scope,
222
+ };
223
+
224
+ await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, {
225
+ encoding: "utf-8",
226
+ mode: 0o600,
227
+ });
228
+ try {
229
+ await chmod(path, 0o600);
230
+ } catch {
231
+ // Windows may not support Unix mode bits on all filesystems.
232
+ }
233
+
234
+ // Takara-token and SageMaker modes are mutually exclusive — activating one mode
235
+ // unconditionally drops the other's env state, regardless of its current value.
236
+ if (payload.kind === "sagemaker") {
237
+ clearTakaraEnv();
238
+ activeStoredToken = null;
239
+ process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = payload.endpoint_arn;
240
+ if (payload.profile) {
241
+ process.env.AWS_PROFILE = payload.profile;
242
+ }
243
+ } else {
244
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
245
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_NAME;
246
+ delete process.env.MIRU_SAGEMAKER_REGION;
247
+ delete process.env.AWS_PROFILE;
248
+ // api_key saves activate immediately; device_code saves (initial login or refresh)
249
+ // leave env hydration to the caller (setup.ts / loadStoredCredentials refresh path).
250
+ if (payload.kind === "api_key") {
251
+ setStoredCredentialsEnvToken(payload.api_key);
252
+ }
159
253
  }
254
+
160
255
  return path;
161
256
  }
162
257
 
@@ -166,15 +261,28 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
166
261
  return { cleared: false, path };
167
262
  }
168
263
 
169
- const stored = await readStoredCredentials();
170
- await Bun.file(path).delete();
171
-
172
- if (stored?.takara_api_key && process.env.TAKARA_API_KEY === stored.takara_api_key) {
173
- clearTakaraEnv();
264
+ // Read for env cleanup, but never let parse errors block deletion.
265
+ let stored: StoredCredentials | null = null;
266
+ try {
267
+ stored = await readStoredCredentials();
268
+ } catch {
269
+ // Ignore parse/read errors — removal still proceeds.
174
270
  }
175
- if (stored?.sagemaker) {
176
- clearSageMakerEnv(stored.sagemaker);
271
+ await rm(path, { force: true });
272
+
273
+ if (stored) {
274
+ if (stored.kind === "sagemaker") {
275
+ if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN === stored.endpoint_arn) {
276
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
277
+ }
278
+ if (stored.profile && process.env.AWS_PROFILE === stored.profile) {
279
+ delete process.env.AWS_PROFILE;
280
+ }
281
+ } else if (process.env.TAKARA_API_KEY === credentialAccessToken(stored)) {
282
+ delete process.env.TAKARA_API_KEY;
283
+ }
177
284
  }
285
+ activeStoredToken = null;
178
286
 
179
287
  return { cleared: true, path };
180
288
  }
@@ -1,4 +1,5 @@
1
1
  import { mapPool, resolveWorkerConcurrency } from "../concurrency.ts";
2
+ import { loadStoredCredentials } from "../credentials.ts";
2
3
  import { envFirstString, envOptionalInt, resolveEmbeddingApiKey } from "../env.ts";
3
4
  import { createSageMakerClient, resolveSageMakerConfig } from "./sagemaker.ts";
4
5
 
@@ -346,8 +347,6 @@ class EmbeddingApiError extends Error {
346
347
  }
347
348
 
348
349
  function createClient(): EmbeddingClient {
349
- const apiKey = resolveEmbeddingApiKey();
350
-
351
350
  const baseUrl = resolveEmbeddingBaseUrl();
352
351
  const endpoint = `${baseUrl}/embeddings`;
353
352
 
@@ -357,6 +356,8 @@ function createClient(): EmbeddingClient {
357
356
  model: string,
358
357
  dimensions?: number,
359
358
  ): Promise<EmbeddingResponse> {
359
+ await loadStoredCredentials();
360
+ const apiKey = resolveEmbeddingApiKey();
360
361
  const body: Record<string, unknown> = { model, input };
361
362
  if (dimensions != null) {
362
363
  body.dimensions = dimensions;
package/src/env.ts CHANGED
@@ -60,7 +60,9 @@ export function resolveEmbeddingApiKey(): string {
60
60
  const key = process.env[TAKARA_API_KEY_ENV]?.trim() ?? "";
61
61
  if (!isUsableTakaraApiKey(key)) {
62
62
  throw new Error(
63
- "Takara API key required. Run `miru setup`, or set TAKARA_API_KEY in your MCP server env or .env.local.",
63
+ "Takara credentials required. If you're an agent with Miru MCP tools available, call the " +
64
+ "`auth` tool to sign in. Otherwise run `miru setup`, or set TAKARA_API_KEY in your MCP " +
65
+ "server env or .env.local.",
64
66
  );
65
67
  }
66
68
  return key;
package/src/help.ts CHANGED
@@ -37,7 +37,7 @@ export function printMainHelp(): void {
37
37
  commandRow("locate", "Exact substring location in the index");
38
38
  commandRow("expand", "Adjacent chunks in the same file as a hit");
39
39
  commandRow("find-related", "Find chunks related to a file:line");
40
- commandRow("setup", "Save your Takara API key locally");
40
+ commandRow("setup", "Authenticate with Takara and save local credentials");
41
41
  commandRow("install", "Configure miru across coding agents");
42
42
  commandRow("uninstall", "Remove miru agent configuration");
43
43
  commandRow("benchmark", "Turn MCP benchmark mode on or off");
@@ -147,13 +147,14 @@ export function printCommandHelp(command: string): void {
147
147
  writeStdout("");
148
148
  return;
149
149
  case "setup":
150
- commandHeader("setup", "Store and validate Takara or self-hosted SageMaker credentials.");
150
+ commandHeader("setup", "Authenticate with Takara or self-hosted SageMaker credentials.");
151
151
  section("Usage");
152
- writeStdout(" miru setup [--key TOKEN] [--force] [--clear]");
152
+ writeStdout(" miru setup [--device] [--key TOKEN] [--force] [--clear]");
153
153
  writeStdout(" miru setup --sagemaker --arn ENDPOINT_ARN --profile NAME");
154
154
  section("Options (Takara)");
155
- writeStdout(" --key, -k TOKEN Non-interactive key entry");
156
- writeStdout(" --force Replace an existing stored key");
155
+ writeStdout(" --device Start device-code login (default when interactive)");
156
+ writeStdout(" --key, -k TOKEN Non-interactive: store a bearer token directly");
157
+ writeStdout(" --force Replace existing stored credentials");
157
158
  writeStdout(" --clear Remove stored credentials");
158
159
  section("Options (SageMaker)");
159
160
  writeStdout(" --sagemaker Switch setup to self-hosted SageMaker mode");
@@ -170,15 +171,18 @@ export function printCommandHelp(command: string): void {
170
171
  return;
171
172
  case "install":
172
173
  commandHeader("install", "Interactive global agent setup.");
173
- writeStdout("Configures MCP server, instructions, and sub-agent files under");
174
+ writeStdout("Configures MCP server, instructions, sub-agent files, and optional");
175
+ writeStdout("experimental integrations (search hooks, Caveman skill) under");
174
176
  writeStdout("your user config (~/.claude, ~/.cursor, etc.).");
175
177
  writeStdout("");
178
+ writeStdout("Caveman = on-demand chat compression (/caveman). Off by default.");
176
179
  writeStdout("Run miru setup first, or set TAKARA_API_KEY for MCP env expansion.");
177
180
  writeStdout("");
178
181
  return;
179
182
  case "uninstall":
180
183
  commandHeader("uninstall", "Remove miru configuration from agents.");
181
- writeStdout("Removes MCP entries, marked instruction blocks, and global sub-agents.");
184
+ writeStdout("Removes MCP entries, marked instruction blocks, global sub-agents,");
185
+ writeStdout("and selected optional integrations (hooks, Caveman skill).");
182
186
  writeStdout("Also deletes the global benchmark report from Miru's state directory.");
183
187
  writeStdout("");
184
188
  return;