@takara-ai/miru-code 1.8.2 → 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.
Files changed (60) hide show
  1. package/README.md +65 -53
  2. package/docs/coverage-badge.json +6 -0
  3. package/grammars/manifest.json +10 -2
  4. package/grammars/tree-sitter-astro.wasm +0 -0
  5. package/grammars/tree-sitter-php.wasm +0 -0
  6. package/grammars/tree-sitter-php_only.wasm +0 -0
  7. package/grammars/tree-sitter-sql.wasm +0 -0
  8. package/grammars/tree-sitter-svelte.wasm +0 -0
  9. package/grammars/tree-sitter-vue.wasm +0 -0
  10. package/package.json +10 -2
  11. package/src/agents.ts +4 -4
  12. package/src/auth/device.ts +26 -21
  13. package/src/auth/providers.ts +26 -11
  14. package/src/benchmark/compare.ts +3 -10
  15. package/src/benchmark/grep.ts +64 -37
  16. package/src/benchmark/locate-compare.ts +8 -1
  17. package/src/benchmark/rg-literal.ts +9 -4
  18. package/src/cache.ts +3 -3
  19. package/src/chunking/ast.ts +21 -3
  20. package/src/chunking/grammars.ts +8 -3
  21. package/src/chunking/structural.ts +13 -24
  22. package/src/cli-mode.ts +18 -0
  23. package/src/cli.ts +54 -98
  24. package/src/credentials.ts +14 -14
  25. package/src/embeddings/openai.ts +6 -3
  26. package/src/embeddings/sagemaker.ts +18 -3
  27. package/src/embeddings/validate.ts +10 -7
  28. package/src/git.ts +6 -2
  29. package/src/help.ts +7 -7
  30. package/src/index/bm25.ts +66 -6
  31. package/src/index/chunk-selector.ts +39 -11
  32. package/src/index/create.ts +5 -9
  33. package/src/index/files.ts +5 -1
  34. package/src/index/incremental.ts +48 -8
  35. package/src/index/sparse.ts +14 -2
  36. package/src/installer/agents.ts +85 -65
  37. package/src/installer/benchmark-mode.ts +40 -11
  38. package/src/installer/config.ts +105 -42
  39. package/src/installer/installer.ts +59 -34
  40. package/src/installer/legacy-hooks.ts +131 -0
  41. package/src/installer/prompt.ts +20 -22
  42. package/src/installer/search-policy.ts +10 -10
  43. package/src/mcp/index-cache.ts +16 -3
  44. package/src/mcp/server.ts +65 -46
  45. package/src/mcp/stdio.ts +17 -2
  46. package/src/miru-index.ts +90 -22
  47. package/src/prompt.ts +51 -14
  48. package/src/ranking/boosting.ts +5 -6
  49. package/src/ranking/location.ts +6 -10
  50. package/src/ranking/penalties.ts +14 -22
  51. package/src/search.ts +8 -12
  52. package/src/setup.ts +25 -13
  53. package/src/snippet.ts +8 -26
  54. package/src/spinner.ts +26 -16
  55. package/src/tokenizer/bert-wordpiece.ts +0 -6
  56. package/src/utils.ts +1 -15
  57. package/src/version.ts +4 -4
  58. package/src/installer/hooks/install.ts +0 -549
  59. package/src/installer/hooks/opencode-plugin.ts +0 -54
  60. package/src/installer/hooks/search-guard.ts +0 -272
@@ -1,549 +0,0 @@
1
- import { unlink } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
- import type { HooksFormat, InstallAction } from "../agents.ts";
4
- import { stripJsonComments } from "../config.ts";
5
- import { isCopilotFamilyId } from "../copilot-family.ts";
6
-
7
- const HOOK_GUARD_MARKER = "hook-guard";
8
- const MIRU_HOOK_MARKER = "miru-search";
9
- const VSCODE_OWNERS_KEY = "miruOwners";
10
-
11
- const MATCHERS = {
12
- cursor: "Grep|Glob|Shell|SemanticSearch",
13
- claude: "Grep|Glob|Bash|grep|read_file",
14
- gemini: "grep_search|glob_file_search|codebase_search",
15
- kiroGrep: "grep",
16
- kiroGlob: "glob",
17
- kiroShell: "shell",
18
- } as const;
19
-
20
- function parseJsonObject(text: string): Record<string, unknown> | "error" {
21
- const trimmed = text.trim();
22
- if (!trimmed) {
23
- return {};
24
- }
25
- try {
26
- const parsed: unknown = JSON.parse(stripJsonComments(trimmed));
27
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
28
- return "error";
29
- }
30
- return parsed as Record<string, unknown>;
31
- } catch {
32
- return "error";
33
- }
34
- }
35
-
36
- function shellQuote(path: string): string {
37
- if (/^[a-zA-Z0-9_./:@%-]+$/.test(path)) {
38
- return path;
39
- }
40
- return `'${path.replace(/'/g, "'\\''")}'`;
41
- }
42
-
43
- /** Resolve the shell command miru install writes into hook configs. */
44
- export function resolveHookCommand(): string {
45
- const entry = process.argv[1];
46
- if (entry) {
47
- const resolved = resolve(entry);
48
- if (resolved.endsWith(".ts")) {
49
- return `bun run ${shellQuote(resolved)} hook-guard`;
50
- }
51
- return `${shellQuote(resolved)} hook-guard`;
52
- }
53
- return "miru hook-guard";
54
- }
55
-
56
- function hookCommandMatches(command: unknown): boolean {
57
- return typeof command === "string" && command.includes(HOOK_GUARD_MARKER);
58
- }
59
-
60
- function isEmptyHookConfig(parsed: Record<string, unknown>): boolean {
61
- if (Object.keys(parsed).length === 0) {
62
- return true;
63
- }
64
- const hooks = parsed.hooks;
65
- if (hooks === undefined) {
66
- return Object.keys(parsed).every((key) => key === "version");
67
- }
68
- if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
69
- return false;
70
- }
71
- return Object.keys(hooks as Record<string, unknown>).length === 0;
72
- }
73
-
74
- function removeMiruHookEntries<T extends Record<string, unknown>>(
75
- entries: T[],
76
- isMiru: (entry: T) => boolean,
77
- ): T[] {
78
- return entries.filter((entry) => !isMiru(entry));
79
- }
80
-
81
- function pascalPreToolUseEntry(command: string, matcher: string): Record<string, unknown> {
82
- return {
83
- matcher,
84
- hooks: [
85
- {
86
- type: "command",
87
- command,
88
- statusMessage: "Miru search policy",
89
- },
90
- ],
91
- };
92
- }
93
-
94
- function cursorHookEntry(command: string): Record<string, unknown> {
95
- return {
96
- command,
97
- matcher: MATCHERS.cursor,
98
- };
99
- }
100
-
101
- function geminiBeforeToolEntry(command: string): Record<string, unknown> {
102
- return {
103
- type: "command",
104
- command,
105
- matcher: MATCHERS.gemini,
106
- timeout: 15_000,
107
- };
108
- }
109
-
110
- function kiroPreToolUseEntry(command: string, matcher: string): Record<string, unknown> {
111
- return {
112
- matcher,
113
- command,
114
- };
115
- }
116
-
117
- function windsurfPreRunEntry(command: string): Record<string, unknown> {
118
- return { command };
119
- }
120
-
121
- async function writeJson(
122
- path: string,
123
- data: Record<string, unknown>,
124
- existed: boolean,
125
- ): Promise<InstallAction> {
126
- await Bun.write(path, `${JSON.stringify(data, null, 2)}\n`);
127
- return existed ? "updated" : "created";
128
- }
129
-
130
- async function mergeHooksSection(
131
- path: string,
132
- eventKey: string,
133
- nextEntries: Record<string, unknown>[],
134
- isMiruEntry: (entry: Record<string, unknown>) => boolean,
135
- buildEntry: (command: string) => Record<string, unknown>,
136
- ): Promise<InstallAction> {
137
- const command = resolveHookCommand();
138
- const existed = await Bun.file(path).exists();
139
- const text = existed ? await Bun.file(path).text() : "";
140
- const parsed = parseJsonObject(text);
141
- if (parsed === "error") {
142
- return "error";
143
- }
144
-
145
- const hooksRoot =
146
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
147
- ? { ...(parsed.hooks as Record<string, unknown>) }
148
- : {};
149
-
150
- const existing = Array.isArray(hooksRoot[eventKey])
151
- ? [...(hooksRoot[eventKey] as Record<string, unknown>[])]
152
- : [];
153
-
154
- const filtered = removeMiruHookEntries(existing, isMiruEntry);
155
- const rebuilt = nextEntries.length > 0 ? nextEntries : [buildEntry(command)];
156
- const nextSection = [...filtered, ...rebuilt];
157
-
158
- if (JSON.stringify(existing) === JSON.stringify(nextSection)) {
159
- return "unchanged";
160
- }
161
-
162
- hooksRoot[eventKey] = nextSection;
163
- parsed.hooks = hooksRoot;
164
- return writeJson(path, parsed, existed);
165
- }
166
-
167
- async function removeHooksSection(
168
- path: string,
169
- eventKeys: string[],
170
- isMiruEntry: (entry: Record<string, unknown>) => boolean,
171
- ): Promise<InstallAction> {
172
- if (!(await Bun.file(path).exists())) {
173
- return "not-found";
174
- }
175
-
176
- const text = await Bun.file(path).text();
177
- const parsed = parseJsonObject(text);
178
- if (parsed === "error") {
179
- return "error";
180
- }
181
-
182
- const hooksRoot =
183
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
184
- ? (parsed.hooks as Record<string, unknown>)
185
- : null;
186
- if (!hooksRoot) {
187
- return "not-found";
188
- }
189
-
190
- let changed = false;
191
- for (const eventKey of eventKeys) {
192
- if (!Array.isArray(hooksRoot[eventKey])) {
193
- continue;
194
- }
195
- const filtered = removeMiruHookEntries(
196
- hooksRoot[eventKey] as Record<string, unknown>[],
197
- isMiruEntry,
198
- );
199
- if (filtered.length !== (hooksRoot[eventKey] as unknown[]).length) {
200
- changed = true;
201
- if (filtered.length === 0) {
202
- delete hooksRoot[eventKey];
203
- } else {
204
- hooksRoot[eventKey] = filtered;
205
- }
206
- }
207
- }
208
-
209
- if (!changed) {
210
- return "not-found";
211
- }
212
-
213
- if (Object.keys(hooksRoot).length === 0) {
214
- delete parsed.hooks;
215
- } else {
216
- parsed.hooks = hooksRoot;
217
- }
218
-
219
- if (isEmptyHookConfig(parsed)) {
220
- await unlink(path);
221
- return "removed";
222
- }
223
-
224
- await Bun.write(path, `${JSON.stringify(parsed, null, 2)}\n`);
225
- return "removed";
226
- }
227
-
228
- export async function mergeCursorHooks(path: string): Promise<InstallAction> {
229
- const command = resolveHookCommand();
230
- const existed = await Bun.file(path).exists();
231
- const text = existed ? await Bun.file(path).text() : "";
232
- const parsed = parseJsonObject(text);
233
- if (parsed === "error") {
234
- return "error";
235
- }
236
-
237
- const hooksRoot =
238
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
239
- ? (parsed.hooks as Record<string, unknown>)
240
- : {};
241
-
242
- const preToolUse = Array.isArray(hooksRoot.preToolUse)
243
- ? [...(hooksRoot.preToolUse as Record<string, unknown>[])]
244
- : [];
245
-
246
- const nextEntry = cursorHookEntry(command);
247
- const existingIdx = preToolUse.findIndex((entry) => hookCommandMatches(entry.command));
248
- if (existingIdx >= 0) {
249
- if (JSON.stringify(preToolUse[existingIdx]) === JSON.stringify(nextEntry)) {
250
- return "unchanged";
251
- }
252
- preToolUse[existingIdx] = nextEntry;
253
- } else {
254
- preToolUse.push(nextEntry);
255
- }
256
-
257
- const next = {
258
- version: typeof parsed.version === "number" ? parsed.version : 1,
259
- hooks: { ...hooksRoot, preToolUse },
260
- };
261
-
262
- return writeJson(path, next, existed);
263
- }
264
-
265
- export async function removeCursorHooks(path: string): Promise<InstallAction> {
266
- return removeHooksSection(path, ["preToolUse"], (entry) => hookCommandMatches(entry.command));
267
- }
268
-
269
- export async function mergeClaudeHooks(path: string): Promise<InstallAction> {
270
- return mergeHooksSection(
271
- path,
272
- "PreToolUse",
273
- [],
274
- (entry) => {
275
- const nested = entry.hooks;
276
- if (!Array.isArray(nested)) {
277
- return false;
278
- }
279
- return nested.some((hook) => hookCommandMatches((hook as Record<string, unknown>).command));
280
- },
281
- (command) => pascalPreToolUseEntry(command, MATCHERS.claude),
282
- );
283
- }
284
-
285
- export async function removeClaudeHooks(path: string): Promise<InstallAction> {
286
- return removeHooksSection(path, ["PreToolUse"], (entry) => {
287
- const nested = entry.hooks;
288
- if (!Array.isArray(nested)) {
289
- return false;
290
- }
291
- return nested.some((hook) => hookCommandMatches((hook as Record<string, unknown>).command));
292
- });
293
- }
294
-
295
- export async function mergeGeminiHooks(path: string): Promise<InstallAction> {
296
- return mergeHooksSection(
297
- path,
298
- "BeforeTool",
299
- [],
300
- (entry) => hookCommandMatches(entry.command),
301
- (command) => geminiBeforeToolEntry(command),
302
- );
303
- }
304
-
305
- export async function removeGeminiHooks(path: string): Promise<InstallAction> {
306
- return removeHooksSection(path, ["BeforeTool"], (entry) => hookCommandMatches(entry.command));
307
- }
308
-
309
- export async function mergeVscodeHooks(path: string, owner?: string): Promise<InstallAction> {
310
- const command = resolveHookCommand();
311
- const existed = await Bun.file(path).exists();
312
- const text = existed ? await Bun.file(path).text() : "";
313
- const parsed = parseJsonObject(text);
314
- if (parsed === "error") {
315
- return "error";
316
- }
317
-
318
- const hooksRoot =
319
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
320
- ? (parsed.hooks as Record<string, unknown>)
321
- : {};
322
-
323
- const nextEntry = {
324
- type: "command",
325
- command,
326
- matcher: MATCHERS.claude,
327
- statusMessage: "Miru search policy",
328
- };
329
-
330
- const existingPreToolUse = Array.isArray(hooksRoot.PreToolUse)
331
- ? (hooksRoot.PreToolUse as unknown[])
332
- : [];
333
- const mergedPreToolUse =
334
- existingPreToolUse.length > 0 &&
335
- existingPreToolUse.some((entry) =>
336
- hookCommandMatches((entry as Record<string, unknown>).command),
337
- )
338
- ? existingPreToolUse.map((entry) => {
339
- const hookEntry = entry as Record<string, unknown>;
340
- if (!hookCommandMatches(hookEntry.command)) {
341
- return hookEntry;
342
- }
343
- return nextEntry;
344
- })
345
- : [...existingPreToolUse, nextEntry];
346
-
347
- const currentOwners = Array.isArray(parsed[VSCODE_OWNERS_KEY])
348
- ? (parsed[VSCODE_OWNERS_KEY] as unknown[]).filter(
349
- (item): item is string => typeof item === "string" && isCopilotFamilyId(item),
350
- )
351
- : [];
352
- const nextOwners = owner
353
- ? Array.from(new Set([...currentOwners, ...(isCopilotFamilyId(owner) ? [owner] : [])]))
354
- : currentOwners;
355
-
356
- const next = {
357
- ...parsed,
358
- hooks: { ...hooksRoot, PreToolUse: mergedPreToolUse },
359
- ...(nextOwners.length > 0 ? { [VSCODE_OWNERS_KEY]: nextOwners } : {}),
360
- };
361
- const nextJson = `${JSON.stringify(next, null, 2)}\n`;
362
- if (nextJson === `${text.endsWith("\n") ? text : `${text}\n`}`) {
363
- return "unchanged";
364
- }
365
- await Bun.write(path, nextJson);
366
- return existed ? "updated" : "created";
367
- }
368
-
369
- export async function removeVscodeHooks(path: string, owner?: string): Promise<InstallAction> {
370
- if (!(await Bun.file(path).exists())) {
371
- return "not-found";
372
- }
373
- const text = await Bun.file(path).text();
374
- const parsed = parseJsonObject(text);
375
- if (parsed === "error") {
376
- return "error";
377
- }
378
-
379
- const owners = Array.isArray(parsed[VSCODE_OWNERS_KEY])
380
- ? (parsed[VSCODE_OWNERS_KEY] as unknown[]).filter(
381
- (item): item is string => typeof item === "string",
382
- )
383
- : null;
384
- if (owners && owner) {
385
- const remainingOwners = owners.filter((value) => value !== owner);
386
- if (remainingOwners.length > 0) {
387
- parsed[VSCODE_OWNERS_KEY] = remainingOwners;
388
- await Bun.write(path, `${JSON.stringify(parsed, null, 2)}\n`);
389
- return "updated";
390
- }
391
- delete parsed[VSCODE_OWNERS_KEY];
392
- const hooksRoot =
393
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
394
- ? (parsed.hooks as Record<string, unknown>)
395
- : {};
396
- const filtered = Array.isArray(hooksRoot.PreToolUse)
397
- ? (hooksRoot.PreToolUse as Record<string, unknown>[]).filter(
398
- (entry) => !hookCommandMatches(entry.command),
399
- )
400
- : [];
401
- if (filtered.length === 0) {
402
- delete hooksRoot.PreToolUse;
403
- } else {
404
- hooksRoot.PreToolUse = filtered;
405
- }
406
- if (Object.keys(hooksRoot).length === 0) {
407
- delete parsed.hooks;
408
- } else {
409
- parsed.hooks = hooksRoot;
410
- }
411
- if (isEmptyHookConfig(parsed)) {
412
- await unlink(path);
413
- return "removed";
414
- }
415
- await Bun.write(path, `${JSON.stringify(parsed, null, 2)}\n`);
416
- return "removed";
417
- }
418
-
419
- if (!text.includes(HOOK_GUARD_MARKER) && !text.includes(MIRU_HOOK_MARKER)) {
420
- return "not-found";
421
- }
422
- await unlink(path);
423
- return "removed";
424
- }
425
-
426
- export async function mergeKiroHooks(path: string): Promise<InstallAction> {
427
- const command = resolveHookCommand();
428
- const existed = await Bun.file(path).exists();
429
- const text = existed ? await Bun.file(path).text() : "";
430
- const parsed = parseJsonObject(text);
431
- if (parsed === "error") {
432
- return "error";
433
- }
434
-
435
- const hooksRoot =
436
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
437
- ? { ...(parsed.hooks as Record<string, unknown>) }
438
- : {};
439
-
440
- const matchers = [MATCHERS.kiroGrep, MATCHERS.kiroGlob, MATCHERS.kiroShell] as const;
441
- const existing = Array.isArray(hooksRoot.preToolUse)
442
- ? [...(hooksRoot.preToolUse as Record<string, unknown>[])]
443
- : [];
444
-
445
- const filtered = removeMiruHookEntries(existing, (entry) => hookCommandMatches(entry.command));
446
- const miruEntries = matchers.map((matcher) => kiroPreToolUseEntry(command, matcher));
447
- const nextSection = [...filtered, ...miruEntries];
448
-
449
- if (JSON.stringify(existing) === JSON.stringify(nextSection)) {
450
- return "unchanged";
451
- }
452
-
453
- hooksRoot.preToolUse = nextSection;
454
- parsed.hooks = hooksRoot;
455
- return writeJson(path, parsed, existed);
456
- }
457
-
458
- export async function removeKiroHooks(path: string): Promise<InstallAction> {
459
- return removeHooksSection(path, ["preToolUse"], (entry) => hookCommandMatches(entry.command));
460
- }
461
-
462
- export async function mergeWindsurfHooks(path: string): Promise<InstallAction> {
463
- return mergeHooksSection(
464
- path,
465
- "pre_run_command",
466
- [],
467
- (entry) => hookCommandMatches(entry.command),
468
- (command) => windsurfPreRunEntry(command),
469
- );
470
- }
471
-
472
- export async function removeWindsurfHooks(path: string): Promise<InstallAction> {
473
- return removeHooksSection(path, ["pre_run_command"], (entry) =>
474
- hookCommandMatches(entry.command),
475
- );
476
- }
477
-
478
- export async function mergeOpenCodePlugin(pluginPath: string): Promise<InstallAction> {
479
- const source = join(import.meta.dir, "opencode-plugin.ts");
480
- const content = await Bun.file(source).text();
481
- const existed = await Bun.file(pluginPath).exists();
482
- if (existed && (await Bun.file(pluginPath).text()) === content) {
483
- return "unchanged";
484
- }
485
- await Bun.write(pluginPath, content);
486
- return existed ? "updated" : "created";
487
- }
488
-
489
- export async function removeOpenCodePlugin(pluginPath: string): Promise<InstallAction> {
490
- if (!(await Bun.file(pluginPath).exists())) {
491
- return "not-found";
492
- }
493
- await unlink(pluginPath);
494
- return "removed";
495
- }
496
-
497
- export async function mergeHooks(
498
- format: HooksFormat,
499
- path: string,
500
- owner?: string,
501
- ): Promise<InstallAction> {
502
- switch (format) {
503
- case "cursor":
504
- return mergeCursorHooks(path);
505
- case "claude":
506
- return mergeClaudeHooks(path);
507
- case "gemini":
508
- return mergeGeminiHooks(path);
509
- case "vscode":
510
- return mergeVscodeHooks(path, owner);
511
- case "kiro":
512
- return mergeKiroHooks(path);
513
- case "windsurf":
514
- return mergeWindsurfHooks(path);
515
- case "opencode":
516
- return mergeOpenCodePlugin(path);
517
- default: {
518
- const _exhaustive: never = format;
519
- return _exhaustive;
520
- }
521
- }
522
- }
523
-
524
- export async function removeHooks(
525
- format: HooksFormat,
526
- path: string,
527
- owner?: string,
528
- ): Promise<InstallAction> {
529
- switch (format) {
530
- case "cursor":
531
- return removeCursorHooks(path);
532
- case "claude":
533
- return removeClaudeHooks(path);
534
- case "gemini":
535
- return removeGeminiHooks(path);
536
- case "vscode":
537
- return removeVscodeHooks(path, owner);
538
- case "kiro":
539
- return removeKiroHooks(path);
540
- case "windsurf":
541
- return removeWindsurfHooks(path);
542
- case "opencode":
543
- return removeOpenCodePlugin(path);
544
- default: {
545
- const _exhaustive: never = format;
546
- return _exhaustive;
547
- }
548
- }
549
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * OpenCode plugin: blocks built-in search tools in favor of Miru MCP.
3
- * Installed to ~/.config/opencode/plugins/miru-search-guard.ts
4
- */
5
-
6
- const REDIRECT =
7
- "Use Miru MCP (repo = project root): `locate` for exact literals, `search` for meaning-based questions — not grep/glob/bash exploration.";
8
-
9
- const GREP_TOOLS = new Set(["grep", "glob", "codesearch", "codebase_search", "search"]);
10
- const SHELL_TOOLS = new Set(["bash", "shell", "sh"]);
11
-
12
- function isExplorationShell(command: string): boolean {
13
- const cmd = command.trim();
14
- if (!cmd) {
15
- return false;
16
- }
17
- if (
18
- /\b(git|npm|bun|pnpm|yarn|cargo|go|make|cmake|docker|kubectl|pytest|jest|vitest)\b/.test(cmd)
19
- ) {
20
- return false;
21
- }
22
- return /\b(rg|ripgrep|grep|find|ag|ack|fd)\b/.test(cmd);
23
- }
24
-
25
- function shouldBlock(tool: string, args: Record<string, unknown>): boolean {
26
- const name = tool.toLowerCase();
27
- if (name.includes("miru")) {
28
- return false;
29
- }
30
- if (name === "glob") {
31
- return true;
32
- }
33
- if (GREP_TOOLS.has(name)) {
34
- return true;
35
- }
36
- if (SHELL_TOOLS.has(name)) {
37
- const command = String(args.command ?? args.cmd ?? "");
38
- return isExplorationShell(command);
39
- }
40
- return false;
41
- }
42
-
43
- export default async function MiruSearchGuardPlugin() {
44
- return {
45
- "tool.execute.before": async (
46
- input: { tool: string },
47
- output: { args: Record<string, unknown> },
48
- ) => {
49
- if (shouldBlock(input.tool, output.args ?? {})) {
50
- throw new Error(REDIRECT);
51
- }
52
- },
53
- };
54
- }