botdocs 0.3.0 → 0.5.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.
Files changed (44) hide show
  1. package/README.md +20 -2
  2. package/dist/src/builder/chunker.d.ts +18 -0
  3. package/dist/src/builder/chunker.js +62 -1
  4. package/dist/src/builder/embedder.js +5 -2
  5. package/dist/src/builder/index-size.d.ts +3 -0
  6. package/dist/src/builder/index-size.js +16 -0
  7. package/dist/src/builder/index.js +34 -3
  8. package/dist/src/builder/markdown-processor.d.ts +8 -1
  9. package/dist/src/builder/markdown-processor.js +40 -7
  10. package/dist/src/builder/paths.d.ts +6 -0
  11. package/dist/src/builder/paths.js +13 -0
  12. package/dist/src/builder/site-generator.d.ts +19 -2
  13. package/dist/src/builder/site-generator.js +117 -34
  14. package/dist/src/builder/template-engine.d.ts +4 -4
  15. package/dist/src/builder/template-engine.js +5 -1
  16. package/dist/src/builder/vector-db-builder.d.ts +1 -0
  17. package/dist/src/builder/vector-db-builder.js +1 -0
  18. package/dist/src/cli/index.js +61 -18
  19. package/dist/src/cli/options.d.ts +2 -0
  20. package/dist/src/cli/options.js +2 -1
  21. package/dist/src/cli/server.d.ts +6 -0
  22. package/dist/src/cli/server.js +79 -0
  23. package/dist/src/cli/watcher.d.ts +7 -0
  24. package/dist/src/cli/watcher.js +40 -0
  25. package/dist/src/shared/site-root.d.ts +2 -0
  26. package/dist/src/shared/site-root.js +9 -0
  27. package/dist/src/types/config.d.ts +4 -0
  28. package/dist/src/types/config.js +2 -0
  29. package/dist/src/types/document.d.ts +1 -1
  30. package/dist-client/assets/chatbox-Dw_HFrfR.js +8 -0
  31. package/dist-client/assets/rag-engine-B9wYRzqT.js +1 -0
  32. package/dist-client/bundle.js +1 -1
  33. package/dist-client/wasm/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  34. package/man/botdocs.1 +193 -0
  35. package/package.json +18 -2
  36. package/src/styles/chat.css +144 -0
  37. package/src/styles/themes/classic.css +29 -0
  38. package/src/styles/themes/material.css +29 -0
  39. package/src/styles/themes/minimal.css +34 -0
  40. package/src/styles/themes/modern.css +34 -0
  41. package/src/styles/themes/slate.css +32 -36
  42. package/src/templates/layout.html +21 -2
  43. package/dist-client/assets/chatbox-CmTTiB2Z.js +0 -1
  44. package/dist-client/assets/rag-engine-L2vj3Y0G.js +0 -7
@@ -1,10 +1,18 @@
1
1
  import { readFileSync, writeFileSync, readdirSync, mkdirSync, cpSync } from 'fs';
2
- import { join, dirname, resolve } from 'path';
2
+ import { join, dirname, basename } from 'path';
3
3
  import { MarkdownProcessor } from './markdown-processor.js';
4
4
  import { TemplateEngine } from './template-engine.js';
5
+ import { underSrc } from './paths.js';
6
+ import { rootPrefix, relativeUrl } from '../shared/site-root.js';
5
7
  import { fileURLToPath } from 'url';
6
8
  const __filename = fileURLToPath(import.meta.url);
7
9
  const __dirname = dirname(__filename);
10
+ function stripHtml(value) {
11
+ return value.replace(/<[^>]*>/g, '');
12
+ }
13
+ export function absoluteUrl(baseUrl, urlPath) {
14
+ return `${baseUrl.replace(/\/+$/, '')}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;
15
+ }
8
16
  export class SiteGenerator {
9
17
  processor;
10
18
  templateEngine;
@@ -42,9 +50,10 @@ export class SiteGenerator {
42
50
  this.documents.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
43
51
  // Generate navigation
44
52
  const navigation = this.buildNavigation(this.documents);
45
- // Load templates from source directory (not dist)
46
- // From dist/src/builder, go to project root, then to src/templates
47
- const templatesDir = resolve(__dirname, '../../../src/templates');
53
+ const pageSequence = this.buildPageSequence(navigation);
54
+ // Load templates from the project's src/templates, whether running
55
+ // compiled from dist/ or directly from src/ via tsx.
56
+ const templatesDir = underSrc(__dirname, 'templates');
48
57
  const layoutTemplate = readFileSync(join(templatesDir, 'layout.html'), 'utf-8');
49
58
  const docPageTemplate = readFileSync(join(templatesDir, 'doc-page.html'), 'utf-8');
50
59
  const indexTemplate = readFileSync(join(templatesDir, 'index.html'), 'utf-8');
@@ -53,9 +62,11 @@ export class SiteGenerator {
53
62
  for (let i = 0; i < this.documents.length; i++) {
54
63
  const doc = this.documents[i];
55
64
  const isIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
65
+ const pageUrl = isIndex ? '/' : doc.url;
56
66
  // Prepare navigation data
57
- const prevDoc = i > 0 ? this.documents[i - 1] : null;
58
- const nextDoc = i < this.documents.length - 1 ? this.documents[i + 1] : null;
67
+ const adjacent = pageSequence.get(doc.url);
68
+ const root = rootPrefix(doc.url);
69
+ const relativeLink = (link) => link && { ...link, url: relativeUrl(root, link.url) };
59
70
  // Render document content
60
71
  const contentTemplate = isIndex ? indexTemplate : docPageTemplate;
61
72
  const content = this.templateEngine.renderWithLoops(contentTemplate, {
@@ -67,31 +78,31 @@ export class SiteGenerator {
67
78
  ? this.documents.filter(d => d !== doc).map(d => ({
68
79
  title: d.metadata.title,
69
80
  description: d.metadata.description,
70
- url: d.url,
81
+ url: relativeUrl(root, d.url),
71
82
  }))
72
83
  : undefined,
73
- prevPage: prevDoc
74
- ? {
75
- title: prevDoc.metadata.title,
76
- url: prevDoc.url,
77
- }
78
- : undefined,
79
- nextPage: nextDoc
80
- ? {
81
- title: nextDoc.metadata.title,
82
- url: nextDoc.url,
83
- }
84
- : undefined,
84
+ prevPage: relativeLink(adjacent?.prev),
85
+ nextPage: relativeLink(adjacent?.next),
85
86
  });
86
87
  // Render full page with layout
87
88
  const html = this.templateEngine.renderWithLoops(layoutTemplate, {
88
89
  title: doc.metadata.title || 'Documentation',
89
- description: doc.metadata.description || config.description || '',
90
+ // The template engine doesn't escape interpolated values, so
91
+ // config.description may contain markup (e.g. a hotlink) meant
92
+ // for the visible siteDescription below — strip it here since
93
+ // this one lands inside a <meta content="..."> attribute.
94
+ description: stripHtml(doc.metadata.description || config.description || ''),
90
95
  siteTitle: config.title || 'Documentation',
91
96
  siteDescription: config.description || '',
92
97
  content,
93
- navigation: this.renderNavigation(navigation, doc.url),
98
+ root,
99
+ navigation: this.renderNavigation(navigation, root, doc.url),
94
100
  chatEnabled: config.chat?.enabled,
101
+ ogUrl: config.baseUrl ? absoluteUrl(config.baseUrl, pageUrl) : undefined,
102
+ searchConfigJson: JSON.stringify({
103
+ topK: config.build?.topK ?? 3,
104
+ minScore: config.build?.minScore ?? 0.75,
105
+ }),
95
106
  attribution: config.attribution !== false, // defaults to true
96
107
  });
97
108
  // Write HTML file
@@ -109,43 +120,115 @@ export class SiteGenerator {
109
120
  }
110
121
  }
111
122
  console.log(`Generated ${this.documents.length} HTML pages`);
123
+ if (config.baseUrl) {
124
+ this.writeSitemap(outputDir, config.baseUrl);
125
+ }
112
126
  return this.documents;
113
127
  }
128
+ writeSitemap(outputDir, baseUrl) {
129
+ const urls = this.documents
130
+ .map((doc) => {
131
+ const isRootIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
132
+ return absoluteUrl(baseUrl, isRootIndex ? '/' : doc.url);
133
+ }).map((loc) => ` <url><loc>${loc}</loc></url>`)
134
+ .join('\n');
135
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
136
+ writeFileSync(join(outputDir, 'sitemap.xml'), sitemap, 'utf-8');
137
+ console.log('Generated sitemap.xml');
138
+ }
114
139
  /**
115
- * Build navigation structure from documents
140
+ * Build navigation structure from documents, grouping by top-level
141
+ * folder so e.g. every doc under `path-of-python/` (its README plus
142
+ * anything in `path-of-python/docs/`) nests under one "Path of Python"
143
+ * entry instead of interleaving flat with every other folder's pages.
144
+ * Files at the root (no folder) stay flat, top-level entries.
116
145
  */
117
146
  buildNavigation(documents) {
118
147
  const nav = [];
148
+ const groups = new Map();
119
149
  for (const doc of documents) {
120
150
  const parts = doc.relativePath.split('/');
121
- const title = doc.metadata.title || parts[parts.length - 1].replace(/\.md$/, '');
122
- nav.push({
123
- title,
124
- url: doc.url,
125
- });
151
+ const title = doc.metadata.title || basename(doc.relativePath, '.md');
152
+ const isRootIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
153
+ if (parts.length === 1) {
154
+ if (!isRootIndex) {
155
+ nav.push({ title, url: doc.url });
156
+ }
157
+ continue;
158
+ }
159
+ const folder = parts[0];
160
+ const restOfPath = parts.slice(1).join('/');
161
+ const isFolderIndex = restOfPath === 'README.md' || restOfPath === 'index.md';
162
+ let group = groups.get(folder);
163
+ if (!group) {
164
+ group = {
165
+ title: folder.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()),
166
+ url: doc.url,
167
+ children: [],
168
+ };
169
+ groups.set(folder, group);
170
+ nav.push(group);
171
+ }
172
+ if (isFolderIndex) {
173
+ group.url = doc.url;
174
+ }
175
+ else {
176
+ group.children.push({ title, url: doc.url });
177
+ }
126
178
  }
127
179
  return nav;
128
180
  }
129
181
  /**
130
- * Render navigation HTML
182
+ * Render navigation HTML. Folder groups only expand their children when
183
+ * the current page is the group's root or one of its children — every
184
+ * other page sees the group collapsed to a single link that leads to
185
+ * its root README.
131
186
  */
132
- renderNavigation(items, currentUrl) {
187
+ renderNavigation(items, root, currentUrl) {
133
188
  if (items.length === 0)
134
189
  return '';
135
190
  let html = '<ul class="nav-list">';
136
191
  for (const item of items) {
137
192
  const isActive = item.url === currentUrl;
138
- const activeClass = isActive ? ' class="active"' : '';
139
- html += `<li${activeClass}>`;
140
- html += `<a href="${item.url}"${activeClass}>${item.title}</a>`;
141
- if (item.children && item.children.length > 0) {
142
- html += this.renderNavigation(item.children, currentUrl);
193
+ const hasChildren = !!item.children && item.children.length > 0;
194
+ const isExpanded = hasChildren && (isActive || item.children.some((child) => child.url === currentUrl));
195
+ const liClasses = [isActive && 'active', hasChildren && 'nav-group']
196
+ .filter(Boolean)
197
+ .join(' ');
198
+ html += `<li${liClasses ? ` class="${liClasses}"` : ''}>`;
199
+ html += `<a href="${relativeUrl(root, item.url)}"${isActive ? ' class="active"' : ''}>${item.title}</a>`;
200
+ if (isExpanded) {
201
+ html += this.renderNavigation(item.children, root, currentUrl);
143
202
  }
144
203
  html += '</li>';
145
204
  }
146
205
  html += '</ul>';
147
206
  return html;
148
207
  }
208
+ /**
209
+ * Compute prev/next page links that walk the top-level navigation
210
+ * (root README to root README) rather than the flat, alphabetical
211
+ * document list — so paging from a folder's root never dips into that
212
+ * folder's children. Paging from within a group's children walks those
213
+ * siblings first, then rolls into the next top-level entry.
214
+ */
215
+ buildPageSequence(navigation) {
216
+ const positions = new Map();
217
+ const asLink = (item) => ({ title: item.title, url: item.url });
218
+ for (let i = 0; i < navigation.length; i++) {
219
+ const item = navigation[i];
220
+ const prevTop = i > 0 ? asLink(navigation[i - 1]) : undefined;
221
+ const nextTop = i < navigation.length - 1 ? asLink(navigation[i + 1]) : undefined;
222
+ positions.set(item.url, { prev: prevTop, next: nextTop });
223
+ const children = item.children ?? [];
224
+ for (let j = 0; j < children.length; j++) {
225
+ const prev = j === 0 ? asLink(item) : asLink(children[j - 1]);
226
+ const next = j < children.length - 1 ? asLink(children[j + 1]) : nextTop;
227
+ positions.set(children[j].url, { prev, next });
228
+ }
229
+ }
230
+ return positions;
231
+ }
149
232
  /**
150
233
  * Get all processed documents
151
234
  */
@@ -6,12 +6,12 @@ export declare class TemplateEngine {
6
6
  * Render a template with variables
7
7
  * Replaces {{variableName}} with values from data object
8
8
  */
9
- render(template: string, data: Record<string, any>): string;
9
+ render(template: string, data: Record<string, unknown>): string;
10
10
  /**
11
11
  * Render a template with nested variables
12
12
  * Supports {{object.property}} syntax
13
13
  */
14
- renderAdvanced(template: string, data: Record<string, any>): string;
14
+ renderAdvanced(template: string, data: Record<string, unknown>): string;
15
15
  /**
16
16
  * Get nested property from object using dot notation
17
17
  */
@@ -21,10 +21,10 @@ export declare class TemplateEngine {
21
21
  * Supports {{#if variable}}...{{/if}} syntax
22
22
  * Handles nested conditionals recursively
23
23
  */
24
- renderWithConditionals(template: string, data: Record<string, any>): string;
24
+ renderWithConditionals(template: string, data: Record<string, unknown>): string;
25
25
  /**
26
26
  * Render template with loops
27
27
  * Supports {{#each items}}...{{/each}} syntax
28
28
  */
29
- renderWithLoops(template: string, data: Record<string, any>): string;
29
+ renderWithLoops(template: string, data: Record<string, unknown>): string;
30
30
  }
@@ -25,7 +25,11 @@ export class TemplateEngine {
25
25
  * Get nested property from object using dot notation
26
26
  */
27
27
  getNestedProperty(obj, path) {
28
- return path.split('.').reduce((current, key) => current?.[key], obj);
28
+ return path
29
+ .split('.')
30
+ .reduce((current, key) => current && typeof current === 'object'
31
+ ? current[key]
32
+ : undefined, obj);
29
33
  }
30
34
  /**
31
35
  * Render template with conditional blocks
@@ -3,6 +3,7 @@ import { VectorDatabase } from '../types/vector-db.js';
3
3
  export interface VectorDBBuilderOptions {
4
4
  chunkSize?: number;
5
5
  chunkOverlap?: number;
6
+ minChunkSize?: number;
6
7
  modelName?: string;
7
8
  }
8
9
  export declare class VectorDBBuilder {
@@ -10,6 +10,7 @@ export class VectorDBBuilder {
10
10
  this.chunker = new Chunker({
11
11
  maxChunkSize: options.chunkSize || 500,
12
12
  chunkOverlap: options.chunkOverlap || 50,
13
+ minChunkSize: options.minChunkSize,
13
14
  });
14
15
  this.embedder = new Embedder(options.modelName);
15
16
  }
@@ -1,14 +1,26 @@
1
1
  import { Command } from 'commander';
2
2
  import { build } from '../builder/index.js';
3
3
  import { defaultOptions } from './options.js';
4
- import { readFileSync } from 'fs';
4
+ import { startServer } from './server.js';
5
+ import { watchDocs } from './watcher.js';
6
+ import { readFileSync, existsSync } from 'fs';
5
7
  import { resolve, dirname } from 'path';
6
8
  import { fileURLToPath } from 'url';
7
9
  const __filename = fileURLToPath(import.meta.url);
8
10
  const __dirname = dirname(__filename);
9
- // Read package.json for version
10
- // From dist/src/cli/index.js, go up 3 levels to project root
11
- const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../../../package.json'), 'utf-8'));
11
+ // Read package.json for version. Walks up from this file so it resolves
12
+ // both from dist/src/cli (installed) and src/cli (tsx).
13
+ function findPackageJson(startDir) {
14
+ let dir = startDir;
15
+ while (dir !== dirname(dir)) {
16
+ const candidate = resolve(dir, 'package.json');
17
+ if (existsSync(candidate))
18
+ return candidate;
19
+ dir = dirname(dir);
20
+ }
21
+ throw new Error('package.json not found');
22
+ }
23
+ const packageJson = JSON.parse(readFileSync(findPackageJson(__dirname), 'utf-8'));
12
24
  const program = new Command();
13
25
  program
14
26
  .name('botdocs')
@@ -18,29 +30,60 @@ program
18
30
  .option('-o, --output <dir>', 'Output directory for generated site', defaultOptions.output)
19
31
  .option('--no-chat', 'Disable AI chatbot functionality')
20
32
  .option('-c, --config <file>', 'Path to config file (botdocs.config.json)')
21
- .option('-t, --theme <theme>', 'Theme to use (classic, material, minimal, slate, modern)', defaultOptions.theme)
33
+ .option('-t, --theme <theme>', 'Theme to use (classic, material, minimal, slate, modern); overrides config file if set')
22
34
  .option('-v, --verbose', 'Enable verbose logging')
35
+ .option('-w, --watch', 'Rebuild on changes and serve the site for live preview')
36
+ .option('-p, --port <number>', 'Port for the preview server (with --watch)', parseInt)
23
37
  .action(async (input, options) => {
24
- try {
25
- const inputDir = resolve(process.cwd(), input);
26
- const outputDir = resolve(process.cwd(), options.output || defaultOptions.output);
27
- if (options.verbose) {
28
- console.log('Botdocs starting...');
29
- console.log(`Input: ${inputDir}`);
30
- console.log(`Output: ${outputDir}`);
31
- console.log(`Chat enabled: ${!options.noChat}`);
32
- console.log(`Theme: ${options.theme || defaultOptions.theme}`);
33
- }
38
+ const inputDir = resolve(process.cwd(), input);
39
+ const outputDir = resolve(process.cwd(), options.output || defaultOptions.output);
40
+ const verbose = options.verbose || false;
41
+ const runBuild = async () => {
34
42
  await build({
35
43
  inputDir,
36
44
  outputDir,
37
45
  chatEnabled: !options.noChat,
38
46
  configPath: options.config,
39
- verbose: options.verbose || false,
47
+ verbose,
40
48
  theme: options.theme,
41
49
  });
42
- console.log('Build complete!');
43
- console.log(`Site generated at: ${outputDir}`);
50
+ };
51
+ try {
52
+ if (verbose) {
53
+ console.log('Botdocs starting...');
54
+ console.log(`Input: ${inputDir}`);
55
+ console.log(`Output: ${outputDir}`);
56
+ console.log(`Chat enabled: ${!options.noChat}`);
57
+ console.log(`Theme override: ${options.theme || '(none — using config file or default)'}`);
58
+ }
59
+ await runBuild();
60
+ if (options.watch) {
61
+ const server = await startServer(outputDir, options.port ?? defaultOptions.port);
62
+ console.log(`Site generated at: ${outputDir}`);
63
+ console.log(`Preview at: ${server.url}`);
64
+ console.log('Watching for changes... Press Ctrl+C to stop.');
65
+ const watcher = watchDocs(inputDir, (changes) => {
66
+ const names = [...new Set(changes.map((c) => c.path.split('/').pop()))].join(', ');
67
+ console.log(`Changed: ${names} — rebuilding...`);
68
+ runBuild().catch((error) => {
69
+ console.error('Rebuild failed:', error);
70
+ });
71
+ });
72
+ let shuttingDown = false;
73
+ const shutdown = () => {
74
+ if (shuttingDown)
75
+ return;
76
+ shuttingDown = true;
77
+ watcher.close();
78
+ server.close().finally(() => process.exit(0));
79
+ };
80
+ process.on('SIGINT', shutdown);
81
+ process.on('SIGTERM', shutdown);
82
+ }
83
+ else {
84
+ console.log('Build complete!');
85
+ console.log(`Site generated at: ${outputDir}`);
86
+ }
44
87
  }
45
88
  catch (error) {
46
89
  console.error('Build failed:', error);
@@ -5,5 +5,7 @@ export interface CliOptions {
5
5
  config?: string;
6
6
  verbose?: boolean;
7
7
  theme?: Theme;
8
+ watch?: boolean;
9
+ port?: number;
8
10
  }
9
11
  export declare const defaultOptions: Partial<CliOptions>;
@@ -2,5 +2,6 @@ export const defaultOptions = {
2
2
  output: 'output',
3
3
  noChat: false,
4
4
  verbose: false,
5
- theme: 'classic',
5
+ watch: false,
6
+ port: 3000,
6
7
  };
@@ -0,0 +1,6 @@
1
+ export interface RunningServer {
2
+ url: string;
3
+ port: number;
4
+ close(): Promise<void>;
5
+ }
6
+ export declare function startServer(rootDir: string, port?: number): Promise<RunningServer>;
@@ -0,0 +1,79 @@
1
+ import { createServer } from 'http';
2
+ import { promises as fs } from 'fs';
3
+ import { join, resolve, sep, extname } from 'path';
4
+ const MIME_TYPES = {
5
+ '.html': 'text/html; charset=utf-8',
6
+ '.css': 'text/css; charset=utf-8',
7
+ '.js': 'text/javascript; charset=utf-8',
8
+ '.mjs': 'text/javascript; charset=utf-8',
9
+ '.json': 'application/json; charset=utf-8',
10
+ '.map': 'application/json; charset=utf-8',
11
+ '.svg': 'image/svg+xml',
12
+ '.png': 'image/png',
13
+ '.jpg': 'image/jpeg',
14
+ '.jpeg': 'image/jpeg',
15
+ '.gif': 'image/gif',
16
+ '.webp': 'image/webp',
17
+ '.ico': 'image/x-icon',
18
+ '.xml': 'application/xml; charset=utf-8',
19
+ '.txt': 'text/plain; charset=utf-8',
20
+ '.woff': 'font/woff',
21
+ '.woff2': 'font/woff2',
22
+ };
23
+ export async function startServer(rootDir, port = 0) {
24
+ const root = resolve(rootDir);
25
+ const server = createServer((req, res) => {
26
+ handleRequest(req, res, root).catch(() => {
27
+ respond(res, 500, 'text/plain; charset=utf-8', 'Internal server error');
28
+ });
29
+ });
30
+ return new Promise((resolveStart, rejectStart) => {
31
+ server.once('error', rejectStart);
32
+ server.listen(port, '127.0.0.1', () => {
33
+ const address = server.address();
34
+ const actualPort = typeof address === 'object' && address ? address.port : port;
35
+ resolveStart({
36
+ url: `http://127.0.0.1:${actualPort}`,
37
+ port: actualPort,
38
+ close: () => new Promise((done) => server.close(() => done())),
39
+ });
40
+ });
41
+ });
42
+ }
43
+ async function handleRequest(req, res, root) {
44
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
45
+ respond(res, 405, 'text/plain; charset=utf-8', 'Method not allowed');
46
+ return;
47
+ }
48
+ let urlPath;
49
+ try {
50
+ urlPath = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
51
+ }
52
+ catch {
53
+ respond(res, 400, 'text/plain; charset=utf-8', 'Bad request');
54
+ return;
55
+ }
56
+ // Complete mediation: every request resolves inside the site root or it
57
+ // never touches the filesystem.
58
+ const filePath = resolve(root, `.${sep}${urlPath}`);
59
+ if (filePath !== root && !filePath.startsWith(root + sep)) {
60
+ respond(res, 403, 'text/plain; charset=utf-8', 'Forbidden');
61
+ return;
62
+ }
63
+ let target = filePath;
64
+ try {
65
+ const stat = await fs.stat(target);
66
+ if (stat.isDirectory()) {
67
+ target = join(target, 'index.html');
68
+ }
69
+ const body = await fs.readFile(target);
70
+ respond(res, 200, MIME_TYPES[extname(target).toLowerCase()] ?? 'application/octet-stream', body);
71
+ }
72
+ catch {
73
+ respond(res, 404, 'text/plain; charset=utf-8', 'Not found');
74
+ }
75
+ }
76
+ function respond(res, status, contentType, body) {
77
+ res.writeHead(status, { 'Content-Type': contentType });
78
+ res.end(body);
79
+ }
@@ -0,0 +1,7 @@
1
+ export interface WatchedChange {
2
+ path: string;
3
+ }
4
+ export interface DocWatcher {
5
+ close(): void;
6
+ }
7
+ export declare function watchDocs(inputDir: string, onChange: (changes: WatchedChange[]) => void, debounceMs?: number): DocWatcher;
@@ -0,0 +1,40 @@
1
+ import { watch } from 'fs';
2
+ const WATCHED_EXTENSIONS = ['.md', '.markdown'];
3
+ const WATCHED_FILES = ['botdocs.config.json'];
4
+ function isWatchedPath(path) {
5
+ if (WATCHED_EXTENSIONS.some((ext) => path.endsWith(ext)))
6
+ return true;
7
+ return WATCHED_FILES.some((file) => path === file || path.endsWith(`/${file}`));
8
+ }
9
+ export function watchDocs(inputDir, onChange, debounceMs = 300) {
10
+ let pending = new Map();
11
+ let timer;
12
+ const flush = () => {
13
+ timer = undefined;
14
+ const changes = [...pending.values()];
15
+ pending = new Map();
16
+ if (changes.length > 0) {
17
+ onChange(changes);
18
+ }
19
+ };
20
+ const schedule = (path) => {
21
+ pending.set(path, { path });
22
+ if (!timer) {
23
+ timer = setTimeout(flush, debounceMs);
24
+ }
25
+ };
26
+ const watcher = watch(inputDir, { recursive: true }, (_eventType, filename) => {
27
+ const path = filename ?? '';
28
+ // Some platforms report no filename — treat as a full rebuild trigger.
29
+ if (!path || isWatchedPath(path)) {
30
+ schedule(path);
31
+ }
32
+ });
33
+ return {
34
+ close: () => {
35
+ if (timer)
36
+ clearTimeout(timer);
37
+ watcher.close();
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function rootPrefix(pageUrl: string): string;
2
+ export declare function relativeUrl(root: string, url: string): string;
@@ -0,0 +1,9 @@
1
+ export function rootPrefix(pageUrl) {
2
+ const depth = pageUrl.split('/').length - 2;
3
+ return depth <= 0 ? './' : '../'.repeat(depth);
4
+ }
5
+ export function relativeUrl(root, url) {
6
+ if (!url.startsWith('/') || url.startsWith('//'))
7
+ return url;
8
+ return root + (url === '/' ? 'index.html' : url.slice(1));
9
+ }
@@ -3,7 +3,9 @@ export interface BotdocsConfig {
3
3
  title?: string;
4
4
  description?: string;
5
5
  theme?: Theme;
6
+ customCss?: string;
6
7
  attribution?: boolean;
8
+ baseUrl?: string;
7
9
  chat?: {
8
10
  enabled?: boolean;
9
11
  welcomeMessage?: string;
@@ -11,7 +13,9 @@ export interface BotdocsConfig {
11
13
  build?: {
12
14
  chunkSize?: number;
13
15
  chunkOverlap?: number;
16
+ minChunkSize?: number;
14
17
  topK?: number;
18
+ minScore?: number;
15
19
  };
16
20
  }
17
21
  export interface BuildOptions {
@@ -10,6 +10,8 @@ export const defaultConfig = {
10
10
  build: {
11
11
  chunkSize: 500,
12
12
  chunkOverlap: 50,
13
+ minChunkSize: 15,
13
14
  topK: 3,
15
+ minScore: 0.75,
14
16
  },
15
17
  };
@@ -3,7 +3,7 @@ export interface DocumentMetadata {
3
3
  description?: string;
4
4
  date?: string;
5
5
  author?: string;
6
- [key: string]: any;
6
+ [key: string]: unknown;
7
7
  }
8
8
  export interface ProcessedDocument {
9
9
  filePath: string;