@takara-ai/miru-code 1.3.1 → 1.5.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 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.3.1",
3
+ "version": "1.5.0",
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,8 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { relativePathFromRoot } from "../index/incremental.ts";
4
+ import { formatExpandResultsText, formatResultsText } from "../mcp/format-text.ts";
4
5
  import type { MiruIndex } from "../miru-index.ts";
5
- import { applySnippetsToResults, estimateResultTokens } from "../snippet.ts";
6
+ import { applySnippetsToResults } from "../snippet.ts";
6
7
  import { countTokens, tokenCountMethod, tokenizerJsonPath } from "../token-count.ts";
7
8
  import type { SearchResult } from "../types.ts";
8
9
  import {
@@ -10,6 +11,8 @@ import {
10
11
  DEFAULT_EXPAND_BEFORE,
11
12
  dedupeResultsByFile,
12
13
  expandChunksAtLine,
14
+ formatExpandResults,
15
+ formatResults,
13
16
  } from "../utils.ts";
14
17
  import { type GrepFileHit, grepSearch } from "./grep.ts";
15
18
  import { agentBenchmarkFromTokens, attachAgentBenchmark, tokenSavingsPct } from "./summary.ts";
@@ -83,6 +86,7 @@ async function readLineWindowTokens(
83
86
  }
84
87
  }
85
88
 
89
+ /** Tokens of the MCP `expand` text body (headers + chunk content), matching what agents receive. */
86
90
  function miruExpandTokens(
87
91
  index: MiruIndex,
88
92
  repoPath: string,
@@ -94,7 +98,7 @@ function miruExpandTokens(
94
98
  }
95
99
  const [{ meta }] = applySnippetsToResults([top], query);
96
100
  const line = meta.truncated ? meta.anchor_line : top.chunk.start_line;
97
- const { chunks } = expandChunksAtLine(
101
+ const { anchor, chunks } = expandChunksAtLine(
98
102
  index.chunks,
99
103
  top.chunk.file_path,
100
104
  line,
@@ -102,8 +106,18 @@ function miruExpandTokens(
102
106
  DEFAULT_EXPAND_BEFORE,
103
107
  DEFAULT_EXPAND_AFTER,
104
108
  );
109
+ if (!anchor) {
110
+ return { tokens: 0, lineSpan: 0 };
111
+ }
112
+ const text = formatExpandResultsText(
113
+ formatExpandResults(top.chunk.file_path, line, anchor, chunks, {
114
+ repoRoot: repoPath,
115
+ before: DEFAULT_EXPAND_BEFORE,
116
+ after: DEFAULT_EXPAND_AFTER,
117
+ }),
118
+ );
105
119
  return {
106
- tokens: chunks.reduce((sum, chunk) => sum + countTokens(chunk.content), 0),
120
+ tokens: countTokens(text),
107
121
  lineSpan: expandLineSpan(chunks),
108
122
  };
109
123
  }
@@ -164,8 +178,10 @@ export async function benchmarkSearchComparison(options: {
164
178
  const started = performance.now();
165
179
  const raw = await index.search({ query, topK, rerank: true });
166
180
  const results = (dedupeByFile ? dedupeResultsByFile(raw) : raw).slice(0, topK);
167
- const snippetResults = applySnippetsToResults(results, query).map((entry) => entry.result);
168
- const searchTokens = estimateResultTokens(snippetResults);
181
+ // Count the MCP tool text body (path headers + snippets), not bare chunk content.
182
+ const searchTokens = countTokens(
183
+ formatResultsText(formatResults(query, results, { repoRoot: repoPath, snippet: true })),
184
+ );
169
185
  const expand = miruExpandTokens(index, repoPath, results[0], query);
170
186
  return {
171
187
  results,
@@ -264,6 +280,7 @@ export function toAgentBenchmarkSummary(block: SearchBenchmarkBlock): AgentBench
264
280
  block.accuracy.rank1_match,
265
281
  );
266
282
  summary.save_pct = block.efficiency.token_savings_pct;
283
+ summary.search_tok = block.miru.search_tokens;
267
284
  if (block.accuracy.miru_only.length > 0) {
268
285
  summary.miru_only = block.accuracy.miru_only.slice(0, MAX_AGENT_MIRU_ONLY);
269
286
  }
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Compare Miru `locate` agent payload tokens vs the costlier native Grep path.
2
+ * Compare Miru `locate` MCP response tokens vs the costlier native Grep path.
3
3
  *
4
- * Baseline mirrors Cursor/Claude Grep-style output: fixed-string matches with
5
- * ±GREP_CONTEXT lines (not slim `rg -n`). That is the expensive path agents
6
- * take without `locate`, so savings measure real workflow cost — not an
7
- * apples-to-apples payload contest against optimally-minimal ripgrep.
4
+ * Miru side counts the agent-facing text body (`formatLiteralLocateText`), not
5
+ * the intermediate JSON payload. Baseline mirrors Cursor/Claude Grep-style
6
+ * output: fixed-string matches with ±GREP_CONTEXT lines (not slim `rg -n`).
7
+ * That is the expensive path agents take without `locate`, so savings measure
8
+ * real workflow cost — not an apples-to-apples contest against minimal ripgrep.
8
9
  */
9
10
 
10
11
  import {
@@ -13,6 +14,7 @@ import {
13
14
  type LiteralLocateOptions,
14
15
  type LiteralLocateResult,
15
16
  } from "../literal.ts";
17
+ import { formatLiteralLocateText } from "../mcp/format-text.ts";
16
18
  import type { MiruIndex } from "../miru-index.ts";
17
19
  import { countTokens } from "../token-count.ts";
18
20
  import { GREP_CONTEXT } from "./grep.ts";
@@ -44,7 +46,7 @@ export async function benchmarkLocateComparison(options: {
44
46
  const result = options.index.locateLiteral(options.literal, locateOpts);
45
47
  const miruMs = performance.now() - miruStart;
46
48
  const payload = formatLiteralLocate(result);
47
- const miruTok = countTokens(JSON.stringify(payload));
49
+ const miruTok = countTokens(formatLiteralLocateText(payload));
48
50
 
49
51
  // Unbounded agent-style Grep dump (±context) — the costlier path without locate.
50
52
  // Never use less context than the caller actually requested via context_lines,
@@ -20,10 +20,19 @@ export function agentBenchmarkFromTokens(
20
20
  };
21
21
  }
22
22
 
23
- /** Attach compact `benchmark` to an MCP tool payload. */
23
+ /** Attach compact `benchmark` to an MCP tool payload (JSON path / tests). */
24
24
  export function attachAgentBenchmark<T extends Record<string, unknown>>(
25
25
  payload: T,
26
26
  benchmark: AgentBenchmarkSummary,
27
27
  ): T & { benchmark: AgentBenchmarkSummary } {
28
28
  return { ...payload, benchmark };
29
29
  }
30
+
31
+ /**
32
+ * Append a compact trailing JSON object so plaintext MCP bodies can still expose
33
+ * `benchmark` without wrapping the result in JSON scaffolding.
34
+ * `miru_tok` counts the body only — never this trailer.
35
+ */
36
+ export function appendAgentBenchmark(body: string, benchmark: AgentBenchmarkSummary): string {
37
+ return `${body}\n\n${JSON.stringify({ benchmark })}`;
38
+ }
@@ -42,7 +42,12 @@ export interface SearchBenchmarkBlock {
42
42
  };
43
43
  }
44
44
 
45
- /** Compact per-search stats attached to MCP `search` responses (agent-facing). */
45
+ /**
46
+ * Compact per-search stats appended to MCP `search` / `locate` responses.
47
+ * `miru_tok` is the agent-facing MCP text body for that tool (search also adds
48
+ * one expand body for workflow vs grep+read). Not JSON scaffolding, not the
49
+ * trailing `{"benchmark":...}` line.
50
+ */
46
51
  export interface AgentBenchmarkSummary {
47
52
  save_pct: number;
48
53
  miru_tok: number;
@@ -51,6 +56,11 @@ export interface AgentBenchmarkSummary {
51
56
  rank1: boolean;
52
57
  /** Present only when non-empty; capped at 3 paths. */
53
58
  miru_only?: string[];
59
+ /**
60
+ * Search only: tokens of the search MCP text body alone (before expand).
61
+ * `miru_tok` = search_tok + expand body when present.
62
+ */
63
+ search_tok?: number;
54
64
  }
55
65
 
56
66
  /** Compact cumulative rollup for MCP `read_benchmark` (agent-facing). */
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,46 @@ 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
+ /** Hydrate TAKARA_API_KEY / SageMaker env vars from the credentials file when env is unset. */
59
72
  export async function loadStoredCredentials(): Promise<boolean> {
60
73
  normalizeTakaraApiKeyEnv();
61
- if (hasTakaraApiKeyInEnv()) {
74
+ const needsTakara = !hasTakaraApiKeyInEnv();
75
+ const needsSageMaker = !process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
76
+ if (!needsTakara && !needsSageMaker) {
62
77
  return false;
63
78
  }
79
+
64
80
  const stored = await readStoredCredentials();
65
81
  if (!stored) {
66
82
  return false;
67
83
  }
68
- process.env.TAKARA_API_KEY = stored.takara_api_key;
69
- return true;
84
+
85
+ let changed = false;
86
+ if (needsTakara && stored.takara_api_key) {
87
+ process.env.TAKARA_API_KEY = stored.takara_api_key;
88
+ changed = true;
89
+ }
90
+ if (needsSageMaker && stored.sagemaker) {
91
+ hydrateSageMakerEnv(stored.sagemaker);
92
+ changed = true;
93
+ }
94
+ return changed;
70
95
  }
71
96
 
72
97
  export async function saveStoredCredentials(apiKey: string): Promise<string> {
73
98
  const dir = resolveCredentialsDir();
74
99
  const path = resolveCredentialsPath();
75
100
  await mkdir(dir, { recursive: true, mode: 0o700 });
101
+ const existing = await readStoredCredentials();
102
+ // Takara and SageMaker modes are mutually exclusive — saving a key drops any stored endpoint.
103
+ const previousSageMaker = existing?.sagemaker;
76
104
  const payload: StoredCredentials = {
77
105
  version: CREDENTIALS_VERSION,
78
106
  takara_api_key: apiKey,
@@ -83,6 +111,39 @@ export async function saveStoredCredentials(apiKey: string): Promise<string> {
83
111
  } catch {
84
112
  // Windows may not support Unix mode bits on all filesystems.
85
113
  }
114
+ if (previousSageMaker) {
115
+ if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN === previousSageMaker.endpoint_arn) {
116
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
117
+ }
118
+ if (previousSageMaker.profile && process.env.AWS_PROFILE === previousSageMaker.profile) {
119
+ delete process.env.AWS_PROFILE;
120
+ }
121
+ }
122
+ return path;
123
+ }
124
+
125
+ export async function saveStoredSageMakerCredentials(
126
+ sagemaker: StoredSageMakerCredentials,
127
+ ): Promise<string> {
128
+ const dir = resolveCredentialsDir();
129
+ const path = resolveCredentialsPath();
130
+ await mkdir(dir, { recursive: true, mode: 0o700 });
131
+ const existing = await readStoredCredentials();
132
+ // Takara and SageMaker modes are mutually exclusive — saving an endpoint drops any stored key.
133
+ const previousTakaraKey = existing?.takara_api_key;
134
+ const payload: StoredCredentials = {
135
+ version: CREDENTIALS_VERSION,
136
+ sagemaker,
137
+ };
138
+ await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
139
+ try {
140
+ await chmod(path, 0o600);
141
+ } catch {
142
+ // Windows may not support Unix mode bits on all filesystems.
143
+ }
144
+ if (previousTakaraKey && process.env.TAKARA_API_KEY === previousTakaraKey) {
145
+ delete process.env.TAKARA_API_KEY;
146
+ }
86
147
  return path;
87
148
  }
88
149
 
@@ -95,9 +156,18 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
95
156
  const stored = await readStoredCredentials();
96
157
  await Bun.file(path).delete();
97
158
 
98
- if (stored && process.env.TAKARA_API_KEY === stored.takara_api_key) {
159
+ if (stored?.takara_api_key && process.env.TAKARA_API_KEY === stored.takara_api_key) {
99
160
  delete process.env.TAKARA_API_KEY;
100
161
  }
162
+ const sagemaker = stored?.sagemaker;
163
+ if (sagemaker) {
164
+ if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN === sagemaker.endpoint_arn) {
165
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
166
+ }
167
+ if (sagemaker.profile && process.env.AWS_PROFILE === sagemaker.profile) {
168
+ delete process.env.AWS_PROFILE;
169
+ }
170
+ }
101
171
 
102
172
  return { cleared: true, path };
103
173
  }