@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.
- package/README.md +65 -53
- 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 +54 -98
- package/src/credentials.ts +14 -14
- 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 +7 -7
- package/src/index/bm25.ts +66 -6
- package/src/index/chunk-selector.ts +39 -11
- 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 +10 -10
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +65 -46
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +90 -22
- 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 +8 -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 +1 -15
- 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
|
@@ -4,14 +4,16 @@ export type ChunkIndexMappings = {
|
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Union of chunk indices matching language and/or path filters.
|
|
7
|
+
* Union of chunk indices matching language and/or path filters. Path filters
|
|
8
|
+
* match an exact indexed file path or every file below a directory prefix.
|
|
8
9
|
*
|
|
9
10
|
* Filters combine with OR semantics: a chunk is included if it matches any
|
|
10
11
|
* requested language or path. Returns `undefined` when no filters are given
|
|
11
|
-
* (search all chunks)
|
|
12
|
+
* (search all chunks), and an empty array when filters match no chunks.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
* callers).
|
|
14
|
+
* Exact-file and single-language queries return the prebuilt map array
|
|
15
|
+
* directly (read-only for callers). Directory and multi-filter queries
|
|
16
|
+
* allocate an index list.
|
|
15
17
|
*/
|
|
16
18
|
export function buildChunkSelector(
|
|
17
19
|
mappings: ChunkIndexMappings,
|
|
@@ -28,19 +30,28 @@ export function buildChunkSelector(
|
|
|
28
30
|
if (langCount === 1 && pathCount === 0) {
|
|
29
31
|
const lang = filterLanguages?.[0];
|
|
30
32
|
if (!lang) {
|
|
31
|
-
return
|
|
33
|
+
return [];
|
|
32
34
|
}
|
|
33
35
|
const indices = mappings.languageMapping.get(lang);
|
|
34
|
-
return indices && indices.length > 0 ? indices :
|
|
36
|
+
return indices && indices.length > 0 ? indices : [];
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
if (pathCount === 1 && langCount === 0) {
|
|
38
40
|
const fp = filterPaths?.[0];
|
|
39
41
|
if (!fp) {
|
|
40
|
-
return
|
|
42
|
+
return [];
|
|
41
43
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
+
const normalized = normalizePathFilter(fp);
|
|
45
|
+
const exact = [...mappings.fileMapping.entries()].find(
|
|
46
|
+
([filePath]) => filePath.replace(/\\/g, "/") === normalized,
|
|
47
|
+
)?.[1];
|
|
48
|
+
if (exact) {
|
|
49
|
+
return exact;
|
|
50
|
+
}
|
|
51
|
+
const prefix = `${normalized}/`;
|
|
52
|
+
return [...mappings.fileMapping.entries()]
|
|
53
|
+
.filter(([filePath]) => filePath.replace(/\\/g, "/").startsWith(prefix))
|
|
54
|
+
.flatMap(([, indices]) => indices);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
const selector: number[] = [];
|
|
@@ -48,10 +59,27 @@ export function buildChunkSelector(
|
|
|
48
59
|
selector.push(...(mappings.languageMapping.get(lang) ?? []));
|
|
49
60
|
}
|
|
50
61
|
for (const fp of filterPaths ?? []) {
|
|
51
|
-
|
|
62
|
+
const normalized = normalizePathFilter(fp);
|
|
63
|
+
const exact = [...mappings.fileMapping.entries()].find(
|
|
64
|
+
([filePath]) => filePath.replace(/\\/g, "/") === normalized,
|
|
65
|
+
)?.[1];
|
|
66
|
+
if (exact) {
|
|
67
|
+
selector.push(...exact);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const prefix = `${normalized}/`;
|
|
71
|
+
for (const [filePath, indices] of mappings.fileMapping) {
|
|
72
|
+
if (filePath.replace(/\\/g, "/").startsWith(prefix)) {
|
|
73
|
+
selector.push(...indices);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
52
76
|
}
|
|
53
77
|
if (selector.length === 0) {
|
|
54
|
-
return
|
|
78
|
+
return [];
|
|
55
79
|
}
|
|
56
80
|
return [...new Set(selector)];
|
|
57
81
|
}
|
|
82
|
+
|
|
83
|
+
function normalizePathFilter(filePath: string): string {
|
|
84
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
85
|
+
}
|
package/src/index/create.ts
CHANGED
|
@@ -136,9 +136,6 @@ export async function createIndexFromPath(
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const take = Math.min(embedBatchSize, pendingWindows.length);
|
|
139
|
-
if (!force && take < embedBatchSize) {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
139
|
|
|
143
140
|
const batch = pendingWindows.splice(0, take);
|
|
144
141
|
emittedApiBatches++;
|
|
@@ -158,9 +155,10 @@ export async function createIndexFromPath(
|
|
|
158
155
|
})();
|
|
159
156
|
embedPromises.push(promise);
|
|
160
157
|
embedInFlight.add(promise);
|
|
161
|
-
promise.
|
|
162
|
-
embedInFlight.delete(promise)
|
|
163
|
-
|
|
158
|
+
promise.then(
|
|
159
|
+
() => embedInFlight.delete(promise),
|
|
160
|
+
() => embedInFlight.delete(promise),
|
|
161
|
+
);
|
|
164
162
|
}
|
|
165
163
|
};
|
|
166
164
|
|
|
@@ -209,9 +207,7 @@ export async function createIndexFromPath(
|
|
|
209
207
|
await maybeScheduleEmbed(true);
|
|
210
208
|
await Promise.all(embedPromises);
|
|
211
209
|
// Finish any BM25 docs not yet processed during idle gaps.
|
|
212
|
-
|
|
213
|
-
progressBm25(50);
|
|
214
|
-
}
|
|
210
|
+
progressBm25(Number.POSITIVE_INFINITY);
|
|
215
211
|
const embedsDoneAt = performance.now();
|
|
216
212
|
|
|
217
213
|
const chunks = emittedChunks;
|
package/src/index/files.ts
CHANGED
|
@@ -52,6 +52,9 @@ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
|
52
52
|
".zig": "zig",
|
|
53
53
|
".vue": "vue",
|
|
54
54
|
".svelte": "svelte",
|
|
55
|
+
".astro": "astro",
|
|
56
|
+
".erb": "embeddedtemplate",
|
|
57
|
+
".ejs": "embeddedtemplate",
|
|
55
58
|
".md": "markdown",
|
|
56
59
|
".mdx": "markdown",
|
|
57
60
|
".rst": "rst",
|
|
@@ -64,6 +67,7 @@ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
|
64
67
|
".cfg": "ini",
|
|
65
68
|
".xml": "xml",
|
|
66
69
|
".html": "html",
|
|
70
|
+
".htm": "html",
|
|
67
71
|
".css": "css",
|
|
68
72
|
".scss": "scss",
|
|
69
73
|
".less": "less",
|
|
@@ -74,7 +78,7 @@ const CODE_EXTENSIONS = new Set(
|
|
|
74
78
|
Object.entries(EXTENSION_TO_LANGUAGE)
|
|
75
79
|
.filter(
|
|
76
80
|
([, lang]) =>
|
|
77
|
-
!["markdown", "rst", "text", "json", "yaml", "toml", "ini", "xml"
|
|
81
|
+
!["markdown", "rst", "text", "json", "yaml", "toml", "ini", "xml"].includes(lang),
|
|
78
82
|
)
|
|
79
83
|
.map(([ext]) => ext),
|
|
80
84
|
);
|
package/src/index/incremental.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Chunk, ContentType } from "../types.ts";
|
|
|
5
5
|
import type { BM25Index } from "./bm25.ts";
|
|
6
6
|
import { detectLanguage, getExtensions, getFileStatus, readFileText } from "./files.ts";
|
|
7
7
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
8
|
-
import { buildBm25FromChunks } from "./sparse.ts";
|
|
8
|
+
import { buildBm25FromChunks, patchBm25 } from "./sparse.ts";
|
|
9
9
|
import { buildSemanticIndex } from "./vector-storage.ts";
|
|
10
10
|
import { vectorAt } from "./vectors.ts";
|
|
11
11
|
|
|
@@ -40,9 +40,34 @@ function isIndexableRelativePath(relativePath: string, extensions: Set<string>):
|
|
|
40
40
|
return extensions.has(ext);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Vector for every text: reused from `known`, otherwise embedded once (duplicates share a call). */
|
|
44
|
+
async function vectorsByText(
|
|
45
|
+
embeddings: EmbeddingBackend,
|
|
46
|
+
texts: readonly string[],
|
|
47
|
+
known: ReadonlyMap<string, Float32Array>,
|
|
48
|
+
): Promise<Map<string, Float32Array>> {
|
|
49
|
+
const result = new Map(known);
|
|
50
|
+
const missing = [...new Set(texts)].filter((text) => !result.has(text));
|
|
51
|
+
if (missing.length === 0) {
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
const embedded = await embeddings.embedDocuments(missing);
|
|
55
|
+
if (embedded.length !== missing.length) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Vector count ${embedded.length} does not match chunk count ${missing.length}.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
for (const [i, text] of missing.entries()) {
|
|
61
|
+
result.set(text, embedded[i] as Float32Array);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
/**
|
|
44
67
|
* Replace chunks for the given repo-relative paths only: remove old chunks,
|
|
45
|
-
* embed new ones, rebuild
|
|
68
|
+
* embed new ones, rebuild the semantic index from the merged vector set, and update
|
|
69
|
+
* BM25 by dropping the old docs and indexing only the new chunks. Pass `bm25` (the
|
|
70
|
+
* index matching `chunks`) to enable that; without it BM25 is rebuilt from scratch.
|
|
46
71
|
*/
|
|
47
72
|
export async function applyIncrementalFileChanges(options: {
|
|
48
73
|
root: string;
|
|
@@ -50,6 +75,7 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
50
75
|
embeddings: EmbeddingBackend;
|
|
51
76
|
chunks: Chunk[];
|
|
52
77
|
semanticIndex: SemanticIndex;
|
|
78
|
+
bm25?: BM25Index;
|
|
53
79
|
relativePaths: readonly string[];
|
|
54
80
|
}): Promise<{ chunks: Chunk[]; bm25: BM25Index; semantic: SemanticIndex }> {
|
|
55
81
|
const root = resolve(options.root);
|
|
@@ -60,16 +86,24 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
60
86
|
.filter((p) => p.length > 0 && isIndexableRelativePath(p, extensions)),
|
|
61
87
|
);
|
|
62
88
|
|
|
89
|
+
// BM25 doc i is chunk i; an index of another size can't be patched safely.
|
|
90
|
+
const baseBm25 = options.bm25?.size === options.chunks.length ? options.bm25 : undefined;
|
|
91
|
+
|
|
63
92
|
if (targets.size === 0) {
|
|
64
93
|
return {
|
|
65
94
|
chunks: options.chunks,
|
|
66
|
-
bm25: buildBm25FromChunks(options.chunks),
|
|
95
|
+
bm25: baseBm25 ?? buildBm25FromChunks(options.chunks),
|
|
67
96
|
semantic: options.semanticIndex,
|
|
68
97
|
};
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
const keptChunks: Chunk[] = [];
|
|
72
101
|
const keptVectors: Float32Array[] = [];
|
|
102
|
+
const removedDocs = new Set<number>();
|
|
103
|
+
// An embedding is a pure function of chunk text, so a re-chunked file can reuse
|
|
104
|
+
// the vector of any old chunk with byte-identical content (keyed on the exact
|
|
105
|
+
// string, never a hash, so a collision can't leave a stale vector).
|
|
106
|
+
const reusableVectors = new Map<string, Float32Array>();
|
|
73
107
|
|
|
74
108
|
for (let i = 0; i < options.chunks.length; i++) {
|
|
75
109
|
const chunk = options.chunks[i];
|
|
@@ -78,6 +112,10 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
78
112
|
}
|
|
79
113
|
const rel = normalizeRelativePath(chunk.file_path);
|
|
80
114
|
if (targets.has(rel)) {
|
|
115
|
+
removedDocs.add(i);
|
|
116
|
+
if (!reusableVectors.has(chunk.content)) {
|
|
117
|
+
reusableVectors.set(chunk.content, vectorAt(options.semanticIndex, i));
|
|
118
|
+
}
|
|
81
119
|
continue;
|
|
82
120
|
}
|
|
83
121
|
keptChunks.push(chunk);
|
|
@@ -89,10 +127,12 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
89
127
|
addedChunks.push(...(await chunksForFile(root, rel)));
|
|
90
128
|
}
|
|
91
129
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
const vectorFor = await vectorsByText(
|
|
131
|
+
options.embeddings,
|
|
132
|
+
addedChunks.map((c) => c.content),
|
|
133
|
+
reusableVectors,
|
|
134
|
+
);
|
|
135
|
+
const addedVectors = addedChunks.map((c) => vectorFor.get(c.content) as Float32Array);
|
|
96
136
|
|
|
97
137
|
const chunks = [...keptChunks, ...addedChunks];
|
|
98
138
|
const vectors = [...keptVectors, ...addedVectors];
|
|
@@ -107,7 +147,7 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
107
147
|
|
|
108
148
|
return {
|
|
109
149
|
chunks,
|
|
110
|
-
bm25: buildBm25FromChunks(chunks),
|
|
150
|
+
bm25: baseBm25 ? patchBm25(baseBm25, removedDocs, addedChunks) : buildBm25FromChunks(chunks),
|
|
111
151
|
semantic: buildSemanticIndex(vectors),
|
|
112
152
|
};
|
|
113
153
|
}
|
package/src/index/sparse.ts
CHANGED
|
@@ -30,10 +30,22 @@ export function addChunkToBm25(bm25: BM25Index, chunk: Chunk): void {
|
|
|
30
30
|
bm25.addDocument(tokenize(enrichForBm25(chunk)));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const bm25 = new BM25Index();
|
|
33
|
+
function appendChunksToBm25(bm25: BM25Index, chunks: readonly Chunk[]): BM25Index {
|
|
35
34
|
for (const chunk of chunks) {
|
|
36
35
|
addChunkToBm25(bm25, chunk);
|
|
37
36
|
}
|
|
38
37
|
return bm25;
|
|
39
38
|
}
|
|
39
|
+
|
|
40
|
+
export function buildBm25FromChunks(chunks: readonly Chunk[]): BM25Index {
|
|
41
|
+
return appendChunksToBm25(new BM25Index(), chunks);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Drop docs `removedDocs` from `base` and append `added`; `base` is left untouched. */
|
|
45
|
+
export function patchBm25(
|
|
46
|
+
base: BM25Index,
|
|
47
|
+
removedDocs: ReadonlySet<number>,
|
|
48
|
+
added: readonly Chunk[],
|
|
49
|
+
): BM25Index {
|
|
50
|
+
return appendChunksToBm25(base.withoutDocuments(removedDocs), added);
|
|
51
|
+
}
|
package/src/installer/agents.ts
CHANGED
|
@@ -5,11 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import type { AgentId } from "../agents.ts";
|
|
6
6
|
import { INSTRUCTIONS_MARKDOWN } from "./search-policy.ts";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
return join(home, ".copilot", "hooks", "miru-search.json");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Shared Copilot / VS Code / Visual Studio config root (MCP, hooks, agents). */
|
|
8
|
+
/** Shared Copilot / VS Code / Visual Studio config root (MCP, agents). */
|
|
13
9
|
export function copilotHomeDir(home: string): string {
|
|
14
10
|
return join(home, ".copilot");
|
|
15
11
|
}
|
|
@@ -55,22 +51,15 @@ export function nativeSteSkillDir(home: string, vendor: NativeSkillVendor): stri
|
|
|
55
51
|
return skillDir(home, `.${vendor}`, "ste");
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
function kiroHooksPath(home: string): string {
|
|
59
|
-
return join(home, ".kiro", "settings", "hooks.json");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function windsurfHooksPath(home: string): string {
|
|
63
|
-
return join(home, ".codeium", "windsurf", "hooks.json");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
54
|
/** OpenCode config root (`$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`). */
|
|
67
55
|
export function opencodeConfigDir(home: string): string {
|
|
68
56
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
69
57
|
return xdg ? join(xdg, "opencode") : join(home, ".config", "opencode");
|
|
70
58
|
}
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
/** OpenCode's first-class global skill location for Miru code-search routing. */
|
|
61
|
+
export function opencodeMiruSkillPath(home: string): string {
|
|
62
|
+
return join(opencodeConfigDir(home), "skills", "miru-code-search", "SKILL.md");
|
|
74
63
|
}
|
|
75
64
|
|
|
76
65
|
export type InstallAction =
|
|
@@ -86,24 +75,24 @@ export type InstallMode = "install" | "uninstall";
|
|
|
86
75
|
|
|
87
76
|
export const MIRU_START = "<!-- miru:start -->";
|
|
88
77
|
export const MIRU_END = "<!-- miru:end -->";
|
|
78
|
+
export const MIRU_BUNX_PACKAGE = "@takara-ai/miru-code@latest";
|
|
89
79
|
|
|
90
80
|
const HOME = homedir();
|
|
91
81
|
|
|
92
82
|
const STDIO_SERVER_CONFIG: Record<string, unknown> = {
|
|
93
83
|
command: "bunx",
|
|
94
|
-
args: ["
|
|
84
|
+
args: [MIRU_BUNX_PACKAGE, "mcp"],
|
|
95
85
|
type: "stdio",
|
|
96
86
|
};
|
|
97
87
|
|
|
98
88
|
const BARE_STDIO_SERVER_CONFIG: Record<string, unknown> = {
|
|
99
89
|
command: "bunx",
|
|
100
|
-
args: ["
|
|
90
|
+
args: [MIRU_BUNX_PACKAGE, "mcp"],
|
|
101
91
|
};
|
|
102
92
|
|
|
103
93
|
const OPENCODE_SERVER_CONFIG: Record<string, unknown> = {
|
|
104
|
-
command: ["bunx", "
|
|
94
|
+
command: ["bunx", MIRU_BUNX_PACKAGE, "mcp"],
|
|
105
95
|
type: "local",
|
|
106
|
-
enabled: true,
|
|
107
96
|
};
|
|
108
97
|
|
|
109
98
|
export const INSTRUCTIONS = `${MIRU_START}
|
|
@@ -121,7 +110,7 @@ export interface McpConfig {
|
|
|
121
110
|
format: McpConfigFormat;
|
|
122
111
|
}
|
|
123
112
|
|
|
124
|
-
export type
|
|
113
|
+
export type LegacyHooksFormat =
|
|
125
114
|
| "claude"
|
|
126
115
|
| "cursor"
|
|
127
116
|
| "gemini"
|
|
@@ -138,8 +127,9 @@ export interface AgentTarget {
|
|
|
138
127
|
mcp: McpConfig | null;
|
|
139
128
|
instructionsPath: string | null;
|
|
140
129
|
cursorRulesPath: string | null;
|
|
141
|
-
|
|
142
|
-
|
|
130
|
+
/** Search-hook paths are retained for uninstalling hooks created by older releases. */
|
|
131
|
+
legacyHooksPath: string | null;
|
|
132
|
+
legacyHooksFormat: LegacyHooksFormat | null;
|
|
143
133
|
subagentPath: string | null;
|
|
144
134
|
subagentId: AgentId | null;
|
|
145
135
|
/**
|
|
@@ -151,6 +141,8 @@ export interface AgentTarget {
|
|
|
151
141
|
* On-demand STE skill directory (`…/skills/ste/`), or null if unsupported.
|
|
152
142
|
*/
|
|
153
143
|
steSkillDir: string | null;
|
|
144
|
+
/** OpenCode's global Miru code-search skill, advertised on each model step. */
|
|
145
|
+
miruSkillPath?: string | null;
|
|
154
146
|
}
|
|
155
147
|
|
|
156
148
|
export function opencodeMcpPath(): string {
|
|
@@ -166,11 +158,11 @@ export function opencodeMcpPath(): string {
|
|
|
166
158
|
return jsonc;
|
|
167
159
|
}
|
|
168
160
|
|
|
169
|
-
export function vscodeMcpPath(): string {
|
|
170
|
-
if (
|
|
161
|
+
export function vscodeMcpPath(platform: NodeJS.Platform = process.platform): string {
|
|
162
|
+
if (platform === "darwin") {
|
|
171
163
|
return join(HOME, "Library", "Application Support", "Code", "User", "mcp.json");
|
|
172
164
|
}
|
|
173
|
-
if (
|
|
165
|
+
if (platform === "win32") {
|
|
174
166
|
const appData = process.env.APPDATA ?? HOME;
|
|
175
167
|
return join(appData, "Code", "User", "mcp.json");
|
|
176
168
|
}
|
|
@@ -184,23 +176,27 @@ export function visualStudioMcpPath(): string {
|
|
|
184
176
|
return join(profile, ".mcp.json");
|
|
185
177
|
}
|
|
186
178
|
|
|
187
|
-
export function visualStudioInstallDir(
|
|
188
|
-
|
|
179
|
+
export function visualStudioInstallDir(
|
|
180
|
+
platform: NodeJS.Platform = process.platform,
|
|
181
|
+
): string | null {
|
|
182
|
+
if (platform !== "win32") {
|
|
189
183
|
return null;
|
|
190
184
|
}
|
|
191
185
|
const programFiles = process.env.ProgramFiles ?? join("C:", "Program Files");
|
|
192
186
|
return join(programFiles, "Microsoft Visual Studio");
|
|
193
187
|
}
|
|
194
188
|
|
|
195
|
-
async function detectVisualStudio(): Promise<boolean> {
|
|
196
|
-
|
|
189
|
+
async function detectVisualStudio(dependencies: AgentDetectionDependencies = {}): Promise<boolean> {
|
|
190
|
+
const platform = dependencies.platform ?? process.platform;
|
|
191
|
+
if (platform !== "win32") {
|
|
197
192
|
return false;
|
|
198
193
|
}
|
|
199
194
|
|
|
200
|
-
const installDir = visualStudioInstallDir();
|
|
201
|
-
|
|
195
|
+
const installDir = visualStudioInstallDir(platform);
|
|
196
|
+
const pathExists = dependencies.existsSync ?? existsSync;
|
|
197
|
+
if (installDir && pathExists(installDir)) {
|
|
202
198
|
try {
|
|
203
|
-
const entries = await readdir(installDir);
|
|
199
|
+
const entries = await (dependencies.readdir ?? ((path: string) => readdir(path)))(installDir);
|
|
204
200
|
if (entries.length > 0) {
|
|
205
201
|
return true;
|
|
206
202
|
}
|
|
@@ -210,8 +206,9 @@ async function detectVisualStudio(): Promise<boolean> {
|
|
|
210
206
|
}
|
|
211
207
|
|
|
212
208
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
return await (dependencies.commandOnPath
|
|
210
|
+
? dependencies.commandOnPath("devenv")
|
|
211
|
+
: commandOnPath("devenv", platform, dependencies.spawn));
|
|
215
212
|
} catch {
|
|
216
213
|
return false;
|
|
217
214
|
}
|
|
@@ -233,8 +230,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
233
230
|
mcp: jsonMcp(join(HOME, ".claude.json"), "mcpServers", STDIO_SERVER_CONFIG),
|
|
234
231
|
instructionsPath: join(HOME, ".claude", "CLAUDE.md"),
|
|
235
232
|
cursorRulesPath: null,
|
|
236
|
-
|
|
237
|
-
|
|
233
|
+
legacyHooksPath: join(HOME, ".claude", "settings.json"),
|
|
234
|
+
legacyHooksFormat: "claude",
|
|
238
235
|
subagentPath: join(HOME, ".claude", "agents", "miru-code.md"),
|
|
239
236
|
subagentId: "claude",
|
|
240
237
|
cavemanSkillPath: nativeCavemanSkillPath(HOME, "claude"),
|
|
@@ -248,12 +245,13 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
248
245
|
mcp: jsonMcp(join(HOME, ".cursor", "mcp.json"), "mcpServers", STDIO_SERVER_CONFIG),
|
|
249
246
|
instructionsPath: null,
|
|
250
247
|
cursorRulesPath: join(HOME, ".cursor", "rules", "miru-code.mdc"),
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
legacyHooksPath: join(HOME, ".cursor", "hooks.json"),
|
|
249
|
+
legacyHooksFormat: "cursor",
|
|
253
250
|
subagentPath: join(HOME, ".cursor", "agents", "miru-code.md"),
|
|
254
251
|
subagentId: "cursor",
|
|
255
252
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
256
253
|
steSkillDir: SHARED_STE_SKILL_DIR,
|
|
254
|
+
miruSkillPath: opencodeMiruSkillPath(HOME),
|
|
257
255
|
},
|
|
258
256
|
{
|
|
259
257
|
id: "gemini",
|
|
@@ -263,8 +261,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
263
261
|
mcp: jsonMcp(join(HOME, ".gemini", "settings.json"), "mcpServers", STDIO_SERVER_CONFIG),
|
|
264
262
|
instructionsPath: join(HOME, ".gemini", "GEMINI.md"),
|
|
265
263
|
cursorRulesPath: null,
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
legacyHooksPath: join(HOME, ".gemini", "settings.json"),
|
|
265
|
+
legacyHooksFormat: "gemini",
|
|
268
266
|
subagentPath: join(HOME, ".gemini", "agents", "miru-code.md"),
|
|
269
267
|
subagentId: "gemini",
|
|
270
268
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -278,8 +276,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
278
276
|
mcp: jsonMcp(join(HOME, ".kiro", "settings", "mcp.json"), "mcpServers", STDIO_SERVER_CONFIG),
|
|
279
277
|
instructionsPath: join(HOME, ".kiro", "steering", "miru.md"),
|
|
280
278
|
cursorRulesPath: null,
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
legacyHooksPath: join(HOME, ".kiro", "settings", "hooks.json"),
|
|
280
|
+
legacyHooksFormat: "kiro",
|
|
283
281
|
subagentPath: join(HOME, ".kiro", "agents", "miru-code.md"),
|
|
284
282
|
subagentId: "kiro",
|
|
285
283
|
cavemanSkillPath: nativeCavemanSkillPath(HOME, "kiro"),
|
|
@@ -290,11 +288,11 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
290
288
|
displayName: "OpenCode",
|
|
291
289
|
binary: "opencode",
|
|
292
290
|
configDir: opencodeConfigDir(HOME),
|
|
293
|
-
mcp: jsonMcp(opencodeMcpPath(), "mcp", OPENCODE_SERVER_CONFIG),
|
|
291
|
+
mcp: jsonMcp(opencodeMcpPath(), "mcp.servers", OPENCODE_SERVER_CONFIG),
|
|
294
292
|
instructionsPath: join(opencodeConfigDir(HOME), "AGENTS.md"),
|
|
295
293
|
cursorRulesPath: null,
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
legacyHooksPath: join(opencodeConfigDir(HOME), "plugins", "miru-search-guard.ts"),
|
|
295
|
+
legacyHooksFormat: "opencode",
|
|
298
296
|
subagentPath: join(opencodeConfigDir(HOME), "agents", "miru-code.md"),
|
|
299
297
|
subagentId: "opencode",
|
|
300
298
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -312,8 +310,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
312
310
|
),
|
|
313
311
|
instructionsPath: null,
|
|
314
312
|
cursorRulesPath: null,
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
legacyHooksPath: join(copilotHomeDir(HOME), "hooks", "miru-search.json"),
|
|
314
|
+
legacyHooksFormat: "vscode",
|
|
317
315
|
subagentPath: join(copilotHomeDir(HOME), "agents", "miru-code.agent.md"),
|
|
318
316
|
subagentId: "copilot",
|
|
319
317
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -333,8 +331,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
333
331
|
},
|
|
334
332
|
instructionsPath: join(HOME, ".codex", "AGENTS.md"),
|
|
335
333
|
cursorRulesPath: null,
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
legacyHooksPath: join(HOME, ".codex", "hooks.json"),
|
|
335
|
+
legacyHooksFormat: "claude",
|
|
338
336
|
subagentPath: null,
|
|
339
337
|
subagentId: null,
|
|
340
338
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -348,8 +346,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
348
346
|
mcp: jsonMcp(vscodeMcpPath(), "servers", STDIO_SERVER_CONFIG),
|
|
349
347
|
instructionsPath: null,
|
|
350
348
|
cursorRulesPath: null,
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
legacyHooksPath: join(copilotHomeDir(HOME), "hooks", "miru-search.json"),
|
|
350
|
+
legacyHooksFormat: "vscode",
|
|
353
351
|
subagentPath: null,
|
|
354
352
|
subagentId: null,
|
|
355
353
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -363,8 +361,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
363
361
|
mcp: null,
|
|
364
362
|
instructionsPath: null,
|
|
365
363
|
cursorRulesPath: null,
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
legacyHooksPath: join(HOME, ".codeium", "windsurf", "hooks.json"),
|
|
365
|
+
legacyHooksFormat: "windsurf",
|
|
368
366
|
subagentPath: null,
|
|
369
367
|
subagentId: null,
|
|
370
368
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -378,8 +376,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
378
376
|
mcp: jsonMcp(visualStudioMcpPath(), "servers", STDIO_SERVER_CONFIG),
|
|
379
377
|
instructionsPath: null,
|
|
380
378
|
cursorRulesPath: null,
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
legacyHooksPath: join(copilotHomeDir(HOME), "hooks", "miru-search.json"),
|
|
380
|
+
legacyHooksFormat: "vscode",
|
|
383
381
|
subagentPath: null,
|
|
384
382
|
subagentId: null,
|
|
385
383
|
cavemanSkillPath: SHARED_CAVEMAN_SKILL,
|
|
@@ -387,38 +385,60 @@ export const AGENT_TARGETS: AgentTarget[] = [
|
|
|
387
385
|
},
|
|
388
386
|
];
|
|
389
387
|
|
|
390
|
-
async function commandOnPath(
|
|
391
|
-
|
|
388
|
+
async function commandOnPath(
|
|
389
|
+
command: string,
|
|
390
|
+
platform: NodeJS.Platform = process.platform,
|
|
391
|
+
spawn: typeof Bun.spawn = Bun.spawn,
|
|
392
|
+
): Promise<boolean> {
|
|
393
|
+
const lookup = platform === "win32" ? ["where", command] : ["which", command];
|
|
392
394
|
try {
|
|
393
|
-
const proc =
|
|
395
|
+
const proc = spawn(lookup, { stdout: "pipe", stderr: "ignore" });
|
|
394
396
|
return (await proc.exited) === 0;
|
|
395
397
|
} catch {
|
|
396
398
|
return false;
|
|
397
399
|
}
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
export
|
|
402
|
+
export interface AgentDetectionDependencies {
|
|
403
|
+
platform?: NodeJS.Platform;
|
|
404
|
+
existsSync?: typeof existsSync;
|
|
405
|
+
readdir?: (path: string) => Promise<string[]>;
|
|
406
|
+
commandOnPath?: (command: string) => Promise<boolean>;
|
|
407
|
+
spawn?: typeof Bun.spawn;
|
|
408
|
+
home?: string;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function isAgentDetected(
|
|
412
|
+
agent: AgentTarget,
|
|
413
|
+
dependencies: AgentDetectionDependencies = {},
|
|
414
|
+
): Promise<boolean> {
|
|
401
415
|
if (agent.id === "visualstudio") {
|
|
402
|
-
return detectVisualStudio();
|
|
416
|
+
return detectVisualStudio(dependencies);
|
|
403
417
|
}
|
|
404
418
|
if (agent.id === "windsurf") {
|
|
405
|
-
if (agent.configDir && existsSync(agent.configDir)) {
|
|
419
|
+
if (agent.configDir && (dependencies.existsSync ?? existsSync)(agent.configDir)) {
|
|
406
420
|
return true;
|
|
407
421
|
}
|
|
408
|
-
|
|
422
|
+
const commandExists = dependencies.commandOnPath
|
|
423
|
+
? dependencies.commandOnPath
|
|
424
|
+
: (command: string) => commandOnPath(command, dependencies.platform, dependencies.spawn);
|
|
425
|
+
if (agent.binary && (await commandExists(agent.binary))) {
|
|
409
426
|
return true;
|
|
410
427
|
}
|
|
411
428
|
return false;
|
|
412
429
|
}
|
|
413
430
|
// Copilot-specific artifacts only — ~/.copilot alone is shared with VS Code / VS.
|
|
414
431
|
if (agent.id === "copilot") {
|
|
415
|
-
return isCopilotInstalled(HOME);
|
|
432
|
+
return isCopilotInstalled(dependencies.home ?? HOME);
|
|
416
433
|
}
|
|
417
|
-
|
|
434
|
+
const commandExists = dependencies.commandOnPath
|
|
435
|
+
? dependencies.commandOnPath
|
|
436
|
+
: (command: string) => commandOnPath(command, dependencies.platform, dependencies.spawn);
|
|
437
|
+
if (agent.binary && (await commandExists(agent.binary))) {
|
|
418
438
|
return true;
|
|
419
439
|
}
|
|
420
440
|
if (agent.configDir) {
|
|
421
|
-
return
|
|
441
|
+
return (dependencies.existsSync ?? existsSync)(agent.configDir);
|
|
422
442
|
}
|
|
423
443
|
return false;
|
|
424
444
|
}
|