@takara-ai/miru-code 1.5.3 → 1.7.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.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Caveman mode — on-demand Agent Skill body (chat compression).
3
+ * Installed by `miru install` (Phase B); content-only module for Phase A.
4
+ */
5
+
6
+ export const CAVEMAN_SKILL_NAME = "caveman";
7
+
8
+ /** Full `SKILL.md` (YAML frontmatter + body) written to the agent skill path. */
9
+ export const CAVEMAN_SKILL_MD = `---
10
+ name: caveman
11
+ description: >-
12
+ Compress agent chat replies: less filler, max meaning. Use when the user
13
+ says caveman mode, /caveman, talk like caveman, be brief, or asks for fewer
14
+ tokens in chat. Intensities: /caveman lite|full|ultra. Stop with stop caveman
15
+ or normal mode. Chat narration only — not commits, PRs, or customer docs
16
+ unless the user asks.
17
+ ---
18
+
19
+ # Caveman mode
20
+
21
+ On-demand chat compression for Miru. **Brain big, mouth small.**
22
+
23
+ Inspired by [JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman) — original Miru skill text.
24
+
25
+ ## Activate / stop
26
+
27
+ | User says | Effect |
28
+ |-----------|--------|
29
+ | \`/caveman\`, \`caveman mode\`, \`talk like caveman\` | ON at **full** (default) |
30
+ | \`/caveman lite\` | ON at lite |
31
+ | \`/caveman full\` | ON at full |
32
+ | \`/caveman ultra\` | ON at ultra |
33
+ | \`stop caveman\`, \`normal mode\` | OFF — resume normal prose |
34
+
35
+ Level sticks until the user changes it or the session ends. Do not stay in caveman after stop.
36
+
37
+ ## Intensity
38
+
39
+ | Level | Behaviour |
40
+ |-------|-----------|
41
+ | **lite** | No polite filler, no task-restatement preamble. Keep articles and full sentences. Lead with the answer. |
42
+ | **full** (default) | Classic caveman: drop glue words when meaning stays clear; short sentences / fragments OK; dense keywords. |
43
+ | **ultra** | Maximum fragment compression. Still never sacrifice untouchables or safety (below). |
44
+
45
+ ## Style rules (when ON)
46
+
47
+ - No polite talk ("sure", "happy to help", "great question")
48
+ - No restating the user's task as a preamble
49
+ - Lead with the answer
50
+ - Prefer keywords / symbols (\`→\`, \`=\`, \`vs\`) over glue-heavy prose
51
+ - Drop articles/copulas only when meaning stays clear
52
+ - Short sentence or fragment OK
53
+
54
+ ## Auto-clarity (drop caveman for that segment)
55
+
56
+ Use **normal clear prose** for:
57
+
58
+ - Security warnings and security-relevant detail
59
+ - Irreversible / destructive action confirmations (force-push, drop table, delete prod, etc.)
60
+ - Multi-step sequences where fragment order could be misread
61
+ - When the user asks to clarify or repeats a question
62
+
63
+ Resume caveman after that segment if still ON.
64
+
65
+ **CAV-13 / safety:** Never omit security-relevant detail or destructive-action warnings for brevity. Brevity never hides risk. If unsure, use normal prose for the warning.
66
+
67
+ ## Never sacrifice (untouchables)
68
+
69
+ Byte-exact — do not compress or paraphrase:
70
+
71
+ - Paths, file:line citations
72
+ - Code fences and identifiers
73
+ - CLI commands
74
+ - Exact error strings / quoted text
75
+ - Miru MCP tool use (\`search\` / \`locate\` / \`expand\` / \`find_related\`) when search policy is installed
76
+
77
+ Caveman applies to surrounding prose only.
78
+
79
+ ## Chat vs persisted docs
80
+
81
+ Default: caveman = **live chat replies only**.
82
+
83
+ Commits, PR bodies, customer docs, READMEs, Linear text, memory files → **normal usable English** unless the user explicitly asks for caveman tone on that artifact.
84
+
85
+ ## Token honesty
86
+
87
+ Output can shrink a lot on chatty replies. Session-level savings are often smaller; the skill itself costs input tokens each turn. No guaranteed % savings.
88
+
89
+ ## Clarity over parody
90
+
91
+ If a reply is harder to use than normal concise prose, loosen grammar for that turn.
92
+
93
+ ## Examples
94
+
95
+ ### 1. Factual lookup
96
+
97
+ **Before:**
98
+ > Sure! I'd be happy to help. Looking at your question about where auth middleware is configured — I searched the codebase and it appears the authentication middleware is set up in \`src/auth/middleware.ts\` around line 42.
99
+
100
+ **After (full):**
101
+ > Auth middleware → \`src/auth/middleware.ts:42\`
102
+
103
+ ### 2. Recommendation
104
+
105
+ **Before:**
106
+ > Great question! There are a few approaches you could take here. I would generally recommend using the existing cache helper rather than rolling your own, because it already handles invalidation.
107
+
108
+ **After (full):**
109
+ > Use existing cache helper → handles invalidation. Don't roll own.
110
+
111
+ ### 3. Debug / failure finding
112
+
113
+ **Before:**
114
+ > I looked into the failure you described. It seems like the issue is that the API key is missing from the environment, which causes the client to throw a 401 Unauthorized when calling embeddings.
115
+
116
+ **After (full):**
117
+ > Fail = missing API key → embeddings 401 Unauthorized.
118
+ `;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * STE mode — on-demand Agent Skill + references (clear technical English).
3
+ * Unofficial ASD-STE100-inspired aid; not ASD-certified.
4
+ */
5
+
6
+ export const STE_SKILL_NAME = "ste";
7
+
8
+ export interface SteReferenceFile {
9
+ relativePath: string;
10
+ content: string;
11
+ }
12
+
13
+ /** Compact pragmatic `SKILL.md` (YAML frontmatter + body). */
14
+ export const STE_SKILL_MD = `---
15
+ name: ste
16
+ description: >-
17
+ Clear technical English for docs, runbooks, errors, and release notes.
18
+ Use when the user says STE, ASD-STE100, /ste, de-slop, write for non-native
19
+ readers, or asks to rewrite technical prose. Pragmatic by default; strict
20
+ when they ask for STE compliance. Not for marketing or brand copy. Not for
21
+ ultra-compressed chat telegraph style.
22
+ ---
23
+
24
+ # STE writing (Simplified Technical English)
25
+
26
+ On-demand clear technical writing for Miru. **Short complete sentences. Keep articles.**
27
+
28
+ Unofficial aid inspired by [ASD-STE100](https://www.asd-ste100.org) Issue 9 themes.
29
+ **Not ASD-certified. Not ASD-endorsed.** Full dictionary compliance needs the official standard at asd-ste100.org.
30
+
31
+ Inspired by prior art ([AminBlg/SimpleEnglish](https://github.com/AminBlg/SimpleEnglish), [woosal STE kit](https://github.com/woosal1337/blog/tree/main/videos/ep01-the-cure-for-ai-slop)) — original Miru skill text. Do not vend copyrighted ASD dictionary word lists.
32
+
33
+ ## Activate
34
+
35
+ | User says | Effect |
36
+ |-----------|--------|
37
+ | \`/ste\`, \`STE\`, \`write in STE\`, \`de-slop this\` | ON **pragmatic** (default) |
38
+ | \`ASD-STE100\`, \`strict STE\`, \`STE compliance\` | ON **strict** — also load \`references/rules.md\` |
39
+ | Check / audit my STE | Use \`references/checklist.md\` |
40
+
41
+ ## Task flow
42
+
43
+ 1. Select mode (pragmatic vs strict)
44
+ 2. Classify text as **procedural** or **descriptive**
45
+ 3. Pick one consistent term per concept
46
+ 4. Apply core rules below
47
+ 5. Run **self-check**
48
+ 6. Deliver
49
+
50
+ ## Modes
51
+
52
+ | Mode | Behaviour |
53
+ |------|-----------|
54
+ | **Pragmatic** (default) | Structural rules + consistent terms. Domain nouns/verbs OK (\`webhook\`, \`deploy\`, \`commit\`). |
55
+ | **Strict** | Stricter vocabulary discipline. Tell the user full compliance needs the official ASD dictionary: https://www.asd-ste100.org — then load \`references/rules.md\`. |
56
+
57
+ Cite rule numbers **only** when they appear in \`references/rules.md\`. Do not invent ASD rule IDs.
58
+
59
+ ## Core rules (pragmatic)
60
+
61
+ - Short **complete** sentences. Keep \`a\` / \`an\` / \`the\` and \`that\`. No telegraph omission.
62
+ - No semicolons — use two sentences.
63
+ - **Procedural:** imperative; about 20 words max per sentence; one instruction per sentence; **condition BEFORE command**.
64
+ - **Descriptive:** simple tenses; about 25 words max per sentence; one topic per paragraph.
65
+ - Active voice; simple tenses; avoid present-perfect stacks and comma "-ing" padding.
66
+ - Prefer modals \`can\` / \`will\` / \`must\`. Avoid \`should\` / \`would\` / \`may\` / \`might\` / \`could\` in instructions.
67
+ - One name per concept in the document (no check/verify/confirm/validate roulette).
68
+ - Warnings: state the command or condition first, then the risk.
69
+
70
+ ## Untouchables
71
+
72
+ Never alter:
73
+
74
+ - Code fences, identifiers, CLI flags, paths
75
+ - Quoted errors, product/API names
76
+ - Required safety conditions, limits, versions
77
+
78
+ If shortening would drop a required qualifier, keep the longer clear sentence.
79
+ Miru MCP tool use stays intact when search policy is installed.
80
+
81
+ ## Not for marketing
82
+
83
+ Do **not** apply STE to marketing, brand, or persuasive copy. Decline and offer a docs-oriented rewrite instead.
84
+
85
+ ## Slop → plain (examples)
86
+
87
+ | Slop | Plain |
88
+ |------|-------|
89
+ | It is worth noting that the service is highly robust | The service stays up if the primary node fails |
90
+ | You should consider carefully validating the input | You must validate the input |
91
+ | Feel free to simply leverage the existing utility | Use the existing utility |
92
+
93
+ ## Before / after
94
+
95
+ **Before (slop):**
96
+ > It is worth noting that you should carefully ensure the migration has completed successfully before you proceed to delete the old volume, as doing so might potentially result in irreversible data loss.
97
+
98
+ **After (STE pragmatic):**
99
+ > Wait until the migration is complete. Then delete the old volume. If you delete the volume too early, you will lose data permanently.
100
+
101
+ ## Self-check (before delivery)
102
+
103
+ 1. Sentences complete with articles where needed?
104
+ 2. One instruction per procedural sentence; condition before command?
105
+ 3. Terms consistent; modals are can/will/must where instructions appear?
106
+ 4. Code, paths, errors, and safety limits untouched?
107
+
108
+ For a longer audit, read \`references/checklist.md\`.
109
+
110
+ ## References
111
+
112
+ - \`references/rules.md\` — fuller paraphrased rule catalog (strict / audit)
113
+ - \`references/checklist.md\` — extended self-check patterns
114
+ `;
115
+
116
+ export const STE_RULES_MD = `# STE rules catalog (Miru paraphrase)
117
+
118
+ Unofficial structural catalog for Miru STE **strict** / audit use.
119
+ Themes follow ASD-STE100 Issue 9. **Not a copy of the official standard. Not a dictionary.**
120
+ Official standard and dictionary: https://www.asd-ste100.org
121
+
122
+ Cite the rule IDs below only. Do not invent other ASD numbers.
123
+
124
+ ## Words and terms
125
+
126
+ ### R1 — One name per concept
127
+ Pick one term for each idea in the document. Do not rotate synonyms (\`check\` / \`verify\` / \`validate\`) for the same action.
128
+
129
+ ### R2 — Approved instruction modals
130
+ In procedures, prefer \`can\`, \`will\`, \`must\`. Avoid \`should\`, \`would\`, \`may\`, \`might\`, \`could\` when you give an instruction.
131
+
132
+ ### R3 — Keep articles and "that"
133
+ Write complete noun phrases. Keep \`a\`, \`an\`, \`the\`, and \`that\`. Do not drop them for brevity.
134
+
135
+ ### R4 — No semicolon glue
136
+ Do not join two ideas with a semicolon. Use two sentences.
137
+
138
+ ## Sentences
139
+
140
+ ### R5 — Procedural length
141
+ In procedures, aim for about **20 words or fewer** per sentence. Give **one instruction** per sentence.
142
+
143
+ ### R6 — Descriptive length
144
+ In descriptions, aim for about **25 words or fewer** per sentence. Keep **one topic** per paragraph.
145
+
146
+ ### R7 — Condition before command
147
+ State the condition first. Then give the command.
148
+
149
+ Example: \`If the node is down, stop the job.\`
150
+ Not: \`Stop the job if the node is down.\` when the condition is the safety gate.
151
+
152
+ ### R8 — Active voice and simple tense
153
+ Prefer active voice and simple present or simple past. Avoid stacked present-perfect and trailing "-ing" clauses that hide the actor.
154
+
155
+ ## Safety
156
+
157
+ ### R9 — Warning shape
158
+ For destructive actions: state the command or the condition clearly, then state the risk.
159
+
160
+ Example: \`Do not run this on production. This command deletes all rows in the table.\`
161
+
162
+ ### R10 — Do not drop required limits
163
+ If a version, limit, or condition is required for safety, keep it. Clarity beats compression.
164
+
165
+ ## Software examples
166
+
167
+ | Weak | Stronger STE shape |
168
+ |------|--------------------|
169
+ | You might want to restart the pod | Restart the pod |
170
+ | Ensure that connectivity has been established | Make sure the connection is up |
171
+ | After having deployed the chart, proceed to verify | Deploy the chart. Then check the release status |
172
+
173
+ ## Dictionary
174
+
175
+ Miru does **not** ship the ASD approved-word list. For strict vocabulary compliance, use the official dictionary from asd-ste100.org. Domain technical nouns (\`Kubernetes\`, \`webhook\`, \`IAM\`) stay as product names (untouchable).
176
+ `;
177
+
178
+ export const STE_CHECKLIST_MD = `# STE extended checklist (Miru)
179
+
180
+ Use this for **check mode** / audits after a rewrite. Unofficial. Not ASD certification.
181
+
182
+ ## Structure
183
+
184
+ - [ ] Procedural sentences ≈ ≤20 words; one instruction each
185
+ - [ ] Descriptive sentences ≈ ≤25 words; one topic per paragraph
186
+ - [ ] Condition appears before the command when safety depends on it
187
+ - [ ] No semicolons used as sentence glue
188
+ - [ ] Articles and \`that\` kept (not telegraph)
189
+
190
+ ## Wording
191
+
192
+ - [ ] One term per concept across the document
193
+ - [ ] Instruction modals are can / will / must (not should / might / could)
194
+ - [ ] Active voice; simple tenses
195
+ - [ ] No marketing filler ("robust", "seamless", "it is worth noting")
196
+
197
+ ## Safety and precision
198
+
199
+ - [ ] Destructive steps warn with command/condition first, then risk
200
+ - [ ] Required versions, limits, and conditions still present
201
+ - [ ] Code, paths, CLI, quoted errors unchanged
202
+
203
+ ## Scope
204
+
205
+ - [ ] Not applied to marketing / brand copy
206
+ - [ ] Strict mode users pointed at https://www.asd-ste100.org for the official dictionary
207
+ - [ ] Disclaimer remembered: Miru STE is unofficial / not certified
208
+
209
+ ## Delivery
210
+
211
+ - [ ] Self-check in the main skill completed before send
212
+ `;
213
+
214
+ /** Files written under the skill directory beside `SKILL.md`. */
215
+ export const STE_REFERENCE_FILES: SteReferenceFile[] = [
216
+ { relativePath: "references/rules.md", content: STE_RULES_MD },
217
+ { relativePath: "references/checklist.md", content: STE_CHECKLIST_MD },
218
+ ];
@@ -0,0 +1,161 @@
1
+ import * as z from "zod";
2
+ import {
3
+ checkDeviceAuthorizationOnce,
4
+ type DeviceAuthConfig,
5
+ type DeviceAuthorizationStart,
6
+ openBrowserForDeviceLogin,
7
+ resolveDeviceAuthConfig,
8
+ startDeviceAuthorization,
9
+ } from "../auth/device.ts";
10
+ import { isCredentialsError } from "../auth/errors.ts";
11
+ import { saveStoredCredentials, setStoredCredentialsEnvToken } from "../credentials.ts";
12
+ import { toolText } from "./index-cache.ts";
13
+ import type { MiruMcpServer } from "./runtime.ts";
14
+
15
+ type ToolResult = ReturnType<typeof toolText>;
16
+
17
+ const CHECK_HINT =
18
+ 'Once the user approves, call `auth` again with action "check" to finish signing in.';
19
+
20
+ /** Appended to credentials failures so an agent knows Miru's own recovery step. */
21
+ const RECOVERY_HINT =
22
+ 'Miru is not signed in. Call the `auth` tool with action "start" — it opens the Takara ' +
23
+ 'device-login page in the browser — then action "check" once the user approves.';
24
+
25
+ const AUTH_TOOL_DESCRIPTION =
26
+ "Sign in with Takara credentials via device-code login — no terminal required. " +
27
+ "Only call this in direct response to a tool error mentioning missing/expired " +
28
+ "credentials — never speculatively, since it starts a real sign-in prompt for the " +
29
+ 'user. Call with no arguments (or action "start") to begin: it opens the device-login ' +
30
+ `page in the user's browser and returns that URL plus a short code. ${CHECK_HINT}`;
31
+
32
+ /** Browser opener seam; the real one spawns a detached `open`/`xdg-open`/`start`. */
33
+ export type BrowserOpener = (url: string) => boolean;
34
+
35
+ /** Opening is on unless MIRU_OPEN_BROWSER is set to something other than "1" (matches `miru setup`). */
36
+ function openIfAllowed(open: BrowserOpener, url: string): boolean {
37
+ const raw = process.env.MIRU_OPEN_BROWSER;
38
+ if (raw !== undefined && raw !== "1") {
39
+ return false;
40
+ }
41
+ return open(url);
42
+ }
43
+
44
+ function approvalText(link: string, userCode: string, opened: boolean): string {
45
+ const lead = opened ? `A browser tab is open at ${link}` : `Ask the user to open ${link}`;
46
+ return `${lead}. Code: ${userCode}. ${CHECK_HINT}`;
47
+ }
48
+
49
+ /** Tool-failure text for every Miru tool, with the sign-in step added when relevant. */
50
+ export function toolErrorText(err: unknown): ToolResult {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ return toolText(isCredentialsError(err) ? `${message}\n\n${RECOVERY_HINT}` : message);
53
+ }
54
+
55
+ type PendingDeviceAuth = {
56
+ start: DeviceAuthorizationStart;
57
+ config: DeviceAuthConfig;
58
+ startedAtMs: number;
59
+ };
60
+
61
+ function isExpired(entry: PendingDeviceAuth): boolean {
62
+ return Date.now() >= entry.startedAtMs + entry.start.expiresIn * 1000;
63
+ }
64
+
65
+ /**
66
+ * Per-registration auth-tool state, scoped to one `registerAuthTool` call rather than
67
+ * the module — a process could in principle host more than one MiruMcpServer (tests
68
+ * already do), and a module-level singleton would let their device logins collide.
69
+ */
70
+ class AuthToolState {
71
+ private pending: PendingDeviceAuth | null = null;
72
+
73
+ constructor(private readonly openBrowser: BrowserOpener) {}
74
+
75
+ async start(): Promise<ToolResult> {
76
+ let pending = this.pending;
77
+ if (!pending || isExpired(pending)) {
78
+ const config = resolveDeviceAuthConfig();
79
+ const start = await startDeviceAuthorization({ config });
80
+ pending = { start, config, startedAtMs: Date.now() };
81
+ this.pending = pending;
82
+ }
83
+
84
+ const { verificationUriComplete, verificationUri, userCode } = pending.start;
85
+ const link = verificationUriComplete ?? verificationUri;
86
+ // Reopening on a repeat call is deliberate: the user may have closed the first tab.
87
+ return toolText(approvalText(link, userCode, openIfAllowed(this.openBrowser, link)));
88
+ }
89
+
90
+ async check(): Promise<ToolResult> {
91
+ if (!this.pending) {
92
+ return toolText('No device login is pending. Call `auth` with action "start" first.');
93
+ }
94
+
95
+ const { start, config } = this.pending;
96
+ // Errors (network failure, unrecognized OAuth error code) intentionally leave
97
+ // `pending` intact — a transient failure shouldn't force the user to restart
98
+ // the whole login, just retry the check.
99
+ const result = await checkDeviceAuthorizationOnce(start, { config });
100
+
101
+ switch (result.status) {
102
+ case "success": {
103
+ this.pending = null;
104
+ const { tokens } = result;
105
+ await saveStoredCredentials({
106
+ kind: "device_code",
107
+ accessToken: tokens.accessToken,
108
+ refreshToken: tokens.refreshToken,
109
+ expiresAt: tokens.expiresAt,
110
+ tokenType: tokens.tokenType,
111
+ scope: tokens.scope,
112
+ });
113
+ setStoredCredentialsEnvToken(tokens.accessToken);
114
+ return toolText("Signed in successfully. Miru tools are now ready to use.");
115
+ }
116
+ case "pending":
117
+ return toolText(
118
+ 'Still waiting for approval. Ask the user to confirm they clicked and approved, then call `auth` again with action "check".',
119
+ );
120
+ case "slow_down":
121
+ return toolText(
122
+ 'Checking too soon — wait a bit before calling `auth` again with action "check".',
123
+ );
124
+ case "denied":
125
+ this.pending = null;
126
+ return toolText('Sign-in was denied. Call `auth` with action "start" to try again.');
127
+ case "expired":
128
+ this.pending = null;
129
+ return toolText(
130
+ 'The device code expired before it was approved. Call `auth` with action "start" to try again.',
131
+ );
132
+ }
133
+ }
134
+ }
135
+
136
+ export function registerAuthTool(
137
+ server: MiruMcpServer,
138
+ options?: { openBrowser?: BrowserOpener },
139
+ ): void {
140
+ const state = new AuthToolState(options?.openBrowser ?? openBrowserForDeviceLogin);
141
+
142
+ server.registerTool(
143
+ "auth",
144
+ {
145
+ description: AUTH_TOOL_DESCRIPTION,
146
+ inputSchema: {
147
+ action: z
148
+ .enum(["start", "check"])
149
+ .optional()
150
+ .describe('"start" begins a device-code login (default); "check" completes it.'),
151
+ },
152
+ },
153
+ async ({ action }) => {
154
+ try {
155
+ return action === "check" ? await state.check() : await state.start();
156
+ } catch (err) {
157
+ return toolText(err instanceof Error ? err.message : String(err));
158
+ }
159
+ },
160
+ );
161
+ }
@@ -399,7 +399,8 @@ export async function getIndexForRepo(
399
399
  return await cache.get(repo, ref);
400
400
  } catch (err) {
401
401
  const message = err instanceof Error ? err.message : String(err);
402
- throw new Error(`Failed to index ${repo}: ${message}`);
402
+ // Keep `cause` so the tool boundary can still recognize a credentials failure.
403
+ throw new Error(`Failed to index ${repo}: ${message}`, { cause: err });
403
404
  }
404
405
  }
405
406
 
package/src/mcp/server.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  MAX_MCP_TOP_K,
35
35
  resolveChunk,
36
36
  } from "../utils.ts";
37
+ import { registerAuthTool, toolErrorText } from "./auth-tool.ts";
37
38
  import {
38
39
  formatExpandResultsText,
39
40
  formatLiteralLocateText,
@@ -88,6 +89,8 @@ export function createMcpServer(
88
89
  },
89
90
  );
90
91
 
92
+ registerAuthTool(server);
93
+
91
94
  server.registerTool(
92
95
  "search",
93
96
  {
@@ -158,7 +161,7 @@ export function createMcpServer(
158
161
  const body = formatResultsText(payload);
159
162
  return toolText(skip ? withBenchmarkSkippedNote(body, skip) : body);
160
163
  } catch (err) {
161
- return toolText(err instanceof Error ? err.message : String(err));
164
+ return toolErrorText(err);
162
165
  }
163
166
  },
164
167
  );
@@ -244,7 +247,7 @@ export function createMcpServer(
244
247
  const body = formatLiteralLocateText(payload);
245
248
  return toolText(skip ? withBenchmarkSkippedNote(body, skip) : body);
246
249
  } catch (err) {
247
- return toolText(err instanceof Error ? err.message : String(err));
250
+ return toolErrorText(err);
248
251
  }
249
252
  },
250
253
  );
@@ -305,7 +308,7 @@ export function createMcpServer(
305
308
  ),
306
309
  );
307
310
  } catch (err) {
308
- return toolText(err instanceof Error ? err.message : String(err));
311
+ return toolErrorText(err);
309
312
  }
310
313
  },
311
314
  );
@@ -357,7 +360,7 @@ export function createMcpServer(
357
360
  ),
358
361
  );
359
362
  } catch (err) {
360
- return toolText(err instanceof Error ? err.message : String(err));
363
+ return toolErrorText(err);
361
364
  }
362
365
  },
363
366
  );
package/src/prompt.ts CHANGED
@@ -73,7 +73,10 @@ export async function promptText(message: string, defaultValue = ""): Promise<st
73
73
  }
74
74
  }
75
75
 
76
- export async function promptHidden(message: string): Promise<string> {
76
+ export async function promptHidden(
77
+ message: string,
78
+ stream: NodeJS.WriteStream = output,
79
+ ): Promise<string> {
77
80
  if (!input.isTTY) {
78
81
  throw new Error(
79
82
  "Cannot prompt for API key: stdin is not a TTY. Run `miru setup --key YOUR_KEY` or set TAKARA_API_KEY.",
@@ -81,7 +84,7 @@ export async function promptHidden(message: string): Promise<string> {
81
84
  }
82
85
 
83
86
  return new Promise((resolve, reject) => {
84
- output.write(message);
87
+ stream.write(message);
85
88
  input.setRawMode?.(true);
86
89
  input.resume();
87
90
  input.setEncoding("utf8");
@@ -95,20 +98,20 @@ export async function promptHidden(message: string): Promise<string> {
95
98
 
96
99
  if (result.cancel) {
97
100
  cleanup();
98
- output.write("\n");
101
+ stream.write("\n");
99
102
  reject(new Error("Setup cancelled."));
100
103
  process.exit(130);
101
104
  }
102
105
 
103
106
  if (result.submit) {
104
107
  cleanup();
105
- output.write("\n");
108
+ stream.write("\n");
106
109
  resolve(state.value.trim());
107
110
  return;
108
111
  }
109
112
 
110
113
  if (result.echo) {
111
- output.write(result.echo);
114
+ stream.write(result.echo);
112
115
  }
113
116
  }
114
117
  };