@takara-ai/miru-code 0.6.0 → 0.7.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 (40) hide show
  1. package/grammars/manifest.json +56 -0
  2. package/grammars/tree-sitter-bash.wasm +0 -0
  3. package/grammars/tree-sitter-c.wasm +0 -0
  4. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  5. package/grammars/tree-sitter-cpp.wasm +0 -0
  6. package/grammars/tree-sitter-css.wasm +0 -0
  7. package/grammars/tree-sitter-dart.wasm +0 -0
  8. package/grammars/tree-sitter-elixir.wasm +0 -0
  9. package/grammars/tree-sitter-embedded_template.wasm +0 -0
  10. package/grammars/tree-sitter-go.wasm +0 -0
  11. package/grammars/tree-sitter-haskell.wasm +0 -0
  12. package/grammars/tree-sitter-html.wasm +0 -0
  13. package/grammars/tree-sitter-java.wasm +0 -0
  14. package/grammars/tree-sitter-javascript.wasm +0 -0
  15. package/grammars/tree-sitter-json.wasm +0 -0
  16. package/grammars/tree-sitter-ocaml.wasm +0 -0
  17. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  18. package/grammars/tree-sitter-ocaml_type.wasm +0 -0
  19. package/grammars/tree-sitter-php.wasm +0 -0
  20. package/grammars/tree-sitter-php_only.wasm +0 -0
  21. package/grammars/tree-sitter-python.wasm +0 -0
  22. package/grammars/tree-sitter-ruby.wasm +0 -0
  23. package/grammars/tree-sitter-rust.wasm +0 -0
  24. package/grammars/tree-sitter-scala.wasm +0 -0
  25. package/grammars/tree-sitter-solidity.wasm +0 -0
  26. package/grammars/tree-sitter-tsx.wasm +0 -0
  27. package/grammars/tree-sitter-typescript.wasm +0 -0
  28. package/package.json +27 -2
  29. package/src/chunking/ast.ts +119 -0
  30. package/src/chunking/chunking.ts +7 -1
  31. package/src/chunking/grammars.ts +99 -0
  32. package/src/cli.ts +20 -1
  33. package/src/embeddings/openai.ts +6 -12
  34. package/src/git.ts +1 -2
  35. package/src/help.ts +1 -1
  36. package/src/index/create.ts +6 -9
  37. package/src/index/incremental.ts +1 -1
  38. package/src/index/vector-storage.ts +1 -3
  39. package/src/installer/installer.ts +12 -13
  40. package/src/version.ts +119 -0
@@ -0,0 +1,56 @@
1
+ {
2
+ "version": 1,
3
+ "runtime": "web-tree-sitter@^0.26.9",
4
+ "languages": {
5
+ "bash": "tree-sitter-bash.wasm",
6
+ "c": "tree-sitter-c.wasm",
7
+ "cpp": "tree-sitter-cpp.wasm",
8
+ "csharp": "tree-sitter-c_sharp.wasm",
9
+ "css": "tree-sitter-css.wasm",
10
+ "dart": "tree-sitter-dart.wasm",
11
+ "elixir": "tree-sitter-elixir.wasm",
12
+ "embeddedtemplate": "tree-sitter-embedded_template.wasm",
13
+ "go": "tree-sitter-go.wasm",
14
+ "haskell": "tree-sitter-haskell.wasm",
15
+ "html": "tree-sitter-html.wasm",
16
+ "java": "tree-sitter-java.wasm",
17
+ "javascript": "tree-sitter-javascript.wasm",
18
+ "json": "tree-sitter-json.wasm",
19
+ "ocaml": "tree-sitter-ocaml.wasm",
20
+ "php": "tree-sitter-php.wasm",
21
+ "python": "tree-sitter-python.wasm",
22
+ "ruby": "tree-sitter-ruby.wasm",
23
+ "rust": "tree-sitter-rust.wasm",
24
+ "scala": "tree-sitter-scala.wasm",
25
+ "solidity": "tree-sitter-solidity.wasm",
26
+ "typescript": "tree-sitter-typescript.wasm"
27
+ },
28
+ "files": [
29
+ "tree-sitter-bash.wasm",
30
+ "tree-sitter-c.wasm",
31
+ "tree-sitter-c_sharp.wasm",
32
+ "tree-sitter-cpp.wasm",
33
+ "tree-sitter-css.wasm",
34
+ "tree-sitter-dart.wasm",
35
+ "tree-sitter-elixir.wasm",
36
+ "tree-sitter-embedded_template.wasm",
37
+ "tree-sitter-go.wasm",
38
+ "tree-sitter-haskell.wasm",
39
+ "tree-sitter-html.wasm",
40
+ "tree-sitter-java.wasm",
41
+ "tree-sitter-javascript.wasm",
42
+ "tree-sitter-json.wasm",
43
+ "tree-sitter-ocaml.wasm",
44
+ "tree-sitter-ocaml_interface.wasm",
45
+ "tree-sitter-ocaml_type.wasm",
46
+ "tree-sitter-php.wasm",
47
+ "tree-sitter-php_only.wasm",
48
+ "tree-sitter-python.wasm",
49
+ "tree-sitter-ruby.wasm",
50
+ "tree-sitter-rust.wasm",
51
+ "tree-sitter-scala.wasm",
52
+ "tree-sitter-solidity.wasm",
53
+ "tree-sitter-tsx.wasm",
54
+ "tree-sitter-typescript.wasm"
55
+ ]
56
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "files": [
38
38
  "src",
39
+ "grammars",
39
40
  "README.md",
40
41
  "LICENSE"
41
42
  ],
@@ -49,7 +50,8 @@
49
50
  "typecheck": "tsc --noEmit",
50
51
  "lint": "biome check .",
51
52
  "lint:fix": "biome check --write .",
52
- "prepublishOnly": "bun run typecheck && bun test",
53
+ "vendor-grammars": "bun run scripts/vendor-grammars.ts",
54
+ "prepublishOnly": "bun run vendor-grammars && bun run typecheck && bun test",
53
55
  "benchmark:ab": "bun run scripts/search-ab-local.ts",
54
56
  "benchmark:ab-multi": "bun run scripts/search-ab-multi.ts",
55
57
  "benchmark:recall": "bun run scripts/benchmark-recall-tokens.ts",
@@ -61,11 +63,34 @@
61
63
  "dependencies": {
62
64
  "@modelcontextprotocol/sdk": "^1.29.0",
63
65
  "ignore": "^7.0.5",
66
+ "web-tree-sitter": "^0.26.9",
64
67
  "zod": "^4.4.3"
65
68
  },
66
69
  "devDependencies": {
67
70
  "@biomejs/biome": "^2.4.16",
68
71
  "@types/bun": "latest",
72
+ "tree-sitter-bash": "^0.25.1",
73
+ "tree-sitter-c": "^0.24.1",
74
+ "tree-sitter-c-sharp": "^0.23.1",
75
+ "tree-sitter-cpp": "^0.23.4",
76
+ "tree-sitter-css": "^0.25.0",
77
+ "tree-sitter-dart": "^1.0.0",
78
+ "tree-sitter-elixir": "^0.3.4",
79
+ "tree-sitter-embedded-template": "^0.25.0",
80
+ "tree-sitter-go": "^0.25.0",
81
+ "tree-sitter-haskell": "^0.23.1",
82
+ "tree-sitter-html": "^0.23.2",
83
+ "tree-sitter-java": "^0.23.5",
84
+ "tree-sitter-javascript": "^0.25.0",
85
+ "tree-sitter-json": "^0.24.8",
86
+ "tree-sitter-ocaml": "^0.24.2",
87
+ "tree-sitter-php": "^0.23.12",
88
+ "tree-sitter-python": "^0.25.0",
89
+ "tree-sitter-ruby": "^0.23.1",
90
+ "tree-sitter-rust": "^0.24.0",
91
+ "tree-sitter-scala": "^0.24.0",
92
+ "tree-sitter-solidity": "^1.2.13",
93
+ "tree-sitter-typescript": "^0.23.2",
69
94
  "typescript": "^5.9.3"
70
95
  }
71
96
  }
@@ -0,0 +1,119 @@
1
+ import type { Node } from "web-tree-sitter";
2
+ import { Parser } from "web-tree-sitter";
3
+ import { getLanguageForFile } from "./grammars.ts";
4
+ import { type ChunkBoundary, mergeAdjacentChunks } from "./lines.ts";
5
+
6
+ const RECURSION_DEPTH = 500;
7
+ const MIN_CHUNK_SIZE = 50;
8
+
9
+ function astChunkingEnabled(): boolean {
10
+ return process.env.MIRU_AST_CHUNKING !== "0";
11
+ }
12
+
13
+ function mergeNodeInner(node: Node, desiredLength: number, depth: number): ChunkBoundary[] {
14
+ if (node.childCount === 0) {
15
+ return [{ start: node.startIndex, end: node.endIndex }];
16
+ }
17
+
18
+ const length = node.endIndex - node.startIndex;
19
+ if (depth > RECURSION_DEPTH) {
20
+ return [{ start: node.startIndex, end: node.endIndex }];
21
+ }
22
+ if (length < MIN_CHUNK_SIZE) {
23
+ return [{ start: node.startIndex, end: node.endIndex }];
24
+ }
25
+
26
+ const groups: ChunkBoundary[] = [];
27
+ const children = node.children;
28
+ let index = 0;
29
+
30
+ while (index < children.length) {
31
+ const child = children[index];
32
+ if (!child) {
33
+ break;
34
+ }
35
+
36
+ const start = child.startIndex;
37
+ let end = child.endIndex;
38
+ let groupLength = end - start;
39
+ index += 1;
40
+
41
+ if (groupLength > desiredLength) {
42
+ groups.push(...mergeNodeInner(child, desiredLength, depth + 1));
43
+ continue;
44
+ }
45
+
46
+ while (index < children.length) {
47
+ const nextChild = children[index];
48
+ if (!nextChild) {
49
+ break;
50
+ }
51
+ const childLength = nextChild.endIndex - nextChild.startIndex;
52
+ if (groupLength + childLength > desiredLength) {
53
+ break;
54
+ }
55
+ end = nextChild.endIndex;
56
+ groupLength += childLength;
57
+ index += 1;
58
+ }
59
+
60
+ groups.push({ start, end });
61
+ }
62
+
63
+ return groups;
64
+ }
65
+
66
+ function mergeNode(node: Node, desiredLength: number): ChunkBoundary[] {
67
+ const rawChunks = mergeNodeInner(node, desiredLength, 0);
68
+ return mergeAdjacentChunks(rawChunks, desiredLength);
69
+ }
70
+
71
+ function byteBoundariesToCharBoundaries(
72
+ source: string,
73
+ byteBoundaries: ChunkBoundary[],
74
+ ): ChunkBoundary[] {
75
+ const sourceBytes = new TextEncoder().encode(source);
76
+ const decoder = new TextDecoder();
77
+
78
+ return byteBoundaries.map((boundary) => ({
79
+ start: decoder.decode(sourceBytes.subarray(0, boundary.start)).length,
80
+ end: decoder.decode(sourceBytes.subarray(0, boundary.end)).length,
81
+ }));
82
+ }
83
+
84
+ /** AST-aware chunk boundaries via vendored tree-sitter grammars; null to fall back. */
85
+ export async function chunkAst(
86
+ source: string,
87
+ filePath: string,
88
+ language: string | null,
89
+ desiredLength: number,
90
+ ): Promise<ChunkBoundary[] | null> {
91
+ if (!source.trim() || !astChunkingEnabled()) {
92
+ return null;
93
+ }
94
+
95
+ const languageObj = await getLanguageForFile(filePath, language);
96
+ if (!languageObj) {
97
+ return null;
98
+ }
99
+
100
+ const parser = new Parser();
101
+ parser.setLanguage(languageObj);
102
+
103
+ let tree: ReturnType<Parser["parse"]>;
104
+ try {
105
+ tree = parser.parse(source);
106
+ } catch {
107
+ return null;
108
+ }
109
+ if (!tree) {
110
+ return null;
111
+ }
112
+
113
+ try {
114
+ const byteBoundaries = mergeNode(tree.rootNode, desiredLength);
115
+ return byteBoundariesToCharBoundaries(source, byteBoundaries);
116
+ } finally {
117
+ tree.delete();
118
+ }
119
+ }
@@ -1,16 +1,22 @@
1
1
  import type { Chunk } from "../types.ts";
2
+ import { chunkAst } from "./ast.ts";
2
3
  import { chunkLines } from "./lines.ts";
3
4
  import { chunkStructural } from "./structural.ts";
4
5
 
5
6
  /** Match upstream hybrid-search chunking target length. */
6
7
  const DESIRED_CHUNK_LENGTH_CHARS = 1500;
7
8
 
8
- export function chunkSource(source: string, filePath: string, language: string | null): Chunk[] {
9
+ export async function chunkSource(
10
+ source: string,
11
+ filePath: string,
12
+ language: string | null,
13
+ ): Promise<Chunk[]> {
9
14
  if (!source.trim()) {
10
15
  return [];
11
16
  }
12
17
 
13
18
  const boundaries =
19
+ (await chunkAst(source, filePath, language, DESIRED_CHUNK_LENGTH_CHARS)) ??
14
20
  chunkStructural(source, language, DESIRED_CHUNK_LENGTH_CHARS) ??
15
21
  chunkLines(source, DESIRED_CHUNK_LENGTH_CHARS);
16
22
  const chunks: Chunk[] = [];
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Language, Parser } from "web-tree-sitter";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
9
+
10
+ export type GrammarManifest = {
11
+ version: number;
12
+ runtime: string;
13
+ languages: Record<string, string>;
14
+ files: string[];
15
+ };
16
+
17
+ export const grammarManifest: GrammarManifest = JSON.parse(
18
+ readFileSync(join(packageRoot, "grammars/manifest.json"), "utf8"),
19
+ ) as GrammarManifest;
20
+
21
+ /** Absolute path to the vendored grammars directory. */
22
+ export function grammarsDir(): string {
23
+ return join(packageRoot, "grammars");
24
+ }
25
+
26
+ /** Resolve vendored wasm path for a Miru language id, if we ship a grammar for it. */
27
+ export function wasmPathForLanguage(language: string | null): string | null {
28
+ if (!language) {
29
+ return null;
30
+ }
31
+ const file = grammarManifest.languages[language];
32
+ if (!file) {
33
+ return null;
34
+ }
35
+ return join(grammarsDir(), file);
36
+ }
37
+
38
+ /** Pick grammar wasm for a file (e.g. tsx vs typescript). */
39
+ export function wasmPathForFile(filePath: string, language: string | null): string | null {
40
+ if (!language) {
41
+ return null;
42
+ }
43
+ if (language === "typescript" && filePath.toLowerCase().endsWith(".tsx")) {
44
+ const tsxPath = join(grammarsDir(), "tree-sitter-tsx.wasm");
45
+ if (existsSync(tsxPath)) {
46
+ return tsxPath;
47
+ }
48
+ }
49
+ return wasmPathForLanguage(language);
50
+ }
51
+
52
+ /** Absolute path to web-tree-sitter's runtime wasm (0.26.x). */
53
+ export function webTreeSitterRuntimePath(): string {
54
+ const pkgDir = dirname(require.resolve("web-tree-sitter/package.json"));
55
+ return join(pkgDir, "web-tree-sitter.wasm");
56
+ }
57
+
58
+ export function hasVendoredGrammar(language: string | null): boolean {
59
+ return wasmPathForLanguage(language) !== null;
60
+ }
61
+
62
+ let parserInitPromise: Promise<void> | null = null;
63
+ const languageCache = new Map<string, Promise<Language>>();
64
+
65
+ /** Initialize web-tree-sitter once per process (safe to call repeatedly). */
66
+ export async function ensureParserInit(): Promise<void> {
67
+ if (!parserInitPromise) {
68
+ parserInitPromise = Parser.init({ locateFile: () => webTreeSitterRuntimePath() });
69
+ }
70
+ await parserInitPromise;
71
+ }
72
+
73
+ async function loadLanguage(wasmPath: string): Promise<Language> {
74
+ let pending = languageCache.get(wasmPath);
75
+ if (!pending) {
76
+ pending = (async () => {
77
+ await ensureParserInit();
78
+ return Language.load(wasmPath);
79
+ })();
80
+ languageCache.set(wasmPath, pending);
81
+ }
82
+ return pending;
83
+ }
84
+
85
+ /** Load a cached Language for the given file, or null if unsupported / missing. */
86
+ export async function getLanguageForFile(
87
+ filePath: string,
88
+ language: string | null,
89
+ ): Promise<Language | null> {
90
+ const wasmPath = wasmPathForFile(filePath, language);
91
+ if (!wasmPath || !existsSync(wasmPath)) {
92
+ return null;
93
+ }
94
+ try {
95
+ return await loadLanguage(wasmPath);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
package/src/cli.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  resolveContent,
37
37
  resolveSearchPath,
38
38
  } from "./utils.ts";
39
+ import { maybeNotifyUpdate, miruVersion } from "./version.ts";
39
40
 
40
41
  loadEnvFiles();
41
42
  normalizeTakaraApiKeyEnv();
@@ -54,6 +55,8 @@ const CLI_COMMANDS = new Set([
54
55
  "help",
55
56
  "-h",
56
57
  "--help",
58
+ "-v",
59
+ "--version",
57
60
  ]);
58
61
 
59
62
  const AGENTS = new Set<AgentId>(AGENT_IDS);
@@ -269,6 +272,11 @@ async function runCli(argv: string[]): Promise<void> {
269
272
  return;
270
273
  }
271
274
 
275
+ if (command === "-v" || command === "--version") {
276
+ console.log(miruVersion());
277
+ return;
278
+ }
279
+
272
280
  if (command === "help") {
273
281
  const topic = rest[0];
274
282
  if (!topic) {
@@ -444,13 +452,24 @@ async function runMcpWithCredentials(argv: string[]): Promise<void> {
444
452
  async function main(): Promise<void> {
445
453
  const argv = process.argv.slice(2);
446
454
  const first = argv[0];
455
+
456
+ if (first === "-v" || first === "--version") {
457
+ console.log(miruVersion());
458
+ return;
459
+ }
460
+
447
461
  if (first === "hook-guard") {
448
462
  process.exit(await runSearchGuardFromStdin());
449
463
  }
464
+
465
+ const updateNotice = maybeNotifyUpdate();
466
+
450
467
  if (first && CLI_COMMANDS.has(first)) {
451
- await runCli(argv);
468
+ await Promise.all([runCli(argv), updateNotice]);
452
469
  return;
453
470
  }
471
+
472
+ await updateNotice;
454
473
  await runMcpWithCredentials(argv);
455
474
  }
456
475
 
@@ -31,17 +31,11 @@ export interface EmbeddingBackend {
31
31
  }
32
32
 
33
33
  export function resolveEmbeddingModel(): string {
34
- return envFirstString(
35
- ["MIRU_OPENAI_EMBEDDING_MODEL", "OPENAI_EMBEDDING_MODEL"],
36
- DEFAULT_MODEL,
37
- );
34
+ return envFirstString(["MIRU_OPENAI_EMBEDDING_MODEL", "OPENAI_EMBEDDING_MODEL"], DEFAULT_MODEL);
38
35
  }
39
36
 
40
37
  export function resolveMaxEmbedChars(): number {
41
- return (
42
- envOptionalInt(["MIRU_MAX_EMBED_CHARS"], 256) ??
43
- DEFAULT_MAX_EMBED_CHARS
44
- );
38
+ return envOptionalInt(["MIRU_MAX_EMBED_CHARS"], 256) ?? DEFAULT_MAX_EMBED_CHARS;
45
39
  }
46
40
 
47
41
  function splitIntoWindows(text: string, maxChars: number): string[] {
@@ -123,10 +117,10 @@ export function resolveEmbeddingBatchSize(): number {
123
117
  }
124
118
 
125
119
  export function resolveEmbeddingBaseUrl(): string {
126
- return envFirstString(
127
- ["MIRU_OPENAI_BASE_URL", "OPENAI_BASE_URL"],
128
- DEFAULT_BASE_URL,
129
- ).replace(/\/$/, "");
120
+ return envFirstString(["MIRU_OPENAI_BASE_URL", "OPENAI_BASE_URL"], DEFAULT_BASE_URL).replace(
121
+ /\/$/,
122
+ "",
123
+ );
130
124
  }
131
125
 
132
126
  interface EmbeddingResponseItem {
package/src/git.ts CHANGED
@@ -6,8 +6,7 @@ import { envOptionalInt } from "./env.ts";
6
6
  const DEFAULT_CLONE_TIMEOUT_SEC = 60;
7
7
 
8
8
  export async function cloneGitRepository(url: string, ref?: string | null): Promise<string> {
9
- const timeoutSec =
10
- envOptionalInt(["MIRU_CLONE_TIMEOUT"], 1) ?? DEFAULT_CLONE_TIMEOUT_SEC;
9
+ const timeoutSec = envOptionalInt(["MIRU_CLONE_TIMEOUT"], 1) ?? DEFAULT_CLONE_TIMEOUT_SEC;
11
10
  const dir = await mkdtemp(join(tmpdir(), "miru-git-"));
12
11
  const args = ["clone", "--depth", "1"];
13
12
  if (ref) {
package/src/help.ts CHANGED
@@ -45,7 +45,7 @@ export function printMainHelp(): void {
45
45
  writeStdout(" miru setup && miru install");
46
46
  writeStdout(' miru search "auth middleware" ./src');
47
47
  writeStdout("");
48
- hint("miru help <command> · miru -h for environment variables");
48
+ hint("miru help <command> · miru -h for environment variables · miru -v for version");
49
49
  writeStdout("");
50
50
  }
51
51
 
@@ -1,5 +1,6 @@
1
1
  import { relative, resolve } from "node:path";
2
2
  import { chunkSource } from "../chunking/chunking.ts";
3
+ import { ensureParserInit } from "../chunking/grammars.ts";
3
4
  import { resolveWorkerConcurrency } from "../concurrency.ts";
4
5
  import type { EmbeddingBackend } from "../embeddings/openai.ts";
5
6
  import { envOptionalInt } from "../env.ts";
@@ -18,17 +19,11 @@ const DEFAULT_PIPELINE_EMBED_BATCH = 64;
18
19
  const DEFAULT_PIPELINE_EMBED_INFLIGHT = 8;
19
20
 
20
21
  function resolvePipelineEmbedBatch(): number {
21
- return (
22
- envOptionalInt(["MIRU_PIPELINE_EMBED_BATCH"], 1) ??
23
- DEFAULT_PIPELINE_EMBED_BATCH
24
- );
22
+ return envOptionalInt(["MIRU_PIPELINE_EMBED_BATCH"], 1) ?? DEFAULT_PIPELINE_EMBED_BATCH;
25
23
  }
26
24
 
27
25
  function resolvePipelineEmbedInflight(): number {
28
- return (
29
- envOptionalInt(["MIRU_PIPELINE_EMBED_INFLIGHT"], 1) ??
30
- DEFAULT_PIPELINE_EMBED_INFLIGHT
31
- );
26
+ return envOptionalInt(["MIRU_PIPELINE_EMBED_INFLIGHT"], 1) ?? DEFAULT_PIPELINE_EMBED_INFLIGHT;
32
27
  }
33
28
 
34
29
  function resolveMaxIndexFiles(): number | undefined {
@@ -54,6 +49,8 @@ export async function createIndexFromPath(
54
49
  embeddings.resetStats();
55
50
  }
56
51
 
52
+ await ensureParserInit();
53
+
57
54
  const resolved = resolve(path);
58
55
  const root = displayRoot ? resolve(displayRoot) : resolved;
59
56
  const extensions = getExtensions(content);
@@ -80,7 +77,7 @@ export async function createIndexFromPath(
80
77
  }
81
78
  const source = await readFileText(filePath);
82
79
  const chunkPath = displayRoot ? relative(root, filePath).replace(/\\/g, "/") : filePath;
83
- return chunkSource(source, chunkPath, language);
80
+ return await chunkSource(source, chunkPath, language);
84
81
  } catch {
85
82
  fileErrors++;
86
83
  return [];
@@ -24,7 +24,7 @@ async function chunksForFile(root: string, relativePath: string): Promise<Chunk[
24
24
  }
25
25
  const source = await readFileText(absolute);
26
26
  const language = detectLanguage(absolute);
27
- return chunkSource(source, rel, language);
27
+ return await chunkSource(source, rel, language);
28
28
  } catch {
29
29
  return [];
30
30
  }
@@ -6,9 +6,7 @@ export type SemanticStorage = "int8" | "float32";
6
6
 
7
7
  /** Default: int8 quantized vectors (~4x less RAM). Set MIRU_FLOAT_VECTORS=1 for float32. */
8
8
  export function resolveSemanticStorage(): SemanticStorage {
9
- return process.env.MIRU_FLOAT_VECTORS === "1"
10
- ? "float32"
11
- : "int8";
9
+ return process.env.MIRU_FLOAT_VECTORS === "1" ? "float32" : "int8";
12
10
  }
13
11
 
14
12
  export function semanticStorageFromMetadata(metadata: Record<string, unknown>): SemanticStorage {
@@ -34,6 +34,8 @@ interface Integration {
34
34
  id: IntegrationId;
35
35
  label: string;
36
36
  description: string;
37
+ experimental?: boolean;
38
+ defaultChecked?: boolean;
37
39
  planPath: (agent: AgentTarget) => string | null;
38
40
  apply: (agent: AgentTarget, mode: InstallMode) => Promise<WriteResult | null>;
39
41
  }
@@ -152,35 +154,37 @@ const INTEGRATIONS: Integration[] = [
152
154
  {
153
155
  id: "mcp",
154
156
  label: "MCP server",
155
- description: "lets the agent call miru directly as a tool",
157
+ description: "Miru search tools via MCP",
156
158
  planPath: (agent) => agent.mcp?.path ?? null,
157
159
  apply: applyMcp,
158
160
  },
159
161
  {
160
162
  id: "instructions",
161
163
  label: "Instructions",
162
- description: "adds CLI usage guidance to CLAUDE.md / AGENTS.md",
164
+ description: "search policy in agent docs",
163
165
  planPath: (agent) => agent.instructionsPath,
164
166
  apply: applyInstructions,
165
167
  },
166
168
  {
167
169
  id: "subagent",
168
170
  label: "Sub-agent",
169
- description: "installs a dedicated miru-code sub-agent",
171
+ description: "miru-code sub-agent file",
170
172
  planPath: (agent) => agent.subagentPath,
171
173
  apply: applySubagent,
172
174
  },
173
175
  {
174
176
  id: "rules",
175
177
  label: "Cursor rules",
176
- description: "always-on .cursor/rules policy for code search",
178
+ description: "search policy in .cursor/rules",
177
179
  planPath: (agent) => agent.cursorRulesPath,
178
180
  apply: applyCursorRules,
179
181
  },
180
182
  {
181
183
  id: "hooks",
182
184
  label: "Search hooks",
183
- description: "blocks Grep/Glob and redirects agents to Miru MCP",
185
+ description: "blocks built-in search; routes to Miru MCP",
186
+ experimental: true,
187
+ defaultChecked: false,
184
188
  planPath: (agent) => agent.hooksPath,
185
189
  apply: applyHooks,
186
190
  },
@@ -269,9 +273,9 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
269
273
  }
270
274
 
271
275
  const integrationItems = INTEGRATIONS.map((integration) => ({
272
- label: `${integration.label} — ${integration.description}`,
276
+ label: `${integration.label}${integration.experimental ? dim(" (experimental)") : ""} — ${integration.description}`,
273
277
  value: integration,
274
- checked: true,
278
+ checked: install ? (integration.defaultChecked ?? true) : true,
275
279
  }));
276
280
 
277
281
  const chosenIntegrations = await promptMultiSelect(
@@ -294,12 +298,7 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
294
298
 
295
299
  await apply(mode, chosenAgents, chosenIntegrations);
296
300
 
297
- success(
298
- install ? "Done! Restart your agents to pick up changes." : "Done! Configuration removed.",
299
- );
300
- if (install) {
301
- hint("Restart agents after install. Hooks block built-in search in favor of Miru MCP.");
302
- }
301
+ success(install ? "Done. Restart agents to apply changes." : "Done. Configuration removed.");
303
302
  writeStdout("");
304
303
  }
305
304
 
package/src/version.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import packageJson from "../package.json";
4
+ import { resolveCacheFolder } from "./cache.ts";
5
+ import { hint, writeStderr } from "./cli-ui.ts";
6
+
7
+ const PACKAGE_NAME = "@takara-ai/miru-code";
8
+ const REGISTRY_URL = "https://registry.npmjs.org/@takara-ai%2Fmiru-code";
9
+ const UPDATE_CHECK_FILENAME = "update-check.json";
10
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
11
+ const FETCH_TIMEOUT_MS = 2_000;
12
+
13
+ interface UpdateCheckCache {
14
+ checkedAt: number;
15
+ latest: string;
16
+ }
17
+
18
+ export function miruVersion(): string {
19
+ return packageJson.version;
20
+ }
21
+
22
+ export function isVersionNewer(latest: string, current: string): boolean {
23
+ const parse = (v: string): [number, number, number] | null => {
24
+ const match = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
25
+ if (!match) {
26
+ return null;
27
+ }
28
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
29
+ };
30
+
31
+ const a = parse(latest);
32
+ const b = parse(current);
33
+ if (!a || !b) {
34
+ return latest !== current;
35
+ }
36
+
37
+ for (let i = 0; i < 3; i++) {
38
+ if (a[i]! > b[i]!) {
39
+ return true;
40
+ }
41
+ if (a[i]! < b[i]!) {
42
+ return false;
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+
48
+ function updateCheckPath(): string {
49
+ return join(resolveCacheFolder(), UPDATE_CHECK_FILENAME);
50
+ }
51
+
52
+ async function readUpdateCheckCache(): Promise<UpdateCheckCache | null> {
53
+ try {
54
+ const raw = await readFile(updateCheckPath(), "utf-8");
55
+ const parsed = JSON.parse(raw) as UpdateCheckCache;
56
+ if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string") {
57
+ return null;
58
+ }
59
+ return parsed;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ async function writeUpdateCheckCache(latest: string): Promise<void> {
66
+ const payload: UpdateCheckCache = { checkedAt: Date.now(), latest };
67
+ await writeFile(updateCheckPath(), `${JSON.stringify(payload)}\n`, "utf-8");
68
+ }
69
+
70
+ export async function fetchLatestPublishedVersion(): Promise<string> {
71
+ const response = await fetch(REGISTRY_URL, {
72
+ headers: { accept: "application/json" },
73
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
74
+ });
75
+ if (!response.ok) {
76
+ throw new Error(`registry ${response.status}`);
77
+ }
78
+ const body = (await response.json()) as { "dist-tags"?: { latest?: string } };
79
+ const latest = body["dist-tags"]?.latest;
80
+ if (!latest) {
81
+ throw new Error("missing dist-tags.latest");
82
+ }
83
+ return latest;
84
+ }
85
+
86
+ /** Print update hint to stderr when a newer package is on npm (at most once per day). */
87
+ export async function maybeNotifyUpdate(): Promise<void> {
88
+ if (process.env.MIRU_NO_UPDATE_CHECK === "1") {
89
+ return;
90
+ }
91
+
92
+ const current = miruVersion();
93
+ const cached = await readUpdateCheckCache();
94
+ if (cached && Date.now() - cached.checkedAt < CHECK_INTERVAL_MS) {
95
+ if (isVersionNewer(cached.latest, current)) {
96
+ writeUpdateNotice(cached.latest, current);
97
+ }
98
+ return;
99
+ }
100
+
101
+ try {
102
+ const latest = await fetchLatestPublishedVersion();
103
+ await writeUpdateCheckCache(latest);
104
+ if (isVersionNewer(latest, current)) {
105
+ writeUpdateNotice(latest, current);
106
+ }
107
+ } catch {
108
+ // Offline or registry unreachable — skip silently.
109
+ }
110
+ }
111
+
112
+ function writeUpdateNotice(latest: string, current: string): void {
113
+ writeStderr();
114
+ hint(
115
+ `Update available: ${latest} (you have ${current}). Run: bun add -g ${PACKAGE_NAME}`,
116
+ process.stderr,
117
+ );
118
+ writeStderr();
119
+ }