@takara-ai/miru-code 0.1.4 → 0.2.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
@@ -93,14 +93,14 @@ Run `miru init --agent <id> --force` to overwrite an existing sub-agent file.
93
93
 
94
94
  ### MCP server
95
95
 
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.
96
+ 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
97
 
98
98
  ```bash
99
99
  bunx @takara-ai/miru-code
100
100
  # or after bun add -g @takara-ai/miru-code: miru
101
101
  ```
102
102
 
103
- **Tools:** `search`, `find_related` — pass `repo` only when querying a different path or git URL.
103
+ **Tools:** `search`, `find_related` — `repo` is required (project root or https:// git URL).
104
104
 
105
105
  For MCP configs, use `bunx` as the command (`bun` with `x` in `args` works too — they're equivalent).
106
106
 
@@ -352,12 +352,14 @@ bun run typecheck
352
352
 
353
353
  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
354
 
355
- **Local first publish** (creates the package on npm):
355
+ **Local publish** (logged in to npm; `publishConfig.access` is already `public`):
356
356
 
357
357
  ```bash
358
- bun publish --access public
358
+ bun publish
359
359
  ```
360
360
 
361
+ 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.
362
+
361
363
  **CI** (after Trusted Publisher is configured on npm):
362
364
 
363
365
  ```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.2.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
@@ -8,7 +8,7 @@ import { serveMcp } from "./mcp/serve.ts";
8
8
  import { MiruIndex } from "./miru-index.ts";
9
9
  import { ensureCredentials, runClearCredentials, runSetup } from "./setup.ts";
10
10
  import type { ContentType } from "./types.ts";
11
- import { formatResults, isGitUrl, resolveChunk, resolveContent } from "./utils.ts";
11
+ import { formatResults, resolveChunk, resolveContent } from "./utils.ts";
12
12
 
13
13
  loadEnvFiles();
14
14
  await loadStoredCredentials();
@@ -121,10 +121,9 @@ CLI:
121
121
  miru clear [path]
122
122
 
123
123
  MCP server (default when no CLI subcommand):
124
- miru
125
- miru [path] [--ref BRANCH] [--content code ...]
124
+ miru [--ref BRANCH] [--content code ...]
126
125
 
127
- With no path, indexes the process working directory (Cursor sets this to your workspace).
126
+ Indexes a repo on the first search/find_related call via the tool repo argument.
128
127
 
129
128
  Examples:
130
129
  miru search "auth middleware" ./src
@@ -226,7 +225,6 @@ async function runCli(argv: string[]): Promise<void> {
226
225
  }
227
226
 
228
227
  async function runMcp(argv: string[]): Promise<void> {
229
- let path: string | null = null;
230
228
  let ref: string | null = null;
231
229
  const contentTokens: string[] = [];
232
230
 
@@ -247,15 +245,10 @@ async function runMcp(argv: string[]): Promise<void> {
247
245
  contentTokens.push(value);
248
246
  i++;
249
247
  }
250
- continue;
251
- }
252
- if (arg && !arg.startsWith("-") && path === null) {
253
- path = isGitUrl(arg) ? arg : resolve(arg);
254
248
  }
255
249
  }
256
250
 
257
251
  await serveMcp({
258
- path,
259
252
  ref,
260
253
  content: resolveContent(contentTokens.length > 0 ? contentTokens : ["code"]),
261
254
  });
@@ -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(