@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.
- package/README.md +102 -25
- package/docs/miru-workflow-diagram.svg +47 -0
- package/docs/self-hosted-sagemaker.md +1 -1
- package/package.json +3 -1
- package/scripts/install-git-hooks.ts +29 -0
- package/src/auth/device.ts +306 -0
- package/src/auth/errors.ts +24 -0
- package/src/auth/providers.ts +115 -0
- package/src/auth/types.ts +65 -0
- package/src/cli.ts +59 -8
- package/src/credentials.ts +173 -65
- package/src/embeddings/openai.ts +3 -2
- package/src/env.ts +6 -2
- package/src/help.ts +29 -13
- package/src/installer/agents.ts +118 -16
- package/src/installer/caveman-shared.ts +51 -0
- package/src/installer/config.ts +73 -0
- package/src/installer/copilot-family.ts +8 -0
- package/src/installer/hooks/install.ts +3 -14
- package/src/installer/installer.ts +332 -11
- package/src/installer/prompt.ts +15 -8
- package/src/installer/search-policy.ts +3 -2
- package/src/installer/shared-skill.ts +200 -0
- package/src/installer/style-packs/caveman.ts +118 -0
- package/src/installer/style-packs/ste/skill.ts +218 -0
- package/src/mcp/auth-tool.ts +161 -0
- package/src/mcp/index-cache.ts +2 -1
- package/src/mcp/server.ts +7 -4
- package/src/prompt.ts +8 -5
- package/src/setup.ts +100 -69
- package/src/spinner.ts +33 -6
- package/tokenizer/tokenizer.json +1 -1
|
@@ -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
|
+
}
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
stream.write("\n");
|
|
106
109
|
resolve(state.value.trim());
|
|
107
110
|
return;
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
if (result.echo) {
|
|
111
|
-
|
|
114
|
+
stream.write(result.echo);
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
};
|