@takara-ai/miru-code 1.5.2 → 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/README.md +78 -25
- package/docs/miru-workflow-diagram.svg +47 -0
- package/docs/self-hosted-sagemaker.md +1 -1
- package/package.json +3 -1
- package/scripts/install-git-hooks.ts +29 -0
- package/src/auth/device.ts +298 -0
- package/src/auth/providers.ts +119 -0
- package/src/auth/types.ts +65 -0
- package/src/cli.ts +34 -5
- package/src/credentials.ts +185 -71
- package/src/embeddings/openai.ts +3 -2
- package/src/embeddings/validate.ts +2 -2
- package/src/env.ts +3 -1
- package/src/help.ts +11 -7
- package/src/installer/agents.ts +13 -6
- package/src/installer/installer.ts +66 -6
- package/src/installer/search-policy.ts +1 -1
- package/src/installer/style-packs/caveman.ts +118 -0
- package/src/mcp/auth-tool.ts +131 -0
- package/src/mcp/server.ts +3 -0
- package/src/prompt.ts +8 -5
- package/src/setup.ts +107 -76
- package/tokenizer/tokenizer.json +1 -1
package/src/setup.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { authenticateWithProvider } from "./auth/providers.ts";
|
|
2
|
+
import { credentialAccessToken } from "./auth/types.ts";
|
|
2
3
|
import {
|
|
4
|
+
divider,
|
|
5
|
+
fail,
|
|
6
|
+
hint,
|
|
7
|
+
info,
|
|
8
|
+
printBrandBanner,
|
|
9
|
+
success,
|
|
10
|
+
writeStderr,
|
|
11
|
+
writeStdout,
|
|
12
|
+
} from "./cli-ui.ts";
|
|
13
|
+
import {
|
|
14
|
+
beginModeSwitch,
|
|
3
15
|
clearStoredCredentials,
|
|
4
16
|
loadStoredCredentials,
|
|
5
17
|
readStoredCredentials,
|
|
6
18
|
resolveCredentialsPath,
|
|
7
|
-
type StoredSageMakerCredentials,
|
|
8
19
|
saveStoredCredentials,
|
|
9
|
-
|
|
20
|
+
setStoredCredentialsEnvToken,
|
|
10
21
|
} from "./credentials.ts";
|
|
11
22
|
import {
|
|
12
23
|
isSageMakerConfigured,
|
|
@@ -14,29 +25,20 @@ import {
|
|
|
14
25
|
type SageMakerEmbeddingConfig,
|
|
15
26
|
validateSageMakerConnection,
|
|
16
27
|
} from "./embeddings/sagemaker.ts";
|
|
17
|
-
import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
|
|
18
28
|
import { hasTakaraApiKeyInEnv, resolveEmbeddingApiKey } from "./env.ts";
|
|
19
|
-
import {
|
|
29
|
+
import { promptText } from "./prompt.ts";
|
|
20
30
|
import { Spinner } from "./spinner.ts";
|
|
21
31
|
|
|
22
|
-
async function promptApiKey(): Promise<string> {
|
|
23
|
-
let key = "";
|
|
24
|
-
while (!key) {
|
|
25
|
-
key = await promptHidden("Takara API key (input hidden): ");
|
|
26
|
-
if (!key) {
|
|
27
|
-
fail("API key cannot be empty.");
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return key;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
32
|
export interface RunSetupOptions {
|
|
34
33
|
apiKey?: string;
|
|
34
|
+
device?: boolean;
|
|
35
35
|
force?: boolean;
|
|
36
36
|
skipValidation?: boolean;
|
|
37
37
|
sagemaker?: boolean;
|
|
38
38
|
sagemakerArn?: string;
|
|
39
39
|
profile?: string;
|
|
40
|
+
allowManualFallback?: boolean;
|
|
41
|
+
interactive?: boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface RunSetupResult {
|
|
@@ -46,6 +48,7 @@ export interface RunSetupResult {
|
|
|
46
48
|
|
|
47
49
|
export interface ParsedSetupCliArgs {
|
|
48
50
|
apiKey?: string;
|
|
51
|
+
device: boolean;
|
|
49
52
|
force: boolean;
|
|
50
53
|
clear: boolean;
|
|
51
54
|
sagemaker: boolean;
|
|
@@ -53,7 +56,11 @@ export interface ParsedSetupCliArgs {
|
|
|
53
56
|
profile?: string;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
export type SetupCliArgError =
|
|
59
|
+
export type SetupCliArgError =
|
|
60
|
+
| "clear_with_key"
|
|
61
|
+
| "sagemaker_with_key"
|
|
62
|
+
| "device_with_key"
|
|
63
|
+
| "device_with_sagemaker";
|
|
57
64
|
|
|
58
65
|
/** Parse `miru setup` argv (everything after the `setup` command). */
|
|
59
66
|
export function parseSetupCliArgs(rest: string[]): {
|
|
@@ -61,6 +68,7 @@ export function parseSetupCliArgs(rest: string[]): {
|
|
|
61
68
|
error?: SetupCliArgError;
|
|
62
69
|
} {
|
|
63
70
|
let apiKey: string | undefined;
|
|
71
|
+
let device = false;
|
|
64
72
|
let force = false;
|
|
65
73
|
let clear = false;
|
|
66
74
|
let sagemaker = false;
|
|
@@ -73,6 +81,8 @@ export function parseSetupCliArgs(rest: string[]): {
|
|
|
73
81
|
force = true;
|
|
74
82
|
} else if (arg === "--clear") {
|
|
75
83
|
clear = true;
|
|
84
|
+
} else if (arg === "--device") {
|
|
85
|
+
device = true;
|
|
76
86
|
} else if ((arg === "--key" || arg === "-k") && rest[i + 1]) {
|
|
77
87
|
apiKey = rest[++i];
|
|
78
88
|
} else if (arg === "--sagemaker") {
|
|
@@ -90,6 +100,7 @@ export function parseSetupCliArgs(rest: string[]): {
|
|
|
90
100
|
|
|
91
101
|
const args: ParsedSetupCliArgs = {
|
|
92
102
|
apiKey,
|
|
103
|
+
device,
|
|
93
104
|
force,
|
|
94
105
|
clear,
|
|
95
106
|
sagemaker,
|
|
@@ -97,12 +108,18 @@ export function parseSetupCliArgs(rest: string[]): {
|
|
|
97
108
|
profile,
|
|
98
109
|
};
|
|
99
110
|
|
|
100
|
-
if (clear && apiKey) {
|
|
111
|
+
if (clear && (apiKey || device || sagemaker)) {
|
|
101
112
|
return { args, error: "clear_with_key" };
|
|
102
113
|
}
|
|
103
114
|
if (sagemaker && apiKey) {
|
|
104
115
|
return { args, error: "sagemaker_with_key" };
|
|
105
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
|
+
}
|
|
106
123
|
return { args };
|
|
107
124
|
}
|
|
108
125
|
|
|
@@ -146,7 +163,9 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
|
|
|
146
163
|
writeStdout("Miru will connect directly to your self-hosted SageMaker embedding endpoint.");
|
|
147
164
|
hint("Miru only inherits AWS credentials from a profile you've already configured —");
|
|
148
165
|
hint("it never creates or writes to ~/.aws. Run `aws configure --profile <name>` first.");
|
|
149
|
-
hint(
|
|
166
|
+
hint(
|
|
167
|
+
"This replaces any stored Takara credentials — only one embedding mode is active at a time.",
|
|
168
|
+
);
|
|
150
169
|
writeStdout("");
|
|
151
170
|
|
|
152
171
|
const arnInput = options.sagemakerArn ?? (await promptSageMakerArn());
|
|
@@ -163,6 +182,7 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
|
|
|
163
182
|
profile = await promptAwsProfile();
|
|
164
183
|
}
|
|
165
184
|
|
|
185
|
+
await beginModeSwitch("sagemaker");
|
|
166
186
|
process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = arnInput;
|
|
167
187
|
process.env.AWS_PROFILE = profile;
|
|
168
188
|
|
|
@@ -184,13 +204,12 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
|
|
|
184
204
|
spinner.succeed("SageMaker endpoint validated");
|
|
185
205
|
}
|
|
186
206
|
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const path = await saveStoredSageMakerCredentials(stored);
|
|
207
|
+
const hadStoredCredentials = Boolean(await readStoredCredentials());
|
|
208
|
+
const path = await saveStoredCredentials({ kind: "sagemaker", endpointArn: arnInput, profile });
|
|
190
209
|
writeStdout("");
|
|
191
210
|
success(`Saved SageMaker config to ${path}`);
|
|
192
|
-
if (
|
|
193
|
-
hint("Removed the stored Takara
|
|
211
|
+
if (hadStoredCredentials) {
|
|
212
|
+
hint("Removed the stored Takara credentials — Miru now embeds only via SageMaker.");
|
|
194
213
|
} else {
|
|
195
214
|
hint("Miru will embed via this SageMaker endpoint (Takara is not used).");
|
|
196
215
|
}
|
|
@@ -203,21 +222,22 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
|
|
|
203
222
|
return runSageMakerSetup(options);
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
|
|
225
|
+
const interactive = options.interactive ?? canPromptForCredentials();
|
|
226
|
+
if (!options.force && hasTakaraApiKeyInEnv() && !options.apiKey && !options.device) {
|
|
207
227
|
const path = resolveCredentialsPath();
|
|
208
228
|
const stored = await readStoredCredentials();
|
|
209
|
-
if (stored?.sagemaker) {
|
|
210
|
-
const
|
|
211
|
-
await saveStoredCredentials(apiKey);
|
|
212
|
-
process.env.TAKARA_API_KEY = apiKey;
|
|
229
|
+
if (stored?.kind === "sagemaker") {
|
|
230
|
+
const saved = await saveStoredCredentials(resolveEmbeddingApiKey());
|
|
213
231
|
writeStdout("");
|
|
214
|
-
success(`Saved credentials to ${
|
|
232
|
+
success(`Saved credentials to ${saved}`);
|
|
215
233
|
hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
|
|
216
234
|
writeStdout("");
|
|
217
|
-
return { path, newlySaved: true };
|
|
235
|
+
return { path: saved, newlySaved: true };
|
|
218
236
|
}
|
|
219
|
-
if (stored
|
|
220
|
-
info(
|
|
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
|
|
230
|
-
info(`
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
// stderr
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
260
|
+
writeStderr("");
|
|
261
|
+
|
|
262
|
+
await beginModeSwitch("takara");
|
|
263
|
+
|
|
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,
|
|
244
275
|
);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const apiKey = options.apiKey ?? (await promptApiKey());
|
|
248
|
-
|
|
249
|
-
if (!options.skipValidation) {
|
|
250
|
-
const spinner = new Spinner("Validating API key");
|
|
251
|
-
spinner.start();
|
|
252
|
-
const result = await validateEmbeddingApiKey({ apiKey });
|
|
253
|
-
if (!result.valid) {
|
|
254
|
-
spinner.stop();
|
|
255
|
-
throw new Error(result.message);
|
|
256
|
-
}
|
|
257
|
-
spinner.succeed("API key validated");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const hadSageMaker = Boolean((await readStoredCredentials())?.sagemaker);
|
|
261
|
-
const path = await saveStoredCredentials(apiKey);
|
|
262
|
-
process.env.TAKARA_API_KEY = apiKey;
|
|
263
|
-
writeStdout("");
|
|
264
|
-
success(`Saved credentials to ${path}`);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
+
writeStderr("");
|
|
327
357
|
|
|
328
358
|
throw new Error(
|
|
329
|
-
"Takara
|
|
330
|
-
"
|
|
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
|
}
|