mcp-docs-service 0.5.2 → 7.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.
@@ -7,6 +7,16 @@ import fs from "fs/promises";
7
7
  import path from "path";
8
8
  import { glob } from "glob";
9
9
  import { createTwoFilesPatch } from "diff";
10
+ import { writeFileSync } from "fs";
11
+ // Log debug info to file
12
+ function debugLog(message) {
13
+ try {
14
+ writeFileSync("docs/debug_log.txt", message + "\n", { flag: "a" });
15
+ }
16
+ catch (error) {
17
+ // Silently fail if we can't write to the file
18
+ }
19
+ }
10
20
  // File editing and diffing utilities
11
21
  function normalizeLineEndings(text) {
12
22
  return text.replace(/\r\n/g, "\n");
@@ -54,8 +64,12 @@ export function parseFrontmatter(content) {
54
64
  }
55
65
  export class DocumentHandler {
56
66
  docsDir;
57
- constructor(docsDir) {
67
+ singleDocPath;
68
+ useSingleDoc;
69
+ constructor(docsDir, useSingleDoc = false) {
58
70
  this.docsDir = docsDir;
71
+ this.useSingleDoc = useSingleDoc;
72
+ this.singleDocPath = path.join(docsDir, "single_doc.md");
59
73
  }
60
74
  /**
61
75
  * Validates that a path is within the docs directory
@@ -77,13 +91,87 @@ export class DocumentHandler {
77
91
  */
78
92
  async readDocument(docPath) {
79
93
  try {
94
+ // If single-doc mode is enabled, redirect to the single doc
95
+ if (this.useSingleDoc) {
96
+ const singleDocPath = path.join(this.docsDir, "single_doc.md");
97
+ // Check if the requested path is a specific part
98
+ if (docPath.startsWith("single_doc_part") && docPath.endsWith(".md")) {
99
+ const requestedPartPath = path.join(this.docsDir, docPath);
100
+ // Check if the requested part exists
101
+ try {
102
+ await fs.access(requestedPartPath);
103
+ const content = await fs.readFile(requestedPartPath, "utf-8");
104
+ return {
105
+ content: [{ type: "text", text: content }],
106
+ metadata: {
107
+ path: docPath,
108
+ ...this.parseFrontmatter(content).frontmatter,
109
+ },
110
+ };
111
+ }
112
+ catch (error) {
113
+ // If the part doesn't exist, fall back to the index
114
+ if (await this.fileExists(singleDocPath)) {
115
+ const content = await fs.readFile(singleDocPath, "utf-8");
116
+ return {
117
+ content: [{ type: "text", text: content }],
118
+ metadata: {
119
+ path: "single_doc.md",
120
+ ...this.parseFrontmatter(content).frontmatter,
121
+ },
122
+ };
123
+ }
124
+ }
125
+ }
126
+ // For all other requested documents in single-doc mode, serve the single document
127
+ if (await this.fileExists(singleDocPath)) {
128
+ const content = await fs.readFile(singleDocPath, "utf-8");
129
+ // Check if this is an index file (indicating multi-part doc)
130
+ const { frontmatter } = this.parseFrontmatter(content);
131
+ if (content.includes("DOCUMENTATION INDEX") ||
132
+ content.includes("Available Parts")) {
133
+ return {
134
+ content: [{ type: "text", text: content }],
135
+ metadata: {
136
+ path: "single_doc.md",
137
+ ...frontmatter,
138
+ isIndex: true,
139
+ },
140
+ };
141
+ }
142
+ return {
143
+ content: [{ type: "text", text: content }],
144
+ metadata: {
145
+ path: "single_doc.md",
146
+ ...frontmatter,
147
+ },
148
+ };
149
+ }
150
+ else {
151
+ // If single-doc doesn't exist yet, try to generate it
152
+ await this.refreshSingleDoc();
153
+ // Check if it was created successfully
154
+ if (await this.fileExists(singleDocPath)) {
155
+ const content = await fs.readFile(singleDocPath, "utf-8");
156
+ return {
157
+ content: [{ type: "text", text: content }],
158
+ metadata: {
159
+ path: "single_doc.md",
160
+ ...this.parseFrontmatter(content).frontmatter,
161
+ },
162
+ };
163
+ }
164
+ // If it still doesn't exist, fall back to the requested path
165
+ }
166
+ }
167
+ // Normal mode or fallback: read the actual requested document
80
168
  const validPath = await this.validatePath(docPath);
81
169
  const content = await fs.readFile(validPath, "utf-8");
82
170
  return {
83
171
  content: [{ type: "text", text: content }],
84
172
  metadata: {
85
173
  path: docPath,
86
- ...parseFrontmatter(content).frontmatter,
174
+ ...this.parseFrontmatter(content).frontmatter,
87
175
  },
88
176
  };
89
177
  }
@@ -97,6 +185,18 @@ export class DocumentHandler {
97
185
  };
98
186
  }
99
187
  }
188
+ /**
189
+ * Helper method to check if a file exists
190
+ */
191
+ async fileExists(filePath) {
192
+ try {
193
+ await fs.access(filePath);
194
+ return true;
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
100
200
  /**
101
201
  * Write a document to the docs directory
102
202
  */
@@ -108,7 +208,31 @@ export class DocumentHandler {
108
208
  const dirPath = path.dirname(validPath);
109
209
  await fs.mkdir(dirPath, { recursive: true });
110
210
  }
211
+ // Write the document
111
212
  await fs.writeFile(validPath, content, "utf-8");
213
+ // If in single-doc mode, refresh the single doc
214
+ if (this.useSingleDoc && !docPath.startsWith("single_doc")) {
215
+ try {
216
+ // Check if we have multi-part docs
217
+ const parts = await glob(path.join(this.docsDir, "single_doc_part*.md"));
218
+ if (parts.length > 0) {
219
+ // If we have multi-part docs, refresh all parts
220
+ await this.refreshSingleDoc();
221
+ }
222
+ else {
223
+ // Check if single_doc.md exists
224
+ const singleDocPath = path.join(this.docsDir, "single_doc.md");
225
+ if (await this.fileExists(singleDocPath)) {
226
+ // Refresh the single doc
227
+ await this.refreshSingleDoc();
228
+ }
229
+ }
230
+ }
231
+ catch (refreshError) {
232
+ // Log but don't fail if refreshing fails
233
+ console.error("Error refreshing single doc:", refreshError);
234
+ }
235
+ }
112
236
  return {
113
237
  content: [{ type: "text", text: `Successfully wrote to ${docPath}` }],
114
238
  };
@@ -280,256 +404,311 @@ export class DocumentHandler {
280
404
  }
281
405
  }
282
406
  /**
283
- * Create a new folder in the docs directory
407
+ * Estimate the number of tokens in a text string
408
+ * This is a simple estimation: ~4 characters per token for English text
284
409
  */
285
- async createFolder(folderPath, createReadme = true) {
286
- try {
287
- const validPath = await this.validatePath(folderPath);
288
- // Create the directory
289
- await fs.mkdir(validPath, { recursive: true });
290
- // Create a README.md file if requested
291
- if (createReadme) {
292
- const readmePath = path.join(validPath, "README.md");
293
- const folderName = path.basename(validPath);
294
- const content = `---
295
- title: ${folderName}
296
- description: Documentation for ${folderName}
297
- date: ${new Date().toISOString()}
298
- status: draft
410
+ estimateTokens(text) {
411
+ return Math.ceil(text.length / 4);
412
+ }
413
+ /**
414
+ * Splits content into multiple documents based on token limits
415
+ * Returns the number of chunks created
416
+ */
417
+ async createDocumentChunks(sections, template, maxTokens) {
418
+ // Create a base template for each chunk
419
+ const projectName = path.basename(path.resolve(this.docsDir, ".."));
420
+ const footer = "\n---\n\n_This document is automatically maintained and optimized for LLM context. This is part {part} of a multi-part document._\n";
421
+ // Token budgeting
422
+ const footerBaseTokens = this.estimateTokens(footer.replace("{part}", "X"));
423
+ const templateTokens = this.estimateTokens(template);
424
+ const availableTokensPerChunk = maxTokens - templateTokens - footerBaseTokens;
425
+ // Track total tokens
426
+ let currentChunk = 1;
427
+ let currentTokens = 0;
428
+ let chunkContent = "";
429
+ for (const [section, content] of sections) {
430
+ // Skip empty sections
431
+ if (content.trim() === `## ${section}`.trim()) {
432
+ continue;
433
+ }
434
+ const sectionTokens = this.estimateTokens(content);
435
+ // Check if we need to start a new chunk
436
+ if (currentTokens + sectionTokens > availableTokensPerChunk &&
437
+ currentTokens > 0) {
438
+ // Write the current chunk to file
439
+ const chunkPath = path.join(this.docsDir, `single_doc_part${currentChunk}.md`);
440
+ const finalChunkContent = template +
441
+ chunkContent +
442
+ footer.replace("{part}", `${currentChunk}`) +
443
+ `\n<!-- Token usage: ${currentTokens + templateTokens + footerBaseTokens}/${maxTokens} -->`;
444
+ await fs.writeFile(chunkPath, finalChunkContent, "utf-8");
445
+ // Reset for next chunk
446
+ currentChunk++;
447
+ currentTokens = 0;
448
+ chunkContent = "";
449
+ }
450
+ // Add section to current chunk
451
+ chunkContent += content + "\n";
452
+ currentTokens += sectionTokens;
453
+ }
454
+ // Write the last chunk if there's any content
455
+ if (chunkContent) {
456
+ const chunkPath = path.join(this.docsDir, `single_doc_part${currentChunk}.md`);
457
+ const finalChunkContent = template +
458
+ chunkContent +
459
+ footer.replace("{part}", `${currentChunk} of ${currentChunk}`) +
460
+ `\n<!-- Token usage: ${currentTokens + templateTokens + footerBaseTokens}/${maxTokens} -->`;
461
+ await fs.writeFile(chunkPath, finalChunkContent, "utf-8");
462
+ }
463
+ // Create an index file that lists all parts
464
+ if (currentChunk > 1) {
465
+ const indexPath = path.join(this.docsDir, "single_doc.md");
466
+ let indexContent = `---
467
+ title: ${projectName} Documentation Index
468
+ description: Index of documentation parts optimized for LLM context
469
+ author: Documentation System
470
+ date: "${new Date().toISOString()}"
471
+ tags:
472
+ - documentation
473
+ - llm-optimized
474
+ status: published
475
+ version: 1.0.0
299
476
  ---
300
477
 
301
- # ${folderName}
478
+ # ${projectName.toUpperCase()}: DOCUMENTATION INDEX
479
+
480
+ _This documentation has been split into multiple parts due to its size. Each part is optimized for LLM context._
481
+
482
+ ## Available Parts
302
483
 
303
- This is the documentation for ${folderName}.
304
484
  `;
305
- await fs.writeFile(readmePath, content, "utf-8");
485
+ for (let i = 1; i <= currentChunk; i++) {
486
+ indexContent += `- [Part ${i}](single_doc_part${i}.md)\n`;
306
487
  }
307
- return {
308
- content: [
309
- { type: "text", text: `Successfully created folder: ${folderPath}` },
310
- ],
311
- metadata: {
312
- path: folderPath,
313
- readme: createReadme ? path.join(folderPath, "README.md") : null,
314
- },
315
- };
316
- }
317
- catch (error) {
318
- const errorMessage = error instanceof Error ? error.message : String(error);
319
- return {
320
- content: [
321
- { type: "text", text: `Error creating folder: ${errorMessage}` },
322
- ],
323
- isError: true,
324
- };
488
+ indexContent +=
489
+ "\n---\n\n_This index was automatically generated to manage documentation that exceeds token limits._\n";
490
+ await fs.writeFile(indexPath, indexContent, "utf-8");
325
491
  }
492
+ return currentChunk;
326
493
  }
327
494
  /**
328
- * Move a document to a new location
495
+ * Refresh the single document by recompiling all markdown files
329
496
  */
330
- async moveDocument(sourcePath, destinationPath, updateReferences = true) {
497
+ async refreshSingleDoc() {
498
+ const singleDocPath = path.join(this.docsDir, "single_doc.md");
499
+ const MAX_TOKENS = 100000; // Limit to 100k tokens to fit within LLM context windows
500
+ const MULTI_DOC_THRESHOLD = 9000; // Lower threshold for testing - if estimated tokens exceed this, create multiple docs
331
501
  try {
332
- const validSourcePath = await this.validatePath(sourcePath);
333
- const validDestPath = await this.validatePath(destinationPath);
334
- // Check if source exists
335
- try {
336
- await fs.access(validSourcePath);
337
- }
338
- catch {
339
- throw new Error(`Source file does not exist: ${sourcePath}`);
340
- }
341
- // Create destination directory if it doesn't exist
342
- const destDir = path.dirname(validDestPath);
343
- await fs.mkdir(destDir, { recursive: true });
344
- // Read the source file
345
- const content = await fs.readFile(validSourcePath, "utf-8");
346
- // Write to destination
347
- await fs.writeFile(validDestPath, content, "utf-8");
348
- // Delete the source file
349
- await fs.unlink(validSourcePath);
350
- // Update references if requested
351
- let referencesUpdated = 0;
352
- if (updateReferences) {
353
- referencesUpdated = await this.updateReferences(sourcePath, destinationPath);
354
- }
355
- return {
356
- content: [
357
- {
358
- type: "text",
359
- text: `Successfully moved document from ${sourcePath} to ${destinationPath}` +
360
- (referencesUpdated > 0
361
- ? `. Updated ${referencesUpdated} references.`
362
- : ""),
363
- },
364
- ],
365
- metadata: {
366
- sourcePath,
367
- destinationPath,
368
- referencesUpdated,
369
- },
502
+ // Get all markdown files
503
+ const files = await glob(path.join(this.docsDir, "**/*.md"));
504
+ // Filter out the single_doc.md itself and any single_doc_part*.md files
505
+ const docsToProcess = files.filter((file) => !path.basename(file).startsWith("single_doc"));
506
+ debugLog(`Processing ${docsToProcess.length} documents`);
507
+ // Create a template for the single doc
508
+ const projectName = path.basename(path.resolve(this.docsDir, ".."));
509
+ const template = `---
510
+ title: ${projectName} Documentation
511
+ description: Complete documentation optimized for LLM context
512
+ author: Documentation System
513
+ date: "${new Date().toISOString()}"
514
+ tags:
515
+ - documentation
516
+ - llm-optimized
517
+ status: published
518
+ version: 1.0.0
519
+ ---
520
+
521
+ # ${projectName.toUpperCase()}: COMPLETE REFERENCE
522
+
523
+ _This document is automatically generated and optimized for LLM context. It contains the complete reference for ${projectName}._
524
+
525
+ ## TABLE OF CONTENTS
526
+
527
+ 1. [Overview](#overview)
528
+ 2. [Core Concepts](#core-concepts)
529
+ 3. [Features](#features)
530
+ 4. [API Reference](#api-reference)
531
+ 5. [Usage Examples](#usage-examples)
532
+ 6. [Integration](#integration)
533
+ 7. [Best Practices](#best-practices)
534
+ 8. [Troubleshooting](#troubleshooting)
535
+ 9. [Additional Information](#additional-information)
536
+
537
+ `;
538
+ // Initialize sections
539
+ const sections = {
540
+ OVERVIEW: "## OVERVIEW\n\n",
541
+ "CORE CONCEPTS": "## CORE CONCEPTS\n\n",
542
+ FEATURES: "## FEATURES\n\n",
543
+ "API REFERENCE": "## API REFERENCE\n\n",
544
+ "USAGE EXAMPLES": "## USAGE EXAMPLES\n\n",
545
+ INTEGRATION: "## INTEGRATION\n\n",
546
+ "BEST PRACTICES": "## BEST PRACTICES\n\n",
547
+ TROUBLESHOOTING: "## TROUBLESHOOTING\n\n",
548
+ "ADDITIONAL INFORMATION": "## ADDITIONAL INFORMATION\n\n",
370
549
  };
371
- }
372
- catch (error) {
373
- const errorMessage = error instanceof Error ? error.message : String(error);
374
- return {
375
- content: [
376
- { type: "text", text: `Error moving document: ${errorMessage}` },
377
- ],
378
- isError: true,
550
+ // Define section priorities (1 = highest priority)
551
+ const sectionPriorities = {
552
+ OVERVIEW: 1,
553
+ FEATURES: 2,
554
+ "API REFERENCE": 3,
555
+ "USAGE EXAMPLES": 4,
556
+ "CORE CONCEPTS": 5,
557
+ INTEGRATION: 6,
558
+ "BEST PRACTICES": 7,
559
+ TROUBLESHOOTING: 8,
560
+ "ADDITIONAL INFORMATION": 9,
379
561
  };
380
- }
381
- }
382
- /**
383
- * Rename a document
384
- */
385
- async renameDocument(docPath, newName, updateReferences = true) {
386
- try {
387
- const validPath = await this.validatePath(docPath);
388
- // Get directory and extension
389
- const dir = path.dirname(validPath);
390
- const ext = path.extname(validPath);
391
- // Create new path
392
- const newPath = path.join(dir, newName + ext);
393
- const validNewPath = await this.validatePath(newPath);
394
- // Check if source exists
395
- try {
396
- await fs.access(validPath);
397
- }
398
- catch {
399
- throw new Error(`Source file does not exist: ${docPath}`);
400
- }
401
- // Check if destination already exists
402
- try {
403
- await fs.access(validNewPath);
404
- throw new Error(`Destination file already exists: ${newPath}`);
405
- }
406
- catch (error) {
407
- // If error is "file doesn't exist", that's good
408
- if (!(error instanceof Error &&
409
- error.message.includes("Destination file already exists"))) {
410
- // Continue with rename
562
+ // Read each document and add to appropriate section
563
+ for (const file of docsToProcess) {
564
+ const relativePath = path.relative(this.docsDir, file);
565
+ const content = await fs.readFile(file, "utf-8");
566
+ // Parse frontmatter
567
+ const { frontmatter, content: docContent } = this.parseFrontmatter(content);
568
+ // Skip empty documents
569
+ if (!docContent.trim())
570
+ continue;
571
+ // Determine which section to add to
572
+ let targetSection = "ADDITIONAL INFORMATION";
573
+ if (file.includes("api") ||
574
+ (frontmatter.tags && frontmatter.tags.includes("api"))) {
575
+ targetSection = "API REFERENCE";
411
576
  }
412
- else {
413
- throw error;
577
+ else if (file.includes("concept") ||
578
+ (frontmatter.tags && frontmatter.tags.includes("concept"))) {
579
+ targetSection = "CORE CONCEPTS";
414
580
  }
415
- }
416
- // Read the source file
417
- const content = await fs.readFile(validPath, "utf-8");
418
- // Parse frontmatter
419
- const { frontmatter, content: docContent } = parseFrontmatter(content);
420
- // Update title in frontmatter if it exists
421
- if (frontmatter.title) {
422
- frontmatter.title = newName;
423
- }
424
- // Reconstruct content with updated frontmatter
425
- let frontmatterStr = "---\n";
426
- for (const [key, value] of Object.entries(frontmatter)) {
427
- if (Array.isArray(value)) {
428
- frontmatterStr += `${key}:\n`;
429
- for (const item of value) {
430
- frontmatterStr += ` - ${item}\n`;
431
- }
581
+ else if (file.includes("feature") ||
582
+ (frontmatter.tags && frontmatter.tags.includes("feature"))) {
583
+ targetSection = "FEATURES";
432
584
  }
433
- else {
434
- frontmatterStr += `${key}: ${value}\n`;
585
+ else if (file.includes("example") ||
586
+ (frontmatter.tags && frontmatter.tags.includes("example"))) {
587
+ targetSection = "USAGE EXAMPLES";
435
588
  }
589
+ else if (file.includes("integration") ||
590
+ (frontmatter.tags && frontmatter.tags.includes("integration"))) {
591
+ targetSection = "INTEGRATION";
592
+ }
593
+ else if (file.includes("best-practice") ||
594
+ (frontmatter.tags && frontmatter.tags.includes("best-practice"))) {
595
+ targetSection = "BEST PRACTICES";
596
+ }
597
+ else if (file.includes("troubleshoot") ||
598
+ (frontmatter.tags && frontmatter.tags.includes("troubleshoot"))) {
599
+ targetSection = "TROUBLESHOOTING";
600
+ }
601
+ else if (file.includes("overview") ||
602
+ (frontmatter.tags && frontmatter.tags.includes("overview")) ||
603
+ file.endsWith("README.md")) {
604
+ targetSection = "OVERVIEW";
605
+ }
606
+ // Add document to the section
607
+ const title = frontmatter.title || path.basename(file, ".md");
608
+ const formattedContent = `### ${title}\n\n_Source: ${relativePath}_\n\n${docContent}\n\n`;
609
+ sections[targetSection] += formattedContent;
436
610
  }
437
- frontmatterStr += "---\n\n";
438
- const updatedContent = frontmatterStr + docContent;
439
- // Write to new path
440
- await fs.writeFile(validNewPath, updatedContent, "utf-8");
441
- // Delete the source file
442
- await fs.unlink(validPath);
443
- // Update references if requested
444
- let referencesUpdated = 0;
445
- if (updateReferences) {
446
- const relativeSrcPath = path.relative(this.docsDir, validPath);
447
- const relativeDestPath = path.relative(this.docsDir, validNewPath);
448
- referencesUpdated = await this.updateReferences(relativeSrcPath, relativeDestPath);
449
- }
450
- return {
451
- content: [
452
- {
453
- type: "text",
454
- text: `Successfully renamed document from ${docPath} to ${newName}${ext}` +
455
- (referencesUpdated > 0
456
- ? `. Updated ${referencesUpdated} references.`
457
- : ""),
458
- },
459
- ],
460
- metadata: {
461
- originalPath: docPath,
462
- newPath: path.relative(this.docsDir, validNewPath),
463
- referencesUpdated,
464
- },
465
- };
466
- }
467
- catch (error) {
468
- const errorMessage = error instanceof Error ? error.message : String(error);
469
- return {
470
- content: [
471
- { type: "text", text: `Error renaming document: ${errorMessage}` },
472
- ],
473
- isError: true,
474
- };
475
- }
476
- }
477
- /**
478
- * Update navigation order for a document
479
- */
480
- async updateNavigationOrder(docPath, order) {
481
- try {
482
- const validPath = await this.validatePath(docPath);
483
- // Check if file exists
484
- try {
485
- await fs.access(validPath);
486
- }
487
- catch {
488
- throw new Error(`File does not exist: ${docPath}`);
611
+ // Calculate estimated total size
612
+ let estimatedTotalTokens = this.estimateTokens(template);
613
+ debugLog(`Template tokens: ${estimatedTotalTokens}`);
614
+ Object.entries(sections).forEach(([section, content]) => {
615
+ const sectionTokens = this.estimateTokens(content);
616
+ debugLog(`Section "${section}" tokens: ${sectionTokens}`);
617
+ estimatedTotalTokens += sectionTokens;
618
+ });
619
+ debugLog(`Total estimated tokens: ${estimatedTotalTokens}`);
620
+ debugLog(`Multi-doc threshold: ${MULTI_DOC_THRESHOLD}`);
621
+ // Check if we need to split into multiple documents
622
+ if (estimatedTotalTokens > MULTI_DOC_THRESHOLD) {
623
+ debugLog("Threshold exceeded, creating multiple documents");
624
+ // Delete any existing single_doc_part*.md files
625
+ const existingParts = await glob(path.join(this.docsDir, "single_doc_part*.md"));
626
+ for (const part of existingParts) {
627
+ await fs.unlink(part);
628
+ }
629
+ // Create multiple document parts
630
+ const prioritizedSections = Object.entries(sections).sort(([sectionA], [sectionB]) => sectionPriorities[sectionA] - sectionPriorities[sectionB]);
631
+ const numChunks = await this.createDocumentChunks(prioritizedSections, template, MAX_TOKENS);
632
+ return {
633
+ content: [
634
+ {
635
+ type: "text",
636
+ text: `Documentation exceeded token limits (${estimatedTotalTokens} estimated tokens). Created ${numChunks} document parts.`,
637
+ },
638
+ ],
639
+ };
489
640
  }
490
- // Read the file
491
- const content = await fs.readFile(validPath, "utf-8");
492
- // Parse frontmatter
493
- const { frontmatter, content: docContent } = parseFrontmatter(content);
494
- // Update order in frontmatter
495
- frontmatter.order = order;
496
- // Reconstruct content with updated frontmatter
497
- let frontmatterStr = "---\n";
498
- for (const [key, value] of Object.entries(frontmatter)) {
499
- if (Array.isArray(value)) {
500
- frontmatterStr += `${key}:\n`;
501
- for (const item of value) {
502
- frontmatterStr += ` - ${item}\n`;
503
- }
641
+ // If not exceeding the multi-doc threshold, continue with standard approach
642
+ let totalTokens = this.estimateTokens(template);
643
+ let includedDocuments = 0;
644
+ let truncatedSections = 0;
645
+ let omittedSections = 0;
646
+ // Sort sections by priority
647
+ const prioritizedSections = Object.entries(sections).sort(([sectionA], [sectionB]) => sectionPriorities[sectionA] - sectionPriorities[sectionB]);
648
+ // Compile the final document with token limits
649
+ let finalContent = template;
650
+ const footer = "\n---\n\n_This document is automatically maintained and optimized for LLM context._\n";
651
+ const footerTokens = this.estimateTokens(footer);
652
+ // Reserve tokens for the footer
653
+ const availableTokens = MAX_TOKENS - totalTokens - footerTokens;
654
+ for (const [section, content] of prioritizedSections) {
655
+ // Skip empty sections
656
+ if (content.trim() === `## ${section}`.trim()) {
657
+ continue;
658
+ }
659
+ const sectionTokens = this.estimateTokens(content);
660
+ if (totalTokens + sectionTokens <= MAX_TOKENS - footerTokens) {
661
+ // Full section fits
662
+ finalContent += content + "\n";
663
+ totalTokens += sectionTokens;
664
+ // Count documents in this section
665
+ const docMatches = content.match(/### .+/g);
666
+ includedDocuments += docMatches ? docMatches.length : 0;
504
667
  }
505
668
  else {
506
- frontmatterStr += `${key}: ${value}\n`;
669
+ // Section doesn't fit entirely - try to include a truncated version
670
+ omittedSections++;
671
+ // Add a note about truncation
672
+ const truncationNote = `## ${section} (Content Limit Reached)\n\nSome content in this section was omitted to fit within token limits.\n\n`;
673
+ const truncationTokens = this.estimateTokens(truncationNote);
674
+ if (totalTokens + truncationTokens <= MAX_TOKENS - footerTokens) {
675
+ finalContent += truncationNote;
676
+ totalTokens += truncationTokens;
677
+ truncatedSections++;
678
+ }
507
679
  }
508
680
  }
509
- frontmatterStr += "---\n\n";
510
- const updatedContent = frontmatterStr + docContent;
511
- // Write updated content
512
- await fs.writeFile(validPath, updatedContent, "utf-8");
681
+ // Add footer with token usage information
682
+ finalContent += footer;
683
+ // Add token usage metadata
684
+ const tokenUsageInfo = `\n<!-- Token usage: ${totalTokens}/${MAX_TOKENS} (${Math.round((totalTokens / MAX_TOKENS) * 100)}%) -->\n`;
685
+ finalContent += tokenUsageInfo;
686
+ // Delete any existing part files since we're creating a single file
687
+ const existingParts = await glob(path.join(this.docsDir, "single_doc_part*.md"));
688
+ for (const part of existingParts) {
689
+ await fs.unlink(part);
690
+ }
691
+ // Write the file
692
+ await fs.writeFile(singleDocPath, finalContent, "utf-8");
513
693
  return {
514
694
  content: [
515
695
  {
516
696
  type: "text",
517
- text: `Successfully updated navigation order for ${docPath} to ${order}`,
697
+ text: `Successfully refreshed single_doc.md from ${docsToProcess.length} documents. Included ${includedDocuments} documents using ${totalTokens} tokens (${Math.round((totalTokens / MAX_TOKENS) * 100)}% of limit).${truncatedSections > 0
698
+ ? ` ${truncatedSections} sections were truncated.`
699
+ : ""}${omittedSections > 0
700
+ ? ` ${omittedSections} sections were omitted due to token limits.`
701
+ : ""}`,
518
702
  },
519
703
  ],
520
- metadata: {
521
- path: docPath,
522
- order,
523
- },
524
704
  };
525
705
  }
526
706
  catch (error) {
527
- const errorMessage = error instanceof Error ? error.message : String(error);
528
707
  return {
529
708
  content: [
530
709
  {
531
710
  type: "text",
532
- text: `Error updating navigation order: ${errorMessage}`,
711
+ text: `Error refreshing single_doc.md: ${error instanceof Error ? error.message : String(error)}`,
533
712
  },
534
713
  ],
535
714
  isError: true,
@@ -537,215 +716,109 @@ This is the documentation for ${folderName}.
537
716
  }
538
717
  }
539
718
  /**
540
- * Create a new navigation section
719
+ * Parse frontmatter from a markdown document
541
720
  */
542
- async createSection(title, sectionPath, order) {
721
+ parseFrontmatter(content) {
722
+ // If no content, return empty
723
+ if (!content || content.trim() === "") {
724
+ return { frontmatter: {}, content: "" };
725
+ }
726
+ // Check if the content has frontmatter (starts with ---)
727
+ if (!content.startsWith("---")) {
728
+ return { frontmatter: {}, content };
729
+ }
543
730
  try {
544
- // Create the directory for the section
545
- const validPath = await this.validatePath(sectionPath);
546
- await fs.mkdir(validPath, { recursive: true });
547
- // Create an index.md file for the section
548
- const indexPath = path.join(validPath, "index.md");
549
- const validIndexPath = await this.validatePath(indexPath);
550
- // Create content with frontmatter
551
- let content = "---\n";
552
- content += `title: ${title}\n`;
553
- content += `description: ${title} section\n`;
554
- content += `date: ${new Date().toISOString()}\n`;
555
- content += `status: published\n`;
556
- if (order !== undefined) {
557
- content += `order: ${order}\n`;
731
+ // Find the end of the frontmatter
732
+ const endIndex = content.indexOf("---", 3);
733
+ if (endIndex === -1) {
734
+ return { frontmatter: {}, content };
558
735
  }
559
- content += "---\n\n";
560
- content += `# ${title}\n\n`;
561
- content += `Welcome to the ${title} section.\n`;
562
- // Write the index file
563
- await fs.writeFile(validIndexPath, content, "utf-8");
564
- return {
565
- content: [
566
- {
567
- type: "text",
568
- text: `Successfully created section: ${title} at ${sectionPath}`,
569
- },
570
- ],
571
- metadata: {
572
- title,
573
- path: sectionPath,
574
- indexPath: path.join(sectionPath, "index.md"),
575
- order,
576
- },
577
- };
736
+ // Extract frontmatter and parse as YAML
737
+ const frontmatterText = content.substring(3, endIndex).trim();
738
+ const frontmatter = this.parseFrontmatterLines(frontmatterText);
739
+ // Extract the content after frontmatter
740
+ const contentWithoutFrontmatter = content.substring(endIndex + 3).trim();
741
+ return { frontmatter, content: contentWithoutFrontmatter };
578
742
  }
579
743
  catch (error) {
580
- const errorMessage = error instanceof Error ? error.message : String(error);
581
- return {
582
- content: [
583
- { type: "text", text: `Error creating section: ${errorMessage}` },
584
- ],
585
- isError: true,
586
- };
744
+ console.error("Error parsing frontmatter:", error);
745
+ return { frontmatter: {}, content };
587
746
  }
588
747
  }
589
748
  /**
590
- * Update references to a moved or renamed document
591
- * @private
749
+ * Parse frontmatter lines into an object
592
750
  */
593
- async updateReferences(oldPath, newPath) {
594
- // Normalize paths for comparison
595
- const normalizedOldPath = oldPath.replace(/\\/g, "/");
596
- const normalizedNewPath = newPath.replace(/\\/g, "/");
597
- // Find all markdown files
598
- const files = await glob("**/*.md", { cwd: this.docsDir });
599
- let updatedCount = 0;
600
- for (const file of files) {
601
- const filePath = path.join(this.docsDir, file);
602
- const content = await fs.readFile(filePath, "utf-8");
603
- // Look for references to the old path
604
- // Match markdown links: [text](path)
605
- const linkRegex = new RegExp(`\\[([^\\]]+)\\]\\(${normalizedOldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\)`, "g");
606
- // Match direct path references
607
- const pathRegex = new RegExp(normalizedOldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
608
- // Replace references
609
- let updatedContent = content.replace(linkRegex, `[$1](${normalizedNewPath})`);
610
- updatedContent = updatedContent.replace(pathRegex, normalizedNewPath);
611
- // If content changed, write the updated file
612
- if (updatedContent !== content) {
613
- await fs.writeFile(filePath, updatedContent, "utf-8");
614
- updatedCount++;
615
- }
616
- }
617
- return updatedCount;
618
- }
619
- /**
620
- * Validate links in documentation
621
- */
622
- async validateLinks(basePath = "", recursive = true) {
623
- try {
624
- const validBasePath = await this.validatePath(basePath || this.docsDir);
625
- // Find all markdown files
626
- const pattern = recursive ? "**/*.md" : "*.md";
627
- const files = await glob(pattern, { cwd: validBasePath });
628
- const brokenLinks = [];
629
- // Check each file for links
630
- for (const file of files) {
631
- const filePath = path.join(validBasePath, file);
632
- const content = await fs.readFile(filePath, "utf-8");
633
- const lines = content.split("\n");
634
- // Find markdown links: [text](path)
635
- const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
636
- for (let i = 0; i < lines.length; i++) {
637
- const line = lines[i];
638
- let match;
639
- while ((match = linkRegex.exec(line)) !== null) {
640
- const [, , linkPath] = match;
641
- // Skip external links and anchors
642
- if (linkPath.startsWith("http://") ||
643
- linkPath.startsWith("https://") ||
644
- linkPath.startsWith("#") ||
645
- linkPath.startsWith("mailto:")) {
646
- continue;
647
- }
648
- // Resolve the link path relative to the current file
649
- const fileDir = path.dirname(filePath);
650
- const resolvedPath = path.resolve(fileDir, linkPath);
651
- // Check if the link target exists
652
- try {
653
- await fs.access(resolvedPath);
654
- }
655
- catch {
656
- brokenLinks.push({
657
- file: path.relative(this.docsDir, filePath),
658
- link: linkPath,
659
- lineNumber: i + 1,
660
- });
661
- }
751
+ parseFrontmatterLines(frontmatterText) {
752
+ const frontmatter = {};
753
+ const lines = frontmatterText.split("\n");
754
+ let currentKey = null;
755
+ let isMultilineValue = false;
756
+ let multilineValue = "";
757
+ let isArray = false;
758
+ for (const line of lines) {
759
+ const trimmedLine = line.trim();
760
+ if (trimmedLine === "")
761
+ continue;
762
+ if (isMultilineValue) {
763
+ if (trimmedLine.startsWith(" -")) {
764
+ // Array item
765
+ const value = trimmedLine.substring(3).trim();
766
+ if (!Array.isArray(frontmatter[currentKey])) {
767
+ frontmatter[currentKey] = [];
768
+ }
769
+ frontmatter[currentKey].push(value);
770
+ }
771
+ else if (trimmedLine.startsWith(" ")) {
772
+ // Continuation of multiline value
773
+ multilineValue += "\n" + trimmedLine.substring(2);
774
+ }
775
+ else {
776
+ // End of multiline value
777
+ if (multilineValue && !isArray) {
778
+ frontmatter[currentKey] = multilineValue.trim();
779
+ multilineValue = "";
662
780
  }
781
+ isMultilineValue = false;
782
+ isArray = false;
783
+ currentKey = null;
663
784
  }
664
785
  }
665
- return {
666
- content: [
667
- {
668
- type: "text",
669
- text: brokenLinks.length > 0
670
- ? `Found ${brokenLinks.length} broken links in ${files.length} files`
671
- : `No broken links found in ${files.length} files`,
672
- },
673
- ],
674
- metadata: {
675
- brokenLinks,
676
- filesChecked: files.length,
677
- basePath: path.relative(this.docsDir, validBasePath),
678
- },
679
- };
680
- }
681
- catch (error) {
682
- const errorMessage = error instanceof Error ? error.message : String(error);
683
- return {
684
- content: [
685
- { type: "text", text: `Error validating links: ${errorMessage}` },
686
- ],
687
- isError: true,
688
- };
689
- }
690
- }
691
- /**
692
- * Validate metadata in documentation
693
- */
694
- async validateMetadata(basePath = "", requiredFields) {
695
- try {
696
- const validBasePath = await this.validatePath(basePath || this.docsDir);
697
- // Default required fields if not specified
698
- const fields = requiredFields || ["title", "description", "status"];
699
- // Find all markdown files
700
- const files = await glob("**/*.md", { cwd: validBasePath });
701
- const missingMetadata = [];
702
- // Check each file for metadata
703
- for (const file of files) {
704
- const filePath = path.join(validBasePath, file);
705
- const content = await fs.readFile(filePath, "utf-8");
706
- // Parse frontmatter
707
- const { frontmatter } = parseFrontmatter(content);
708
- // Check for required fields
709
- const missing = fields.filter((field) => !frontmatter[field]);
710
- if (missing.length > 0) {
711
- missingMetadata.push({
712
- file: path.relative(this.docsDir, filePath),
713
- missingFields: missing,
714
- });
786
+ if (!isMultilineValue) {
787
+ const colonIndex = trimmedLine.indexOf(":");
788
+ if (colonIndex > 0) {
789
+ currentKey = trimmedLine.substring(0, colonIndex).trim();
790
+ const value = trimmedLine.substring(colonIndex + 1).trim();
791
+ if (value === "" || value === "|" || value === ">") {
792
+ // Multiline value
793
+ isMultilineValue = true;
794
+ multilineValue = "";
795
+ isArray = false;
796
+ }
797
+ else if (value === "[]" ||
798
+ (value.startsWith("[") && value.endsWith("]"))) {
799
+ // Array
800
+ frontmatter[currentKey] =
801
+ value === "[]"
802
+ ? []
803
+ : value
804
+ .substring(1, value.length - 1)
805
+ .split(",")
806
+ .map((v) => v.trim());
807
+ }
808
+ else {
809
+ // Simple value
810
+ if (value === "true")
811
+ frontmatter[currentKey] = true;
812
+ else if (value === "false")
813
+ frontmatter[currentKey] = false;
814
+ else if (!isNaN(Number(value)))
815
+ frontmatter[currentKey] = Number(value);
816
+ else
817
+ frontmatter[currentKey] = value.replace(/^["'](.*)["']$/, "$1");
818
+ }
715
819
  }
716
820
  }
717
- // Calculate completeness percentage
718
- const totalFields = files.length * fields.length;
719
- const missingFields = missingMetadata.reduce((sum, item) => sum + item.missingFields.length, 0);
720
- const completenessPercentage = totalFields > 0
721
- ? Math.round(((totalFields - missingFields) / totalFields) * 100)
722
- : 100;
723
- return {
724
- content: [
725
- {
726
- type: "text",
727
- text: missingMetadata.length > 0
728
- ? `Found ${missingMetadata.length} files with missing metadata. Completeness: ${completenessPercentage}%`
729
- : `All ${files.length} files have complete metadata. Completeness: 100%`,
730
- },
731
- ],
732
- metadata: {
733
- missingMetadata,
734
- filesChecked: files.length,
735
- requiredFields: fields,
736
- completenessPercentage,
737
- basePath: path.relative(this.docsDir, validBasePath),
738
- },
739
- };
740
- }
741
- catch (error) {
742
- const errorMessage = error instanceof Error ? error.message : String(error);
743
- return {
744
- content: [
745
- { type: "text", text: `Error validating metadata: ${errorMessage}` },
746
- ],
747
- isError: true,
748
- };
749
821
  }
822
+ return frontmatter;
750
823
  }
751
824
  }