@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.
- package/grammars/manifest.json +56 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-css.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-embedded_template.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-haskell.wasm +0 -0
- package/grammars/tree-sitter-html.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-json.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
- package/grammars/tree-sitter-ocaml_type.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-php_only.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-scala.wasm +0 -0
- package/grammars/tree-sitter-solidity.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +27 -2
- package/src/chunking/ast.ts +119 -0
- package/src/chunking/chunking.ts +7 -1
- package/src/chunking/grammars.ts +99 -0
- package/src/cli.ts +20 -1
- package/src/embeddings/openai.ts +6 -12
- package/src/git.ts +1 -2
- package/src/help.ts +1 -1
- package/src/index/create.ts +6 -9
- package/src/index/incremental.ts +1 -1
- package/src/index/vector-storage.ts +1 -3
- package/src/installer/installer.ts +12 -13
- 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
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|
package/src/chunking/chunking.ts
CHANGED
|
@@ -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(
|
|
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
|
|
package/src/embeddings/openai.ts
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
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
|
|
package/src/index/create.ts
CHANGED
|
@@ -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 [];
|
package/src/index/incremental.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
+
}
|