@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/README.md CHANGED
@@ -238,6 +238,8 @@ Miru sends **file contents** to the [Takara inference API](https://takara.ai) wh
238
238
 
239
239
  If you index proprietary code, make sure that sending snippets to Takara's endpoint fits your security and compliance requirements. `MIRU_WORKSPACE_ROOT` is an opt-in boundary for MCP local `repo` paths only, and restricts indexing to a single workspace directory when set.
240
240
 
241
+ Enterprise self-hosted embeddings (no Takara egress): see [docs/self-hosted-sagemaker.md](docs/self-hosted-sagemaker.md).
242
+
241
243
  ## Benchmark mode
242
244
 
243
245
  Optional measurement of how many tokens Miru saves versus a simple Grep workflow (ripgrep + reading the top matched file). Useful when evaluating Miru; leave it off day-to-day. Only local repo paths are compared — git URL repos skip the comparison and return `benchmark_skipped: "local_repo_only"`.
@@ -0,0 +1,116 @@
1
+ # Self-hosted embeddings (AWS SageMaker)
2
+
3
+ Run Miru against your own SageMaker embedding endpoint instead of Takara’s hosted API (`infer.takara.ai`).
4
+
5
+ The embedding model is sold on AWS Marketplace:
6
+
7
+ **[Takara DS1 Miru (int8) — AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-la5olwsicr6ue)**
8
+
9
+ ## Overview
10
+
11
+
12
+ | Step | Who | What |
13
+ | ---- | --------- | ---------------------------------------------------------------- |
14
+ | 1 | AWS admin | Subscribe on Marketplace and deploy a SageMaker endpoint |
15
+ | 2 | AWS admin | Create an invoke-only IAM user / profile (runbook below) |
16
+ | 3 | Developer | `miru setup --sagemaker` — Miru validates and saves the endpoint |
17
+
18
+
19
+ Miru **never** creates IAM users or writes `~/.aws`. It only inherits a profile you already have and checks that it can call the endpoint.
20
+
21
+ ## One mode at a time
22
+
23
+ Miru stores **either** a Takara API key **or** a SageMaker endpoint — never both.
24
+
25
+ - `miru setup --sagemaker …` **removes** any stored Takara API key
26
+ - `miru setup` (Takara) **removes** any stored SageMaker endpoint
27
+
28
+ You do not need `--clear` when switching; the other mode is purged automatically.
29
+
30
+ ## 1. Deploy the Marketplace endpoint
31
+
32
+ 1. Open the [Marketplace listing](https://aws.amazon.com/marketplace/pp/prodview-la5olwsicr6ue)
33
+ 2. Subscribe and deploy the model package as a SageMaker endpoint in your account
34
+ 3. Copy the endpoint ARN — it looks like:
35
+
36
+ ```text
37
+ arn:aws:sagemaker:<region>:<account-id>:endpoint/<name>
38
+ ```
39
+
40
+
41
+
42
+ ## 2. Create an invoke-only AWS profile (admin runbook)
43
+
44
+ You need an AWS identity that can call `sagemaker:InvokeEndpoint` on that ARN. The simplest path for a laptop / CI user is a **least-privilege IAM user** scoped to that one endpoint, installed as a named profile.
45
+
46
+ From a checkout of this repo, with the [AWS CLI](https://docs.aws.amazon.com/cli/) available and **admin** credentials in your current shell:
47
+
48
+ ```bash
49
+ bun run sagemaker:create-invoke-user -- \
50
+ --endpoint-arn arn:aws:sagemaker:<region>:<account-id>:endpoint/<name> \
51
+ --profile miru
52
+ ```
53
+
54
+ That script (`scripts/create-sagemaker-invoke-user.ts`):
55
+
56
+ 1. Creates an IAM user tagged for this purpose (or reuses one with the same name)
57
+ 2. Attaches an inline policy allowing **only** `sagemaker:InvokeEndpoint` on that ARN
58
+ 3. Creates an access key and writes it into `~/.aws` as the named profile (`miru` by default)
59
+
60
+ Optional flags: `--user-name <name>`, `--profile <name>` (alias: `--arn` for the endpoint).
61
+
62
+ Prefer SSO / IAM roles in production if your org requires it — the runbook script is the easy start with long-lived keys. Rotate or delete keys when someone leaves.
63
+
64
+ Smoke-test invoke (optional):
65
+
66
+ ```bash
67
+ bun run sagemaker:auth-check -- --arn arn:aws:sagemaker:<region>:<account-id>:endpoint/<name>
68
+ ```
69
+
70
+
71
+
72
+ ## 3. Point Miru at the endpoint
73
+
74
+ ```bash
75
+ miru setup --sagemaker \
76
+ --arn arn:aws:sagemaker:<region>:<account-id>:endpoint/<name> \
77
+ --profile miru
78
+ ```
79
+
80
+ Or run `miru setup --sagemaker` and answer the prompts.
81
+
82
+ **What setup does**
83
+
84
+ 1. Parses the endpoint ARN + AWS profile name
85
+ 2. Invokes the endpoint once to confirm auth and that it returns embeddings
86
+ 3. Saves the SageMaker config and **deletes any stored Takara API key**
87
+
88
+ After that, indexing and search embed via SageMaker — no egress to `infer.takara.ai`.
89
+
90
+ If `TAKARA_API_KEY` is still set in your shell or `.env.local`, remove it there too so it cannot conflict later.
91
+
92
+ ## Switch back to Takara
93
+
94
+ ```bash
95
+ miru setup
96
+ ```
97
+
98
+ Enter your Takara API key. Setup validates it, saves it, and **deletes any stored SageMaker endpoint**.
99
+
100
+ Also unset any `MIRU_SAGEMAKER_*` variables if you set them in the environment or `.env.local`.
101
+
102
+ ## Environment alternatives
103
+
104
+ Prefer `miru setup --sagemaker` so auth is checked automatically. You can also set:
105
+
106
+
107
+ | Variable | Purpose |
108
+ | -------------------------------------------------------- | --------------------------------------------------------- |
109
+ | `MIRU_SAGEMAKER_ENDPOINT_ARN` | `arn:aws:sagemaker:<region>:<account-id>:endpoint/<name>` |
110
+ | `MIRU_SAGEMAKER_ENDPOINT_NAME` + `MIRU_SAGEMAKER_REGION` | Alternative to the ARN |
111
+ | `AWS_PROFILE` / `AWS_ACCESS_KEY_ID` / … | Standard AWS credential chain |
112
+
113
+
114
+ Optional TEI-style knobs: `MIRU_SAGEMAKER_NORMALIZE`, `MIRU_SAGEMAKER_TRUNCATE`, `MIRU_SAGEMAKER_TRUNCATION_DIRECTION`, `MIRU_SAGEMAKER_PROMPT_NAME`.
115
+
116
+ See `miru help setup` and `miru -h` for the same flags in the CLI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "1.4.0",
3
+ "version": "1.5.2",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,6 +38,7 @@
38
38
  "src",
39
39
  "grammars",
40
40
  "tokenizer",
41
+ "docs",
41
42
  "README.md",
42
43
  "LICENSE"
43
44
  ],
@@ -61,9 +62,13 @@
61
62
  "benchmark:literal": "bun run scripts/benchmark-literal-tokens.ts",
62
63
  "benchmark:quality": "bun run scripts/benchmark-quality.ts",
63
64
  "benchmark:quantization": "bun run scripts/benchmark-quantization.ts",
64
- "benchmark:tokenizer-ab": "bun run scripts/tokenizer-ab.ts"
65
+ "benchmark:tokenizer-ab": "bun run scripts/tokenizer-ab.ts",
66
+ "sagemaker:auth-check": "bun run scripts/sagemaker-auth-check.ts",
67
+ "sagemaker:create-invoke-user": "bun run scripts/create-sagemaker-invoke-user.ts",
68
+ "benchmark:embedding-latency": "bun run scripts/embedding-latency-ab.ts"
65
69
  },
66
70
  "dependencies": {
71
+ "@aws-sdk/client-sagemaker-runtime": "^3.1090.0",
67
72
  "ignore": "^7.0.5",
68
73
  "web-tree-sitter": "^0.26.9",
69
74
  "zod": "^4.4.3"
@@ -1,6 +1,7 @@
1
1
  /** Native grep baseline for Miru benchmark comparisons. */
2
2
 
3
3
  import { relative } from "node:path";
4
+ import { envOptionalInt } from "../env.ts";
4
5
  import { countTokens } from "../token-count.ts";
5
6
 
6
7
  const STOPWORDS = new Set(
@@ -12,6 +13,9 @@ const STOPWORDS = new Set(
12
13
  export const GREP_LINES_PER_FILE = 3;
13
14
  export const GREP_CONTEXT = 2;
14
15
 
16
+ /** Default cap for each rg/grep/findstr spawn in benchmark mode (seconds). */
17
+ const DEFAULT_BENCHMARK_SEARCH_TIMEOUT_SEC = 10;
18
+
15
19
  /** Shared `-g` excludes for ripgrep baselines (search + locate). */
16
20
  export const RG_EXCLUDE_ARGS = [
17
21
  "-g",
@@ -24,6 +28,44 @@ export const RG_EXCLUDE_ARGS = [
24
28
  "!tokenizer/**",
25
29
  ] as const;
26
30
 
31
+ export class BenchmarkSearchTimeoutError extends Error {
32
+ constructor(timeoutSec: number) {
33
+ super(`Benchmark search timed out after ${timeoutSec}s`);
34
+ this.name = "BenchmarkSearchTimeoutError";
35
+ }
36
+ }
37
+
38
+ /** `Bun.spawn` for benchmark baselines, killed after MIRU_BENCHMARK_SEARCH_TIMEOUT. */
39
+ export async function spawnBenchmarkSearch(args: string[], cwd?: string): Promise<string> {
40
+ const timeoutSec =
41
+ envOptionalInt(["MIRU_BENCHMARK_SEARCH_TIMEOUT"], 1) ?? DEFAULT_BENCHMARK_SEARCH_TIMEOUT_SEC;
42
+ const proc = Bun.spawn(args, {
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ cwd,
46
+ timeout: timeoutSec * 1000,
47
+ });
48
+ const text = await new Response(proc.stdout).text();
49
+ await proc.exited;
50
+ // Prefer signalCode over killed — Bun may set killed=true after a normal exit.
51
+ if (proc.signalCode != null) {
52
+ throw new BenchmarkSearchTimeoutError(timeoutSec);
53
+ }
54
+ return text;
55
+ }
56
+
57
+ /** Run a Grep baseline; return null on timeout so Miru results can still be returned. */
58
+ export async function withGrepTimeoutFallback<T>(run: () => Promise<T>): Promise<T | null> {
59
+ try {
60
+ return await run();
61
+ } catch (err) {
62
+ if (err instanceof BenchmarkSearchTimeoutError) {
63
+ return null;
64
+ }
65
+ throw err;
66
+ }
67
+ }
68
+
27
69
  export interface GrepFileHit {
28
70
  file: string;
29
71
  matchCount: number;
@@ -172,34 +214,31 @@ export async function grepSearch(
172
214
  }
173
215
 
174
216
  async function rgRankedMatches(repoRoot: string, pattern: string, topK: number) {
175
- const countProc = Bun.spawn(
176
- ["rg", "-i", "--count-matches", pattern, repoRoot, ...RG_EXCLUDE_ARGS],
177
- { stdout: "pipe", stderr: "pipe" },
178
- );
179
- const countText = await new Response(countProc.stdout).text();
180
- await countProc.exited;
217
+ const countText = await spawnBenchmarkSearch([
218
+ "rg",
219
+ "-i",
220
+ "--count-matches",
221
+ pattern,
222
+ repoRoot,
223
+ ...RG_EXCLUDE_ARGS,
224
+ ]);
181
225
  return parseCountMatches(repoRoot, countText, topK);
182
226
  }
183
227
 
184
228
  async function grepRankedMatches(repoRoot: string, pattern: string, topK: number) {
185
- const countProc = Bun.spawn(
186
- [
187
- "grep",
188
- "-R",
189
- "-I",
190
- "-i",
191
- "-E",
192
- "-o",
193
- "--exclude-dir=node_modules",
194
- "--exclude-dir=.git",
195
- "--exclude-dir=tokenizer",
196
- pattern,
197
- repoRoot,
198
- ],
199
- { stdout: "pipe", stderr: "pipe" },
200
- );
201
- const countText = await new Response(countProc.stdout).text();
202
- await countProc.exited;
229
+ const countText = await spawnBenchmarkSearch([
230
+ "grep",
231
+ "-R",
232
+ "-I",
233
+ "-i",
234
+ "-E",
235
+ "-o",
236
+ "--exclude-dir=node_modules",
237
+ "--exclude-dir=.git",
238
+ "--exclude-dir=tokenizer",
239
+ pattern,
240
+ repoRoot,
241
+ ]);
203
242
  const counts = new Map<string, number>();
204
243
  for (const line of countText.split("\n")) {
205
244
  if (!line) {
@@ -240,13 +279,10 @@ function splitSearchToolLine(line: string): { path: string; rest: string } | nul
240
279
  async function findstrRankedMatches(repoRoot: string, keywords: string[], topK: number) {
241
280
  const counts = new Map<string, number>();
242
281
  for (const keyword of keywords) {
243
- const proc = Bun.spawn(["findstr", "/S", "/N", "/I", "/P", `/C:${keyword}`, "*"], {
244
- stdout: "pipe",
245
- stderr: "pipe",
246
- cwd: repoRoot,
247
- });
248
- const text = await new Response(proc.stdout).text();
249
- await proc.exited;
282
+ const text = await spawnBenchmarkSearch(
283
+ ["findstr", "/S", "/N", "/I", "/P", `/C:${keyword}`, "*"],
284
+ repoRoot,
285
+ );
250
286
  for (const line of text.split("\n")) {
251
287
  const parsed = parsePathLinePrefix(line);
252
288
  if (!parsed?.path) {
@@ -302,51 +338,42 @@ function parseCountMatches(repoRoot: string, countText: string, topK: number) {
302
338
  }
303
339
 
304
340
  async function rgFilePreview(absPath: string, pattern: string): Promise<string> {
305
- const proc = Bun.spawn(
306
- [
307
- "rg",
308
- "-i",
309
- "-n",
310
- "-C",
311
- String(GREP_CONTEXT),
312
- "-m",
313
- String(GREP_LINES_PER_FILE),
314
- pattern,
315
- absPath,
316
- ],
317
- { stdout: "pipe", stderr: "pipe" },
318
- );
319
- const output = await new Response(proc.stdout).text();
320
- await proc.exited;
321
- return output;
341
+ return spawnBenchmarkSearch([
342
+ "rg",
343
+ "-i",
344
+ "-n",
345
+ "-C",
346
+ String(GREP_CONTEXT),
347
+ "-m",
348
+ String(GREP_LINES_PER_FILE),
349
+ pattern,
350
+ absPath,
351
+ ]);
322
352
  }
323
353
 
324
354
  async function grepFilePreview(absPath: string, pattern: string): Promise<string> {
325
- const proc = Bun.spawn(
326
- [
327
- "grep",
328
- "-I",
329
- "-i",
330
- "-n",
331
- "-E",
332
- "-C",
333
- String(GREP_CONTEXT),
334
- "-m",
335
- String(GREP_LINES_PER_FILE),
336
- pattern,
337
- absPath,
338
- ],
339
- { stdout: "pipe", stderr: "pipe" },
340
- );
341
- const output = await new Response(proc.stdout).text();
342
- await proc.exited;
343
- return output;
355
+ return spawnBenchmarkSearch([
356
+ "grep",
357
+ "-I",
358
+ "-i",
359
+ "-n",
360
+ "-E",
361
+ "-C",
362
+ String(GREP_CONTEXT),
363
+ "-m",
364
+ String(GREP_LINES_PER_FILE),
365
+ pattern,
366
+ absPath,
367
+ ]);
344
368
  }
345
369
 
346
370
  async function findstrFilePreview(absPath: string, keywords: string[]): Promise<string> {
347
- const args = ["findstr", "/N", "/I", "/P", ...keywords.map((k) => `/C:${k}`), absPath];
348
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
349
- const output = await new Response(proc.stdout).text();
350
- await proc.exited;
351
- return output;
371
+ return spawnBenchmarkSearch([
372
+ "findstr",
373
+ "/N",
374
+ "/I",
375
+ "/P",
376
+ ...keywords.map((k) => `/C:${k}`),
377
+ absPath,
378
+ ]);
352
379
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { relative } from "node:path";
4
4
  import { countTokens } from "../token-count.ts";
5
- import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool } from "./grep.ts";
5
+ import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool, spawnBenchmarkSearch } from "./grep.ts";
6
6
 
7
7
  export interface RgLiteralOutput {
8
8
  text: string;
@@ -76,16 +76,10 @@ export async function rgLiteralOutput(
76
76
  throw new Error("No search tool found in PATH (tried rg, grep, and findstr on Windows)");
77
77
  }
78
78
  const start = performance.now();
79
- const proc = Bun.spawn(
79
+ const text = await spawnBenchmarkSearch(
80
80
  buildLiteralArgs(tool, repoRoot, literals, context, maxCount, !!options.ignoreCase),
81
- {
82
- stdout: "pipe",
83
- stderr: "pipe",
84
- ...(tool === "findstr" ? { cwd: repoRoot } : {}),
85
- },
81
+ tool === "findstr" ? repoRoot : undefined,
86
82
  );
87
- const text = await new Response(proc.stdout).text();
88
- await proc.exited;
89
83
  const latency_ms = performance.now() - start;
90
84
  const stats = parseRgLiteralStats(text, repoRoot);
91
85
  return { text, tokens: countTokens(text), latency_ms, ...stats };
package/src/cli.ts CHANGED
@@ -38,6 +38,7 @@ import { MiruIndex } from "./miru-index.ts";
38
38
  import {
39
39
  canPromptForCredentials,
40
40
  ensureCredentials,
41
+ parseSetupCliArgs,
41
42
  runClearCredentials,
42
43
  runSetup,
43
44
  } from "./setup.ts";
@@ -471,30 +472,28 @@ async function runCli(argv: string[]): Promise<void> {
471
472
  }
472
473
 
473
474
  if (command === "setup") {
474
- let apiKey: string | undefined;
475
- let force = false;
476
- let clear = false;
477
- for (let i = 0; i < rest.length; i++) {
478
- const arg = rest[i];
479
- if (arg === "--force") {
480
- force = true;
481
- } else if (arg === "--clear") {
482
- clear = true;
483
- } else if ((arg === "--key" || arg === "-k") && rest[i + 1]) {
484
- apiKey = rest[++i];
485
- }
475
+ const { args, error } = parseSetupCliArgs(rest);
476
+ if (error === "clear_with_key") {
477
+ fail("miru setup --clear cannot be combined with --key.");
478
+ process.exit(1);
486
479
  }
487
- if (clear) {
488
- if (apiKey) {
489
- fail("miru setup --clear cannot be combined with --key.");
490
- process.exit(1);
491
- }
480
+ if (error === "sagemaker_with_key") {
481
+ fail("miru setup --sagemaker cannot be combined with --key.");
482
+ process.exit(1);
483
+ }
484
+ if (args.clear) {
492
485
  await runClearCredentials();
493
486
  return;
494
487
  }
495
- const { newlySaved } = await runSetup({ apiKey, force });
488
+ const { newlySaved } = await runSetup({
489
+ apiKey: args.apiKey,
490
+ force: args.force,
491
+ sagemaker: args.sagemaker,
492
+ sagemakerArn: args.sagemakerArn,
493
+ profile: args.profile,
494
+ });
496
495
  if (newlySaved) {
497
- const offerInstall = canPromptForCredentials() && !apiKey && !force;
496
+ const offerInstall = canPromptForCredentials() && !args.apiKey && !args.force;
498
497
  if (offerInstall) {
499
498
  const install = await promptConfirm("Configure Miru in your coding agent now?");
500
499
  if (install) {
@@ -5,9 +5,15 @@ import { hasTakaraApiKeyInEnv, normalizeTakaraApiKeyEnv } from "./env.ts";
5
5
  const CREDENTIALS_FILENAME = "credentials.json";
6
6
  const CREDENTIALS_VERSION = 1;
7
7
 
8
+ export interface StoredSageMakerCredentials {
9
+ endpoint_arn: string;
10
+ profile?: string;
11
+ }
12
+
8
13
  interface StoredCredentials {
9
14
  version: number;
10
- takara_api_key: string;
15
+ takara_api_key?: string;
16
+ sagemaker?: StoredSageMakerCredentials;
11
17
  }
12
18
 
13
19
  export function resolveCredentialsDir(): string {
@@ -46,7 +52,7 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
46
52
  }
47
53
  try {
48
54
  const parsed = JSON.parse(await Bun.file(path).text()) as StoredCredentials;
49
- if (parsed.version !== CREDENTIALS_VERSION || !parsed.takara_api_key) {
55
+ if (parsed.version !== CREDENTIALS_VERSION || (!parsed.takara_api_key && !parsed.sagemaker)) {
50
56
  return null;
51
57
  }
52
58
  return parsed;
@@ -55,24 +61,65 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
55
61
  }
56
62
  }
57
63
 
58
- /** Set TAKARA_API_KEY from the user credentials file when env is unset. */
64
+ function hydrateSageMakerEnv(sagemaker: StoredSageMakerCredentials): void {
65
+ process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = sagemaker.endpoint_arn;
66
+ if (sagemaker.profile && !process.env.AWS_PROFILE) {
67
+ process.env.AWS_PROFILE = sagemaker.profile;
68
+ }
69
+ }
70
+
71
+ function clearSageMakerEnv(sagemaker?: StoredSageMakerCredentials): void {
72
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
73
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_NAME;
74
+ delete process.env.MIRU_SAGEMAKER_REGION;
75
+ if (sagemaker?.profile && process.env.AWS_PROFILE === sagemaker.profile) {
76
+ delete process.env.AWS_PROFILE;
77
+ }
78
+ }
79
+
80
+ function clearTakaraEnv(): void {
81
+ delete process.env.TAKARA_API_KEY;
82
+ }
83
+
84
+ /** Apply credentials.json as the sole mode — never leave Takara and SageMaker both active. */
59
85
  export async function loadStoredCredentials(): Promise<boolean> {
60
86
  normalizeTakaraApiKeyEnv();
61
- if (hasTakaraApiKeyInEnv()) {
62
- return false;
63
- }
64
87
  const stored = await readStoredCredentials();
65
88
  if (!stored) {
66
89
  return false;
67
90
  }
68
- process.env.TAKARA_API_KEY = stored.takara_api_key;
69
- return true;
91
+
92
+ let changed = false;
93
+ if (stored.takara_api_key) {
94
+ if (!hasTakaraApiKeyInEnv()) {
95
+ process.env.TAKARA_API_KEY = stored.takara_api_key;
96
+ changed = true;
97
+ }
98
+ if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN || process.env.MIRU_SAGEMAKER_ENDPOINT_NAME) {
99
+ clearSageMakerEnv(stored.sagemaker);
100
+ changed = true;
101
+ }
102
+ return changed;
103
+ }
104
+
105
+ if (stored.sagemaker) {
106
+ if (!process.env.MIRU_SAGEMAKER_ENDPOINT_ARN?.trim()) {
107
+ hydrateSageMakerEnv(stored.sagemaker);
108
+ changed = true;
109
+ }
110
+ if (hasTakaraApiKeyInEnv()) {
111
+ clearTakaraEnv();
112
+ changed = true;
113
+ }
114
+ }
115
+ return changed;
70
116
  }
71
117
 
72
118
  export async function saveStoredCredentials(apiKey: string): Promise<string> {
73
119
  const dir = resolveCredentialsDir();
74
120
  const path = resolveCredentialsPath();
75
121
  await mkdir(dir, { recursive: true, mode: 0o700 });
122
+ const previousSageMaker = (await readStoredCredentials())?.sagemaker;
76
123
  const payload: StoredCredentials = {
77
124
  version: CREDENTIALS_VERSION,
78
125
  takara_api_key: apiKey,
@@ -83,6 +130,27 @@ export async function saveStoredCredentials(apiKey: string): Promise<string> {
83
130
  } catch {
84
131
  // Windows may not support Unix mode bits on all filesystems.
85
132
  }
133
+ clearSageMakerEnv(previousSageMaker);
134
+ return path;
135
+ }
136
+
137
+ export async function saveStoredSageMakerCredentials(
138
+ sagemaker: StoredSageMakerCredentials,
139
+ ): Promise<string> {
140
+ const dir = resolveCredentialsDir();
141
+ const path = resolveCredentialsPath();
142
+ await mkdir(dir, { recursive: true, mode: 0o700 });
143
+ const payload: StoredCredentials = {
144
+ version: CREDENTIALS_VERSION,
145
+ sagemaker,
146
+ };
147
+ await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
148
+ try {
149
+ await chmod(path, 0o600);
150
+ } catch {
151
+ // Windows may not support Unix mode bits on all filesystems.
152
+ }
153
+ clearTakaraEnv();
86
154
  return path;
87
155
  }
88
156
 
@@ -95,8 +163,11 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
95
163
  const stored = await readStoredCredentials();
96
164
  await Bun.file(path).delete();
97
165
 
98
- if (stored && process.env.TAKARA_API_KEY === stored.takara_api_key) {
99
- delete process.env.TAKARA_API_KEY;
166
+ if (stored?.takara_api_key && process.env.TAKARA_API_KEY === stored.takara_api_key) {
167
+ clearTakaraEnv();
168
+ }
169
+ if (stored?.sagemaker) {
170
+ clearSageMakerEnv(stored.sagemaker);
100
171
  }
101
172
 
102
173
  return { cleared: true, path };