@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 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` / `OPENAI_API_KEY` / `MIRU_OPENAI_API_KEY` | (required) Bearer token for Takara |
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. It indexes the **current working directory** (most IDEs set this to your workspace). Optional path/ref flags pre-index a different repo.
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` — pass `repo` only when querying a different path or git URL.
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 first publish** (creates the package on npm):
351
+ **Local publish** (logged in to npm; `publishConfig.access` is already `public`):
356
352
 
357
353
  ```bash
358
- bun publish --access public
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.1.4",
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, isGitUrl, resolveChunk, resolveContent } from "./utils.ts";
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(["search", "find-related", "init", "setup", "clear", "-h", "--help"]);
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>(["claude", "copilot", "cursor", "gemini", "kiro", "opencode"]);
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
- const dest = await writeAgentFile(agent, { force });
105
- console.log(`Created ${dest}`);
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 === "-h" || command === "--help" || command === undefined) {
143
- printHelp();
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 = "claude";
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 ((arg === "--agent" || arg === "-a") && rest[i + 1]) {
155
- const value = rest[++i] as AgentId;
156
- if (AGENTS.has(value)) {
157
- agent = value;
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
- console.error("Usage: miru search <query> [path] [-k N] [--content ...]");
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
- console.error("Usage: miru find-related <file_path> <line> [path] [-k N] [--content ...]");
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
- printHelp();
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
  });
@@ -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 { EMBEDDING_API_KEY_NAMES, envFirstString } from "./env.ts";
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 (hasEmbeddingApiKeyInEnv()) {
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 EMBEDDING_API_KEY_NAMES = [
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 { EMBEDDING_API_KEY_NAMES };
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 = envFirstString([...EMBEDDING_API_KEY_NAMES], "");
41
+ const key = process.env[TAKARA_API_KEY_ENV]?.trim() ?? "";
45
42
  if (!key) {
46
43
  throw new Error(
47
- "Embedding API key required. Run `miru setup`, set TAKARA_API_KEY, OPENAI_API_KEY, " +
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
+ }
@@ -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 cacheKey = computeSourceCacheKey(source, ref);
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, ref, cacheKey);
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 (repo && isGitUrl(repo) && !repo.startsWith("https://") && !repo.startsWith("http://")) {
281
+ if (!repo) {
265
282
  throw new Error(
266
- `Only https://, http://, or local directory paths are accepted as \`repo\`. Got: ${repo}`,
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
- const source = repo ?? defaultSource;
271
- if (!source) {
287
+ if (isGitUrl(repo) && !repo.startsWith("https://") && !repo.startsWith("http://")) {
272
288
  throw new Error(
273
- "No repo specified and no default index. Pass an https:// or http:// git URL or local directory path as `repo`.",
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(source, ref);
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 ${source}: ${message}`);
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 { isGitUrl } from "../utils.ts";
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 defaultSource = options.path ?? resolve(process.cwd());
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
- "Required when no default index was configured at startup. " +
10
- "The index is cached after the first call, so repeat queries are fast.";
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
- "When working in a local project, pass the project root as `repo`. " +
16
- "For remote repos, pass an explicit https:// URL. Never guess or infer URLs. " +
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, defaultSource: string | null = null): McpServer {
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
- "Pass a git URL or local path as `repo` to index it on demand; indexes are cached for the session.",
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().nullable().optional().describe(REPO_DESCRIPTION),
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 ?? null, defaultSource, cache);
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().nullable().optional().describe(REPO_DESCRIPTION),
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 ?? null, defaultSource, cache);
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 { stdin as input, stdout as output } from "node:process";
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 && hasEmbeddingApiKeyInEnv()) {
32
+ if (!options.force && hasTakaraApiKeyInEnv()) {
82
33
  const path = resolveCredentialsPath();
83
34
  const stored = await readStoredCredentials();
84
35
  if (stored) {