@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 +2 -0
- package/docs/self-hosted-sagemaker.md +116 -0
- package/package.json +7 -2
- package/src/benchmark/compare.ts +22 -5
- package/src/benchmark/locate-compare.ts +8 -6
- package/src/benchmark/summary.ts +10 -1
- package/src/benchmark/types.ts +11 -1
- package/src/cli.ts +18 -19
- package/src/credentials.ts +77 -7
- package/src/embeddings/openai.ts +194 -84
- package/src/embeddings/sagemaker.ts +268 -0
- package/src/embeddings/validate.ts +2 -1
- package/src/help.ts +38 -4
- package/src/index/bm25.ts +31 -21
- package/src/index/create.ts +114 -44
- package/src/index/incremental.ts +4 -11
- package/src/index/sparse.ts +14 -0
- package/src/installer/search-policy.ts +4 -1
- package/src/literal.ts +1 -1
- package/src/mcp/format-text.ts +67 -0
- package/src/mcp/index-cache.ts +5 -0
- package/src/mcp/server.ts +28 -28
- package/src/prompt.ts +13 -0
- package/src/setup.ts +194 -14
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
|
+
"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"
|
package/src/benchmark/compare.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
168
|
-
const searchTokens =
|
|
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`
|
|
2
|
+
* Compare Miru `locate` MCP response tokens vs the costlier native Grep path.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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(
|
|
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,
|
package/src/benchmark/summary.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/benchmark/types.ts
CHANGED
|
@@ -42,7 +42,12 @@ export interface SearchBenchmarkBlock {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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 (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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({
|
|
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) {
|
package/src/credentials.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
}
|