@takara-ai/miru-code 0.6.1 → 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 (39) 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/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.1",
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 {
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
+ }