@takara-ai/miru-code 1.7.7 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "1.7.7",
3
+ "version": "1.8.0",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,10 +54,15 @@
54
54
  "lint": "biome check .",
55
55
  "lint:fix": "biome check --write .",
56
56
  "prepare": "bun run scripts/install-git-hooks.ts",
57
+ "build:kiro-plugin": "bun run scripts/build-kiro-plugin.ts",
57
58
  "vendor-grammars": "bun run scripts/vendor-grammars.ts",
58
59
  "prepublishOnly": "bun run vendor-grammars && bun run typecheck && bun test",
59
60
  "benchmark:ab": "bun run scripts/search-ab-local.ts",
60
61
  "benchmark:ab-multi": "bun run scripts/search-ab-multi.ts",
62
+ "benchmark:codesearchnet-compression": "bun run scripts/benchmark-codesearchnet-compression.ts",
63
+ "benchmark:codesearchnet-alpha": "bun run scripts/calibrate-codesearchnet-alpha.ts",
64
+ "benchmark:ranking-sweep": "bun run scripts/sweep-ranking-configs.ts",
65
+ "benchmark:ranking-factorial": "bun run scripts/sweep-ranking-factorial.ts",
61
66
  "benchmark:recall": "bun run scripts/benchmark-recall-tokens.ts",
62
67
  "benchmark:vs-grep": "bun run scripts/benchmark-miru-vs-grep.ts",
63
68
  "benchmark:workflow": "bun run scripts/benchmark-workflow-tokens.ts",
@@ -71,6 +76,7 @@
71
76
  },
72
77
  "dependencies": {
73
78
  "@aws-sdk/client-sagemaker-runtime": "^3.1090.0",
79
+ "@aws-sdk/credential-provider-node": "^3.972.70",
74
80
  "ignore": "^7.0.5",
75
81
  "web-tree-sitter": "^0.26.9",
76
82
  "zod": "^4.4.3"
@@ -26,6 +26,12 @@ export const RG_EXCLUDE_ARGS = [
26
26
  "!tokenizer",
27
27
  "-g",
28
28
  "!tokenizer/**",
29
+ "-g",
30
+ "!*.map",
31
+ "-g",
32
+ "!*.min.js",
33
+ "-g",
34
+ "!*.min.css",
29
35
  ] as const;
30
36
 
31
37
  export class BenchmarkSearchTimeoutError extends Error {
@@ -236,6 +242,9 @@ async function grepRankedMatches(repoRoot: string, pattern: string, topK: number
236
242
  "--exclude-dir=node_modules",
237
243
  "--exclude-dir=.git",
238
244
  "--exclude-dir=tokenizer",
245
+ "--exclude=*.map",
246
+ "--exclude=*.min.js",
247
+ "--exclude=*.min.css",
239
248
  pattern,
240
249
  repoRoot,
241
250
  ]);
@@ -2,14 +2,15 @@
2
2
  * Compare Miru `locate` MCP response tokens vs the costlier native Grep path.
3
3
  *
4
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.
5
+ * the intermediate JSON payload. The grep baseline requests only the same
6
+ * information: counts for `count`, line locations for `locations`, and context
7
+ * for `lines`. It also mirrors locate's include/exclude scope and variants.
9
8
  */
10
9
 
10
+ import { join } from "node:path";
11
11
  import {
12
12
  DEFAULT_LITERAL_MODE,
13
+ filterChunksByGlob,
13
14
  formatLiteralLocate,
14
15
  type LiteralLocateOptions,
15
16
  type LiteralLocateResult,
@@ -18,7 +19,7 @@ import { formatLiteralLocateText } from "../mcp/format-text.ts";
18
19
  import type { MiruIndex } from "../miru-index.ts";
19
20
  import { countTokens } from "../token-count.ts";
20
21
  import { GREP_CONTEXT } from "./grep.ts";
21
- import { rgLiteralOutput } from "./rg-literal.ts";
22
+ import { rgLiteralOutput, selectComparableLiteralSearchTool } from "./rg-literal.ts";
22
23
  import { agentBenchmarkFromTokens } from "./summary.ts";
23
24
  import type { AgentBenchmarkSummary } from "./types.ts";
24
25
 
@@ -28,6 +29,8 @@ export interface LocateBenchmarkComparison {
28
29
  result: LiteralLocateResult;
29
30
  payload: Record<string, unknown>;
30
31
  benchmark: AgentBenchmarkSummary;
32
+ /** Baseline match stats; compare with `result.n`/`result.files` for exact-literal parity. */
33
+ grep: { n: number; files: number };
31
34
  latency_ms: {
32
35
  miru: number;
33
36
  grep: number;
@@ -40,6 +43,16 @@ export async function benchmarkLocateComparison(options: {
40
43
  index: MiruIndex;
41
44
  locate?: LiteralLocateOptions;
42
45
  }): Promise<LocateBenchmarkComparison> {
46
+ if (options.locate?.limit != null) {
47
+ throw new Error(
48
+ "Cannot benchmark a limited locate response against native grep: rg/grep limits are per file, while locate.limit is global. Omit limit to keep token and recall comparisons valid.",
49
+ );
50
+ }
51
+ if (!selectComparableLiteralSearchTool()) {
52
+ throw new Error(
53
+ "A comparable literal benchmark requires rg or compatible grep; findstr is not equivalent for scoped locate output.",
54
+ );
55
+ }
43
56
  const locateOpts: LiteralLocateOptions = { mode: DEFAULT_LITERAL_MODE, ...options.locate };
44
57
 
45
58
  const miruStart = performance.now();
@@ -48,22 +61,30 @@ export async function benchmarkLocateComparison(options: {
48
61
  const payload = formatLiteralLocate(result);
49
62
  const miruTok = countTokens(formatLiteralLocateText(payload));
50
63
 
51
- // Unbounded agent-style Grep dump (±context) — the costlier path without locate.
52
- // Never use less context than the caller actually requested via context_lines,
53
- // or a locate call that returns inline context looks artificially expensive
54
- // against a baseline that didn't have to fetch that context at all. Likewise,
55
- // grep every variant `match_variants`/an array `literal` actually matched —
56
- // one grep pattern isn't equivalent recall, so it isn't a fair baseline.
64
+ // Grep every variant `match_variants`/an array `literal` actually matched — one
65
+ // pattern is not equivalent recall. Keep contextual lines only for `lines` mode.
66
+ const context =
67
+ locateOpts.mode === "lines" ? Math.max(GREP_CONTEXT, locateOpts.context_lines ?? 0) : 0;
68
+ const indexedPaths = [
69
+ ...new Set(
70
+ filterChunksByGlob(options.index.chunks, locateOpts.include, locateOpts.exclude).map(
71
+ (chunk) => join(options.repoPath, chunk.file_path),
72
+ ),
73
+ ),
74
+ ];
57
75
  const grep = await rgLiteralOutput(options.repoPath, result.literals ?? [options.literal], {
58
- context: Math.max(GREP_CONTEXT, locateOpts.context_lines ?? 0),
76
+ context,
59
77
  maxCount: 0,
60
78
  ignoreCase: locateOpts.ignore_case,
79
+ countOnly: locateOpts.mode === "count",
80
+ paths: indexedPaths,
61
81
  });
62
82
 
63
83
  return {
64
84
  result,
65
85
  payload,
66
86
  benchmark: agentBenchmarkFromTokens(miruTok, grep.tokens, result.n > 0),
87
+ grep: { n: grep.n, files: grep.files },
67
88
  latency_ms: { miru: miruMs, grep: grep.latency_ms },
68
89
  };
69
90
  }
@@ -4,6 +4,9 @@ import { relative } from "node:path";
4
4
  import { countTokens } from "../token-count.ts";
5
5
  import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool, spawnBenchmarkSearch } from "./grep.ts";
6
6
 
7
+ /** Keep each native-search invocation well below platform command-line limits. */
8
+ const MAX_LITERAL_PATH_ARGUMENT_CHARS = 4_096;
9
+
7
10
  export interface RgLiteralOutput {
8
11
  text: string;
9
12
  tokens: number;
@@ -12,6 +15,52 @@ export interface RgLiteralOutput {
12
15
  files: number;
13
16
  }
14
17
 
18
+ export interface RgLiteralOptions {
19
+ context?: number;
20
+ maxCount?: number;
21
+ ignoreCase?: boolean;
22
+ /** Return per-file match counts rather than matching lines. */
23
+ countOnly?: boolean;
24
+ /** Gitignore-style globs included by the comparable locate call. */
25
+ include?: string[];
26
+ /** Gitignore-style globs excluded by the comparable locate call. */
27
+ exclude?: string[];
28
+ /** Absolute or repo-relative files in the same corpus as the compared tool. */
29
+ paths?: string[];
30
+ }
31
+
32
+ /**
33
+ * `findstr` cannot reproduce the scoped, contextual, count-mode literal baseline.
34
+ * Do not silently substitute it and report the result as an equivalent grep comparison.
35
+ */
36
+ export function selectComparableLiteralSearchTool(
37
+ options?: Parameters<typeof selectBenchmarkSearchTool>[0],
38
+ ): "rg" | "grep" | null {
39
+ const tool = selectBenchmarkSearchTool(options);
40
+ return tool === "rg" || tool === "grep" ? tool : null;
41
+ }
42
+
43
+ /** Split an indexed corpus into safe native-command argument batches. */
44
+ export function batchLiteralPaths(paths: readonly string[]): string[][] {
45
+ const batches: string[][] = [];
46
+ let batch: string[] = [];
47
+ let chars = 0;
48
+ for (const path of paths) {
49
+ const nextChars = chars + path.length + 1;
50
+ if (batch.length > 0 && nextChars > MAX_LITERAL_PATH_ARGUMENT_CHARS) {
51
+ batches.push(batch);
52
+ batch = [];
53
+ chars = 0;
54
+ }
55
+ batch.push(path);
56
+ chars += path.length + 1;
57
+ }
58
+ if (batch.length > 0) {
59
+ batches.push(batch);
60
+ }
61
+ return batches;
62
+ }
63
+
15
64
  /** Count match lines (path:line:…) and unique files from `rg -n` output. */
16
65
  export function parseRgLiteralStats(text: string, repoRoot: string): { n: number; files: number } {
17
66
  const files = new Set<string>();
@@ -59,29 +108,69 @@ function parsePathLinePrefix(line: string): { path: string; line: number } | nul
59
108
  return null;
60
109
  }
61
110
 
111
+ function parseCountStats(text: string, repoRoot: string): { n: number; files: number } {
112
+ let n = 0;
113
+ let files = 0;
114
+ for (const line of text.split("\n")) {
115
+ const colon = line.lastIndexOf(":");
116
+ if (colon < 0) {
117
+ continue;
118
+ }
119
+ const count = Number(line.slice(colon + 1));
120
+ if (!Number.isFinite(count) || count <= 0) {
121
+ continue;
122
+ }
123
+ // Ensure this is a path under the requested root, rather than an arbitrary line.
124
+ const path = line.slice(0, colon);
125
+ if (relative(repoRoot, path).startsWith("..")) {
126
+ continue;
127
+ }
128
+ n += count;
129
+ files += 1;
130
+ }
131
+ return { n, files };
132
+ }
133
+
62
134
  export async function rgLiteralOutput(
63
135
  repoRoot: string,
64
136
  literal: string | readonly string[],
65
- options: {
66
- context?: number;
67
- maxCount?: number;
68
- ignoreCase?: boolean;
69
- } = {},
137
+ options: RgLiteralOptions = {},
70
138
  ): Promise<RgLiteralOutput> {
71
139
  const literals = Array.isArray(literal) ? literal : [literal as string];
72
140
  const context = options.context ?? 0;
73
141
  const maxCount = options.maxCount ?? 20;
74
- const tool = selectBenchmarkSearchTool();
142
+ const tool = selectComparableLiteralSearchTool();
75
143
  if (!tool) {
76
- throw new Error("No search tool found in PATH (tried rg, grep, and findstr on Windows)");
144
+ throw new Error(
145
+ "A comparable literal benchmark requires rg or compatible grep; findstr cannot preserve locate scope, context, and count semantics.",
146
+ );
77
147
  }
78
148
  const start = performance.now();
79
- const text = await spawnBenchmarkSearch(
80
- buildLiteralArgs(tool, repoRoot, literals, context, maxCount, !!options.ignoreCase),
81
- tool === "findstr" ? repoRoot : undefined,
82
- );
149
+ const pathBatches = options.paths?.length ? batchLiteralPaths(options.paths) : [undefined];
150
+ const output: string[] = [];
151
+ for (const paths of pathBatches) {
152
+ output.push(
153
+ await spawnBenchmarkSearch(
154
+ buildLiteralArgs(
155
+ tool,
156
+ repoRoot,
157
+ literals,
158
+ context,
159
+ maxCount,
160
+ !!options.ignoreCase,
161
+ options.countOnly,
162
+ options.include,
163
+ options.exclude,
164
+ paths,
165
+ ),
166
+ ),
167
+ );
168
+ }
169
+ const text = output.filter(Boolean).join("\n");
83
170
  const latency_ms = performance.now() - start;
84
- const stats = parseRgLiteralStats(text, repoRoot);
171
+ const stats = options.countOnly
172
+ ? parseCountStats(text, repoRoot)
173
+ : parseRgLiteralStats(text, repoRoot);
85
174
  return { text, tokens: countTokens(text), latency_ms, ...stats };
86
175
  }
87
176
 
@@ -90,15 +179,28 @@ export async function rgLiteralOutput(
90
179
  * to get the same recall as `literals`/`match_variants` (one pattern isn't equivalent).
91
180
  */
92
181
  function buildLiteralArgs(
93
- tool: "rg" | "grep" | "findstr",
182
+ tool: "rg" | "grep",
94
183
  repoRoot: string,
95
184
  literals: readonly string[],
96
185
  context: number,
97
186
  maxCount: number,
98
187
  ignoreCase: boolean,
188
+ countOnly = false,
189
+ include: string[] = [],
190
+ exclude: string[] = [],
191
+ paths?: string[],
99
192
  ): string[] {
100
193
  if (tool === "rg") {
101
194
  const args = ["rg", "-F", "-n", "--no-heading", ...RG_EXCLUDE_ARGS];
195
+ if (countOnly) {
196
+ args.splice(2, 2, "--count");
197
+ }
198
+ for (const glob of include) {
199
+ args.push("-g", glob);
200
+ }
201
+ for (const glob of exclude) {
202
+ args.push("-g", `!${glob}`);
203
+ }
102
204
  if (ignoreCase) {
103
205
  args.push("-i");
104
206
  }
@@ -111,7 +213,7 @@ function buildLiteralArgs(
111
213
  for (const l of literals) {
112
214
  args.push("-e", l);
113
215
  }
114
- args.push(repoRoot);
216
+ args.push(...(paths?.length ? paths : [repoRoot]));
115
217
  return args;
116
218
  }
117
219
  if (tool === "grep") {
@@ -124,7 +226,19 @@ function buildLiteralArgs(
124
226
  "--exclude-dir=node_modules",
125
227
  "--exclude-dir=.git",
126
228
  "--exclude-dir=tokenizer",
229
+ "--exclude=*.map",
230
+ "--exclude=*.min.js",
231
+ "--exclude=*.min.css",
127
232
  ];
233
+ for (const glob of include) {
234
+ args.push(`--include=${glob}`);
235
+ }
236
+ for (const glob of exclude) {
237
+ args.push(`--exclude=${glob}`);
238
+ }
239
+ if (countOnly) {
240
+ args.push("-c");
241
+ }
128
242
  if (ignoreCase) {
129
243
  args.push("-i");
130
244
  }
@@ -137,16 +251,8 @@ function buildLiteralArgs(
137
251
  for (const l of literals) {
138
252
  args.push("-e", l);
139
253
  }
140
- args.push(repoRoot);
254
+ args.push(...(paths?.length ? paths : [repoRoot]));
141
255
  return args;
142
256
  }
143
- const args = ["findstr", "/S", "/N", "/P"];
144
- for (const l of literals) {
145
- args.push(`/C:${l}`);
146
- }
147
- args.push("*");
148
- if (ignoreCase) {
149
- args.splice(4, 0, "/I");
150
- }
151
- return args;
257
+ throw new Error(`Unsupported comparable literal benchmark tool: ${tool}`);
152
258
  }
@@ -1,22 +1,38 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import type { InvokeEndpointCommandOutput } from "@aws-sdk/client-sagemaker-runtime";
2
5
  import type { EmbeddingClient, EmbeddingPayload, EmbeddingResponse } from "./openai.ts";
3
6
 
4
7
  type SageMakerRuntimeModule = typeof import("@aws-sdk/client-sagemaker-runtime");
8
+ type AwsCredentialProviderModule = typeof import("@aws-sdk/credential-provider-node");
9
+ type StaticAwsCredentials = {
10
+ accessKeyId: string;
11
+ secretAccessKey: string;
12
+ sessionToken?: string;
13
+ };
5
14
 
6
15
  /** Lazily loaded so Takara-only MCP/CLI processes never pay the AWS SDK startup cost. */
7
16
  let sageMakerRuntimePromise: Promise<SageMakerRuntimeModule> | null = null;
17
+ let awsCredentialProviderPromise: Promise<AwsCredentialProviderModule> | null = null;
8
18
 
9
19
  function loadSageMakerRuntime(): Promise<SageMakerRuntimeModule> {
10
20
  sageMakerRuntimePromise ??= import("@aws-sdk/client-sagemaker-runtime");
11
21
  return sageMakerRuntimePromise;
12
22
  }
13
23
 
24
+ function loadAwsCredentialProvider(): Promise<AwsCredentialProviderModule> {
25
+ awsCredentialProviderPromise ??= import("@aws-sdk/credential-provider-node");
26
+ return awsCredentialProviderPromise;
27
+ }
28
+
14
29
  /** arn:aws:sagemaker:<region>:<account-id>:endpoint/<endpoint-name> (also covers aws-cn/aws-us-gov). */
15
30
  const ENDPOINT_ARN_PATTERN = /^arn:aws[a-z0-9-]*:sagemaker:([a-z0-9-]+):(\d{12}):endpoint\/(.+)$/;
16
31
 
17
32
  export interface SageMakerEmbeddingConfig {
18
33
  endpointName: string;
19
34
  region: string;
35
+ profile?: string;
20
36
  normalize: boolean;
21
37
  truncate: boolean;
22
38
  truncationDirection: "Left" | "Right";
@@ -54,6 +70,103 @@ function resolveTruncationDirection(): "Left" | "Right" {
54
70
  return process.env.MIRU_SAGEMAKER_TRUNCATION_DIRECTION?.trim() === "Left" ? "Left" : "Right";
55
71
  }
56
72
 
73
+ function parseIniProfiles(contents: string): Map<string, Record<string, string>> {
74
+ const profiles = new Map<string, Record<string, string>>();
75
+ let section: Record<string, string> | undefined;
76
+
77
+ for (const line of contents.split(/\r?\n/)) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) {
80
+ continue;
81
+ }
82
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
83
+ section = {};
84
+ profiles.set(trimmed.slice(1, -1).trim().toLowerCase(), section);
85
+ continue;
86
+ }
87
+ if (!section) {
88
+ continue;
89
+ }
90
+ const separator = trimmed.indexOf("=");
91
+ if (separator <= 0) {
92
+ continue;
93
+ }
94
+ const key = trimmed.slice(0, separator).trim().toLowerCase();
95
+ const value = trimmed.slice(separator + 1).trim();
96
+ section[key] = value;
97
+ }
98
+
99
+ return profiles;
100
+ }
101
+
102
+ /**
103
+ * AWS CLI accepts credential-key casing that the AWS SDK's INI parser does not.
104
+ * Keep this fallback deliberately limited to static credentials; SSO, role, and
105
+ * credential_process profiles remain handled by the SDK's normal provider chain.
106
+ */
107
+ export function parseCaseInsensitiveStaticCredentials(
108
+ configContents: string,
109
+ credentialsContents: string,
110
+ profileName: string,
111
+ ): StaticAwsCredentials | null {
112
+ const profile = profileName.trim().toLowerCase();
113
+ if (!profile) {
114
+ return null;
115
+ }
116
+
117
+ const merged: Record<string, string> = {};
118
+ for (const [sectionName, values] of parseIniProfiles(configContents)) {
119
+ if (sectionName === profile || sectionName === `profile ${profile}`) {
120
+ Object.assign(merged, values);
121
+ }
122
+ }
123
+ for (const [sectionName, values] of parseIniProfiles(credentialsContents)) {
124
+ if (sectionName === profile) {
125
+ Object.assign(merged, values);
126
+ }
127
+ }
128
+
129
+ const accessKeyId = merged.aws_access_key_id;
130
+ const secretAccessKey = merged.aws_secret_access_key;
131
+ if (!accessKeyId || !secretAccessKey) {
132
+ return null;
133
+ }
134
+ return {
135
+ accessKeyId,
136
+ secretAccessKey,
137
+ sessionToken: merged.aws_session_token,
138
+ };
139
+ }
140
+
141
+ function resolveAwsHome(): string {
142
+ return process.env.HOME || process.env.USERPROFILE || homedir();
143
+ }
144
+
145
+ function resolveAwsFilePath(variable: string, fallback: string): string {
146
+ const configured = process.env[variable] || fallback;
147
+ return configured.startsWith("~/") ? join(resolveAwsHome(), configured.slice(2)) : configured;
148
+ }
149
+
150
+ async function loadCaseInsensitiveStaticCredentials(
151
+ profileName: string,
152
+ ): Promise<StaticAwsCredentials | null> {
153
+ const home = resolveAwsHome();
154
+ const configPath = resolveAwsFilePath("AWS_CONFIG_FILE", join(home, ".aws", "config"));
155
+ const credentialsPath = resolveAwsFilePath(
156
+ "AWS_SHARED_CREDENTIALS_FILE",
157
+ join(home, ".aws", "credentials"),
158
+ );
159
+
160
+ const [configResult, credentialsResult] = await Promise.allSettled([
161
+ readFile(configPath, "utf8"),
162
+ readFile(credentialsPath, "utf8"),
163
+ ]);
164
+ const configContents = configResult.status === "fulfilled" ? configResult.value : "";
165
+ const credentialsContents =
166
+ credentialsResult.status === "fulfilled" ? credentialsResult.value : "";
167
+ return parseCaseInsensitiveStaticCredentials(configContents, credentialsContents, profileName);
168
+ }
169
+
57
170
  /**
58
171
  * Reads MIRU_SAGEMAKER_ENDPOINT_ARN (or MIRU_SAGEMAKER_ENDPOINT_NAME + a region) to enable
59
172
  * self-hosted mode. AWS credentials are resolved by the SDK's default provider chain
@@ -91,6 +204,7 @@ export function resolveSageMakerConfig(): SageMakerEmbeddingConfig | null {
91
204
  return {
92
205
  endpointName,
93
206
  region,
207
+ profile: process.env.AWS_PROFILE?.trim() || undefined,
94
208
  normalize: envBool("MIRU_SAGEMAKER_NORMALIZE", true),
95
209
  truncate: envBool("MIRU_SAGEMAKER_TRUNCATE", true),
96
210
  truncationDirection: resolveTruncationDirection(),
@@ -108,6 +222,18 @@ export const SAGEMAKER_AUTH_ERROR_MESSAGE =
108
222
  "(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN or AWS_PROFILE) and that the " +
109
223
  "identity has sagemaker:InvokeEndpoint permission on the endpoint.";
110
224
 
225
+ /** Shown when the endpoint name in the stored/env config no longer exists. */
226
+ export const SAGEMAKER_NOT_FOUND_ERROR_MESSAGE =
227
+ "SageMaker endpoint not found — it may have been deleted, renamed, or deployed in a " +
228
+ "different region. Confirm the endpoint still exists, then run `miru setup --sagemaker` " +
229
+ "again to re-authenticate and re-point Miru at the correct endpoint.";
230
+
231
+ /** Shown for DNS/connection-level failures reaching the SageMaker Runtime API itself. */
232
+ export const SAGEMAKER_UNREACHABLE_ERROR_MESSAGE =
233
+ "Could not reach the SageMaker endpoint (network error). Check your network/VPN/VPC " +
234
+ "access and that the configured region is correct, then run `miru setup --sagemaker` " +
235
+ "again to re-authenticate and re-validate the connection.";
236
+
111
237
  const AUTH_EXCEPTION_NAMES = new Set([
112
238
  "AccessDeniedException",
113
239
  "UnrecognizedClientException",
@@ -115,6 +241,15 @@ const AUTH_EXCEPTION_NAMES = new Set([
115
241
  "CredentialsProviderError",
116
242
  ]);
117
243
 
244
+ /** Node/undici connection-level error codes — the request never reached AWS. */
245
+ const NETWORK_ERROR_CODES = new Set([
246
+ "ENOTFOUND",
247
+ "ECONNREFUSED",
248
+ "ECONNRESET",
249
+ "ETIMEDOUT",
250
+ "EAI_AGAIN",
251
+ ]);
252
+
118
253
  class SageMakerInvokeError extends Error {
119
254
  readonly status: number;
120
255
 
@@ -125,10 +260,11 @@ class SageMakerInvokeError extends Error {
125
260
  }
126
261
  }
127
262
 
128
- function toInvokeError(err: unknown): SageMakerInvokeError {
263
+ export function toInvokeError(err: unknown): SageMakerInvokeError {
129
264
  const awsErr = err as {
130
265
  name?: string;
131
266
  message?: string;
267
+ code?: string;
132
268
  $metadata?: { httpStatusCode?: number };
133
269
  OriginalStatusCode?: string | number;
134
270
  OriginalMessage?: string;
@@ -138,6 +274,17 @@ function toInvokeError(err: unknown): SageMakerInvokeError {
138
274
  return new SageMakerInvokeError(403, SAGEMAKER_AUTH_ERROR_MESSAGE);
139
275
  }
140
276
 
277
+ // SageMaker returns a generic ValidationError for both "endpoint doesn't exist" and
278
+ // malformed-request cases — only the message distinguishes them.
279
+ if (awsErr?.name === "ValidationError" && /not found/i.test(awsErr?.message ?? "")) {
280
+ return new SageMakerInvokeError(404, SAGEMAKER_NOT_FOUND_ERROR_MESSAGE);
281
+ }
282
+
283
+ const errorCode = awsErr?.code ?? (err as { cause?: { code?: string } })?.cause?.code;
284
+ if (errorCode && NETWORK_ERROR_CODES.has(errorCode)) {
285
+ return new SageMakerInvokeError(503, SAGEMAKER_UNREACHABLE_ERROR_MESSAGE);
286
+ }
287
+
141
288
  // ModelError wraps the embedding container's real status/message (e.g. 424 when the
142
289
  // deployed model isn't an embedding model) behind SageMaker's own 424 response.
143
290
  const originalStatus =
@@ -206,8 +353,24 @@ export function createSageMakerClient(config: SageMakerEmbeddingConfig): Embeddi
206
353
  _model: string,
207
354
  dimensions?: number,
208
355
  ): Promise<EmbeddingResponse> {
209
- const { InvokeEndpointCommand, SageMakerRuntimeClient } = await loadSageMakerRuntime();
210
- client ??= new SageMakerRuntimeClient({ region: config.region });
356
+ const [{ InvokeEndpointCommand, SageMakerRuntimeClient }, { defaultProvider }] =
357
+ await Promise.all([loadSageMakerRuntime(), loadAwsCredentialProvider()]);
358
+ if (!client) {
359
+ const profile = config.profile?.trim() || process.env.AWS_PROFILE?.trim();
360
+ const sdkCredentials = defaultProvider({ profile });
361
+ const credentials = async (): Promise<StaticAwsCredentials> => {
362
+ try {
363
+ return await sdkCredentials();
364
+ } catch (error) {
365
+ const fallback = profile ? await loadCaseInsensitiveStaticCredentials(profile) : null;
366
+ if (fallback) {
367
+ return fallback;
368
+ }
369
+ throw error;
370
+ }
371
+ };
372
+ client = new SageMakerRuntimeClient({ region: config.region, credentials });
373
+ }
211
374
 
212
375
  const body = buildRequestBody(config, input, dimensions);
213
376
 
package/src/literal.ts CHANGED
@@ -124,7 +124,7 @@ function expandLiterals(literals: readonly string[], matchVariants: boolean): st
124
124
  return [...seen];
125
125
  }
126
126
 
127
- function filterChunksByGlob(
127
+ export function filterChunksByGlob(
128
128
  chunks: readonly Chunk[],
129
129
  include: string[] | undefined,
130
130
  exclude: string[] | undefined,