@takara-ai/miru-code 1.1.2 → 1.2.1
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 +178 -107
- package/package.json +9 -5
- package/src/benchmark/compare.ts +278 -0
- package/src/benchmark/grep.ts +352 -0
- package/src/benchmark/history.ts +206 -0
- package/src/benchmark/locate-compare.ts +65 -0
- package/src/benchmark/rg-literal.ts +143 -0
- package/src/benchmark/summary.ts +29 -0
- package/src/benchmark/types.ts +65 -0
- package/src/cli.ts +185 -4
- package/src/credentials.ts +8 -0
- package/src/help.ts +45 -2
- package/src/index/create.ts +2 -2
- package/src/index/incremental.ts +4 -3
- package/src/index.ts +3 -1
- package/src/installer/benchmark-mode.ts +248 -0
- package/src/installer/config.ts +14 -2
- package/src/installer/hooks/opencode-plugin.ts +6 -20
- package/src/installer/hooks/search-guard.ts +4 -5
- package/src/installer/installer.ts +59 -6
- package/src/installer/search-policy.ts +28 -9
- package/src/literal.ts +139 -0
- package/src/mcp/index-cache.ts +2 -2
- package/src/mcp/serve.ts +4 -3
- package/src/mcp/server.ts +166 -9
- package/src/miru-index.ts +15 -6
- package/src/snippet.ts +6 -3
- package/src/token-count.ts +62 -0
- package/src/tokenizer/bert-wordpiece.ts +219 -0
- package/src/tokenizer/load.ts +14 -0
- package/src/types.ts +8 -0
- package/src/utils.ts +8 -1
- package/tokenizer/tokenizer.json +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { AGENT_TARGETS, type AgentTarget, type InstallAction } from "./agents.ts";
|
|
2
|
+
import { removeTomlBlock, stripJsonComments } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
export const MCP_BENCHMARK_FLAG = "--benchmark";
|
|
5
|
+
|
|
6
|
+
const CODEX_MCP_HEADER = "[mcp_servers.miru]";
|
|
7
|
+
|
|
8
|
+
function codexMcpBlock(enabled: boolean): string {
|
|
9
|
+
const args = enabled
|
|
10
|
+
? `["@takara-ai/miru-code", "${MCP_BENCHMARK_FLAG}"]`
|
|
11
|
+
: '["@takara-ai/miru-code"]';
|
|
12
|
+
return `${CODEX_MCP_HEADER}\ncommand = "bunx"\nargs = ${args}\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function withBenchmarkFlag(list: string[], enabled: boolean): string[] {
|
|
16
|
+
const without = list.filter((item) => item !== MCP_BENCHMARK_FLAG);
|
|
17
|
+
return enabled ? [...without, MCP_BENCHMARK_FLAG] : without;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listHasBenchmarkFlag(list: unknown): boolean {
|
|
21
|
+
return Array.isArray(list) && list.some((item) => item === MCP_BENCHMARK_FLAG);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Toggle `--benchmark` on a JSON MCP server entry (`args` and/or array `command`). */
|
|
25
|
+
export function applyBenchmarkFlagToMcpEntry(
|
|
26
|
+
entry: Record<string, unknown>,
|
|
27
|
+
enabled: boolean,
|
|
28
|
+
): { entry: Record<string, unknown>; changed: boolean; enabled: boolean } {
|
|
29
|
+
const next: Record<string, unknown> = { ...entry };
|
|
30
|
+
let changed = false;
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(next.args)) {
|
|
33
|
+
const args = next.args.filter((item): item is string => typeof item === "string");
|
|
34
|
+
const updated = withBenchmarkFlag(args, enabled);
|
|
35
|
+
if (JSON.stringify(updated) !== JSON.stringify(args)) {
|
|
36
|
+
next.args = updated;
|
|
37
|
+
changed = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(next.command)) {
|
|
42
|
+
const command = next.command.filter((item): item is string => typeof item === "string");
|
|
43
|
+
const updated = withBenchmarkFlag(command, enabled);
|
|
44
|
+
if (JSON.stringify(updated) !== JSON.stringify(command)) {
|
|
45
|
+
next.command = updated;
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
entry: next,
|
|
52
|
+
changed,
|
|
53
|
+
enabled: mcpEntryHasBenchmark(next),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mcpEntryHasBenchmark(entry: Record<string, unknown>): boolean {
|
|
58
|
+
return listHasBenchmarkFlag(entry.args) || listHasBenchmarkFlag(entry.command);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Keep `--benchmark` on the canonical install entry when the existing MCP
|
|
63
|
+
* entry already had it (so `miru install` does not silently turn mode off).
|
|
64
|
+
*/
|
|
65
|
+
export function withPreservedBenchmarkFlag(
|
|
66
|
+
canonical: Record<string, unknown>,
|
|
67
|
+
existing: Record<string, unknown> | null | undefined,
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
if (!existing || !mcpEntryHasBenchmark(existing)) {
|
|
70
|
+
return canonical;
|
|
71
|
+
}
|
|
72
|
+
return applyBenchmarkFlagToMcpEntry(canonical, true).entry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface BenchmarkModeTargetResult {
|
|
76
|
+
agent: string;
|
|
77
|
+
path: string;
|
|
78
|
+
action: InstallAction | "enabled" | "disabled" | "missing";
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readJsonObject(path: string): Promise<Record<string, unknown> | null> {
|
|
83
|
+
if (!(await Bun.file(path).exists())) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const text = stripJsonComments(await Bun.file(path).text());
|
|
88
|
+
const parsed = JSON.parse(text) as unknown;
|
|
89
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return parsed as Record<string, unknown>;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeJsonObject(path: string, value: Record<string, unknown>): Promise<void> {
|
|
99
|
+
await Bun.write(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function statusForToml(path: string): Promise<boolean | null> {
|
|
103
|
+
if (!(await Bun.file(path).exists())) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const text = await Bun.file(path).text();
|
|
107
|
+
if (!text.includes(CODEX_MCP_HEADER)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return text.includes(`"${MCP_BENCHMARK_FLAG}"`) || text.includes(`'${MCP_BENCHMARK_FLAG}'`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function setTomlBenchmark(
|
|
114
|
+
path: string,
|
|
115
|
+
enabled: boolean,
|
|
116
|
+
): Promise<InstallAction | "missing"> {
|
|
117
|
+
if (!(await Bun.file(path).exists())) {
|
|
118
|
+
return "missing";
|
|
119
|
+
}
|
|
120
|
+
const text = await Bun.file(path).text();
|
|
121
|
+
if (!text.includes(CODEX_MCP_HEADER)) {
|
|
122
|
+
return "missing";
|
|
123
|
+
}
|
|
124
|
+
const current = await statusForToml(path);
|
|
125
|
+
if (current === enabled) {
|
|
126
|
+
return "unchanged";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Replace the miru block with the desired args, preserving the rest of the file.
|
|
130
|
+
await removeTomlBlock(path);
|
|
131
|
+
const existed = await Bun.file(path).exists();
|
|
132
|
+
const existing = existed ? await Bun.file(path).text() : "";
|
|
133
|
+
const base = existing.replace(/\n+$/, "");
|
|
134
|
+
const block = codexMcpBlock(enabled);
|
|
135
|
+
const next = base.length > 0 ? `${base}\n\n${block}` : block;
|
|
136
|
+
await Bun.write(path, next.endsWith("\n") ? next : `${next}\n`);
|
|
137
|
+
return "updated";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function statusForJson(agent: AgentTarget): Promise<boolean | null> {
|
|
141
|
+
const mcp = agent.mcp;
|
|
142
|
+
if (mcp?.format !== "json") {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const parsed = await readJsonObject(mcp.path);
|
|
146
|
+
if (!parsed) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const section = parsed[mcp.key];
|
|
150
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const entry = (section as Record<string, unknown>)[mcp.memberKey];
|
|
154
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return mcpEntryHasBenchmark(entry as Record<string, unknown>);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function setJsonBenchmark(
|
|
161
|
+
agent: AgentTarget,
|
|
162
|
+
enabled: boolean,
|
|
163
|
+
): Promise<InstallAction | "missing"> {
|
|
164
|
+
const mcp = agent.mcp;
|
|
165
|
+
if (mcp?.format !== "json") {
|
|
166
|
+
return "missing";
|
|
167
|
+
}
|
|
168
|
+
const parsed = await readJsonObject(mcp.path);
|
|
169
|
+
if (!parsed) {
|
|
170
|
+
return "missing";
|
|
171
|
+
}
|
|
172
|
+
const sectionRaw = parsed[mcp.key];
|
|
173
|
+
if (!sectionRaw || typeof sectionRaw !== "object" || Array.isArray(sectionRaw)) {
|
|
174
|
+
return "missing";
|
|
175
|
+
}
|
|
176
|
+
const section = sectionRaw as Record<string, unknown>;
|
|
177
|
+
const entryRaw = section[mcp.memberKey];
|
|
178
|
+
if (!entryRaw || typeof entryRaw !== "object" || Array.isArray(entryRaw)) {
|
|
179
|
+
return "missing";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const applied = applyBenchmarkFlagToMcpEntry(entryRaw as Record<string, unknown>, enabled);
|
|
183
|
+
if (!applied.changed) {
|
|
184
|
+
return "unchanged";
|
|
185
|
+
}
|
|
186
|
+
section[mcp.memberKey] = applied.entry;
|
|
187
|
+
parsed[mcp.key] = section;
|
|
188
|
+
await writeJsonObject(mcp.path, parsed);
|
|
189
|
+
return "updated";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Inspect installed Miru MCP configs for `--benchmark`. */
|
|
193
|
+
export async function getBenchmarkModeStatus(): Promise<BenchmarkModeTargetResult[]> {
|
|
194
|
+
const results: BenchmarkModeTargetResult[] = [];
|
|
195
|
+
for (const agent of AGENT_TARGETS) {
|
|
196
|
+
const mcp = agent.mcp;
|
|
197
|
+
if (!mcp) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (mcp.format === "toml") {
|
|
201
|
+
const enabled = await statusForToml(mcp.path);
|
|
202
|
+
results.push({
|
|
203
|
+
agent: agent.displayName,
|
|
204
|
+
path: mcp.path,
|
|
205
|
+
action: enabled == null ? "missing" : enabled ? "enabled" : "disabled",
|
|
206
|
+
enabled: enabled === true,
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const enabled = await statusForJson(agent);
|
|
211
|
+
results.push({
|
|
212
|
+
agent: agent.displayName,
|
|
213
|
+
path: mcp.path,
|
|
214
|
+
action: enabled == null ? "missing" : enabled ? "enabled" : "disabled",
|
|
215
|
+
enabled: enabled === true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Enable or disable `--benchmark` on every installed Miru MCP entry. */
|
|
222
|
+
export async function setBenchmarkMode(enabled: boolean): Promise<BenchmarkModeTargetResult[]> {
|
|
223
|
+
const results: BenchmarkModeTargetResult[] = [];
|
|
224
|
+
for (const agent of AGENT_TARGETS) {
|
|
225
|
+
const mcp = agent.mcp;
|
|
226
|
+
if (!mcp) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (mcp.format === "toml") {
|
|
230
|
+
const action = await setTomlBenchmark(mcp.path, enabled);
|
|
231
|
+
results.push({
|
|
232
|
+
agent: agent.displayName,
|
|
233
|
+
path: mcp.path,
|
|
234
|
+
action,
|
|
235
|
+
enabled: action === "missing" ? false : enabled,
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const action = await setJsonBenchmark(agent, enabled);
|
|
240
|
+
results.push({
|
|
241
|
+
agent: agent.displayName,
|
|
242
|
+
path: mcp.path,
|
|
243
|
+
action,
|
|
244
|
+
enabled: action === "missing" ? false : enabled,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return results;
|
|
248
|
+
}
|
package/src/installer/config.ts
CHANGED
|
@@ -6,6 +6,11 @@ const CODEX_MCP_BLOCK = `[mcp_servers.miru]
|
|
|
6
6
|
command = "bunx"
|
|
7
7
|
args = ["@takara-ai/miru-code"]
|
|
8
8
|
`;
|
|
9
|
+
/** Same Codex block with `--benchmark` preserved across reinstall. */
|
|
10
|
+
const CODEX_MCP_BLOCK_BENCHMARK = `[mcp_servers.miru]
|
|
11
|
+
command = "bunx"
|
|
12
|
+
args = ["@takara-ai/miru-code", "--benchmark"]
|
|
13
|
+
`;
|
|
9
14
|
|
|
10
15
|
/** Strip line and block comments so JSONC configs can be parsed. */
|
|
11
16
|
export function stripJsonComments(text: string): string {
|
|
@@ -487,12 +492,19 @@ export async function mergeTomlBlock(path: string): Promise<InstallAction> {
|
|
|
487
492
|
const existed = await Bun.file(path).exists();
|
|
488
493
|
const existing = existed ? await Bun.file(path).text() : "";
|
|
489
494
|
|
|
490
|
-
|
|
495
|
+
const defaultBlock = CODEX_MCP_BLOCK.trim();
|
|
496
|
+
const benchmarkBlock = CODEX_MCP_BLOCK_BENCHMARK.trim();
|
|
497
|
+
if (existing.includes(defaultBlock) || existing.includes(benchmarkBlock)) {
|
|
491
498
|
return "unchanged";
|
|
492
499
|
}
|
|
493
500
|
|
|
501
|
+
const preserveBenchmark =
|
|
502
|
+
existing.includes(CODEX_MCP_HEADER) &&
|
|
503
|
+
(existing.includes('"--benchmark"') || existing.includes("'--benchmark'"));
|
|
504
|
+
const block = preserveBenchmark ? CODEX_MCP_BLOCK_BENCHMARK : CODEX_MCP_BLOCK;
|
|
505
|
+
|
|
494
506
|
const base = stripTomlSection(existing, CODEX_MCP_HEADER).replace(/\n+$/, "");
|
|
495
|
-
const next = base.length > 0 ? `${base}\n\n${
|
|
507
|
+
const next = base.length > 0 ? `${base}\n\n${block}` : block;
|
|
496
508
|
await Bun.write(path, next.endsWith("\n") ? next : `${next}\n`);
|
|
497
509
|
return existed ? "updated" : "created";
|
|
498
510
|
}
|
|
@@ -3,28 +3,12 @@
|
|
|
3
3
|
* Installed to ~/.config/opencode/plugins/miru-search-guard.ts
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const REDIRECT =
|
|
6
|
+
const REDIRECT =
|
|
7
|
+
"Use Miru MCP (repo = project root): `locate` for exact literals, `search` for meaning-based questions — not grep/glob/bash exploration.";
|
|
7
8
|
|
|
8
9
|
const GREP_TOOLS = new Set(["grep", "glob", "codesearch", "codebase_search", "search"]);
|
|
9
10
|
const SHELL_TOOLS = new Set(["bash", "shell", "sh"]);
|
|
10
11
|
|
|
11
|
-
function isLiteral(pattern: string): boolean {
|
|
12
|
-
const trimmed = pattern.trim();
|
|
13
|
-
if (!trimmed) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
if (/^[`'"][^`'"]+[`'"]$/.test(trimmed)) {
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
if (/^[A-Z][A-Z0-9_]{2,}$/.test(trimmed)) {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed) && trimmed.length <= 48) {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
12
|
function isExplorationShell(command: string): boolean {
|
|
29
13
|
const cmd = command.trim();
|
|
30
14
|
if (!cmd) {
|
|
@@ -43,9 +27,11 @@ function shouldBlock(tool: string, args: Record<string, unknown>): boolean {
|
|
|
43
27
|
if (name.includes("miru")) {
|
|
44
28
|
return false;
|
|
45
29
|
}
|
|
30
|
+
if (name === "glob") {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
46
33
|
if (GREP_TOOLS.has(name)) {
|
|
47
|
-
|
|
48
|
-
return !isLiteral(pattern);
|
|
34
|
+
return true;
|
|
49
35
|
}
|
|
50
36
|
if (SHELL_TOOLS.has(name)) {
|
|
51
37
|
const command = String(args.command ?? args.cmd ?? "");
|
|
@@ -2,7 +2,7 @@ import { SEARCH_GUARD_EXPAND_HINT } from "../search-policy.ts";
|
|
|
2
2
|
|
|
3
3
|
const REDIRECT_PREFIX =
|
|
4
4
|
"Use Miru MCP instead of built-in search tools (pass project root as `repo`). " +
|
|
5
|
-
"
|
|
5
|
+
"Prefer Miru `locate` for exact literals and `search` for meaning-based questions.";
|
|
6
6
|
|
|
7
7
|
const MIRU_TOOL_RE = /miru/i;
|
|
8
8
|
|
|
@@ -121,6 +121,9 @@ export function searchGuardBlockReason(
|
|
|
121
121
|
toolInput: Record<string, unknown>,
|
|
122
122
|
): string {
|
|
123
123
|
const query = suggestedMiruQuery(toolName, toolInput);
|
|
124
|
+
if (GREP_TOOL_RE.test(toolName) && isLiteralGrepPattern(grepPatternFromInput(toolInput))) {
|
|
125
|
+
return `${REDIRECT_PREFIX} Try Miru \`locate\` with literal "${query.replace(/^['"`]|['"`]$/g, "")}". ${SEARCH_GUARD_EXPAND_HINT}`;
|
|
126
|
+
}
|
|
124
127
|
return `${REDIRECT_PREFIX} Try Miru \`search\` with query "${query}". ${SEARCH_GUARD_EXPAND_HINT}`;
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -166,10 +169,6 @@ export function evaluateSearchGuard(payload: HookPayload): GuardDecision {
|
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
if (GREP_TOOL_RE.test(toolName)) {
|
|
169
|
-
const pattern = grepPatternFromInput(toolInput);
|
|
170
|
-
if (isLiteralGrepPattern(pattern)) {
|
|
171
|
-
return { block: false, reason: "" };
|
|
172
|
-
}
|
|
173
172
|
return { block: true, reason: searchGuardBlockReason(toolName, toolInput) };
|
|
174
173
|
}
|
|
175
174
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unlink } from "node:fs/promises";
|
|
2
2
|
import { loadAgentTemplate } from "../agents.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { clearBenchmarkHistory } from "../benchmark/history.ts";
|
|
4
|
+
import { brandTitle, dim, divider, green, hint, info, success, writeStdout } from "../cli-ui.ts";
|
|
4
5
|
import { ensureCredentials } from "../setup.ts";
|
|
5
6
|
import {
|
|
6
7
|
AGENT_TARGETS,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
type InstallMode,
|
|
11
12
|
isAgentDetected,
|
|
12
13
|
} from "./agents.ts";
|
|
14
|
+
import { withPreservedBenchmarkFlag } from "./benchmark-mode.ts";
|
|
13
15
|
import {
|
|
14
16
|
mergeJsonMember,
|
|
15
17
|
mergeTomlBlock,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
removeMarked,
|
|
18
20
|
removeTomlBlock,
|
|
19
21
|
replaceOrAppendMarked,
|
|
22
|
+
stripJsonComments,
|
|
20
23
|
} from "./config.ts";
|
|
21
24
|
import { mergeHooks, removeHooks } from "./hooks/install.ts";
|
|
22
25
|
import { promptConfirm, promptMultiSelect, requireInteractiveTerminal } from "./prompt.ts";
|
|
@@ -54,6 +57,33 @@ const ACTION_ICON: Partial<Record<InstallAction, string>> = {
|
|
|
54
57
|
error: dim("✗"),
|
|
55
58
|
};
|
|
56
59
|
|
|
60
|
+
async function readExistingJsonMcpEntry(
|
|
61
|
+
path: string,
|
|
62
|
+
sectionKey: string,
|
|
63
|
+
memberKey: string,
|
|
64
|
+
): Promise<Record<string, unknown> | null> {
|
|
65
|
+
if (!(await Bun.file(path).exists())) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(stripJsonComments(await Bun.file(path).text())) as unknown;
|
|
70
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const section = (parsed as Record<string, unknown>)[sectionKey];
|
|
74
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const entry = (section as Record<string, unknown>)[memberKey];
|
|
78
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return entry as Record<string, unknown>;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
async function applyMcp(agent: AgentTarget, mode: InstallMode): Promise<WriteResult | null> {
|
|
58
88
|
const mcp = agent.mcp;
|
|
59
89
|
if (!mcp) {
|
|
@@ -66,10 +96,14 @@ async function applyMcp(agent: AgentTarget, mode: InstallMode): Promise<WriteRes
|
|
|
66
96
|
return { path: mcp.path, action };
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
if (mode !== "install") {
|
|
100
|
+
const action = await removeJsonMember(mcp.path, mcp.key, mcp.memberKey);
|
|
101
|
+
return { path: mcp.path, action };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const existing = await readExistingJsonMcpEntry(mcp.path, mcp.key, mcp.memberKey);
|
|
105
|
+
const entry = withPreservedBenchmarkFlag(mcp.entry, existing);
|
|
106
|
+
const action = await mergeJsonMember(mcp.path, mcp.key, mcp.memberKey, entry);
|
|
73
107
|
return { path: mcp.path, action };
|
|
74
108
|
}
|
|
75
109
|
|
|
@@ -183,7 +217,7 @@ const INTEGRATIONS: Integration[] = [
|
|
|
183
217
|
label: "Search hooks",
|
|
184
218
|
description: "blocks built-in search; routes to Miru MCP",
|
|
185
219
|
experimental: true,
|
|
186
|
-
defaultChecked:
|
|
220
|
+
defaultChecked: false,
|
|
187
221
|
planPath: (agent) => agent.hooksPath,
|
|
188
222
|
apply: applyHooks,
|
|
189
223
|
},
|
|
@@ -235,6 +269,18 @@ async function apply(
|
|
|
235
269
|
writeStdout("");
|
|
236
270
|
}
|
|
237
271
|
|
|
272
|
+
/** Local Miru state cleaned up during uninstall (global, not per-agent). */
|
|
273
|
+
export async function removeUninstallLocalData(): Promise<{
|
|
274
|
+
benchmarkHistoryCleared: boolean;
|
|
275
|
+
benchmarkHistoryPath: string;
|
|
276
|
+
}> {
|
|
277
|
+
const result = await clearBenchmarkHistory();
|
|
278
|
+
return {
|
|
279
|
+
benchmarkHistoryCleared: result.cleared,
|
|
280
|
+
benchmarkHistoryPath: result.path,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
238
284
|
export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
239
285
|
const install = mode === "install";
|
|
240
286
|
requireInteractiveTerminal(`miru ${mode}`);
|
|
@@ -297,6 +343,13 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
297
343
|
|
|
298
344
|
await apply(mode, chosenAgents, chosenIntegrations);
|
|
299
345
|
|
|
346
|
+
if (!install) {
|
|
347
|
+
const local = await removeUninstallLocalData();
|
|
348
|
+
if (local.benchmarkHistoryCleared) {
|
|
349
|
+
info(`Removed benchmark report ${local.benchmarkHistoryPath}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
300
353
|
success(install ? "Done. Restart agents to apply changes." : "Done. Configuration removed.");
|
|
301
354
|
writeStdout("");
|
|
302
355
|
}
|
|
@@ -18,7 +18,7 @@ export function buildSearchPolicyTable(native: NativeToolNames): string {
|
|
|
18
18
|
| Same file, more context | Miru MCP \`expand\` on \`truncated: true\` | Re-search, ${native.read} whole file |
|
|
19
19
|
| Similar code elsewhere | Miru MCP \`find_related\` | ${native.grep} chains |
|
|
20
20
|
| Search docs or config | Miru \`search\` | ${native.grep} README paths |
|
|
21
|
-
| Exact literal string in a file? | ${native.grep}
|
|
21
|
+
| Exact literal string in a file? | Miru MCP \`locate\` | ${native.grep}, Miru \`search\` |
|
|
22
22
|
| Edit a known file:line | ${native.read} (after Miru found it) | ${native.read}-before-search |`;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -29,6 +29,7 @@ ${SNIPPET_GUIDANCE}
|
|
|
29
29
|
|
|
30
30
|
Use Miru MCP tools:
|
|
31
31
|
- \`search\` — one call per question; pass project root as \`repo\`
|
|
32
|
+
- \`locate\` — exact substring (env var, symbol, error code); prefer \`mode=count\` or \`locations\`
|
|
32
33
|
- \`expand\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\`)
|
|
33
34
|
- \`find_related\` — similar code in other files (hits may also be snippets; use \`expand\` if truncated)
|
|
34
35
|
|
|
@@ -38,7 +39,6 @@ Stop rules:
|
|
|
38
39
|
- ${native.read} is for editing a path Miru already gave you, not for exploration
|
|
39
40
|
|
|
40
41
|
Native tools are allowed ONLY when:
|
|
41
|
-
- confirming an exact literal string (env var name, error code, quoted text)
|
|
42
42
|
- reading a file you already located via Miru, to edit it
|
|
43
43
|
- searching outside the indexed repo
|
|
44
44
|
|
|
@@ -49,15 +49,17 @@ export function buildMcpWorkflow(native: NativeToolNames): string {
|
|
|
49
49
|
return `### MCP workflow
|
|
50
50
|
|
|
51
51
|
1. Call \`search\` with \`repo\` set to the project root (local path or https:// git URL).
|
|
52
|
-
2.
|
|
53
|
-
3.
|
|
54
|
-
4.
|
|
52
|
+
2. For exact literals, call \`locate\` (prefer \`mode=count\` or \`locations\`).
|
|
53
|
+
3. If a hit has \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\`.
|
|
54
|
+
4. Use \`find_related\` to trace similar code in other files — not for more context in the same file.
|
|
55
|
+
5. ${native.read} via \`absolute_path\` only when editing or when \`expand\` still lacks context.`;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const CLI_FALLBACK = `### CLI fallback (no MCP in this session)
|
|
58
59
|
|
|
59
60
|
\`\`\`bash
|
|
60
61
|
miru search "authentication flow" .
|
|
62
|
+
miru locate DATABASE_URL . --mode locations
|
|
61
63
|
miru expand src/auth.ts 42 .
|
|
62
64
|
miru find-related src/auth.ts 42 .
|
|
63
65
|
\`\`\`
|
|
@@ -65,7 +67,7 @@ miru find-related src/auth.ts 42 .
|
|
|
65
67
|
If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;
|
|
66
68
|
|
|
67
69
|
export function buildSubagentBody(native: NativeToolNames): string {
|
|
68
|
-
return `When Miru MCP is available, use MCP \`search\`, \`expand\`, and \`find_related\` — not ${native.explorationDenied} for exploration.
|
|
70
|
+
return `When Miru MCP is available, use MCP \`search\`, \`locate\`, \`expand\`, and \`find_related\` — not ${native.explorationDenied} for exploration.
|
|
69
71
|
|
|
70
72
|
${buildSearchPolicyBody(native)}
|
|
71
73
|
|
|
@@ -108,7 +110,7 @@ alwaysApply: true
|
|
|
108
110
|
|
|
109
111
|
${SEARCH_POLICY_BODY}
|
|
110
112
|
|
|
111
|
-
When Miru MCP is connected, call \`search\` once per question. On \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\`. Never use Cursor SemanticSearch
|
|
113
|
+
When Miru MCP is connected, call \`search\` once per question. On \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\`. Use \`locate\` for exact literals — not Grep. Never use Cursor SemanticSearch for codebase questions.`;
|
|
112
114
|
|
|
113
115
|
export const SUBAGENT_BODY = buildSubagentBody(DEFAULT_NATIVE_TOOLS);
|
|
114
116
|
|
|
@@ -119,15 +121,28 @@ export const MCP_SERVER_INSTRUCTIONS =
|
|
|
119
121
|
"Workflow: (1) `search` once per question — do NOT re-search with paraphrases. " +
|
|
120
122
|
"(2) On `truncated: true`, call `expand` with `file_path` and `anchor_line` — do NOT read the whole file. " +
|
|
121
123
|
"(3) `find_related` traces similar code in other files, not more context in the same file. " +
|
|
124
|
+
"(4) `locate` finds exact substrings (env vars, symbols, error codes) — prefer over Grep; use mode=count or locations when possible. " +
|
|
122
125
|
"Always pass the project root as `repo`. Local repos return `absolute_path` on each hit — use Read only to edit. " +
|
|
123
|
-
"Native
|
|
126
|
+
"Native Grep/Glob only outside the indexed repo or for non-code tasks.";
|
|
127
|
+
|
|
128
|
+
export const MCP_BENCHMARK_SERVER_INSTRUCTIONS =
|
|
129
|
+
`${MCP_SERVER_INSTRUCTIONS} ` +
|
|
130
|
+
"BENCHMARK MODE: each `search` and `locate` includes compact `benchmark` {save_pct,miru_tok,grep_tok,saved_tok,rank1}. " +
|
|
131
|
+
"Call `read_benchmark` for cumulative totals across both. Do not narrate benchmark stats unless the user asks. " +
|
|
132
|
+
"To leave benchmark mode, tell the user to run `miru benchmark off` and restart the agent.";
|
|
124
133
|
|
|
125
134
|
export const MCP_SEARCH_TOOL_DESCRIPTION =
|
|
126
135
|
"Your default search for all code search queries in this indexed repo — the best, fastest, and cheapest way to find code; better than any other tool. " +
|
|
127
136
|
"Returns compact snippets (~±15 lines). One call per question is usually enough. " +
|
|
128
|
-
"
|
|
137
|
+
"For exact literals (env vars, symbols, error codes), use `locate` instead. " +
|
|
129
138
|
"When a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — not re-search or Read.";
|
|
130
139
|
|
|
140
|
+
export const MCP_LOCATE_TOOL_DESCRIPTION =
|
|
141
|
+
"Exact substring locator over the Miru index. Use for known literals (env vars, symbols, error codes, quoted text) — not meaning-based questions (`search`). " +
|
|
142
|
+
"Returns ALL matches by default as compact {n,files,hits} — do NOT fall back to Grep/rg when n is large. " +
|
|
143
|
+
"Prefer mode=locations (or count for totals only); use lines when you need matching line text. " +
|
|
144
|
+
"Optional `limit` only if you intentionally want a sample. In benchmark mode, includes compact token savings vs agent Grep.";
|
|
145
|
+
|
|
131
146
|
export const MCP_EXPAND_TOOL_DESCRIPTION =
|
|
132
147
|
"More context in the SAME file as a search hit. Pass `file_path` + `anchor_line` from the hit; " +
|
|
133
148
|
"returns adjacent indexed chunks. Use when `truncated: true` — NOT for similar code in other files (use find_related). " +
|
|
@@ -137,5 +152,9 @@ export const MCP_FIND_RELATED_TOOL_DESCRIPTION =
|
|
|
137
152
|
"Find code similar to a file:line in OTHER parts of the codebase. Results may be snippets; use `expand` when `truncated: true`. " +
|
|
138
153
|
"For more context in the same file, use `expand` instead.";
|
|
139
154
|
|
|
155
|
+
export const MCP_READ_BENCHMARK_TOOL_DESCRIPTION =
|
|
156
|
+
"Cumulative Miru vs Grep token savings from saved `search` and `locate` calls. Returns compact totals {n,saved,save_pct,miru,grep}. " +
|
|
157
|
+
"Do not call unless the user asks about savings.";
|
|
158
|
+
|
|
140
159
|
export const SEARCH_GUARD_EXPAND_HINT =
|
|
141
160
|
"If a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — do not re-search or read the whole file.";
|