@takara-ai/miru-code 1.10.0 → 1.10.1

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.10.0",
3
+ "version": "1.10.1",
4
4
  "mcpName": "ai.takara/miru",
5
5
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
6
6
  "type": "module",
@@ -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
- 'Once the user approves, call `auth` again with action "check" to finish signing in.';
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
- 'Miru could not authorize its current credentials. Call the `auth` tool with action "start" ' +
23
- 'to open the Takara device-login page, then action "check" once the user approves. If access ' +
24
- "is still denied after signing in, check the account token balance.";
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
- 'user. Call with no arguments (or action "start") to begin: it opens the device-login ' +
31
- `page in the user's browser and returns that URL plus a short code. ${CHECK_HINT}`;
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
- function approvalText(link: string, userCode: string, opened: boolean): string {
46
- const lead = opened ? `A browser tab is open at ${link}` : `Ask the user to open ${link}`;
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(private readonly openBrowser: BrowserOpener) {}
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
- // Reopening on a repeat call is deliberate: the user may have closed the first tab.
88
- return toolText(approvalText(link, userCode, openIfAllowed(this.openBrowser, link)));
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
- async check(): Promise<ToolResult> {
92
- if (!this.pending) {
93
- return toolText('No device login is pending. Call `auth` with action "start" first.');
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
- const { start, config } = this.pending;
97
- // Errors (network failure, unrecognized OAuth error code) intentionally leave
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('Sign-in was denied. Call `auth` with action "start" to try again.');
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
- 'The device code expired before it was approved. Call `auth` with action "start" to try again.',
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(options?.openBrowser ?? openBrowserForDeviceLogin);
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('"start" begins a device-code login (default); "check" completes it.'),
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 }) => {