@takara-ai/miru-code 1.9.0 → 1.10.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 +43 -48
- package/docs/coverage-badge.json +6 -0
- package/grammars/manifest.json +10 -2
- package/grammars/tree-sitter-astro.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-php_only.wasm +0 -0
- package/grammars/tree-sitter-sql.wasm +0 -0
- package/grammars/tree-sitter-svelte.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/package.json +10 -2
- package/src/agents.ts +4 -4
- package/src/auth/device.ts +26 -21
- package/src/auth/providers.ts +26 -11
- package/src/benchmark/compare.ts +3 -10
- package/src/benchmark/grep.ts +64 -37
- package/src/benchmark/locate-compare.ts +8 -1
- package/src/benchmark/rg-literal.ts +9 -4
- package/src/cache.ts +3 -3
- package/src/chunking/ast.ts +21 -3
- package/src/chunking/grammars.ts +8 -3
- package/src/chunking/structural.ts +13 -24
- package/src/cli-mode.ts +18 -0
- package/src/cli.ts +10 -45
- package/src/credentials.ts +8 -13
- package/src/embeddings/openai.ts +6 -3
- package/src/embeddings/sagemaker.ts +18 -3
- package/src/embeddings/validate.ts +10 -7
- package/src/git.ts +6 -2
- package/src/help.ts +5 -4
- package/src/index/bm25.ts +66 -6
- package/src/index/create.ts +5 -9
- package/src/index/files.ts +5 -1
- package/src/index/incremental.ts +48 -8
- package/src/index/sparse.ts +14 -2
- package/src/installer/agents.ts +85 -65
- package/src/installer/benchmark-mode.ts +40 -11
- package/src/installer/config.ts +105 -42
- package/src/installer/installer.ts +59 -34
- package/src/installer/legacy-hooks.ts +131 -0
- package/src/installer/prompt.ts +20 -22
- package/src/installer/search-policy.ts +8 -8
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +42 -22
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +54 -18
- package/src/prompt.ts +51 -14
- package/src/ranking/boosting.ts +5 -6
- package/src/ranking/location.ts +6 -10
- package/src/ranking/penalties.ts +14 -22
- package/src/search.ts +4 -12
- package/src/setup.ts +25 -13
- package/src/snippet.ts +8 -26
- package/src/spinner.ts +26 -16
- package/src/tokenizer/bert-wordpiece.ts +0 -6
- package/src/utils.ts +0 -4
- package/src/version.ts +4 -4
- package/src/installer/hooks/install.ts +0 -549
- package/src/installer/hooks/opencode-plugin.ts +0 -54
- package/src/installer/hooks/search-guard.ts +0 -272
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
replaceOrAppendMarked,
|
|
31
31
|
stripJsonComments,
|
|
32
32
|
} from "./config.ts";
|
|
33
|
-
import {
|
|
33
|
+
import { removeLegacySearchHooks } from "./legacy-hooks.ts";
|
|
34
34
|
import { promptConfirm, promptMultiSelect, requireInteractiveTerminal } from "./prompt.ts";
|
|
35
35
|
import { CURSOR_RULES_MDC } from "./search-policy.ts";
|
|
36
36
|
import {
|
|
@@ -56,7 +56,7 @@ export type IntegrationId =
|
|
|
56
56
|
| "mcp"
|
|
57
57
|
| "instructions"
|
|
58
58
|
| "subagent"
|
|
59
|
-
| "hooks"
|
|
59
|
+
| "legacy-hooks"
|
|
60
60
|
| "rules"
|
|
61
61
|
| "caveman"
|
|
62
62
|
| "ste";
|
|
@@ -72,6 +72,21 @@ export interface ApplyCtx {
|
|
|
72
72
|
isDetected?: (agent: AgentTarget) => Promise<boolean>;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
type InstallerMultiSelect = <T>(
|
|
76
|
+
title: string,
|
|
77
|
+
items: Array<{ label: string; value: T; checked: boolean }>,
|
|
78
|
+
) => Promise<T[] | null>;
|
|
79
|
+
|
|
80
|
+
export interface RunInstallerDependencies {
|
|
81
|
+
requireInteractiveTerminal?: (command: string) => void;
|
|
82
|
+
ensureCredentials?: typeof ensureCredentials;
|
|
83
|
+
agents?: AgentTarget[];
|
|
84
|
+
isAgentDetected?: (agent: AgentTarget) => Promise<boolean>;
|
|
85
|
+
promptMultiSelect?: InstallerMultiSelect;
|
|
86
|
+
promptConfirm?: typeof promptConfirm;
|
|
87
|
+
removeUninstallLocalData?: typeof removeUninstallLocalData;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
interface Integration {
|
|
76
91
|
id: IntegrationId;
|
|
77
92
|
label: string;
|
|
@@ -110,10 +125,12 @@ async function readExistingJsonMcpEntry(
|
|
|
110
125
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
111
126
|
return null;
|
|
112
127
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return null;
|
|
128
|
+
let section: unknown = parsed;
|
|
129
|
+
for (const key of sectionKey.split(".")) {
|
|
130
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) return null;
|
|
131
|
+
section = (section as Record<string, unknown>)[key];
|
|
116
132
|
}
|
|
133
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) return null;
|
|
117
134
|
const entry = (section as Record<string, unknown>)[memberKey];
|
|
118
135
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
119
136
|
return null;
|
|
@@ -161,18 +178,13 @@ async function applyInstructions(
|
|
|
161
178
|
return { path, action };
|
|
162
179
|
}
|
|
163
180
|
|
|
164
|
-
async function
|
|
165
|
-
const path = agent.
|
|
166
|
-
const format = agent.
|
|
181
|
+
async function removeLegacyHooks(agent: AgentTarget): Promise<WriteResult | null> {
|
|
182
|
+
const path = agent.legacyHooksPath;
|
|
183
|
+
const format = agent.legacyHooksFormat;
|
|
167
184
|
if (!path || !format) {
|
|
168
185
|
return null;
|
|
169
186
|
}
|
|
170
|
-
|
|
171
|
-
const action =
|
|
172
|
-
mode === "install"
|
|
173
|
-
? await mergeHooks(format, path, agent.id)
|
|
174
|
-
: await removeHooks(format, path, agent.id);
|
|
175
|
-
|
|
187
|
+
const action = await removeLegacySearchHooks(format, path, agent.id);
|
|
176
188
|
return { path, action };
|
|
177
189
|
}
|
|
178
190
|
|
|
@@ -311,9 +323,6 @@ async function applyCaveman(
|
|
|
311
323
|
|
|
312
324
|
async function stePackMatches(skillDir: string): Promise<boolean> {
|
|
313
325
|
const skillPath = join(skillDir, "SKILL.md");
|
|
314
|
-
if (!(await Bun.file(skillPath).exists())) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
326
|
if ((await Bun.file(skillPath).text()) !== STE_SKILL_MD) {
|
|
318
327
|
return false;
|
|
319
328
|
}
|
|
@@ -449,13 +458,12 @@ const INTEGRATIONS: Integration[] = [
|
|
|
449
458
|
apply: applyCursorRules,
|
|
450
459
|
},
|
|
451
460
|
{
|
|
452
|
-
id: "hooks",
|
|
453
|
-
label: "
|
|
454
|
-
description: "
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
apply: applyHooks,
|
|
461
|
+
id: "legacy-hooks",
|
|
462
|
+
label: "Remove old Miru hooks",
|
|
463
|
+
description: "clean up stale search hooks from older releases",
|
|
464
|
+
defaultChecked: true,
|
|
465
|
+
planPath: (agent) => agent.legacyHooksPath,
|
|
466
|
+
apply: removeLegacyHooks,
|
|
459
467
|
},
|
|
460
468
|
{
|
|
461
469
|
id: "caveman",
|
|
@@ -593,12 +601,25 @@ export async function removeUninstallLocalData(): Promise<{
|
|
|
593
601
|
};
|
|
594
602
|
}
|
|
595
603
|
|
|
596
|
-
export async function runInstaller(
|
|
604
|
+
export async function runInstaller(
|
|
605
|
+
mode: InstallMode,
|
|
606
|
+
dependencies: RunInstallerDependencies = {},
|
|
607
|
+
): Promise<void> {
|
|
608
|
+
const deps = {
|
|
609
|
+
requireInteractiveTerminal,
|
|
610
|
+
ensureCredentials,
|
|
611
|
+
agents: AGENT_TARGETS,
|
|
612
|
+
isAgentDetected,
|
|
613
|
+
promptMultiSelect,
|
|
614
|
+
promptConfirm,
|
|
615
|
+
removeUninstallLocalData,
|
|
616
|
+
...dependencies,
|
|
617
|
+
};
|
|
597
618
|
const install = mode === "install";
|
|
598
|
-
requireInteractiveTerminal(`miru ${mode}`);
|
|
619
|
+
deps.requireInteractiveTerminal(`miru ${mode}`);
|
|
599
620
|
|
|
600
621
|
if (install) {
|
|
601
|
-
await ensureCredentials({ interactive: true });
|
|
622
|
+
await deps.ensureCredentials({ interactive: true });
|
|
602
623
|
}
|
|
603
624
|
|
|
604
625
|
writeStdout("");
|
|
@@ -607,9 +628,9 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
607
628
|
hint("↑↓ move space select enter confirm");
|
|
608
629
|
|
|
609
630
|
const detected = await Promise.all(
|
|
610
|
-
|
|
631
|
+
deps.agents.map(async (agent) => ({
|
|
611
632
|
agent,
|
|
612
|
-
detected: await isAgentDetected(agent),
|
|
633
|
+
detected: await deps.isAgentDetected(agent),
|
|
613
634
|
})),
|
|
614
635
|
);
|
|
615
636
|
|
|
@@ -619,7 +640,7 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
619
640
|
checked: isDetected && install,
|
|
620
641
|
}));
|
|
621
642
|
|
|
622
|
-
const chosenAgents = await promptMultiSelect(
|
|
643
|
+
const chosenAgents = await deps.promptMultiSelect(
|
|
623
644
|
`Agents to ${install ? "configure" : "clean up"}`,
|
|
624
645
|
agentItems,
|
|
625
646
|
);
|
|
@@ -641,7 +662,7 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
641
662
|
checked: install ? (integration.defaultChecked ?? true) : true,
|
|
642
663
|
}));
|
|
643
664
|
|
|
644
|
-
const chosenIntegrations = await promptMultiSelect(
|
|
665
|
+
const chosenIntegrations = await deps.promptMultiSelect(
|
|
645
666
|
`Integrations to ${install ? "enable" : "remove"}`,
|
|
646
667
|
integrationItems,
|
|
647
668
|
);
|
|
@@ -653,7 +674,10 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
653
674
|
|
|
654
675
|
await printPlan(mode, chosenAgents, chosenIntegrations);
|
|
655
676
|
|
|
656
|
-
const proceed = await promptConfirm(
|
|
677
|
+
const proceed = await deps.promptConfirm(
|
|
678
|
+
install ? "Proceed?" : "Remove miru configuration?",
|
|
679
|
+
install,
|
|
680
|
+
);
|
|
657
681
|
if (!proceed) {
|
|
658
682
|
hint("Cancelled.");
|
|
659
683
|
return;
|
|
@@ -662,7 +686,7 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
662
686
|
await apply(mode, chosenAgents, chosenIntegrations);
|
|
663
687
|
|
|
664
688
|
if (!install) {
|
|
665
|
-
const local = await removeUninstallLocalData();
|
|
689
|
+
const local = await deps.removeUninstallLocalData();
|
|
666
690
|
if (local.benchmarkHistoryCleared) {
|
|
667
691
|
info(`Removed benchmark report ${local.benchmarkHistoryPath}`);
|
|
668
692
|
}
|
|
@@ -674,7 +698,6 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
674
698
|
|
|
675
699
|
export {
|
|
676
700
|
applyCaveman,
|
|
677
|
-
applyHooks,
|
|
678
701
|
applyInstructions,
|
|
679
702
|
applyMcp,
|
|
680
703
|
applySte,
|
|
@@ -684,3 +707,5 @@ export {
|
|
|
684
707
|
mergeJsonMember as mergeMcpJson,
|
|
685
708
|
removeJsonMember as removeMcpJson,
|
|
686
709
|
};
|
|
710
|
+
|
|
711
|
+
export const installerTestUtils = { applyCursorRules, printPlan };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
2
|
+
import type { InstallAction, LegacyHooksFormat } from "./agents.ts";
|
|
3
|
+
import { stripJsonComments } from "./config.ts";
|
|
4
|
+
import { isCopilotFamilyId } from "./copilot-family.ts";
|
|
5
|
+
|
|
6
|
+
const LEGACY_MARKERS = ["hook-guard", "miru-search"];
|
|
7
|
+
const VSCODE_OWNERS_KEY = "miruOwners";
|
|
8
|
+
|
|
9
|
+
function isLegacyCommand(value: unknown): boolean {
|
|
10
|
+
return typeof value === "string" && LEGACY_MARKERS.some((marker) => value.includes(marker));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseObject(text: string): Record<string, unknown> | null {
|
|
14
|
+
try {
|
|
15
|
+
const value: unknown = JSON.parse(stripJsonComments(text));
|
|
16
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
17
|
+
? (value as Record<string, unknown>)
|
|
18
|
+
: null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function object(value: unknown): Record<string, unknown> | null {
|
|
25
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
26
|
+
? (value as Record<string, unknown>)
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasOnlyMiruConfig(data: Record<string, unknown>): boolean {
|
|
31
|
+
const keys = Object.keys(data);
|
|
32
|
+
if (keys.length === 0 || keys.every((key) => key === "version")) return true;
|
|
33
|
+
const hooks = object(data.hooks);
|
|
34
|
+
return (
|
|
35
|
+
!!hooks &&
|
|
36
|
+
Object.keys(data).every((key) => key === "version" || key === "hooks") &&
|
|
37
|
+
Object.keys(hooks).length === 0
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function writeResult(path: string, data: Record<string, unknown>): Promise<void> {
|
|
42
|
+
if (hasOnlyMiruConfig(data)) {
|
|
43
|
+
await unlink(path);
|
|
44
|
+
} else {
|
|
45
|
+
await Bun.write(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isMiruEntry(format: LegacyHooksFormat, entry: Record<string, unknown>): boolean {
|
|
50
|
+
if (format === "claude") {
|
|
51
|
+
const nested = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
52
|
+
return nested.some((hook) => isLegacyCommand(object(hook)?.command));
|
|
53
|
+
}
|
|
54
|
+
return isLegacyCommand(entry.command);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Remove search hooks installed by older Miru releases; Miru no longer installs hooks. */
|
|
58
|
+
export async function removeLegacySearchHooks(
|
|
59
|
+
format: LegacyHooksFormat,
|
|
60
|
+
path: string,
|
|
61
|
+
owner?: string,
|
|
62
|
+
): Promise<InstallAction> {
|
|
63
|
+
if (!(await Bun.file(path).exists())) return "not-found";
|
|
64
|
+
|
|
65
|
+
if (format === "opencode") {
|
|
66
|
+
await unlink(path);
|
|
67
|
+
return "removed";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const text = await Bun.file(path).text();
|
|
71
|
+
const data = parseObject(text);
|
|
72
|
+
if (!data) return "error";
|
|
73
|
+
|
|
74
|
+
const hooks = object(data.hooks);
|
|
75
|
+
|
|
76
|
+
let ownerChanged = false;
|
|
77
|
+
if (format === "vscode") {
|
|
78
|
+
const owners = Array.isArray(data[VSCODE_OWNERS_KEY])
|
|
79
|
+
? (data[VSCODE_OWNERS_KEY] as unknown[]).filter(
|
|
80
|
+
(value): value is string => typeof value === "string" && isCopilotFamilyId(value),
|
|
81
|
+
)
|
|
82
|
+
: null;
|
|
83
|
+
if (owners && owner) {
|
|
84
|
+
const remaining = owners.filter((value) => value !== owner);
|
|
85
|
+
if (remaining.length > 0) {
|
|
86
|
+
data[VSCODE_OWNERS_KEY] = remaining;
|
|
87
|
+
await Bun.write(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
88
|
+
return "updated";
|
|
89
|
+
}
|
|
90
|
+
delete data[VSCODE_OWNERS_KEY];
|
|
91
|
+
ownerChanged = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hooks) {
|
|
96
|
+
if (!ownerChanged) return "not-found";
|
|
97
|
+
await writeResult(path, data);
|
|
98
|
+
return "removed";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const eventKey = {
|
|
102
|
+
claude: "PreToolUse",
|
|
103
|
+
cursor: "preToolUse",
|
|
104
|
+
gemini: "BeforeTool",
|
|
105
|
+
vscode: "PreToolUse",
|
|
106
|
+
kiro: "preToolUse",
|
|
107
|
+
windsurf: "pre_run_command",
|
|
108
|
+
opencode: "",
|
|
109
|
+
}[format];
|
|
110
|
+
const entries = Array.isArray(hooks[eventKey]) ? (hooks[eventKey] as unknown[]) : null;
|
|
111
|
+
if (!entries) {
|
|
112
|
+
if (!ownerChanged) return "not-found";
|
|
113
|
+
await writeResult(path, data);
|
|
114
|
+
return "removed";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const retained = entries.filter((entry) => {
|
|
118
|
+
const record = object(entry);
|
|
119
|
+
return !record || !isMiruEntry(format, record);
|
|
120
|
+
});
|
|
121
|
+
if (retained.length === entries.length) {
|
|
122
|
+
if (!ownerChanged) return "not-found";
|
|
123
|
+
await writeResult(path, data);
|
|
124
|
+
return "removed";
|
|
125
|
+
}
|
|
126
|
+
if (retained.length === 0) delete hooks[eventKey];
|
|
127
|
+
else hooks[eventKey] = retained;
|
|
128
|
+
if (Object.keys(hooks).length === 0) delete data.hooks;
|
|
129
|
+
await writeResult(path, data);
|
|
130
|
+
return "removed";
|
|
131
|
+
}
|
package/src/installer/prompt.ts
CHANGED
|
@@ -113,18 +113,9 @@ function parseArrowKey(chunk: Buffer, text: string): string | null {
|
|
|
113
113
|
const introducer = text.charAt(1);
|
|
114
114
|
if (introducer === "[" || introducer === "O") {
|
|
115
115
|
const arrow = text.charAt(text.length - 1);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (arrow === "B") {
|
|
120
|
-
return "down";
|
|
121
|
-
}
|
|
122
|
-
if (arrow === "C") {
|
|
123
|
-
return "right";
|
|
124
|
-
}
|
|
125
|
-
if (arrow === "D") {
|
|
126
|
-
return "left";
|
|
127
|
-
}
|
|
116
|
+
return (
|
|
117
|
+
({ A: "up", B: "down", C: "right", D: "left" } as Record<string, string>)[arrow] ?? null
|
|
118
|
+
);
|
|
128
119
|
}
|
|
129
120
|
}
|
|
130
121
|
|
|
@@ -308,9 +299,13 @@ function updateConfirmPromptLine(question: string, yesSelected: boolean): void {
|
|
|
308
299
|
);
|
|
309
300
|
}
|
|
310
301
|
|
|
311
|
-
export async function promptConfirm(
|
|
302
|
+
export async function promptConfirm(
|
|
303
|
+
question: string,
|
|
304
|
+
defaultYes = true,
|
|
305
|
+
io: { input?: typeof input; output?: typeof output } = {},
|
|
306
|
+
): Promise<boolean> {
|
|
312
307
|
if (!input.isTTY) {
|
|
313
|
-
const rl = readline.createInterface({ input, output });
|
|
308
|
+
const rl = readline.createInterface({ input: io.input ?? input, output: io.output ?? output });
|
|
314
309
|
try {
|
|
315
310
|
const hintText = defaultYes ? dim("[Y/n]") : dim("[y/N]");
|
|
316
311
|
const answer = (await rl.question(`${question} ${hintText} `)).trim().toLowerCase();
|
|
@@ -354,10 +349,13 @@ export async function promptConfirm(question: string, defaultYes = true): Promis
|
|
|
354
349
|
export async function promptMultiSelectLegacy<T>(
|
|
355
350
|
title: string,
|
|
356
351
|
items: Array<{ label: string; value: T; checked: boolean }>,
|
|
352
|
+
io: { input?: typeof input; output?: typeof output } = {},
|
|
357
353
|
): Promise<T[] | null> {
|
|
358
|
-
|
|
359
|
-
output
|
|
360
|
-
|
|
354
|
+
const promptInput = io.input ?? input;
|
|
355
|
+
const promptOutput = io.output ?? output;
|
|
356
|
+
promptOutput.write("\n");
|
|
357
|
+
promptOutput.write(`${bold(title)}\n`);
|
|
358
|
+
divider("─", 48, promptOutput);
|
|
361
359
|
|
|
362
360
|
for (let i = 0; i < items.length; i++) {
|
|
363
361
|
const item = items[i];
|
|
@@ -365,13 +363,13 @@ export async function promptMultiSelectLegacy<T>(
|
|
|
365
363
|
continue;
|
|
366
364
|
}
|
|
367
365
|
const mark = item.checked ? cyan("[x]") : dim("[ ]");
|
|
368
|
-
|
|
366
|
+
promptOutput.write(` ${dim(String(i + 1).padStart(2))}. ${mark} ${item.label}\n`);
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
|
|
372
|
-
hint("Enter numbers (1,3), 'all', or press Enter for defaults",
|
|
369
|
+
promptOutput.write("\n");
|
|
370
|
+
hint("Enter numbers (1,3), 'all', or press Enter for defaults", promptOutput);
|
|
373
371
|
|
|
374
|
-
const rl = readline.createInterface({ input, output });
|
|
372
|
+
const rl = readline.createInterface({ input: promptInput, output: promptOutput });
|
|
375
373
|
try {
|
|
376
374
|
const answer = (await rl.question(`${dim("> ")}`)).toLowerCase();
|
|
377
375
|
if (!answer) {
|
|
@@ -390,7 +388,7 @@ export async function promptMultiSelectLegacy<T>(
|
|
|
390
388
|
}
|
|
391
389
|
|
|
392
390
|
if (indices.size === 0) {
|
|
393
|
-
warn("No valid selection.",
|
|
391
|
+
warn("No valid selection.", promptOutput);
|
|
394
392
|
return null;
|
|
395
393
|
}
|
|
396
394
|
|
|
@@ -28,15 +28,17 @@ export function buildSearchPolicyBody(native: NativeToolNames): string {
|
|
|
28
28
|
|
|
29
29
|
${SNIPPET_GUIDANCE}
|
|
30
30
|
|
|
31
|
+
\`repo\` is optional, set it for another repo.
|
|
32
|
+
|
|
31
33
|
Use Miru MCP tools:
|
|
32
34
|
- \`locate\` — exact substring (env var, symbol, error code); prefer \`mode=count\` or \`locations\`
|
|
33
|
-
- \`search\` — one call per conceptual question;
|
|
35
|
+
- \`search\` — one call per conceptual question; use \`include\` / \`exclude\` Gitignore-style globs to scope files (same syntax as \`locate\`)
|
|
34
36
|
- \`expand\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\`)
|
|
35
37
|
- \`find_related\` — similar code in other files (hits may also be snippets; use \`expand\` if truncated)
|
|
36
38
|
|
|
37
39
|
Call shapes:
|
|
38
|
-
- \`locate(literal="<exact substring>"
|
|
39
|
-
- \`search(query="<natural language>",
|
|
40
|
+
- \`locate(literal="<exact substring>")\` — prefer \`mode="locations"\`/\`"count"\`
|
|
41
|
+
- \`search(query="<natural language>", include=["src/auth/**"])\` — use the same \`include\` / \`exclude\` globs on \`locate\` when scoping an exact-string search
|
|
40
42
|
|
|
41
43
|
Stop rules:
|
|
42
44
|
- Literal in the request → \`locate\`, not \`search\`
|
|
@@ -54,7 +56,7 @@ ${buildSearchPolicyTable(native)}`;
|
|
|
54
56
|
export function buildMcpWorkflow(native: NativeToolNames): string {
|
|
55
57
|
return `### MCP workflow
|
|
56
58
|
|
|
57
|
-
1. Call \`search
|
|
59
|
+
1. Call \`search\`; add \`repo\` only for another repo.
|
|
58
60
|
2. For exact literals, call \`locate\` (prefer \`mode=count\` or \`locations\`).
|
|
59
61
|
3. On \`truncated: true\`, call \`expand\` — only if the snippet doesn't already answer.
|
|
60
62
|
4. Use \`find_related\` to trace similar code in other files — not for more context in the same file.
|
|
@@ -71,7 +73,7 @@ miru find-related src/auth.ts 42 .
|
|
|
71
73
|
\`\`\`
|
|
72
74
|
|
|
73
75
|
After large refactors or CLI-only use, run \`miru clear <path>\` to rebuild the disk cache.
|
|
74
|
-
If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;
|
|
76
|
+
If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code@latest\`.`;
|
|
75
77
|
|
|
76
78
|
export function buildSubagentBody(native: NativeToolNames): string {
|
|
77
79
|
return `When Miru MCP is available, use MCP \`search\`, \`locate\`, \`expand\`, and \`find_related\` — not ${native.explorationDenied} for exploration.
|
|
@@ -118,7 +120,7 @@ export const MCP_SERVER_INSTRUCTIONS =
|
|
|
118
120
|
"Exact token (identifier, quoted string, env var, error code) → `locate`; everything else → `search` once, no paraphrase retries. " +
|
|
119
121
|
`${SNIPPET_GUIDANCE} ` +
|
|
120
122
|
"`find_related` traces similar code elsewhere, not more context in the same file. " +
|
|
121
|
-
"
|
|
123
|
+
"`repo` is optional, good for multi repo workflows. " +
|
|
122
124
|
"Native Grep/Glob only outside the indexed repo or for non-code tasks. " +
|
|
123
125
|
"Default indexed scope is code, config, and docs; an empty result can mean out-of-scope, not nonexistent. " +
|
|
124
126
|
"On credential errors, call this server's `auth` tool — not the host `mcp_auth` tool.";
|
|
@@ -161,5 +163,3 @@ export const MCP_FIND_RELATED_TOOL_DESCRIPTION =
|
|
|
161
163
|
export const MCP_READ_BENCHMARK_TOOL_DESCRIPTION =
|
|
162
164
|
"Cumulative Miru vs Grep token savings from saved `search` and `locate` calls. Returns compact totals {n,saved,save_pct,miru,grep}. " +
|
|
163
165
|
"Do not call unless the user asks about savings.";
|
|
164
|
-
|
|
165
|
-
export const SEARCH_GUARD_EXPAND_HINT = SNIPPET_GUIDANCE;
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -49,6 +49,11 @@ type CacheEntry = {
|
|
|
49
49
|
lastError: Error | null;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
export interface IndexCacheDependencies {
|
|
53
|
+
fromSource?: typeof MiruIndex.fromSource;
|
|
54
|
+
watch?: typeof watch;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
export function mcpWatchEnabled(): boolean {
|
|
53
58
|
const raw = process.env.MIRU_MCP_WATCH;
|
|
54
59
|
return raw !== "0" && raw !== "false";
|
|
@@ -76,12 +81,20 @@ export function shouldIgnoreWatchPath(relativePath: string | null | undefined):
|
|
|
76
81
|
export class IndexCache {
|
|
77
82
|
private readonly content: ContentType[];
|
|
78
83
|
private readonly defaultRef: string | null;
|
|
84
|
+
private readonly fromSource: typeof MiruIndex.fromSource;
|
|
85
|
+
private readonly watchSource: typeof watch;
|
|
79
86
|
private readonly entries = new Map<string, CacheEntry>();
|
|
80
87
|
readonly watchers = new Map<string, WatcherHandle>();
|
|
81
88
|
|
|
82
|
-
constructor(
|
|
89
|
+
constructor(
|
|
90
|
+
content: ContentType[] = defaultContentTypes(),
|
|
91
|
+
defaultRef: string | null = null,
|
|
92
|
+
dependencies: IndexCacheDependencies = {},
|
|
93
|
+
) {
|
|
83
94
|
this.content = content;
|
|
84
95
|
this.defaultRef = defaultRef;
|
|
96
|
+
this.fromSource = dependencies.fromSource ?? MiruIndex.fromSource;
|
|
97
|
+
this.watchSource = dependencies.watch ?? watch;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
private ensureEntry(cacheKey: string, source: string): CacheEntry {
|
|
@@ -128,7 +141,7 @@ export class IndexCache {
|
|
|
128
141
|
): Promise<MiruIndex> {
|
|
129
142
|
const entry = this.ensureEntry(cacheKey, source);
|
|
130
143
|
const task = (async () => {
|
|
131
|
-
const index = await
|
|
144
|
+
const index = await this.fromSource(source, this.content, undefined, ref);
|
|
132
145
|
if (!isGitUrl(source)) {
|
|
133
146
|
await index.saveToCache(resolve(source));
|
|
134
147
|
}
|
|
@@ -342,7 +355,7 @@ export class IndexCache {
|
|
|
342
355
|
|
|
343
356
|
let nativeWatcher: ReturnType<typeof watch> | null = null;
|
|
344
357
|
try {
|
|
345
|
-
nativeWatcher =
|
|
358
|
+
nativeWatcher = this.watchSource(resolved, { recursive: true }, (_event, filename) => {
|
|
346
359
|
this.noteFileChange(path, filename);
|
|
347
360
|
});
|
|
348
361
|
} catch {
|
package/src/mcp/server.ts
CHANGED
|
@@ -44,8 +44,7 @@ import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
|
|
|
44
44
|
import { MiruMcpServer } from "./runtime.ts";
|
|
45
45
|
|
|
46
46
|
const REPO_DESCRIPTION =
|
|
47
|
-
"
|
|
48
|
-
"Pass the project root for local workspaces. " +
|
|
47
|
+
"`repo` is optional. Set a local path or Git URL for another repo. " +
|
|
49
48
|
"The index is built on the first tool call and cached for the session.";
|
|
50
49
|
|
|
51
50
|
const BENCHMARK_SKIP_NOTES = {
|
|
@@ -121,9 +120,26 @@ async function persistBenchmarkQuery(
|
|
|
121
120
|
|
|
122
121
|
export function createMcpServer(
|
|
123
122
|
cache: IndexCache,
|
|
124
|
-
options?: {
|
|
123
|
+
options?: {
|
|
124
|
+
benchmark?: boolean;
|
|
125
|
+
dependencies?: {
|
|
126
|
+
withGrepTimeoutFallback?: typeof withGrepTimeoutFallback;
|
|
127
|
+
benchmarkSearchComparison?: typeof benchmarkSearchComparison;
|
|
128
|
+
benchmarkLocateComparison?: typeof benchmarkLocateComparison;
|
|
129
|
+
selectComparableLiteralSearchTool?: typeof selectComparableLiteralSearchTool;
|
|
130
|
+
readBenchmarkRollup?: typeof readBenchmarkRollup;
|
|
131
|
+
};
|
|
132
|
+
},
|
|
125
133
|
): MiruMcpServer {
|
|
134
|
+
const defaultRepo = process.cwd();
|
|
126
135
|
const benchmark = options?.benchmark ?? false;
|
|
136
|
+
const dependencies = options?.dependencies;
|
|
137
|
+
const runWithGrepFallback = dependencies?.withGrepTimeoutFallback ?? withGrepTimeoutFallback;
|
|
138
|
+
const compareSearch = dependencies?.benchmarkSearchComparison ?? benchmarkSearchComparison;
|
|
139
|
+
const compareLocate = dependencies?.benchmarkLocateComparison ?? benchmarkLocateComparison;
|
|
140
|
+
const selectLiteralBaseline =
|
|
141
|
+
dependencies?.selectComparableLiteralSearchTool ?? selectComparableLiteralSearchTool;
|
|
142
|
+
const readRollup = dependencies?.readBenchmarkRollup ?? readBenchmarkRollup;
|
|
127
143
|
const server = new MiruMcpServer(
|
|
128
144
|
{
|
|
129
145
|
name: "miru",
|
|
@@ -139,14 +155,14 @@ export function createMcpServer(
|
|
|
139
155
|
server.registerTool(
|
|
140
156
|
"search",
|
|
141
157
|
{
|
|
142
|
-
description: `${MCP_SEARCH_TOOL_DESCRIPTION} Indexes
|
|
158
|
+
description: `${MCP_SEARCH_TOOL_DESCRIPTION} Indexes the selected directory or URL on first call; later calls reuse the session cache.`,
|
|
143
159
|
inputSchema: {
|
|
144
160
|
query: z
|
|
145
161
|
.string()
|
|
146
162
|
.describe(
|
|
147
163
|
"Natural language or code query — your default for all code search in this repo.",
|
|
148
164
|
),
|
|
149
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
165
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
150
166
|
include: z
|
|
151
167
|
.array(z.string().min(1))
|
|
152
168
|
.min(1)
|
|
@@ -169,15 +185,16 @@ export function createMcpServer(
|
|
|
169
185
|
},
|
|
170
186
|
async ({ query, repo, include, exclude, dedupe_by_file: dedupeByFile }) => {
|
|
171
187
|
try {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
188
|
+
const source = repo ?? defaultRepo;
|
|
189
|
+
const index = await getIndexForRepo(source, cache);
|
|
190
|
+
const repoRoot = localRepoRoot(source);
|
|
174
191
|
const k = SEARCH_RESULT_COUNT;
|
|
175
192
|
let skip: BenchmarkSkipReason | undefined;
|
|
176
193
|
if (benchmark && (include || exclude)) {
|
|
177
194
|
skip = "filtered_search";
|
|
178
195
|
} else if (benchmark && repoRoot) {
|
|
179
|
-
const comparison = await
|
|
180
|
-
|
|
196
|
+
const comparison = await runWithGrepFallback(() =>
|
|
197
|
+
compareSearch({
|
|
181
198
|
query,
|
|
182
199
|
repoPath: repoRoot,
|
|
183
200
|
index,
|
|
@@ -245,7 +262,7 @@ export function createMcpServer(
|
|
|
245
262
|
.describe(
|
|
246
263
|
"Alias for `literal` — accepted so a call shaped like `search(query=...)` still resolves as an exact-substring lookup.",
|
|
247
264
|
),
|
|
248
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
265
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
249
266
|
mode: z
|
|
250
267
|
.enum(["count", "locations", "lines"])
|
|
251
268
|
.optional()
|
|
@@ -291,19 +308,20 @@ export function createMcpServer(
|
|
|
291
308
|
return toolErrorText(new Error("locate requires `literal` (exact substring to find)."));
|
|
292
309
|
}
|
|
293
310
|
try {
|
|
294
|
-
const
|
|
311
|
+
const source = repo ?? defaultRepo;
|
|
312
|
+
const index = await getIndexForRepo(source, cache);
|
|
295
313
|
|
|
296
314
|
let skip: BenchmarkSkipReason | undefined;
|
|
297
|
-
const repoRoot = localRepoRoot(
|
|
315
|
+
const repoRoot = localRepoRoot(source);
|
|
298
316
|
if (benchmark && repoRoot && typeof lit === "string") {
|
|
299
317
|
if (locateOpts.limit != null) {
|
|
300
318
|
skip = "limited_locate";
|
|
301
|
-
} else if (!
|
|
319
|
+
} else if (!selectLiteralBaseline()) {
|
|
302
320
|
skip = "incompatible_literal_baseline";
|
|
303
321
|
}
|
|
304
322
|
if (!skip) {
|
|
305
|
-
const comparison = await
|
|
306
|
-
|
|
323
|
+
const comparison = await runWithGrepFallback(() =>
|
|
324
|
+
compareLocate({
|
|
307
325
|
literal: lit,
|
|
308
326
|
repoPath: repoRoot,
|
|
309
327
|
index,
|
|
@@ -343,7 +361,7 @@ export function createMcpServer(
|
|
|
343
361
|
.number()
|
|
344
362
|
.int()
|
|
345
363
|
.describe("Line from the search hit (`anchor_line` when truncated, else `start_line`)."),
|
|
346
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
364
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
347
365
|
before: z
|
|
348
366
|
.number()
|
|
349
367
|
.int()
|
|
@@ -360,8 +378,9 @@ export function createMcpServer(
|
|
|
360
378
|
},
|
|
361
379
|
async ({ file_path: filePath, anchor_line: anchorLine, repo, before, after }) => {
|
|
362
380
|
try {
|
|
363
|
-
const
|
|
364
|
-
const
|
|
381
|
+
const source = repo ?? defaultRepo;
|
|
382
|
+
const index = await getIndexForRepo(source, cache);
|
|
383
|
+
const repoRoot = localRepoRoot(source);
|
|
365
384
|
const beforeCount = before ?? DEFAULT_EXPAND_BEFORE;
|
|
366
385
|
const afterCount = after ?? DEFAULT_EXPAND_AFTER;
|
|
367
386
|
const { anchor, chunks: expanded } = expandChunksAtLine(
|
|
@@ -404,13 +423,14 @@ export function createMcpServer(
|
|
|
404
423
|
.number()
|
|
405
424
|
.int()
|
|
406
425
|
.describe("Line from the search hit (`anchor_line` when truncated, else `start_line`)."),
|
|
407
|
-
repo: z.string().describe(REPO_DESCRIPTION),
|
|
426
|
+
repo: z.string().optional().describe(REPO_DESCRIPTION),
|
|
408
427
|
},
|
|
409
428
|
},
|
|
410
429
|
async ({ file_path: filePath, anchor_line: anchorLine, repo }) => {
|
|
411
430
|
try {
|
|
412
|
-
const
|
|
413
|
-
const
|
|
431
|
+
const source = repo ?? defaultRepo;
|
|
432
|
+
const index = await getIndexForRepo(source, cache);
|
|
433
|
+
const repoRoot = localRepoRoot(source);
|
|
414
434
|
const chunk = resolveChunk(index.chunks, filePath, anchorLine, repoRoot);
|
|
415
435
|
if (!chunk) {
|
|
416
436
|
return toolText(
|
|
@@ -452,7 +472,7 @@ export function createMcpServer(
|
|
|
452
472
|
},
|
|
453
473
|
async ({ repo }) => {
|
|
454
474
|
try {
|
|
455
|
-
const rollup = await
|
|
475
|
+
const rollup = await readRollup({ repo });
|
|
456
476
|
return toolText(JSON.stringify(rollup));
|
|
457
477
|
} catch (err) {
|
|
458
478
|
return toolText(err instanceof Error ? err.message : String(err));
|