mdorigin 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.
- package/README.md +2 -0
- package/dist/adapters/cloudflare.d.ts +2 -1
- package/dist/adapters/cloudflare.js +87 -3
- package/dist/cli/build-search.js +15 -4
- package/dist/cli/help.js +3 -3
- package/dist/cli/search.js +16 -4
- package/dist/cloudflare.d.ts +4 -2
- package/dist/cloudflare.js +118 -33
- package/dist/core/api.js +34 -1
- package/dist/core/markdown.d.ts +1 -0
- package/dist/core/markdown.js +3 -0
- package/dist/search.d.ts +27 -4
- package/dist/search.js +298 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -135,3 +135,5 @@ Runtime endpoints:
|
|
|
135
135
|
- CLI: [`docs/site/reference/cli.md`](docs/site/reference/cli.md)
|
|
136
136
|
- Search setup: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md#quick-start)
|
|
137
137
|
- Cloudflare deployment: [`docs/site/guides/cloudflare.md`](docs/site/guides/cloudflare.md)
|
|
138
|
+
|
|
139
|
+
The docs site at <https://mdorigin.jolestar.workers.dev> is deployed automatically from `main` with GitHub Actions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MdoPlugin } from '../core/extensions.js';
|
|
2
2
|
import type { ResolvedSiteConfig } from '../core/site-config.js';
|
|
3
|
-
import { type SearchBundleEntry } from '../search.js';
|
|
3
|
+
import { type ExternalSearchBundleEntry, type SearchBundleEntry } from '../search.js';
|
|
4
4
|
export interface TextCloudflareManifestEntry {
|
|
5
5
|
path: string;
|
|
6
6
|
kind: 'text';
|
|
@@ -30,6 +30,7 @@ export interface CloudflareManifest {
|
|
|
30
30
|
entries: CloudflareManifestEntry[];
|
|
31
31
|
siteConfig?: ResolvedSiteConfig;
|
|
32
32
|
searchEntries?: SearchBundleEntry[];
|
|
33
|
+
externalSearchEntries?: ExternalSearchBundleEntry[];
|
|
33
34
|
runtime?: CloudflareBundleRuntimeConfig;
|
|
34
35
|
}
|
|
35
36
|
export interface CloudflareAssetsBindingLike {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MemoryContentStore } from '../core/content-store.js';
|
|
2
2
|
import { handleSiteRequest } from '../core/request-handler.js';
|
|
3
3
|
import { resolveRequest } from '../core/router.js';
|
|
4
|
-
import { createSearchApiFromBundle } from '../search.js';
|
|
4
|
+
import { createSearchApiFromBundle, createSearchApiFromExternalBundle, } from '../search.js';
|
|
5
5
|
export function createCloudflareWorker(manifest, options = {}) {
|
|
6
6
|
const storeIndex = new MemoryContentStore(manifest.entries.map((entry) => {
|
|
7
7
|
if (entry.kind === 'text') {
|
|
@@ -26,12 +26,18 @@ export function createCloudflareWorker(manifest, options = {}) {
|
|
|
26
26
|
bytes: decodeBase64(entry.base64 ?? ''),
|
|
27
27
|
};
|
|
28
28
|
}));
|
|
29
|
-
const
|
|
29
|
+
const inlineSearchApi = manifest.searchEntries && manifest.searchEntries.length > 0
|
|
30
30
|
? createSearchApiFromBundle(manifest.searchEntries)
|
|
31
31
|
: undefined;
|
|
32
|
+
const externalSearchApis = new WeakMap();
|
|
33
|
+
let defaultExternalSearchApi;
|
|
32
34
|
return {
|
|
33
35
|
async fetch(request, env) {
|
|
34
36
|
const url = new URL(request.url);
|
|
37
|
+
const externalSearchApi = getExternalSearchApi(manifest, env, externalSearchApis, defaultExternalSearchApi);
|
|
38
|
+
if (env === undefined && externalSearchApi !== undefined) {
|
|
39
|
+
defaultExternalSearchApi = externalSearchApi;
|
|
40
|
+
}
|
|
35
41
|
const directBinaryResponse = await tryServeExternalBinary(manifest, request, env);
|
|
36
42
|
if (directBinaryResponse !== null) {
|
|
37
43
|
return directBinaryResponse;
|
|
@@ -60,7 +66,7 @@ export function createCloudflareWorker(manifest, options = {}) {
|
|
|
60
66
|
acceptHeader: request.headers.get('accept') ?? undefined,
|
|
61
67
|
searchParams: url.searchParams,
|
|
62
68
|
requestUrl: request.url,
|
|
63
|
-
searchApi,
|
|
69
|
+
searchApi: inlineSearchApi ?? externalSearchApi,
|
|
64
70
|
plugins: options.plugins,
|
|
65
71
|
});
|
|
66
72
|
const headers = new Headers(siteResponse.headers);
|
|
@@ -76,6 +82,84 @@ export function createCloudflareWorker(manifest, options = {}) {
|
|
|
76
82
|
},
|
|
77
83
|
};
|
|
78
84
|
}
|
|
85
|
+
function getExternalSearchApi(manifest, env, cache, defaultApi) {
|
|
86
|
+
if (!manifest.externalSearchEntries || manifest.externalSearchEntries.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (!env) {
|
|
90
|
+
return (defaultApi ??
|
|
91
|
+
createSearchApiFromExternalBundle(manifest.externalSearchEntries, async (entry) => loadExternalSearchEntryResponse(entry, undefined, manifest.runtime?.r2Binding)));
|
|
92
|
+
}
|
|
93
|
+
const cached = cache.get(env);
|
|
94
|
+
if (cached) {
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
const searchApi = createSearchApiFromExternalBundle(manifest.externalSearchEntries, async (entry) => loadExternalSearchEntryResponse(entry, env, manifest.runtime?.r2Binding));
|
|
98
|
+
cache.set(env, searchApi);
|
|
99
|
+
return searchApi;
|
|
100
|
+
}
|
|
101
|
+
async function loadExternalSearchEntryResponse(entry, env, r2Binding) {
|
|
102
|
+
if (entry.storageKind === 'assets') {
|
|
103
|
+
const assetsBinding = env?.ASSETS;
|
|
104
|
+
if (!assetsBinding) {
|
|
105
|
+
throw new Error(`Cloudflare ASSETS binding is required to serve search bundle file ${entry.path}.`);
|
|
106
|
+
}
|
|
107
|
+
const assetResponse = await assetsBinding.fetch(new Request(new URL(`/${entry.storageKey}`, 'https://mdorigin-search.invalid/'), {
|
|
108
|
+
method: 'GET',
|
|
109
|
+
}));
|
|
110
|
+
if (assetResponse.ok) {
|
|
111
|
+
return assetResponse;
|
|
112
|
+
}
|
|
113
|
+
return new Response('Not Found', {
|
|
114
|
+
status: 404,
|
|
115
|
+
headers: {
|
|
116
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const bindingName = r2Binding ?? 'MDORIGIN_R2';
|
|
121
|
+
const bucket = env?.[bindingName];
|
|
122
|
+
if (!bucket) {
|
|
123
|
+
throw new Error(`Cloudflare R2 binding ${bindingName} is required to serve search bundle file ${entry.path}.`);
|
|
124
|
+
}
|
|
125
|
+
const object = await bucket.get(entry.storageKey);
|
|
126
|
+
if (!object) {
|
|
127
|
+
return new Response('Not Found', {
|
|
128
|
+
status: 404,
|
|
129
|
+
headers: {
|
|
130
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const headers = new Headers({
|
|
135
|
+
'content-type': entry.mediaType,
|
|
136
|
+
});
|
|
137
|
+
if (object.httpEtag) {
|
|
138
|
+
headers.set('etag', object.httpEtag);
|
|
139
|
+
}
|
|
140
|
+
if (object.body instanceof ReadableStream) {
|
|
141
|
+
return new Response(object.body, {
|
|
142
|
+
status: 200,
|
|
143
|
+
headers,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (object.body && 'arrayBuffer' in object.body) {
|
|
147
|
+
return new Response(await object.body.arrayBuffer(), {
|
|
148
|
+
status: 200,
|
|
149
|
+
headers,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (typeof object.arrayBuffer === 'function') {
|
|
153
|
+
return new Response(await object.arrayBuffer(), {
|
|
154
|
+
status: 200,
|
|
155
|
+
headers,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return new Response(null, {
|
|
159
|
+
status: 200,
|
|
160
|
+
headers,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
79
163
|
async function tryServeExternalBinary(manifest, request, env) {
|
|
80
164
|
const resolved = resolveRequest(new URL(request.url).pathname);
|
|
81
165
|
if (resolved.kind !== 'asset' || !resolved.sourcePath) {
|
package/dist/cli/build-search.js
CHANGED
|
@@ -5,11 +5,11 @@ import { createFileSystemContentStore } from '../adapters/node.js';
|
|
|
5
5
|
export async function runBuildSearchCommand(rawArgs) {
|
|
6
6
|
const args = parseArgs(rawArgs);
|
|
7
7
|
if (args.help) {
|
|
8
|
-
console.log('Usage: mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]');
|
|
8
|
+
console.log('Usage: mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>] [--incremental]');
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
if (!args.root) {
|
|
12
|
-
throw new Error('Usage: mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]');
|
|
12
|
+
throw new Error('Usage: mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>] [--incremental]');
|
|
13
13
|
}
|
|
14
14
|
const rootDir = path.resolve(args.root);
|
|
15
15
|
const store = createFileSystemContentStore(rootDir);
|
|
@@ -24,18 +24,28 @@ export async function runBuildSearchCommand(rawArgs) {
|
|
|
24
24
|
siteConfig,
|
|
25
25
|
embeddingBackend: args.embeddingBackend ?? 'model2vec',
|
|
26
26
|
model: args.model,
|
|
27
|
+
incremental: args.incremental,
|
|
27
28
|
});
|
|
28
|
-
console.log(
|
|
29
|
+
console.log([
|
|
30
|
+
`search bundle written to ${result.outputDir} (${result.documentCount} documents, ${result.chunkCount} chunks)`,
|
|
31
|
+
result.incremental
|
|
32
|
+
? `incremental cache: ${result.cachePath} (${result.incremental.newDocumentCount} new, ${result.incremental.changedDocumentCount} changed, ${result.incremental.unchangedDocumentCount} unchanged, ${result.incremental.removedDocumentCount} removed)`
|
|
33
|
+
: '',
|
|
34
|
+
].filter(Boolean).join('\n'));
|
|
29
35
|
}
|
|
30
36
|
function parseArgs(rawArgs) {
|
|
31
37
|
const parsed = {};
|
|
32
|
-
const supportedFlags = new Set(['root', 'out', 'embedding-backend', 'model', 'config']);
|
|
38
|
+
const supportedFlags = new Set(['root', 'out', 'embedding-backend', 'model', 'config', 'incremental']);
|
|
33
39
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
34
40
|
const arg = rawArgs[index];
|
|
35
41
|
if (arg === '--help' || arg === '-h') {
|
|
36
42
|
parsed.help = 'true';
|
|
37
43
|
continue;
|
|
38
44
|
}
|
|
45
|
+
if (arg === '--incremental') {
|
|
46
|
+
parsed.incremental = 'true';
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
39
49
|
if (arg.startsWith('--')) {
|
|
40
50
|
const flag = arg.slice(2);
|
|
41
51
|
if (!supportedFlags.has(flag)) {
|
|
@@ -57,6 +67,7 @@ function parseArgs(rawArgs) {
|
|
|
57
67
|
embeddingBackend: parsed['embedding-backend'],
|
|
58
68
|
model: parsed.model,
|
|
59
69
|
config: parsed.config,
|
|
70
|
+
incremental: parsed.incremental === 'true',
|
|
60
71
|
help: parsed.help === 'true',
|
|
61
72
|
};
|
|
62
73
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -2,11 +2,11 @@ export const ROOT_USAGE_LINES = [
|
|
|
2
2
|
'Usage:',
|
|
3
3
|
' mdorigin dev --root <content-dir> [--port 3000] [--config <config-file>] [--search ./dist/search]',
|
|
4
4
|
' mdorigin build index (--root <content-dir> | --dir <content-dir>) [--config <config-file>]',
|
|
5
|
-
' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]',
|
|
5
|
+
' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>] [--incremental]',
|
|
6
6
|
' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config <config-file>] [--search ./dist/search] [--binary-mode inline|external] [--assets-max-bytes 26214400] [--r2-binding MDORIGIN_R2]',
|
|
7
7
|
' mdorigin init cloudflare [--dir .] [--entry ./dist/cloudflare/worker.mjs] [--name <worker-name>] [--compatibility-date 2026-03-20] [--r2-bucket <bucket-name>] [--force]',
|
|
8
8
|
' mdorigin sync cloudflare-r2 --dir ./dist/cloudflare --bucket <bucket-name> [--force]',
|
|
9
|
-
' mdorigin search --index <search-dir> [--top-k 10] <query>',
|
|
9
|
+
' mdorigin search --index <search-dir> [--top-k 10] [--meta key=value] <query>',
|
|
10
10
|
' mdorigin version',
|
|
11
11
|
'',
|
|
12
12
|
'Global options:',
|
|
@@ -16,7 +16,7 @@ export const ROOT_USAGE_LINES = [
|
|
|
16
16
|
export const BUILD_USAGE_LINES = [
|
|
17
17
|
'Usage:',
|
|
18
18
|
' mdorigin build index (--root <content-dir> | --dir <content-dir>) [--config <config-file>]',
|
|
19
|
-
' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]',
|
|
19
|
+
' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>] [--incremental]',
|
|
20
20
|
' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config <config-file>] [--search ./dist/search] [--binary-mode inline|external] [--assets-max-bytes 26214400] [--r2-binding MDORIGIN_R2]',
|
|
21
21
|
];
|
|
22
22
|
export const INIT_USAGE_LINES = [
|
package/dist/cli/search.js
CHANGED
|
@@ -3,23 +3,25 @@ import { searchBundle } from '../search.js';
|
|
|
3
3
|
export async function runSearchCommand(rawArgs) {
|
|
4
4
|
const args = parseArgs(rawArgs);
|
|
5
5
|
if (args.help) {
|
|
6
|
-
console.log('Usage: mdorigin search --index <search-dir> [--top-k 10] <query>');
|
|
6
|
+
console.log('Usage: mdorigin search --index <search-dir> [--top-k 10] [--meta key=value] <query>');
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
if (!args.indexDir || !args.query) {
|
|
10
|
-
throw new Error('Usage: mdorigin search --index <search-dir> [--top-k 10] <query>');
|
|
10
|
+
throw new Error('Usage: mdorigin search --index <search-dir> [--top-k 10] [--meta key=value] <query>');
|
|
11
11
|
}
|
|
12
12
|
const hits = await searchBundle({
|
|
13
13
|
indexDir: path.resolve(args.indexDir),
|
|
14
14
|
query: args.query,
|
|
15
15
|
topK: args.topK,
|
|
16
|
+
metadata: args.metadata,
|
|
16
17
|
});
|
|
17
18
|
console.log(JSON.stringify(hits, null, 2));
|
|
18
19
|
}
|
|
19
20
|
function parseArgs(rawArgs) {
|
|
20
21
|
const flags = {};
|
|
22
|
+
const metadata = {};
|
|
21
23
|
const positionals = [];
|
|
22
|
-
const supportedFlags = new Set(['index', 'top-k']);
|
|
24
|
+
const supportedFlags = new Set(['index', 'top-k', 'meta']);
|
|
23
25
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
24
26
|
const arg = rawArgs[index];
|
|
25
27
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -33,7 +35,16 @@ function parseArgs(rawArgs) {
|
|
|
33
35
|
}
|
|
34
36
|
const value = rawArgs[index + 1];
|
|
35
37
|
if (value && !value.startsWith('--')) {
|
|
36
|
-
|
|
38
|
+
if (flag === 'meta') {
|
|
39
|
+
const separator = value.indexOf('=');
|
|
40
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
41
|
+
throw new Error(`Invalid value for --meta: ${value}`);
|
|
42
|
+
}
|
|
43
|
+
metadata[value.slice(0, separator)] = value.slice(separator + 1);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
flags[flag] = value;
|
|
47
|
+
}
|
|
37
48
|
index += 1;
|
|
38
49
|
continue;
|
|
39
50
|
}
|
|
@@ -46,6 +57,7 @@ function parseArgs(rawArgs) {
|
|
|
46
57
|
indexDir: flags.index,
|
|
47
58
|
topK: Number.isInteger(topK) && topK > 0 ? topK : undefined,
|
|
48
59
|
query: positionals.join(' ').trim(),
|
|
60
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
49
61
|
help: flags.help === 'true',
|
|
50
62
|
};
|
|
51
63
|
}
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface WriteCloudflareBundleOptions extends BuildCloudflareManifestOpt
|
|
|
18
18
|
configModulePath?: string;
|
|
19
19
|
}
|
|
20
20
|
export interface CloudflareBundleMetadata {
|
|
21
|
-
version:
|
|
21
|
+
version: 2;
|
|
22
22
|
workerEntry: string;
|
|
23
23
|
binaryMode: CloudflareBinaryMode;
|
|
24
24
|
assetsMaxBytes?: number;
|
|
@@ -26,9 +26,11 @@ export interface CloudflareBundleMetadata {
|
|
|
26
26
|
r2Dir?: string;
|
|
27
27
|
r2Binding?: string;
|
|
28
28
|
siteTitle?: string;
|
|
29
|
-
|
|
29
|
+
stagedObjects: Array<{
|
|
30
|
+
kind: 'binary' | 'search';
|
|
30
31
|
path: string;
|
|
31
32
|
mediaType: string;
|
|
33
|
+
storageKind: 'assets' | 'r2';
|
|
32
34
|
storageKey: string;
|
|
33
35
|
file: string;
|
|
34
36
|
byteSize: number;
|
package/dist/cloudflare.js
CHANGED
|
@@ -11,6 +11,7 @@ const DEFAULT_ASSETS_BINDING = 'ASSETS';
|
|
|
11
11
|
const DEFAULT_R2_BINDING = 'MDORIGIN_R2';
|
|
12
12
|
const BUNDLE_FILE_NAME = 'bundle.json';
|
|
13
13
|
const R2_STATE_FILE_NAME = 'r2-sync-state.json';
|
|
14
|
+
const SEARCH_ASSETS_PREFIX = '__mdorigin/search';
|
|
14
15
|
export async function buildCloudflareManifest(options) {
|
|
15
16
|
const rootDir = path.resolve(options.rootDir);
|
|
16
17
|
const files = await listFiles(rootDir);
|
|
@@ -50,15 +51,15 @@ export async function buildCloudflareManifest(options) {
|
|
|
50
51
|
r2Binding,
|
|
51
52
|
}));
|
|
52
53
|
}
|
|
53
|
-
const
|
|
54
|
-
? await
|
|
54
|
+
const externalSearchEntries = options.searchDir
|
|
55
|
+
? await buildSearchBundleEntries(path.resolve(options.searchDir), assetsMaxBytes)
|
|
55
56
|
: undefined;
|
|
56
57
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
57
58
|
return {
|
|
58
59
|
entries,
|
|
59
60
|
siteConfig: options.siteConfig,
|
|
60
|
-
|
|
61
|
-
runtime: binaryMode === 'external'
|
|
61
|
+
externalSearchEntries,
|
|
62
|
+
runtime: binaryMode === 'external' || (externalSearchEntries?.length ?? 0) > 0
|
|
62
63
|
? {
|
|
63
64
|
binaryMode,
|
|
64
65
|
r2Binding,
|
|
@@ -118,7 +119,7 @@ export async function writeCloudflareBundle(options) {
|
|
|
118
119
|
'',
|
|
119
120
|
].join('\n');
|
|
120
121
|
await mkdir(outDir, { recursive: true });
|
|
121
|
-
const metadata = await
|
|
122
|
+
const metadata = await writeExternalStaging(path.resolve(options.rootDir), options.searchDir ? path.resolve(options.searchDir) : undefined, outDir, manifest, {
|
|
122
123
|
binaryMode,
|
|
123
124
|
assetsMaxBytes,
|
|
124
125
|
r2Binding,
|
|
@@ -140,10 +141,10 @@ export async function initCloudflareProject(options) {
|
|
|
140
141
|
throw new Error(`Refusing to overwrite ${configFile}. Re-run with --force to replace it.`);
|
|
141
142
|
}
|
|
142
143
|
const bundleMetadata = await readCloudflareBundleMetadata(options.workerEntry);
|
|
143
|
-
if (bundleMetadata
|
|
144
|
-
bundleMetadata.
|
|
144
|
+
if (bundleMetadata &&
|
|
145
|
+
bundleMetadata.stagedObjects.some((object) => object.storageKind === 'r2') &&
|
|
145
146
|
!options.r2Bucket) {
|
|
146
|
-
throw new Error('Cloudflare bundle contains R2-backed
|
|
147
|
+
throw new Error('Cloudflare bundle contains R2-backed staged objects. Re-run init cloudflare with --r2-bucket <bucket-name>.');
|
|
147
148
|
}
|
|
148
149
|
const workerName = options.workerName ??
|
|
149
150
|
slugifyWorkerName(bundleMetadata?.siteTitle ?? options.siteTitle) ??
|
|
@@ -156,7 +157,7 @@ export async function initCloudflareProject(options) {
|
|
|
156
157
|
` "main": ${JSON.stringify(toPosixPath(path.relative(projectDir, options.workerEntry)))},`,
|
|
157
158
|
` "compatibility_date": ${JSON.stringify(compatibilityDate)},`,
|
|
158
159
|
' "compatibility_flags": ["nodejs_compat"]',
|
|
159
|
-
bundleMetadata?.
|
|
160
|
+
bundleMetadata?.assetsDir
|
|
160
161
|
? [
|
|
161
162
|
',',
|
|
162
163
|
' "assets": {',
|
|
@@ -166,8 +167,8 @@ export async function initCloudflareProject(options) {
|
|
|
166
167
|
' }',
|
|
167
168
|
].join('\n')
|
|
168
169
|
: '',
|
|
169
|
-
bundleMetadata
|
|
170
|
-
bundleMetadata.
|
|
170
|
+
bundleMetadata &&
|
|
171
|
+
bundleMetadata.stagedObjects.some((object) => object.storageKind === 'r2') &&
|
|
171
172
|
bundleMetadata.r2Binding &&
|
|
172
173
|
options.r2Bucket
|
|
173
174
|
? [
|
|
@@ -193,15 +194,16 @@ export async function syncCloudflareR2(options) {
|
|
|
193
194
|
const outDir = path.resolve(options.dir);
|
|
194
195
|
const bundleFile = path.join(outDir, BUNDLE_FILE_NAME);
|
|
195
196
|
const metadata = await readBundleMetadataFile(bundleFile);
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
const r2Objects = metadata.stagedObjects.filter((object) => object.storageKind === 'r2');
|
|
198
|
+
if (r2Objects.length === 0) {
|
|
199
|
+
throw new Error(`No R2-backed staged objects found in ${bundleFile}.`);
|
|
198
200
|
}
|
|
199
201
|
const stateFile = path.join(outDir, R2_STATE_FILE_NAME);
|
|
200
202
|
const state = await readR2SyncState(stateFile);
|
|
201
203
|
const runCommand = options.runCommand ?? runWranglerCommand;
|
|
202
204
|
let uploadedCount = 0;
|
|
203
205
|
let skippedCount = 0;
|
|
204
|
-
for (const object of
|
|
206
|
+
for (const object of r2Objects) {
|
|
205
207
|
const stateKey = `${options.bucketName}:${object.storageKey}`;
|
|
206
208
|
if (!options.force && state.uploaded[stateKey]) {
|
|
207
209
|
skippedCount += 1;
|
|
@@ -254,12 +256,14 @@ async function buildExternalBinaryEntry(filePath, normalizedPath, mediaType, byt
|
|
|
254
256
|
byteSize,
|
|
255
257
|
};
|
|
256
258
|
}
|
|
257
|
-
async function
|
|
259
|
+
async function writeExternalStaging(rootDir, searchDir, outDir, manifest, options) {
|
|
258
260
|
const assetsDir = path.join(outDir, 'assets');
|
|
259
261
|
const r2Dir = path.join(outDir, 'r2');
|
|
260
262
|
await rm(assetsDir, { recursive: true, force: true });
|
|
261
263
|
await rm(r2Dir, { recursive: true, force: true });
|
|
262
|
-
const
|
|
264
|
+
const stagedObjects = new Map();
|
|
265
|
+
let hasAssets = false;
|
|
266
|
+
let hasR2 = false;
|
|
263
267
|
if (options.binaryMode === 'external') {
|
|
264
268
|
for (const entry of manifest.entries) {
|
|
265
269
|
if (entry.kind !== 'binary' || !('storageKind' in entry)) {
|
|
@@ -270,16 +274,66 @@ async function writeExternalBinaryStaging(rootDir, outDir, manifest, options) {
|
|
|
270
274
|
const targetFile = path.join(assetsDir, entry.storageKey);
|
|
271
275
|
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
272
276
|
await copyFile(sourceFile, targetFile);
|
|
277
|
+
hasAssets = true;
|
|
278
|
+
stagedObjects.set(`assets:${entry.storageKey}`, {
|
|
279
|
+
kind: 'binary',
|
|
280
|
+
path: entry.path,
|
|
281
|
+
mediaType: entry.mediaType,
|
|
282
|
+
storageKind: 'assets',
|
|
283
|
+
storageKey: entry.storageKey,
|
|
284
|
+
file: toPosixPath(path.join('assets', entry.storageKey)),
|
|
285
|
+
byteSize: entry.byteSize,
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const relativeFile = toPosixPath(path.join('r2', entry.storageKey));
|
|
290
|
+
const targetFile = path.join(outDir, relativeFile);
|
|
291
|
+
if (!stagedObjects.has(`r2:${entry.storageKey}`)) {
|
|
292
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
293
|
+
await copyFile(sourceFile, targetFile);
|
|
294
|
+
hasR2 = true;
|
|
295
|
+
stagedObjects.set(`r2:${entry.storageKey}`, {
|
|
296
|
+
kind: 'binary',
|
|
297
|
+
path: entry.path,
|
|
298
|
+
mediaType: entry.mediaType,
|
|
299
|
+
storageKind: 'r2',
|
|
300
|
+
storageKey: entry.storageKey,
|
|
301
|
+
file: relativeFile,
|
|
302
|
+
byteSize: entry.byteSize,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (searchDir && manifest.externalSearchEntries) {
|
|
308
|
+
for (const entry of manifest.externalSearchEntries) {
|
|
309
|
+
const sourceFile = path.join(searchDir, entry.path);
|
|
310
|
+
if (entry.storageKind === 'assets') {
|
|
311
|
+
const targetFile = path.join(assetsDir, entry.storageKey);
|
|
312
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
313
|
+
await copyFile(sourceFile, targetFile);
|
|
314
|
+
hasAssets = true;
|
|
315
|
+
stagedObjects.set(`assets:${entry.storageKey}`, {
|
|
316
|
+
kind: 'search',
|
|
317
|
+
path: entry.path,
|
|
318
|
+
mediaType: entry.mediaType,
|
|
319
|
+
storageKind: 'assets',
|
|
320
|
+
storageKey: entry.storageKey,
|
|
321
|
+
file: toPosixPath(path.join('assets', entry.storageKey)),
|
|
322
|
+
byteSize: entry.byteSize,
|
|
323
|
+
});
|
|
273
324
|
continue;
|
|
274
325
|
}
|
|
275
326
|
const relativeFile = toPosixPath(path.join('r2', entry.storageKey));
|
|
276
327
|
const targetFile = path.join(outDir, relativeFile);
|
|
277
|
-
if (!
|
|
328
|
+
if (!stagedObjects.has(`r2:${entry.storageKey}`)) {
|
|
278
329
|
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
279
330
|
await copyFile(sourceFile, targetFile);
|
|
280
|
-
|
|
331
|
+
hasR2 = true;
|
|
332
|
+
stagedObjects.set(`r2:${entry.storageKey}`, {
|
|
333
|
+
kind: 'search',
|
|
281
334
|
path: entry.path,
|
|
282
335
|
mediaType: entry.mediaType,
|
|
336
|
+
storageKind: 'r2',
|
|
283
337
|
storageKey: entry.storageKey,
|
|
284
338
|
file: relativeFile,
|
|
285
339
|
byteSize: entry.byteSize,
|
|
@@ -288,17 +342,15 @@ async function writeExternalBinaryStaging(rootDir, outDir, manifest, options) {
|
|
|
288
342
|
}
|
|
289
343
|
}
|
|
290
344
|
return {
|
|
291
|
-
version:
|
|
345
|
+
version: 2,
|
|
292
346
|
workerEntry: 'worker.mjs',
|
|
293
347
|
binaryMode: options.binaryMode,
|
|
294
|
-
assetsMaxBytes: options.binaryMode === 'external' ? options.assetsMaxBytes : undefined,
|
|
295
|
-
assetsDir:
|
|
296
|
-
r2Dir:
|
|
297
|
-
r2Binding:
|
|
298
|
-
? options.r2Binding
|
|
299
|
-
: undefined,
|
|
348
|
+
assetsMaxBytes: options.binaryMode === 'external' || searchDir ? options.assetsMaxBytes : undefined,
|
|
349
|
+
assetsDir: hasAssets ? 'assets' : undefined,
|
|
350
|
+
r2Dir: hasR2 ? 'r2' : undefined,
|
|
351
|
+
r2Binding: hasR2 ? options.r2Binding : undefined,
|
|
300
352
|
siteTitle: options.siteTitle,
|
|
301
|
-
|
|
353
|
+
stagedObjects: Array.from(stagedObjects.values()).sort((left, right) => left.storageKey.localeCompare(right.storageKey)),
|
|
302
354
|
};
|
|
303
355
|
}
|
|
304
356
|
async function readCloudflareBundleMetadata(workerEntry) {
|
|
@@ -309,7 +361,32 @@ async function readCloudflareBundleMetadata(workerEntry) {
|
|
|
309
361
|
return readBundleMetadataFile(bundleFile);
|
|
310
362
|
}
|
|
311
363
|
async function readBundleMetadataFile(bundleFile) {
|
|
312
|
-
|
|
364
|
+
const parsed = JSON.parse(await readFile(bundleFile, 'utf8'));
|
|
365
|
+
if ('stagedObjects' in parsed && Array.isArray(parsed.stagedObjects)) {
|
|
366
|
+
return parsed;
|
|
367
|
+
}
|
|
368
|
+
if ('r2Objects' in parsed && Array.isArray(parsed.r2Objects)) {
|
|
369
|
+
return {
|
|
370
|
+
version: 2,
|
|
371
|
+
workerEntry: parsed.workerEntry,
|
|
372
|
+
binaryMode: parsed.binaryMode,
|
|
373
|
+
assetsMaxBytes: parsed.assetsMaxBytes,
|
|
374
|
+
assetsDir: parsed.assetsDir,
|
|
375
|
+
r2Dir: parsed.r2Dir,
|
|
376
|
+
r2Binding: parsed.r2Binding,
|
|
377
|
+
siteTitle: parsed.siteTitle,
|
|
378
|
+
stagedObjects: parsed.r2Objects.map((object) => ({
|
|
379
|
+
kind: 'binary',
|
|
380
|
+
path: object.path,
|
|
381
|
+
mediaType: object.mediaType,
|
|
382
|
+
storageKind: 'r2',
|
|
383
|
+
storageKey: object.storageKey,
|
|
384
|
+
file: object.file,
|
|
385
|
+
byteSize: object.byteSize,
|
|
386
|
+
})),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
throw new Error(`Bundle metadata in ${bundleFile} is not supported. Rebuild the Cloudflare bundle with the current mdorigin CLI.`);
|
|
313
390
|
}
|
|
314
391
|
async function readR2SyncState(stateFile) {
|
|
315
392
|
if (!(await pathExists(stateFile))) {
|
|
@@ -371,31 +448,39 @@ async function listFiles(directory, visitedRealDirectories = new Set()) {
|
|
|
371
448
|
}
|
|
372
449
|
return files;
|
|
373
450
|
}
|
|
374
|
-
async function
|
|
451
|
+
async function buildSearchBundleEntries(directory, assetsMaxBytes) {
|
|
375
452
|
const files = await listFiles(directory);
|
|
376
453
|
const entries = [];
|
|
377
454
|
for (const filePath of files) {
|
|
378
455
|
const relativePath = path.relative(directory, filePath).replaceAll(path.sep, '/');
|
|
379
456
|
const mediaType = getMediaTypeForPath(relativePath);
|
|
380
|
-
|
|
457
|
+
const fileStats = await stat(filePath);
|
|
458
|
+
if (fileStats.size <= assetsMaxBytes) {
|
|
381
459
|
entries.push({
|
|
382
460
|
path: relativePath,
|
|
383
|
-
kind: 'text',
|
|
384
461
|
mediaType,
|
|
385
|
-
|
|
462
|
+
storageKind: 'assets',
|
|
463
|
+
storageKey: `${SEARCH_ASSETS_PREFIX}/${relativePath}`,
|
|
464
|
+
byteSize: fileStats.size,
|
|
386
465
|
});
|
|
387
466
|
continue;
|
|
388
467
|
}
|
|
389
468
|
entries.push({
|
|
390
469
|
path: relativePath,
|
|
391
|
-
kind: 'binary',
|
|
392
470
|
mediaType,
|
|
393
|
-
|
|
471
|
+
storageKind: 'r2',
|
|
472
|
+
storageKey: await buildSearchStorageKey(filePath, relativePath),
|
|
473
|
+
byteSize: fileStats.size,
|
|
394
474
|
});
|
|
395
475
|
}
|
|
396
476
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
397
477
|
return entries;
|
|
398
478
|
}
|
|
479
|
+
async function buildSearchStorageKey(filePath, relativePath) {
|
|
480
|
+
const extension = path.posix.extname(relativePath).toLowerCase();
|
|
481
|
+
const hash = await hashFile(filePath);
|
|
482
|
+
return extension ? `search/${hash}${extension}` : `search/${hash}`;
|
|
483
|
+
}
|
|
399
484
|
async function pathExists(filePath) {
|
|
400
485
|
try {
|
|
401
486
|
await stat(filePath);
|
package/dist/core/api.js
CHANGED
|
@@ -13,10 +13,15 @@ export async function handleApiRoute(pathname, searchParams, options) {
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
const topK = normalizePositiveInteger(searchParams?.get('topK')) ?? 10;
|
|
16
|
-
const
|
|
16
|
+
const metadata = readSearchMetadataFilters(searchParams);
|
|
17
|
+
const hits = await options.searchApi.search(query, {
|
|
18
|
+
topK,
|
|
19
|
+
metadata,
|
|
20
|
+
});
|
|
17
21
|
return json(200, {
|
|
18
22
|
query,
|
|
19
23
|
topK,
|
|
24
|
+
metadata,
|
|
20
25
|
count: hits.length,
|
|
21
26
|
hits: hits.map(serializeSearchHit),
|
|
22
27
|
});
|
|
@@ -56,6 +61,13 @@ function buildOpenApiDocument(options) {
|
|
|
56
61
|
schema: { type: 'integer', minimum: 1, default: 10 },
|
|
57
62
|
description: 'Maximum number of hits to return.',
|
|
58
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'meta.<field>',
|
|
66
|
+
in: 'query',
|
|
67
|
+
required: false,
|
|
68
|
+
schema: { type: 'string' },
|
|
69
|
+
description: 'Exact-match metadata filter. Use query parameters such as meta.type=post or meta.section=guides.',
|
|
70
|
+
},
|
|
59
71
|
],
|
|
60
72
|
responses: {
|
|
61
73
|
'200': {
|
|
@@ -68,6 +80,10 @@ function buildOpenApiDocument(options) {
|
|
|
68
80
|
properties: {
|
|
69
81
|
query: { type: 'string' },
|
|
70
82
|
topK: { type: 'integer' },
|
|
83
|
+
metadata: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
additionalProperties: { type: 'string' },
|
|
86
|
+
},
|
|
71
87
|
count: { type: 'integer' },
|
|
72
88
|
hits: {
|
|
73
89
|
type: 'array',
|
|
@@ -130,6 +146,23 @@ function buildOpenApiDocument(options) {
|
|
|
130
146
|
},
|
|
131
147
|
};
|
|
132
148
|
}
|
|
149
|
+
function readSearchMetadataFilters(searchParams) {
|
|
150
|
+
if (!searchParams) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const metadata = {};
|
|
154
|
+
for (const [key, value] of searchParams.entries()) {
|
|
155
|
+
if (!key.startsWith('meta.') || value.trim() === '') {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const metadataKey = key.slice('meta.'.length).trim();
|
|
159
|
+
if (metadataKey === '') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
metadata[metadataKey] = value;
|
|
163
|
+
}
|
|
164
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
165
|
+
}
|
|
133
166
|
function serializeSearchHit(hit) {
|
|
134
167
|
return {
|
|
135
168
|
docId: hit.docId,
|
package/dist/core/markdown.d.ts
CHANGED
|
@@ -28,5 +28,6 @@ export declare function parseMarkdownDocument(sourcePath: string, markdown: stri
|
|
|
28
28
|
export declare function renderMarkdown(markdown: string): Promise<string>;
|
|
29
29
|
export declare function rewriteMarkdownLinksInHtml(html: string): string;
|
|
30
30
|
export declare function stripManagedIndexBlock(markdown: string): string;
|
|
31
|
+
export declare function stripMachineOnlyMarkdownComments(markdown: string): string;
|
|
31
32
|
export declare function stripManagedIndexLinks(markdown: string, hrefs: ReadonlySet<string>): string;
|
|
32
33
|
export declare function extractManagedIndexEntries(markdown: string): ManagedIndexEntry[];
|
package/dist/core/markdown.js
CHANGED
|
@@ -38,6 +38,9 @@ export function rewriteMarkdownLinksInHtml(html) {
|
|
|
38
38
|
export function stripManagedIndexBlock(markdown) {
|
|
39
39
|
return markdown.replace(/\n?<!-- INDEX:START -->[\s\S]*?<!-- INDEX:END -->\n?/g, '\n').trimEnd();
|
|
40
40
|
}
|
|
41
|
+
export function stripMachineOnlyMarkdownComments(markdown) {
|
|
42
|
+
return markdown.replace(/<!--\s*mdorigin:[\s\S]*?-->/g, '');
|
|
43
|
+
}
|
|
41
44
|
export function stripManagedIndexLinks(markdown, hrefs) {
|
|
42
45
|
if (hrefs.size === 0) {
|
|
43
46
|
return markdown;
|
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,60 +1,100 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
6
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;
|
|
7
12
|
export async function buildSearchBundle(options) {
|
|
8
13
|
const buildModule = await loadIndexbindBuildModule();
|
|
9
14
|
const rootDir = path.resolve(options.rootDir);
|
|
15
|
+
const outputDir = path.resolve(options.outDir);
|
|
10
16
|
const documents = await collectSearchDocuments(rootDir, options.siteConfig, {
|
|
11
17
|
draftMode: options.draftMode ?? 'exclude',
|
|
12
18
|
});
|
|
13
|
-
const
|
|
19
|
+
const buildOptions = {
|
|
14
20
|
embeddingBackend: options.embeddingBackend ?? 'model2vec',
|
|
15
21
|
model: options.model,
|
|
16
22
|
sourceRootId: path.basename(rootDir),
|
|
17
23
|
sourceRootPath: rootDir,
|
|
18
|
-
}
|
|
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);
|
|
19
42
|
return {
|
|
20
|
-
outputDir
|
|
43
|
+
outputDir,
|
|
21
44
|
documentCount: stats.documentCount,
|
|
22
45
|
chunkCount: stats.chunkCount,
|
|
23
46
|
vectorDimensions: stats.vectorDimensions,
|
|
24
47
|
};
|
|
25
48
|
}
|
|
26
49
|
export async function searchBundle(options) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
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({
|
|
30
52
|
topK: options.topK ?? 10,
|
|
31
53
|
relativePathPrefix: options.relativePathPrefix,
|
|
32
|
-
|
|
54
|
+
metadata: options.metadata,
|
|
55
|
+
})), searchContext.documentsById));
|
|
33
56
|
}
|
|
34
57
|
export async function createSearchApiFromDirectory(indexDir) {
|
|
35
|
-
const
|
|
36
|
-
const index = await webModule.openWebIndex(path.resolve(indexDir));
|
|
58
|
+
const searchContext = await openSearchContextFromDirectory(path.resolve(indexDir));
|
|
37
59
|
return {
|
|
38
60
|
async search(query, options) {
|
|
39
|
-
return rerankSearchHits(await index.search(query, {
|
|
61
|
+
return rerankSearchHits(hydrateSearchHits(await searchContext.index.search(query, buildIndexbindSearchOptions({
|
|
40
62
|
topK: options?.topK,
|
|
41
63
|
relativePathPrefix: options?.relativePathPrefix,
|
|
42
|
-
|
|
64
|
+
metadata: options?.metadata,
|
|
65
|
+
})), searchContext.documentsById));
|
|
43
66
|
},
|
|
44
67
|
};
|
|
45
68
|
}
|
|
46
69
|
export function createSearchApiFromBundle(bundleEntries) {
|
|
47
|
-
let
|
|
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;
|
|
48
87
|
return {
|
|
49
88
|
async search(query, options) {
|
|
50
|
-
if (
|
|
51
|
-
|
|
89
|
+
if (searchContextPromise === null) {
|
|
90
|
+
searchContextPromise = openSearchContextFromBundle(bundleEntries, loadResponse);
|
|
52
91
|
}
|
|
53
|
-
const
|
|
54
|
-
return rerankSearchHits(await index.search(query, {
|
|
92
|
+
const searchContext = await searchContextPromise;
|
|
93
|
+
return rerankSearchHits(hydrateSearchHits(await searchContext.index.search(query, buildIndexbindSearchOptions({
|
|
55
94
|
topK: options?.topK,
|
|
56
95
|
relativePathPrefix: options?.relativePathPrefix,
|
|
57
|
-
|
|
96
|
+
metadata: options?.metadata,
|
|
97
|
+
})), searchContext.documentsById));
|
|
58
98
|
},
|
|
59
99
|
};
|
|
60
100
|
}
|
|
@@ -78,12 +118,15 @@ async function collectSearchDocuments(rootDir, siteConfig, options) {
|
|
|
78
118
|
canonicalUrl: absoluteCanonicalUrl,
|
|
79
119
|
title: getDocumentTitle(parsed.meta, parsed.body, fallbackTitleFromRelativePath(document.relativePath)),
|
|
80
120
|
summary: getDocumentSummary(parsed.meta, parsed.body),
|
|
81
|
-
content:
|
|
121
|
+
content: buildSearchableMarkdownBody(parsed.body),
|
|
82
122
|
metadata: buildSearchMetadata(document.relativePath, canonicalPath, parsed.meta, siteConfig),
|
|
83
123
|
});
|
|
84
124
|
}
|
|
85
125
|
return documents;
|
|
86
126
|
}
|
|
127
|
+
function buildSearchableMarkdownBody(markdownBody) {
|
|
128
|
+
return stripMachineOnlyMarkdownComments(stripManagedIndexBlock(markdownBody)).trim();
|
|
129
|
+
}
|
|
87
130
|
async function listSearchDocuments(rootDir) {
|
|
88
131
|
const results = [];
|
|
89
132
|
await walkDirectory(rootDir, '', new Set(), results);
|
|
@@ -197,6 +240,8 @@ function buildSearchMetadata(relativePath, canonicalPath, meta, siteConfig) {
|
|
|
197
240
|
markdownPath: `/${relativePath}`,
|
|
198
241
|
canonicalPath,
|
|
199
242
|
siteTitle: siteConfig.siteTitle,
|
|
243
|
+
section: getSearchSection(relativePath),
|
|
244
|
+
isOverview: isOverviewContentPath(relativePath),
|
|
200
245
|
};
|
|
201
246
|
if (meta.type === 'page' || meta.type === 'post') {
|
|
202
247
|
metadata.type = meta.type;
|
|
@@ -212,6 +257,17 @@ function buildSearchMetadata(relativePath, canonicalPath, meta, siteConfig) {
|
|
|
212
257
|
}
|
|
213
258
|
return metadata;
|
|
214
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
|
+
}
|
|
215
271
|
function fallbackTitleFromRelativePath(relativePath) {
|
|
216
272
|
const baseName = path.posix.basename(relativePath);
|
|
217
273
|
if (DIRECTORY_INDEX_FILENAMES_LOWER.has(baseName.toLowerCase())) {
|
|
@@ -250,6 +306,33 @@ async function pathExists(filePath) {
|
|
|
250
306
|
throw error;
|
|
251
307
|
}
|
|
252
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
|
+
}
|
|
253
336
|
function isIgnoredSkillSupportDirectory(name) {
|
|
254
337
|
return (name === 'scripts' ||
|
|
255
338
|
name === 'references' ||
|
|
@@ -280,8 +363,10 @@ async function loadIndexbindCloudflareModule() {
|
|
|
280
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 });
|
|
281
364
|
}
|
|
282
365
|
}
|
|
283
|
-
async function
|
|
284
|
-
|
|
366
|
+
async function openWebIndexFromVirtualBundle(bundleEntries, loadResponse) {
|
|
367
|
+
if (isNodeRuntime()) {
|
|
368
|
+
return openWebIndexFromMaterializedBundle(bundleEntries, loadResponse);
|
|
369
|
+
}
|
|
285
370
|
const baseUrl = 'https://mdorigin-search.invalid/';
|
|
286
371
|
const originalFetch = globalThis.fetch;
|
|
287
372
|
const bundleMap = new Map(bundleEntries.map((entry) => [new URL(entry.path, baseUrl).toString(), entry]));
|
|
@@ -293,31 +378,209 @@ async function openWebIndexFromBundle(bundleEntries) {
|
|
|
293
378
|
: input.url;
|
|
294
379
|
const entry = bundleMap.get(requestUrl);
|
|
295
380
|
if (entry) {
|
|
296
|
-
|
|
297
|
-
const binaryBody = decodeBase64(entry.base64 ?? '');
|
|
298
|
-
const body = entry.kind === 'text'
|
|
299
|
-
? entry.text ?? ''
|
|
300
|
-
: new Blob([new Uint8Array(binaryBody)], {
|
|
301
|
-
type: entry.mediaType,
|
|
302
|
-
});
|
|
303
|
-
return new Response(body, {
|
|
304
|
-
status: 200,
|
|
305
|
-
headers,
|
|
306
|
-
});
|
|
381
|
+
return loadResponse(entry);
|
|
307
382
|
}
|
|
308
383
|
return originalFetch(input, init);
|
|
309
384
|
};
|
|
310
385
|
try {
|
|
311
|
-
|
|
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
|
+
}
|
|
312
397
|
}
|
|
313
398
|
finally {
|
|
314
399
|
globalThis.fetch = originalFetch;
|
|
315
400
|
}
|
|
316
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
|
+
}
|
|
317
472
|
function decodeBase64(value) {
|
|
318
473
|
const decoded = atob(value);
|
|
319
474
|
return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
|
|
320
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
|
+
}
|
|
321
584
|
function rerankSearchHits(hits) {
|
|
322
585
|
const remaining = [...hits];
|
|
323
586
|
const ordered = [];
|
|
@@ -360,8 +623,7 @@ function compareHits(left, right) {
|
|
|
360
623
|
return left.relativePath.localeCompare(right.relativePath);
|
|
361
624
|
}
|
|
362
625
|
function isOverviewSearchHit(hit) {
|
|
363
|
-
|
|
364
|
-
return baseName === 'readme.md' || baseName === 'index.md';
|
|
626
|
+
return OVERVIEW_CONTENT_FILENAMES.has(path.posix.basename(hit.relativePath).toLowerCase());
|
|
365
627
|
}
|
|
366
628
|
function getTopLevelSection(relativePath) {
|
|
367
629
|
const normalized = relativePath.replaceAll('\\', '/');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdorigin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Markdown-first publishing for humans and agents.",
|
|
6
6
|
"repository": {
|
|
@@ -78,6 +78,6 @@
|
|
|
78
78
|
"typescript": "^5.9.2"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
|
-
"indexbind": "^0.
|
|
81
|
+
"indexbind": "^0.3.0"
|
|
82
82
|
}
|
|
83
83
|
}
|