@takara-ai/miru-code 1.5.3 → 1.7.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/README.md +102 -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 +306 -0
- package/src/auth/errors.ts +24 -0
- package/src/auth/providers.ts +115 -0
- package/src/auth/types.ts +65 -0
- package/src/cli.ts +59 -8
- package/src/credentials.ts +173 -65
- package/src/embeddings/openai.ts +3 -2
- package/src/env.ts +6 -2
- package/src/help.ts +29 -13
- package/src/installer/agents.ts +118 -16
- package/src/installer/caveman-shared.ts +51 -0
- package/src/installer/config.ts +73 -0
- package/src/installer/copilot-family.ts +8 -0
- package/src/installer/hooks/install.ts +3 -14
- package/src/installer/installer.ts +332 -11
- package/src/installer/prompt.ts +15 -8
- package/src/installer/search-policy.ts +3 -2
- package/src/installer/shared-skill.ts +200 -0
- package/src/installer/style-packs/caveman.ts +118 -0
- package/src/installer/style-packs/ste/skill.ts +218 -0
- package/src/mcp/auth-tool.ts +161 -0
- package/src/mcp/index-cache.ts +2 -1
- package/src/mcp/server.ts +7 -4
- package/src/prompt.ts +8 -5
- package/src/setup.ts +100 -69
- package/src/spinner.ts +33 -6
- package/tokenizer/tokenizer.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { dim, 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 || options.interactive) {
|
|
39
|
+
return "device_code";
|
|
40
|
+
}
|
|
41
|
+
throw new Error("Choose an auth mode with `miru setup --device` or `miru setup --key TOKEN`.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function authenticateWithApiKey(
|
|
45
|
+
options: AuthenticateOptions,
|
|
46
|
+
): Promise<AuthenticatedCredentials> {
|
|
47
|
+
const apiKey = options.apiKey ?? (await promptApiKey());
|
|
48
|
+
if (!options.skipValidation) {
|
|
49
|
+
const spinner = new Spinner("Validating API key");
|
|
50
|
+
spinner.start();
|
|
51
|
+
const result = await validateEmbeddingApiKey({ apiKey });
|
|
52
|
+
if (!result.valid) {
|
|
53
|
+
spinner.stop();
|
|
54
|
+
throw new Error(result.message);
|
|
55
|
+
}
|
|
56
|
+
spinner.succeed("API key validated");
|
|
57
|
+
}
|
|
58
|
+
return { kind: "api_key", apiKey };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function authenticateWithDeviceCode(
|
|
62
|
+
options: AuthenticateOptions,
|
|
63
|
+
): Promise<AuthenticatedCredentials> {
|
|
64
|
+
writeStderr("");
|
|
65
|
+
const spinner = new Spinner("Authenticating");
|
|
66
|
+
spinner.start();
|
|
67
|
+
try {
|
|
68
|
+
const start = await startDeviceAuthorization();
|
|
69
|
+
const verificationUrl = start.verificationUriComplete ?? start.verificationUri;
|
|
70
|
+
|
|
71
|
+
const shouldOpenBrowser =
|
|
72
|
+
options.interactive &&
|
|
73
|
+
(process.env.MIRU_OPEN_BROWSER === undefined || process.env.MIRU_OPEN_BROWSER === "1");
|
|
74
|
+
if (shouldOpenBrowser) {
|
|
75
|
+
openBrowserForDeviceLogin(verificationUrl);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let visit = dim(` Visit ${verificationUrl}`);
|
|
79
|
+
if (!start.verificationUriComplete) {
|
|
80
|
+
visit += `\n${dim(` Code: ${start.userCode}`)}`;
|
|
81
|
+
}
|
|
82
|
+
spinner.follow(visit);
|
|
83
|
+
|
|
84
|
+
const tokens = await pollDeviceAuthorization(start);
|
|
85
|
+
spinner.succeed("");
|
|
86
|
+
return {
|
|
87
|
+
kind: "device_code",
|
|
88
|
+
accessToken: tokens.accessToken,
|
|
89
|
+
refreshToken: tokens.refreshToken,
|
|
90
|
+
expiresAt: tokens.expiresAt,
|
|
91
|
+
tokenType: tokens.tokenType,
|
|
92
|
+
scope: tokens.scope,
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
95
|
+
spinner.stop();
|
|
96
|
+
if (options.allowManualFallback && options.interactive) {
|
|
97
|
+
warn(err instanceof Error ? err.message : String(err));
|
|
98
|
+
const fallback = await promptConfirm("Enter an API key instead?", true);
|
|
99
|
+
if (fallback) {
|
|
100
|
+
return authenticateWithApiKey(options);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function authenticateWithProvider(
|
|
108
|
+
options: AuthenticateOptions,
|
|
109
|
+
): Promise<AuthenticatedCredentials> {
|
|
110
|
+
const mode = await resolveAuthMode(options);
|
|
111
|
+
if (mode === "device_code") {
|
|
112
|
+
return authenticateWithDeviceCode(options);
|
|
113
|
+
}
|
|
114
|
+
return authenticateWithApiKey(options);
|
|
115
|
+
}
|
|
@@ -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";
|
|
@@ -20,8 +21,9 @@ import {
|
|
|
20
21
|
AGENT_IDS,
|
|
21
22
|
formatUnknownAgent,
|
|
22
23
|
printCommandHelp,
|
|
23
|
-
|
|
24
|
+
printEnvironmentHelp,
|
|
24
25
|
printMainHelp,
|
|
26
|
+
printSageMakerHelp,
|
|
25
27
|
} from "./help.ts";
|
|
26
28
|
import { getBenchmarkModeStatus, setBenchmarkMode } from "./installer/benchmark-mode.ts";
|
|
27
29
|
import { runSearchGuardFromStdin } from "./installer/hooks/search-guard.ts";
|
|
@@ -61,7 +63,13 @@ process.title = "miru";
|
|
|
61
63
|
|
|
62
64
|
await loadEnvFiles();
|
|
63
65
|
normalizeTakaraApiKeyEnv();
|
|
64
|
-
|
|
66
|
+
// Soft-fail: expired/revoked device refresh must not brick `miru -v`, `setup --clear`, etc.
|
|
67
|
+
// Command paths call ensureCredentials() which recovers interactively when allowed.
|
|
68
|
+
try {
|
|
69
|
+
await loadStoredCredentials();
|
|
70
|
+
} catch {
|
|
71
|
+
// Leave recovery to ensureCredentials / setup.
|
|
72
|
+
}
|
|
65
73
|
|
|
66
74
|
const CLI_COMMANDS = new Set([
|
|
67
75
|
"search",
|
|
@@ -74,6 +82,8 @@ const CLI_COMMANDS = new Set([
|
|
|
74
82
|
"setup",
|
|
75
83
|
"clear",
|
|
76
84
|
"benchmark",
|
|
85
|
+
"env",
|
|
86
|
+
"environment",
|
|
77
87
|
"hook-guard",
|
|
78
88
|
"help",
|
|
79
89
|
"-h",
|
|
@@ -408,7 +418,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
408
418
|
}
|
|
409
419
|
|
|
410
420
|
if (command === "-h" || command === "--help") {
|
|
411
|
-
|
|
421
|
+
printMainHelp();
|
|
412
422
|
return;
|
|
413
423
|
}
|
|
414
424
|
|
|
@@ -419,7 +429,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
419
429
|
|
|
420
430
|
if (command === "help") {
|
|
421
431
|
const topic = rest[0];
|
|
422
|
-
if (!topic) {
|
|
432
|
+
if (!topic || topic === "-h" || topic === "--help") {
|
|
423
433
|
printMainHelp();
|
|
424
434
|
return;
|
|
425
435
|
}
|
|
@@ -427,6 +437,25 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
427
437
|
return;
|
|
428
438
|
}
|
|
429
439
|
|
|
440
|
+
if (command === "setup" && (rest.includes("-h") || rest.includes("--help"))) {
|
|
441
|
+
if (rest.includes("--sagemaker")) {
|
|
442
|
+
printSageMakerHelp();
|
|
443
|
+
} else {
|
|
444
|
+
printCommandHelp("setup");
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (command === "env" || command === "environment") {
|
|
450
|
+
printEnvironmentHelp();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (rest[0] === "-h" || rest[0] === "--help") {
|
|
455
|
+
printCommandHelp(command);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
430
459
|
if (command === "hook-guard") {
|
|
431
460
|
process.exit(await runSearchGuardFromStdin());
|
|
432
461
|
}
|
|
@@ -474,26 +503,35 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
474
503
|
if (command === "setup") {
|
|
475
504
|
const { args, error } = parseSetupCliArgs(rest);
|
|
476
505
|
if (error === "clear_with_key") {
|
|
477
|
-
fail("miru setup --clear cannot be combined with --key.");
|
|
506
|
+
fail("miru setup --clear cannot be combined with --key, --device, or --sagemaker.");
|
|
478
507
|
process.exit(1);
|
|
479
508
|
}
|
|
480
509
|
if (error === "sagemaker_with_key") {
|
|
481
510
|
fail("miru setup --sagemaker cannot be combined with --key.");
|
|
482
511
|
process.exit(1);
|
|
483
512
|
}
|
|
513
|
+
if (error === "device_with_key") {
|
|
514
|
+
fail("miru setup accepts either --device or --key TOKEN, not both.");
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
if (error === "device_with_sagemaker") {
|
|
518
|
+
fail("miru setup --device cannot be combined with --sagemaker.");
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
484
521
|
if (args.clear) {
|
|
485
522
|
await runClearCredentials();
|
|
486
523
|
return;
|
|
487
524
|
}
|
|
488
525
|
const { newlySaved } = await runSetup({
|
|
489
526
|
apiKey: args.apiKey,
|
|
527
|
+
device: args.device,
|
|
490
528
|
force: args.force,
|
|
491
529
|
sagemaker: args.sagemaker,
|
|
492
530
|
sagemakerArn: args.sagemakerArn,
|
|
493
531
|
profile: args.profile,
|
|
494
532
|
});
|
|
495
533
|
if (newlySaved) {
|
|
496
|
-
const offerInstall = canPromptForCredentials() && !args.apiKey && !args.force;
|
|
534
|
+
const offerInstall = canPromptForCredentials() && !args.apiKey && !args.device && !args.force;
|
|
497
535
|
if (offerInstall) {
|
|
498
536
|
const install = await promptConfirm("Configure Miru in your coding agent now?");
|
|
499
537
|
if (install) {
|
|
@@ -501,7 +539,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
501
539
|
return;
|
|
502
540
|
}
|
|
503
541
|
}
|
|
504
|
-
hint("
|
|
542
|
+
hint("To add Miru to your IDE, run `miru install`.");
|
|
505
543
|
}
|
|
506
544
|
return;
|
|
507
545
|
}
|
|
@@ -675,7 +713,20 @@ async function runMcp(argv: string[]): Promise<void> {
|
|
|
675
713
|
}
|
|
676
714
|
|
|
677
715
|
async function runMcpWithCredentials(argv: string[]): Promise<void> {
|
|
678
|
-
|
|
716
|
+
// The MCP server is spawned as a headless stdio subprocess, never a real
|
|
717
|
+
// login terminal, so it must never crash the whole process over missing
|
|
718
|
+
// credentials — that would kill every tool, not just the ones needing
|
|
719
|
+
// embeddings. Best-effort load/refresh whatever's already stored; leave it
|
|
720
|
+
// to individual tool calls to report a missing-credentials error, and to
|
|
721
|
+
// the `auth` tool (src/mcp/auth-tool.ts) to complete device-code login
|
|
722
|
+
// in-band without a terminal.
|
|
723
|
+
await loadStoredCredentials().catch((err: unknown) => {
|
|
724
|
+
// Swallow so a stale/revoked refresh token etc. can't crash the server — but
|
|
725
|
+
// don't swallow silently, or the only symptom is a later "credentials
|
|
726
|
+
// required" tool error with no clue why stored credentials didn't work.
|
|
727
|
+
warn(`Could not load stored credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
728
|
+
return false;
|
|
729
|
+
});
|
|
679
730
|
await runMcp(argv);
|
|
680
731
|
}
|
|
681
732
|
|
package/src/credentials.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
+
let activeStoredToken: string | null = null;
|
|
7
17
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
}
|
package/src/embeddings/openai.ts
CHANGED
|
@@ -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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CredentialsError } from "./auth/errors.ts";
|
|
2
|
+
|
|
1
3
|
export function envInt(name: string, fallback: number, min = 1): number {
|
|
2
4
|
const raw = process.env[name];
|
|
3
5
|
if (!raw) {
|
|
@@ -59,8 +61,10 @@ export function hasTakaraApiKeyInEnv(): boolean {
|
|
|
59
61
|
export function resolveEmbeddingApiKey(): string {
|
|
60
62
|
const key = process.env[TAKARA_API_KEY_ENV]?.trim() ?? "";
|
|
61
63
|
if (!isUsableTakaraApiKey(key)) {
|
|
62
|
-
throw new
|
|
63
|
-
"Takara
|
|
64
|
+
throw new CredentialsError(
|
|
65
|
+
"Takara credentials required. If you're an agent with Miru MCP tools available, call the " +
|
|
66
|
+
"`auth` tool to sign in. Otherwise run `miru setup`, or set TAKARA_API_KEY in your MCP " +
|
|
67
|
+
"server env or .env.local.",
|
|
64
68
|
);
|
|
65
69
|
}
|
|
66
70
|
return key;
|