@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 +2 -0
- package/docs/self-hosted-sagemaker.md +116 -0
- package/package.json +7 -2
- package/src/benchmark/grep.ts +99 -72
- package/src/benchmark/rg-literal.ts +3 -9
- package/src/cli.ts +18 -19
- package/src/credentials.ts +81 -10
- package/src/embeddings/openai.ts +194 -84
- package/src/embeddings/sagemaker.ts +268 -0
- package/src/embeddings/validate.ts +2 -1
- package/src/git.ts +3 -8
- package/src/help.ts +39 -2
- 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/literal.ts +1 -1
- package/src/mcp/server.ts +61 -44
- package/src/prompt.ts +13 -0
- package/src/setup.ts +205 -15
package/src/setup.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { divider, fail, hint, info, printBrandBanner, success,
|
|
1
|
+
import { divider, fail, hint, info, printBrandBanner, success, writeStdout } from "./cli-ui.ts";
|
|
2
2
|
import {
|
|
3
3
|
clearStoredCredentials,
|
|
4
4
|
loadStoredCredentials,
|
|
5
5
|
readStoredCredentials,
|
|
6
6
|
resolveCredentialsPath,
|
|
7
|
+
type StoredSageMakerCredentials,
|
|
7
8
|
saveStoredCredentials,
|
|
9
|
+
saveStoredSageMakerCredentials,
|
|
8
10
|
} from "./credentials.ts";
|
|
11
|
+
import {
|
|
12
|
+
isSageMakerConfigured,
|
|
13
|
+
parseSageMakerEndpointArn,
|
|
14
|
+
type SageMakerEmbeddingConfig,
|
|
15
|
+
validateSageMakerConnection,
|
|
16
|
+
} from "./embeddings/sagemaker.ts";
|
|
9
17
|
import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
|
|
10
18
|
import { hasTakaraApiKeyInEnv, resolveEmbeddingApiKey } from "./env.ts";
|
|
11
|
-
import { promptHidden } from "./prompt.ts";
|
|
19
|
+
import { promptHidden, promptText } from "./prompt.ts";
|
|
12
20
|
import { Spinner } from "./spinner.ts";
|
|
13
21
|
|
|
14
22
|
async function promptApiKey(): Promise<string> {
|
|
@@ -26,6 +34,9 @@ export interface RunSetupOptions {
|
|
|
26
34
|
apiKey?: string;
|
|
27
35
|
force?: boolean;
|
|
28
36
|
skipValidation?: boolean;
|
|
37
|
+
sagemaker?: boolean;
|
|
38
|
+
sagemakerArn?: string;
|
|
39
|
+
profile?: string;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
export interface RunSetupResult {
|
|
@@ -33,11 +44,179 @@ export interface RunSetupResult {
|
|
|
33
44
|
newlySaved: boolean;
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
export interface ParsedSetupCliArgs {
|
|
48
|
+
apiKey?: string;
|
|
49
|
+
force: boolean;
|
|
50
|
+
clear: boolean;
|
|
51
|
+
sagemaker: boolean;
|
|
52
|
+
sagemakerArn?: string;
|
|
53
|
+
profile?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type SetupCliArgError = "clear_with_key" | "sagemaker_with_key";
|
|
57
|
+
|
|
58
|
+
/** Parse `miru setup` argv (everything after the `setup` command). */
|
|
59
|
+
export function parseSetupCliArgs(rest: string[]): {
|
|
60
|
+
args: ParsedSetupCliArgs;
|
|
61
|
+
error?: SetupCliArgError;
|
|
62
|
+
} {
|
|
63
|
+
let apiKey: string | undefined;
|
|
64
|
+
let force = false;
|
|
65
|
+
let clear = false;
|
|
66
|
+
let sagemaker = false;
|
|
67
|
+
let sagemakerArn: string | undefined;
|
|
68
|
+
let profile: string | undefined;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < rest.length; i++) {
|
|
71
|
+
const arg = rest[i];
|
|
72
|
+
if (arg === "--force") {
|
|
73
|
+
force = true;
|
|
74
|
+
} else if (arg === "--clear") {
|
|
75
|
+
clear = true;
|
|
76
|
+
} else if ((arg === "--key" || arg === "-k") && rest[i + 1]) {
|
|
77
|
+
apiKey = rest[++i];
|
|
78
|
+
} else if (arg === "--sagemaker") {
|
|
79
|
+
sagemaker = true;
|
|
80
|
+
} else if (arg === "--arn" && rest[i + 1]) {
|
|
81
|
+
sagemakerArn = rest[++i];
|
|
82
|
+
} else if (arg === "--profile" && rest[i + 1]) {
|
|
83
|
+
profile = rest[++i];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sagemakerArn) {
|
|
88
|
+
sagemaker = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const args: ParsedSetupCliArgs = {
|
|
92
|
+
apiKey,
|
|
93
|
+
force,
|
|
94
|
+
clear,
|
|
95
|
+
sagemaker,
|
|
96
|
+
sagemakerArn,
|
|
97
|
+
profile,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (clear && apiKey) {
|
|
101
|
+
return { args, error: "clear_with_key" };
|
|
102
|
+
}
|
|
103
|
+
if (sagemaker && apiKey) {
|
|
104
|
+
return { args, error: "sagemaker_with_key" };
|
|
105
|
+
}
|
|
106
|
+
return { args };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function promptSageMakerArn(): Promise<string> {
|
|
110
|
+
while (true) {
|
|
111
|
+
const arn = await promptText("SageMaker endpoint ARN");
|
|
112
|
+
if (!arn) {
|
|
113
|
+
fail("Endpoint ARN cannot be empty.");
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
parseSageMakerEndpointArn(arn);
|
|
118
|
+
return arn;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function promptAwsProfile(): Promise<string> {
|
|
126
|
+
while (true) {
|
|
127
|
+
const profile = await promptText(
|
|
128
|
+
"AWS profile name (must already exist in ~/.aws)",
|
|
129
|
+
process.env.AWS_PROFILE || "miru",
|
|
130
|
+
);
|
|
131
|
+
if (profile) {
|
|
132
|
+
return profile;
|
|
133
|
+
}
|
|
134
|
+
fail("AWS profile name cannot be empty.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Miru only ever inherits AWS credentials — it never creates, writes, or rotates an
|
|
140
|
+
* AWS profile. Set one up yourself first (e.g. `aws configure --profile miru`).
|
|
141
|
+
*/
|
|
142
|
+
export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<RunSetupResult> {
|
|
143
|
+
writeStdout("");
|
|
144
|
+
printBrandBanner(process.stderr);
|
|
145
|
+
divider("─", 48, process.stderr);
|
|
146
|
+
writeStdout("Miru will connect directly to your self-hosted SageMaker embedding endpoint.");
|
|
147
|
+
hint("Miru only inherits AWS credentials from a profile you've already configured —");
|
|
148
|
+
hint("it never creates or writes to ~/.aws. Run `aws configure --profile <name>` first.");
|
|
149
|
+
hint("This replaces any stored Takara API key — only one embedding mode is active at a time.");
|
|
150
|
+
writeStdout("");
|
|
151
|
+
|
|
152
|
+
const arnInput = options.sagemakerArn ?? (await promptSageMakerArn());
|
|
153
|
+
const parsed = parseSageMakerEndpointArn(arnInput);
|
|
154
|
+
|
|
155
|
+
let profile = options.profile;
|
|
156
|
+
if (!profile) {
|
|
157
|
+
if (!canPromptForCredentials()) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"An AWS profile name is required. Pass --profile <name>, or run " +
|
|
160
|
+
"`miru setup --sagemaker` interactively.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
profile = await promptAwsProfile();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = arnInput;
|
|
167
|
+
process.env.AWS_PROFILE = profile;
|
|
168
|
+
|
|
169
|
+
if (!options.skipValidation) {
|
|
170
|
+
const spinner = new Spinner(`Validating SageMaker endpoint (profile "${profile}")`);
|
|
171
|
+
spinner.start();
|
|
172
|
+
const config: SageMakerEmbeddingConfig = {
|
|
173
|
+
endpointName: parsed.endpointName,
|
|
174
|
+
region: parsed.region,
|
|
175
|
+
normalize: true,
|
|
176
|
+
truncate: true,
|
|
177
|
+
truncationDirection: "Right",
|
|
178
|
+
};
|
|
179
|
+
const result = await validateSageMakerConnection(config);
|
|
180
|
+
if (!result.valid) {
|
|
181
|
+
spinner.stop();
|
|
182
|
+
throw new Error(result.message);
|
|
183
|
+
}
|
|
184
|
+
spinner.succeed("SageMaker endpoint validated");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const stored: StoredSageMakerCredentials = { endpoint_arn: arnInput, profile };
|
|
188
|
+
const hadTakaraKey = Boolean((await readStoredCredentials())?.takara_api_key);
|
|
189
|
+
const path = await saveStoredSageMakerCredentials(stored);
|
|
190
|
+
writeStdout("");
|
|
191
|
+
success(`Saved SageMaker config to ${path}`);
|
|
192
|
+
if (hadTakaraKey) {
|
|
193
|
+
hint("Removed the stored Takara API key — Miru now embeds only via SageMaker.");
|
|
194
|
+
} else {
|
|
195
|
+
hint("Miru will embed via this SageMaker endpoint (Takara is not used).");
|
|
196
|
+
}
|
|
197
|
+
writeStdout("");
|
|
198
|
+
return { path, newlySaved: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
36
201
|
export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupResult> {
|
|
202
|
+
if (options.sagemaker || options.sagemakerArn) {
|
|
203
|
+
return runSageMakerSetup(options);
|
|
204
|
+
}
|
|
205
|
+
|
|
37
206
|
if (!options.force && hasTakaraApiKeyInEnv()) {
|
|
38
207
|
const path = resolveCredentialsPath();
|
|
39
208
|
const stored = await readStoredCredentials();
|
|
40
|
-
if (stored) {
|
|
209
|
+
if (stored?.sagemaker) {
|
|
210
|
+
const apiKey = resolveEmbeddingApiKey();
|
|
211
|
+
await saveStoredCredentials(apiKey);
|
|
212
|
+
process.env.TAKARA_API_KEY = apiKey;
|
|
213
|
+
writeStdout("");
|
|
214
|
+
success(`Saved credentials to ${path}`);
|
|
215
|
+
hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
|
|
216
|
+
writeStdout("");
|
|
217
|
+
return { path, newlySaved: true };
|
|
218
|
+
}
|
|
219
|
+
if (stored?.takara_api_key) {
|
|
41
220
|
info(`API key already configured (env + ${path}). Use --force to replace stored key.`);
|
|
42
221
|
return { path, newlySaved: false };
|
|
43
222
|
}
|
|
@@ -47,20 +226,23 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
|
|
|
47
226
|
|
|
48
227
|
if (!options.force) {
|
|
49
228
|
const stored = await readStoredCredentials();
|
|
50
|
-
if (stored && !options.apiKey) {
|
|
229
|
+
if (stored?.takara_api_key && !options.apiKey) {
|
|
51
230
|
info(`API key already stored at ${resolveCredentialsPath()}. Use --force to replace.`);
|
|
52
231
|
process.env.TAKARA_API_KEY = stored.takara_api_key;
|
|
53
232
|
return { path: resolveCredentialsPath(), newlySaved: false };
|
|
54
233
|
}
|
|
55
234
|
}
|
|
56
235
|
|
|
57
|
-
|
|
236
|
+
writeStdout("");
|
|
58
237
|
// stderr banner: setup runs before stdout may be a TTY (e.g. piped miru search).
|
|
59
238
|
printBrandBanner(process.stderr);
|
|
60
239
|
divider("─", 48, process.stderr);
|
|
61
|
-
|
|
240
|
+
writeStdout("Miru needs a Takara API key for code embeddings.");
|
|
62
241
|
hint("Get a bearer token from Takara, then enter it below.");
|
|
63
|
-
|
|
242
|
+
hint(
|
|
243
|
+
"This replaces any stored SageMaker endpoint — only one embedding mode is active at a time.",
|
|
244
|
+
);
|
|
245
|
+
writeStdout("");
|
|
64
246
|
|
|
65
247
|
const apiKey = options.apiKey ?? (await promptApiKey());
|
|
66
248
|
|
|
@@ -75,22 +257,27 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
|
|
|
75
257
|
spinner.succeed("API key validated");
|
|
76
258
|
}
|
|
77
259
|
|
|
260
|
+
const hadSageMaker = Boolean((await readStoredCredentials())?.sagemaker);
|
|
78
261
|
const path = await saveStoredCredentials(apiKey);
|
|
79
262
|
process.env.TAKARA_API_KEY = apiKey;
|
|
80
|
-
|
|
263
|
+
writeStdout("");
|
|
81
264
|
success(`Saved credentials to ${path}`);
|
|
82
|
-
|
|
83
|
-
|
|
265
|
+
if (hadSageMaker) {
|
|
266
|
+
hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
|
|
267
|
+
} else {
|
|
268
|
+
hint("MCP loads this key from credentials.json automatically.");
|
|
269
|
+
}
|
|
270
|
+
writeStdout("");
|
|
84
271
|
return { path, newlySaved: true };
|
|
85
272
|
}
|
|
86
273
|
|
|
87
274
|
export async function runClearCredentials(): Promise<void> {
|
|
88
275
|
const { cleared, path } = await clearStoredCredentials();
|
|
89
276
|
if (cleared) {
|
|
90
|
-
success(`Removed stored
|
|
277
|
+
success(`Removed stored credentials from ${path}`);
|
|
91
278
|
return;
|
|
92
279
|
}
|
|
93
|
-
info(`No stored
|
|
280
|
+
info(`No stored credentials at ${path}`);
|
|
94
281
|
}
|
|
95
282
|
|
|
96
283
|
export function canPromptForCredentials(): boolean {
|
|
@@ -98,6 +285,9 @@ export function canPromptForCredentials(): boolean {
|
|
|
98
285
|
}
|
|
99
286
|
|
|
100
287
|
export function hasCredentials(): boolean {
|
|
288
|
+
if (isSageMakerConfigured()) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
101
291
|
try {
|
|
102
292
|
resolveEmbeddingApiKey();
|
|
103
293
|
return true;
|
|
@@ -122,7 +312,7 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
|
|
|
122
312
|
|
|
123
313
|
const wantsPrompt = options?.interactive ?? true;
|
|
124
314
|
if (wantsPrompt && canPromptForCredentials()) {
|
|
125
|
-
|
|
315
|
+
writeStdout("");
|
|
126
316
|
info("No Takara API key found.");
|
|
127
317
|
hint("Miru needs one for embeddings — enter it below (same as `miru setup`).");
|
|
128
318
|
await runSetup();
|
|
@@ -130,10 +320,10 @@ export async function ensureCredentials(options?: { interactive?: boolean }): Pr
|
|
|
130
320
|
return;
|
|
131
321
|
}
|
|
132
322
|
|
|
133
|
-
|
|
323
|
+
writeStdout("");
|
|
134
324
|
// Brand on stderr when credentials are missing in non-interactive mode (stdout may not be a TTY).
|
|
135
325
|
printBrandBanner(process.stderr);
|
|
136
|
-
|
|
326
|
+
writeStdout("");
|
|
137
327
|
|
|
138
328
|
throw new Error(
|
|
139
329
|
"Takara API key required. Run `miru setup` in a terminal, or set TAKARA_API_KEY " +
|