@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 +9 -8
- package/package.json +2 -7
- package/src/mcp/index-cache.ts +160 -21
- package/src/mcp/serve.ts +2 -2
- package/bin/miru.cjs +0 -41
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
|
-
|
|
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
|
-
|
|
15
|
+
bun x @takara-ai/miru-code search "auth middleware" ./src
|
|
16
16
|
|
|
17
|
-
# Global CLI
|
|
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:
|
|
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": "
|
|
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 `
|
|
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.
|
|
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": "
|
|
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
|
},
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
78
|
+
this.invalidate(computeSourceCacheKey(source, ref));
|
|
22
79
|
}
|
|
23
80
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
95
|
+
this.clearEntry(oldest);
|
|
32
96
|
}
|
|
33
97
|
}
|
|
34
|
-
|
|
35
|
-
this.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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);
|