@takara-ai/miru-code 0.4.5 → 0.4.9

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.
@@ -0,0 +1,215 @@
1
+ const REDIRECT_MESSAGE =
2
+ "Use Miru MCP `search` (pass project root as `repo`) instead of built-in search tools. " +
3
+ "Grep/Glob/Shell are only for exact literal confirmation or non-code tasks.";
4
+
5
+ const MIRU_TOOL_RE = /miru/i;
6
+
7
+ const GREP_TOOL_RE = /^(grep|grep_search|Grep)$/i;
8
+ const GLOB_TOOL_RE = /^(glob|glob_file_search|Glob)$/i;
9
+ const SEMANTIC_TOOL_RE = /^(SemanticSearch|codebase_search)$/i;
10
+ const SHELL_TOOL_RE = /^(Shell|Bash|execute_bash|shell)$/i;
11
+
12
+ /** Returns true when a grep pattern looks like an exact literal lookup. */
13
+ export function isLiteralGrepPattern(pattern: string): boolean {
14
+ const trimmed = pattern.trim();
15
+ if (!trimmed) {
16
+ return false;
17
+ }
18
+ if (/^[`'"][^`'"]+[`'"]$/.test(trimmed)) {
19
+ return true;
20
+ }
21
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(trimmed)) {
22
+ return true;
23
+ }
24
+ if (/^[a-f0-9-]{36}$/i.test(trimmed)) {
25
+ return true;
26
+ }
27
+ if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed) && trimmed.length <= 48) {
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ /** Shell commands that look like codebase exploration rather than builds/tests. */
34
+ export function isExplorationShell(command: string): boolean {
35
+ const cmd = command.trim();
36
+ if (!cmd) {
37
+ return false;
38
+ }
39
+ if (
40
+ /\b(git|npm|bun|pnpm|yarn|cargo|go|make|cmake|docker|kubectl|pytest|jest|vitest)\b/.test(cmd)
41
+ ) {
42
+ return false;
43
+ }
44
+ return /\b(rg|ripgrep|grep|find|ag|ack|fd|locate)\b/.test(cmd);
45
+ }
46
+
47
+ export type HookPayload = {
48
+ tool_name?: string;
49
+ tool_input?: Record<string, unknown>;
50
+ hook_event_name?: string;
51
+ agent_action_name?: string;
52
+ tool_info?: Record<string, unknown>;
53
+ };
54
+
55
+ export type GuardDecision = {
56
+ block: boolean;
57
+ reason: string;
58
+ };
59
+
60
+ export type HookResponseFormat = "claude" | "gemini" | "cursor" | "stderr";
61
+
62
+ function grepPatternFromInput(toolInput: Record<string, unknown>): string {
63
+ return String(
64
+ toolInput.pattern ??
65
+ toolInput.query ??
66
+ toolInput.regex ??
67
+ toolInput.needle ??
68
+ toolInput.search_term ??
69
+ "",
70
+ );
71
+ }
72
+
73
+ /** Map host-specific stdin JSON into a common evaluation shape. */
74
+ export function normalizeHookPayload(raw: Record<string, unknown>): HookPayload {
75
+ const agentAction = raw.agent_action_name;
76
+ if (typeof agentAction === "string") {
77
+ const toolInfo =
78
+ raw.tool_info && typeof raw.tool_info === "object" && !Array.isArray(raw.tool_info)
79
+ ? (raw.tool_info as Record<string, unknown>)
80
+ : {};
81
+ if (agentAction === "pre_run_command") {
82
+ return {
83
+ hook_event_name: "windsurf_pre_run_command",
84
+ tool_name: "Shell",
85
+ tool_input: { command: toolInfo.command_line ?? "" },
86
+ };
87
+ }
88
+ }
89
+
90
+ return {
91
+ tool_name: typeof raw.tool_name === "string" ? raw.tool_name : undefined,
92
+ tool_input:
93
+ raw.tool_input && typeof raw.tool_input === "object" && !Array.isArray(raw.tool_input)
94
+ ? (raw.tool_input as Record<string, unknown>)
95
+ : {},
96
+ hook_event_name: typeof raw.hook_event_name === "string" ? raw.hook_event_name : undefined,
97
+ agent_action_name:
98
+ typeof raw.agent_action_name === "string" ? raw.agent_action_name : undefined,
99
+ tool_info:
100
+ raw.tool_info && typeof raw.tool_info === "object" && !Array.isArray(raw.tool_info)
101
+ ? (raw.tool_info as Record<string, unknown>)
102
+ : undefined,
103
+ };
104
+ }
105
+
106
+ export function evaluateSearchGuard(payload: HookPayload): GuardDecision {
107
+ const toolName = String(payload.tool_name ?? "");
108
+ const toolInput = payload.tool_input ?? {};
109
+
110
+ if (!toolName || MIRU_TOOL_RE.test(toolName)) {
111
+ return { block: false, reason: "" };
112
+ }
113
+
114
+ if (GREP_TOOL_RE.test(toolName)) {
115
+ const pattern = grepPatternFromInput(toolInput);
116
+ if (isLiteralGrepPattern(pattern)) {
117
+ return { block: false, reason: "" };
118
+ }
119
+ return { block: true, reason: REDIRECT_MESSAGE };
120
+ }
121
+
122
+ if (GLOB_TOOL_RE.test(toolName)) {
123
+ return { block: true, reason: REDIRECT_MESSAGE };
124
+ }
125
+
126
+ if (SEMANTIC_TOOL_RE.test(toolName)) {
127
+ return { block: true, reason: REDIRECT_MESSAGE };
128
+ }
129
+
130
+ if (SHELL_TOOL_RE.test(toolName)) {
131
+ const command = String(toolInput.command ?? toolInput.cmd ?? "");
132
+ if (isExplorationShell(command)) {
133
+ return { block: true, reason: REDIRECT_MESSAGE };
134
+ }
135
+ return { block: false, reason: "" };
136
+ }
137
+
138
+ return { block: false, reason: "" };
139
+ }
140
+
141
+ export function hookResponseFormat(payload: HookPayload): HookResponseFormat {
142
+ const event = payload.hook_event_name ?? "";
143
+ if (event === "PreToolUse") {
144
+ return "claude";
145
+ }
146
+ if (event === "BeforeTool") {
147
+ return "gemini";
148
+ }
149
+ if (event === "preToolUse" || event === "windsurf_pre_run_command") {
150
+ return "stderr";
151
+ }
152
+ return "cursor";
153
+ }
154
+
155
+ export function claudeHookResponse(reason: string): string {
156
+ return JSON.stringify({
157
+ hookSpecificOutput: {
158
+ hookEventName: "PreToolUse",
159
+ permissionDecision: "deny",
160
+ permissionDecisionReason: reason,
161
+ },
162
+ });
163
+ }
164
+
165
+ export function geminiHookResponse(reason: string): string {
166
+ return JSON.stringify({
167
+ decision: "deny",
168
+ reason,
169
+ });
170
+ }
171
+
172
+ export function cursorHookResponse(reason: string): string {
173
+ return JSON.stringify({
174
+ permission: "deny",
175
+ agent_message: reason,
176
+ user_message: "Miru: use MCP search instead of built-in grep/glob.",
177
+ });
178
+ }
179
+
180
+ export async function runSearchGuardFromStdin(): Promise<number> {
181
+ const text = await Bun.stdin.text();
182
+ if (!text.trim()) {
183
+ return 0;
184
+ }
185
+
186
+ let raw: Record<string, unknown>;
187
+ try {
188
+ raw = JSON.parse(text) as Record<string, unknown>;
189
+ } catch {
190
+ return 0;
191
+ }
192
+
193
+ const payload = normalizeHookPayload(raw);
194
+ const decision = evaluateSearchGuard(payload);
195
+ if (!decision.block) {
196
+ return 0;
197
+ }
198
+
199
+ const format = hookResponseFormat(payload);
200
+ if (format === "claude") {
201
+ process.stdout.write(claudeHookResponse(decision.reason));
202
+ return 0;
203
+ }
204
+ if (format === "gemini") {
205
+ process.stdout.write(geminiHookResponse(decision.reason));
206
+ return 0;
207
+ }
208
+ if (format === "stderr") {
209
+ process.stderr.write(decision.reason);
210
+ return 2;
211
+ }
212
+
213
+ process.stdout.write(cursorHookResponse(decision.reason));
214
+ return 2;
215
+ }
@@ -19,14 +19,16 @@ import {
19
19
  removeTomlBlock,
20
20
  replaceOrAppendMarked,
21
21
  } from "./config.ts";
22
+ import { mergeHooks, removeHooks } from "./hooks/install.ts";
22
23
  import { promptConfirm, promptMultiSelect, requireInteractiveTerminal } from "./prompt.ts";
24
+ import { CURSOR_RULES_MDC } from "./search-policy.ts";
23
25
 
24
26
  export interface WriteResult {
25
27
  path: string;
26
28
  action: InstallAction;
27
29
  }
28
30
 
29
- export type IntegrationId = "mcp" | "instructions" | "subagent";
31
+ export type IntegrationId = "mcp" | "instructions" | "subagent" | "hooks" | "rules";
30
32
 
31
33
  interface Integration {
32
34
  id: IntegrationId;
@@ -84,6 +86,42 @@ async function applyInstructions(
84
86
  return { path, action };
85
87
  }
86
88
 
89
+ async function applyHooks(agent: AgentTarget, mode: InstallMode): Promise<WriteResult | null> {
90
+ const path = agent.hooksPath;
91
+ const format = agent.hooksFormat;
92
+ if (!path || !format) {
93
+ return null;
94
+ }
95
+
96
+ const action =
97
+ mode === "install" ? await mergeHooks(format, path) : await removeHooks(format, path);
98
+
99
+ return { path, action };
100
+ }
101
+
102
+ async function applyCursorRules(
103
+ agent: AgentTarget,
104
+ mode: InstallMode,
105
+ ): Promise<WriteResult | null> {
106
+ const path = agent.cursorRulesPath;
107
+ if (!path) {
108
+ return null;
109
+ }
110
+
111
+ if (mode === "uninstall") {
112
+ if (!(await Bun.file(path).exists())) {
113
+ return { path, action: "not-found" };
114
+ }
115
+ await unlink(path);
116
+ return { path, action: "removed" };
117
+ }
118
+
119
+ const existed = await Bun.file(path).exists();
120
+ await mkdir(dirname(path), { recursive: true });
121
+ await writeFile(path, `${CURSOR_RULES_MDC.trim()}\n`, "utf-8");
122
+ return { path, action: existed ? "updated" : "created" };
123
+ }
124
+
87
125
  async function applySubagent(agent: AgentTarget, mode: InstallMode): Promise<WriteResult | null> {
88
126
  const dest = agent.subagentPath;
89
127
  const templateId = agent.subagentId;
@@ -132,6 +170,20 @@ const INTEGRATIONS: Integration[] = [
132
170
  planPath: (agent) => agent.subagentPath,
133
171
  apply: applySubagent,
134
172
  },
173
+ {
174
+ id: "rules",
175
+ label: "Cursor rules",
176
+ description: "always-on .cursor/rules policy for code search",
177
+ planPath: (agent) => agent.cursorRulesPath,
178
+ apply: applyCursorRules,
179
+ },
180
+ {
181
+ id: "hooks",
182
+ label: "Search hooks",
183
+ description: "blocks Grep/Glob and redirects agents to Miru MCP",
184
+ planPath: (agent) => agent.hooksPath,
185
+ apply: applyHooks,
186
+ },
135
187
  ];
136
188
 
137
189
  function formatActionLine(integration: Integration, result: WriteResult): string {
@@ -191,7 +243,7 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
191
243
  writeStdout("");
192
244
  writeStdout(`${brandTitle()}${install ? " installer" : " uninstaller"}`);
193
245
  divider();
194
- hint("Select agents and integrations to configure.");
246
+ hint("↑↓ move space select enter confirm");
195
247
 
196
248
  const detected = await Promise.all(
197
249
  AGENT_TARGETS.map(async (agent) => ({
@@ -246,12 +298,13 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
246
298
  install ? "Done! Restart your agents to pick up changes." : "Done! Configuration removed.",
247
299
  );
248
300
  if (install) {
249
- hint("Run miru setup — MCP loads the key from credentials.json automatically.");
301
+ hint("Restart agents after install. Hooks block built-in search in favor of Miru MCP.");
250
302
  }
251
303
  writeStdout("");
252
304
  }
253
305
 
254
306
  export {
307
+ applyHooks,
255
308
  applyInstructions,
256
309
  applyMcp,
257
310
  applySubagent,
@@ -1,6 +1,6 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
2
  import * as readline from "node:readline/promises";
3
- import { bold, cyan, dim, divider, hint, warn } from "../cli-ui.ts";
3
+ import { bold, cyan, dim, divider, green, hint, warn } from "../cli-ui.ts";
4
4
 
5
5
  export function requireInteractiveTerminal(command: string): void {
6
6
  if (!input.isTTY) {
@@ -11,27 +11,208 @@ export function requireInteractiveTerminal(command: string): void {
11
11
  }
12
12
  }
13
13
 
14
- async function readLine(prompt: string): Promise<string> {
15
- const rl = readline.createInterface({ input, output });
14
+ const ESC = "\x1b";
15
+ const HIDE_CURSOR = `${ESC}[?25l`;
16
+ const SHOW_CURSOR = `${ESC}[?25h`;
17
+ const CLEAR_LINE = `${ESC}[2K`;
18
+
19
+ function writeRaw(text: string): void {
20
+ output.write(text);
21
+ }
22
+
23
+ function readKey(): Promise<Buffer> {
24
+ return new Promise((resolve, reject) => {
25
+ const onData = (chunk: Buffer) => {
26
+ cleanup();
27
+ resolve(chunk);
28
+ };
29
+ const onError = (err: Error) => {
30
+ cleanup();
31
+ reject(err);
32
+ };
33
+ const cleanup = () => {
34
+ input.off("data", onData);
35
+ input.off("error", onError);
36
+ };
37
+ input.once("data", onData);
38
+ input.once("error", onError);
39
+ });
40
+ }
41
+
42
+ function parseKey(chunk: Buffer): string {
43
+ const text = chunk.toString("utf8");
44
+ if (chunk.length >= 3 && chunk[0] === 0x1b && chunk[1] === 0x5b) {
45
+ const code = chunk[2];
46
+ if (code === 0x41) {
47
+ return "up";
48
+ }
49
+ if (code === 0x42) {
50
+ return "down";
51
+ }
52
+ if (code === 0x43) {
53
+ return "right";
54
+ }
55
+ if (code === 0x44) {
56
+ return "left";
57
+ }
58
+ }
59
+ if (text === "\r" || text === "\n") {
60
+ return "enter";
61
+ }
62
+ if (text === " ") {
63
+ return "space";
64
+ }
65
+ if (text === "\x03") {
66
+ return "ctrl-c";
67
+ }
68
+ if (text === "a" || text === "A") {
69
+ return "all";
70
+ }
71
+ if (text === "y" || text === "Y") {
72
+ return "yes";
73
+ }
74
+ if (text === "n" || text === "N") {
75
+ return "no";
76
+ }
77
+ return text;
78
+ }
79
+
80
+ async function withRawMode<T>(fn: () => Promise<T>): Promise<T> {
81
+ input.setRawMode(true);
82
+ input.resume();
83
+ writeRaw(HIDE_CURSOR);
16
84
  try {
17
- return (await rl.question(prompt)).trim();
85
+ return await fn();
18
86
  } finally {
19
- rl.close();
87
+ writeRaw(SHOW_CURSOR);
88
+ input.setRawMode(false);
89
+ input.pause();
20
90
  }
21
91
  }
22
92
 
23
- export async function promptConfirm(question: string, defaultYes = true): Promise<boolean> {
24
- const hintText = defaultYes ? dim("[Y/n]") : dim("[y/N]");
25
- const answer = (await readLine(`${question} ${hintText} `)).toLowerCase();
26
- if (!answer) {
27
- return defaultYes;
93
+ function renderMultiSelect<T>(
94
+ title: string,
95
+ items: Array<{ label: string; value: T; checked: boolean }>,
96
+ cursor: number,
97
+ footer: string,
98
+ ): void {
99
+ const lines: string[] = [];
100
+ lines.push("");
101
+ lines.push(bold(title));
102
+ lines.push(dim("─".repeat(48)));
103
+ for (let i = 0; i < items.length; i++) {
104
+ const item = items[i];
105
+ if (!item) {
106
+ continue;
107
+ }
108
+ const selected = i === cursor;
109
+ const pointer = selected ? cyan("›") : " ";
110
+ const mark = item.checked ? cyan("[x]") : dim("[ ]");
111
+ const label = selected ? bold(item.label) : item.label;
112
+ lines.push(` ${pointer} ${mark} ${label}`);
28
113
  }
29
- return answer === "y" || answer === "yes";
114
+ lines.push("");
115
+ lines.push(dim(footer));
116
+ writeRaw(`${CLEAR_LINE}${lines.join(`\n${CLEAR_LINE}`)}\n`);
30
117
  }
31
118
 
32
119
  export async function promptMultiSelect<T>(
33
120
  title: string,
34
121
  items: Array<{ label: string; value: T; checked: boolean }>,
122
+ ): Promise<T[] | null> {
123
+ if (items.length === 0) {
124
+ return [];
125
+ }
126
+
127
+ if (!input.isTTY) {
128
+ return items.filter((item) => item.checked).map((item) => item.value);
129
+ }
130
+
131
+ const footer = "↑↓ move space toggle a all enter confirm ctrl-c cancel";
132
+ let cursor = 0;
133
+
134
+ return withRawMode(async () => {
135
+ while (true) {
136
+ renderMultiSelect(title, items, cursor, footer);
137
+ const key = parseKey(await readKey());
138
+
139
+ if (key === "ctrl-c") {
140
+ writeRaw("\n");
141
+ return null;
142
+ }
143
+ if (key === "enter") {
144
+ writeRaw("\n");
145
+ return items.filter((item) => item.checked).map((item) => item.value);
146
+ }
147
+ if (key === "up") {
148
+ cursor = Math.max(0, cursor - 1);
149
+ } else if (key === "down") {
150
+ cursor = Math.min(items.length - 1, cursor + 1);
151
+ } else if (key === "space") {
152
+ const item = items[cursor];
153
+ if (item) {
154
+ item.checked = !item.checked;
155
+ }
156
+ } else if (key === "all") {
157
+ const allChecked = items.every((item) => item.checked);
158
+ for (const item of items) {
159
+ item.checked = !allChecked;
160
+ }
161
+ }
162
+ }
163
+ });
164
+ }
165
+
166
+ function renderConfirm(question: string, yesSelected: boolean): void {
167
+ const yes = yesSelected ? green(bold(" Yes ")) : dim(" Yes ");
168
+ const no = yesSelected ? dim(" No ") : green(bold(" No "));
169
+ writeRaw(`${CLEAR_LINE}${question} ${yes} / ${no}\n`);
170
+ writeRaw(`${CLEAR_LINE}${dim("←→ or y/n to choose enter to confirm ctrl-c to cancel")}\n`);
171
+ }
172
+
173
+ export async function promptConfirm(question: string, defaultYes = true): Promise<boolean> {
174
+ if (!input.isTTY) {
175
+ const rl = readline.createInterface({ input, output });
176
+ try {
177
+ const hintText = defaultYes ? dim("[Y/n]") : dim("[y/N]");
178
+ const answer = (await rl.question(`${question} ${hintText} `)).trim().toLowerCase();
179
+ if (!answer) {
180
+ return defaultYes;
181
+ }
182
+ return answer === "y" || answer === "yes";
183
+ } finally {
184
+ rl.close();
185
+ }
186
+ }
187
+
188
+ let yesSelected = defaultYes;
189
+
190
+ return withRawMode(async () => {
191
+ while (true) {
192
+ renderConfirm(question, yesSelected);
193
+ const key = parseKey(await readKey());
194
+
195
+ if (key === "ctrl-c") {
196
+ writeRaw("\n");
197
+ return false;
198
+ }
199
+ if (key === "enter") {
200
+ writeRaw("\n");
201
+ return yesSelected;
202
+ }
203
+ if (key === "left" || key === "no") {
204
+ yesSelected = false;
205
+ } else if (key === "right" || key === "yes") {
206
+ yesSelected = true;
207
+ }
208
+ }
209
+ });
210
+ }
211
+
212
+ /** Legacy numbered prompt kept for non-TTY fallback in tests. */
213
+ export async function promptMultiSelectLegacy<T>(
214
+ title: string,
215
+ items: Array<{ label: string; value: T; checked: boolean }>,
35
216
  ): Promise<T[] | null> {
36
217
  output.write("\n");
37
218
  output.write(`${bold(title)}\n`);
@@ -49,29 +230,34 @@ export async function promptMultiSelect<T>(
49
230
  output.write("\n");
50
231
  hint("Enter numbers (1,3), 'all', or press Enter for defaults", output);
51
232
 
52
- const answer = (await readLine(`${dim("> ")}`)).toLowerCase();
53
- if (!answer) {
54
- return items.filter((item) => item.checked).map((item) => item.value);
55
- }
56
- if (answer === "all" || answer === "a") {
57
- return items.map((item) => item.value);
58
- }
233
+ const rl = readline.createInterface({ input, output });
234
+ try {
235
+ const answer = (await rl.question(`${dim("> ")}`)).toLowerCase();
236
+ if (!answer) {
237
+ return items.filter((item) => item.checked).map((item) => item.value);
238
+ }
239
+ if (answer === "all" || answer === "a") {
240
+ return items.map((item) => item.value);
241
+ }
59
242
 
60
- const indices = new Set<number>();
61
- for (const part of answer.split(/[,\s]+/)) {
62
- const n = Number(part);
63
- if (Number.isInteger(n) && n >= 1 && n <= items.length) {
64
- indices.add(n - 1);
243
+ const indices = new Set<number>();
244
+ for (const part of answer.split(/[,\s]+/)) {
245
+ const n = Number(part);
246
+ if (Number.isInteger(n) && n >= 1 && n <= items.length) {
247
+ indices.add(n - 1);
248
+ }
65
249
  }
66
- }
67
250
 
68
- if (indices.size === 0) {
69
- warn("No valid selection.", output);
70
- return null;
71
- }
251
+ if (indices.size === 0) {
252
+ warn("No valid selection.", output);
253
+ return null;
254
+ }
72
255
 
73
- return [...indices]
74
- .sort((a, b) => a - b)
75
- .map((i) => items[i]?.value)
76
- .filter((value): value is T => value !== undefined);
256
+ return [...indices]
257
+ .sort((a, b) => a - b)
258
+ .map((i) => items[i]?.value)
259
+ .filter((value): value is T => value !== undefined);
260
+ } finally {
261
+ rl.close();
262
+ }
77
263
  }
@@ -0,0 +1,68 @@
1
+ /** Shared code-search policy text for instructions, Cursor rules, and sub-agents. */
2
+
3
+ export const SEARCH_POLICY_TABLE = `| Task | Use | Not |
4
+ |------|-----|-----|
5
+ | How/where/what handles X? | Miru MCP \`search\` | Grep, Glob, SemanticSearch, Read |
6
+ | Similar code elsewhere | Miru MCP \`find_related\` | Read whole files |
7
+ | Search docs or config | Miru \`search\` with \`content\` | Grep README paths |
8
+ | Exact literal exists? | Grep | Miru |
9
+ | Edit a known file:line | Read (after Miru found it) | Read-before-search |`;
10
+
11
+ export const SEARCH_POLICY_BODY = `DO NOT use Grep, Glob, SemanticSearch, or Read to explore how code works when Miru MCP is available.
12
+
13
+ Use Miru MCP tools first:
14
+ - \`search\` — natural-language or code query; pass project root as \`repo\`
15
+ - \`find_related\` — similar chunks after a \`search\` hit (\`file_path\` + \`line\`)
16
+
17
+ Native tools are allowed ONLY when:
18
+ - confirming an exact literal string (env var name, error code, quoted text)
19
+ - reading a file you already located via Miru, to edit it
20
+ - searching outside the indexed repo
21
+
22
+ ${SEARCH_POLICY_TABLE}`;
23
+
24
+ export const INSTRUCTIONS_MARKDOWN = `## Miru Code Search
25
+
26
+ ${SEARCH_POLICY_BODY}
27
+
28
+ Run \`miru setup\` once — the MCP server loads your API key from \`credentials.json\`.
29
+
30
+ CLI fallback when MCP is unavailable:
31
+
32
+ \`\`\`bash
33
+ miru search "authentication flow" .
34
+ miru find-related src/auth.ts 42 .
35
+ \`\`\`
36
+
37
+ After large refactors or CLI-only use, run \`miru clear <path>\` to rebuild the disk cache.
38
+ If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;
39
+
40
+ export const CURSOR_RULES_MDC = `---
41
+ description: Miru MCP is the default for all code exploration
42
+ alwaysApply: true
43
+ ---
44
+
45
+ # Code search policy (Miru)
46
+
47
+ ${SEARCH_POLICY_BODY}
48
+
49
+ When Miru MCP is connected, call \`search\` before any built-in search tool. Never use Cursor SemanticSearch or Grep for codebase questions unless confirming an exact literal.`;
50
+
51
+ export const SUBAGENT_BODY = `When Miru MCP is available, use MCP \`search\` and \`find_related\` — not Grep, Glob, SemanticSearch, or Read-for-exploration.
52
+
53
+ ${SEARCH_POLICY_BODY}
54
+
55
+ ### MCP workflow
56
+
57
+ 1. Call \`search\` with \`repo\` set to the project root (local path or https:// git URL).
58
+ 2. Use returned \`file_path\` and \`line\` from hits; call \`find_related\` to explore neighbors.
59
+ 3. Read files only when a chunk lacks context or you are about to edit.
60
+
61
+ ### CLI fallback (no MCP in this session)
62
+
63
+ \`\`\`bash
64
+ miru search "authentication flow" .
65
+ miru find-related src/auth.ts 42 .
66
+ \`\`\`
67
+
68
+ If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;