botdocs 0.2.0 → 0.3.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.
@@ -17,7 +17,7 @@ export declare class Chunker {
17
17
  /**
18
18
  * Chunk a document into semantically meaningful pieces
19
19
  */
20
- chunkDocument(doc: ProcessedDocument): TextChunk[];
20
+ chunkDocument(doc: ProcessedDocument, fileHash?: string): TextChunk[];
21
21
  /**
22
22
  * Create a chunk with metadata
23
23
  */
@@ -37,5 +37,5 @@ export declare class Chunker {
37
37
  /**
38
38
  * Chunk multiple documents
39
39
  */
40
- chunkDocuments(documents: ProcessedDocument[]): TextChunk[];
40
+ chunkDocuments(documents: ProcessedDocument[], fileHashes?: Map<string, string>): TextChunk[];
41
41
  }
@@ -12,7 +12,7 @@ export class Chunker {
12
12
  /**
13
13
  * Chunk a document into semantically meaningful pieces
14
14
  */
15
- chunkDocument(doc) {
15
+ chunkDocument(doc, fileHash) {
16
16
  const chunks = [];
17
17
  const lines = doc.content.split('\n');
18
18
  let currentChunk = [];
@@ -47,7 +47,7 @@ export class Chunker {
47
47
  if (headingMatch) {
48
48
  // Save current chunk if it exists
49
49
  if (currentChunk.length > 0) {
50
- chunks.push(this.createChunk(currentChunk.join('\n'), doc, currentHeading));
50
+ chunks.push(this.createChunk(currentChunk.join('\n'), doc, currentHeading, fileHash));
51
51
  }
52
52
  // Start new chunk with this heading
53
53
  currentHeading = headingMatch[2];
@@ -60,7 +60,7 @@ export class Chunker {
60
60
  if (tokenCount >= this.options.maxChunkSize) {
61
61
  // Split chunk
62
62
  const chunkText = currentChunk.join('\n');
63
- chunks.push(this.createChunk(chunkText, doc, currentHeading));
63
+ chunks.push(this.createChunk(chunkText, doc, currentHeading, fileHash));
64
64
  // Create overlap for next chunk
65
65
  const overlapLines = this.getOverlapLines(currentChunk, this.options.chunkOverlap);
66
66
  currentChunk = overlapLines;
@@ -69,14 +69,14 @@ export class Chunker {
69
69
  }
70
70
  // Add final chunk
71
71
  if (currentChunk.length > 0) {
72
- chunks.push(this.createChunk(currentChunk.join('\n'), doc, currentHeading));
72
+ chunks.push(this.createChunk(currentChunk.join('\n'), doc, currentHeading, fileHash));
73
73
  }
74
74
  return chunks.filter((chunk) => chunk.text.trim().length > 0);
75
75
  }
76
76
  /**
77
77
  * Create a chunk with metadata
78
78
  */
79
- createChunk(text, doc, heading) {
79
+ createChunk(text, doc, heading, fileHash) {
80
80
  return {
81
81
  text: text.trim(),
82
82
  metadata: {
@@ -85,6 +85,7 @@ export class Chunker {
85
85
  heading,
86
86
  headingId: heading ? this.slugify(heading) : undefined,
87
87
  url: doc.url,
88
+ fileHash,
88
89
  },
89
90
  };
90
91
  }
@@ -128,10 +129,11 @@ export class Chunker {
128
129
  /**
129
130
  * Chunk multiple documents
130
131
  */
131
- chunkDocuments(documents) {
132
+ chunkDocuments(documents, fileHashes) {
132
133
  const allChunks = [];
133
134
  for (const doc of documents) {
134
- const chunks = this.chunkDocument(doc);
135
+ const fileHash = fileHashes?.get(doc.relativePath);
136
+ const chunks = this.chunkDocument(doc, fileHash);
135
137
  allChunks.push(...chunks);
136
138
  }
137
139
  return allChunks;
@@ -10,9 +10,21 @@ export declare class VectorDBBuilder {
10
10
  private embedder;
11
11
  constructor(options?: VectorDBBuilderOptions);
12
12
  /**
13
- * Build vector database from documents
13
+ * Build vector database from documents (with incremental build support)
14
14
  */
15
15
  build(documents: ProcessedDocument[], outputDir: string, verbose?: boolean): Promise<VectorDatabase>;
16
+ /**
17
+ * Load existing vector database from disk
18
+ */
19
+ private loadExistingDatabase;
20
+ /**
21
+ * Build a map of sourceFile -> chunks for quick lookup
22
+ */
23
+ private buildCacheMap;
24
+ /**
25
+ * Hash file content for change detection
26
+ */
27
+ private hashContent;
16
28
  /**
17
29
  * Generate unique ID for a chunk
18
30
  */
@@ -1,6 +1,6 @@
1
1
  import { Chunker } from './chunker.js';
2
2
  import { Embedder } from './embedder.js';
3
- import { writeFileSync } from 'fs';
3
+ import { writeFileSync, existsSync, readFileSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { createHash } from 'crypto';
6
6
  export class VectorDBBuilder {
@@ -14,43 +14,130 @@ export class VectorDBBuilder {
14
14
  this.embedder = new Embedder(options.modelName);
15
15
  }
16
16
  /**
17
- * Build vector database from documents
17
+ * Build vector database from documents (with incremental build support)
18
18
  */
19
19
  async build(documents, outputDir, verbose = false) {
20
20
  if (verbose) {
21
21
  console.log('Building vector database...');
22
22
  }
23
- // Step 1: Chunk documents
24
- console.log('Chunking documents...');
25
- const chunks = this.chunker.chunkDocuments(documents);
26
- console.log(`Created ${chunks.length} chunks`);
27
- // Step 2: Generate embeddings
28
- console.log('Generating embeddings...');
29
- await this.embedder.initialize();
30
- const texts = chunks.map((chunk) => chunk.text);
31
- const embeddings = await this.embedder.embedBatch(texts, 10);
32
- // Step 3: Create document chunks with embeddings
33
- const documentChunks = chunks.map((chunk, index) => ({
34
- id: this.generateChunkId(chunk.text, chunk.metadata.sourceFile, index),
35
- text: chunk.text,
36
- embedding: embeddings[index],
37
- metadata: chunk.metadata,
38
- }));
39
- // Step 4: Create vector database
23
+ const outputPath = join(outputDir, 'vector-db.json');
24
+ // Step 1: Load existing vector database for incremental builds
25
+ const existingDB = this.loadExistingDatabase(outputPath, verbose);
26
+ const cachedChunks = this.buildCacheMap(existingDB);
27
+ // Step 2: Hash documents and determine what needs rebuilding
28
+ const fileHashes = new Map();
29
+ const changedDocs = [];
30
+ const unchangedDocs = [];
31
+ for (const doc of documents) {
32
+ const hash = this.hashContent(doc.content);
33
+ fileHashes.set(doc.relativePath, hash);
34
+ const cachedDocChunks = cachedChunks.get(doc.relativePath);
35
+ if (cachedDocChunks && cachedDocChunks[0]?.metadata.fileHash === hash) {
36
+ unchangedDocs.push(doc);
37
+ if (verbose) {
38
+ console.log(` ✓ Cached: ${doc.relativePath}`);
39
+ }
40
+ }
41
+ else {
42
+ changedDocs.push(doc);
43
+ if (verbose) {
44
+ console.log(` ⟳ Changed: ${doc.relativePath}`);
45
+ }
46
+ }
47
+ }
48
+ console.log(`Unchanged: ${unchangedDocs.length}, Changed: ${changedDocs.length}, Total: ${documents.length}`);
49
+ // Step 3: Reuse cached chunks for unchanged documents
50
+ const reusedChunks = [];
51
+ for (const doc of unchangedDocs) {
52
+ const chunks = cachedChunks.get(doc.relativePath);
53
+ if (chunks) {
54
+ reusedChunks.push(...chunks);
55
+ }
56
+ }
57
+ // Step 4: Process changed documents only
58
+ let newChunks = [];
59
+ if (changedDocs.length > 0) {
60
+ console.log('Chunking changed documents...');
61
+ const chunks = this.chunker.chunkDocuments(changedDocs, fileHashes);
62
+ console.log(`Created ${chunks.length} chunks from ${changedDocs.length} changed documents`);
63
+ // Generate embeddings only for changed documents
64
+ console.log('Generating embeddings for changed documents...');
65
+ await this.embedder.initialize();
66
+ const texts = chunks.map((chunk) => chunk.text);
67
+ const embeddings = await this.embedder.embedBatch(texts, 10);
68
+ // Create document chunks with embeddings
69
+ newChunks = chunks.map((chunk, index) => ({
70
+ id: this.generateChunkId(chunk.text, chunk.metadata.sourceFile, index),
71
+ text: chunk.text,
72
+ embedding: embeddings[index],
73
+ metadata: chunk.metadata,
74
+ }));
75
+ }
76
+ else {
77
+ console.log('No changes detected - reusing all cached embeddings');
78
+ }
79
+ // Step 5: Merge cached and new chunks
80
+ const allChunks = [...reusedChunks, ...newChunks];
81
+ // Step 6: Create vector database
40
82
  const vectorDB = {
41
83
  version: '1.0',
42
84
  model: this.embedder.getModelName(),
43
85
  dimension: this.embedder.getDimension(),
44
- chunks: documentChunks,
86
+ chunks: allChunks,
45
87
  };
46
- // Step 5: Write to file
47
- const outputPath = join(outputDir, 'vector-db.json');
88
+ // Step 7: Write to file
48
89
  writeFileSync(outputPath, JSON.stringify(vectorDB, null, 2), 'utf-8');
49
90
  console.log(`Vector database saved: ${outputPath}`);
50
- console.log(`Total chunks: ${vectorDB.chunks.length}`);
91
+ console.log(`Total chunks: ${vectorDB.chunks.length} (${reusedChunks.length} cached, ${newChunks.length} new)`);
51
92
  console.log(`Embedding dimension: ${vectorDB.dimension}`);
52
93
  return vectorDB;
53
94
  }
95
+ /**
96
+ * Load existing vector database from disk
97
+ */
98
+ loadExistingDatabase(outputPath, verbose) {
99
+ if (!existsSync(outputPath)) {
100
+ if (verbose) {
101
+ console.log('No existing vector database found - performing full build');
102
+ }
103
+ return null;
104
+ }
105
+ try {
106
+ const content = readFileSync(outputPath, 'utf-8');
107
+ const db = JSON.parse(content);
108
+ if (verbose) {
109
+ console.log(`Loaded existing vector database with ${db.chunks.length} chunks`);
110
+ }
111
+ return db;
112
+ }
113
+ catch (error) {
114
+ console.warn('Failed to load existing vector database - performing full build');
115
+ return null;
116
+ }
117
+ }
118
+ /**
119
+ * Build a map of sourceFile -> chunks for quick lookup
120
+ */
121
+ buildCacheMap(db) {
122
+ const cache = new Map();
123
+ if (!db) {
124
+ return cache;
125
+ }
126
+ for (const chunk of db.chunks) {
127
+ const sourceFile = chunk.metadata.sourceFile;
128
+ if (!cache.has(sourceFile)) {
129
+ cache.set(sourceFile, []);
130
+ }
131
+ cache.get(sourceFile).push(chunk);
132
+ }
133
+ return cache;
134
+ }
135
+ /**
136
+ * Hash file content for change detection
137
+ */
138
+ hashContent(content) {
139
+ return createHash('md5').update(content).digest('hex');
140
+ }
54
141
  /**
55
142
  * Generate unique ID for a chunk
56
143
  */
@@ -6,6 +6,7 @@ export interface ChunkMetadata {
6
6
  url: string;
7
7
  startLine?: number;
8
8
  endLine?: number;
9
+ fileHash?: string;
9
10
  }
10
11
  export interface DocumentChunk {
11
12
  id: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "botdocs",
3
- "version": "0.2.0",
4
- "description": "An npm CLI tool that converts markdown documentation into a static website with an optional client-side AI chatbot",
3
+ "version": "0.3.0",
4
+ "description": "CLI tool that converts markdown documentation into a static website with an optional client-side AI chatbot",
5
5
  "author": "usr-wwelsh <https://wwel.sh>",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -258,7 +258,7 @@ blockquote {
258
258
  }
259
259
 
260
260
  .markdown-alert-note .markdown-alert-title::before {
261
- content: 'Note: ';
261
+ content: '';
262
262
  font-weight: 700;
263
263
  }
264
264
 
@@ -272,7 +272,7 @@ blockquote {
272
272
  }
273
273
 
274
274
  .markdown-alert-tip .markdown-alert-title::before {
275
- content: 'Tip: ';
275
+ content: '';
276
276
  font-weight: 700;
277
277
  }
278
278
 
@@ -286,7 +286,7 @@ blockquote {
286
286
  }
287
287
 
288
288
  .markdown-alert-important .markdown-alert-title::before {
289
- content: 'Important: ';
289
+ content: '';
290
290
  font-weight: 700;
291
291
  }
292
292
 
@@ -300,7 +300,7 @@ blockquote {
300
300
  }
301
301
 
302
302
  .markdown-alert-warning .markdown-alert-title::before {
303
- content: 'Warning: ';
303
+ content: '';
304
304
  font-weight: 700;
305
305
  }
306
306
 
@@ -314,7 +314,7 @@ blockquote {
314
314
  }
315
315
 
316
316
  .markdown-alert-caution .markdown-alert-title::before {
317
- content: 'Caution: ';
317
+ content: '';
318
318
  font-weight: 700;
319
319
  }
320
320