@afterxleep/doc-bot 1.7.11 → 1.8.1

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/README.md CHANGED
@@ -10,24 +10,21 @@ It's platform agnostic and designed to replace and extend the rule systems provi
10
10
  ## What is doc-bot?
11
11
 
12
12
  doc-bot is an intelligent documentation server that:
13
- - 🔍 **Searches** your project documentation instantly
14
- - 🧠 **Auto-indexes** content for smart inference (no manual keyword mapping!)
15
- - 📋 **Applies** global rules to every AI interaction
16
- - 🎯 **Suggests** contextual documentation based on file patterns
17
- - 🤖 **Detects** code patterns, frameworks, and keywords automatically
13
+ - 🧠 **Auto-indexes** content for smart inference, based on metadata and keywords
14
+ - 🤖 **Provides agentic tools** to query, and update your documentation
18
15
  - 🔄 **Updates** automatically when docs change
19
16
 
20
17
  ## Why MCP Instead of Static Rules?
21
18
 
22
- Traditional AI assistants use static rule files (like Cursor Rules or Copilot's .github/copilot-instructions.md) that have significant limitations. doc-bot's MCP approach offers powerful advantages:
19
+ IDE's use static rule files (like Cursor Rules or Copilot's .github/copilot-instructions.md), and each one has their own format, metadata and approach.
23
20
 
24
21
  ### 🚀 Dynamic Search vs Static Rules
25
22
 
26
23
  **Static Systems:**
27
24
  - All rules must fit in a single file or limited token window
28
25
  - AI reads everything, even irrelevant rules
29
- - No way to search or filter documentation
30
- - Rules compete for precious context space
26
+ - No way to search or filter documentation (besides plain 'grep')
27
+ - Rules compete for context space
31
28
 
32
29
  **MCP with doc-bot:**
33
30
  - AI searches for exactly what it needs
@@ -38,9 +35,8 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
38
35
  ### 🧠 Contextual Intelligence
39
36
 
40
37
  **Static Systems:**
41
- - Same rules applied everywhere
42
- - No awareness of what you're working on
43
- - Can't provide specific help for your current task
38
+ - Duplicate or symlinked rules to work with multiple agents
39
+ - Agents use `grep` for basic text-base searching
44
40
 
45
41
  **MCP with doc-bot:**
46
42
  - AI searches for relevant documentation based on your query
@@ -51,8 +47,8 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
51
47
  ### 📈 Scalability Without Limits
52
48
 
53
49
  **Static Systems:**
54
- - Limited by token count (typically 2-4k tokens)
55
- - Adding more rules means removing others
50
+ - Limited by token count
51
+ - Adding more rules has impact in your context window
56
52
  - Documentation competes with your actual code for context
57
53
 
58
54
  **MCP with doc-bot:**
@@ -66,7 +62,7 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
66
62
  **Static Systems:**
67
63
  - Changes require restarting your AI/IDE
68
64
  - No way to know if rules are current
69
- - Manual synchronization across tools
65
+ - Manual synchronization across tools and AI agents
70
66
 
71
67
  **MCP with doc-bot:**
72
68
  - Hot reload on documentation changes
@@ -84,7 +80,7 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
84
80
  **MCP with doc-bot:**
85
81
  - AI can list all available documentation
86
82
  - Discovers relevant docs automatically
87
- - Suggests documentation based on context
83
+ - Suggests documentation based on relevance
88
84
  - Searchable knowledge base with intelligent ranking
89
85
  - No need for AI to grep through your codebase - dedicated search engine
90
86
 
@@ -99,7 +95,7 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
99
95
  "mcpServers": {
100
96
  "doc-bot": {
101
97
  "command": "npx",
102
- "args": ["@afterxleep/doc-bot"]
98
+ "args": ["@afterxleep/doc-bot@latest"]
103
99
  }
104
100
  }
105
101
  }
@@ -111,11 +107,13 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
111
107
  "mcpServers": {
112
108
  "doc-bot": {
113
109
  "command": "npx",
114
- "args": ["@afterxleep/doc-bot", "--docs", "./my-custom-docs"]
110
+ "args": ["@afterxleep/doc-bot@latest", "--docs", "./my-custom-docs"]
115
111
  }
116
112
  }
117
113
  }
118
114
  ```
115
+ Note: If a relative path does not work, you can use VSCode `${workspaceFolder}`environment variable `${workspaceFolder}/my-custom-docs`
116
+
119
117
 
120
118
  **With verbose logging (for debugging):**
121
119
  ```json
@@ -123,7 +121,7 @@ Traditional AI assistants use static rule files (like Cursor Rules or Copilot's
123
121
  "mcpServers": {
124
122
  "doc-bot": {
125
123
  "command": "npx",
126
- "args": ["@afterxleep/doc-bot", "--verbose"]
124
+ "args": ["@afterxleep/doc-bot@latest", "--verbose"]
127
125
  }
128
126
  }
129
127
  }
@@ -193,6 +191,7 @@ alwaysApply: false
193
191
  title: "Testing Guide"
194
192
  description: "How to write and run tests"
195
193
  keywords: ["testing", "jest", "tdd", "unit-tests"]
194
+ filePatterns: ["*.test.js", "*.spec.js", "__tests__/**/*"]
196
195
  ---
197
196
 
198
197
  # Testing Guide
@@ -208,14 +207,16 @@ Run tests with: `npm test`
208
207
 
209
208
  ## Frontmatter-Based Configuration
210
209
 
211
- doc-bot uses frontmatter in your markdown files to automatically detect and categorize rules - **no manifest.json required!**
210
+ doc-bot uses frontmatter in your markdown files to automatically detect and categorize rules.
212
211
 
213
212
  ### Frontmatter Fields:
214
213
 
215
214
  - **`alwaysApply: true`** - Global rules applied to every AI interaction
216
215
  - **`alwaysApply: false`** - Contextual rules searched and applied based on relevance
217
216
  - **`keywords: ["list", "of", "keywords"]`** - For smart indexing and search
217
+ - **`filePatterns: ["*.js", "src/**/*.ts"]`** - Apply docs to specific files (see below)
218
218
  - **`title`** and **`description`** - Standard metadata
219
+ - **`confidence: 0.9`** - Relevance confidence score (0-1)
219
220
 
220
221
  ### 🎯 Automatic Intelligence
221
222
 
@@ -243,6 +244,31 @@ keywords: ["react", "components", "hooks", "jsx"]
243
244
  Your documentation content here...
244
245
  ```
245
246
 
247
+ ### File Pattern Matching
248
+
249
+ doc-bot supports contextual documentation using file patterns. Documentation can be targeted to specific files:
250
+
251
+ ```markdown
252
+ ---
253
+ alwaysApply: false
254
+ title: "React Testing Guide"
255
+ keywords: ["testing", "jest", "react"]
256
+ filePatterns: ["*.test.jsx", "*.test.tsx", "__tests__/**/*"]
257
+ ---
258
+
259
+ # React Testing Guide
260
+
261
+ This documentation appears only when working with test files...
262
+ ```
263
+
264
+ **Pattern Examples:**
265
+ - `*.js` - All JavaScript files
266
+ - `src/**/*.ts` - TypeScript files in src directory
267
+ - `[Tt]est.js` - Test.js or test.js
268
+ - `*.{jsx,tsx}` - React component files
269
+
270
+ When AI requests documentation for a specific file (e.g., `Button.test.jsx`), only docs with matching patterns are returned.
271
+
246
272
  ## Development setup
247
273
 
248
274
  ### Running locally
@@ -267,13 +293,7 @@ Your documentation content here...
267
293
  ```bash
268
294
  npm run dev
269
295
  ```
270
-
271
- 5. **Run with examples documentation:**
272
- ```bash
273
- npm run start:examples
274
- ```
275
-
276
- 6. **Run tests:**
296
+ 5. **Run tests:**
277
297
  ```bash
278
298
  npm test
279
299
  ```
@@ -291,7 +311,6 @@ doc-bot [options]
291
311
 
292
312
  Options:
293
313
  -d, --docs <path> Path to docs folder (default: doc-bot)
294
- -c, --config <path> Path to manifest file (optional, for backward compatibility)
295
314
  -v, --verbose Enable verbose logging
296
315
  -w, --watch Watch for file changes
297
316
  -h, --help Show help
@@ -307,9 +326,6 @@ doc-bot --docs ./my-docs
307
326
 
308
327
  # With verbose logging and file watching
309
328
  doc-bot --verbose --watch
310
-
311
- # With optional manifest for backward compatibility
312
- doc-bot --config ./manifest.json
313
329
  ```
314
330
 
315
331
  ## Publishing and Development
@@ -378,4 +394,4 @@ MIT License - see the [LICENSE](LICENSE) file for details.
378
394
 
379
395
  - [npm package](https://www.npmjs.com/package/@afterxleep/doc-bot)
380
396
  - [GitHub repository](https://github.com/afterxleep/doc-bot)
381
- - [Model Context Protocol](https://github.com/modelcontextprotocol/specification)
397
+ - [Model Context Protocol](https://github.com/modelcontextprotocol/specification)
package/bin/doc-bot.js CHANGED
@@ -1,17 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { program } = require('commander');
4
- const path = require('path');
5
- const fs = require('fs-extra');
6
- const { DocsServer } = require('../src/index.js');
7
- const packageJson = require('../package.json');
3
+ import { program } from 'commander';
4
+ import path from 'path';
5
+ import fs from 'fs-extra';
6
+ import { DocsServer } from '../src/index.js';
7
+ import { readFileSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
8
14
 
9
15
  program
10
16
  .name('doc-bot')
11
17
  .description('Generic MCP server for intelligent documentation access')
12
18
  .version(packageJson.version)
13
19
  .option('-d, --docs <path>', 'Path to docs folder', 'doc-bot')
14
- .option('-c, --config <path>', 'Path to manifest file')
15
20
  .option('-v, --verbose', 'Enable verbose logging')
16
21
  .option('-w, --watch', 'Watch for file changes')
17
22
  .parse();
@@ -20,7 +25,6 @@ const options = program.opts();
20
25
 
21
26
  async function main() {
22
27
  const docsPath = path.resolve(options.docs);
23
- const configPath = options.config ? path.resolve(options.config) : path.resolve(options.docs, 'manifest.json');
24
28
 
25
29
  // Check if documentation folder exists
26
30
  if (!await fs.pathExists(docsPath)) {
@@ -40,23 +44,8 @@ async function main() {
40
44
  process.exit(1);
41
45
  }
42
46
 
43
- // Manifest is now optional - only create if explicitly requested
44
- if (options.config && !await fs.pathExists(configPath)) {
45
- if (options.verbose) {
46
- console.error('📝 Creating default manifest.json...');
47
- }
48
- const defaultManifest = {
49
- name: 'Project Documentation',
50
- version: '1.0.0',
51
- description: 'AI-powered documentation (auto-generated from frontmatter)',
52
- note: 'This manifest is auto-generated. Configure rules using frontmatter in your markdown files.'
53
- };
54
- await fs.writeJSON(configPath, defaultManifest, { spaces: 2 });
55
- }
56
-
57
47
  const server = new DocsServer({
58
48
  docsPath,
59
- configPath,
60
49
  verbose: options.verbose,
61
50
  watch: options.watch
62
51
  });
@@ -64,11 +53,7 @@ async function main() {
64
53
  if (options.verbose) {
65
54
  console.error('🚀 Starting doc-bot...');
66
55
  console.error(`📁 Documentation: ${docsPath}`);
67
- if (await fs.pathExists(configPath)) {
68
- console.error(`⚙️ Configuration: ${configPath}`);
69
- } else {
70
- console.error(`⚙️ Configuration: Auto-generated from frontmatter`);
71
- }
56
+ console.error(`⚙️ Configuration: Frontmatter-based`);
72
57
 
73
58
  if (options.watch) {
74
59
  console.error('👀 Watching for file changes...');
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@afterxleep/doc-bot",
3
- "version": "1.7.11",
3
+ "version": "1.8.1",
4
4
  "description": "Generic MCP server for intelligent documentation access in any project",
5
+ "type": "module",
5
6
  "main": "src/index.js",
6
7
  "bin": {
7
8
  "doc-bot": "bin/doc-bot.js"
@@ -11,7 +12,7 @@
11
12
  "start:watch": "node bin/doc-bot.js --verbose --watch",
12
13
  "start:examples": "node bin/doc-bot.js --docs ./samples --verbose",
13
14
  "dev": "node bin/doc-bot.js --verbose --watch",
14
- "test": "jest",
15
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
15
16
  "test:watch": "jest --watch",
16
17
  "test:coverage": "jest --coverage",
17
18
  "lint": "eslint src/ bin/ --ext .js",
@@ -51,6 +52,10 @@
51
52
  "jest": "^29.7.0",
52
53
  "supertest": "^6.3.0"
53
54
  },
55
+ "jest": {
56
+ "testEnvironment": "node",
57
+ "transform": {}
58
+ },
54
59
  "engines": {
55
60
  "node": ">=18.0.0"
56
61
  },
package/src/index.js CHANGED
@@ -1,18 +1,21 @@
1
- const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
2
- const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
3
- const { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
4
- const { DocumentationService } = require('./services/DocumentationService.js');
5
- const { InferenceEngine } = require('./services/InferenceEngine.js');
6
- const { ManifestLoader } = require('./services/ManifestLoader.js');
7
- const chokidar = require('chokidar');
8
- const path = require('path');
9
- const fs = require('fs').promises;
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { DocumentationService } from './services/DocumentationService.js';
5
+ import { InferenceEngine } from './services/InferenceEngine.js';
6
+ import chokidar from 'chokidar';
7
+ import path from 'path';
8
+ import { promises as fs } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname } from 'path';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
10
14
 
11
15
  class DocsServer {
12
16
  constructor(options = {}) {
13
17
  this.options = {
14
18
  docsPath: options.docsPath || './doc-bot',
15
- configPath: options.configPath || './doc-bot/manifest.json', // Optional, for backward compatibility
16
19
  verbose: options.verbose || false,
17
20
  watch: options.watch || false,
18
21
  ...options
@@ -32,10 +35,8 @@ class DocsServer {
32
35
  }
33
36
  });
34
37
 
35
- // ManifestLoader is now optional - only create if manifest exists
36
- this.manifestLoader = null;
37
38
  this.docService = new DocumentationService(this.options.docsPath);
38
- this.inferenceEngine = new InferenceEngine(this.docService, null);
39
+ this.inferenceEngine = new InferenceEngine(this.docService);
39
40
 
40
41
  this.setupHandlers();
41
42
 
@@ -60,7 +61,6 @@ class DocsServer {
60
61
  setupHandlers() {
61
62
  // List available resources
62
63
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
63
- const manifest = await this.manifestLoader.load();
64
64
  return {
65
65
  resources: [
66
66
  {
@@ -81,12 +81,6 @@ class DocsServer {
81
81
  description: 'Smart documentation suggestions based on your current context',
82
82
  mimeType: 'application/json'
83
83
  },
84
- {
85
- uri: 'docs://manifest',
86
- name: 'Documentation Manifest',
87
- description: 'Project documentation configuration',
88
- mimeType: 'application/json'
89
- },
90
84
  {
91
85
  uri: 'docs://system-prompt',
92
86
  name: 'System Prompt Injection',
@@ -122,24 +116,6 @@ class DocsServer {
122
116
  }]
123
117
  };
124
118
 
125
- case 'docs://manifest':
126
- // Generate manifest from frontmatter data
127
- const manifestDocs = await this.docService.getAllDocuments();
128
- const generatedManifest = {
129
- name: 'Project Documentation',
130
- version: '1.0.0',
131
- description: 'Auto-generated from frontmatter',
132
- globalRules: manifestDocs.filter(doc => doc.metadata?.alwaysApply === true).map(doc => doc.fileName),
133
- contextualRules: this.extractContextualRules(manifestDocs)
134
- };
135
- return {
136
- contents: [{
137
- uri,
138
- mimeType: 'application/json',
139
- text: JSON.stringify(generatedManifest, null, 2)
140
- }]
141
- };
142
-
143
119
  case 'docs://system-prompt':
144
120
  const systemPrompt = await this.generateSystemPrompt();
145
121
  return {
@@ -267,6 +243,15 @@ class DocsServer {
267
243
  properties: {},
268
244
  additionalProperties: false
269
245
  }
246
+ },
247
+ {
248
+ name: 'get_document_index',
249
+ description: 'Get an index of all documents in the store with title, description, and last updated date.',
250
+ inputSchema: {
251
+ type: 'object',
252
+ properties: {},
253
+ additionalProperties: false
254
+ }
270
255
  }
271
256
  ]
272
257
  };
@@ -372,6 +357,16 @@ class DocsServer {
372
357
  text: `✅ Documentation refreshed successfully!\n\n**Files indexed:** ${docCount}\n**Last updated:** ${new Date().toLocaleString()}\n\n💡 All manually added files should now be available for search and reading.`
373
358
  }]
374
359
  };
360
+
361
+ case 'get_document_index':
362
+ const documentIndex = await this.docService.getDocumentIndex();
363
+
364
+ return {
365
+ content: [{
366
+ type: 'text',
367
+ text: await this.formatDocumentIndex(documentIndex)
368
+ }]
369
+ };
375
370
 
376
371
  default:
377
372
  throw new Error(`Unknown tool: ${name}`);
@@ -388,7 +383,7 @@ class DocsServer {
388
383
  }
389
384
 
390
385
  setupWatcher() {
391
- const watcher = chokidar.watch([this.options.docsPath, this.options.configPath], {
386
+ const watcher = chokidar.watch(this.options.docsPath, {
392
387
  ignored: /(^|[\/\\])\../, // ignore dotfiles
393
388
  persistent: true
394
389
  });
@@ -398,15 +393,8 @@ class DocsServer {
398
393
  console.error(`📄 Documentation updated: ${path.relative(process.cwd(), filePath)}`);
399
394
  }
400
395
 
401
- // Reload manifest if config changed
402
- if (filePath === this.options.configPath) {
403
- await this.manifestLoader.reload();
404
- }
405
-
406
396
  // Reload docs if documentation changed
407
- if (filePath.startsWith(this.options.docsPath)) {
408
- await this.docService.reload();
409
- }
397
+ await this.docService.reload();
410
398
  });
411
399
  }
412
400
 
@@ -554,13 +542,37 @@ class DocsServer {
554
542
  return output;
555
543
  }
556
544
 
545
+ async formatDocumentIndex(documentIndex) {
546
+ if (!documentIndex || documentIndex.length === 0) {
547
+ return 'No documents found in the store.';
548
+ }
549
+
550
+ let output = '# Document Index\n\n';
551
+ output += `Found ${documentIndex.length} document(s) in the store:\n\n`;
552
+
553
+ documentIndex.forEach((doc, index) => {
554
+ output += `## ${index + 1}. ${doc.title}\n\n`;
555
+ output += `**File:** ${doc.fileName}\n`;
556
+
557
+ if (doc.description) {
558
+ output += `**Description:** ${doc.description}\n`;
559
+ }
560
+
561
+ output += `**Last Updated:** ${new Date(doc.lastUpdated).toLocaleString()}\n\n`;
562
+ output += '---\n\n';
563
+ });
564
+
565
+ output += '💡 **Next Steps:** Use the `read_specific_document` tool with the file name to get the full content of any document above.\n';
566
+
567
+ return output;
568
+ }
569
+
557
570
  async createOrUpdateRule({ fileName, title, description, keywords, alwaysApply, content }) {
558
- const fs = require('fs-extra');
559
- const path = require('path');
571
+ const { default: fsExtra } = await import('fs-extra');
560
572
 
561
573
  try {
562
574
  // Ensure the docs directory exists
563
- await fs.ensureDir(this.options.docsPath);
575
+ await fsExtra.ensureDir(this.options.docsPath);
564
576
 
565
577
  // Create the full file path
566
578
  const filePath = path.join(this.options.docsPath, fileName);
@@ -581,7 +593,7 @@ class DocsServer {
581
593
  const fullContent = frontmatter + content;
582
594
 
583
595
  // Check if file exists to determine if this is create or update
584
- const fileExists = await fs.pathExists(filePath);
596
+ const fileExists = await fsExtra.pathExists(filePath);
585
597
  const action = fileExists ? 'updated' : 'created';
586
598
 
587
599
  // Write the file
@@ -823,15 +835,6 @@ class DocsServer {
823
835
  }
824
836
 
825
837
  async start() {
826
- // Initialize manifest loader if manifest exists (backward compatibility)
827
- const fs = require('fs-extra');
828
- if (await fs.pathExists(this.options.configPath)) {
829
- this.manifestLoader = new ManifestLoader(this.options.configPath);
830
- await this.manifestLoader.load();
831
- // Update services with manifest loader
832
- this.inferenceEngine = new InferenceEngine(this.docService, this.manifestLoader);
833
- }
834
-
835
838
  // Initialize services
836
839
  await this.docService.initialize();
837
840
  await this.inferenceEngine.initialize();
@@ -842,13 +845,9 @@ class DocsServer {
842
845
 
843
846
  if (this.options.verbose) {
844
847
  console.error('🔧 Server initialized with MCP transport');
845
- if (this.manifestLoader) {
846
- console.error('📄 Using manifest.json for additional configuration');
847
- } else {
848
- console.error('🚀 Using frontmatter-based configuration (no manifest needed)');
849
- }
848
+ console.error('🚀 Using frontmatter-based configuration');
850
849
  }
851
850
  }
852
851
  }
853
852
 
854
- module.exports = { DocsServer };
853
+ export { DocsServer };
@@ -0,0 +1,114 @@
1
+ import { DocumentationService } from './services/DocumentationService.js';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ describe('DocumentationService get_document_index functionality', () => {
11
+ let docService;
12
+ let tempDocsPath;
13
+
14
+ beforeEach(async () => {
15
+ // Create a temporary directory for test documents
16
+ tempDocsPath = path.join(__dirname, '../test-docs');
17
+ await fs.ensureDir(tempDocsPath);
18
+
19
+ // Create test documents
20
+ const testDocs = [
21
+ {
22
+ fileName: 'react-guide.md',
23
+ content: '---\nalwaysApply: false\ntitle: "React Component Guide"\ndescription: "Learn how to build React components"\nkeywords: ["react", "components", "jsx"]\n---\n\n# React Components\n\nThis guide covers React components.'
24
+ },
25
+ {
26
+ fileName: 'testing-standards.md',
27
+ content: '---\nalwaysApply: true\ntitle: "Testing Standards"\ndescription: "Project testing requirements"\nkeywords: ["testing", "jest", "standards"]\n---\n\n# Testing Standards\n\nAll code must have tests.'
28
+ },
29
+ {
30
+ fileName: 'api-design.md',
31
+ content: '---\nalwaysApply: false\ntitle: "API Design Guidelines"\ndescription: "REST API design patterns"\nkeywords: ["api", "rest", "design"]\n---\n\n# API Design\n\nFollow REST principles.'
32
+ }
33
+ ];
34
+
35
+ // Write test documents to temp directory
36
+ for (const doc of testDocs) {
37
+ await fs.writeFile(path.join(tempDocsPath, doc.fileName), doc.content);
38
+ }
39
+
40
+ // Create DocumentationService instance
41
+ docService = new DocumentationService(tempDocsPath);
42
+ await docService.initialize();
43
+ });
44
+
45
+ afterEach(async () => {
46
+ // Clean up temporary directory
47
+ await fs.remove(tempDocsPath);
48
+ });
49
+
50
+ describe('getDocumentIndex method', () => {
51
+ it('should be implemented and return document index', async () => {
52
+ expect(typeof docService.getDocumentIndex).toBe('function');
53
+
54
+ const index = await docService.getDocumentIndex();
55
+
56
+ expect(Array.isArray(index)).toBe(true);
57
+ expect(index.length).toBe(3);
58
+
59
+ // Check that each document has required fields
60
+ index.forEach(doc => {
61
+ expect(doc).toHaveProperty('title');
62
+ expect(doc).toHaveProperty('description');
63
+ expect(doc).toHaveProperty('fileName');
64
+ expect(doc).toHaveProperty('lastUpdated');
65
+ expect(typeof doc.lastUpdated).toBe('string');
66
+ });
67
+ });
68
+
69
+ it('should return documents sorted by title', async () => {
70
+ const index = await docService.getDocumentIndex();
71
+
72
+ // Should be sorted alphabetically by title
73
+ expect(index[0].title).toBe('API Design Guidelines');
74
+ expect(index[1].title).toBe('React Component Guide');
75
+ expect(index[2].title).toBe('Testing Standards');
76
+ });
77
+
78
+ it('should include metadata from frontmatter', async () => {
79
+ const index = await docService.getDocumentIndex();
80
+
81
+ const reactDoc = index.find(doc => doc.fileName === 'react-guide.md');
82
+ expect(reactDoc.title).toBe('React Component Guide');
83
+ expect(reactDoc.description).toBe('Learn how to build React components');
84
+
85
+ const testingDoc = index.find(doc => doc.fileName === 'testing-standards.md');
86
+ expect(testingDoc.title).toBe('Testing Standards');
87
+ expect(testingDoc.description).toBe('Project testing requirements');
88
+ });
89
+
90
+ it('should use file name as title when no title in metadata', async () => {
91
+ // Create a document without title metadata
92
+ const docWithoutTitle = '---\ndescription: "A document without title"\n---\n\nSome content';
93
+ await fs.writeFile(path.join(tempDocsPath, 'no-title.md'), docWithoutTitle);
94
+
95
+ // Reload documents to pick up the new file
96
+ await docService.reload();
97
+
98
+ const index = await docService.getDocumentIndex();
99
+ const noTitleDoc = index.find(doc => doc.fileName === 'no-title.md');
100
+
101
+ expect(noTitleDoc.title).toBe('no-title.md');
102
+ expect(noTitleDoc.description).toBe('A document without title');
103
+ });
104
+
105
+ it('should handle empty description gracefully', async () => {
106
+ const index = await docService.getDocumentIndex();
107
+
108
+ // All test documents should have descriptions, but let's test the structure
109
+ index.forEach(doc => {
110
+ expect(typeof doc.description).toBe('string');
111
+ });
112
+ });
113
+ });
114
+ });
@@ -376,4 +376,4 @@ class DocumentIndex {
376
376
  }
377
377
  }
378
378
 
379
- module.exports = { DocumentIndex };
379
+ export { DocumentIndex };
@@ -1,7 +1,7 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const glob = require('glob');
4
- const yaml = require('yaml');
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import yaml from 'yaml';
5
5
 
6
6
  class DocumentationService {
7
7
  constructor(docsPath, manifestLoader = null) {
@@ -308,6 +308,22 @@ class DocumentationService {
308
308
  return results;
309
309
  }
310
310
 
311
+ async getDocumentIndex() {
312
+ const index = [];
313
+
314
+ for (const doc of this.documents.values()) {
315
+ index.push({
316
+ title: doc.metadata?.title || doc.fileName,
317
+ description: doc.metadata?.description || '',
318
+ fileName: doc.fileName,
319
+ lastUpdated: doc.lastModified.toISOString()
320
+ });
321
+ }
322
+
323
+ // Sort by title for consistent ordering
324
+ return index.sort((a, b) => a.title.localeCompare(b.title));
325
+ }
326
+
311
327
  }
312
328
 
313
- module.exports = { DocumentationService };
329
+ export { DocumentationService };
@@ -1,10 +1,9 @@
1
- const path = require('path');
2
- const { DocumentIndex } = require('./DocumentIndex');
1
+ import path from 'path';
2
+ import { DocumentIndex } from './DocumentIndex.js';
3
3
 
4
4
  class InferenceEngine {
5
- constructor(documentationService, manifestLoader = null) {
5
+ constructor(documentationService) {
6
6
  this.docService = documentationService;
7
- this.manifestLoader = manifestLoader;
8
7
  this.documentIndex = new DocumentIndex();
9
8
  this.isIndexBuilt = false;
10
9
  }
@@ -106,52 +105,13 @@ class InferenceEngine {
106
105
  }
107
106
 
108
107
  async getDocsByKeywords(query) {
109
- if (!this.manifestLoader) {
110
- return [];
111
- }
112
-
113
- const manifest = await this.manifestLoader.load();
114
- const keywords = manifest.inference?.keywords || {};
115
-
116
- const docs = [];
117
- const queryLower = query.toLowerCase();
118
-
119
- for (const [keyword, docPaths] of Object.entries(keywords)) {
120
- if (queryLower.includes(keyword.toLowerCase())) {
121
- for (const docPath of docPaths) {
122
- const doc = this.docService.getDocument(docPath);
123
- if (doc) {
124
- docs.push(doc);
125
- }
126
- }
127
- }
128
- }
129
-
130
- return docs;
108
+ // Now handled by DocumentIndex
109
+ return [];
131
110
  }
132
111
 
133
112
  async getDocsByPatterns(codeSnippet) {
134
- if (!this.manifestLoader) {
135
- return [];
136
- }
137
-
138
- const manifest = await this.manifestLoader.load();
139
- const patterns = manifest.inference?.patterns || {};
140
-
141
- const docs = [];
142
-
143
- for (const [pattern, docPaths] of Object.entries(patterns)) {
144
- if (codeSnippet.includes(pattern)) {
145
- for (const docPath of docPaths) {
146
- const doc = this.docService.getDocument(docPath);
147
- if (doc) {
148
- docs.push(doc);
149
- }
150
- }
151
- }
152
- }
153
-
154
- return docs;
113
+ // Now handled by DocumentIndex
114
+ return [];
155
115
  }
156
116
 
157
117
  async getDocsByFileExtension(filePath) {
@@ -245,4 +205,4 @@ class InferenceEngine {
245
205
  }
246
206
  }
247
207
 
248
- module.exports = { InferenceEngine };
208
+ export { InferenceEngine };
@@ -1,4 +1,4 @@
1
- const { DocumentIndex } = require('../DocumentIndex');
1
+ import { DocumentIndex } from '../DocumentIndex.js';
2
2
 
3
3
  describe('DocumentIndex', () => {
4
4
  let documentIndex;
@@ -1,13 +1,17 @@
1
- const { InferenceEngine } = require('../InferenceEngine');
2
- const { DocumentationService } = require('../DocumentationService');
3
- const { ManifestLoader } = require('../ManifestLoader');
4
- const fs = require('fs-extra');
5
- const path = require('path');
1
+ import { jest } from '@jest/globals';
2
+ import { InferenceEngine } from '../InferenceEngine.js';
3
+ import { DocumentationService } from '../DocumentationService.js';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname } from 'path';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
6
11
 
7
12
  describe('InferenceEngine Integration', () => {
8
13
  let inferenceEngine;
9
14
  let mockDocService;
10
- let mockManifestLoader;
11
15
  let tempDir;
12
16
 
13
17
  beforeEach(async () => {
@@ -109,19 +113,7 @@ Building REST APIs with Express.js.
109
113
  getContextualDocs: jest.fn().mockResolvedValue([])
110
114
  };
111
115
 
112
- // Create mock ManifestLoader
113
- mockManifestLoader = {
114
- load: jest.fn().mockResolvedValue({
115
- globalRules: [],
116
- contextualRules: {},
117
- inference: {
118
- keywords: {},
119
- patterns: {}
120
- }
121
- })
122
- };
123
-
124
- inferenceEngine = new InferenceEngine(mockDocService, mockManifestLoader);
116
+ inferenceEngine = new InferenceEngine(mockDocService);
125
117
  await inferenceEngine.initialize();
126
118
  });
127
119
 
@@ -257,7 +249,7 @@ Building REST APIs with Express.js.
257
249
  getContextualDocs: jest.fn().mockResolvedValue([])
258
250
  };
259
251
 
260
- const fallbackEngine = new InferenceEngine(failingDocService, mockManifestLoader);
252
+ const fallbackEngine = new InferenceEngine(failingDocService);
261
253
  await fallbackEngine.initialize();
262
254
 
263
255
  expect(fallbackEngine.isIndexBuilt).toBe(false);
@@ -1,135 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
-
4
- class ManifestLoader {
5
- constructor(configPath) {
6
- this.configPath = configPath;
7
- this.manifest = null;
8
- this.lastModified = null;
9
- }
10
-
11
- async load() {
12
- try {
13
- const stats = await fs.stat(this.configPath);
14
-
15
- // Only reload if file has changed
16
- if (this.manifest && this.lastModified && stats.mtime <= this.lastModified) {
17
- return this.manifest;
18
- }
19
-
20
- const content = await fs.readFile(this.configPath, 'utf8');
21
- this.manifest = JSON.parse(content);
22
- this.lastModified = stats.mtime;
23
-
24
- // Validate manifest structure
25
- this.validateManifest();
26
-
27
- return this.manifest;
28
- } catch (error) {
29
- if (error.code === 'ENOENT') {
30
- // Create default manifest if file doesn't exist
31
- this.manifest = this.createDefaultManifest();
32
- await this.save();
33
- return this.manifest;
34
- }
35
- throw new Error(`Failed to load manifest: ${error.message}`);
36
- }
37
- }
38
-
39
- async reload() {
40
- this.manifest = null;
41
- this.lastModified = null;
42
- return await this.load();
43
- }
44
-
45
- async save() {
46
- if (!this.manifest) {
47
- throw new Error('No manifest to save');
48
- }
49
-
50
- await fs.ensureDir(path.dirname(this.configPath));
51
- await fs.writeJSON(this.configPath, this.manifest, { spaces: 2 });
52
- }
53
-
54
- validateManifest() {
55
- if (!this.manifest) {
56
- throw new Error('Manifest is null');
57
- }
58
-
59
- // Required fields
60
- if (!this.manifest.name) {
61
- this.manifest.name = 'Project Documentation';
62
- }
63
-
64
- if (!this.manifest.version) {
65
- this.manifest.version = '1.0.0';
66
- }
67
-
68
- // Optional fields with defaults
69
- if (!this.manifest.globalRules) {
70
- this.manifest.globalRules = [];
71
- }
72
-
73
- if (!this.manifest.contextualRules) {
74
- this.manifest.contextualRules = {};
75
- }
76
-
77
- if (!this.manifest.inference) {
78
- this.manifest.inference = {
79
- keywords: {},
80
- patterns: {}
81
- };
82
- }
83
-
84
- // Validate globalRules is array
85
- if (!Array.isArray(this.manifest.globalRules)) {
86
- throw new Error('globalRules must be an array');
87
- }
88
-
89
- // Validate contextualRules is object
90
- if (typeof this.manifest.contextualRules !== 'object') {
91
- throw new Error('contextualRules must be an object');
92
- }
93
-
94
- // Validate inference structure
95
- if (!this.manifest.inference.keywords) {
96
- this.manifest.inference.keywords = {};
97
- }
98
-
99
- if (!this.manifest.inference.patterns) {
100
- this.manifest.inference.patterns = {};
101
- }
102
- }
103
-
104
- createDefaultManifest() {
105
- return {
106
- name: 'Project Documentation',
107
- version: '1.0.0',
108
- description: 'AI-powered documentation',
109
- globalRules: [],
110
- contextualRules: {},
111
- inference: {
112
- keywords: {},
113
- patterns: {}
114
- }
115
- };
116
- }
117
-
118
- getManifest() {
119
- return this.manifest;
120
- }
121
-
122
- getGlobalRules() {
123
- return this.manifest?.globalRules || [];
124
- }
125
-
126
- getContextualRules() {
127
- return this.manifest?.contextualRules || {};
128
- }
129
-
130
- getInferenceConfig() {
131
- return this.manifest?.inference || { keywords: {}, patterns: {} };
132
- }
133
- }
134
-
135
- module.exports = { ManifestLoader };