mdorigin 0.1.8 → 0.2.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/dist/search.d.ts CHANGED
@@ -19,11 +19,13 @@ export interface SearchHit {
19
19
  score: number;
20
20
  };
21
21
  }
22
+ export interface SearchQueryOptions {
23
+ topK?: number;
24
+ relativePathPrefix?: string;
25
+ metadata?: Record<string, string>;
26
+ }
22
27
  export interface SearchApi {
23
- search(query: string, options?: {
24
- topK?: number;
25
- relativePathPrefix?: string;
26
- }): Promise<SearchHit[]>;
28
+ search(query: string, options?: SearchQueryOptions): Promise<SearchHit[]>;
27
29
  }
28
30
  export interface SearchBundleEntry {
29
31
  path: string;
@@ -32,6 +34,13 @@ export interface SearchBundleEntry {
32
34
  text?: string;
33
35
  base64?: string;
34
36
  }
37
+ export interface ExternalSearchBundleEntry {
38
+ path: string;
39
+ mediaType: string;
40
+ storageKind: 'assets' | 'r2';
41
+ storageKey: string;
42
+ byteSize: number;
43
+ }
35
44
  export interface BuildSearchBundleOptions {
36
45
  rootDir: string;
37
46
  outDir: string;
@@ -39,21 +48,35 @@ export interface BuildSearchBundleOptions {
39
48
  draftMode?: 'include' | 'exclude';
40
49
  embeddingBackend?: 'hashing' | 'model2vec';
41
50
  model?: string;
51
+ incremental?: boolean;
52
+ cachePath?: string;
42
53
  }
43
54
  export interface BuildSearchBundleResult {
44
55
  outputDir: string;
45
56
  documentCount: number;
46
57
  chunkCount: number;
47
58
  vectorDimensions: number;
59
+ cachePath?: string;
60
+ incremental?: {
61
+ scannedDocumentCount: number;
62
+ newDocumentCount: number;
63
+ changedDocumentCount: number;
64
+ unchangedDocumentCount: number;
65
+ removedDocumentCount: number;
66
+ activeDocumentCount: number;
67
+ activeChunkCount: number;
68
+ };
48
69
  }
49
70
  export interface SearchBundleOptions {
50
71
  indexDir: string;
51
72
  query: string;
52
73
  topK?: number;
53
74
  relativePathPrefix?: string;
75
+ metadata?: Record<string, string>;
54
76
  }
55
77
  export declare function buildSearchBundle(options: BuildSearchBundleOptions): Promise<BuildSearchBundleResult>;
56
78
  export declare function searchBundle(options: SearchBundleOptions): Promise<SearchHit[]>;
57
79
  export declare function createSearchApiFromDirectory(indexDir: string): Promise<SearchApi>;
58
80
  export declare function createSearchApiFromBundle(bundleEntries: SearchBundleEntry[]): SearchApi;
81
+ export declare function createSearchApiFromExternalBundle(bundleEntries: ExternalSearchBundleEntry[], loadResponse: (entry: ExternalSearchBundleEntry) => Promise<Response>): SearchApi;
59
82
  export {};
package/dist/search.js CHANGED
@@ -1,59 +1,100 @@
1
- import { readdir, readFile, realpath, stat } from 'node:fs/promises';
1
+ import { rmSync } from 'node:fs';
2
+ import { mkdtemp, mkdir, readdir, readFile, realpath, stat, writeFile, } from 'node:fs/promises';
3
+ import os from 'node:os';
2
4
  import path from 'node:path';
3
5
  import { inferDirectoryContentType } from './core/content-type.js';
4
6
  import { getDirectoryIndexCandidates } from './core/directory-index.js';
5
- import { getDocumentSummary, getDocumentTitle, parseMarkdownDocument, } from './core/markdown.js';
7
+ import { getDocumentSummary, getDocumentTitle, parseMarkdownDocument, stripMachineOnlyMarkdownComments, stripManagedIndexBlock, } from './core/markdown.js';
8
+ import { isIgnoredContentName } from './core/content-store.js';
9
+ const OVERVIEW_CONTENT_FILENAMES = new Set(['readme.md', 'index.md', 'skill.md']);
10
+ const materializedSearchBundleDirectories = new Set();
11
+ let searchBundleDirectoryCleanupRegistered = false;
6
12
  export async function buildSearchBundle(options) {
7
13
  const buildModule = await loadIndexbindBuildModule();
8
14
  const rootDir = path.resolve(options.rootDir);
15
+ const outputDir = path.resolve(options.outDir);
9
16
  const documents = await collectSearchDocuments(rootDir, options.siteConfig, {
10
17
  draftMode: options.draftMode ?? 'exclude',
11
18
  });
12
- const stats = await buildModule.buildCanonicalBundle(path.resolve(options.outDir), documents, {
19
+ const buildOptions = {
13
20
  embeddingBackend: options.embeddingBackend ?? 'model2vec',
14
21
  model: options.model,
15
22
  sourceRootId: path.basename(rootDir),
16
23
  sourceRootPath: rootDir,
17
- });
24
+ };
25
+ if (options.incremental) {
26
+ const cachePath = path.resolve(options.cachePath ?? defaultSearchBuildCachePath(outputDir));
27
+ await mkdir(path.dirname(cachePath), { recursive: true });
28
+ const removedRelativePaths = await resolveRemovedRelativePaths(cachePath, documents.map((document) => document.relativePath));
29
+ const incrementalStats = await buildModule.updateBuildCache(cachePath, documents, buildOptions, removedRelativePaths);
30
+ const stats = await buildModule.exportCanonicalBundleFromBuildCache(cachePath, outputDir);
31
+ await writeSearchBuildState(cachePath, documents.map((document) => document.relativePath));
32
+ return {
33
+ outputDir,
34
+ documentCount: stats.documentCount,
35
+ chunkCount: stats.chunkCount,
36
+ vectorDimensions: stats.vectorDimensions,
37
+ cachePath,
38
+ incremental: incrementalStats,
39
+ };
40
+ }
41
+ const stats = await buildModule.buildCanonicalBundle(outputDir, documents, buildOptions);
18
42
  return {
19
- outputDir: path.resolve(options.outDir),
43
+ outputDir,
20
44
  documentCount: stats.documentCount,
21
45
  chunkCount: stats.chunkCount,
22
46
  vectorDimensions: stats.vectorDimensions,
23
47
  };
24
48
  }
25
49
  export async function searchBundle(options) {
26
- const webModule = await loadIndexbindWebModule();
27
- const index = await webModule.openWebIndex(path.resolve(options.indexDir));
28
- return rerankSearchHits(await index.search(options.query, {
50
+ const searchContext = await openSearchContextFromDirectory(path.resolve(options.indexDir));
51
+ return rerankSearchHits(hydrateSearchHits(await searchContext.index.search(options.query, buildIndexbindSearchOptions({
29
52
  topK: options.topK ?? 10,
30
53
  relativePathPrefix: options.relativePathPrefix,
31
- }));
54
+ metadata: options.metadata,
55
+ })), searchContext.documentsById));
32
56
  }
33
57
  export async function createSearchApiFromDirectory(indexDir) {
34
- const webModule = await loadIndexbindWebModule();
35
- const index = await webModule.openWebIndex(path.resolve(indexDir));
58
+ const searchContext = await openSearchContextFromDirectory(path.resolve(indexDir));
36
59
  return {
37
60
  async search(query, options) {
38
- return rerankSearchHits(await index.search(query, {
61
+ return rerankSearchHits(hydrateSearchHits(await searchContext.index.search(query, buildIndexbindSearchOptions({
39
62
  topK: options?.topK,
40
63
  relativePathPrefix: options?.relativePathPrefix,
41
- }));
64
+ metadata: options?.metadata,
65
+ })), searchContext.documentsById));
42
66
  },
43
67
  };
44
68
  }
45
69
  export function createSearchApiFromBundle(bundleEntries) {
46
- let indexPromise = null;
70
+ let searchContextPromise = null;
71
+ return {
72
+ async search(query, options) {
73
+ if (searchContextPromise === null) {
74
+ searchContextPromise = openSearchContextFromBundle(bundleEntries, async (entry) => createInlineSearchBundleResponse(entry));
75
+ }
76
+ const searchContext = await searchContextPromise;
77
+ return rerankSearchHits(hydrateSearchHits(await searchContext.index.search(query, buildIndexbindSearchOptions({
78
+ topK: options?.topK,
79
+ relativePathPrefix: options?.relativePathPrefix,
80
+ metadata: options?.metadata,
81
+ })), searchContext.documentsById));
82
+ },
83
+ };
84
+ }
85
+ export function createSearchApiFromExternalBundle(bundleEntries, loadResponse) {
86
+ let searchContextPromise = null;
47
87
  return {
48
88
  async search(query, options) {
49
- if (indexPromise === null) {
50
- indexPromise = openWebIndexFromBundle(bundleEntries);
89
+ if (searchContextPromise === null) {
90
+ searchContextPromise = openSearchContextFromBundle(bundleEntries, loadResponse);
51
91
  }
52
- const index = await indexPromise;
53
- return rerankSearchHits(await index.search(query, {
92
+ const searchContext = await searchContextPromise;
93
+ return rerankSearchHits(hydrateSearchHits(await searchContext.index.search(query, buildIndexbindSearchOptions({
54
94
  topK: options?.topK,
55
95
  relativePathPrefix: options?.relativePathPrefix,
56
- }));
96
+ metadata: options?.metadata,
97
+ })), searchContext.documentsById));
57
98
  },
58
99
  };
59
100
  }
@@ -77,12 +118,15 @@ async function collectSearchDocuments(rootDir, siteConfig, options) {
77
118
  canonicalUrl: absoluteCanonicalUrl,
78
119
  title: getDocumentTitle(parsed.meta, parsed.body, fallbackTitleFromRelativePath(document.relativePath)),
79
120
  summary: getDocumentSummary(parsed.meta, parsed.body),
80
- content: markdown,
121
+ content: buildSearchableMarkdownBody(parsed.body),
81
122
  metadata: buildSearchMetadata(document.relativePath, canonicalPath, parsed.meta, siteConfig),
82
123
  });
83
124
  }
84
125
  return documents;
85
126
  }
127
+ function buildSearchableMarkdownBody(markdownBody) {
128
+ return stripMachineOnlyMarkdownComments(stripManagedIndexBlock(markdownBody)).trim();
129
+ }
86
130
  async function listSearchDocuments(rootDir) {
87
131
  const results = [];
88
132
  await walkDirectory(rootDir, '', new Set(), results);
@@ -102,7 +146,7 @@ async function walkDirectory(absoluteDirectoryPath, relativeDirectoryPath, visit
102
146
  results.push(indexFile);
103
147
  }
104
148
  for (const entry of entries) {
105
- if (entry.name.startsWith('.')) {
149
+ if (isIgnoredContentName(entry.name)) {
106
150
  continue;
107
151
  }
108
152
  const absoluteEntryPath = path.join(absoluteDirectoryPath, entry.name);
@@ -160,7 +204,7 @@ async function inspectDirectoryShape(directoryPath) {
160
204
  let hasExtraMarkdownFiles = false;
161
205
  let hasAssetFiles = false;
162
206
  for (const entry of entries) {
163
- if (entry.name.startsWith('.')) {
207
+ if (isIgnoredContentName(entry.name)) {
164
208
  continue;
165
209
  }
166
210
  const absoluteEntryPath = path.join(directoryPath, entry.name);
@@ -196,6 +240,8 @@ function buildSearchMetadata(relativePath, canonicalPath, meta, siteConfig) {
196
240
  markdownPath: `/${relativePath}`,
197
241
  canonicalPath,
198
242
  siteTitle: siteConfig.siteTitle,
243
+ section: getSearchSection(relativePath),
244
+ isOverview: isOverviewContentPath(relativePath),
199
245
  };
200
246
  if (meta.type === 'page' || meta.type === 'post') {
201
247
  metadata.type = meta.type;
@@ -211,6 +257,17 @@ function buildSearchMetadata(relativePath, canonicalPath, meta, siteConfig) {
211
257
  }
212
258
  return metadata;
213
259
  }
260
+ function getSearchSection(relativePath) {
261
+ const directory = path.posix.dirname(relativePath.replaceAll('\\', '/'));
262
+ if (directory === '.') {
263
+ return '';
264
+ }
265
+ const [firstSegment] = directory.split('/', 1);
266
+ return firstSegment ?? '';
267
+ }
268
+ function isOverviewContentPath(relativePath) {
269
+ return OVERVIEW_CONTENT_FILENAMES.has(path.posix.basename(relativePath).toLowerCase());
270
+ }
214
271
  function fallbackTitleFromRelativePath(relativePath) {
215
272
  const baseName = path.posix.basename(relativePath);
216
273
  if (DIRECTORY_INDEX_FILENAMES_LOWER.has(baseName.toLowerCase())) {
@@ -249,6 +306,33 @@ async function pathExists(filePath) {
249
306
  throw error;
250
307
  }
251
308
  }
309
+ function defaultSearchBuildCachePath(outputDir) {
310
+ return path.join(path.dirname(outputDir), `${path.basename(outputDir)}.indexbind-cache.sqlite`);
311
+ }
312
+ function getSearchBuildStatePath(cachePath) {
313
+ return `${cachePath}.state.json`;
314
+ }
315
+ async function resolveRemovedRelativePaths(cachePath, currentRelativePaths) {
316
+ const previousRelativePaths = await readSearchBuildState(cachePath);
317
+ const current = new Set(currentRelativePaths);
318
+ return previousRelativePaths.filter((relativePath) => !current.has(relativePath));
319
+ }
320
+ async function readSearchBuildState(cachePath) {
321
+ const statePath = getSearchBuildStatePath(cachePath);
322
+ if (!(await pathExists(statePath))) {
323
+ return [];
324
+ }
325
+ const parsed = JSON.parse(await readFile(statePath, 'utf8'));
326
+ return Array.isArray(parsed.relativePaths)
327
+ ? parsed.relativePaths.filter((value) => typeof value === 'string')
328
+ : [];
329
+ }
330
+ async function writeSearchBuildState(cachePath, relativePaths) {
331
+ await writeFile(getSearchBuildStatePath(cachePath), JSON.stringify({
332
+ version: 1,
333
+ relativePaths: [...relativePaths].sort((left, right) => left.localeCompare(right)),
334
+ }, null, 2), 'utf8');
335
+ }
252
336
  function isIgnoredSkillSupportDirectory(name) {
253
337
  return (name === 'scripts' ||
254
338
  name === 'references' ||
@@ -279,8 +363,10 @@ async function loadIndexbindCloudflareModule() {
279
363
  throw new Error(`Search query requires the optional package "indexbind". Install it first, for example: npm install indexbind`, { cause: error instanceof Error ? error : undefined });
280
364
  }
281
365
  }
282
- async function openWebIndexFromBundle(bundleEntries) {
283
- const cloudflareModule = await loadIndexbindCloudflareModule();
366
+ async function openWebIndexFromVirtualBundle(bundleEntries, loadResponse) {
367
+ if (isNodeRuntime()) {
368
+ return openWebIndexFromMaterializedBundle(bundleEntries, loadResponse);
369
+ }
284
370
  const baseUrl = 'https://mdorigin-search.invalid/';
285
371
  const originalFetch = globalThis.fetch;
286
372
  const bundleMap = new Map(bundleEntries.map((entry) => [new URL(entry.path, baseUrl).toString(), entry]));
@@ -292,31 +378,209 @@ async function openWebIndexFromBundle(bundleEntries) {
292
378
  : input.url;
293
379
  const entry = bundleMap.get(requestUrl);
294
380
  if (entry) {
295
- const headers = new Headers({ 'content-type': entry.mediaType });
296
- const binaryBody = decodeBase64(entry.base64 ?? '');
297
- const body = entry.kind === 'text'
298
- ? entry.text ?? ''
299
- : new Blob([new Uint8Array(binaryBody)], {
300
- type: entry.mediaType,
301
- });
302
- return new Response(body, {
303
- status: 200,
304
- headers,
305
- });
381
+ return loadResponse(entry);
306
382
  }
307
383
  return originalFetch(input, init);
308
384
  };
309
385
  try {
310
- return await cloudflareModule.openWebIndex(new URL(baseUrl));
386
+ try {
387
+ const cloudflareModule = await loadIndexbindCloudflareModule();
388
+ return await cloudflareModule.openWebIndex(new URL(baseUrl));
389
+ }
390
+ catch (error) {
391
+ if (!isCloudflareWasmImportError(error)) {
392
+ throw error;
393
+ }
394
+ const webModule = await loadIndexbindWebModule();
395
+ return await webModule.openWebIndex(new URL(baseUrl));
396
+ }
311
397
  }
312
398
  finally {
313
399
  globalThis.fetch = originalFetch;
314
400
  }
315
401
  }
402
+ async function openSearchContextFromDirectory(indexDir) {
403
+ const webModule = await loadIndexbindWebModule();
404
+ const [index, documentsById] = await Promise.all([
405
+ webModule.openWebIndex(indexDir),
406
+ readSearchBundleDocumentsFromDirectory(indexDir),
407
+ ]);
408
+ return { index, documentsById };
409
+ }
410
+ async function openSearchContextFromBundle(bundleEntries, loadResponse) {
411
+ const [index, documentsById] = await Promise.all([
412
+ openWebIndexFromVirtualBundle(bundleEntries, loadResponse),
413
+ readSearchBundleDocumentsFromResponses(bundleEntries, loadResponse),
414
+ ]);
415
+ return { index, documentsById };
416
+ }
417
+ async function openWebIndexFromMaterializedBundle(bundleEntries, loadResponse) {
418
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'mdorigin-search-bundle-'));
419
+ registerMaterializedSearchBundleDirectory(tempDir);
420
+ for (const entry of bundleEntries) {
421
+ const response = await loadResponse(entry);
422
+ if (!response.ok) {
423
+ throw new Error(`Failed to materialize search bundle file ${entry.path}: ${response.status} ${response.statusText}`);
424
+ }
425
+ const outputPath = resolveMaterializedSearchBundlePath(tempDir, entry.path);
426
+ await mkdir(path.dirname(outputPath), { recursive: true });
427
+ if (entry.mediaType.startsWith('text/') || entry.mediaType.includes('json')) {
428
+ await writeFile(outputPath, await response.text(), 'utf8');
429
+ continue;
430
+ }
431
+ await writeFile(outputPath, new Uint8Array(await response.arrayBuffer()));
432
+ }
433
+ const webModule = await loadIndexbindWebModule();
434
+ return webModule.openWebIndex(tempDir);
435
+ }
436
+ function createInlineSearchBundleResponse(entry) {
437
+ const headers = new Headers({ 'content-type': entry.mediaType });
438
+ const binaryBody = decodeBase64(entry.base64 ?? '');
439
+ const body = entry.kind === 'text'
440
+ ? entry.text ?? ''
441
+ : new Blob([new Uint8Array(binaryBody)], {
442
+ type: entry.mediaType,
443
+ });
444
+ return new Response(body, {
445
+ status: 200,
446
+ headers,
447
+ });
448
+ }
449
+ function isCloudflareWasmImportError(error) {
450
+ const targetMessages = [
451
+ 'Unknown file extension ".wasm"',
452
+ 'WebAssembly.Module(): Argument 0 must be a buffer source',
453
+ ];
454
+ const visited = new Set();
455
+ let current = error;
456
+ while (current && typeof current === 'object' && !visited.has(current)) {
457
+ visited.add(current);
458
+ if (current instanceof Error) {
459
+ const currentMessage = current.message;
460
+ if (targetMessages.some((message) => currentMessage.includes(message))) {
461
+ return true;
462
+ }
463
+ }
464
+ const cause = current.cause;
465
+ if (!cause) {
466
+ break;
467
+ }
468
+ current = cause;
469
+ }
470
+ return false;
471
+ }
316
472
  function decodeBase64(value) {
317
473
  const decoded = atob(value);
318
474
  return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
319
475
  }
476
+ function buildIndexbindSearchOptions(options) {
477
+ const normalized = {};
478
+ if (typeof options.topK === 'number' &&
479
+ Number.isFinite(options.topK) &&
480
+ Number.isInteger(options.topK) &&
481
+ options.topK > 0) {
482
+ normalized.topK = options.topK;
483
+ }
484
+ if (typeof options.relativePathPrefix === 'string' && options.relativePathPrefix !== '') {
485
+ normalized.relativePathPrefix = options.relativePathPrefix;
486
+ }
487
+ if (options.metadata && Object.keys(options.metadata).length > 0) {
488
+ normalized.metadata = options.metadata;
489
+ }
490
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
491
+ }
492
+ function hydrateSearchHits(hits, documentsById) {
493
+ return hits.map((hit) => {
494
+ const document = documentsById.get(hit.docId);
495
+ if (!document) {
496
+ return hit;
497
+ }
498
+ return {
499
+ ...hit,
500
+ canonicalUrl: hit.canonicalUrl ?? document.canonicalUrl ?? undefined,
501
+ title: hit.title ?? document.title ?? undefined,
502
+ summary: hit.summary ?? document.summary ?? undefined,
503
+ metadata: Object.keys(hit.metadata).length > 0
504
+ ? hit.metadata
505
+ : document.metadata ?? hit.metadata,
506
+ };
507
+ });
508
+ }
509
+ async function readSearchBundleDocumentsFromDirectory(indexDir) {
510
+ const manifest = JSON.parse(await readFile(path.join(indexDir, 'manifest.json'), 'utf8'));
511
+ const documentsFile = manifest.files?.documents;
512
+ if (!documentsFile) {
513
+ return new Map();
514
+ }
515
+ const documents = JSON.parse(await readFile(path.join(indexDir, documentsFile), 'utf8'));
516
+ return new Map(documents.map((document) => [document.docId, document]));
517
+ }
518
+ async function readSearchBundleDocumentsFromResponses(bundleEntries, loadResponse) {
519
+ const manifestEntry = bundleEntries.find((entry) => entry.path === 'manifest.json');
520
+ if (!manifestEntry) {
521
+ return new Map();
522
+ }
523
+ const manifest = JSON.parse(await loadBundleResponseText(manifestEntry, loadResponse));
524
+ const documentsFile = manifest.files?.documents;
525
+ if (!documentsFile) {
526
+ return new Map();
527
+ }
528
+ const documentsEntry = bundleEntries.find((entry) => entry.path === documentsFile);
529
+ if (!documentsEntry) {
530
+ return new Map();
531
+ }
532
+ const documents = JSON.parse(await loadBundleResponseText(documentsEntry, loadResponse));
533
+ return new Map(documents.map((document) => [document.docId, document]));
534
+ }
535
+ async function loadBundleResponseText(entry, loadResponse) {
536
+ const response = await loadResponse(entry);
537
+ if (!response.ok) {
538
+ throw new Error(`Failed to read search bundle file ${entry.path}: ${response.status} ${response.statusText}`);
539
+ }
540
+ return response.text();
541
+ }
542
+ function isNodeRuntime() {
543
+ return (typeof process !== 'undefined' &&
544
+ typeof process.versions === 'object' &&
545
+ typeof process.versions.node === 'string');
546
+ }
547
+ function resolveMaterializedSearchBundlePath(baseDir, entryPath) {
548
+ if (path.isAbsolute(entryPath)) {
549
+ throw new Error(`Search bundle path must be relative: ${entryPath}`);
550
+ }
551
+ const normalizedEntryPath = entryPath.replaceAll('/', path.sep);
552
+ const outputPath = path.resolve(baseDir, normalizedEntryPath);
553
+ const relativeOutputPath = path.relative(baseDir, outputPath);
554
+ if (relativeOutputPath === '' ||
555
+ relativeOutputPath.startsWith(`..${path.sep}`) ||
556
+ relativeOutputPath === '..' ||
557
+ path.isAbsolute(relativeOutputPath)) {
558
+ throw new Error(`Search bundle path escapes materialized bundle directory: ${entryPath}`);
559
+ }
560
+ return outputPath;
561
+ }
562
+ function registerMaterializedSearchBundleDirectory(directoryPath) {
563
+ materializedSearchBundleDirectories.add(directoryPath);
564
+ if (searchBundleDirectoryCleanupRegistered || !isNodeRuntime()) {
565
+ return;
566
+ }
567
+ const cleanup = () => {
568
+ for (const stagedDirectory of materializedSearchBundleDirectories) {
569
+ rmSync(stagedDirectory, { recursive: true, force: true });
570
+ }
571
+ materializedSearchBundleDirectories.clear();
572
+ };
573
+ process.once('exit', cleanup);
574
+ process.once('SIGINT', () => {
575
+ cleanup();
576
+ process.exit(130);
577
+ });
578
+ process.once('SIGTERM', () => {
579
+ cleanup();
580
+ process.exit(143);
581
+ });
582
+ searchBundleDirectoryCleanupRegistered = true;
583
+ }
320
584
  function rerankSearchHits(hits) {
321
585
  const remaining = [...hits];
322
586
  const ordered = [];
@@ -359,8 +623,7 @@ function compareHits(left, right) {
359
623
  return left.relativePath.localeCompare(right.relativePath);
360
624
  }
361
625
  function isOverviewSearchHit(hit) {
362
- const baseName = path.posix.basename(hit.relativePath).toLowerCase();
363
- return baseName === 'readme.md' || baseName === 'index.md';
626
+ return OVERVIEW_CONTENT_FILENAMES.has(path.posix.basename(hit.relativePath).toLowerCase());
364
627
  }
365
628
  function getTopLevelSection(relativePath) {
366
629
  const normalized = relativePath.replaceAll('\\', '/');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdorigin",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Markdown-first publishing for humans and agents.",
6
6
  "repository": {
@@ -66,9 +66,11 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "gray-matter": "^4.0.3",
69
+ "rehype-raw": "^7.0.0",
70
+ "rehype-stringify": "^10.0.1",
69
71
  "remark": "^15.0.1",
70
72
  "remark-gfm": "^4.0.1",
71
- "remark-html": "^16.0.1",
73
+ "remark-rehype": "^11.1.2",
72
74
  "tsx": "^4.20.5"
73
75
  },
74
76
  "devDependencies": {
@@ -76,6 +78,6 @@
76
78
  "typescript": "^5.9.2"
77
79
  },
78
80
  "optionalDependencies": {
79
- "indexbind": "^0.2.1"
81
+ "indexbind": "^0.3.0"
80
82
  }
81
83
  }
@@ -1 +0,0 @@
1
- export type TemplateName = 'document' | 'catalog';
@@ -1 +0,0 @@
1
- export {};