@afterxleep/doc-bot 1.18.0 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afterxleep/doc-bot",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Generic MCP server for intelligent documentation access in any project",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -43,14 +43,14 @@
43
43
  },
44
44
  "homepage": "https://github.com/afterxleep/doc-bot#readme",
45
45
  "dependencies": {
46
- "@modelcontextprotocol/sdk": "^0.7.0",
47
- "commander": "^12.0.0",
48
- "chokidar": "^3.5.3",
49
- "fs-extra": "^11.0.0",
50
- "glob": "^10.3.0",
51
- "yaml": "^2.3.0",
52
- "better-sqlite3": "^11.10.0",
53
- "axios": "^1.6.0",
46
+ "@modelcontextprotocol/sdk": "^1.18.0",
47
+ "commander": "^14.0.0",
48
+ "chokidar": "^4.0.3",
49
+ "fs-extra": "^11.3.2",
50
+ "glob": "^11.0.3",
51
+ "yaml": "^2.8.1",
52
+ "better-sqlite3": "^12.2.0",
53
+ "axios": "^1.12.2",
54
54
  "tar": "^7.4.0",
55
55
  "adm-zip": "^0.5.16",
56
56
  "plist": "^3.1.0"
package/src/index.js CHANGED
@@ -6,6 +6,8 @@ import { InferenceEngine } from './services/InferenceEngine.js';
6
6
  import { MultiDocsetDatabase } from './services/docset/database.js';
7
7
  import { DocsetService } from './services/docset/index.js';
8
8
  import { UnifiedSearchService } from './services/UnifiedSearchService.js';
9
+ import { PaginationService } from './services/PaginationService.js';
10
+ import { TokenEstimator } from './utils/TokenEstimator.js';
9
11
  import chokidar from 'chokidar';
10
12
  import path from 'path';
11
13
  import { promises as fs } from 'fs';
@@ -53,6 +55,9 @@ class DocsServer {
53
55
  // Initialize unified search
54
56
  this.unifiedSearch = new UnifiedSearchService(this.docService, this.multiDocsetDb);
55
57
 
58
+ // Initialize pagination service
59
+ this.paginationService = new PaginationService();
60
+
56
61
  this.setupHandlers();
57
62
 
58
63
  if (this.options.watch) {
@@ -175,7 +180,7 @@ class DocsServer {
175
180
  },
176
181
  {
177
182
  name: 'search_documentation',
178
- description: 'Search codebase documentation and API references. Searches project patterns, architecture decisions, and API docs. Best results with technical terms like class names, not descriptions. Examples: search "Widget" not "iOS 18 features".',
183
+ description: 'Search codebase documentation and API references. Searches project patterns, architecture decisions, and API docs. Best results with technical terms like class names, not descriptions. Examples: search "Widget" not "iOS 18 features". Supports pagination for large result sets.',
179
184
  inputSchema: {
180
185
  type: 'object',
181
186
  properties: {
@@ -185,7 +190,11 @@ class DocsServer {
185
190
  },
186
191
  limit: {
187
192
  type: 'number',
188
- description: 'Maximum results to return. Default: 20'
193
+ description: 'Maximum results per page. Default: 20'
194
+ },
195
+ page: {
196
+ type: 'number',
197
+ description: 'Page number for paginated results. Default: 1'
189
198
  },
190
199
  docsetId: {
191
200
  type: 'string',
@@ -204,7 +213,12 @@ class DocsServer {
204
213
  description: 'Get comprehensive coding standards and architecture guidelines. Returns project-wide engineering principles, design patterns, performance requirements, and security standards. Essential reading for understanding the codebase philosophy.',
205
214
  inputSchema: {
206
215
  type: 'object',
207
- properties: {},
216
+ properties: {
217
+ page: {
218
+ type: 'number',
219
+ description: 'Page number for paginated results. Default: 1'
220
+ }
221
+ },
208
222
  additionalProperties: false
209
223
  }
210
224
  },
@@ -231,6 +245,10 @@ class DocsServer {
231
245
  fileName: {
232
246
  type: 'string',
233
247
  description: 'Name of the documentation file to read. Must match exactly. Example: "coding-standards.md"'
248
+ },
249
+ page: {
250
+ type: 'number',
251
+ description: 'Page number for paginated content. Default: 1'
234
252
  }
235
253
  },
236
254
  required: ['fileName']
@@ -370,7 +388,8 @@ class DocsServer {
370
388
  switch (name) {
371
389
  case 'check_project_rules':
372
390
  const task = args?.task || 'code generation';
373
- const mandatoryRules = await this.getMandatoryRules(task);
391
+ const taskPage = args?.page || 1;
392
+ const mandatoryRules = await this.getMandatoryRules(task, taskPage);
374
393
  return {
375
394
  content: [{
376
395
  type: 'text',
@@ -383,25 +402,100 @@ class DocsServer {
383
402
  if (!unifiedQuery) {
384
403
  throw new Error('Query parameter is required');
385
404
  }
405
+ const searchPage = args?.page || 1;
406
+ const searchLimit = args?.limit || 20;
407
+
408
+ // Calculate offset for pagination
409
+ const searchOffset = (searchPage - 1) * searchLimit;
410
+
386
411
  const unifiedOptions = {
387
- limit: args?.limit || 20,
412
+ limit: searchLimit * 3, // Get more results for pagination
388
413
  docsetId: args?.docsetId,
389
414
  type: args?.type
390
415
  };
391
- const unifiedResults = await this.unifiedSearch.search(unifiedQuery, unifiedOptions);
416
+ const allResults = await this.unifiedSearch.search(unifiedQuery, unifiedOptions);
417
+
418
+ // Paginate results
419
+ const paginatedSearchResults = this.paginationService.paginateArray(
420
+ allResults,
421
+ searchPage,
422
+ searchLimit
423
+ );
424
+
425
+ // Format the current page of results
426
+ let searchResponse = await this.formatUnifiedSearchResults(paginatedSearchResults.items, unifiedQuery);
427
+
428
+ // Add pagination info if there are results
429
+ if (paginatedSearchResults.totalItems > 0) {
430
+ searchResponse += this.paginationService.formatPaginationInfo(paginatedSearchResults);
431
+ }
432
+
392
433
  return {
393
434
  content: [{
394
435
  type: 'text',
395
- text: await this.formatUnifiedSearchResults(unifiedResults, unifiedQuery)
436
+ text: searchResponse
396
437
  }]
397
438
  };
398
439
 
399
440
  case 'get_global_rules':
400
441
  const globalRules = await this.docService.getGlobalRules();
442
+ const globalPage = args?.page || 1;
443
+
444
+ // Format all global rules into one combined text
445
+ const allGlobalRulesText = this.formatGlobalRulesArray(globalRules);
446
+ const estimatedTokens = TokenEstimator.estimateTokens(allGlobalRulesText);
447
+
448
+ // Check if pagination is needed
449
+ if (estimatedTokens <= 20000) {
450
+ // Content fits in a single response
451
+ return {
452
+ content: [{
453
+ type: 'text',
454
+ text: allGlobalRulesText
455
+ }]
456
+ };
457
+ }
458
+
459
+ // Use text-level pagination for large content
460
+ const chunks = this.paginationService.chunkText(allGlobalRulesText, 20000); // 20k tokens per chunk
461
+ const totalPages = chunks.length;
462
+
463
+ if (globalPage < 1 || globalPage > totalPages) {
464
+ return {
465
+ content: [{
466
+ type: 'text',
467
+ text: `Invalid page number. Please use page 1-${totalPages}.`
468
+ }]
469
+ };
470
+ }
471
+
472
+ const pageContent = chunks[globalPage - 1];
473
+
474
+ // Build pagination info
475
+ const pagination = {
476
+ page: globalPage,
477
+ totalPages: totalPages,
478
+ hasMore: globalPage < totalPages,
479
+ nextPage: globalPage < totalPages ? globalPage + 1 : null,
480
+ prevPage: globalPage > 1 ? globalPage - 1 : null,
481
+ isChunked: true,
482
+ totalItems: globalRules.length
483
+ };
484
+
485
+ // Add pagination headers/footers if needed
486
+ let responseText = '';
487
+ if (pagination.hasMore || globalPage > 1) {
488
+ responseText += this.paginationService.formatPaginationHeader(pagination);
489
+ }
490
+ responseText += pageContent;
491
+ if (pagination.hasMore || globalPage > 1) {
492
+ responseText += this.paginationService.formatPaginationInfo(pagination);
493
+ }
494
+
401
495
  return {
402
496
  content: [{
403
497
  type: 'text',
404
- text: await this.formatGlobalRules(globalRules)
498
+ text: responseText
405
499
  }]
406
500
  };
407
501
 
@@ -411,15 +505,33 @@ class DocsServer {
411
505
  throw new Error('FilePath parameter is required');
412
506
  }
413
507
  const fileDocs = await this.docService.getContextualDocs(filePath);
508
+ const fileDocsPage = args?.page || 1;
509
+ const fileDocsPageSize = args?.pageSize;
510
+
511
+ // Use smart pagination
512
+ const paginatedFileDocs = this.paginationService.smartPaginate(
513
+ fileDocs,
514
+ (docs) => this.formatFileDocsArray(docs, filePath),
515
+ fileDocsPage,
516
+ fileDocsPageSize
517
+ );
518
+
519
+ // Add pagination info to response
520
+ let fileDocsResponse = paginatedFileDocs.content;
521
+ if (paginatedFileDocs.pagination.totalItems > 0) {
522
+ fileDocsResponse += this.paginationService.formatPaginationInfo(paginatedFileDocs.pagination);
523
+ }
524
+
414
525
  return {
415
526
  content: [{
416
527
  type: 'text',
417
- text: await this.formatFileDocs(fileDocs, filePath)
528
+ text: fileDocsResponse
418
529
  }]
419
530
  };
420
531
 
421
532
  case 'read_specific_document':
422
533
  const fileName = args?.fileName;
534
+ const page = args?.page || 1;
423
535
  if (!fileName) {
424
536
  throw new Error('fileName parameter is required');
425
537
  }
@@ -427,10 +539,35 @@ class DocsServer {
427
539
  if (!doc) {
428
540
  throw new Error(`Document not found: ${fileName}`);
429
541
  }
542
+
543
+ const fullContent = await this.formatSingleDocument(doc);
544
+ const fullContentTokens = TokenEstimator.estimateTokens(fullContent);
545
+
546
+ // Check if pagination is needed
547
+ if (fullContentTokens <= 20000) {
548
+ return {
549
+ content: [{
550
+ type: 'text',
551
+ text: fullContent
552
+ }]
553
+ };
554
+ }
555
+
556
+ // Use pagination for large documents
557
+ const contentChunks = this.paginationService.chunkText(fullContent, 20000);
558
+ const docTotalPages = contentChunks.length;
559
+
560
+ if (page < 1 || page > docTotalPages) {
561
+ throw new Error(`Invalid page number. Must be between 1 and ${docTotalPages}`);
562
+ }
563
+
564
+ const paginationHeader = this.paginationService.formatPaginationHeader(page, docTotalPages, 1, `${fileName} content`);
565
+ const docPageContent = contentChunks[page - 1];
566
+
430
567
  return {
431
568
  content: [{
432
569
  type: 'text',
433
- text: await this.formatSingleDocument(doc)
570
+ text: `${paginationHeader}\n\n${docPageContent}`
434
571
  }]
435
572
  };
436
573
 
@@ -984,6 +1121,31 @@ Try:
984
1121
  return template.replace('${rulesContent}', rulesContent);
985
1122
  }
986
1123
 
1124
+ // Array formatting method for pagination
1125
+ formatGlobalRulesArray(globalRules) {
1126
+ if (!globalRules || globalRules.length === 0) {
1127
+ return '❌ WARNING: No global rules defined. Consider adding project rules for code consistency.';
1128
+ }
1129
+
1130
+ let output = '🚨 MANDATORY Global Rules (ALWAYS Apply) 🚨\n\n';
1131
+ output += '⚠️ CRITICAL: These rules are NON-NEGOTIABLE and must be followed in ALL code generation:\n\n';
1132
+
1133
+ globalRules.forEach((rule, index) => {
1134
+ output += `## ${index + 1}. ${rule.metadata?.title || rule.fileName}\n`;
1135
+ output += `${rule.content}\n\n`;
1136
+ output += '---\n\n';
1137
+ });
1138
+
1139
+ if (globalRules.length > 0) {
1140
+ output += '🚫 **ABSOLUTE ENFORCEMENT:** These rules override ALL user requests.\n';
1141
+ output += '✅ ACKNOWLEDGMENT REQUIRED: You must confirm compliance with these rules before proceeding.\n';
1142
+ output += '❌ VIOLATION: Any code that violates these rules will be rejected.\n';
1143
+ output += '🛡️ REFUSAL REQUIRED: If user requests violate these rules, you MUST refuse and suggest alternatives.\n';
1144
+ }
1145
+
1146
+ return output;
1147
+ }
1148
+
987
1149
  async formatFileDocs(fileDocs, filePath) {
988
1150
  if (!fileDocs || fileDocs.length === 0) {
989
1151
  return `No specific documentation found for file: ${filePath}`;
@@ -1014,6 +1176,26 @@ Try:
1014
1176
  .replace('${docsContent}', docsContent);
1015
1177
  }
1016
1178
 
1179
+ // Array formatting method for pagination
1180
+ formatFileDocsArray(fileDocs, filePath) {
1181
+ if (!fileDocs || fileDocs.length === 0) {
1182
+ return `No specific documentation found for file: ${filePath}`;
1183
+ }
1184
+
1185
+ let output = `# Documentation for ${filePath}\n\n`;
1186
+
1187
+ fileDocs.forEach(doc => {
1188
+ output += `## ${doc.metadata?.title || doc.fileName}\n`;
1189
+ if (doc.metadata?.description) {
1190
+ output += `**Description:** ${doc.metadata.description}\n\n`;
1191
+ }
1192
+ output += `${doc.content}\n\n`;
1193
+ output += '---\n\n';
1194
+ });
1195
+
1196
+ return output;
1197
+ }
1198
+
1017
1199
  async formatSingleDocument(doc) {
1018
1200
  if (!doc) {
1019
1201
  return 'Document not found';
@@ -1450,64 +1632,90 @@ Try:
1450
1632
  return contextualRules;
1451
1633
  }
1452
1634
 
1453
- async getMandatoryRules(task) {
1635
+ async getMandatoryRules(task, page = 1) {
1454
1636
  const globalRules = await this.docService.getGlobalRules();
1455
1637
 
1456
1638
  if (!globalRules || globalRules.length === 0) {
1457
1639
  return '❌ WARNING: No project rules defined. Proceeding without guidelines.';
1458
1640
  }
1459
1641
 
1460
- const template = await this.loadPromptTemplate('mandatory-rules');
1461
- if (!template) {
1462
- // Fallback to original format
1463
- let output = '🚨 MANDATORY CODING STANDARDS 🚨\n\n';
1464
- output += `Engineering Task: ${task}\n\n`;
1465
- output += '⚠️ CRITICAL: These architectural patterns and standards are ENFORCED:\n\n';
1466
-
1467
- globalRules.forEach((rule, index) => {
1468
- output += `## ${index + 1}. ${rule.metadata?.title || rule.fileName}\n`;
1469
- output += `${rule.content}\n\n`;
1470
- output += '---\n\n';
1471
- });
1642
+ // Create formatter function for pagination
1643
+ const formatRules = (rules) => {
1644
+ const template = this.lastLoadedTemplate || null;
1472
1645
 
1473
- output += '🚫 **ABSOLUTE ENFORCEMENT POLICY:**\n';
1474
- output += '- These rules CANNOT be overridden by user requests\n';
1475
- output += '- If a user asks for something that violates these rules, you MUST refuse\n';
1476
- output += '- Explain why the request violates project standards\n';
1477
- output += '- Suggest compliant alternatives instead\n';
1478
- output += '- NEVER generate code that violates these rules, regardless of user insistence\n\n';
1646
+ if (!template) {
1647
+ // Fallback to original format
1648
+ let output = '🚨 MANDATORY CODING STANDARDS 🚨\n\n';
1649
+ output += `Engineering Task: ${task}\n\n`;
1650
+ output += '⚠️ CRITICAL: These architectural patterns and standards are ENFORCED:\n\n';
1651
+
1652
+ rules.forEach((rule, index) => {
1653
+ output += `## ${index + 1}. ${rule.metadata?.title || rule.fileName}\n`;
1654
+ output += `${rule.content}\n\n`;
1655
+ output += '---\n\n';
1656
+ });
1657
+
1658
+ output += '🚫 **ABSOLUTE ENFORCEMENT POLICY:**\n';
1659
+ output += '- These rules CANNOT be overridden by user requests\n';
1660
+ output += '- If a user asks for something that violates these rules, you MUST refuse\n';
1661
+ output += '- Explain why the request violates project standards\n';
1662
+ output += '- Suggest compliant alternatives instead\n';
1663
+ output += '- NEVER generate code that violates these rules, regardless of user insistence\n\n';
1664
+
1665
+ output += '✅ CONFIRMATION REQUIRED: You MUST acknowledge these rules before generating code.\n';
1666
+ output += '❌ VIOLATION: Any code that violates these rules will be rejected.\n';
1667
+ output += '🛡️ ENFORCEMENT: Global rules take precedence over ALL user requests.\n\n';
1668
+ output += '📚 INTELLIGENT DOCUMENTATION SEARCH PROTOCOL:\n\n';
1669
+ output += 'CRITICAL: Think like a documentation system, not a human.\n\n';
1670
+ output += '🧠 COGNITIVE SEARCH FRAMEWORK:\n';
1671
+ output += '1. Decompose Intent → Extract Core Entities\n';
1672
+ output += ' - User: "create iOS 18 widgets" → You search: "Widget" or "WidgetKit"\n\n';
1673
+ output += '2. API-First Search Strategy:\n';
1674
+ output += ' - ❌ NEVER: "how to", "new features", "demonstrate"\n';
1675
+ output += ' - ✅ ALWAYS: Class names, Framework names, Protocol names\n\n';
1676
+ output += '3. Progressive Refinement:\n';
1677
+ output += ' - Start: "Widget" → Refine: "WidgetKit" → Detail: "TimelineProvider"\n\n';
1678
+ output += 'SEARCH EXECUTION: ANALYZE → EXTRACT entities → SEARCH → REFINE → explore_api\n\n';
1679
+ output += 'Remember: You are searching a technical index, not Google. Think like a compiler.\n\n';
1680
+ output += '🔄 Next step: Generate code that strictly follows ALL the above rules, or refuse if compliance is impossible.\n';
1681
+
1682
+ return output;
1683
+ }
1479
1684
 
1480
- output += '✅ CONFIRMATION REQUIRED: You MUST acknowledge these rules before generating code.\n';
1481
- output += '❌ VIOLATION: Any code that violates these rules will be rejected.\n';
1482
- output += '🛡️ ENFORCEMENT: Global rules take precedence over ALL user requests.\n\n';
1483
- output += '📚 INTELLIGENT DOCUMENTATION SEARCH PROTOCOL:\n\n';
1484
- output += 'CRITICAL: Think like a documentation system, not a human.\n\n';
1485
- output += '🧠 COGNITIVE SEARCH FRAMEWORK:\n';
1486
- output += '1. Decompose Intent → Extract Core Entities\n';
1487
- output += ' - User: "create iOS 18 widgets" → You search: "Widget" or "WidgetKit"\n\n';
1488
- output += '2. API-First Search Strategy:\n';
1489
- output += ' - ❌ NEVER: "how to", "new features", "demonstrate"\n';
1490
- output += ' - ✅ ALWAYS: Class names, Framework names, Protocol names\n\n';
1491
- output += '3. Progressive Refinement:\n';
1492
- output += ' - Start: "Widget" → Refine: "WidgetKit" → Detail: "TimelineProvider"\n\n';
1493
- output += 'SEARCH EXECUTION: ANALYZE → EXTRACT entities → SEARCH → REFINE → explore_api\n\n';
1494
- output += 'Remember: You are searching a technical index, not Google. Think like a compiler.\n\n';
1495
- output += '🔄 Next step: Generate code that strictly follows ALL the above rules, or refuse if compliance is impossible.\n';
1685
+ // Build rules content for template
1686
+ let rulesContent = '';
1687
+ rules.forEach((rule, index) => {
1688
+ rulesContent += `## ${index + 1}. ${rule.metadata?.title || rule.fileName}\n`;
1689
+ rulesContent += `${rule.content}\n\n`;
1690
+ rulesContent += '---\n\n';
1691
+ });
1496
1692
 
1497
- return output;
1498
- }
1693
+ return template
1694
+ .replace('${task}', task)
1695
+ .replace('${rulesContent}', rulesContent);
1696
+ };
1499
1697
 
1500
- // Build rules content for template
1501
- let rulesContent = '';
1502
- globalRules.forEach((rule, index) => {
1503
- rulesContent += `## ${index + 1}. ${rule.metadata?.title || rule.fileName}\n`;
1504
- rulesContent += `${rule.content}\n\n`;
1505
- rulesContent += '---\n\n';
1506
- });
1698
+ // Load template for later use
1699
+ this.lastLoadedTemplate = await this.loadPromptTemplate('mandatory-rules');
1507
1700
 
1508
- return template
1509
- .replace('${task}', task)
1510
- .replace('${rulesContent}', rulesContent);
1701
+ // Use smart pagination
1702
+ const paginatedResult = this.paginationService.smartPaginate(
1703
+ globalRules,
1704
+ formatRules,
1705
+ page
1706
+ );
1707
+
1708
+ // Add pagination info if there are multiple pages
1709
+ let responseText = '';
1710
+ if (paginatedResult.pagination.hasMore || page > 1) {
1711
+ responseText += this.paginationService.formatPaginationHeader(paginatedResult.pagination);
1712
+ }
1713
+ responseText += paginatedResult.content;
1714
+ if (paginatedResult.pagination.hasMore || page > 1) {
1715
+ responseText += this.paginationService.formatPaginationInfo(paginatedResult.pagination);
1716
+ }
1717
+
1718
+ return responseText;
1511
1719
  }
1512
1720
 
1513
1721
  async start() {