kiri-mcp-server 0.2.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 (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +399 -0
  3. package/config/default.example.yml +12 -0
  4. package/config/denylist.yml +15 -0
  5. package/config/scoring-profiles.yml +37 -0
  6. package/config/security.yml +10 -0
  7. package/dist/client/cli.js +68 -0
  8. package/dist/client/cli.js.map +1 -0
  9. package/dist/client/index.js +5 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/config/default.example.yml +12 -0
  12. package/dist/config/denylist.yml +15 -0
  13. package/dist/config/scoring-profiles.yml +37 -0
  14. package/dist/config/security.yml +10 -0
  15. package/dist/eval/metrics.js +47 -0
  16. package/dist/eval/metrics.js.map +1 -0
  17. package/dist/indexer/cli.js +362 -0
  18. package/dist/indexer/cli.js.map +1 -0
  19. package/dist/indexer/codeintel.js +182 -0
  20. package/dist/indexer/codeintel.js.map +1 -0
  21. package/dist/indexer/git.js +30 -0
  22. package/dist/indexer/git.js.map +1 -0
  23. package/dist/indexer/language.js +34 -0
  24. package/dist/indexer/language.js.map +1 -0
  25. package/dist/indexer/pipeline/filters/denylist.js +71 -0
  26. package/dist/indexer/pipeline/filters/denylist.js.map +1 -0
  27. package/dist/indexer/schema.js +101 -0
  28. package/dist/indexer/schema.js.map +1 -0
  29. package/dist/package.json +93 -0
  30. package/dist/server/bootstrap.js +19 -0
  31. package/dist/server/bootstrap.js.map +1 -0
  32. package/dist/server/context.js +2 -0
  33. package/dist/server/context.js.map +1 -0
  34. package/dist/server/fallbacks/degradeController.js +69 -0
  35. package/dist/server/fallbacks/degradeController.js.map +1 -0
  36. package/dist/server/handlers.js +800 -0
  37. package/dist/server/handlers.js.map +1 -0
  38. package/dist/server/main.js +151 -0
  39. package/dist/server/main.js.map +1 -0
  40. package/dist/server/observability/metrics.js +56 -0
  41. package/dist/server/observability/metrics.js.map +1 -0
  42. package/dist/server/observability/tracing.js +58 -0
  43. package/dist/server/observability/tracing.js.map +1 -0
  44. package/dist/server/rpc.js +477 -0
  45. package/dist/server/rpc.js.map +1 -0
  46. package/dist/server/runtime.js +47 -0
  47. package/dist/server/runtime.js.map +1 -0
  48. package/dist/server/scoring.js +111 -0
  49. package/dist/server/scoring.js.map +1 -0
  50. package/dist/server/stdio.js +76 -0
  51. package/dist/server/stdio.js.map +1 -0
  52. package/dist/shared/duckdb.js +121 -0
  53. package/dist/shared/duckdb.js.map +1 -0
  54. package/dist/shared/embedding.js +85 -0
  55. package/dist/shared/embedding.js.map +1 -0
  56. package/dist/shared/index.js +9 -0
  57. package/dist/shared/index.js.map +1 -0
  58. package/dist/shared/security/config.js +64 -0
  59. package/dist/shared/security/config.js.map +1 -0
  60. package/dist/shared/security/masker.js +56 -0
  61. package/dist/shared/security/masker.js.map +1 -0
  62. package/dist/shared/tokenizer.js +5 -0
  63. package/dist/shared/tokenizer.js.map +1 -0
  64. package/dist/shared/utils/simpleYaml.js +90 -0
  65. package/dist/shared/utils/simpleYaml.js.map +1 -0
  66. package/dist/sql/schema.sql +6 -0
  67. package/dist/src/client/cli.d.ts +3 -0
  68. package/dist/src/client/cli.d.ts.map +1 -0
  69. package/dist/src/client/cli.js +68 -0
  70. package/dist/src/client/cli.js.map +1 -0
  71. package/dist/src/client/index.d.ts +5 -0
  72. package/dist/src/client/index.d.ts.map +1 -0
  73. package/dist/src/client/index.js +5 -0
  74. package/dist/src/client/index.js.map +1 -0
  75. package/dist/src/client/proxy.d.ts +9 -0
  76. package/dist/src/client/proxy.d.ts.map +1 -0
  77. package/dist/src/client/proxy.js +198 -0
  78. package/dist/src/client/proxy.js.map +1 -0
  79. package/dist/src/client/start-daemon.d.ts +30 -0
  80. package/dist/src/client/start-daemon.d.ts.map +1 -0
  81. package/dist/src/client/start-daemon.js +175 -0
  82. package/dist/src/client/start-daemon.js.map +1 -0
  83. package/dist/src/daemon/daemon.d.ts +9 -0
  84. package/dist/src/daemon/daemon.d.ts.map +1 -0
  85. package/dist/src/daemon/daemon.js +149 -0
  86. package/dist/src/daemon/daemon.js.map +1 -0
  87. package/dist/src/daemon/lifecycle.d.ts +101 -0
  88. package/dist/src/daemon/lifecycle.d.ts.map +1 -0
  89. package/dist/src/daemon/lifecycle.js +266 -0
  90. package/dist/src/daemon/lifecycle.js.map +1 -0
  91. package/dist/src/daemon/socket.d.ts +26 -0
  92. package/dist/src/daemon/socket.d.ts.map +1 -0
  93. package/dist/src/daemon/socket.js +132 -0
  94. package/dist/src/daemon/socket.js.map +1 -0
  95. package/dist/src/eval/metrics.d.ts +23 -0
  96. package/dist/src/eval/metrics.d.ts.map +1 -0
  97. package/dist/src/eval/metrics.js +47 -0
  98. package/dist/src/eval/metrics.js.map +1 -0
  99. package/dist/src/index.d.ts +11 -0
  100. package/dist/src/index.d.ts.map +1 -0
  101. package/dist/src/index.js +11 -0
  102. package/dist/src/index.js.map +1 -0
  103. package/dist/src/indexer/cli.d.ts +9 -0
  104. package/dist/src/indexer/cli.d.ts.map +1 -0
  105. package/dist/src/indexer/cli.js +402 -0
  106. package/dist/src/indexer/cli.js.map +1 -0
  107. package/dist/src/indexer/codeintel.d.ts +28 -0
  108. package/dist/src/indexer/codeintel.d.ts.map +1 -0
  109. package/dist/src/indexer/codeintel.js +451 -0
  110. package/dist/src/indexer/codeintel.js.map +1 -0
  111. package/dist/src/indexer/git.d.ts +4 -0
  112. package/dist/src/indexer/git.d.ts.map +1 -0
  113. package/dist/src/indexer/git.js +30 -0
  114. package/dist/src/indexer/git.js.map +1 -0
  115. package/dist/src/indexer/language.d.ts +2 -0
  116. package/dist/src/indexer/language.d.ts.map +1 -0
  117. package/dist/src/indexer/language.js +34 -0
  118. package/dist/src/indexer/language.js.map +1 -0
  119. package/dist/src/indexer/pipeline/filters/denylist.d.ts +10 -0
  120. package/dist/src/indexer/pipeline/filters/denylist.d.ts.map +1 -0
  121. package/dist/src/indexer/pipeline/filters/denylist.js +71 -0
  122. package/dist/src/indexer/pipeline/filters/denylist.js.map +1 -0
  123. package/dist/src/indexer/schema.d.ts +9 -0
  124. package/dist/src/indexer/schema.d.ts.map +1 -0
  125. package/dist/src/indexer/schema.js +125 -0
  126. package/dist/src/indexer/schema.js.map +1 -0
  127. package/dist/src/indexer/watch.d.ts +97 -0
  128. package/dist/src/indexer/watch.d.ts.map +1 -0
  129. package/dist/src/indexer/watch.js +264 -0
  130. package/dist/src/indexer/watch.js.map +1 -0
  131. package/dist/src/server/bootstrap.d.ts +11 -0
  132. package/dist/src/server/bootstrap.d.ts.map +1 -0
  133. package/dist/src/server/bootstrap.js +19 -0
  134. package/dist/src/server/bootstrap.js.map +1 -0
  135. package/dist/src/server/context.d.ts +9 -0
  136. package/dist/src/server/context.d.ts.map +1 -0
  137. package/dist/src/server/context.js +2 -0
  138. package/dist/src/server/context.js.map +1 -0
  139. package/dist/src/server/fallbacks/degradeController.d.ts +24 -0
  140. package/dist/src/server/fallbacks/degradeController.d.ts.map +1 -0
  141. package/dist/src/server/fallbacks/degradeController.js +135 -0
  142. package/dist/src/server/fallbacks/degradeController.js.map +1 -0
  143. package/dist/src/server/handlers.d.ts +105 -0
  144. package/dist/src/server/handlers.d.ts.map +1 -0
  145. package/dist/src/server/handlers.js +954 -0
  146. package/dist/src/server/handlers.js.map +1 -0
  147. package/dist/src/server/indexBootstrap.d.ts +13 -0
  148. package/dist/src/server/indexBootstrap.d.ts.map +1 -0
  149. package/dist/src/server/indexBootstrap.js +109 -0
  150. package/dist/src/server/indexBootstrap.js.map +1 -0
  151. package/dist/src/server/main.d.ts +10 -0
  152. package/dist/src/server/main.d.ts.map +1 -0
  153. package/dist/src/server/main.js +217 -0
  154. package/dist/src/server/main.js.map +1 -0
  155. package/dist/src/server/observability/metrics.d.ts +35 -0
  156. package/dist/src/server/observability/metrics.d.ts.map +1 -0
  157. package/dist/src/server/observability/metrics.js +70 -0
  158. package/dist/src/server/observability/metrics.js.map +1 -0
  159. package/dist/src/server/observability/tracing.d.ts +3 -0
  160. package/dist/src/server/observability/tracing.d.ts.map +1 -0
  161. package/dist/src/server/observability/tracing.js +58 -0
  162. package/dist/src/server/observability/tracing.js.map +1 -0
  163. package/dist/src/server/rpc.d.ts +39 -0
  164. package/dist/src/server/rpc.d.ts.map +1 -0
  165. package/dist/src/server/rpc.js +551 -0
  166. package/dist/src/server/rpc.js.map +1 -0
  167. package/dist/src/server/runtime.d.ts +21 -0
  168. package/dist/src/server/runtime.d.ts.map +1 -0
  169. package/dist/src/server/runtime.js +59 -0
  170. package/dist/src/server/runtime.js.map +1 -0
  171. package/dist/src/server/scoring.d.ts +20 -0
  172. package/dist/src/server/scoring.d.ts.map +1 -0
  173. package/dist/src/server/scoring.js +112 -0
  174. package/dist/src/server/scoring.js.map +1 -0
  175. package/dist/src/server/stdio.d.ts +4 -0
  176. package/dist/src/server/stdio.d.ts.map +1 -0
  177. package/dist/src/server/stdio.js +88 -0
  178. package/dist/src/server/stdio.js.map +1 -0
  179. package/dist/src/shared/duckdb.d.ts +16 -0
  180. package/dist/src/shared/duckdb.d.ts.map +1 -0
  181. package/dist/src/shared/duckdb.js +121 -0
  182. package/dist/src/shared/duckdb.js.map +1 -0
  183. package/dist/src/shared/embedding.d.ts +19 -0
  184. package/dist/src/shared/embedding.d.ts.map +1 -0
  185. package/dist/src/shared/embedding.js +85 -0
  186. package/dist/src/shared/embedding.js.map +1 -0
  187. package/dist/src/shared/index.d.ts +3 -0
  188. package/dist/src/shared/index.d.ts.map +1 -0
  189. package/dist/src/shared/index.js +9 -0
  190. package/dist/src/shared/index.js.map +1 -0
  191. package/dist/src/shared/security/config.d.ts +23 -0
  192. package/dist/src/shared/security/config.d.ts.map +1 -0
  193. package/dist/src/shared/security/config.js +66 -0
  194. package/dist/src/shared/security/config.js.map +1 -0
  195. package/dist/src/shared/security/masker.d.ts +10 -0
  196. package/dist/src/shared/security/masker.d.ts.map +1 -0
  197. package/dist/src/shared/security/masker.js +56 -0
  198. package/dist/src/shared/security/masker.js.map +1 -0
  199. package/dist/src/shared/tokenizer.d.ts +2 -0
  200. package/dist/src/shared/tokenizer.d.ts.map +1 -0
  201. package/dist/src/shared/tokenizer.js +5 -0
  202. package/dist/src/shared/tokenizer.js.map +1 -0
  203. package/dist/src/shared/utils/lockfile.d.ts +46 -0
  204. package/dist/src/shared/utils/lockfile.d.ts.map +1 -0
  205. package/dist/src/shared/utils/lockfile.js +136 -0
  206. package/dist/src/shared/utils/lockfile.js.map +1 -0
  207. package/dist/src/shared/utils/simpleYaml.d.ts +6 -0
  208. package/dist/src/shared/utils/simpleYaml.d.ts.map +1 -0
  209. package/dist/src/shared/utils/simpleYaml.js +90 -0
  210. package/dist/src/shared/utils/simpleYaml.js.map +1 -0
  211. package/package.json +91 -0
  212. package/sql/schema.sql +6 -0
@@ -0,0 +1,800 @@
1
+ import path from "node:path";
2
+ import { generateEmbedding, structuralSimilarity } from "../shared/embedding.js";
3
+ import { encode as encodeGPT } from "../shared/tokenizer.js";
4
+ import { coerceProfileName, loadScoringProfile } from "./scoring.js";
5
+ const DEFAULT_SEARCH_LIMIT = 50;
6
+ const DEFAULT_SNIPPET_WINDOW = 150;
7
+ const DEFAULT_BUNDLE_LIMIT = 12;
8
+ const MAX_BUNDLE_LIMIT = 20;
9
+ const MAX_KEYWORDS = 12;
10
+ const MAX_MATCHES_PER_KEYWORD = 40;
11
+ const MAX_DEPENDENCY_SEEDS = 8;
12
+ const MAX_DEPENDENCY_SEEDS_QUERY_LIMIT = 100; // SQL injection防御用の上限
13
+ const NEARBY_LIMIT = 6;
14
+ const FALLBACK_SNIPPET_WINDOW = 120;
15
+ const MAX_RERANK_LIMIT = 50;
16
+ const STOP_WORDS = new Set([
17
+ "the",
18
+ "and",
19
+ "for",
20
+ "with",
21
+ "from",
22
+ "this",
23
+ "that",
24
+ "have",
25
+ "has",
26
+ "will",
27
+ "would",
28
+ "into",
29
+ "about",
30
+ "there",
31
+ "their",
32
+ "your",
33
+ "fix",
34
+ "test",
35
+ "tests",
36
+ "issue",
37
+ "error",
38
+ "bug",
39
+ "fail",
40
+ "failing",
41
+ "make",
42
+ "when",
43
+ "where",
44
+ "should",
45
+ "could",
46
+ "need",
47
+ "goal",
48
+ ]);
49
+ function normalizeLimit(limit) {
50
+ if (!limit || Number.isNaN(limit)) {
51
+ return DEFAULT_SEARCH_LIMIT;
52
+ }
53
+ return Math.min(Math.max(1, Math.floor(limit)), 100);
54
+ }
55
+ function buildPreview(content, query) {
56
+ const lowerContent = content.toLowerCase();
57
+ const lowerQuery = query.toLowerCase();
58
+ const index = lowerContent.indexOf(lowerQuery);
59
+ if (index === -1) {
60
+ return { preview: content.slice(0, 240), line: 1 };
61
+ }
62
+ const prefix = content.slice(0, index);
63
+ const prefixLines = prefix.split(/\r?\n/);
64
+ const matchLine = prefix.length === 0 ? 1 : prefixLines.length;
65
+ const snippetStart = Math.max(0, index - 120);
66
+ const snippetEnd = Math.min(content.length, index + query.length + 120);
67
+ const preview = content.slice(snippetStart, snippetEnd);
68
+ return { preview, line: matchLine };
69
+ }
70
+ function normalizeBundleLimit(limit) {
71
+ if (!limit || Number.isNaN(limit)) {
72
+ return DEFAULT_BUNDLE_LIMIT;
73
+ }
74
+ return Math.min(Math.max(1, Math.floor(limit)), MAX_BUNDLE_LIMIT);
75
+ }
76
+ function extractKeywords(text) {
77
+ const words = text
78
+ .toLowerCase()
79
+ .split(/[^a-z0-9_]+/iu)
80
+ .map((word) => word.trim())
81
+ .filter((word) => word.length >= 3 && !STOP_WORDS.has(word));
82
+ const unique = [];
83
+ for (const word of words) {
84
+ if (!unique.includes(word)) {
85
+ unique.push(word);
86
+ if (unique.length >= MAX_KEYWORDS) {
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ return unique;
92
+ }
93
+ function ensureCandidate(map, filePath) {
94
+ let candidate = map.get(filePath);
95
+ if (!candidate) {
96
+ candidate = {
97
+ path: filePath,
98
+ score: 0,
99
+ reasons: new Set(),
100
+ matchLine: null,
101
+ content: null,
102
+ totalLines: null,
103
+ lang: null,
104
+ ext: null,
105
+ embedding: null,
106
+ semanticSimilarity: null,
107
+ };
108
+ map.set(filePath, candidate);
109
+ }
110
+ return candidate;
111
+ }
112
+ function parseEmbedding(vectorJson, vectorDims) {
113
+ if (!vectorJson || !vectorDims || vectorDims <= 0) {
114
+ return null;
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(vectorJson);
118
+ if (!Array.isArray(parsed)) {
119
+ return null;
120
+ }
121
+ const values = [];
122
+ for (let i = 0; i < parsed.length && i < vectorDims; i += 1) {
123
+ const raw = parsed[i];
124
+ const num = typeof raw === "number" ? raw : Number(raw);
125
+ if (!Number.isFinite(num)) {
126
+ return null;
127
+ }
128
+ values.push(num);
129
+ }
130
+ return values.length === vectorDims ? values : null;
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ function applyStructuralScores(candidates, queryEmbedding, structuralWeight) {
137
+ if (!queryEmbedding || structuralWeight <= 0) {
138
+ return;
139
+ }
140
+ for (const candidate of candidates) {
141
+ if (!candidate.embedding) {
142
+ continue;
143
+ }
144
+ const similarity = structuralSimilarity(queryEmbedding, candidate.embedding);
145
+ if (!Number.isFinite(similarity) || similarity <= 0) {
146
+ continue;
147
+ }
148
+ candidate.semanticSimilarity = similarity;
149
+ candidate.score += structuralWeight * similarity;
150
+ candidate.reasons.add(`structural:${similarity.toFixed(2)}`);
151
+ }
152
+ }
153
+ async function fetchEmbeddingMap(db, repoId, paths) {
154
+ const map = new Map();
155
+ if (paths.length === 0) {
156
+ return map;
157
+ }
158
+ const placeholders = paths.map(() => "?").join(", ");
159
+ const rows = await db.all(`
160
+ SELECT path, vector_json, dims AS vector_dims
161
+ FROM file_embedding
162
+ WHERE repo_id = ? AND path IN (${placeholders})
163
+ `, [repoId, ...paths]);
164
+ for (const row of rows) {
165
+ const embedding = parseEmbedding(row.vector_json, row.vector_dims);
166
+ if (embedding) {
167
+ map.set(row.path, embedding);
168
+ }
169
+ }
170
+ return map;
171
+ }
172
+ async function loadFileContent(db, repoId, filePath) {
173
+ const rows = await db.all(`
174
+ SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
175
+ FROM file f
176
+ JOIN blob b ON b.hash = f.blob_hash
177
+ LEFT JOIN file_embedding fe
178
+ ON fe.repo_id = f.repo_id
179
+ AND fe.path = f.path
180
+ WHERE f.repo_id = ? AND f.path = ?
181
+ LIMIT 1
182
+ `, [repoId, filePath]);
183
+ const row = rows[0];
184
+ if (!row || row.is_binary || row.content === null) {
185
+ return null;
186
+ }
187
+ const totalLines = row.content.length === 0 ? 0 : row.content.split(/\r?\n/).length;
188
+ return {
189
+ content: row.content,
190
+ lang: row.lang,
191
+ ext: row.ext,
192
+ totalLines,
193
+ embedding: parseEmbedding(row.vector_json ?? null, row.vector_dims ?? null),
194
+ };
195
+ }
196
+ function selectSnippet(snippets, matchLine) {
197
+ const firstSnippet = snippets[0];
198
+ if (!firstSnippet) {
199
+ return null;
200
+ }
201
+ if (matchLine === null) {
202
+ return firstSnippet;
203
+ }
204
+ const containing = snippets.find((snippet) => matchLine >= snippet.start_line && matchLine <= snippet.end_line);
205
+ if (containing) {
206
+ return containing;
207
+ }
208
+ if (matchLine < firstSnippet.start_line) {
209
+ return firstSnippet;
210
+ }
211
+ const lastSnippet = snippets[snippets.length - 1];
212
+ return lastSnippet ?? firstSnippet;
213
+ }
214
+ function buildSnippetPreview(content, startLine, endLine) {
215
+ const lines = content.split(/\r?\n/);
216
+ const startIndex = Math.max(0, Math.min(startLine - 1, lines.length));
217
+ const endIndex = Math.max(startIndex, Math.min(endLine, lines.length));
218
+ const snippet = lines.slice(startIndex, endIndex).join("\n");
219
+ if (snippet.length <= 480) {
220
+ return snippet;
221
+ }
222
+ return `${snippet.slice(0, 479)}…`;
223
+ }
224
+ /**
225
+ * トークン数を推定(コンテンツベース)
226
+ * 実際のGPTトークナイザーを使用して正確にカウント
227
+ *
228
+ * @param content - ファイル全体のコンテンツ
229
+ * @param startLine - 開始行(1-indexed)
230
+ * @param endLine - 終了行(1-indexed)
231
+ * @returns 推定トークン数
232
+ */
233
+ function estimateTokensFromContent(content, startLine, endLine) {
234
+ const lines = content.split(/\r?\n/);
235
+ const startIndex = Math.max(0, startLine - 1);
236
+ const endIndex = Math.min(endLine, lines.length);
237
+ const selectedLines = lines.slice(startIndex, endIndex);
238
+ const text = selectedLines.join("\n");
239
+ try {
240
+ // 実際のGPTトークナイザーを使用
241
+ return encodeGPT(text).length;
242
+ }
243
+ catch (error) {
244
+ // フォールバック: 平均的な英語テキストで4文字 ≈ 1トークン
245
+ console.warn("Token encoding failed, using character-based fallback", error);
246
+ return Math.max(1, Math.ceil(text.length / 4));
247
+ }
248
+ }
249
+ export async function filesSearch(context, params) {
250
+ const { db, repoId } = context;
251
+ const { query } = params;
252
+ if (!query || query.trim().length === 0) {
253
+ throw new Error("files.search requires a non-empty query. Provide a search keyword to continue.");
254
+ }
255
+ const limit = normalizeLimit(params.limit);
256
+ const conditions = ["f.repo_id = ?", "b.content IS NOT NULL", "b.content ILIKE '%' || ? || '%'"];
257
+ const values = [repoId, query];
258
+ if (params.lang) {
259
+ conditions.push("COALESCE(f.lang, '') = ?");
260
+ values.push(params.lang);
261
+ }
262
+ if (params.ext) {
263
+ conditions.push("COALESCE(f.ext, '') = ?");
264
+ values.push(params.ext);
265
+ }
266
+ if (params.path_prefix) {
267
+ conditions.push("f.path LIKE ?");
268
+ values.push(`${params.path_prefix}%`);
269
+ }
270
+ const sql = `
271
+ SELECT f.path, f.lang, f.ext, b.content
272
+ FROM file f
273
+ JOIN blob b ON b.hash = f.blob_hash
274
+ WHERE ${conditions.join(" AND ")}
275
+ ORDER BY f.path
276
+ LIMIT ?
277
+ `;
278
+ values.push(limit);
279
+ const rows = await db.all(sql, values);
280
+ return rows.map((row) => {
281
+ const { preview, line } = buildPreview(row.content ?? "", query);
282
+ return {
283
+ path: row.path,
284
+ preview,
285
+ matchLine: line,
286
+ lang: row.lang,
287
+ ext: row.ext,
288
+ score: 1.0,
289
+ };
290
+ });
291
+ }
292
+ export async function snippetsGet(context, params) {
293
+ const { db, repoId } = context;
294
+ if (!params.path) {
295
+ throw new Error("snippets.get requires a file path. Specify a tracked text file path to continue.");
296
+ }
297
+ const rows = await db.all(`
298
+ SELECT f.path, f.lang, f.ext, f.is_binary, b.content
299
+ FROM file f
300
+ JOIN blob b ON b.hash = f.blob_hash
301
+ WHERE f.repo_id = ? AND f.path = ?
302
+ LIMIT 1
303
+ `, [repoId, params.path]);
304
+ if (rows.length === 0) {
305
+ throw new Error("Requested snippet file was not indexed. Re-run the indexer or choose another path.");
306
+ }
307
+ const row = rows[0];
308
+ if (!row) {
309
+ throw new Error("Requested snippet file was not indexed. Re-run the indexer or choose another path.");
310
+ }
311
+ if (row.is_binary) {
312
+ throw new Error("Binary snippets are not supported. Choose a text file to preview its content.");
313
+ }
314
+ if (row.content === null) {
315
+ throw new Error("Snippet content is unavailable. Re-run the indexer to refresh DuckDB state.");
316
+ }
317
+ const lines = row.content.split(/\r?\n/);
318
+ const totalLines = lines.length;
319
+ const snippetRows = await db.all(`
320
+ SELECT s.snippet_id, s.start_line, s.end_line, s.symbol_id, sym.name AS symbol_name, sym.kind AS symbol_kind
321
+ FROM snippet s
322
+ LEFT JOIN symbol sym
323
+ ON sym.repo_id = s.repo_id
324
+ AND sym.path = s.path
325
+ AND sym.symbol_id = s.symbol_id
326
+ WHERE s.repo_id = ? AND s.path = ?
327
+ ORDER BY s.start_line
328
+ `, [repoId, params.path]);
329
+ const requestedStart = params.start_line ?? 1;
330
+ const requestedEnd = params.end_line ?? Math.min(totalLines, requestedStart + DEFAULT_SNIPPET_WINDOW - 1);
331
+ const useSymbolSnippets = snippetRows.length > 0 && params.end_line === undefined;
332
+ let snippetSelection = null;
333
+ if (useSymbolSnippets) {
334
+ snippetSelection =
335
+ snippetRows.find((snippet) => requestedStart >= snippet.start_line && requestedStart <= snippet.end_line) ?? null;
336
+ if (!snippetSelection) {
337
+ const firstSnippet = snippetRows[0];
338
+ if (firstSnippet && requestedStart < firstSnippet.start_line) {
339
+ snippetSelection = firstSnippet;
340
+ }
341
+ else {
342
+ snippetSelection = snippetRows[snippetRows.length - 1] ?? null;
343
+ }
344
+ }
345
+ }
346
+ let startLine;
347
+ let endLine;
348
+ let symbolName = null;
349
+ let symbolKind = null;
350
+ if (snippetSelection) {
351
+ startLine = snippetSelection.start_line;
352
+ endLine = snippetSelection.end_line;
353
+ symbolName = snippetSelection.symbol_name;
354
+ symbolKind = snippetSelection.symbol_kind;
355
+ }
356
+ else {
357
+ startLine = Math.max(1, Math.min(totalLines, requestedStart));
358
+ endLine = Math.max(startLine, Math.min(totalLines, requestedEnd));
359
+ }
360
+ const snippetContent = lines.slice(startLine - 1, endLine).join("\n");
361
+ return {
362
+ path: row.path,
363
+ startLine,
364
+ endLine,
365
+ content: snippetContent,
366
+ totalLines,
367
+ symbolName,
368
+ symbolKind,
369
+ };
370
+ }
371
+ export async function contextBundle(context, params) {
372
+ const { db, repoId } = context;
373
+ const goal = params.goal?.trim() ?? "";
374
+ if (goal.length === 0) {
375
+ throw new Error("context.bundle requires a non-empty goal. Describe your objective to receive context.");
376
+ }
377
+ const limit = normalizeBundleLimit(params.limit);
378
+ const artifacts = params.artifacts ?? {};
379
+ // スコアリング重みをロード(将来的には設定ファイルや引数から)
380
+ const profileName = coerceProfileName(params.profile ?? null);
381
+ const weights = loadScoringProfile(profileName);
382
+ const keywordSources = [goal];
383
+ if (artifacts.failing_tests && artifacts.failing_tests.length > 0) {
384
+ keywordSources.push(artifacts.failing_tests.join(" "));
385
+ }
386
+ if (artifacts.last_diff) {
387
+ keywordSources.push(artifacts.last_diff);
388
+ }
389
+ if (artifacts.editing_path) {
390
+ keywordSources.push(artifacts.editing_path);
391
+ }
392
+ const semanticSeed = keywordSources.join(" ");
393
+ const queryEmbedding = generateEmbedding(semanticSeed)?.values ?? null;
394
+ let keywords = extractKeywords(semanticSeed);
395
+ if (keywords.length === 0 && artifacts.editing_path) {
396
+ const pathSegments = artifacts.editing_path
397
+ .split(/[/_.-]/)
398
+ .map((segment) => segment.toLowerCase())
399
+ .filter((segment) => segment.length >= 3 && !STOP_WORDS.has(segment));
400
+ keywords = pathSegments.slice(0, MAX_KEYWORDS);
401
+ }
402
+ const candidates = new Map();
403
+ const stringMatchSeeds = new Set();
404
+ const fileCache = new Map();
405
+ for (const keyword of keywords) {
406
+ const rows = await db.all(`
407
+ SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
408
+ FROM file f
409
+ JOIN blob b ON b.hash = f.blob_hash
410
+ LEFT JOIN file_embedding fe
411
+ ON fe.repo_id = f.repo_id
412
+ AND fe.path = f.path
413
+ WHERE f.repo_id = ?
414
+ AND f.is_binary = FALSE
415
+ AND b.content ILIKE '%' || ? || '%'
416
+ ORDER BY f.path
417
+ LIMIT ?
418
+ `, [repoId, keyword, MAX_MATCHES_PER_KEYWORD]);
419
+ for (const row of rows) {
420
+ if (row.content === null) {
421
+ continue;
422
+ }
423
+ const candidate = ensureCandidate(candidates, row.path);
424
+ candidate.score += weights.textMatch;
425
+ candidate.reasons.add(`text:${keyword}`);
426
+ const { line } = buildPreview(row.content, keyword);
427
+ candidate.matchLine =
428
+ candidate.matchLine === null ? line : Math.min(candidate.matchLine, line);
429
+ candidate.content ??= row.content;
430
+ candidate.lang ??= row.lang;
431
+ candidate.ext ??= row.ext;
432
+ candidate.totalLines ??= row.content.length === 0 ? 0 : row.content.split(/\r?\n/).length;
433
+ candidate.embedding ??= parseEmbedding(row.vector_json ?? null, row.vector_dims ?? null);
434
+ stringMatchSeeds.add(row.path);
435
+ if (!fileCache.has(row.path)) {
436
+ fileCache.set(row.path, {
437
+ content: row.content,
438
+ lang: row.lang,
439
+ ext: row.ext,
440
+ totalLines: candidate.totalLines ?? 0,
441
+ embedding: candidate.embedding,
442
+ });
443
+ }
444
+ }
445
+ }
446
+ if (artifacts.editing_path) {
447
+ const editingCandidate = ensureCandidate(candidates, artifacts.editing_path);
448
+ editingCandidate.score += weights.editingPath;
449
+ editingCandidate.reasons.add("artifact:editing_path");
450
+ editingCandidate.matchLine ??= 1;
451
+ }
452
+ // SQL injection防御: ファイルパスの検証パターン
453
+ const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_.\-/]+$/;
454
+ const dependencySeeds = new Set();
455
+ for (const pathSeed of stringMatchSeeds) {
456
+ if (!SAFE_PATH_PATTERN.test(pathSeed)) {
457
+ console.warn(`Skipping potentially unsafe path in dependency seeds: ${pathSeed}`);
458
+ continue;
459
+ }
460
+ dependencySeeds.add(pathSeed);
461
+ if (dependencySeeds.size >= MAX_DEPENDENCY_SEEDS) {
462
+ break;
463
+ }
464
+ }
465
+ if (artifacts.editing_path) {
466
+ if (!SAFE_PATH_PATTERN.test(artifacts.editing_path)) {
467
+ throw new Error(`Invalid editing_path format. Path must contain only alphanumeric characters, underscores, dots, hyphens, and forward slashes.`);
468
+ }
469
+ dependencySeeds.add(artifacts.editing_path);
470
+ }
471
+ if (dependencySeeds.size > 0) {
472
+ // SQL injection防御: プレースホルダー生成前にサイズを検証
473
+ if (dependencySeeds.size > MAX_DEPENDENCY_SEEDS_QUERY_LIMIT) {
474
+ throw new Error(`Too many dependency seeds: ${dependencySeeds.size} (max ${MAX_DEPENDENCY_SEEDS_QUERY_LIMIT}). Narrow your search criteria.`);
475
+ }
476
+ const placeholders = Array.from(dependencySeeds, () => "?").join(", ");
477
+ // 防御的チェック: プレースホルダーが正しい形式であることを確認
478
+ // 期待される形式: "?, ?, ..." (クエスチョンマーク、カンマ、スペースのみ)
479
+ if (!/^(\?)(,\s*\?)*$/.test(placeholders)) {
480
+ throw new Error("Invalid placeholder generation detected. Operation aborted for safety.");
481
+ }
482
+ const depRows = await db.all(`
483
+ SELECT src_path, dst_kind, dst, rel
484
+ FROM dependency
485
+ WHERE repo_id = ? AND src_path IN (${placeholders})
486
+ `, [repoId, ...dependencySeeds]);
487
+ for (const dep of depRows) {
488
+ if (dep.dst_kind !== "path") {
489
+ continue;
490
+ }
491
+ const candidate = ensureCandidate(candidates, dep.dst);
492
+ candidate.score += weights.dependency;
493
+ candidate.reasons.add(`dep:${dep.src_path}`);
494
+ }
495
+ }
496
+ if (artifacts.editing_path) {
497
+ const directory = path.posix.dirname(artifacts.editing_path);
498
+ if (directory && directory !== ".") {
499
+ const nearRows = await db.all(`
500
+ SELECT path
501
+ FROM file
502
+ WHERE repo_id = ?
503
+ AND is_binary = FALSE
504
+ AND path LIKE ?
505
+ ORDER BY path
506
+ LIMIT ?
507
+ `, [repoId, `${directory}/%`, NEARBY_LIMIT + 1]);
508
+ for (const near of nearRows) {
509
+ if (near.path === artifacts.editing_path) {
510
+ continue;
511
+ }
512
+ const candidate = ensureCandidate(candidates, near.path);
513
+ candidate.score += weights.proximity;
514
+ candidate.reasons.add(`near:${directory}`);
515
+ }
516
+ }
517
+ }
518
+ const materializedCandidates = [];
519
+ for (const candidate of candidates.values()) {
520
+ if (!candidate.content) {
521
+ const cached = fileCache.get(candidate.path);
522
+ if (cached) {
523
+ candidate.content = cached.content;
524
+ candidate.lang = cached.lang;
525
+ candidate.ext = cached.ext;
526
+ candidate.totalLines = cached.totalLines;
527
+ candidate.embedding = cached.embedding;
528
+ }
529
+ else {
530
+ const loaded = await loadFileContent(db, repoId, candidate.path);
531
+ if (!loaded) {
532
+ continue;
533
+ }
534
+ candidate.content = loaded.content;
535
+ candidate.lang = loaded.lang;
536
+ candidate.ext = loaded.ext;
537
+ candidate.totalLines = loaded.totalLines;
538
+ candidate.embedding = loaded.embedding;
539
+ fileCache.set(candidate.path, loaded);
540
+ }
541
+ }
542
+ materializedCandidates.push(candidate);
543
+ }
544
+ if (materializedCandidates.length === 0) {
545
+ return { context: [], tokens_estimate: 0 };
546
+ }
547
+ applyStructuralScores(materializedCandidates, queryEmbedding, weights.structural);
548
+ const sortedCandidates = materializedCandidates
549
+ .sort((a, b) => {
550
+ if (b.score === a.score) {
551
+ return a.path.localeCompare(b.path);
552
+ }
553
+ return b.score - a.score;
554
+ })
555
+ .slice(0, limit);
556
+ const maxScore = Math.max(...sortedCandidates.map((candidate) => candidate.score));
557
+ const results = [];
558
+ for (const candidate of sortedCandidates) {
559
+ if (!candidate.content) {
560
+ continue;
561
+ }
562
+ const snippets = await db.all(`
563
+ SELECT s.snippet_id, s.start_line, s.end_line, s.symbol_id, sym.name AS symbol_name, sym.kind AS symbol_kind
564
+ FROM snippet s
565
+ LEFT JOIN symbol sym
566
+ ON sym.repo_id = s.repo_id
567
+ AND sym.path = s.path
568
+ AND sym.symbol_id = s.symbol_id
569
+ WHERE s.repo_id = ? AND s.path = ?
570
+ ORDER BY s.start_line
571
+ `, [repoId, candidate.path]);
572
+ const selected = selectSnippet(snippets, candidate.matchLine);
573
+ let startLine;
574
+ let endLine;
575
+ if (selected) {
576
+ startLine = selected.start_line;
577
+ endLine = selected.end_line;
578
+ }
579
+ else {
580
+ const totalLines = candidate.totalLines ?? 0;
581
+ const matchLine = candidate.matchLine ?? 1;
582
+ const windowHalf = Math.floor(FALLBACK_SNIPPET_WINDOW / 2);
583
+ startLine = Math.max(1, matchLine - windowHalf);
584
+ endLine = Math.min(totalLines === 0 ? matchLine + windowHalf : totalLines, startLine + FALLBACK_SNIPPET_WINDOW - 1);
585
+ }
586
+ if (endLine < startLine) {
587
+ endLine = startLine;
588
+ }
589
+ const preview = buildSnippetPreview(candidate.content, startLine, endLine);
590
+ const reasons = new Set(candidate.reasons);
591
+ if (selected && selected.symbol_name) {
592
+ reasons.add(`symbol:${selected.symbol_name}`);
593
+ }
594
+ const normalizedScore = maxScore > 0 ? candidate.score / maxScore : 0;
595
+ results.push({
596
+ path: candidate.path,
597
+ range: [startLine, endLine],
598
+ preview,
599
+ why: Array.from(reasons).sort(),
600
+ score: Number.isFinite(normalizedScore) ? normalizedScore : 0,
601
+ });
602
+ }
603
+ // コンテンツベースのトークン推定を使用(より正確)
604
+ const tokensEstimate = results.reduce((acc, item) => {
605
+ const candidate = sortedCandidates.find((c) => c.path === item.path);
606
+ if (candidate && candidate.content) {
607
+ return acc + estimateTokensFromContent(candidate.content, item.range[0], item.range[1]);
608
+ }
609
+ // フォールバック: 行ベース推定(コンテンツが利用不可の場合)
610
+ const lineCount = Math.max(1, item.range[1] - item.range[0] + 1);
611
+ return acc + lineCount * 4;
612
+ }, 0);
613
+ return { context: results, tokens_estimate: tokensEstimate };
614
+ }
615
+ export async function semanticRerank(context, params) {
616
+ const text = params.text?.trim() ?? "";
617
+ if (text.length === 0) {
618
+ throw new Error("semantic.rerank requires non-empty text. Describe the intent to compute semantic similarity.");
619
+ }
620
+ if (!Array.isArray(params.candidates) || params.candidates.length === 0) {
621
+ return { candidates: [] };
622
+ }
623
+ const uniqueCandidates = [];
624
+ const seenPaths = new Set();
625
+ for (const candidate of params.candidates) {
626
+ if (!candidate || typeof candidate.path !== "string" || candidate.path.length === 0) {
627
+ continue;
628
+ }
629
+ if (seenPaths.has(candidate.path)) {
630
+ continue;
631
+ }
632
+ seenPaths.add(candidate.path);
633
+ uniqueCandidates.push(candidate);
634
+ if (uniqueCandidates.length >= MAX_RERANK_LIMIT) {
635
+ break;
636
+ }
637
+ }
638
+ if (uniqueCandidates.length === 0) {
639
+ return { candidates: [] };
640
+ }
641
+ const limitRaw = params.k ?? uniqueCandidates.length;
642
+ const limit = Math.max(1, Math.min(MAX_RERANK_LIMIT, Math.floor(limitRaw)));
643
+ const profileName = coerceProfileName(params.profile ?? null);
644
+ const weights = loadScoringProfile(profileName);
645
+ const structuralWeight = weights.structural;
646
+ const queryEmbedding = generateEmbedding(text)?.values ?? null;
647
+ let embeddingMap = new Map();
648
+ if (queryEmbedding && structuralWeight > 0) {
649
+ const paths = uniqueCandidates.map((candidate) => candidate.path);
650
+ embeddingMap = await fetchEmbeddingMap(context.db, context.repoId, paths);
651
+ }
652
+ const scored = uniqueCandidates.map((candidate) => {
653
+ const base = typeof candidate.score === "number" && Number.isFinite(candidate.score) ? candidate.score : 0;
654
+ let semantic = 0;
655
+ if (queryEmbedding && structuralWeight > 0) {
656
+ const embedding = embeddingMap.get(candidate.path);
657
+ if (embedding) {
658
+ const similarity = structuralSimilarity(queryEmbedding, embedding);
659
+ if (Number.isFinite(similarity) && similarity > 0) {
660
+ semantic = similarity;
661
+ }
662
+ }
663
+ }
664
+ const combined = base + structuralWeight * semantic;
665
+ return {
666
+ path: candidate.path,
667
+ base,
668
+ semantic,
669
+ combined,
670
+ };
671
+ });
672
+ const sorted = scored.sort((a, b) => {
673
+ if (b.combined === a.combined) {
674
+ if (b.semantic === a.semantic) {
675
+ return a.path.localeCompare(b.path);
676
+ }
677
+ return b.semantic - a.semantic;
678
+ }
679
+ return b.combined - a.combined;
680
+ });
681
+ return { candidates: sorted.slice(0, limit) };
682
+ }
683
+ export async function depsClosure(context, params) {
684
+ const { db, repoId } = context;
685
+ if (!params.path) {
686
+ throw new Error("deps.closure requires a file path. Provide a tracked source file path to continue.");
687
+ }
688
+ if (params.direction && params.direction !== "outbound") {
689
+ throw new Error("deps.closure currently supports only outbound direction. Use outbound.");
690
+ }
691
+ const maxDepth = params.max_depth ?? 3;
692
+ const includePackages = params.include_packages ?? true;
693
+ const dependencyRows = await db.all(`
694
+ SELECT src_path, dst_kind, dst, rel
695
+ FROM dependency
696
+ WHERE repo_id = ?
697
+ `, [repoId]);
698
+ const outbound = new Map();
699
+ for (const row of dependencyRows) {
700
+ if (!outbound.has(row.src_path)) {
701
+ outbound.set(row.src_path, []);
702
+ }
703
+ outbound.get(row.src_path)?.push(row);
704
+ }
705
+ const queue = [{ path: params.path, depth: 0 }];
706
+ const visitedPaths = new Set([params.path]);
707
+ const nodeDepth = new Map();
708
+ const edgeSet = new Map();
709
+ const recordNode = (node) => {
710
+ const key = `${node.kind}:${node.target}`;
711
+ const existing = nodeDepth.get(key);
712
+ if (!existing || node.depth < existing.depth) {
713
+ nodeDepth.set(key, { ...node });
714
+ }
715
+ };
716
+ const recordEdge = (edge) => {
717
+ const key = `${edge.from}->${edge.to}:${edge.kind}:${edge.rel}`;
718
+ const existing = edgeSet.get(key);
719
+ if (!existing || edge.depth < existing.depth) {
720
+ edgeSet.set(key, { ...edge });
721
+ }
722
+ };
723
+ recordNode({ kind: "path", target: params.path, depth: 0 });
724
+ while (queue.length > 0) {
725
+ const current = queue.shift();
726
+ if (current.depth >= maxDepth) {
727
+ continue;
728
+ }
729
+ const edges = outbound.get(current.path) ?? [];
730
+ for (const edge of edges) {
731
+ const nextDepth = current.depth + 1;
732
+ if (edge.dst_kind === "path") {
733
+ recordEdge({
734
+ from: current.path,
735
+ to: edge.dst,
736
+ kind: "path",
737
+ rel: edge.rel,
738
+ depth: nextDepth,
739
+ });
740
+ recordNode({ kind: "path", target: edge.dst, depth: nextDepth });
741
+ if (!visitedPaths.has(edge.dst)) {
742
+ visitedPaths.add(edge.dst);
743
+ queue.push({ path: edge.dst, depth: nextDepth });
744
+ }
745
+ }
746
+ else if (edge.dst_kind === "package" && includePackages) {
747
+ recordEdge({
748
+ from: current.path,
749
+ to: edge.dst,
750
+ kind: "package",
751
+ rel: edge.rel,
752
+ depth: nextDepth,
753
+ });
754
+ recordNode({ kind: "package", target: edge.dst, depth: nextDepth });
755
+ }
756
+ }
757
+ }
758
+ const nodes = Array.from(nodeDepth.values()).sort((a, b) => {
759
+ if (a.depth === b.depth) {
760
+ return a.target.localeCompare(b.target);
761
+ }
762
+ return a.depth - b.depth;
763
+ });
764
+ const edges = Array.from(edgeSet.values()).sort((a, b) => {
765
+ if (a.depth === b.depth) {
766
+ const fromCmp = a.from.localeCompare(b.from);
767
+ if (fromCmp !== 0) {
768
+ return fromCmp;
769
+ }
770
+ return a.to.localeCompare(b.to);
771
+ }
772
+ return a.depth - b.depth;
773
+ });
774
+ return {
775
+ root: params.path,
776
+ direction: "outbound",
777
+ nodes,
778
+ edges,
779
+ };
780
+ }
781
+ export async function resolveRepoId(db, repoRoot) {
782
+ try {
783
+ const rows = await db.all("SELECT id FROM repo WHERE root = ?", [repoRoot]);
784
+ if (rows.length === 0) {
785
+ throw new Error("Target repository is missing from DuckDB. Run the indexer before starting the server.");
786
+ }
787
+ const row = rows[0];
788
+ if (!row) {
789
+ throw new Error("Failed to retrieve repository record. Database returned empty result.");
790
+ }
791
+ return row.id;
792
+ }
793
+ catch (error) {
794
+ if (error instanceof Error && error.message.includes("Table with name repo")) {
795
+ throw new Error("Target repository is missing from DuckDB. Run the indexer before starting the server.");
796
+ }
797
+ throw error;
798
+ }
799
+ }
800
+ //# sourceMappingURL=handlers.js.map