@takara-ai/miru-code 0.1.4 → 0.3.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 +7 -9
- package/package.json +1 -2
- package/src/cli.ts +66 -47
- package/src/credentials.ts +2 -6
- package/src/env.ts +6 -10
- package/src/help.ts +175 -0
- package/src/mcp/index-cache.ts +27 -11
- package/src/mcp/serve.ts +3 -13
- package/src/mcp/server.ts +10 -10
- package/src/prompt.ts +111 -0
- package/src/setup.ts +4 -53
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Add credentials to `.env.local` (loaded automatically when you run from this rep
|
|
|
60
60
|
|
|
61
61
|
| Variable | Default |
|
|
62
62
|
|----------|---------|
|
|
63
|
-
| `TAKARA_API_KEY`
|
|
63
|
+
| `TAKARA_API_KEY` | (required) Takara bearer token for embeddings |
|
|
64
64
|
| `MIRU_OPENAI_BASE_URL` | `https://infer.dev.takara.ai/v1` |
|
|
65
65
|
| `MIRU_OPENAI_EMBEDDING_MODEL` | `ds1-potion-code-16m` |
|
|
66
66
|
| `MIRU_EMBEDDING_DIMENSIONS` | `256` (auto for potion) |
|
|
@@ -68,12 +68,8 @@ Add credentials to `.env.local` (loaded automatically when you run from this rep
|
|
|
68
68
|
| `MIRU_EMBEDDING_BATCH_SIZE` | `32` (max for this model) |
|
|
69
69
|
| `MIRU_CONCURRENCY` | logical CPUs **minus 2** (min 1) |
|
|
70
70
|
|
|
71
|
-
`SEMBLE_*` env names are still accepted as aliases for migration.
|
|
72
|
-
|
|
73
71
|
Parallelism: file reads/chunking, embedding batches (up to concurrency in flight), query embed overlapping BM25, and **BM25 scoring in Bun workers** (shards by doc range when index has 256+ chunks). BM25 uses an inverted postings list for fast single-threaded search on small indexes. Override with `MIRU_CONCURRENCY`.
|
|
74
72
|
|
|
75
|
-
`OPENAI_*` env aliases are also accepted where noted in the CLI help.
|
|
76
|
-
|
|
77
73
|
## IDE integration
|
|
78
74
|
|
|
79
75
|
Miru supports six agent platforms. For each, you can wire the **MCP server** (recommended) and/or install a **CLI sub-agent** with `miru init`.
|
|
@@ -93,14 +89,14 @@ Run `miru init --agent <id> --force` to overwrite an existing sub-agent file.
|
|
|
93
89
|
|
|
94
90
|
### MCP server
|
|
95
91
|
|
|
96
|
-
Run with **no arguments** to start the stdio MCP server.
|
|
92
|
+
Run with **no arguments** to start the stdio MCP server. Indexing begins on the first `search` or `find_related` call, using the `repo` path you pass. Optional `--ref` sets the default git ref for remote repos.
|
|
97
93
|
|
|
98
94
|
```bash
|
|
99
95
|
bunx @takara-ai/miru-code
|
|
100
96
|
# or after bun add -g @takara-ai/miru-code: miru
|
|
101
97
|
```
|
|
102
98
|
|
|
103
|
-
**Tools:** `search`, `find_related` —
|
|
99
|
+
**Tools:** `search`, `find_related` — `repo` is required (project root or https:// git URL).
|
|
104
100
|
|
|
105
101
|
For MCP configs, use `bunx` as the command (`bun` with `x` in `args` works too — they're equivalent).
|
|
106
102
|
|
|
@@ -352,12 +348,14 @@ bun run typecheck
|
|
|
352
348
|
|
|
353
349
|
Releases use **Bun** for install, test, and `bun pm pack`. The registry upload uses **`npm publish`** on that tarball because [Bun does not yet support npm OIDC trusted publishing](https://github.com/oven-sh/bun/issues/22423) — no long-lived npm tokens in GitHub secrets.
|
|
354
350
|
|
|
355
|
-
**Local
|
|
351
|
+
**Local publish** (logged in to npm; `publishConfig.access` is already `public`):
|
|
356
352
|
|
|
357
353
|
```bash
|
|
358
|
-
bun publish
|
|
354
|
+
bun publish
|
|
359
355
|
```
|
|
360
356
|
|
|
357
|
+
Runs `prepublishOnly` (typecheck + tests), then a single registry upload. Do not add a `publish` script that calls `npm publish` — Bun runs lifecycle scripts after its own upload and would publish twice.
|
|
358
|
+
|
|
361
359
|
**CI** (after Trusted Publisher is configured on npm):
|
|
362
360
|
|
|
363
361
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takara-ai/miru-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"lint": "biome check .",
|
|
51
51
|
"lint:fix": "biome check --write .",
|
|
52
52
|
"prepublishOnly": "bun run typecheck && bun test",
|
|
53
|
-
"publish": "npm publish --access public",
|
|
54
53
|
"benchmark:quality": "bun run scripts/benchmark-quality.ts",
|
|
55
54
|
"benchmark:quantization": "bun run scripts/benchmark-quantization.ts"
|
|
56
55
|
},
|
package/src/cli.ts
CHANGED
|
@@ -4,18 +4,34 @@ import { type AgentId, writeAgentFile } from "./agents.ts";
|
|
|
4
4
|
import { clearCache } from "./cache.ts";
|
|
5
5
|
import { loadStoredCredentials } from "./credentials.ts";
|
|
6
6
|
import { loadEnvFiles } from "./env-files.ts";
|
|
7
|
+
import {
|
|
8
|
+
AGENT_IDS,
|
|
9
|
+
formatUnknownAgent,
|
|
10
|
+
printCommandHelp,
|
|
11
|
+
printFullHelp,
|
|
12
|
+
printMainHelp,
|
|
13
|
+
} from "./help.ts";
|
|
7
14
|
import { serveMcp } from "./mcp/serve.ts";
|
|
8
15
|
import { MiruIndex } from "./miru-index.ts";
|
|
9
16
|
import { ensureCredentials, runClearCredentials, runSetup } from "./setup.ts";
|
|
10
17
|
import type { ContentType } from "./types.ts";
|
|
11
|
-
import { formatResults,
|
|
18
|
+
import { formatResults, resolveChunk, resolveContent } from "./utils.ts";
|
|
12
19
|
|
|
13
20
|
loadEnvFiles();
|
|
14
21
|
await loadStoredCredentials();
|
|
15
22
|
|
|
16
|
-
const CLI_COMMANDS = new Set([
|
|
23
|
+
const CLI_COMMANDS = new Set([
|
|
24
|
+
"search",
|
|
25
|
+
"find-related",
|
|
26
|
+
"init",
|
|
27
|
+
"setup",
|
|
28
|
+
"clear",
|
|
29
|
+
"help",
|
|
30
|
+
"-h",
|
|
31
|
+
"--help",
|
|
32
|
+
]);
|
|
17
33
|
|
|
18
|
-
const AGENTS = new Set<AgentId>(
|
|
34
|
+
const AGENTS = new Set<AgentId>(AGENT_IDS);
|
|
19
35
|
|
|
20
36
|
function parseContentArgv(argv: string[]): { content: ContentType[]; rest: string[] } {
|
|
21
37
|
const rest: string[] = [];
|
|
@@ -101,8 +117,15 @@ async function runFindRelated(
|
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
async function runInit(agent: AgentId, force: boolean): Promise<void> {
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
try {
|
|
121
|
+
const dest = await writeAgentFile(agent, { force });
|
|
122
|
+
process.stdout.write(`Wrote sub-agent: ${dest}\n`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
+
process.stderr.write(`${message}\n`);
|
|
126
|
+
process.stderr.write("Use --force to overwrite an existing file.\n");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
async function runClear(path: string): Promise<void> {
|
|
@@ -110,54 +133,55 @@ async function runClear(path: string): Promise<void> {
|
|
|
110
133
|
console.log(`Cleared cached index for: ${path}`);
|
|
111
134
|
}
|
|
112
135
|
|
|
113
|
-
function printHelp(): void {
|
|
114
|
-
console.log(`Miru (見る) — hybrid code search (Bun + TypeScript + Takara)
|
|
115
|
-
|
|
116
|
-
CLI:
|
|
117
|
-
miru search <query> [path] [-k N] [--content code|docs|config|all]
|
|
118
|
-
miru find-related <file_path> <line> [path] [-k N] [--content ...]
|
|
119
|
-
miru setup [--key KEY] [--force] [--clear]
|
|
120
|
-
miru init [--agent cursor] [--force]
|
|
121
|
-
miru clear [path]
|
|
122
|
-
|
|
123
|
-
MCP server (default when no CLI subcommand):
|
|
124
|
-
miru
|
|
125
|
-
miru [path] [--ref BRANCH] [--content code ...]
|
|
126
|
-
|
|
127
|
-
With no path, indexes the process working directory (Cursor sets this to your workspace).
|
|
128
|
-
|
|
129
|
-
Examples:
|
|
130
|
-
miru search "auth middleware" ./src
|
|
131
|
-
miru find-related src/auth.ts 42 .
|
|
132
|
-
|
|
133
|
-
Environment:
|
|
134
|
-
TAKARA_API_KEY / OPENAI_API_KEY / MIRU_OPENAI_API_KEY Takara bearer token
|
|
135
|
-
MIRU_OPENAI_BASE_URL Default: infer.dev.takara.ai
|
|
136
|
-
`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
136
|
async function runCli(argv: string[]): Promise<void> {
|
|
140
137
|
const [command, ...rest] = argv;
|
|
141
138
|
|
|
142
|
-
if (command ===
|
|
143
|
-
|
|
139
|
+
if (command === undefined) {
|
|
140
|
+
printMainHelp();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (command === "-h" || command === "--help") {
|
|
145
|
+
printFullHelp();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (command === "help") {
|
|
150
|
+
const topic = rest[0];
|
|
151
|
+
if (!topic) {
|
|
152
|
+
printMainHelp();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
printCommandHelp(topic);
|
|
144
156
|
return;
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
if (command === "init") {
|
|
148
|
-
let agent: AgentId
|
|
160
|
+
let agent: AgentId | undefined;
|
|
149
161
|
let force = false;
|
|
150
162
|
for (let i = 0; i < rest.length; i++) {
|
|
151
163
|
const arg = rest[i];
|
|
152
164
|
if (arg === "--force") {
|
|
153
165
|
force = true;
|
|
154
|
-
} else if (
|
|
155
|
-
const value = rest[++i]
|
|
156
|
-
if (
|
|
157
|
-
|
|
166
|
+
} else if (arg === "--agent" || arg === "-a") {
|
|
167
|
+
const value = rest[++i];
|
|
168
|
+
if (!value) {
|
|
169
|
+
process.stderr.write("Missing value for --agent.\n");
|
|
170
|
+
printCommandHelp("init");
|
|
171
|
+
process.exit(1);
|
|
158
172
|
}
|
|
173
|
+
if (!AGENTS.has(value as AgentId)) {
|
|
174
|
+
process.stderr.write(`${formatUnknownAgent(value)}\n`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
agent = value as AgentId;
|
|
159
178
|
}
|
|
160
179
|
}
|
|
180
|
+
if (!agent) {
|
|
181
|
+
process.stderr.write("miru init requires --agent.\n");
|
|
182
|
+
printCommandHelp("init");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
161
185
|
await runInit(agent, force);
|
|
162
186
|
return;
|
|
163
187
|
}
|
|
@@ -200,7 +224,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
200
224
|
if (command === "search") {
|
|
201
225
|
const query = sizedRest[0];
|
|
202
226
|
if (!query) {
|
|
203
|
-
|
|
227
|
+
printCommandHelp("search");
|
|
204
228
|
process.exit(1);
|
|
205
229
|
}
|
|
206
230
|
const path = resolve(sizedRest[1] ?? process.cwd());
|
|
@@ -212,7 +236,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
212
236
|
const filePath = sizedRest[0];
|
|
213
237
|
const lineRaw = sizedRest[1];
|
|
214
238
|
if (!filePath || !lineRaw) {
|
|
215
|
-
|
|
239
|
+
printCommandHelp("find-related");
|
|
216
240
|
process.exit(1);
|
|
217
241
|
}
|
|
218
242
|
const line = Number(lineRaw);
|
|
@@ -221,12 +245,12 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
221
245
|
return;
|
|
222
246
|
}
|
|
223
247
|
|
|
224
|
-
|
|
248
|
+
process.stderr.write(`Unknown command: ${command}\n`);
|
|
249
|
+
printMainHelp();
|
|
225
250
|
process.exit(1);
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
async function runMcp(argv: string[]): Promise<void> {
|
|
229
|
-
let path: string | null = null;
|
|
230
254
|
let ref: string | null = null;
|
|
231
255
|
const contentTokens: string[] = [];
|
|
232
256
|
|
|
@@ -247,15 +271,10 @@ async function runMcp(argv: string[]): Promise<void> {
|
|
|
247
271
|
contentTokens.push(value);
|
|
248
272
|
i++;
|
|
249
273
|
}
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (arg && !arg.startsWith("-") && path === null) {
|
|
253
|
-
path = isGitUrl(arg) ? arg : resolve(arg);
|
|
254
274
|
}
|
|
255
275
|
}
|
|
256
276
|
|
|
257
277
|
await serveMcp({
|
|
258
|
-
path,
|
|
259
278
|
ref,
|
|
260
279
|
content: resolveContent(contentTokens.length > 0 ? contentTokens : ["code"]),
|
|
261
280
|
});
|
package/src/credentials.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { hasTakaraApiKeyInEnv } from "./env.ts";
|
|
4
4
|
|
|
5
5
|
const CREDENTIALS_FILENAME = "credentials.json";
|
|
6
6
|
const CREDENTIALS_VERSION = 1;
|
|
@@ -31,10 +31,6 @@ export function resolveCredentialsPath(): string {
|
|
|
31
31
|
return join(resolveCredentialsDir(), CREDENTIALS_FILENAME);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function hasEmbeddingApiKeyInEnv(): boolean {
|
|
35
|
-
return Boolean(envFirstString([...EMBEDDING_API_KEY_NAMES], ""));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
export async function readStoredCredentials(): Promise<StoredCredentials | null> {
|
|
39
35
|
const path = resolveCredentialsPath();
|
|
40
36
|
if (!(await Bun.file(path).exists())) {
|
|
@@ -53,7 +49,7 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
|
|
|
53
49
|
|
|
54
50
|
/** Set TAKARA_API_KEY from the user credentials file when env is unset. */
|
|
55
51
|
export async function loadStoredCredentials(): Promise<boolean> {
|
|
56
|
-
if (
|
|
52
|
+
if (hasTakaraApiKeyInEnv()) {
|
|
57
53
|
return false;
|
|
58
54
|
}
|
|
59
55
|
const stored = await readStoredCredentials();
|
package/src/env.ts
CHANGED
|
@@ -31,21 +31,17 @@ export function envOptionalInt(names: string[], min = 1): number | undefined {
|
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
"TAKARA_API_KEY",
|
|
36
|
-
"OPENAI_API_KEY",
|
|
37
|
-
"MIRU_OPENAI_API_KEY",
|
|
38
|
-
"SEMBLE_OPENAI_API_KEY",
|
|
39
|
-
] as const;
|
|
34
|
+
export const TAKARA_API_KEY_ENV = "TAKARA_API_KEY";
|
|
40
35
|
|
|
41
|
-
export
|
|
36
|
+
export function hasTakaraApiKeyInEnv(): boolean {
|
|
37
|
+
return Boolean(process.env[TAKARA_API_KEY_ENV]?.trim());
|
|
38
|
+
}
|
|
42
39
|
|
|
43
40
|
export function resolveEmbeddingApiKey(): string {
|
|
44
|
-
const key =
|
|
41
|
+
const key = process.env[TAKARA_API_KEY_ENV]?.trim() ?? "";
|
|
45
42
|
if (!key) {
|
|
46
43
|
throw new Error(
|
|
47
|
-
"
|
|
48
|
-
"MIRU_OPENAI_API_KEY, or SEMBLE_OPENAI_API_KEY in your MCP server env or .env.local.",
|
|
44
|
+
"Takara API key required. Run `miru setup`, or set TAKARA_API_KEY in your MCP server env or .env.local.",
|
|
49
45
|
);
|
|
50
46
|
}
|
|
51
47
|
return key;
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { AgentId } from "./agents.ts";
|
|
2
|
+
|
|
3
|
+
export const AGENT_IDS: readonly AgentId[] = [
|
|
4
|
+
"claude",
|
|
5
|
+
"copilot",
|
|
6
|
+
"cursor",
|
|
7
|
+
"gemini",
|
|
8
|
+
"kiro",
|
|
9
|
+
"opencode",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
const AGENT_LIST = AGENT_IDS.join(", ");
|
|
13
|
+
|
|
14
|
+
export function printMainHelp(): void {
|
|
15
|
+
const lines = [
|
|
16
|
+
"",
|
|
17
|
+
"Miru (見る) — hybrid code search for agents",
|
|
18
|
+
"",
|
|
19
|
+
"Usage:",
|
|
20
|
+
" miru Start MCP server (stdio)",
|
|
21
|
+
" miru <command> [options]",
|
|
22
|
+
"",
|
|
23
|
+
"Commands:",
|
|
24
|
+
" search Hybrid search over a codebase",
|
|
25
|
+
" find-related Find chunks related to a file:line",
|
|
26
|
+
" setup Save your Takara API key locally",
|
|
27
|
+
" init Write a miru-code sub-agent for your IDE",
|
|
28
|
+
" clear Remove cached index for a path",
|
|
29
|
+
"",
|
|
30
|
+
"Quick start:",
|
|
31
|
+
" miru setup",
|
|
32
|
+
" miru init --agent cursor",
|
|
33
|
+
' miru search "auth middleware" ./src',
|
|
34
|
+
"",
|
|
35
|
+
"Run miru help <command> for details.",
|
|
36
|
+
"Run miru -h for environment variables.",
|
|
37
|
+
"",
|
|
38
|
+
];
|
|
39
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function printEnvHelp(): void {
|
|
43
|
+
const lines = [
|
|
44
|
+
"",
|
|
45
|
+
"Environment:",
|
|
46
|
+
" TAKARA_API_KEY",
|
|
47
|
+
" Takara bearer token for embeddings",
|
|
48
|
+
" MIRU_OPENAI_BASE_URL",
|
|
49
|
+
" Default: infer.dev.takara.ai",
|
|
50
|
+
"",
|
|
51
|
+
];
|
|
52
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function printFullHelp(): void {
|
|
56
|
+
printMainHelp();
|
|
57
|
+
printEnvHelp();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function printCommandHelp(command: string): void {
|
|
61
|
+
switch (command) {
|
|
62
|
+
case "search":
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
[
|
|
65
|
+
"",
|
|
66
|
+
"miru search — hybrid search",
|
|
67
|
+
"",
|
|
68
|
+
"Usage:",
|
|
69
|
+
" miru search <query> [path] [-k N] [--content TYPE ...]",
|
|
70
|
+
"",
|
|
71
|
+
"Options:",
|
|
72
|
+
" -k, --top-k N Number of results (default: 5)",
|
|
73
|
+
" --content TYPE code | docs | config | all (default: code)",
|
|
74
|
+
"",
|
|
75
|
+
"Example:",
|
|
76
|
+
' miru search "where is auth" ./src -k 10 --content code docs',
|
|
77
|
+
"",
|
|
78
|
+
].join("\n"),
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
case "find-related":
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
[
|
|
84
|
+
"",
|
|
85
|
+
"miru find-related — related chunks",
|
|
86
|
+
"",
|
|
87
|
+
"Usage:",
|
|
88
|
+
" miru find-related <file> <line> [path] [-k N] [--content TYPE ...]",
|
|
89
|
+
"",
|
|
90
|
+
"Example:",
|
|
91
|
+
" miru find-related src/auth.ts 42 . -k 8",
|
|
92
|
+
"",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
case "setup":
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
[
|
|
99
|
+
"",
|
|
100
|
+
"miru setup — store API key",
|
|
101
|
+
"",
|
|
102
|
+
"Usage:",
|
|
103
|
+
" miru setup",
|
|
104
|
+
" miru setup --key TOKEN",
|
|
105
|
+
" miru setup --force",
|
|
106
|
+
" miru setup --clear",
|
|
107
|
+
"",
|
|
108
|
+
"Options:",
|
|
109
|
+
" --key, -k TOKEN Provide key on the command line (non-interactive)",
|
|
110
|
+
" --force Replace an existing stored key",
|
|
111
|
+
" --clear Remove stored credentials",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
);
|
|
115
|
+
return;
|
|
116
|
+
case "init":
|
|
117
|
+
process.stdout.write(
|
|
118
|
+
[
|
|
119
|
+
"",
|
|
120
|
+
"miru init — IDE sub-agent file",
|
|
121
|
+
"",
|
|
122
|
+
"Usage:",
|
|
123
|
+
" miru init --agent AGENT [--force]",
|
|
124
|
+
"",
|
|
125
|
+
"Agents:",
|
|
126
|
+
` ${AGENT_LIST}`,
|
|
127
|
+
"",
|
|
128
|
+
"Examples:",
|
|
129
|
+
" miru init --agent cursor",
|
|
130
|
+
" miru init --agent claude --force",
|
|
131
|
+
"",
|
|
132
|
+
].join("\n"),
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
case "clear":
|
|
136
|
+
process.stdout.write(
|
|
137
|
+
[
|
|
138
|
+
"",
|
|
139
|
+
"miru clear — drop cached index",
|
|
140
|
+
"",
|
|
141
|
+
"Usage:",
|
|
142
|
+
" miru clear [path]",
|
|
143
|
+
"",
|
|
144
|
+
"Example:",
|
|
145
|
+
" miru clear .",
|
|
146
|
+
"",
|
|
147
|
+
].join("\n"),
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
case "mcp":
|
|
151
|
+
process.stdout.write(
|
|
152
|
+
[
|
|
153
|
+
"",
|
|
154
|
+
"miru — MCP server (default with no subcommand)",
|
|
155
|
+
"",
|
|
156
|
+
"Usage:",
|
|
157
|
+
" miru [--ref BRANCH] [--content TYPE ...]",
|
|
158
|
+
"",
|
|
159
|
+
"Indexes a repo on the first search/find_related tool call (repo argument).",
|
|
160
|
+
"With no path, uses the process working directory.",
|
|
161
|
+
"",
|
|
162
|
+
].join("\n"),
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
default:
|
|
166
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
167
|
+
printMainHelp();
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function formatUnknownAgent(agent: string): string {
|
|
174
|
+
return `Unknown agent "${agent}". Choose one of: ${AGENT_LIST}`;
|
|
175
|
+
}
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -57,11 +57,14 @@ export function shouldIgnoreWatchPath(relativePath: string | null | undefined):
|
|
|
57
57
|
|
|
58
58
|
export class IndexCache {
|
|
59
59
|
private readonly content: ContentType[];
|
|
60
|
+
private readonly defaultRef: string | null;
|
|
60
61
|
private readonly entries = new Map<string, CacheEntry>();
|
|
61
62
|
private watcher: ReturnType<typeof watch> | null = null;
|
|
63
|
+
private watchedSource: string | null = null;
|
|
62
64
|
|
|
63
|
-
constructor(content: ContentType[] = ["code"]) {
|
|
65
|
+
constructor(content: ContentType[] = ["code"], defaultRef: string | null = null) {
|
|
64
66
|
this.content = content;
|
|
67
|
+
this.defaultRef = defaultRef;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
private ensureEntry(cacheKey: string): CacheEntry {
|
|
@@ -107,6 +110,7 @@ export class IndexCache {
|
|
|
107
110
|
void task
|
|
108
111
|
.then((index) => {
|
|
109
112
|
entry.index = index;
|
|
113
|
+
this.maybeStartWatcher(source);
|
|
110
114
|
void this.flushFileUpdates(cacheKey, source);
|
|
111
115
|
return index;
|
|
112
116
|
})
|
|
@@ -120,11 +124,12 @@ export class IndexCache {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
async get(source: string, ref?: string | null): Promise<MiruIndex> {
|
|
123
|
-
const
|
|
127
|
+
const resolvedRef = ref ?? this.defaultRef;
|
|
128
|
+
const cacheKey = computeSourceCacheKey(source, resolvedRef);
|
|
124
129
|
const entry = this.ensureEntry(cacheKey);
|
|
125
130
|
|
|
126
131
|
if (!entry.task) {
|
|
127
|
-
this.startBuild(source,
|
|
132
|
+
this.startBuild(source, resolvedRef, cacheKey);
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
const index = await entry.task;
|
|
@@ -237,6 +242,18 @@ export class IndexCache {
|
|
|
237
242
|
entry.updateChain = entry.updateChain.then(run, run);
|
|
238
243
|
}
|
|
239
244
|
|
|
245
|
+
private maybeStartWatcher(source: string): void {
|
|
246
|
+
if (!mcpWatchEnabled() || isGitUrl(source)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const resolved = resolve(source);
|
|
250
|
+
if (this.watchedSource === resolved) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.watchedSource = resolved;
|
|
254
|
+
this.startWatcher(resolved);
|
|
255
|
+
}
|
|
256
|
+
|
|
240
257
|
startWatcher(path: string): void {
|
|
241
258
|
const resolved = resolve(path);
|
|
242
259
|
if (this.watcher) {
|
|
@@ -251,34 +268,33 @@ export class IndexCache {
|
|
|
251
268
|
close(): void {
|
|
252
269
|
this.watcher?.close();
|
|
253
270
|
this.watcher = null;
|
|
271
|
+
this.watchedSource = null;
|
|
254
272
|
this.entries.clear();
|
|
255
273
|
}
|
|
256
274
|
}
|
|
257
275
|
|
|
258
276
|
export async function getIndexForRepo(
|
|
259
277
|
repo: string | null | undefined,
|
|
260
|
-
defaultSource: string | null,
|
|
261
278
|
cache: IndexCache,
|
|
262
279
|
ref?: string | null,
|
|
263
280
|
): Promise<MiruIndex> {
|
|
264
|
-
if (
|
|
281
|
+
if (!repo) {
|
|
265
282
|
throw new Error(
|
|
266
|
-
|
|
283
|
+
"Pass an https:// or http:// git URL or local directory path as `repo` (project root for local workspaces).",
|
|
267
284
|
);
|
|
268
285
|
}
|
|
269
286
|
|
|
270
|
-
|
|
271
|
-
if (!source) {
|
|
287
|
+
if (isGitUrl(repo) && !repo.startsWith("https://") && !repo.startsWith("http://")) {
|
|
272
288
|
throw new Error(
|
|
273
|
-
|
|
289
|
+
`Only https://, http://, or local directory paths are accepted as \`repo\`. Got: ${repo}`,
|
|
274
290
|
);
|
|
275
291
|
}
|
|
276
292
|
|
|
277
293
|
try {
|
|
278
|
-
return await cache.get(
|
|
294
|
+
return await cache.get(repo, ref);
|
|
279
295
|
} catch (err) {
|
|
280
296
|
const message = err instanceof Error ? err.message : String(err);
|
|
281
|
-
throw new Error(`Failed to index ${
|
|
297
|
+
throw new Error(`Failed to index ${repo}: ${message}`);
|
|
282
298
|
}
|
|
283
299
|
}
|
|
284
300
|
|
package/src/mcp/serve.ts
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
2
|
import type { ContentType } from "../types.ts";
|
|
4
|
-
import {
|
|
5
|
-
import { IndexCache, mcpWatchEnabled } from "./index-cache.ts";
|
|
3
|
+
import { IndexCache } from "./index-cache.ts";
|
|
6
4
|
import { createMcpServer } from "./server.ts";
|
|
7
5
|
|
|
8
6
|
export async function serveMcp(options: {
|
|
9
|
-
path?: string | null;
|
|
10
7
|
ref?: string | null;
|
|
11
8
|
content?: ContentType[];
|
|
12
9
|
}): Promise<void> {
|
|
13
|
-
const cache = new IndexCache(options.content ?? ["code"]);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
void cache.get(defaultSource, options.ref).catch(() => undefined);
|
|
17
|
-
if (!isGitUrl(defaultSource) && mcpWatchEnabled()) {
|
|
18
|
-
cache.startWatcher(defaultSource);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const server = createMcpServer(cache, defaultSource);
|
|
10
|
+
const cache = new IndexCache(options.content ?? ["code"], options.ref ?? null);
|
|
11
|
+
const server = createMcpServer(cache);
|
|
22
12
|
const transport = new StdioServerTransport();
|
|
23
13
|
await server.connect(transport);
|
|
24
14
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -6,17 +6,17 @@ import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
|
|
|
6
6
|
|
|
7
7
|
const REPO_DESCRIPTION =
|
|
8
8
|
"https:// or http:// git URL (e.g. https://github.com/org/repo) or local directory path to index and search. " +
|
|
9
|
-
"
|
|
10
|
-
"The index is
|
|
9
|
+
"Pass the project root for local workspaces. " +
|
|
10
|
+
"The index is built on the first tool call and cached for the session.";
|
|
11
11
|
|
|
12
12
|
const SERVER_INSTRUCTIONS =
|
|
13
13
|
"Instant code search for any local or remote git repository. " +
|
|
14
14
|
"Call `search` to find relevant code; call `find_related` on a result to discover similar code elsewhere. " +
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"Always pass the project root as `repo` on the first call (local path or https:// git URL). " +
|
|
16
|
+
"Never guess or infer URLs. " +
|
|
17
17
|
"Prefer these tools over Grep, Glob, or Read for any question about how code works.";
|
|
18
18
|
|
|
19
|
-
export function createMcpServer(cache: IndexCache
|
|
19
|
+
export function createMcpServer(cache: IndexCache): McpServer {
|
|
20
20
|
const server = new McpServer(
|
|
21
21
|
{
|
|
22
22
|
name: "miru",
|
|
@@ -32,16 +32,16 @@ export function createMcpServer(cache: IndexCache, defaultSource: string | null
|
|
|
32
32
|
{
|
|
33
33
|
description:
|
|
34
34
|
"Search a codebase with a natural-language or code query. " +
|
|
35
|
-
"
|
|
35
|
+
"Indexes `repo` on the first call; later calls reuse the session cache.",
|
|
36
36
|
inputSchema: {
|
|
37
37
|
query: z.string().describe("Natural language or code query."),
|
|
38
|
-
repo: z.string().
|
|
38
|
+
repo: z.string().describe(REPO_DESCRIPTION),
|
|
39
39
|
top_k: z.number().int().min(1).optional().describe("Number of results to return."),
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
42
|
async ({ query, repo, top_k: topK }) => {
|
|
43
43
|
try {
|
|
44
|
-
const index = await getIndexForRepo(repo
|
|
44
|
+
const index = await getIndexForRepo(repo, cache);
|
|
45
45
|
const results = await index.search({ query, topK: topK ?? 5 });
|
|
46
46
|
if (results.length === 0) {
|
|
47
47
|
return toolText(JSON.stringify({ error: "No results found." }));
|
|
@@ -66,13 +66,13 @@ export function createMcpServer(cache: IndexCache, defaultSource: string | null
|
|
|
66
66
|
"Path to the file as stored in the index (use file_path from a search result).",
|
|
67
67
|
),
|
|
68
68
|
line: z.number().int().describe("Line number (1-indexed)."),
|
|
69
|
-
repo: z.string().
|
|
69
|
+
repo: z.string().describe(REPO_DESCRIPTION),
|
|
70
70
|
top_k: z.number().int().min(1).optional().describe("Number of similar chunks to return."),
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
73
|
async ({ file_path: filePath, line, repo, top_k: topK }) => {
|
|
74
74
|
try {
|
|
75
|
-
const index = await getIndexForRepo(repo
|
|
75
|
+
const index = await getIndexForRepo(repo, cache);
|
|
76
76
|
const chunk = resolveChunk(index.chunks, filePath, line);
|
|
77
77
|
if (!chunk) {
|
|
78
78
|
return toolText(
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
|
|
3
|
+
export interface HiddenPromptState {
|
|
4
|
+
value: string;
|
|
5
|
+
escapeBuffer: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createHiddenPromptState(): HiddenPromptState {
|
|
9
|
+
return { value: "", escapeBuffer: "" };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Apply one UTF-16 code unit from raw stdin; returns terminal echo bytes (may be empty). */
|
|
13
|
+
export function applyHiddenPromptChar(
|
|
14
|
+
state: HiddenPromptState,
|
|
15
|
+
char: string,
|
|
16
|
+
): { state: HiddenPromptState; echo: string; submit?: boolean; cancel?: boolean } {
|
|
17
|
+
if (char === "\x1b") {
|
|
18
|
+
return { state: { ...state, escapeBuffer: "\x1b" }, echo: "" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (state.escapeBuffer.length > 0) {
|
|
22
|
+
const escapeBuffer = state.escapeBuffer + char;
|
|
23
|
+
let done = false;
|
|
24
|
+
if (escapeBuffer.startsWith("\x1b[")) {
|
|
25
|
+
done = escapeBuffer.length > 2 && char >= "@" && char <= "~";
|
|
26
|
+
} else if (escapeBuffer.startsWith("\x1b")) {
|
|
27
|
+
done = escapeBuffer.length >= 2;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
state: { ...state, escapeBuffer: done ? "" : escapeBuffer },
|
|
31
|
+
echo: "",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (char === "\n" || char === "\r" || char === "\u0004") {
|
|
36
|
+
return { state, echo: "", submit: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (char === "\u0003") {
|
|
40
|
+
return { state, echo: "", cancel: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (char === "\u007f" || char === "\b") {
|
|
44
|
+
if (state.value.length === 0) {
|
|
45
|
+
return { state, echo: "" };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
state: { ...state, value: state.value.slice(0, -1) },
|
|
49
|
+
echo: "\b \b",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (char < " " || char === "\x7f") {
|
|
54
|
+
return { state, echo: "" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
state: { ...state, value: state.value + char },
|
|
59
|
+
echo: "*",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function promptHidden(message: string): Promise<string> {
|
|
64
|
+
if (!input.isTTY) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Cannot prompt for API key: stdin is not a TTY. Run `miru setup --key YOUR_KEY` or set TAKARA_API_KEY.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
output.write(message);
|
|
72
|
+
input.setRawMode?.(true);
|
|
73
|
+
input.resume();
|
|
74
|
+
input.setEncoding("utf8");
|
|
75
|
+
|
|
76
|
+
let state = createHiddenPromptState();
|
|
77
|
+
|
|
78
|
+
const onData = (chunk: string) => {
|
|
79
|
+
for (const char of chunk) {
|
|
80
|
+
const result = applyHiddenPromptChar(state, char);
|
|
81
|
+
state = result.state;
|
|
82
|
+
|
|
83
|
+
if (result.cancel) {
|
|
84
|
+
cleanup();
|
|
85
|
+
output.write("\n");
|
|
86
|
+
reject(new Error("Setup cancelled."));
|
|
87
|
+
process.exit(130);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (result.submit) {
|
|
91
|
+
cleanup();
|
|
92
|
+
output.write("\n");
|
|
93
|
+
resolve(state.value.trim());
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (result.echo) {
|
|
98
|
+
output.write(result.echo);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const cleanup = () => {
|
|
104
|
+
input.setRawMode?.(false);
|
|
105
|
+
input.pause();
|
|
106
|
+
input.removeListener("data", onData);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
input.on("data", onData);
|
|
110
|
+
});
|
|
111
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -1,65 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stdout as output } from "node:process";
|
|
2
2
|
import {
|
|
3
3
|
clearStoredCredentials,
|
|
4
|
-
hasEmbeddingApiKeyInEnv,
|
|
5
4
|
loadStoredCredentials,
|
|
6
5
|
readStoredCredentials,
|
|
7
6
|
resolveCredentialsPath,
|
|
8
7
|
saveStoredCredentials,
|
|
9
8
|
} from "./credentials.ts";
|
|
10
9
|
import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
|
|
11
|
-
import { resolveEmbeddingApiKey } from "./env.ts";
|
|
10
|
+
import { hasTakaraApiKeyInEnv, resolveEmbeddingApiKey } from "./env.ts";
|
|
11
|
+
import { promptHidden } from "./prompt.ts";
|
|
12
12
|
import { Spinner } from "./spinner.ts";
|
|
13
13
|
|
|
14
|
-
async function promptHidden(message: string): Promise<string> {
|
|
15
|
-
if (!input.isTTY) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
"Cannot prompt for API key: stdin is not a TTY. Run `miru setup --key YOUR_KEY` or set TAKARA_API_KEY.",
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
output.write(message);
|
|
23
|
-
input.setRawMode?.(true);
|
|
24
|
-
input.resume();
|
|
25
|
-
input.setEncoding("utf8");
|
|
26
|
-
|
|
27
|
-
let value = "";
|
|
28
|
-
const onData = (chunk: string) => {
|
|
29
|
-
const char = chunk[0];
|
|
30
|
-
if (char === "\n" || char === "\r" || char === "\u0004") {
|
|
31
|
-
cleanup();
|
|
32
|
-
output.write("\n");
|
|
33
|
-
resolve(value.trim());
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (char === "\u0003") {
|
|
37
|
-
cleanup();
|
|
38
|
-
output.write("\n");
|
|
39
|
-
reject(new Error("Setup cancelled."));
|
|
40
|
-
process.exit(130);
|
|
41
|
-
}
|
|
42
|
-
if (char === "\u007f" || char === "\b") {
|
|
43
|
-
if (value.length > 0) {
|
|
44
|
-
value = value.slice(0, -1);
|
|
45
|
-
output.write("\b \b");
|
|
46
|
-
}
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
value += chunk;
|
|
50
|
-
output.write("*");
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const cleanup = () => {
|
|
54
|
-
input.setRawMode?.(false);
|
|
55
|
-
input.pause();
|
|
56
|
-
input.removeListener("data", onData);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
input.on("data", onData);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
14
|
async function promptApiKey(): Promise<string> {
|
|
64
15
|
let key = "";
|
|
65
16
|
while (!key) {
|
|
@@ -78,7 +29,7 @@ export interface RunSetupOptions {
|
|
|
78
29
|
}
|
|
79
30
|
|
|
80
31
|
export async function runSetup(options: RunSetupOptions = {}): Promise<string> {
|
|
81
|
-
if (!options.force &&
|
|
32
|
+
if (!options.force && hasTakaraApiKeyInEnv()) {
|
|
82
33
|
const path = resolveCredentialsPath();
|
|
83
34
|
const stored = await readStoredCredentials();
|
|
84
35
|
if (stored) {
|