@takara-ai/miru-code 1.10.0 → 1.11.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 -1
- package/docs/self-hosted-sagemaker.md +24 -0
- package/package.json +1 -1
- package/src/credentials.ts +29 -0
- package/src/mcp/auth-tool.ts +93 -33
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ Plugin installs don't carry full CLI parity — what each one gives you depends
|
|
|
48
48
|
| `miru` / Caveman / STE skills | ✅ | ✅ | ✅ |
|
|
49
49
|
| Dedicated sub-agent (`miru:miru-code`) | ✅ | — | — |
|
|
50
50
|
| Benchmark mode toggle (`/plugin configure`) | ✅ | — | — |
|
|
51
|
+
| Self-hosted SageMaker embeddings (`/plugin configure`) | ✅ | — | — |
|
|
51
52
|
| Credentials | own plugin-scoped dir | own plugin-scoped dir | n/a |
|
|
52
53
|
|
|
53
54
|
`—` means the IDE's plugin schema has no equivalent mechanism to port these to (not a packaging gap we can close): Codex's and Cursor's plugin manifests have no `agents` or `userConfig` fields, so the dedicated sub-agent and the benchmark toggle are Claude-Code-only.
|
|
@@ -323,7 +324,7 @@ Miru sends **file contents** to the [Takara inference API](https://takara.ai) wh
|
|
|
323
324
|
|
|
324
325
|
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.
|
|
325
326
|
|
|
326
|
-
Enterprise self-hosted embeddings (no Takara egress): see [docs/self-hosted-sagemaker.md](docs/self-hosted-sagemaker.md).
|
|
327
|
+
Enterprise self-hosted embeddings (no Takara egress): see [docs/self-hosted-sagemaker.md](docs/self-hosted-sagemaker.md), including the [Claude Code plugin settings](docs/self-hosted-sagemaker.md#claude-code-plugin).
|
|
327
328
|
|
|
328
329
|
## Benchmark mode
|
|
329
330
|
|
|
@@ -174,6 +174,28 @@ After that, indexing and search embed via SageMaker — no egress to `infer.taka
|
|
|
174
174
|
|
|
175
175
|
If `TAKARA_API_KEY` is still set in your shell or `.env.local`, remove it there too so it cannot conflict later.
|
|
176
176
|
|
|
177
|
+
### Claude Code plugin
|
|
178
|
+
|
|
179
|
+
The plugin has no terminal step. Do steps 1–3 first, then set the plugin settings. Run `/plugin`, open **miru**, and set:
|
|
180
|
+
|
|
181
|
+
| Setting | Value |
|
|
182
|
+
| ------------------------ | ---------------------------------------------------------------------------------------- |
|
|
183
|
+
| `SageMaker endpoint ARN` | The `EndpointArn` output from step 2 |
|
|
184
|
+
| `AWS profile` | The profile from step 3. It must already exist in `~/.aws`; Miru never creates one. |
|
|
185
|
+
|
|
186
|
+
Or from a shell, then restart Claude Code:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
echo '{"sagemaker_endpoint_arn":"arn:aws:sagemaker:<region>:<account-id>:endpoint/<name>","aws_profile":"miru"}' \
|
|
190
|
+
| claude plugin configure miru@miru --values-stdin
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
When the ARN is set, it **takes priority over any stored Takara sign-in** and Miru does not contact `infer.takara.ai`. Nothing is deleted, so clearing the ARN returns you to Takara. Leaving the ARN blank means Takara is used. The profile is only read when an ARN is set.
|
|
194
|
+
|
|
195
|
+
If the profile uses SSO, run `aws sso login --profile <name>` again whenever the token expires.
|
|
196
|
+
|
|
197
|
+
The plugin's `auth` tool only handles Takara sign-in; SageMaker is configured through these settings only.
|
|
198
|
+
|
|
177
199
|
## Switch back to Takara
|
|
178
200
|
|
|
179
201
|
```bash
|
|
@@ -184,6 +206,8 @@ Enter your Takara API key. Setup validates it, saves it, and **deletes any store
|
|
|
184
206
|
|
|
185
207
|
Also unset any `MIRU_SAGEMAKER_*` variables if you set them in the environment or `.env.local`.
|
|
186
208
|
|
|
209
|
+
In the Claude Code plugin, clear the **SageMaker endpoint ARN** setting and restart.
|
|
210
|
+
|
|
187
211
|
## Environment alternatives
|
|
188
212
|
|
|
189
213
|
Prefer `miru setup --sagemaker` so auth is checked automatically. You can also set:
|
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -116,6 +116,32 @@ function clearTakaraEnv(): void {
|
|
|
116
116
|
delete process.env.TAKARA_API_KEY;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** A plugin `userConfig` value, or undefined when blank or left as an unsubstituted `${...}`. */
|
|
120
|
+
function pluginConfigValue(name: string): string | undefined {
|
|
121
|
+
const value = process.env[name]?.trim();
|
|
122
|
+
return value && !isUnsubstitutedPlaceholder(value) ? value : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Plugin settings (`sagemaker_endpoint_arn` / `aws_profile` in plugin.json) arrive as
|
|
127
|
+
* MIRU_PLUGIN_* env vars. When an endpoint is configured there it is an explicit choice, so it
|
|
128
|
+
* wins over any stored credentials; Takara env is dropped because the modes are exclusive.
|
|
129
|
+
*/
|
|
130
|
+
function applyPluginSageMakerConfig(): boolean {
|
|
131
|
+
const arn = pluginConfigValue("MIRU_PLUGIN_SAGEMAKER_ENDPOINT_ARN");
|
|
132
|
+
if (!arn) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = arn;
|
|
136
|
+
const profile = pluginConfigValue("MIRU_PLUGIN_AWS_PROFILE");
|
|
137
|
+
if (profile) {
|
|
138
|
+
process.env.AWS_PROFILE = profile;
|
|
139
|
+
}
|
|
140
|
+
clearTakaraEnv();
|
|
141
|
+
activeStoredToken = null;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
function hydrateSageMakerEnv(sagemaker: StoredSageMakerCredentials): void {
|
|
120
146
|
process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = sagemaker.endpoint_arn;
|
|
121
147
|
if (sagemaker.profile && !process.env.AWS_PROFILE) {
|
|
@@ -144,6 +170,9 @@ function markLoadedToken(token: string): void {
|
|
|
144
170
|
/** Hydrate TAKARA_API_KEY / SageMaker env vars from the credentials file when env is unset. */
|
|
145
171
|
export async function loadStoredCredentials(): Promise<boolean> {
|
|
146
172
|
normalizeTakaraApiKeyEnv();
|
|
173
|
+
if (applyPluginSageMakerConfig()) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
147
176
|
const stored = await readStoredCredentials();
|
|
148
177
|
if (!stored) {
|
|
149
178
|
return false;
|
package/src/mcp/auth-tool.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
import {
|
|
3
3
|
checkDeviceAuthorizationOnce,
|
|
4
4
|
type DeviceAuthConfig,
|
|
5
|
+
type DeviceAuthorizationCheck,
|
|
5
6
|
type DeviceAuthorizationStart,
|
|
6
7
|
openBrowserForDeviceLogin,
|
|
7
8
|
resolveDeviceAuthConfig,
|
|
@@ -15,24 +16,37 @@ import type { MiruMcpServer } from "./runtime.ts";
|
|
|
15
16
|
type ToolResult = ReturnType<typeof toolText>;
|
|
16
17
|
|
|
17
18
|
const CHECK_HINT =
|
|
18
|
-
|
|
19
|
+
"If the user has not approved yet, call `auth` again to keep waiting; it will not open a second tab.";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How long one `auth` call waits for the user to approve in the browser. Kept well under
|
|
23
|
+
* typical MCP client tool timeouts; a slower login just takes another call.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_WAIT_MS = 45_000;
|
|
26
|
+
const SLOW_DOWN_STEP_MS = 5_000;
|
|
27
|
+
/** The server may advertise `interval: 0`; never poll the token endpoint faster than this. */
|
|
28
|
+
const MIN_POLL_INTERVAL_MS = 1_000;
|
|
19
29
|
|
|
20
30
|
/** Appended to credentials failures so an agent knows Miru's own recovery step. */
|
|
21
31
|
const RECOVERY_HINT =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
32
|
+
"Miru could not authorize its current credentials. Call the `auth` tool to open the Takara " +
|
|
33
|
+
"device-login page; it waits for the user to approve. If access is still denied after " +
|
|
34
|
+
"signing in, check the account token balance.";
|
|
25
35
|
|
|
26
36
|
const AUTH_TOOL_DESCRIPTION =
|
|
27
37
|
"Sign in with Takara credentials via device-code login — no terminal required. " +
|
|
28
38
|
"Only call this in direct response to a tool error mentioning missing, expired, rejected, or " +
|
|
29
39
|
"invalid credentials — never speculatively, since it starts a real sign-in prompt for the " +
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
"user. Call with no arguments: it opens the device-login page in the user's browser, then " +
|
|
41
|
+
"waits (up to about 45 seconds) for them to approve and returns once they have. If it " +
|
|
42
|
+
'returns "still waiting", tell the user the URL and code and call `auth` again.';
|
|
32
43
|
|
|
33
44
|
/** Browser opener seam; the real one spawns a detached `open`/`xdg-open`/`start`. */
|
|
34
45
|
export type BrowserOpener = (url: string) => boolean;
|
|
35
46
|
|
|
47
|
+
/** Sleep seam so tests can drive the wait loop without real delays. */
|
|
48
|
+
export type Sleeper = (ms: number) => Promise<void>;
|
|
49
|
+
|
|
36
50
|
/** Opening is on unless MIRU_OPEN_BROWSER is set to something other than "1" (matches `miru setup`). */
|
|
37
51
|
function openIfAllowed(open: BrowserOpener, url: string): boolean {
|
|
38
52
|
const raw = process.env.MIRU_OPEN_BROWSER;
|
|
@@ -42,8 +56,14 @@ function openIfAllowed(open: BrowserOpener, url: string): boolean {
|
|
|
42
56
|
return open(url);
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
/** `opened` is null when resuming an existing login (no new tab was opened). */
|
|
60
|
+
function approvalText(link: string, userCode: string, opened: boolean | null): string {
|
|
61
|
+
const lead =
|
|
62
|
+
opened === null
|
|
63
|
+
? `Still waiting for approval at ${link}`
|
|
64
|
+
: opened
|
|
65
|
+
? `A browser tab is open at ${link}`
|
|
66
|
+
: `Ask the user to open ${link}`;
|
|
47
67
|
return `${lead}. Code: ${userCode}. ${CHECK_HINT}`;
|
|
48
68
|
}
|
|
49
69
|
|
|
@@ -71,11 +91,17 @@ function isExpired(entry: PendingDeviceAuth): boolean {
|
|
|
71
91
|
class AuthToolState {
|
|
72
92
|
private pending: PendingDeviceAuth | null = null;
|
|
73
93
|
|
|
74
|
-
constructor(
|
|
94
|
+
constructor(
|
|
95
|
+
private readonly openBrowser: BrowserOpener,
|
|
96
|
+
private readonly waitMs: number,
|
|
97
|
+
private readonly sleep: Sleeper,
|
|
98
|
+
) {}
|
|
75
99
|
|
|
76
100
|
async start(): Promise<ToolResult> {
|
|
77
101
|
let pending = this.pending;
|
|
102
|
+
let fresh = false;
|
|
78
103
|
if (!pending || isExpired(pending)) {
|
|
104
|
+
fresh = true;
|
|
79
105
|
const config = resolveDeviceAuthConfig();
|
|
80
106
|
const start = await startDeviceAuthorization({ config });
|
|
81
107
|
pending = { start, config, startedAtMs: Date.now() };
|
|
@@ -84,21 +110,33 @@ class AuthToolState {
|
|
|
84
110
|
|
|
85
111
|
const { verificationUriComplete, verificationUri, userCode } = pending.start;
|
|
86
112
|
const link = verificationUriComplete ?? verificationUri;
|
|
87
|
-
//
|
|
88
|
-
|
|
113
|
+
// Only a new login opens a tab; a repeat call just resumes the wait.
|
|
114
|
+
const opened = fresh ? openIfAllowed(this.openBrowser, link) : null;
|
|
115
|
+
|
|
116
|
+
const settled = await this.waitForApproval(pending);
|
|
117
|
+
return settled ?? toolText(approvalText(link, userCode, opened));
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
/** Poll until approved, denied or expired, or the wait budget runs out (then null). */
|
|
121
|
+
private async waitForApproval(entry: PendingDeviceAuth): Promise<ToolResult | null> {
|
|
122
|
+
const deadline = Date.now() + this.waitMs;
|
|
123
|
+
let intervalMs = Math.max(entry.start.interval * 1000, MIN_POLL_INTERVAL_MS);
|
|
124
|
+
while (Date.now() + intervalMs <= deadline) {
|
|
125
|
+
await this.sleep(intervalMs);
|
|
126
|
+
const result = await checkDeviceAuthorizationOnce(entry.start, { config: entry.config });
|
|
127
|
+
const settled = await this.settle(result);
|
|
128
|
+
if (settled) {
|
|
129
|
+
return settled;
|
|
130
|
+
}
|
|
131
|
+
if (result.status === "slow_down") {
|
|
132
|
+
intervalMs += SLOW_DOWN_STEP_MS;
|
|
133
|
+
}
|
|
94
134
|
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
95
137
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// `pending` intact — a transient failure shouldn't force the user to restart
|
|
99
|
-
// the whole login, just retry the check.
|
|
100
|
-
const result = await checkDeviceAuthorizationOnce(start, { config });
|
|
101
|
-
|
|
138
|
+
/** Handle a final poll result; null means the login is still pending. */
|
|
139
|
+
private async settle(result: DeviceAuthorizationCheck): Promise<ToolResult | null> {
|
|
102
140
|
switch (result.status) {
|
|
103
141
|
case "success": {
|
|
104
142
|
this.pending = null;
|
|
@@ -114,31 +152,51 @@ class AuthToolState {
|
|
|
114
152
|
setStoredCredentialsEnvToken(tokens.accessToken);
|
|
115
153
|
return toolText("Signed in successfully. Miru tools are now ready to use.");
|
|
116
154
|
}
|
|
117
|
-
case "pending":
|
|
118
|
-
return toolText(
|
|
119
|
-
'Still waiting for approval. Ask the user to confirm they clicked and approved, then call `auth` again with action "check".',
|
|
120
|
-
);
|
|
121
|
-
case "slow_down":
|
|
122
|
-
return toolText(
|
|
123
|
-
'Checking too soon — wait a bit before calling `auth` again with action "check".',
|
|
124
|
-
);
|
|
125
155
|
case "denied":
|
|
126
156
|
this.pending = null;
|
|
127
|
-
return toolText(
|
|
157
|
+
return toolText("Sign-in was denied. Call `auth` again to try again.");
|
|
128
158
|
case "expired":
|
|
129
159
|
this.pending = null;
|
|
130
160
|
return toolText(
|
|
131
|
-
|
|
161
|
+
"The device code expired before it was approved. Call `auth` again to get a new one.",
|
|
132
162
|
);
|
|
163
|
+
default:
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async check(): Promise<ToolResult> {
|
|
169
|
+
if (!this.pending) {
|
|
170
|
+
return toolText('No device login is pending. Call `auth` with action "start" first.');
|
|
133
171
|
}
|
|
172
|
+
|
|
173
|
+
const { start, config } = this.pending;
|
|
174
|
+
// Errors (network failure, unrecognized OAuth error code) intentionally leave
|
|
175
|
+
// `pending` intact — a transient failure shouldn't force the user to restart
|
|
176
|
+
// the whole login, just retry the check.
|
|
177
|
+
const result = await checkDeviceAuthorizationOnce(start, { config });
|
|
178
|
+
|
|
179
|
+
const settled = await this.settle(result);
|
|
180
|
+
if (settled) {
|
|
181
|
+
return settled;
|
|
182
|
+
}
|
|
183
|
+
return toolText(
|
|
184
|
+
result.status === "slow_down"
|
|
185
|
+
? "Checking too soon — wait a bit before calling `auth` again."
|
|
186
|
+
: "Still waiting for approval. Ask the user to confirm they clicked and approved, then call `auth` again.",
|
|
187
|
+
);
|
|
134
188
|
}
|
|
135
189
|
}
|
|
136
190
|
|
|
137
191
|
export function registerAuthTool(
|
|
138
192
|
server: MiruMcpServer,
|
|
139
|
-
options?: { openBrowser?: BrowserOpener },
|
|
193
|
+
options?: { openBrowser?: BrowserOpener; waitMs?: number; sleep?: Sleeper },
|
|
140
194
|
): void {
|
|
141
|
-
const state = new AuthToolState(
|
|
195
|
+
const state = new AuthToolState(
|
|
196
|
+
options?.openBrowser ?? openBrowserForDeviceLogin,
|
|
197
|
+
options?.waitMs ?? DEFAULT_WAIT_MS,
|
|
198
|
+
options?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
199
|
+
);
|
|
142
200
|
|
|
143
201
|
server.registerTool(
|
|
144
202
|
"auth",
|
|
@@ -148,7 +206,9 @@ export function registerAuthTool(
|
|
|
148
206
|
action: z
|
|
149
207
|
.enum(["start", "check"])
|
|
150
208
|
.optional()
|
|
151
|
-
.describe(
|
|
209
|
+
.describe(
|
|
210
|
+
'"start" begins or resumes a device-code login and waits for approval (default); "check" is a single non-blocking poll.',
|
|
211
|
+
),
|
|
152
212
|
},
|
|
153
213
|
},
|
|
154
214
|
async ({ action }) => {
|