@takara-ai/miru-code 0.4.4 → 0.4.6

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 CHANGED
@@ -25,7 +25,7 @@ miru setup --key YOUR_TOKEN # non-interactive
25
25
  miru setup --clear # remove stored key
26
26
  ```
27
27
 
28
- MCP loads your key from `credentials.json` automatically. Set `TAKARA_API_KEY` in MCP config only to override.
28
+ MCP loads your key from `credentials.json` automatically — no env block needed in MCP config.
29
29
 
30
30
  ## Add to your IDE
31
31
 
@@ -39,7 +39,7 @@ Interactive — pick your agent and what to enable (MCP, instructions, sub-agent
39
39
  miru uninstall # remove miru config
40
40
  ```
41
41
 
42
- **Supported:** Cursor · Claude Code · Gemini CLI · Kiro · OpenCode · GitHub Copilot · Codex · VS Code
42
+ **Supported:** Cursor · Claude Code · Gemini CLI · Kiro · OpenCode · GitHub Copilot · Codex · VS Code · Visual Studio (Windows)
43
43
 
44
44
  | IDE | Config written by `miru install` |
45
45
  |-----|----------------------------------|
@@ -50,6 +50,7 @@ miru uninstall # remove miru config
50
50
  | OpenCode | `~/.config/opencode/opencode.json(c)` + agents dir |
51
51
  | GitHub Copilot | `~/.copilot/mcp-config.json` + `~/.copilot/agents/miru-code.agent.md` |
52
52
  | Codex / VS Code | MCP config only |
53
+ | Visual Studio | `%USERPROFILE%\.mcp.json` (Windows, Copilot Agent Mode) |
53
54
 
54
55
  **Team sub-agent in a repo** (optional):
55
56
 
@@ -132,12 +133,13 @@ See `.env.example` for more.
132
133
  {
133
134
  "miru": {
134
135
  "command": "miru",
135
- "args": [],
136
- "env": { "TAKARA_API_KEY": "your-token" }
136
+ "args": []
137
137
  }
138
138
  }
139
139
  ```
140
140
 
141
+ Run `miru setup` once so the server can load your key from `credentials.json`.
142
+
141
143
  Use `bunx` + `@takara-ai/miru-code` if `miru` is not global. Wrapper key varies by IDE (`mcpServers`, `servers`, or `mcp`).
142
144
 
143
145
  ## Developing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cache.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { rm } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
3
+ import { join } from "node:path";
4
4
  import { resolveEmbeddingDimensions, resolveEmbeddingModel } from "./embeddings/openai.ts";
5
5
  import {
6
6
  loadBm25,
@@ -12,6 +12,7 @@ import {
12
12
  import { resolveSemanticStorage, semanticStorageFromMetadata } from "./index/vector-storage.ts";
13
13
  import type { Chunk, ContentType } from "./types.ts";
14
14
  import { chunkFromDict } from "./types.ts";
15
+ import { computeSourceCacheKey } from "./utils.ts";
15
16
 
16
17
  export function resolveCacheFolder(): string {
17
18
  const name = "miru";
@@ -30,8 +31,8 @@ export function resolveCacheFolder(): string {
30
31
  return base;
31
32
  }
32
33
 
33
- export function findIndexCachePath(path: string): string {
34
- const normalized = resolve(path);
34
+ export function findIndexCachePath(path: string, ref?: string | null): string {
35
+ const normalized = computeSourceCacheKey(path, ref);
35
36
  const subdir = createHash("sha256").update(normalized, "utf-8").digest("hex");
36
37
  return join(resolveCacheFolder(), subdir, "index");
37
38
  }
@@ -5,7 +5,16 @@ import {
5
5
  splitLinesKeepEnds,
6
6
  } from "./lines.ts";
7
7
 
8
- const SUPPORTED_STRUCTURAL_LANGUAGES = new Set(["python", "go", "typescript", "javascript"]);
8
+ const SUPPORTED_STRUCTURAL_LANGUAGES = new Set([
9
+ "python",
10
+ "go",
11
+ "typescript",
12
+ "javascript",
13
+ "cpp",
14
+ "c",
15
+ ]);
16
+
17
+ const BRACE_FAMILY_LANGUAGES = new Set(["cpp", "c"]);
9
18
 
10
19
  export function chunkStructural(
11
20
  source: string,
@@ -164,9 +173,27 @@ function declPattern(language: string): RegExp {
164
173
  if (language === "go") {
165
174
  return /^\s*(func\b|type\b.*\b(struct|interface)\b)/;
166
175
  }
176
+ if (language === "cpp") {
177
+ return /^\s*(?:(?:template\s*<[^>]*>\s*)?(?:class|struct|namespace|enum(?:\s+class)?)\b|(?!(?:if|for|while|switch|catch|return|sizeof|static_assert|else|do)\b)(?:[\w:~<>,*&]+\s+)+\w+\s*\([^;{}]*\)\s*(?:const)?\s*(?:override)?(?:\s*=\s*0)?)/;
178
+ }
179
+ if (language === "c") {
180
+ return /^\s*(?:struct|enum|union|typedef)\b|(?!(?:if|for|while|switch|return|sizeof|else|do)\b)(?:[\w:*\s]+\s+)+\w+\s*\([^;{}]*\)/;
181
+ }
167
182
  return /^\s*(export\s+)?(async\s+function\b|function\b|class\b|interface\b|type\b|(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)(?:\s*:\s*[^=]+)?\s*=>)/;
168
183
  }
169
184
 
185
+ function isBraceFamilyTypeDecl(text: string, language: string): boolean {
186
+ if (language === "cpp") {
187
+ return /^\s*(?:(?:template\s*<[^>]*>\s*)?(?:class|struct|namespace|enum(?:\s+class)?)\b)/.test(
188
+ text,
189
+ );
190
+ }
191
+ if (language === "c") {
192
+ return /^\s*(?:struct|enum|union)\b/.test(text);
193
+ }
194
+ return false;
195
+ }
196
+
170
197
  function braceUnits(lines: LineGroup[], language: string): ChunkBoundary[] {
171
198
  const units: ChunkBoundary[] = [];
172
199
  const full = lines.map((l) => l.text).join("");
@@ -178,6 +205,13 @@ function braceUnits(lines: LineGroup[], language: string): ChunkBoundary[] {
178
205
  if (!re.test(text)) {
179
206
  continue;
180
207
  }
208
+ if (
209
+ BRACE_FAMILY_LANGUAGES.has(language) &&
210
+ lineIndent(text) > 0 &&
211
+ !isBraceFamilyTypeDecl(text, language)
212
+ ) {
213
+ continue;
214
+ }
181
215
  if (firstDeclStart === null) {
182
216
  firstDeclStart = lines[i]?.start;
183
217
  }
package/src/cli.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- import { resolve } from "node:path";
3
2
  import { type AgentId, writeAgentFile } from "./agents.ts";
4
3
  import { clearCache } from "./cache.ts";
5
4
  import {
@@ -12,8 +11,8 @@ import {
12
11
  success,
13
12
  } from "./cli-ui.ts";
14
13
  import { loadStoredCredentials } from "./credentials.ts";
15
- import { loadEnvFiles } from "./env-files.ts";
16
14
  import { normalizeTakaraApiKeyEnv } from "./env.ts";
15
+ import { loadEnvFiles } from "./env-files.ts";
17
16
  import {
18
17
  AGENT_IDS,
19
18
  formatUnknownAgent,
@@ -27,7 +26,7 @@ import { MiruIndex } from "./miru-index.ts";
27
26
  import { ensureCredentials, runClearCredentials, runSetup } from "./setup.ts";
28
27
  import { withSpinner } from "./spinner.ts";
29
28
  import type { ContentType, SearchResult } from "./types.ts";
30
- import { formatResults, resolveChunk, resolveContent } from "./utils.ts";
29
+ import { formatResults, resolveChunk, resolveContent, resolveSearchPath } from "./utils.ts";
31
30
 
32
31
  loadEnvFiles();
33
32
  normalizeTakaraApiKeyEnv();
@@ -283,7 +282,7 @@ async function runCli(argv: string[]): Promise<void> {
283
282
  }
284
283
 
285
284
  if (command === "clear") {
286
- const path = resolve(rest[0] ?? process.cwd());
285
+ const path = resolveSearchPath(rest[0] ?? process.cwd());
287
286
  await runClear(path);
288
287
  return;
289
288
  }
@@ -298,7 +297,7 @@ async function runCli(argv: string[]): Promise<void> {
298
297
  printCommandHelp("search");
299
298
  process.exit(1);
300
299
  }
301
- const path = resolve(sizedRest[1] ?? process.cwd());
300
+ const path = resolveSearchPath(sizedRest[1] ?? process.cwd());
302
301
  await runSearch(path, query, topK, content, jsonFlag);
303
302
  return;
304
303
  }
@@ -311,7 +310,7 @@ async function runCli(argv: string[]): Promise<void> {
311
310
  process.exit(1);
312
311
  }
313
312
  const line = Number(lineRaw);
314
- const path = resolve(sizedRest[2] ?? process.cwd());
313
+ const path = resolveSearchPath(sizedRest[2] ?? process.cwd());
315
314
  try {
316
315
  await runFindRelated(path, filePath, line, topK, content, jsonFlag);
317
316
  } catch (err) {
package/src/env.ts CHANGED
@@ -33,7 +33,8 @@ export function envOptionalInt(names: string[], min = 1): number | undefined {
33
33
 
34
34
  export const TAKARA_API_KEY_ENV = "TAKARA_API_KEY";
35
35
 
36
- const TAKARA_API_KEY_PLACEHOLDERS = new Set(["${TAKARA_API_KEY}", "$TAKARA_API_KEY"]);
36
+ const MCP_KEY_PLACEHOLDER = "$" + "{TAKARA_API_KEY}";
37
+ const TAKARA_API_KEY_PLACEHOLDERS = new Set([MCP_KEY_PLACEHOLDER, "$TAKARA_API_KEY"]);
37
38
 
38
39
  /** True when the value is a real key, not empty or an unexpanded MCP env placeholder. */
39
40
  export function isUsableTakaraApiKey(value: string | undefined): boolean {
@@ -21,11 +21,14 @@ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
21
21
  ".kt": "kotlin",
22
22
  ".kts": "kotlin",
23
23
  ".c": "c",
24
- ".h": "c",
24
+ ".h": "cpp",
25
25
  ".cpp": "cpp",
26
26
  ".cc": "cpp",
27
27
  ".cxx": "cpp",
28
28
  ".hpp": "cpp",
29
+ ".hh": "cpp",
30
+ ".hxx": "cpp",
31
+ ".h++": "cpp",
29
32
  ".cs": "csharp",
30
33
  ".rb": "ruby",
31
34
  ".php": "php",
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
5
  import type { AgentId } from "../agents.ts";
@@ -19,30 +20,21 @@ export const MIRU_END = "<!-- miru:end -->";
19
20
 
20
21
  const HOME = homedir();
21
22
 
22
- const TAKARA_KEY_PLACEHOLDER = "$" + "{TAKARA_API_KEY}";
23
-
24
- const MCP_ENV: Record<string, string> = {
25
- TAKARA_API_KEY: TAKARA_KEY_PLACEHOLDER,
26
- };
27
-
28
23
  const STDIO_SERVER_CONFIG: Record<string, unknown> = {
29
24
  command: "bunx",
30
25
  args: ["@takara-ai/miru-code"],
31
26
  type: "stdio",
32
- env: MCP_ENV,
33
27
  };
34
28
 
35
29
  const BARE_STDIO_SERVER_CONFIG: Record<string, unknown> = {
36
30
  command: "bunx",
37
31
  args: ["@takara-ai/miru-code"],
38
- env: MCP_ENV,
39
32
  };
40
33
 
41
34
  const OPENCODE_SERVER_CONFIG: Record<string, unknown> = {
42
35
  command: ["bunx", "@takara-ai/miru-code"],
43
36
  type: "local",
44
37
  enabled: true,
45
- environment: MCP_ENV,
46
38
  };
47
39
 
48
40
  export const INSTRUCTIONS = `${MIRU_START}
@@ -54,7 +46,7 @@ A \`miru\` MCP server is available with two tools:
54
46
 
55
47
  Always call \`search\` before using Grep, Glob, or Read to explore the codebase. Use Grep/Glob/Read only for exact path lookup, exhaustive literal matches, or when the returned chunk lacks enough context.
56
48
 
57
- Run \`miru setup\` once to store your API key — the MCP server loads it from \`credentials.json\`. Optionally set \`TAKARA_API_KEY\` in MCP config to override.
49
+ Run \`miru setup\` once to store your API key — the MCP server loads it from \`credentials.json\` automatically.
58
50
 
59
51
  Pass \`--content docs\` to search documentation, \`--content config\` for config files, or \`--content all\` for everything.
60
52
 
@@ -92,7 +84,7 @@ export interface McpConfig {
92
84
  }
93
85
 
94
86
  export interface AgentTarget {
95
- id: AgentId | "codex" | "vscode";
87
+ id: AgentId | "codex" | "vscode" | "visualstudio";
96
88
  displayName: string;
97
89
  binary: string | null;
98
90
  configDir: string | null;
@@ -128,6 +120,45 @@ export function vscodeMcpPath(): string {
128
120
  return join(xdg, "Code", "User", "mcp.json");
129
121
  }
130
122
 
123
+ /** Global MCP config for Visual Studio (GitHub Copilot Agent Mode). */
124
+ export function visualStudioMcpPath(): string {
125
+ const profile = process.env.USERPROFILE ?? HOME;
126
+ return join(profile, ".mcp.json");
127
+ }
128
+
129
+ export function visualStudioInstallDir(): string | null {
130
+ if (process.platform !== "win32") {
131
+ return null;
132
+ }
133
+ const programFiles = process.env.ProgramFiles ?? join("C:", "Program Files");
134
+ return join(programFiles, "Microsoft Visual Studio");
135
+ }
136
+
137
+ async function detectVisualStudio(): Promise<boolean> {
138
+ if (process.platform !== "win32") {
139
+ return false;
140
+ }
141
+
142
+ const installDir = visualStudioInstallDir();
143
+ if (installDir && existsSync(installDir)) {
144
+ try {
145
+ const entries = await readdir(installDir);
146
+ if (entries.length > 0) {
147
+ return true;
148
+ }
149
+ } catch {
150
+ // ignore unreadable install directory
151
+ }
152
+ }
153
+
154
+ try {
155
+ const proc = Bun.spawn(["where", "devenv"], { stdout: "pipe", stderr: "ignore" });
156
+ return (await proc.exited) === 0;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
131
162
  function jsonMcp(path: string, key: string, entry: Record<string, unknown>): McpConfig {
132
163
  return { path, key, memberKey: "miru", entry, format: "json" };
133
164
  }
@@ -171,7 +202,6 @@ export const AGENT_TARGETS: AgentTarget[] = [
171
202
  mcp: jsonMcp(join(HOME, ".kiro", "settings", "mcp.json"), "mcpServers", {
172
203
  ...STDIO_SERVER_CONFIG,
173
204
  env: {
174
- ...MCP_ENV,
175
205
  PATH: "/usr/local/bin:/usr/bin:/bin",
176
206
  },
177
207
  }),
@@ -225,19 +255,34 @@ export const AGENT_TARGETS: AgentTarget[] = [
225
255
  subagentPath: null,
226
256
  subagentId: null,
227
257
  },
258
+ {
259
+ id: "visualstudio",
260
+ displayName: process.platform === "win32" ? "Visual Studio" : "Visual Studio (Windows)",
261
+ binary: null,
262
+ configDir: visualStudioInstallDir(),
263
+ mcp: jsonMcp(visualStudioMcpPath(), "servers", STDIO_SERVER_CONFIG),
264
+ instructionsPath: null,
265
+ subagentPath: null,
266
+ subagentId: null,
267
+ },
228
268
  ];
229
269
 
270
+ async function commandOnPath(command: string): Promise<boolean> {
271
+ const lookup = process.platform === "win32" ? ["where", command] : ["which", command];
272
+ try {
273
+ const proc = Bun.spawn(lookup, { stdout: "pipe", stderr: "ignore" });
274
+ return (await proc.exited) === 0;
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
230
280
  export async function isAgentDetected(agent: AgentTarget): Promise<boolean> {
231
- if (agent.binary) {
232
- try {
233
- const proc = Bun.spawn(["which", agent.binary], { stdout: "pipe", stderr: "ignore" });
234
- const code = await proc.exited;
235
- if (code === 0) {
236
- return true;
237
- }
238
- } catch {
239
- // which unavailable
240
- }
281
+ if (agent.id === "visualstudio") {
282
+ return detectVisualStudio();
283
+ }
284
+ if (agent.binary && (await commandOnPath(agent.binary))) {
285
+ return true;
241
286
  }
242
287
  if (agent.configDir) {
243
288
  return Bun.file(agent.configDir).exists();
@@ -6,7 +6,6 @@ const CODEX_MCP_HEADER = "[mcp_servers.miru]";
6
6
  const CODEX_MCP_BLOCK = `[mcp_servers.miru]
7
7
  command = "bunx"
8
8
  args = ["@takara-ai/miru-code"]
9
- env = { TAKARA_API_KEY = "\${TAKARA_API_KEY}" }
10
9
  `;
11
10
 
12
11
  /** Strip line and block comments so JSONC configs can be parsed. */
package/src/utils.ts CHANGED
@@ -62,3 +62,8 @@ export function computeSourceCacheKey(source: string, ref?: string | null): stri
62
62
  }
63
63
  return resolve(source);
64
64
  }
65
+
66
+ /** Resolve a local search path; leave git URLs unchanged for remote indexing. */
67
+ export function resolveSearchPath(path: string): string {
68
+ return isGitUrl(path) ? path : resolve(path);
69
+ }