dfns-mcp 1.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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/package.json +50 -0
- package/src/docs-fetcher.ts +264 -0
- package/src/index.ts +1021 -0
- package/src/indexer.ts +915 -0
package/src/indexer.ts
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface DocEntry {
|
|
5
|
+
path: string;
|
|
6
|
+
relativePath: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
category: string;
|
|
10
|
+
keywords: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// TypeScript Type Indexing
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface TypeEntry {
|
|
18
|
+
/** The type/interface/class name */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Kind of definition: 'type', 'interface', or 'class' */
|
|
21
|
+
kind: "type" | "interface" | "class";
|
|
22
|
+
/** Full definition including JSDoc comments */
|
|
23
|
+
definition: string;
|
|
24
|
+
/** The npm package to import from (e.g., '@dfns/sdk', '@dfns/sdk-browser') */
|
|
25
|
+
importPackage: string;
|
|
26
|
+
/** The subpath import if needed (e.g., '@dfns/sdk/generated/wallets') */
|
|
27
|
+
importPath: string;
|
|
28
|
+
/** Category (wallets, auth, keys, etc.) */
|
|
29
|
+
category: string;
|
|
30
|
+
/** JSDoc description if available */
|
|
31
|
+
description: string;
|
|
32
|
+
/** Source file path */
|
|
33
|
+
sourceFile: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SearchResult {
|
|
37
|
+
path: string;
|
|
38
|
+
title: string;
|
|
39
|
+
category: string;
|
|
40
|
+
snippet: string;
|
|
41
|
+
score: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ApiEndpoint {
|
|
45
|
+
method: string;
|
|
46
|
+
path: string;
|
|
47
|
+
docPath: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse TypeScript file and extract type/interface/class definitions
|
|
52
|
+
*/
|
|
53
|
+
function parseTypeScriptTypes(content: string, filePath: string): Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
kind: "type" | "interface" | "class";
|
|
56
|
+
definition: string;
|
|
57
|
+
description: string;
|
|
58
|
+
}> {
|
|
59
|
+
const results: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
kind: "type" | "interface" | "class";
|
|
62
|
+
definition: string;
|
|
63
|
+
description: string;
|
|
64
|
+
}> = [];
|
|
65
|
+
|
|
66
|
+
// Match exported type aliases: export type Name = ...
|
|
67
|
+
// This regex handles nested braces and complex union types
|
|
68
|
+
const typeRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(export\s+type\s+(\w+)(?:<[^>]+>)?\s*=\s*)/g;
|
|
69
|
+
let match;
|
|
70
|
+
|
|
71
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
72
|
+
const startIndex = match.index;
|
|
73
|
+
const name = match[2];
|
|
74
|
+
const afterEquals = content.slice(match.index + match[0].length);
|
|
75
|
+
|
|
76
|
+
// Find the end of the type definition (handle nested braces and semicolons)
|
|
77
|
+
const endIndex = findTypeDefinitionEnd(afterEquals);
|
|
78
|
+
const typeBody = afterEquals.slice(0, endIndex);
|
|
79
|
+
|
|
80
|
+
// Extract JSDoc if present
|
|
81
|
+
const jsDocMatch = content.slice(Math.max(0, startIndex - 500), startIndex).match(/\/\*\*[\s\S]*?\*\/\s*$/);
|
|
82
|
+
const jsDoc = jsDocMatch ? jsDocMatch[0] : "";
|
|
83
|
+
const description = extractJSDocDescription(jsDoc);
|
|
84
|
+
|
|
85
|
+
const fullDefinition = jsDoc + match[1] + typeBody;
|
|
86
|
+
|
|
87
|
+
results.push({
|
|
88
|
+
name,
|
|
89
|
+
kind: "type",
|
|
90
|
+
definition: fullDefinition.trim(),
|
|
91
|
+
description,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Match exported interfaces: export interface Name { ... }
|
|
96
|
+
const interfaceRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(export\s+interface\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+[^{]+)?\s*\{)/g;
|
|
97
|
+
|
|
98
|
+
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
99
|
+
const startIndex = match.index;
|
|
100
|
+
const name = match[2];
|
|
101
|
+
const afterBrace = content.slice(match.index + match[0].length);
|
|
102
|
+
|
|
103
|
+
// Find matching closing brace
|
|
104
|
+
const endIndex = findMatchingBrace(afterBrace);
|
|
105
|
+
const interfaceBody = afterBrace.slice(0, endIndex);
|
|
106
|
+
|
|
107
|
+
// Extract JSDoc if present
|
|
108
|
+
const jsDocMatch = content.slice(Math.max(0, startIndex - 500), startIndex).match(/\/\*\*[\s\S]*?\*\/\s*$/);
|
|
109
|
+
const jsDoc = jsDocMatch ? jsDocMatch[0] : "";
|
|
110
|
+
const description = extractJSDocDescription(jsDoc);
|
|
111
|
+
|
|
112
|
+
const fullDefinition = jsDoc + match[1] + interfaceBody + "}";
|
|
113
|
+
|
|
114
|
+
results.push({
|
|
115
|
+
name,
|
|
116
|
+
kind: "interface",
|
|
117
|
+
definition: fullDefinition.trim(),
|
|
118
|
+
description,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Match exported classes: export class Name { ... }
|
|
123
|
+
const classRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(export\s+class\s+(\w+)(?:<[^>]+>)?(?:\s+(?:extends|implements)\s+[^{]+)?\s*\{)/g;
|
|
124
|
+
|
|
125
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
126
|
+
const startIndex = match.index;
|
|
127
|
+
const name = match[2];
|
|
128
|
+
const afterBrace = content.slice(match.index + match[0].length);
|
|
129
|
+
|
|
130
|
+
// Find matching closing brace
|
|
131
|
+
const endIndex = findMatchingBrace(afterBrace);
|
|
132
|
+
const classBody = afterBrace.slice(0, endIndex);
|
|
133
|
+
|
|
134
|
+
// Extract JSDoc if present
|
|
135
|
+
const jsDocMatch = content.slice(Math.max(0, startIndex - 500), startIndex).match(/\/\*\*[\s\S]*?\*\/\s*$/);
|
|
136
|
+
const jsDoc = jsDocMatch ? jsDocMatch[0] : "";
|
|
137
|
+
const description = extractJSDocDescription(jsDoc);
|
|
138
|
+
|
|
139
|
+
const fullDefinition = jsDoc + match[1] + classBody + "}";
|
|
140
|
+
|
|
141
|
+
results.push({
|
|
142
|
+
name,
|
|
143
|
+
kind: "class",
|
|
144
|
+
definition: fullDefinition.trim(),
|
|
145
|
+
description,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return results;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Find the end of a type definition (handles nested braces, arrays, unions)
|
|
154
|
+
*/
|
|
155
|
+
function findTypeDefinitionEnd(content: string): number {
|
|
156
|
+
let braceCount = 0;
|
|
157
|
+
let parenCount = 0;
|
|
158
|
+
let inString = false;
|
|
159
|
+
let stringChar = "";
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < content.length; i++) {
|
|
162
|
+
const char = content[i];
|
|
163
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
164
|
+
|
|
165
|
+
// Handle string literals
|
|
166
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
167
|
+
if (!inString) {
|
|
168
|
+
inString = true;
|
|
169
|
+
stringChar = char;
|
|
170
|
+
} else if (char === stringChar) {
|
|
171
|
+
inString = false;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (inString) continue;
|
|
177
|
+
|
|
178
|
+
if (char === "{") braceCount++;
|
|
179
|
+
if (char === "}") braceCount--;
|
|
180
|
+
if (char === "(") parenCount++;
|
|
181
|
+
if (char === ")") parenCount--;
|
|
182
|
+
|
|
183
|
+
// Type definition ends at semicolon or newline when not inside braces/parens
|
|
184
|
+
if (braceCount === 0 && parenCount === 0) {
|
|
185
|
+
if (char === ";") return i + 1;
|
|
186
|
+
// Also end at double newline (next export statement)
|
|
187
|
+
if (char === "\n" && content[i + 1] === "\n" && content.slice(i + 2, i + 8) === "export") {
|
|
188
|
+
return i;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return content.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find matching closing brace
|
|
198
|
+
*/
|
|
199
|
+
function findMatchingBrace(content: string): number {
|
|
200
|
+
let braceCount = 1;
|
|
201
|
+
let inString = false;
|
|
202
|
+
let stringChar = "";
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < content.length; i++) {
|
|
205
|
+
const char = content[i];
|
|
206
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
207
|
+
|
|
208
|
+
// Handle string literals
|
|
209
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
210
|
+
if (!inString) {
|
|
211
|
+
inString = true;
|
|
212
|
+
stringChar = char;
|
|
213
|
+
} else if (char === stringChar) {
|
|
214
|
+
inString = false;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (inString) continue;
|
|
220
|
+
|
|
221
|
+
if (char === "{") braceCount++;
|
|
222
|
+
if (char === "}") {
|
|
223
|
+
braceCount--;
|
|
224
|
+
if (braceCount === 0) return i;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return content.length;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract description from JSDoc comment
|
|
233
|
+
*/
|
|
234
|
+
function extractJSDocDescription(jsDoc: string): string {
|
|
235
|
+
if (!jsDoc) return "";
|
|
236
|
+
|
|
237
|
+
// Remove /** and */ and * prefixes
|
|
238
|
+
const cleaned = jsDoc
|
|
239
|
+
.replace(/^\/\*\*\s*/, "")
|
|
240
|
+
.replace(/\s*\*\/$/, "")
|
|
241
|
+
.split("\n")
|
|
242
|
+
.map(line => line.replace(/^\s*\*\s?/, ""))
|
|
243
|
+
.filter(line => !line.startsWith("@"))
|
|
244
|
+
.join(" ")
|
|
245
|
+
.trim();
|
|
246
|
+
|
|
247
|
+
return cleaned;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Determine package name from file path
|
|
252
|
+
*/
|
|
253
|
+
function determinePackageName(filePath: string): { importPackage: string; importPath: string } {
|
|
254
|
+
// Extract package info from path like:
|
|
255
|
+
// dfns-sdk-ts/packages/sdk/generated/wallets/types.ts -> @dfns/sdk, @dfns/sdk/generated/wallets
|
|
256
|
+
// dfns-sdk-ts/packages/sdk-browser/signers/webauthn.ts -> @dfns/sdk-browser
|
|
257
|
+
// dfns-sdk-ts/packages/lib-ethersjs6/index.ts -> @dfns/lib-ethersjs6
|
|
258
|
+
|
|
259
|
+
const packagesMatch = filePath.match(/packages\/([^/]+)/);
|
|
260
|
+
if (!packagesMatch) {
|
|
261
|
+
return { importPackage: "@dfns/sdk", importPath: "@dfns/sdk" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const packageDir = packagesMatch[1];
|
|
265
|
+
const importPackage = `@dfns/${packageDir}`;
|
|
266
|
+
|
|
267
|
+
// For generated types, include the subpath
|
|
268
|
+
const afterPackage = filePath.slice(filePath.indexOf(packageDir) + packageDir.length + 1);
|
|
269
|
+
if (afterPackage.startsWith("generated/")) {
|
|
270
|
+
// e.g., generated/wallets/types.ts -> @dfns/sdk/generated/wallets
|
|
271
|
+
const subPath = afterPackage.replace(/\/types\.ts$/, "").replace(/\/index\.ts$/, "");
|
|
272
|
+
return { importPackage, importPath: `${importPackage}/${subPath}` };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For types/ directory
|
|
276
|
+
if (afterPackage.startsWith("types/")) {
|
|
277
|
+
const subPath = afterPackage.replace(/\.ts$/, "");
|
|
278
|
+
return { importPackage, importPath: `${importPackage}/${subPath}` };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { importPackage, importPath: importPackage };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Determine type category from file path
|
|
286
|
+
*/
|
|
287
|
+
function determineTypeCategory(filePath: string): string {
|
|
288
|
+
const categoryPatterns: Record<string, string> = {
|
|
289
|
+
"generated/wallets": "Wallets",
|
|
290
|
+
"generated/auth": "Authentication",
|
|
291
|
+
"generated/keys": "Keys",
|
|
292
|
+
"generated/policies": "Policies",
|
|
293
|
+
"generated/permissions": "Permissions",
|
|
294
|
+
"generated/networks": "Networks",
|
|
295
|
+
"generated/webhooks": "Webhooks",
|
|
296
|
+
"generated/staking": "Staking",
|
|
297
|
+
"generated/exchanges": "Exchanges",
|
|
298
|
+
"generated/feeSponsors": "Fee Sponsors",
|
|
299
|
+
"generated/signers": "Signers",
|
|
300
|
+
"generated/swaps": "Swaps",
|
|
301
|
+
"generated/agreements": "Agreements",
|
|
302
|
+
"generated/allocations": "Allocations",
|
|
303
|
+
"types/wallets": "Wallets",
|
|
304
|
+
"types/auth": "Authentication",
|
|
305
|
+
"sdk-browser": "Browser SDK",
|
|
306
|
+
"sdk-keysigner": "Key Signer",
|
|
307
|
+
"sdk-react-native": "React Native SDK",
|
|
308
|
+
"lib-ethersjs": "Ethereum (ethers.js)",
|
|
309
|
+
"lib-viem": "Ethereum (viem)",
|
|
310
|
+
"lib-solana": "Solana",
|
|
311
|
+
"lib-bitcoin": "Bitcoin",
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
for (const [pattern, category] of Object.entries(categoryPatterns)) {
|
|
315
|
+
if (filePath.includes(pattern)) {
|
|
316
|
+
return category;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return "Core SDK";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Extracts title from markdown content
|
|
325
|
+
*/
|
|
326
|
+
function extractTitle(content: string, filePath: string): string {
|
|
327
|
+
// Try to find first h1 heading
|
|
328
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
329
|
+
if (h1Match) {
|
|
330
|
+
return h1Match[1].replace(/[*_`\[\]]/g, "").trim();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fall back to filename
|
|
334
|
+
const fileName = filePath.split("/").pop() || "";
|
|
335
|
+
return fileName.replace(/\.md$/, "").replace(/-/g, " ");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Extracts keywords from markdown content
|
|
340
|
+
*/
|
|
341
|
+
function extractKeywords(content: string): string[] {
|
|
342
|
+
const keywords: Set<string> = new Set();
|
|
343
|
+
|
|
344
|
+
// Extract code identifiers (camelCase, PascalCase)
|
|
345
|
+
const codeMatches = content.match(/`([A-Za-z][A-Za-z0-9]*(?:[A-Z][a-z0-9]*)*)`/g);
|
|
346
|
+
if (codeMatches) {
|
|
347
|
+
codeMatches.forEach((m) => keywords.add(m.replace(/`/g, "").toLowerCase()));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Extract API endpoints
|
|
351
|
+
const endpointMatches = content.match(/(GET|POST|PUT|DELETE|PATCH)\s+\/[^\s\n]+/g);
|
|
352
|
+
if (endpointMatches) {
|
|
353
|
+
endpointMatches.forEach((m) => keywords.add(m.toLowerCase()));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Extract headings as keywords
|
|
357
|
+
const headingMatches = content.match(/^#{1,3}\s+(.+)$/gm);
|
|
358
|
+
if (headingMatches) {
|
|
359
|
+
headingMatches.forEach((h) => {
|
|
360
|
+
const text = h.replace(/^#+\s+/, "").toLowerCase();
|
|
361
|
+
keywords.add(text);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return Array.from(keywords);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Determines category from file path using a configuration approach
|
|
370
|
+
*/
|
|
371
|
+
function determineCategory(filePath: string): string {
|
|
372
|
+
const categoryMap: Record<string, string> = {
|
|
373
|
+
"api-docs/authentication": "Authentication API",
|
|
374
|
+
"api-docs/wallets": "Wallets API",
|
|
375
|
+
"api-docs/keys": "Keys API",
|
|
376
|
+
"api-docs/policy-engine": "Policy Engine API",
|
|
377
|
+
"api-docs/permissions": "Permissions API",
|
|
378
|
+
"api-docs/webhooks": "Webhooks API",
|
|
379
|
+
"api-docs/networks": "Networks API",
|
|
380
|
+
"api-docs/fee-sponsors": "Fee Sponsors API",
|
|
381
|
+
"integrations/exchanges": "Exchange Integrations",
|
|
382
|
+
"integrations/staking": "Staking",
|
|
383
|
+
"integrations/swaps": "Swaps",
|
|
384
|
+
"integrations": "Integrations",
|
|
385
|
+
"getting-started": "Getting Started",
|
|
386
|
+
"advanced-topics": "Advanced Topics",
|
|
387
|
+
"guides": "Guides",
|
|
388
|
+
"use-cases": "Use Cases",
|
|
389
|
+
"lib-": "SDK Libraries",
|
|
390
|
+
"sdk-": "SDK Core",
|
|
391
|
+
"examples": "SDK Examples",
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
for (const [key, value] of Object.entries(categoryMap)) {
|
|
395
|
+
if (filePath.includes(key)) {
|
|
396
|
+
return value;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return "General";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Recursively finds all markdown files in a directory
|
|
405
|
+
*/
|
|
406
|
+
async function findMarkdownFiles(dir: string): Promise<string[]> {
|
|
407
|
+
const files: string[] = [];
|
|
408
|
+
|
|
409
|
+
async function walk(currentDir: string) {
|
|
410
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
411
|
+
for (const entry of entries) {
|
|
412
|
+
const fullPath = join(currentDir, entry.name);
|
|
413
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
414
|
+
await walk(fullPath);
|
|
415
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
416
|
+
files.push(fullPath);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await walk(dir);
|
|
422
|
+
return files;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Recursively finds all TypeScript files in SDK packages
|
|
427
|
+
*/
|
|
428
|
+
async function findTypeScriptFiles(dir: string): Promise<string[]> {
|
|
429
|
+
const files: string[] = [];
|
|
430
|
+
|
|
431
|
+
async function walk(currentDir: string) {
|
|
432
|
+
try {
|
|
433
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
const fullPath = join(currentDir, entry.name);
|
|
436
|
+
if (entry.isDirectory()) {
|
|
437
|
+
// Skip node_modules, dist, examples, and hidden directories
|
|
438
|
+
if (entry.name === "node_modules" || entry.name === "dist" ||
|
|
439
|
+
entry.name === "examples" || entry.name.startsWith(".")) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
await walk(fullPath);
|
|
443
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
444
|
+
// Only include type definition files and main files
|
|
445
|
+
const inSignersDir = currentDir.includes("/signers/") || currentDir.endsWith("/signers");
|
|
446
|
+
const inTypesDir = currentDir.includes("/types/") || currentDir.endsWith("/types");
|
|
447
|
+
|
|
448
|
+
if (entry.name === "types.ts" || entry.name === "index.ts" ||
|
|
449
|
+
entry.name === "signer.ts" || inSignersDir ||
|
|
450
|
+
inTypesDir || entry.name.includes("Client")) {
|
|
451
|
+
files.push(fullPath);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// Directory might not exist
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
await walk(dir);
|
|
461
|
+
return files;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export class DocumentIndex {
|
|
465
|
+
private docs: Map<string, DocEntry> = new Map();
|
|
466
|
+
private endpointIndex: Map<string, ApiEndpoint> = new Map();
|
|
467
|
+
private typeIndex: Map<string, TypeEntry> = new Map();
|
|
468
|
+
private docsDir: string;
|
|
469
|
+
private sdkDir: string;
|
|
470
|
+
|
|
471
|
+
constructor(docsDir: string, sdkDir: string) {
|
|
472
|
+
this.docsDir = docsDir;
|
|
473
|
+
this.sdkDir = sdkDir;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async build(): Promise<void> {
|
|
477
|
+
console.error("Building document index...");
|
|
478
|
+
|
|
479
|
+
// Index documentation
|
|
480
|
+
const docFiles = await findMarkdownFiles(this.docsDir);
|
|
481
|
+
await this.indexFiles(docFiles, this.docsDir, "docs");
|
|
482
|
+
|
|
483
|
+
// Index SDK READMEs and key files
|
|
484
|
+
const sdkFiles = await findMarkdownFiles(this.sdkDir);
|
|
485
|
+
await this.indexFiles(sdkFiles, this.sdkDir, "sdk");
|
|
486
|
+
|
|
487
|
+
// Index TypeScript types from SDK packages
|
|
488
|
+
console.error("Building TypeScript type index...");
|
|
489
|
+
const packagesDir = join(this.sdkDir, "packages");
|
|
490
|
+
const tsFiles = await findTypeScriptFiles(packagesDir);
|
|
491
|
+
await this.indexTypeScriptFiles(tsFiles);
|
|
492
|
+
|
|
493
|
+
console.error(`Indexed ${this.docs.size} documents, ${this.endpointIndex.size} endpoints, and ${this.typeIndex.size} types`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Index TypeScript files for type definitions
|
|
498
|
+
*/
|
|
499
|
+
private async indexTypeScriptFiles(files: string[]): Promise<void> {
|
|
500
|
+
for (const filePath of files) {
|
|
501
|
+
try {
|
|
502
|
+
const content = await readFile(filePath, "utf-8");
|
|
503
|
+
const types = parseTypeScriptTypes(content, filePath);
|
|
504
|
+
const { importPackage, importPath } = determinePackageName(filePath);
|
|
505
|
+
const category = determineTypeCategory(filePath);
|
|
506
|
+
|
|
507
|
+
for (const typeInfo of types) {
|
|
508
|
+
const entry: TypeEntry = {
|
|
509
|
+
name: typeInfo.name,
|
|
510
|
+
kind: typeInfo.kind,
|
|
511
|
+
definition: typeInfo.definition,
|
|
512
|
+
importPackage,
|
|
513
|
+
importPath,
|
|
514
|
+
category,
|
|
515
|
+
description: typeInfo.description,
|
|
516
|
+
sourceFile: filePath,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// Use lowercase name as key for case-insensitive lookup
|
|
520
|
+
this.typeIndex.set(typeInfo.name.toLowerCase(), entry);
|
|
521
|
+
}
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.error(`Failed to index types from ${filePath}:`, err);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private async indexFiles(files: string[], baseDir: string, prefix: string) {
|
|
529
|
+
for (const filePath of files) {
|
|
530
|
+
try {
|
|
531
|
+
const content = await readFile(filePath, "utf-8");
|
|
532
|
+
const relativePath = relative(baseDir, filePath);
|
|
533
|
+
const fullRelativePath = `${prefix}/${relativePath}`;
|
|
534
|
+
|
|
535
|
+
const entry: DocEntry = {
|
|
536
|
+
path: filePath,
|
|
537
|
+
relativePath: fullRelativePath,
|
|
538
|
+
title: extractTitle(content, filePath),
|
|
539
|
+
content,
|
|
540
|
+
category: determineCategory(filePath),
|
|
541
|
+
keywords: extractKeywords(content),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
this.docs.set(fullRelativePath, entry);
|
|
545
|
+
|
|
546
|
+
// Index endpoints found in this file
|
|
547
|
+
this.extractEndpoints(content, fullRelativePath);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error(`Failed to index ${filePath}:`, err);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private extractEndpoints(content: string, docPath: string) {
|
|
555
|
+
// Look for patterns like "POST /wallets" or "GET /wallets/{id}"
|
|
556
|
+
// Common in headers or code blocks
|
|
557
|
+
const regex = /(GET|POST|PUT|DELETE|PATCH)\s+(\/[a-zA-Z0-9\-\/_{}]+)/g;
|
|
558
|
+
let match;
|
|
559
|
+
while ((match = regex.exec(content)) !== null) {
|
|
560
|
+
const [fullMatch, method, path] = match;
|
|
561
|
+
const key = `${method.toUpperCase()} ${path}`;
|
|
562
|
+
|
|
563
|
+
// Only index if not already present (prioritize first occurrence which is usually definition)
|
|
564
|
+
if (!this.endpointIndex.has(key)) {
|
|
565
|
+
this.endpointIndex.set(key, {
|
|
566
|
+
method: method.toUpperCase(),
|
|
567
|
+
path,
|
|
568
|
+
docPath
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Search documents by query
|
|
576
|
+
*/
|
|
577
|
+
search(query: string, limit: number = 10): SearchResult[] {
|
|
578
|
+
const queryLower = query.toLowerCase();
|
|
579
|
+
const queryTerms = queryLower.split(/\s+/).filter((t) => t.length > 2);
|
|
580
|
+
|
|
581
|
+
const results: SearchResult[] = [];
|
|
582
|
+
|
|
583
|
+
for (const doc of this.docs.values()) {
|
|
584
|
+
let score = 0;
|
|
585
|
+
const contentLower = doc.content.toLowerCase();
|
|
586
|
+
const titleLower = doc.title.toLowerCase();
|
|
587
|
+
|
|
588
|
+
// Exact phrase match in title is highest value
|
|
589
|
+
if (titleLower.includes(queryLower)) {
|
|
590
|
+
score += 100;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Exact phrase match in content
|
|
594
|
+
if (contentLower.includes(queryLower)) {
|
|
595
|
+
score += 30;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Title matches individual terms
|
|
599
|
+
for (const term of queryTerms) {
|
|
600
|
+
if (titleLower.includes(term)) {
|
|
601
|
+
score += 15;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Keyword matches
|
|
606
|
+
for (const keyword of doc.keywords) {
|
|
607
|
+
// Exact phrase match in keyword
|
|
608
|
+
if (keyword.includes(queryLower)) {
|
|
609
|
+
score += 20;
|
|
610
|
+
}
|
|
611
|
+
for (const term of queryTerms) {
|
|
612
|
+
if (keyword.includes(term)) {
|
|
613
|
+
score += 5;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Content matches - but cap it to avoid long docs dominating
|
|
619
|
+
for (const term of queryTerms) {
|
|
620
|
+
const regex = new RegExp(term, "gi");
|
|
621
|
+
const matches = contentLower.match(regex);
|
|
622
|
+
if (matches) {
|
|
623
|
+
// Cap at 10 matches per term to avoid SUMMARY.md type docs
|
|
624
|
+
score += Math.min(matches.length, 10);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Penalize index/summary files that match everything
|
|
629
|
+
if (doc.relativePath.includes("SUMMARY") || doc.relativePath.includes("README.md")) {
|
|
630
|
+
score = Math.floor(score * 0.5);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (score > 0) {
|
|
634
|
+
// Extract relevant snippet
|
|
635
|
+
const snippet = this.extractSmartSnippet(doc.content, queryTerms, queryLower);
|
|
636
|
+
|
|
637
|
+
results.push({
|
|
638
|
+
path: doc.relativePath,
|
|
639
|
+
title: doc.title,
|
|
640
|
+
category: doc.category,
|
|
641
|
+
snippet,
|
|
642
|
+
score,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Sort by score descending
|
|
648
|
+
results.sort((a, b) => b.score - a.score);
|
|
649
|
+
|
|
650
|
+
return results.slice(0, limit);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* "Smart" snippet extraction:
|
|
655
|
+
* - Tries to return full paragraphs or code blocks
|
|
656
|
+
* - Avoids cutting off sentences
|
|
657
|
+
*/
|
|
658
|
+
private extractSmartSnippet(content: string, queryTerms: string[], fullQuery: string): string {
|
|
659
|
+
const contentLower = content.toLowerCase();
|
|
660
|
+
|
|
661
|
+
// Find the best match position
|
|
662
|
+
let idx = contentLower.indexOf(fullQuery);
|
|
663
|
+
if (idx === -1) {
|
|
664
|
+
for (const term of queryTerms) {
|
|
665
|
+
idx = contentLower.indexOf(term);
|
|
666
|
+
if (idx !== -1) break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (idx === -1) {
|
|
671
|
+
// Fallback to start of file
|
|
672
|
+
return content.slice(0, 200).replace(/\n+/g, " ").trim() + "...";
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Expand to paragraph boundaries (double newlines)
|
|
676
|
+
const startSearch = Math.max(0, idx - 500);
|
|
677
|
+
const endSearch = Math.min(content.length, idx + 500);
|
|
678
|
+
|
|
679
|
+
const preText = content.slice(startSearch, idx);
|
|
680
|
+
const postText = content.slice(idx, endSearch);
|
|
681
|
+
|
|
682
|
+
// Find start of paragraph (last \n\n before match)
|
|
683
|
+
const paraStart = preText.lastIndexOf("\n\n");
|
|
684
|
+
const start = paraStart !== -1 ? startSearch + paraStart + 2 : Math.max(0, idx - 100);
|
|
685
|
+
|
|
686
|
+
// Find end of paragraph (first \n\n after match)
|
|
687
|
+
const paraEnd = postText.indexOf("\n\n");
|
|
688
|
+
const end = paraEnd !== -1 ? idx + paraEnd : Math.min(content.length, idx + 200);
|
|
689
|
+
|
|
690
|
+
// Check if we are inside a code block
|
|
691
|
+
// Simple heuristic: count backticks before match
|
|
692
|
+
const backticksBefore = (content.slice(0, idx).match(/```/g) || []).length;
|
|
693
|
+
if (backticksBefore % 2 !== 0) {
|
|
694
|
+
// We are likely inside a code block. Try to capture the whole block.
|
|
695
|
+
const blockStart = content.lastIndexOf("```", idx);
|
|
696
|
+
const blockEnd = content.indexOf("```", idx);
|
|
697
|
+
if (blockStart !== -1 && blockEnd !== -1) {
|
|
698
|
+
return content.slice(blockStart, blockEnd + 3);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
let snippet = content.slice(start, end).trim();
|
|
703
|
+
if (start > 0) snippet = "..." + snippet;
|
|
704
|
+
if (end < content.length) snippet = snippet + "...";
|
|
705
|
+
|
|
706
|
+
return snippet;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Get a specific document by path
|
|
711
|
+
*/
|
|
712
|
+
getDocument(path: string): DocEntry | undefined {
|
|
713
|
+
// Try exact match first
|
|
714
|
+
if (this.docs.has(path)) {
|
|
715
|
+
return this.docs.get(path);
|
|
716
|
+
}
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Find potential matches for a partial path
|
|
722
|
+
*/
|
|
723
|
+
findDocuments(partialPath: string): DocEntry[] {
|
|
724
|
+
const matches: DocEntry[] = [];
|
|
725
|
+
for (const [key, doc] of this.docs) {
|
|
726
|
+
if (key.includes(partialPath) || partialPath.includes(key)) {
|
|
727
|
+
matches.push(doc);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return matches;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Get endpoint details directly
|
|
735
|
+
*/
|
|
736
|
+
getEndpoint(method: string, path: string): ApiEndpoint | undefined {
|
|
737
|
+
const key = `${method.toUpperCase()} ${path}`;
|
|
738
|
+
return this.endpointIndex.get(key);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Get all indexed endpoints
|
|
743
|
+
*/
|
|
744
|
+
getAllEndpoints(): ApiEndpoint[] {
|
|
745
|
+
return Array.from(this.endpointIndex.values());
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Extract code examples related to a query
|
|
750
|
+
*/
|
|
751
|
+
getCodeExamples(query: string, limit: number = 5): Array<{ title: string; language: string; code: string }> {
|
|
752
|
+
// First find relevant docs
|
|
753
|
+
const docs = this.search(query, limit);
|
|
754
|
+
const examples: Array<{ title: string; language: string; code: string }> = [];
|
|
755
|
+
|
|
756
|
+
for (const res of docs) {
|
|
757
|
+
const doc = this.docs.get(res.path);
|
|
758
|
+
if (!doc) continue;
|
|
759
|
+
|
|
760
|
+
// Extract code blocks
|
|
761
|
+
const regex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
762
|
+
let match;
|
|
763
|
+
while ((match = regex.exec(doc.content)) !== null) {
|
|
764
|
+
const [_, language, code] = match;
|
|
765
|
+
if (code.length > 20) { // Filter out tiny snippets
|
|
766
|
+
examples.push({
|
|
767
|
+
title: doc.title,
|
|
768
|
+
language: language || "text",
|
|
769
|
+
code: code.trim()
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (examples.length >= limit) break;
|
|
773
|
+
}
|
|
774
|
+
if (examples.length >= limit) break;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return examples;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* List all documents, optionally filtered by category
|
|
782
|
+
*/
|
|
783
|
+
listDocuments(category?: string): Array<{ path: string; title: string; category: string }> {
|
|
784
|
+
const results: Array<{ path: string; title: string; category: string }> = [];
|
|
785
|
+
|
|
786
|
+
for (const doc of this.docs.values()) {
|
|
787
|
+
if (!category || doc.category.toLowerCase().includes(category.toLowerCase())) {
|
|
788
|
+
results.push({
|
|
789
|
+
path: doc.relativePath,
|
|
790
|
+
title: doc.title,
|
|
791
|
+
category: doc.category,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return results.sort((a, b) => a.category.localeCompare(b.category));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Get all unique categories
|
|
801
|
+
*/
|
|
802
|
+
getCategories(): string[] {
|
|
803
|
+
const categories = new Set<string>();
|
|
804
|
+
for (const doc of this.docs.values()) {
|
|
805
|
+
categories.add(doc.category);
|
|
806
|
+
}
|
|
807
|
+
return Array.from(categories).sort();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ============================================================================
|
|
811
|
+
// Type Index Methods
|
|
812
|
+
// ============================================================================
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Get a type by exact name (case-insensitive)
|
|
816
|
+
*/
|
|
817
|
+
getType(name: string): TypeEntry | undefined {
|
|
818
|
+
return this.typeIndex.get(name.toLowerCase());
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Search types by name pattern
|
|
823
|
+
*/
|
|
824
|
+
searchTypes(query: string, limit: number = 20): TypeEntry[] {
|
|
825
|
+
const queryLower = query.toLowerCase();
|
|
826
|
+
const results: Array<{ entry: TypeEntry; score: number }> = [];
|
|
827
|
+
|
|
828
|
+
for (const entry of this.typeIndex.values()) {
|
|
829
|
+
const nameLower = entry.name.toLowerCase();
|
|
830
|
+
let score = 0;
|
|
831
|
+
|
|
832
|
+
// Exact match is highest
|
|
833
|
+
if (nameLower === queryLower) {
|
|
834
|
+
score = 1000;
|
|
835
|
+
}
|
|
836
|
+
// Starts with query
|
|
837
|
+
else if (nameLower.startsWith(queryLower)) {
|
|
838
|
+
score = 500;
|
|
839
|
+
}
|
|
840
|
+
// Contains query
|
|
841
|
+
else if (nameLower.includes(queryLower)) {
|
|
842
|
+
score = 100;
|
|
843
|
+
}
|
|
844
|
+
// Word boundary match (e.g., "Wallet" matches "CreateWalletRequest")
|
|
845
|
+
else {
|
|
846
|
+
// Split camelCase/PascalCase into words
|
|
847
|
+
const words = entry.name.split(/(?=[A-Z])/).map(w => w.toLowerCase());
|
|
848
|
+
for (const word of words) {
|
|
849
|
+
if (word === queryLower) {
|
|
850
|
+
score = 200;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
if (word.startsWith(queryLower)) {
|
|
854
|
+
score = Math.max(score, 50);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Also check description
|
|
860
|
+
if (score === 0 && entry.description.toLowerCase().includes(queryLower)) {
|
|
861
|
+
score = 25;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (score > 0) {
|
|
865
|
+
results.push({ entry, score });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Sort by score descending, then by name length (prefer shorter names)
|
|
870
|
+
results.sort((a, b) => {
|
|
871
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
872
|
+
return a.entry.name.length - b.entry.name.length;
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
return results.slice(0, limit).map(r => r.entry);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* List all types, optionally filtered by category
|
|
880
|
+
*/
|
|
881
|
+
listTypes(category?: string): Array<{ name: string; kind: string; category: string; importPath: string }> {
|
|
882
|
+
const results: Array<{ name: string; kind: string; category: string; importPath: string }> = [];
|
|
883
|
+
|
|
884
|
+
for (const entry of this.typeIndex.values()) {
|
|
885
|
+
if (!category || entry.category.toLowerCase().includes(category.toLowerCase())) {
|
|
886
|
+
results.push({
|
|
887
|
+
name: entry.name,
|
|
888
|
+
kind: entry.kind,
|
|
889
|
+
category: entry.category,
|
|
890
|
+
importPath: entry.importPath,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Get all unique type categories
|
|
900
|
+
*/
|
|
901
|
+
getTypeCategories(): string[] {
|
|
902
|
+
const categories = new Set<string>();
|
|
903
|
+
for (const entry of this.typeIndex.values()) {
|
|
904
|
+
categories.add(entry.category);
|
|
905
|
+
}
|
|
906
|
+
return Array.from(categories).sort();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Get total type count
|
|
911
|
+
*/
|
|
912
|
+
getTypeCount(): number {
|
|
913
|
+
return this.typeIndex.size;
|
|
914
|
+
}
|
|
915
|
+
}
|