@takara-ai/miru-code 0.1.0 → 0.1.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 CHANGED
@@ -8,15 +8,14 @@ Indexes a local directory, combines semantic search with BM25, RRF fusion, and c
8
8
 
9
9
  ## Install
10
10
 
11
- Miru ships **Bun as a dependency** so `miru` uses a compatible runtime after install. You still need [Node.js](https://nodejs.org) on your PATH for the install step (Bun’s `postinstall`); do not use `--ignore-scripts`.
11
+ Requires [Bun](https://bun.sh) 1.1+ on your PATH (`curl -fsSL https://bun.sh/install | bash`).
12
12
 
13
13
  ```bash
14
14
  # Run once without installing
15
- bunx @takara-ai/miru-code search "auth middleware" ./src
15
+ bun x @takara-ai/miru-code search "auth middleware" ./src
16
16
 
17
- # Global CLI (npm or bun)
17
+ # Global CLI
18
18
  bun add -g @takara-ai/miru-code
19
- # or: npm install -g @takara-ai/miru-code
20
19
  miru search "auth middleware" ./src
21
20
 
22
21
  # Library
@@ -78,19 +77,21 @@ Run with **no arguments** to start the stdio MCP server. It indexes the **curren
78
77
  ```bash
79
78
  bun /path/to/miru/src/cli.ts
80
79
  # or after bun add -g @takara-ai/miru-code: miru
81
- # or without installing: bunx @takara-ai/miru-code
80
+ # or without installing: bun x @takara-ai/miru-code
82
81
  ```
83
82
 
84
83
  **Tools:** `search`, `find_related` — pass `repo` only when querying a different path or git URL.
85
84
 
86
85
  Cursor `~/.cursor/mcp.json` (or project `.cursor/mcp.json`):
87
86
 
87
+ Use **`bun x`** (not `bunx` as the command) so Cursor does not pull the `bun` npm package postinstall, which breaks in MCP sandboxes:
88
+
88
89
  ```json
89
90
  {
90
91
  "mcpServers": {
91
92
  "miru": {
92
- "command": "bunx",
93
- "args": ["@takara-ai/miru-code"],
93
+ "command": "bun",
94
+ "args": ["x", "@takara-ai/miru-code"],
94
95
  "env": {
95
96
  "TAKARA_API_KEY": "your-takara-bearer-token",
96
97
  "MIRU_OPENAI_BASE_URL": "https://infer.dev.takara.ai/v1",
@@ -133,7 +134,7 @@ Local development (path to this repo):
133
134
  }
134
135
  ```
135
136
 
136
- No project path in `args` for `bunx` / global install — Cursor runs the server with `cwd` set to the open workspace, which becomes the default index. Put the bearer token in `env`; MCP will not read `.env.local` unless those vars are unset.
137
+ No project path in `args` for `bun x` / global install — Cursor runs the server with `cwd` set to the open workspace, which becomes the default index. Put the bearer token in `env`; MCP will not read `.env.local` unless those vars are unset.
137
138
 
138
139
  ## Library
139
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,11 +26,8 @@
26
26
  "engines": {
27
27
  "bun": ">=1.1.0"
28
28
  },
29
- "trustedDependencies": [
30
- "bun"
31
- ],
32
29
  "bin": {
33
- "miru": "bin/miru.cjs"
30
+ "miru": "./src/cli.ts"
34
31
  },
35
32
  "main": "./src/index.ts",
36
33
  "types": "./src/index.ts",
@@ -38,7 +35,6 @@
38
35
  ".": "./src/index.ts"
39
36
  },
40
37
  "files": [
41
- "bin",
42
38
  "src",
43
39
  "README.md",
44
40
  "LICENSE"
@@ -60,7 +56,6 @@
60
56
  },
61
57
  "dependencies": {
62
58
  "@modelcontextprotocol/sdk": "^1.29.0",
63
- "bun": "^1.1.0",
64
59
  "ignore": "^7.0.5",
65
60
  "zod": "^4.4.3"
66
61
  },
@@ -1,16 +1,73 @@
1
1
  import { watch } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { MiruIndex } from "../miru-index.ts";
4
+ import { envOptionalInt } from "../env.ts";
4
5
  import type { ContentType } from "../types.ts";
5
6
  import { computeSourceCacheKey, isGitUrl } from "../utils.ts";
6
7
 
7
8
  const CACHE_MAX_SIZE = 10;
9
+ const DEFAULT_REINDEX_DEBOUNCE_MS = 3000;
8
10
 
9
- type IndexTask = Promise<MiruIndex>;
11
+ /** Directory names we skip for MCP fs.watch re-index triggers (aligned with file-walker). */
12
+ const WATCH_IGNORED_DIR_NAMES = new Set([
13
+ ".git",
14
+ ".hg",
15
+ ".svn",
16
+ "__pycache__",
17
+ "node_modules",
18
+ ".venv",
19
+ "venv",
20
+ ".tox",
21
+ ".mypy_cache",
22
+ ".pytest_cache",
23
+ ".ruff_cache",
24
+ ".cache",
25
+ ".miru",
26
+ ".next",
27
+ "dist",
28
+ "build",
29
+ ".eggs",
30
+ ]);
31
+
32
+ class StaleIndexError extends Error {
33
+ constructor() {
34
+ super("Index build superseded by a newer change");
35
+ this.name = "StaleIndexError";
36
+ }
37
+ }
38
+
39
+ type CacheEntry = {
40
+ generation: number;
41
+ task: Promise<MiruIndex> | null;
42
+ };
43
+
44
+ function resolveReindexDebounceMs(): number {
45
+ return envOptionalInt(["MIRU_MCP_REINDEX_DEBOUNCE_MS"], 0) ?? DEFAULT_REINDEX_DEBOUNCE_MS;
46
+ }
47
+
48
+ export function mcpWatchEnabled(): boolean {
49
+ const raw = process.env.MIRU_MCP_WATCH ?? process.env.SEMBLE_MCP_WATCH;
50
+ return raw !== "0" && raw !== "false";
51
+ }
52
+
53
+ /** Returns true when a watch event path should not trigger a re-index. */
54
+ export function shouldIgnoreWatchPath(relativePath: string | null | undefined): boolean {
55
+ if (!relativePath) {
56
+ return false;
57
+ }
58
+ const normalized = relativePath.replace(/\\/g, "/");
59
+ for (const segment of normalized.split("/")) {
60
+ if (segment && WATCH_IGNORED_DIR_NAMES.has(segment)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
10
66
 
11
67
  export class IndexCache {
12
68
  private readonly content: ContentType[];
13
- private readonly tasks = new Map<string, IndexTask>();
69
+ private readonly entries = new Map<string, CacheEntry>();
70
+ private readonly reindexTimers = new Map<string, ReturnType<typeof setTimeout>>();
14
71
  private watcher: ReturnType<typeof watch> | null = null;
15
72
 
16
73
  constructor(content: ContentType[] = ["code"]) {
@@ -18,46 +75,128 @@ export class IndexCache {
18
75
  }
19
76
 
20
77
  evict(source: string, ref?: string | null): void {
21
- this.tasks.delete(computeSourceCacheKey(source, ref));
78
+ this.invalidate(computeSourceCacheKey(source, ref));
22
79
  }
23
80
 
24
- async get(source: string, ref?: string | null): Promise<MiruIndex> {
25
- const cacheKey = computeSourceCacheKey(source, ref);
26
- let task = this.tasks.get(cacheKey);
27
- if (!task) {
28
- if (this.tasks.size >= CACHE_MAX_SIZE) {
29
- const oldest = this.tasks.keys().next().value;
81
+ private invalidate(cacheKey: string): void {
82
+ const entry = this.entries.get(cacheKey);
83
+ if (entry) {
84
+ entry.generation++;
85
+ entry.task = null;
86
+ }
87
+ }
88
+
89
+ private ensureEntry(cacheKey: string): CacheEntry {
90
+ let entry = this.entries.get(cacheKey);
91
+ if (!entry) {
92
+ if (this.entries.size >= CACHE_MAX_SIZE) {
93
+ const oldest = this.entries.keys().next().value;
30
94
  if (oldest) {
31
- this.tasks.delete(oldest);
95
+ this.clearEntry(oldest);
32
96
  }
33
97
  }
34
- task = MiruIndex.fromSource(source, this.content, undefined, ref);
35
- this.tasks.set(cacheKey, task);
98
+ entry = { generation: 0, task: null };
99
+ this.entries.set(cacheKey, entry);
36
100
  }
101
+ return entry;
102
+ }
103
+
104
+ private clearEntry(cacheKey: string): void {
105
+ const timer = this.reindexTimers.get(cacheKey);
106
+ if (timer) {
107
+ clearTimeout(timer);
108
+ this.reindexTimers.delete(cacheKey);
109
+ }
110
+ this.entries.delete(cacheKey);
111
+ }
37
112
 
38
- try {
39
- return await task;
40
- } catch (err) {
41
- if (this.tasks.get(cacheKey) === task) {
42
- this.tasks.delete(cacheKey);
113
+ private startBuild(
114
+ source: string,
115
+ ref: string | null | undefined,
116
+ entry: CacheEntry,
117
+ generationAtStart: number,
118
+ ): Promise<MiruIndex> {
119
+ return (async () => {
120
+ const index = await MiruIndex.fromSource(source, this.content, undefined, ref);
121
+ if (entry.generation !== generationAtStart) {
122
+ throw new StaleIndexError();
123
+ }
124
+ if (!isGitUrl(source)) {
125
+ await index.saveToDefaultCache(resolve(source));
126
+ }
127
+ return index;
128
+ })();
129
+ }
130
+
131
+ async get(source: string, ref?: string | null): Promise<MiruIndex> {
132
+ const cacheKey = computeSourceCacheKey(source, ref);
133
+
134
+ for (;;) {
135
+ const entry = this.ensureEntry(cacheKey);
136
+ const generationAtStart = entry.generation;
137
+
138
+ if (!entry.task) {
139
+ entry.task = this.startBuild(source, ref, entry, generationAtStart);
140
+ }
141
+
142
+ const task = entry.task;
143
+ try {
144
+ const index = await task;
145
+ if (entry.generation !== generationAtStart) {
146
+ continue;
147
+ }
148
+ return index;
149
+ } catch (err) {
150
+ if (entry.task === task) {
151
+ entry.task = null;
152
+ }
153
+ if (err instanceof StaleIndexError) {
154
+ continue;
155
+ }
156
+ if (this.entries.get(cacheKey) === entry) {
157
+ this.entries.delete(cacheKey);
158
+ }
159
+ throw err;
43
160
  }
44
- throw err;
45
161
  }
46
162
  }
47
163
 
164
+ private scheduleReindex(source: string, ref?: string | null): void {
165
+ const cacheKey = computeSourceCacheKey(source, ref);
166
+ const existing = this.reindexTimers.get(cacheKey);
167
+ if (existing) {
168
+ clearTimeout(existing);
169
+ }
170
+
171
+ const debounceMs = resolveReindexDebounceMs();
172
+ const timer = setTimeout(() => {
173
+ this.reindexTimers.delete(cacheKey);
174
+ this.invalidate(cacheKey);
175
+ void this.get(source, ref).catch(() => undefined);
176
+ }, debounceMs);
177
+
178
+ this.reindexTimers.set(cacheKey, timer);
179
+ }
180
+
48
181
  startWatcher(path: string): void {
49
182
  const resolved = resolve(path);
50
183
  if (this.watcher) {
51
184
  this.watcher.close();
52
185
  }
53
186
 
54
- this.watcher = watch(resolved, { recursive: true }, () => {
55
- this.evict(resolved);
56
- void this.get(resolved).catch(() => undefined);
187
+ this.watcher = watch(resolved, { recursive: true }, (_event, filename) => {
188
+ if (shouldIgnoreWatchPath(filename)) {
189
+ return;
190
+ }
191
+ this.scheduleReindex(resolved);
57
192
  });
58
193
  }
59
194
 
60
195
  close(): void {
196
+ for (const timer of this.reindexTimers.values()) {
197
+ clearTimeout(timer);
198
+ }
199
+ this.reindexTimers.clear();
61
200
  this.watcher?.close();
62
201
  this.watcher = null;
63
202
  }
package/src/mcp/serve.ts CHANGED
@@ -2,7 +2,7 @@ import { resolve } from "node:path";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import type { ContentType } from "../types.ts";
4
4
  import { isGitUrl } from "../utils.ts";
5
- import { IndexCache } from "./index-cache.ts";
5
+ import { IndexCache, mcpWatchEnabled } from "./index-cache.ts";
6
6
  import { createMcpServer } from "./server.ts";
7
7
 
8
8
  export async function serveMcp(options: {
@@ -14,7 +14,7 @@ export async function serveMcp(options: {
14
14
  const defaultSource = options.path ?? resolve(process.cwd());
15
15
 
16
16
  void cache.get(defaultSource, options.ref).catch(() => undefined);
17
- if (!isGitUrl(defaultSource)) {
17
+ if (!isGitUrl(defaultSource) && mcpWatchEnabled()) {
18
18
  cache.startWatcher(defaultSource);
19
19
  }
20
20
 
package/bin/miru.cjs DELETED
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { spawnSync } = require("node:child_process");
5
- const fs = require("node:fs");
6
- const path = require("node:path");
7
-
8
- const packageRoot = path.join(__dirname, "..");
9
-
10
- function resolveBunExecutable() {
11
- const binName = process.platform === "win32" ? "bun.cmd" : "bun";
12
- const candidates = [
13
- path.join(packageRoot, "node_modules", ".bin", binName),
14
- path.join(packageRoot, "node_modules", "bun", "bin", process.platform === "win32" ? "bun.exe" : "bun"),
15
- ];
16
- for (const candidate of candidates) {
17
- if (fs.existsSync(candidate)) {
18
- return candidate;
19
- }
20
- }
21
- return binName;
22
- }
23
-
24
- const cli = path.join(packageRoot, "src", "cli.ts");
25
- const bun = resolveBunExecutable();
26
- const result = spawnSync(bun, [cli, ...process.argv.slice(2)], {
27
- stdio: "inherit",
28
- cwd: process.cwd(),
29
- env: process.env,
30
- shell: process.platform === "win32",
31
- });
32
-
33
- if (result.error) {
34
- console.error(
35
- "miru: failed to run Bun. Reinstall with lifecycle scripts enabled (do not use --ignore-scripts).",
36
- );
37
- console.error(result.error.message);
38
- process.exit(1);
39
- }
40
-
41
- process.exit(result.status ?? 1);