@takara-ai/miru-code 0.1.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/bin/miru.cjs +41 -0
  4. package/package.json +72 -0
  5. package/src/agents/claude.md +41 -0
  6. package/src/agents/copilot.md +41 -0
  7. package/src/agents/cursor.md +40 -0
  8. package/src/agents/gemini.md +43 -0
  9. package/src/agents/kiro.md +43 -0
  10. package/src/agents/opencode.md +44 -0
  11. package/src/agents.ts +30 -0
  12. package/src/cache.ts +141 -0
  13. package/src/chunking/chunking.ts +36 -0
  14. package/src/chunking/lines.ts +79 -0
  15. package/src/chunking/structural.ts +244 -0
  16. package/src/cli.ts +245 -0
  17. package/src/concurrency.ts +58 -0
  18. package/src/embeddings/openai.ts +442 -0
  19. package/src/env-files.ts +44 -0
  20. package/src/env.ts +50 -0
  21. package/src/git.ts +52 -0
  22. package/src/index/bm25-types.ts +18 -0
  23. package/src/index/bm25-worker.ts +48 -0
  24. package/src/index/bm25.ts +238 -0
  25. package/src/index/create.ts +195 -0
  26. package/src/index/dense.ts +105 -0
  27. package/src/index/file-walker.ts +113 -0
  28. package/src/index/files.ts +126 -0
  29. package/src/index/persistence.ts +100 -0
  30. package/src/index/quantize.ts +168 -0
  31. package/src/index/semantic-index.ts +11 -0
  32. package/src/index/sparse.ts +25 -0
  33. package/src/index/topk.ts +52 -0
  34. package/src/index/vector-storage.ts +27 -0
  35. package/src/index.ts +3 -0
  36. package/src/mcp/index-cache.ts +95 -0
  37. package/src/mcp/serve.ts +24 -0
  38. package/src/mcp/server.ts +100 -0
  39. package/src/miru-index.ts +269 -0
  40. package/src/ranking/boosting.ts +329 -0
  41. package/src/ranking/penalties.ts +110 -0
  42. package/src/ranking/weighting.ts +11 -0
  43. package/src/search.ts +180 -0
  44. package/src/tokens.ts +27 -0
  45. package/src/types.ts +47 -0
  46. package/src/utils.ts +64 -0
@@ -0,0 +1,110 @@
1
+ import { basename } from "node:path";
2
+ import type { Chunk } from "../types.ts";
3
+
4
+ const TEST_FILE_RE =
5
+ /(?:^|\/)(?:test_[^/]*\.py|[^/]*_test\.py|[^/]*_test\.go|[^/]*Tests?\.java|[^/]*Test\.php|[^/]*_spec\.rb|[^/]*_test\.rb|[^/]*\.test\.[jt]sx?|[^/]*\.spec\.[jt]sx?|[^/]*Tests?\.kt|[^/]*Spec\.kt|[^/]*Tests?\.swift|[^/]*Spec\.swift|[^/]*Tests?\.cs|test_[^/]*\.cpp|[^/]*_test\.cpp|test_[^/]*\.c|[^/]*_test\.c|[^/]*Spec\.scala|[^/]*Suite\.scala|[^/]*Test\.scala|[^/]*_test\.dart|test_[^/]*\.dart|[^/]*_spec\.lua|[^/]*_test\.lua|test_[^/]*\.lua|test_helpers?[^/]*\.\w+)$/;
6
+
7
+ const TEST_DIR_RE = /(?:^|\/)(?:tests?|__tests__|spec|testing)(?:\/|$)/;
8
+ const COMPAT_DIR_RE = /(?:^|\/)(?:compat|_compat|legacy)(?:\/|$)/;
9
+ const EXAMPLES_DIR_RE = /(?:^|\/)(?:_?examples?|docs?_src)(?:\/|$)/;
10
+ const TYPE_DEFS_RE = /\.d\.ts$/;
11
+
12
+ const STRONG_PENALTY = 0.3;
13
+ const MODERATE_PENALTY = 0.5;
14
+ const MILD_PENALTY = 0.7;
15
+
16
+ const REEXPORT_FILENAMES = new Set(["__init__.py", "package-info.java"]);
17
+ const FILE_SATURATION_THRESHOLD = 1;
18
+ const FILE_SATURATION_DECAY = 0.5;
19
+
20
+ function filePathPenalty(filePath: string): number {
21
+ const normalised = filePath.replace(/\\/g, "/");
22
+ let penalty = 1.0;
23
+ if (TEST_FILE_RE.test(normalised) || TEST_DIR_RE.test(normalised)) {
24
+ penalty *= STRONG_PENALTY;
25
+ }
26
+ if (REEXPORT_FILENAMES.has(basename(filePath))) {
27
+ penalty *= MODERATE_PENALTY;
28
+ }
29
+ if (COMPAT_DIR_RE.test(normalised)) {
30
+ penalty *= STRONG_PENALTY;
31
+ }
32
+ if (EXAMPLES_DIR_RE.test(normalised)) {
33
+ penalty *= STRONG_PENALTY;
34
+ }
35
+ if (TYPE_DEFS_RE.test(normalised)) {
36
+ penalty *= MILD_PENALTY;
37
+ }
38
+ return penalty;
39
+ }
40
+
41
+ export function rerankTopk(
42
+ scores: Map<string, number>,
43
+ chunksByKey: Map<string, Chunk>,
44
+ topK: number,
45
+ penalisePaths = true,
46
+ ): [Chunk, number][] {
47
+ if (scores.size === 0) {
48
+ return [];
49
+ }
50
+
51
+ const penaltyCache = new Map<string, number>();
52
+ const penalised = new Map<string, number>();
53
+
54
+ for (const [key, score] of scores) {
55
+ const chunk = chunksByKey.get(key);
56
+ if (!chunk) {
57
+ continue;
58
+ }
59
+ let pen = score;
60
+ if (penalisePaths) {
61
+ let mult = penaltyCache.get(chunk.file_path);
62
+ if (mult === undefined) {
63
+ mult = filePathPenalty(chunk.file_path);
64
+ penaltyCache.set(chunk.file_path, mult);
65
+ }
66
+ pen *= mult;
67
+ }
68
+ penalised.set(key, pen);
69
+ }
70
+
71
+ const ranked = [...penalised.entries()].sort((a, b) => b[1] - a[1]);
72
+
73
+ const fileSelected = new Map<string, number>();
74
+ const selected: { score: number; key: string }[] = [];
75
+ let minSelected = Infinity;
76
+
77
+ for (const [key, penScore] of ranked) {
78
+ const chunk = chunksByKey.get(key);
79
+ if (!chunk) {
80
+ continue;
81
+ }
82
+
83
+ if (selected.length >= topK && penScore <= minSelected) {
84
+ break;
85
+ }
86
+
87
+ const already = fileSelected.get(chunk.file_path) ?? 0;
88
+ let effScore = penScore;
89
+ if (already >= FILE_SATURATION_THRESHOLD) {
90
+ const excess = already - FILE_SATURATION_THRESHOLD + 1;
91
+ effScore *= FILE_SATURATION_DECAY ** excess;
92
+ }
93
+
94
+ selected.push({ score: effScore, key });
95
+ fileSelected.set(chunk.file_path, already + 1);
96
+
97
+ if (selected.length >= topK) {
98
+ minSelected = Math.min(...selected.map((s) => s.score));
99
+ }
100
+ }
101
+
102
+ selected.sort((a, b) => b.score - a.score);
103
+ return selected.slice(0, topK).flatMap(({ key, score }) => {
104
+ const chunk = chunksByKey.get(key);
105
+ if (!chunk) {
106
+ return [];
107
+ }
108
+ return [[chunk, score] as [Chunk, number]];
109
+ });
110
+ }
@@ -0,0 +1,11 @@
1
+ import { isSymbolQuery } from "./boosting.ts";
2
+
3
+ const ALPHA_SYMBOL = 0.3;
4
+ const ALPHA_NL = 0.5;
5
+
6
+ export function resolveAlpha(query: string, alpha: number | null | undefined): number {
7
+ if (alpha != null) {
8
+ return alpha;
9
+ }
10
+ return isSymbolQuery(query) ? ALPHA_SYMBOL : ALPHA_NL;
11
+ }
package/src/search.ts ADDED
@@ -0,0 +1,180 @@
1
+ import type { EmbeddingBackend } from "./embeddings/openai.ts";
2
+ import type { BM25Index } from "./index/bm25.ts";
3
+ import type { SemanticIndex } from "./index/semantic-index.ts";
4
+ import { selectorToMask } from "./index/sparse.ts";
5
+ import { selectTopKScoreIndices } from "./index/topk.ts";
6
+ import { applyQueryBoost, boostMultiChunkFiles } from "./ranking/boosting.ts";
7
+ import { rerankTopk } from "./ranking/penalties.ts";
8
+ import { resolveAlpha } from "./ranking/weighting.ts";
9
+ import { tokenize } from "./tokens.ts";
10
+ import type { Chunk, SearchResult } from "./types.ts";
11
+ import { chunkKey } from "./types.ts";
12
+
13
+ const RRF_K = 60;
14
+
15
+ function rrfScores(scores: Map<string, number>): Map<string, number> {
16
+ if (scores.size === 0) {
17
+ return scores;
18
+ }
19
+ const ranked = [...scores.entries()].sort((a, b) => b[1] - a[1]);
20
+ const out = new Map<string, number>();
21
+ ranked.forEach(([key], i) => {
22
+ out.set(key, 1.0 / (RRF_K + i + 1));
23
+ });
24
+ return out;
25
+ }
26
+
27
+ function sortTopK(arr: number[], topK: number): number[] {
28
+ return selectTopKScoreIndices(arr, topK);
29
+ }
30
+
31
+ function semanticFromQueryVector(
32
+ queryVec: Float32Array,
33
+ semanticIndex: SemanticIndex,
34
+ chunks: Chunk[],
35
+ topK: number,
36
+ selector?: number[],
37
+ ): SearchResult[] {
38
+ const { indices, distances } = semanticIndex.query(queryVec, topK, selector);
39
+ return indices.flatMap((index, i) => {
40
+ const chunk = chunks[index];
41
+ if (!chunk) {
42
+ return [];
43
+ }
44
+ return [{ chunk, score: 1.0 - (distances[i] ?? 0) }];
45
+ });
46
+ }
47
+
48
+ async function searchBm25(
49
+ query: string,
50
+ bm25Index: BM25Index,
51
+ chunks: Chunk[],
52
+ topK: number,
53
+ selector?: number[],
54
+ ): Promise<SearchResult[]> {
55
+ const tokens = tokenize(query);
56
+ if (tokens.length === 0) {
57
+ return [];
58
+ }
59
+ const mask = selectorToMask(selector, chunks.length);
60
+ const scores = await bm25Index.getScoresAsync(tokens, mask);
61
+ const indices = sortTopK(scores, topK);
62
+ return indices.flatMap((i) => {
63
+ const score = scores[i];
64
+ const chunk = chunks[i];
65
+ if (score === undefined || chunk === undefined || score <= 0) {
66
+ return [];
67
+ }
68
+ return [{ chunk, score }];
69
+ });
70
+ }
71
+
72
+ export async function hybridSearch(options: {
73
+ query: string;
74
+ embeddings: EmbeddingBackend;
75
+ semanticIndex: SemanticIndex;
76
+ bm25Index: BM25Index;
77
+ chunks: Chunk[];
78
+ topK: number;
79
+ alpha?: number | null;
80
+ selector?: number[];
81
+ rerank?: boolean;
82
+ }): Promise<SearchResult[]> {
83
+ const {
84
+ query,
85
+ embeddings,
86
+ semanticIndex,
87
+ bm25Index,
88
+ chunks,
89
+ topK,
90
+ alpha,
91
+ selector,
92
+ rerank = true,
93
+ } = options;
94
+
95
+ const alphaWeight = resolveAlpha(query, alpha);
96
+ const candidateCount = topK * 5;
97
+ const chunksByKey = new Map(chunks.map((c) => [chunkKey(c), c]));
98
+
99
+ const queryVecPromise = embeddings.embedQuery(query);
100
+ const bm25Promise = searchBm25(query, bm25Index, chunks, candidateCount, selector);
101
+ const queryVec = await queryVecPromise;
102
+ const bm25Hits = await bm25Promise;
103
+ const semantic = semanticFromQueryVector(
104
+ queryVec,
105
+ semanticIndex,
106
+ chunks,
107
+ candidateCount,
108
+ selector,
109
+ );
110
+
111
+ const semanticScores = new Map<string, number>();
112
+ for (const r of semantic) {
113
+ semanticScores.set(chunkKey(r.chunk), r.score);
114
+ }
115
+
116
+ const bm25Scores = new Map<string, number>();
117
+ for (const r of bm25Hits) {
118
+ if (r.score) {
119
+ bm25Scores.set(chunkKey(r.chunk), r.score);
120
+ }
121
+ }
122
+
123
+ const normalizedSemantic = rrfScores(semanticScores);
124
+ const normalizedBm25 = rrfScores(bm25Scores);
125
+
126
+ const allKeys = new Set([...normalizedSemantic.keys(), ...normalizedBm25.keys()]);
127
+ const sortedKeys = [...allKeys].sort((a, b) => {
128
+ const ca = chunksByKey.get(a);
129
+ const cb = chunksByKey.get(b);
130
+ if (!ca || !cb) {
131
+ return 0;
132
+ }
133
+ return ca.start_line - cb.start_line;
134
+ });
135
+
136
+ const combinedScores = new Map<string, number>();
137
+ for (const key of sortedKeys) {
138
+ combinedScores.set(
139
+ key,
140
+ alphaWeight * (normalizedSemantic.get(key) ?? 0) +
141
+ (1 - alphaWeight) * (normalizedBm25.get(key) ?? 0),
142
+ );
143
+ }
144
+
145
+ if (rerank) {
146
+ boostMultiChunkFiles(combinedScores, chunksByKey);
147
+ applyQueryBoost(combinedScores, query, chunks, chunksByKey);
148
+ const ranked = rerankTopk(combinedScores, chunksByKey, topK, alphaWeight < 1.0);
149
+ return ranked.map(([chunk, score]) => ({ chunk, score }));
150
+ }
151
+
152
+ return [...combinedScores.entries()]
153
+ .sort((a, b) => b[1] - a[1])
154
+ .slice(0, topK)
155
+ .flatMap(([key, score]) => {
156
+ const chunk = chunksByKey.get(key);
157
+ if (!chunk) {
158
+ return [];
159
+ }
160
+ return [{ chunk, score }];
161
+ });
162
+ }
163
+
164
+ export async function searchSemanticOnly(options: {
165
+ query: string;
166
+ embeddings: EmbeddingBackend;
167
+ semanticIndex: SemanticIndex;
168
+ chunks: Chunk[];
169
+ topK: number;
170
+ selector?: number[];
171
+ }): Promise<SearchResult[]> {
172
+ const queryVec = await options.embeddings.embedQuery(options.query);
173
+ return semanticFromQueryVector(
174
+ queryVec,
175
+ options.semanticIndex,
176
+ options.chunks,
177
+ options.topK,
178
+ options.selector,
179
+ );
180
+ }
package/src/tokens.ts ADDED
@@ -0,0 +1,27 @@
1
+ const TOKEN_RE = /[a-zA-Z_][a-zA-Z0-9_]*/g;
2
+ const CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+/g;
3
+
4
+ export function splitIdentifier(token: string): string[] {
5
+ const lower = token.toLowerCase();
6
+ let parts: string[];
7
+
8
+ if (token.includes("_")) {
9
+ parts = lower.split("_").filter(Boolean);
10
+ } else {
11
+ parts = (token.match(CAMEL_RE) ?? []).map((m) => m.toLowerCase());
12
+ }
13
+
14
+ if (parts.length >= 2) {
15
+ return [lower, ...parts];
16
+ }
17
+ return [lower];
18
+ }
19
+
20
+ export function tokenize(text: string): string[] {
21
+ const raw = text.match(TOKEN_RE) ?? [];
22
+ const result: string[] = [];
23
+ for (const tok of raw) {
24
+ result.push(...splitIdentifier(tok));
25
+ }
26
+ return result;
27
+ }
package/src/types.ts ADDED
@@ -0,0 +1,47 @@
1
+ export type ContentType = "code" | "docs" | "config";
2
+
3
+ export interface Chunk {
4
+ content: string;
5
+ file_path: string;
6
+ start_line: number;
7
+ end_line: number;
8
+ language: string | null;
9
+ }
10
+
11
+ export interface SearchResult {
12
+ chunk: Chunk;
13
+ score: number;
14
+ }
15
+
16
+ export function chunkKey(chunk: Chunk): string {
17
+ return `${chunk.file_path}:${chunk.start_line}:${chunk.end_line}`;
18
+ }
19
+
20
+ export function chunkToDict(chunk: Chunk): Record<string, unknown> {
21
+ return {
22
+ content: chunk.content,
23
+ file_path: chunk.file_path,
24
+ start_line: chunk.start_line,
25
+ end_line: chunk.end_line,
26
+ language: chunk.language,
27
+ location: `${chunk.file_path}:${chunk.start_line}-${chunk.end_line}`,
28
+ };
29
+ }
30
+
31
+ export function chunkFromDict(data: Record<string, unknown>): Chunk {
32
+ const { location: _loc, ...rest } = data;
33
+ return {
34
+ content: String(rest.content),
35
+ file_path: String(rest.file_path),
36
+ start_line: Number(rest.start_line),
37
+ end_line: Number(rest.end_line),
38
+ language: rest.language == null ? null : String(rest.language),
39
+ };
40
+ }
41
+
42
+ export function searchResultToDict(result: SearchResult): Record<string, unknown> {
43
+ return {
44
+ chunk: chunkToDict(result.chunk),
45
+ score: result.score,
46
+ };
47
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { resolve } from "node:path";
2
+ import type { Chunk, ContentType, SearchResult } from "./types.ts";
3
+ import { searchResultToDict } from "./types.ts";
4
+
5
+ const GIT_URL_SCHEMES = [
6
+ "https://",
7
+ "http://",
8
+ "ssh://",
9
+ "git://",
10
+ "git+ssh://",
11
+ "file://",
12
+ ] as const;
13
+ const SCP_GIT_URL_RE = /^[\w.-]+@[\w.-]+:(?!\/)/;
14
+
15
+ export function isGitUrl(path: string): boolean {
16
+ return GIT_URL_SCHEMES.some((scheme) => path.startsWith(scheme)) || SCP_GIT_URL_RE.test(path);
17
+ }
18
+
19
+ export function isAllowedRepoSource(repo: string): boolean {
20
+ if (!isGitUrl(repo)) {
21
+ return true;
22
+ }
23
+ return repo.startsWith("https://") || repo.startsWith("http://");
24
+ }
25
+
26
+ export function resolveChunk(chunks: Chunk[], filePath: string, line: number): Chunk | null {
27
+ let fallback: Chunk | null = null;
28
+ for (const chunk of chunks) {
29
+ if (chunk.file_path === filePath && chunk.start_line <= line && line <= chunk.end_line) {
30
+ if (line < chunk.end_line) {
31
+ return chunk;
32
+ }
33
+ if (fallback === null) {
34
+ fallback = chunk;
35
+ }
36
+ }
37
+ }
38
+ return fallback;
39
+ }
40
+
41
+ export function formatResults(
42
+ query: string,
43
+ results: SearchResult[],
44
+ ): { query: string; results: Record<string, unknown>[] } {
45
+ return {
46
+ query,
47
+ results: results.map(searchResultToDict),
48
+ };
49
+ }
50
+
51
+ export function resolveContent(raw: string[]): ContentType[] {
52
+ if (raw.includes("all")) {
53
+ return ["code", "docs", "config"];
54
+ }
55
+ const valid: ContentType[] = ["code", "docs", "config"];
56
+ return raw.filter((item): item is ContentType => valid.includes(item as ContentType));
57
+ }
58
+
59
+ export function computeSourceCacheKey(source: string, ref?: string | null): string {
60
+ if (isGitUrl(source)) {
61
+ return ref ? `${source}@${ref}` : source;
62
+ }
63
+ return resolve(source);
64
+ }