@dominikcz/greg 0.9.27
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 +397 -0
- package/bin/greg.js +241 -0
- package/bin/init.js +351 -0
- package/bin/templates/docs/getting-started.md +47 -0
- package/bin/templates/docs/index.md +11 -0
- package/bin/templates/greg.config.js +39 -0
- package/bin/templates/greg.config.ts +38 -0
- package/bin/templates/index.html +16 -0
- package/bin/templates/src/App.svelte +5 -0
- package/bin/templates/src/app.css +20 -0
- package/bin/templates/src/main.js +9 -0
- package/bin/templates/svelte.config.js +1 -0
- package/bin/templates/tsconfig.json +21 -0
- package/bin/templates/vite.config.js +23 -0
- package/docs/__partials/markdown/examples/basic.md +4 -0
- package/docs/__partials/markdown/examples/diff.md +10 -0
- package/docs/__partials/markdown/examples/focus.md +5 -0
- package/docs/__partials/markdown/examples/language-title.md +3 -0
- package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
- package/docs/__partials/markdown/examples/line-numbers.md +5 -0
- package/docs/__partials/note.md +4 -0
- package/docs/guide/__shared-warning.md +4 -0
- package/docs/guide/asset-handling.md +88 -0
- package/docs/guide/deploying.md +162 -0
- package/docs/guide/getting-started.md +334 -0
- package/docs/guide/index.md +23 -0
- package/docs/guide/localization.md +290 -0
- package/docs/guide/markdown/code.md +95 -0
- package/docs/guide/markdown/components-and-mermaid.md +43 -0
- package/docs/guide/markdown/containers.md +110 -0
- package/docs/guide/markdown/header-anchors.md +34 -0
- package/docs/guide/markdown/includes.md +84 -0
- package/docs/guide/markdown/index.md +20 -0
- package/docs/guide/markdown/inline-attributes.md +21 -0
- package/docs/guide/markdown/links-and-toc.md +64 -0
- package/docs/guide/markdown/math.md +54 -0
- package/docs/guide/markdown/syntax-highlighting.md +75 -0
- package/docs/guide/routing.md +150 -0
- package/docs/guide/using-svelte.md +88 -0
- package/docs/guide/versioning.md +281 -0
- package/docs/incompatibilities.md +48 -0
- package/docs/index.md +43 -0
- package/docs/reference/badge.md +100 -0
- package/docs/reference/carbon-ads.md +46 -0
- package/docs/reference/code-group.md +126 -0
- package/docs/reference/home-page.md +232 -0
- package/docs/reference/index.md +18 -0
- package/docs/reference/markdowndocs.md +275 -0
- package/docs/reference/outline.md +79 -0
- package/docs/reference/search.md +263 -0
- package/docs/reference/steps.md +200 -0
- package/docs/reference/team-page.md +189 -0
- package/docs/reference/theme.md +150 -0
- package/fakeDocsGenerator/generate_docs.js +310 -0
- package/package.json +92 -0
- package/scripts/build-versions.js +609 -0
- package/scripts/generate-static.js +79 -0
- package/scripts/render-markdown.js +420 -0
- package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
- package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
- package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
- package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
- package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
- package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
- package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
- package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
- package/src/lib/MarkdownDocs/Outline.svelte +238 -0
- package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
- package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
- package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
- package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
- package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
- package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
- package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
- package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
- package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
- package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
- package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
- package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
- package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
- package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
- package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
- package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
- package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
- package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
- package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
- package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
- package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
- package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
- package/src/lib/MarkdownDocs/ai/characters.js +52 -0
- package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
- package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
- package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
- package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
- package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
- package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
- package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
- package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
- package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
- package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
- package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
- package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
- package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
- package/src/lib/MarkdownDocs/ai/types.ts +71 -0
- package/src/lib/MarkdownDocs/aiServer.js +288 -0
- package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
- package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
- package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
- package/src/lib/MarkdownDocs/common.ts +47 -0
- package/src/lib/MarkdownDocs/docsUtils.js +281 -0
- package/src/lib/MarkdownDocs/index.plugins.js +22 -0
- package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
- package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
- package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
- package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
- package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
- package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
- package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
- package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
- package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
- package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
- package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
- package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
- package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
- package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
- package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
- package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
- package/src/lib/MarkdownDocs/remarkImports.js +461 -0
- package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
- package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
- package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
- package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
- package/src/lib/MarkdownDocs/searchServer.js +263 -0
- package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
- package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
- package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
- package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
- package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
- package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
- package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
- package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
- package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
- package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
- package/src/lib/components/Badge.svelte +59 -0
- package/src/lib/components/Button.svelte +138 -0
- package/src/lib/components/CarbonAds.svelte +99 -0
- package/src/lib/components/CodeGroup.svelte +102 -0
- package/src/lib/components/Feature.svelte +209 -0
- package/src/lib/components/Features.svelte +123 -0
- package/src/lib/components/Hero.svelte +399 -0
- package/src/lib/components/Image.svelte +128 -0
- package/src/lib/components/Link.svelte +105 -0
- package/src/lib/components/SocialLink.svelte +84 -0
- package/src/lib/components/SocialLinks.svelte +33 -0
- package/src/lib/components/Steps.svelte +143 -0
- package/src/lib/components/TeamMember.svelte +273 -0
- package/src/lib/components/TeamMembers.svelte +81 -0
- package/src/lib/components/TeamPage.svelte +65 -0
- package/src/lib/components/TeamPageSection.svelte +108 -0
- package/src/lib/components/TeamPageTitle.svelte +89 -0
- package/src/lib/components/index.js +24 -0
- package/src/lib/portal/context.js +12 -0
- package/src/lib/portal/index.js +3 -0
- package/src/lib/portal/portal.svelte +14 -0
- package/src/lib/portal/slot.svelte +8 -0
- package/src/lib/scss/__code.scss +128 -0
- package/src/lib/scss/__containers.scss +99 -0
- package/src/lib/scss/__markdown.scss +447 -0
- package/src/lib/scss/__scrollbar.scss +60 -0
- package/src/lib/scss/__steps.scss +100 -0
- package/src/lib/scss/__theme.scss +238 -0
- package/src/lib/scss/__toc.scss +55 -0
- package/src/lib/scss/__utilities.scss +7 -0
- package/src/lib/scss/greg.scss +9 -0
- package/src/lib/spinner/spinner.svelte +42 -0
- package/svelte.config.js +146 -0
- package/types/index.d.ts +456 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Greg standalone search server — for production deployments.
|
|
4
|
+
*
|
|
5
|
+
* Reads a pre-built search-index.json, loads it into Fuse.js, then serves
|
|
6
|
+
* GET /api/search?q=<query>&limit=<n> as a lightweight HTTP endpoint.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node src/lib/MarkdownDocs/searchServer.js [options]
|
|
10
|
+
* greg search-server (via CLI)
|
|
11
|
+
*
|
|
12
|
+
* Options (CLI flags or environment variables):
|
|
13
|
+
* --index PATH Path to search-index.json (GREG_SEARCH_INDEX, default: dist/search-index.json)
|
|
14
|
+
* --port NUMBER HTTP port (GREG_SEARCH_PORT, default: 3100)
|
|
15
|
+
* --host HOSTNAME Bind address (GREG_SEARCH_HOST, default: localhost)
|
|
16
|
+
* --url PATH Search endpoint URL path (GREG_SEARCH_URL, default: /api/search)
|
|
17
|
+
* --cors-origin VALUE Access-Control-Allow-Origin (GREG_SEARCH_CORS_ORIGIN, default: *)
|
|
18
|
+
* Use 'request' to mirror request Origin header.
|
|
19
|
+
* --cors-methods VALUE Access-Control-Allow-Methods (GREG_SEARCH_CORS_METHODS, default: GET, OPTIONS)
|
|
20
|
+
* --cors-headers VALUE Access-Control-Allow-Headers (GREG_SEARCH_CORS_HEADERS, default: Content-Type)
|
|
21
|
+
* --cors-max-age VALUE Access-Control-Max-Age (GREG_SEARCH_CORS_MAX_AGE, default: 86400)
|
|
22
|
+
*
|
|
23
|
+
* Example — run after `greg build`:
|
|
24
|
+
* greg search-server --index dist/search-index.json --port 3100
|
|
25
|
+
*
|
|
26
|
+
* Then point greg.config.js → search.serverUrl to http://localhost:3100/api/search
|
|
27
|
+
* (or configure a reverse proxy so /api/search hits this server).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { createServer } from 'node:http';
|
|
31
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
32
|
+
import { resolve } from 'node:path';
|
|
33
|
+
import { statSync } from 'node:fs';
|
|
34
|
+
import { buildFuseResult } from './searchIndexBuilder.js';
|
|
35
|
+
import { loadGregConfig } from './loadGregConfig.js';
|
|
36
|
+
import Fuse from 'fuse.js';
|
|
37
|
+
|
|
38
|
+
const startupT0 = process.hrtime.bigint();
|
|
39
|
+
|
|
40
|
+
function msSince(t0) {
|
|
41
|
+
return Number(process.hrtime.bigint() - t0) / 1e6;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fmtMs(ms) {
|
|
45
|
+
return `${ms.toFixed(1)}ms`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── CLI argument parser ───────────────────────────────────────────────────────
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const args = {};
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const a = argv[i];
|
|
53
|
+
if (a.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
54
|
+
args[a.slice(2)] = argv[i + 1];
|
|
55
|
+
i++;
|
|
56
|
+
} else if (a.startsWith('--')) {
|
|
57
|
+
args[a.slice(2)] = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const args = parseArgs(process.argv.slice(2));
|
|
64
|
+
const gregConfig = await loadGregConfig();
|
|
65
|
+
const configuredFuzzy = gregConfig?.search?.fuzzy ?? {};
|
|
66
|
+
const port = parseInt(args.port ?? process.env.GREG_SEARCH_PORT ?? '3100', 10);
|
|
67
|
+
const host = String(args.host ?? process.env.GREG_SEARCH_HOST ?? 'localhost');
|
|
68
|
+
const url = String(args.url ?? process.env.GREG_SEARCH_URL ?? '/api/search');
|
|
69
|
+
const index = resolve(String(args.index ?? process.env.GREG_SEARCH_INDEX ?? 'dist/search-index.json'));
|
|
70
|
+
const corsOrigin = String(args['cors-origin'] ?? process.env.GREG_SEARCH_CORS_ORIGIN ?? '*');
|
|
71
|
+
const corsMethods = String(args['cors-methods'] ?? process.env.GREG_SEARCH_CORS_METHODS ?? 'GET, OPTIONS');
|
|
72
|
+
const corsHeaders = String(args['cors-headers'] ?? process.env.GREG_SEARCH_CORS_HEADERS ?? 'Content-Type');
|
|
73
|
+
const corsMaxAge = String(args['cors-max-age'] ?? process.env.GREG_SEARCH_CORS_MAX_AGE ?? '86400');
|
|
74
|
+
const thresholdRaw =
|
|
75
|
+
args.threshold ?? process.env.GREG_SEARCH_THRESHOLD ?? configuredFuzzy.threshold;
|
|
76
|
+
const threshold = Number.isFinite(Number(thresholdRaw))
|
|
77
|
+
? Number(thresholdRaw)
|
|
78
|
+
: 0.35;
|
|
79
|
+
const minMatchRaw =
|
|
80
|
+
args['min-match-chars'] ??
|
|
81
|
+
process.env.GREG_SEARCH_MIN_MATCH_CHARS ??
|
|
82
|
+
configuredFuzzy.minMatchCharLength;
|
|
83
|
+
const minMatchCharLength = Number.isFinite(Number(minMatchRaw))
|
|
84
|
+
? Math.max(1, Number(minMatchRaw))
|
|
85
|
+
: 3;
|
|
86
|
+
const ignoreLocationRaw =
|
|
87
|
+
args['ignore-location'] ??
|
|
88
|
+
process.env.GREG_SEARCH_IGNORE_LOCATION ??
|
|
89
|
+
configuredFuzzy.ignoreLocation;
|
|
90
|
+
const ignoreLocation =
|
|
91
|
+
String(ignoreLocationRaw ?? 'true').toLowerCase() !== 'false';
|
|
92
|
+
|
|
93
|
+
function getCorsHeaders(req) {
|
|
94
|
+
const reflectedOrigin = req.headers.origin ? String(req.headers.origin) : '*';
|
|
95
|
+
const allowOrigin = corsOrigin === 'request' ? reflectedOrigin : corsOrigin;
|
|
96
|
+
return {
|
|
97
|
+
'Access-Control-Allow-Origin': allowOrigin,
|
|
98
|
+
'Access-Control-Allow-Methods': corsMethods,
|
|
99
|
+
'Access-Control-Allow-Headers': corsHeaders,
|
|
100
|
+
'Access-Control-Max-Age': corsMaxAge,
|
|
101
|
+
...(corsOrigin === 'request' ? { 'Vary': 'Origin' } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizePath(value) {
|
|
106
|
+
const raw = String(value ?? '').trim();
|
|
107
|
+
if (!raw || raw === '/') return '/';
|
|
108
|
+
return '/' + raw.replace(/^\/+|\/+$/g, '');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isPathInLocale(id, localeRoot, baseRoot, localeRoots) {
|
|
112
|
+
const currentRoot = normalizePath(localeRoot);
|
|
113
|
+
const normalizedBase = normalizePath(baseRoot);
|
|
114
|
+
const normalizedId = normalizePath(id);
|
|
115
|
+
const roots = (localeRoots ?? []).map(normalizePath);
|
|
116
|
+
|
|
117
|
+
const inCurrentRoot = currentRoot === '/'
|
|
118
|
+
? normalizedId.startsWith('/')
|
|
119
|
+
: (normalizedId === currentRoot || normalizedId.startsWith(currentRoot + '/'));
|
|
120
|
+
|
|
121
|
+
if (!inCurrentRoot) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (currentRoot === normalizedBase) {
|
|
126
|
+
const otherRoots = roots.filter((rp) => rp !== currentRoot);
|
|
127
|
+
if (otherRoots.some((rp) => normalizedId === rp || normalizedId.startsWith(rp + '/'))) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Load index ────────────────────────────────────────────────────────────────
|
|
136
|
+
console.log(`[greg-search] Loading index: ${index}`);
|
|
137
|
+
|
|
138
|
+
let data = [];
|
|
139
|
+
let fuse = null;
|
|
140
|
+
let sectionCount = 0;
|
|
141
|
+
let loadParseMs = 0;
|
|
142
|
+
let fuseBuildMs = 0;
|
|
143
|
+
let indexMtimeMs = -1;
|
|
144
|
+
|
|
145
|
+
function buildFuse(searchData) {
|
|
146
|
+
return new Fuse(searchData, {
|
|
147
|
+
includeScore: true,
|
|
148
|
+
includeMatches: true,
|
|
149
|
+
threshold,
|
|
150
|
+
ignoreLocation,
|
|
151
|
+
minMatchCharLength,
|
|
152
|
+
keys: [
|
|
153
|
+
{ name: 'title', weight: 3 },
|
|
154
|
+
{ name: 'sections.heading', weight: 2 },
|
|
155
|
+
{ name: 'sections.content', weight: 1 },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function loadIndex(force = false) {
|
|
161
|
+
let stats;
|
|
162
|
+
try {
|
|
163
|
+
stats = statSync(index);
|
|
164
|
+
} catch (/** @type {any} */ e) {
|
|
165
|
+
console.error(`[greg-search] Failed to stat index: ${e.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!force && stats.mtimeMs === indexMtimeMs && fuse) return false;
|
|
170
|
+
|
|
171
|
+
const loadParseT0 = process.hrtime.bigint();
|
|
172
|
+
let nextData;
|
|
173
|
+
try {
|
|
174
|
+
nextData = JSON.parse(readFileSync(index, 'utf-8'));
|
|
175
|
+
} catch (/** @type {any} */ e) {
|
|
176
|
+
console.error(`[greg-search] Failed to load index: ${e.message}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
loadParseMs = msSince(loadParseT0);
|
|
180
|
+
|
|
181
|
+
const fuseBuildT0 = process.hrtime.bigint();
|
|
182
|
+
fuse = buildFuse(nextData);
|
|
183
|
+
fuseBuildMs = msSince(fuseBuildT0);
|
|
184
|
+
|
|
185
|
+
data = nextData;
|
|
186
|
+
indexMtimeMs = stats.mtimeMs;
|
|
187
|
+
sectionCount = data.reduce((sum, doc) => sum + (doc.sections?.length ?? 0), 0);
|
|
188
|
+
|
|
189
|
+
console.log(`[greg-search] Indexed ${data.length} document(s).`);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
loadIndex(true);
|
|
194
|
+
|
|
195
|
+
// ── HTTP server ───────────────────────────────────────────────────────────────
|
|
196
|
+
const server = createServer((req, res) => {
|
|
197
|
+
const rawUrl = req.url ?? '';
|
|
198
|
+
const qIdx = rawUrl.indexOf('?');
|
|
199
|
+
const pathname = qIdx >= 0 ? rawUrl.slice(0, qIdx) : rawUrl;
|
|
200
|
+
|
|
201
|
+
// CORS preflight
|
|
202
|
+
if (req.method === 'OPTIONS') {
|
|
203
|
+
res.writeHead(204, {
|
|
204
|
+
...getCorsHeaders(req),
|
|
205
|
+
});
|
|
206
|
+
res.end();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (pathname !== url || req.method !== 'GET') {
|
|
211
|
+
res.writeHead(404, {
|
|
212
|
+
...getCorsHeaders(req),
|
|
213
|
+
});
|
|
214
|
+
res.end();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const params = new URLSearchParams(qIdx >= 0 ? rawUrl.slice(qIdx + 1) : '');
|
|
219
|
+
const q = (params.get('q') ?? '').trim();
|
|
220
|
+
const limit = Math.min(Math.max(parseInt(params.get('limit') ?? '10', 10) || 10, 1), 50);
|
|
221
|
+
const localeRoot = params.get('localeRoot');
|
|
222
|
+
const baseRoot = params.get('baseRoot');
|
|
223
|
+
const localeRoots = (params.get('localeRoots') ?? '')
|
|
224
|
+
.split(',')
|
|
225
|
+
.map(v => v.trim())
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
|
|
228
|
+
loadIndex(false);
|
|
229
|
+
|
|
230
|
+
let results = q ? fuse.search(q, { limit }).map(buildFuseResult) : [];
|
|
231
|
+
if (localeRoot && baseRoot) {
|
|
232
|
+
results = results.filter((result) =>
|
|
233
|
+
isPathInLocale(result.id, localeRoot, baseRoot, localeRoots),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const body = JSON.stringify({ query: q, results });
|
|
237
|
+
|
|
238
|
+
res.writeHead(200, {
|
|
239
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
240
|
+
'Cache-Control': 'no-cache',
|
|
241
|
+
'Content-Length': Buffer.byteLength(body),
|
|
242
|
+
...getCorsHeaders(req),
|
|
243
|
+
});
|
|
244
|
+
res.end(body);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
server.listen(port, host, () => {
|
|
248
|
+
console.log(`[greg-search] Listening on http://${host}:${port}${url}`);
|
|
249
|
+
const startupMs = msSince(startupT0);
|
|
250
|
+
console.log(
|
|
251
|
+
`[greg-search] Startup summary: load+parse=${fmtMs(loadParseMs)}, ` +
|
|
252
|
+
`fuse-index=${fmtMs(fuseBuildMs)}, total=${fmtMs(startupMs)}, ` +
|
|
253
|
+
`docs=${data.length}, sections=${sectionCount}`,
|
|
254
|
+
);
|
|
255
|
+
console.log(
|
|
256
|
+
`[greg-search] CORS: origin=${corsOrigin}, methods="${corsMethods}", ` +
|
|
257
|
+
`headers="${corsHeaders}", max-age=${corsMaxAge}`,
|
|
258
|
+
);
|
|
259
|
+
console.log(
|
|
260
|
+
`[greg-search] Fuzzy: threshold=${threshold}, min-match-chars=${minMatchCharLength}, ` +
|
|
261
|
+
`ignore-location=${ignoreLocation}`,
|
|
262
|
+
);
|
|
263
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface TreeViewItem {
|
|
2
|
+
label: string;
|
|
3
|
+
link: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
rel?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
/** Optional badge displayed next to the label in the navigation tree. */
|
|
8
|
+
badge?: { text: string; type?: string };
|
|
9
|
+
type?: string;
|
|
10
|
+
children: TreeViewItem[];
|
|
11
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { tick } from 'svelte';
|
|
2
|
+
import { flattenMenu, getBreadcrumb } from './docsUtils';
|
|
3
|
+
|
|
4
|
+
type FlatItem = { label: string; link: string; type: string };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SPA router rune for MarkdownDocs.
|
|
8
|
+
* Manages the active URL, module resolution and popstate handling.
|
|
9
|
+
*
|
|
10
|
+
* `knownPaths` is a set of known markdown file paths (e.g. from the frontmatter
|
|
11
|
+
* virtual module). Values are ignored – only keys are used for existence checks.
|
|
12
|
+
*/
|
|
13
|
+
function normalizePath(raw: string): string {
|
|
14
|
+
return decodeURI(raw).replace(/\/$/, '') || '/';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeHash(raw?: string): string {
|
|
18
|
+
if (!raw) return '';
|
|
19
|
+
const value = String(raw).trim().replace(/^#/, '');
|
|
20
|
+
return value ? `#${value}` : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildNavigationUrl(path: string, anchor?: string): string {
|
|
24
|
+
const normalized = normalizePath(path);
|
|
25
|
+
const isBaseLikeRoot = /^\/[^/]+$/.test(normalized);
|
|
26
|
+
const preserveTrailingSlash =
|
|
27
|
+
isBaseLikeRoot
|
|
28
|
+
&& !anchor
|
|
29
|
+
&& String(path).endsWith('/');
|
|
30
|
+
const pathname = preserveTrailingSlash ? `${normalized}/` : normalized;
|
|
31
|
+
return `${pathname}${normalizeHash(anchor)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isSameNavigationTarget(
|
|
35
|
+
currentPath: string,
|
|
36
|
+
currentHash: string | undefined,
|
|
37
|
+
nextPath: string,
|
|
38
|
+
nextAnchor?: string,
|
|
39
|
+
): boolean {
|
|
40
|
+
return normalizePath(currentPath) === normalizePath(nextPath)
|
|
41
|
+
&& normalizeHash(currentHash) === normalizeHash(nextAnchor);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useRouter(
|
|
45
|
+
knownPaths: Record<string, unknown>,
|
|
46
|
+
getSrcDirForPath: (path: string) => string,
|
|
47
|
+
normalizePathForLookup: (path: string) => string = (path) => path,
|
|
48
|
+
) {
|
|
49
|
+
let active = $state(normalizePath(window.location.pathname));
|
|
50
|
+
|
|
51
|
+
const lookupActive = $derived.by(() =>
|
|
52
|
+
normalizePath(normalizePathForLookup(active)),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function navigate(path: string, anchor?: string) {
|
|
56
|
+
if (isSameNavigationTarget(window.location.pathname, window.location.hash || '', path, anchor)) return;
|
|
57
|
+
history.pushState(null, '', buildNavigationUrl(path, anchor));
|
|
58
|
+
active = normalizePath(path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handlePopState() {
|
|
62
|
+
active = normalizePath(window.location.pathname);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$effect(() => {
|
|
66
|
+
window.addEventListener('popstate', handlePopState);
|
|
67
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Resolved .md file path for the active route, or null if unknown. */
|
|
71
|
+
const activeMarkdownPath = $derived.by(() => {
|
|
72
|
+
const srcDir = getSrcDirForPath(lookupActive);
|
|
73
|
+
const rel = lookupActive.replace(srcDir, '').replace(/^\//, '');
|
|
74
|
+
const candidates = rel
|
|
75
|
+
? (
|
|
76
|
+
srcDir === '/'
|
|
77
|
+
? [`/${rel}.md`, `/${rel}/index.md`]
|
|
78
|
+
: [`${srcDir}/${rel}.md`, `${srcDir}/${rel}/index.md`]
|
|
79
|
+
)
|
|
80
|
+
: (
|
|
81
|
+
srcDir === '/'
|
|
82
|
+
? ['/index.md']
|
|
83
|
+
: [`${srcDir}/index.md`, `${srcDir}index.md`]
|
|
84
|
+
);
|
|
85
|
+
for (const c of candidates) {
|
|
86
|
+
if (c in knownPaths) return c;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
get active() { return active; },
|
|
93
|
+
/** Resolved .md path suitable for fetch('/docs/guide/page.md'). */
|
|
94
|
+
get activeMarkdownPath() { return activeMarkdownPath; },
|
|
95
|
+
navigate,
|
|
96
|
+
/** Navigate and after content loads scroll to anchor. */
|
|
97
|
+
navigateWithAnchor(path: string, anchor?: string) {
|
|
98
|
+
navigate(path, anchor);
|
|
99
|
+
if (anchor) {
|
|
100
|
+
tick().then(() => setTimeout(() => {
|
|
101
|
+
document.getElementById(anchor)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
102
|
+
}, 120));
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
/** Returns true when active path corresponds to a markdown page. */
|
|
106
|
+
isMarkdown(flat: FlatItem[]) {
|
|
107
|
+
const m = flat.find((x: FlatItem) => x.link === lookupActive);
|
|
108
|
+
return m ? m.type === 'md' : true;
|
|
109
|
+
},
|
|
110
|
+
title(flat: FlatItem[]) {
|
|
111
|
+
return getBreadcrumb(lookupActive, flat);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag-to-resize rune for the left nav panel splitter.
|
|
3
|
+
* Returns refs and event handlers to bind on the aside + splitter elements.
|
|
4
|
+
*/
|
|
5
|
+
export function useSplitter() {
|
|
6
|
+
let aside = $state<HTMLElement | undefined>(undefined);
|
|
7
|
+
let splitter = $state<HTMLElement | undefined>(undefined);
|
|
8
|
+
let dragging = false;
|
|
9
|
+
|
|
10
|
+
function onMouseDown() {
|
|
11
|
+
dragging = true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function onMouseMove(e: MouseEvent) {
|
|
15
|
+
if (!dragging || !aside || !splitter) return;
|
|
16
|
+
aside.style.width = e.x - splitter.getBoundingClientRect().width / 2 + 'px';
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function onMouseUp() {
|
|
21
|
+
dragging = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
get aside() { return aside; },
|
|
26
|
+
set aside(el) { aside = el; },
|
|
27
|
+
get splitter() { return splitter; },
|
|
28
|
+
set splitter(el) { splitter = el; },
|
|
29
|
+
onMouseDown,
|
|
30
|
+
onMouseMove,
|
|
31
|
+
onMouseUp,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_SITE_BASE = '/';
|
|
2
|
+
export const DEFAULT_OUTPUT_BASE_DIR = 'dist';
|
|
3
|
+
export const DEFAULT_VERSIONS_DIR_NAME = '__versions';
|
|
4
|
+
export const LEGACY_VERSIONS_DIR_NAME = 'versions';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_OUTPUT_ROOT = `${DEFAULT_OUTPUT_BASE_DIR}/${DEFAULT_VERSIONS_DIR_NAME}`;
|
|
7
|
+
export const DEFAULT_PATH_PREFIX = `/${DEFAULT_VERSIONS_DIR_NAME}`;
|
|
8
|
+
|
|
9
|
+
export function normalizeBasePath(value, fallback = DEFAULT_SITE_BASE) {
|
|
10
|
+
const raw = String(value ?? fallback).trim();
|
|
11
|
+
if (!raw || raw === '/') return '/';
|
|
12
|
+
const cleaned = raw.replace(/^\/+|\/+$/g, '');
|
|
13
|
+
return `/${cleaned}/`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildDefaultVersionPathPrefix(basePath) {
|
|
17
|
+
const normalizedBase = normalizeBasePath(basePath);
|
|
18
|
+
if (normalizedBase === '/') return DEFAULT_PATH_PREFIX;
|
|
19
|
+
return `${normalizedBase.replace(/\/$/, '')}/${DEFAULT_VERSIONS_DIR_NAME}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitePluginAiServer
|
|
3
|
+
*
|
|
4
|
+
* Vite plugin that exposes an AI question-answering REST API in both
|
|
5
|
+
* dev (`configureServer`) and preview (`configurePreviewServer`) modes:
|
|
6
|
+
*
|
|
7
|
+
* POST /api/ai/ask — ask a question, receive an AI answer + sources
|
|
8
|
+
* GET /api/ai/characters — list available personas
|
|
9
|
+
*
|
|
10
|
+
* Only active when `search.ai.enabled = true` in greg.config.js.
|
|
11
|
+
* For production deployments use the standalone `aiServer.js` instead
|
|
12
|
+
* (invoked via `greg ai-server`).
|
|
13
|
+
*
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string} [options.docsDir='docs'] Docs directory relative to project root
|
|
16
|
+
* @param {string} [options.srcDir='/docs'] SPA route prefix
|
|
17
|
+
* @param {string} [options.aiUrl='/api/ai'] URL path prefix for AI endpoints
|
|
18
|
+
* @param {object} [options.ai={}] Contents of greg.config.js → search.ai
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { buildSearchIndex, invalidateSearchIndexCache } from './searchIndexBuilder.js';
|
|
23
|
+
import { buildChunks } from './ai/chunker.js';
|
|
24
|
+
import { MemoryStore } from './ai/stores/memoryStore.js';
|
|
25
|
+
import { resolveCharacters } from './ai/characters.js';
|
|
26
|
+
import { RagPipeline } from './ai/ragPipeline.js';
|
|
27
|
+
|
|
28
|
+
/** @typedef {import('../../../types/index.js').AiConfig} AiConfig */
|
|
29
|
+
|
|
30
|
+
export function vitePluginAiServer({
|
|
31
|
+
docsDir = 'docs',
|
|
32
|
+
srcDir = '/docs',
|
|
33
|
+
aiUrl = '/api/ai',
|
|
34
|
+
ai = /** @type {AiConfig} */ ({}),
|
|
35
|
+
} = {}) {
|
|
36
|
+
// Return a no-op plugin when AI is disabled — zero overhead
|
|
37
|
+
if (!ai?.enabled) {
|
|
38
|
+
return { name: 'greg:ai-server' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let resolvedDocsDir;
|
|
42
|
+
let viteBase = '/';
|
|
43
|
+
|
|
44
|
+
/** @type {RagPipeline | null} */
|
|
45
|
+
let pipelineCache = null;
|
|
46
|
+
/** @type {Promise<RagPipeline> | null} */
|
|
47
|
+
let buildPromise = null;
|
|
48
|
+
|
|
49
|
+
async function buildPipeline() {
|
|
50
|
+
const provider = await createProvider(ai);
|
|
51
|
+
const index = await buildSearchIndex(resolvedDocsDir, srcDir);
|
|
52
|
+
const chunks = buildChunks(index, ai?.chunking ?? {});
|
|
53
|
+
const store = new MemoryStore();
|
|
54
|
+
await store.index(chunks);
|
|
55
|
+
|
|
56
|
+
const characters = resolveCharacters(
|
|
57
|
+
ai?.characters,
|
|
58
|
+
ai?.customCharacters ?? [],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return new RagPipeline(provider, store, characters);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function loadPipeline() {
|
|
65
|
+
if (pipelineCache) return pipelineCache;
|
|
66
|
+
if (!buildPromise) {
|
|
67
|
+
buildPromise = buildPipeline()
|
|
68
|
+
.then(p => { pipelineCache = p; return p; })
|
|
69
|
+
.catch(err => { buildPromise = null; throw err; });
|
|
70
|
+
}
|
|
71
|
+
return buildPromise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function invalidate() {
|
|
75
|
+
pipelineCache = null;
|
|
76
|
+
buildPromise = null;
|
|
77
|
+
invalidateSearchIndexCache();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readBody(req) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
let body = '';
|
|
83
|
+
req.on('data', chunk => { body += chunk; });
|
|
84
|
+
req.on('end', () => {
|
|
85
|
+
try { resolve(JSON.parse(body || '{}')); }
|
|
86
|
+
catch { resolve({}); }
|
|
87
|
+
});
|
|
88
|
+
req.on('error', reject);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function middleware() {
|
|
93
|
+
return async (req, res, next) => {
|
|
94
|
+
const urlStr = req.url ?? '';
|
|
95
|
+
const qIdx = urlStr.indexOf('?');
|
|
96
|
+
const rawPathname = qIdx >= 0 ? urlStr.slice(0, qIdx) : urlStr;
|
|
97
|
+
const base = viteBase === '/' ? '' : viteBase.replace(/\/$/, '');
|
|
98
|
+
const pathname = (base && rawPathname.startsWith(base))
|
|
99
|
+
? '/' + rawPathname.slice(base.length).replace(/^\/+/, '')
|
|
100
|
+
: rawPathname;
|
|
101
|
+
|
|
102
|
+
// GET /api/ai/characters
|
|
103
|
+
if (pathname === `${aiUrl}/characters` && req.method === 'GET') {
|
|
104
|
+
try {
|
|
105
|
+
const pipeline = await loadPipeline();
|
|
106
|
+
const body = JSON.stringify({ characters: pipeline.getCharacters() });
|
|
107
|
+
res.writeHead(200, {
|
|
108
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
109
|
+
'Cache-Control': 'no-cache',
|
|
110
|
+
});
|
|
111
|
+
res.end(body);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('[greg:ai-server] characters error:', err.message);
|
|
114
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
115
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// POST /api/ai/ask
|
|
121
|
+
if (pathname === `${aiUrl}/ask` && req.method === 'POST') {
|
|
122
|
+
try {
|
|
123
|
+
const body = await readBody(req);
|
|
124
|
+
const query = String(body.query ?? body.q ?? '').trim();
|
|
125
|
+
if (!query) {
|
|
126
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'query is required' }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const characterId = String(body.character ?? ai?.defaultCharacter ?? 'professional');
|
|
132
|
+
const locale = String(body.locale ?? '');
|
|
133
|
+
|
|
134
|
+
const pipeline = await loadPipeline();
|
|
135
|
+
const result = await pipeline.ask(query, characterId, locale);
|
|
136
|
+
|
|
137
|
+
const responseBody = JSON.stringify(result);
|
|
138
|
+
res.writeHead(200, {
|
|
139
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
140
|
+
'Cache-Control': 'no-cache',
|
|
141
|
+
'Content-Length': Buffer.byteLength(responseBody),
|
|
142
|
+
});
|
|
143
|
+
res.end(responseBody);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('[greg:ai-server] ask error:', err.message);
|
|
146
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
147
|
+
res.end(JSON.stringify({ error: 'AI query failed', detail: err.message }));
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
next();
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: 'greg:ai-server',
|
|
158
|
+
|
|
159
|
+
configResolved(config) {
|
|
160
|
+
const dirs = Array.isArray(docsDir) ? docsDir : [docsDir];
|
|
161
|
+
resolvedDocsDir = dirs.map(d => path.resolve(config.root, d));
|
|
162
|
+
viteBase = config.base ?? '/';
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
configureServer(server) {
|
|
166
|
+
server.middlewares.use(middleware());
|
|
167
|
+
server.watcher.on('change', f => { if (f.endsWith('.md')) invalidate(); });
|
|
168
|
+
server.watcher.on('add', f => { if (f.endsWith('.md')) invalidate(); });
|
|
169
|
+
server.watcher.on('unlink', f => { if (f.endsWith('.md')) invalidate(); });
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
configurePreviewServer(server) {
|
|
173
|
+
server.middlewares.use(middleware());
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create the appropriate AiProvider from the `search.ai` config object.
|
|
180
|
+
* @param {object} aiConfig
|
|
181
|
+
*/
|
|
182
|
+
async function createProvider(aiConfig) {
|
|
183
|
+
const providerType = aiConfig?.provider ?? 'ollama';
|
|
184
|
+
|
|
185
|
+
if (providerType === 'ollama') {
|
|
186
|
+
const { OllamaAdapter } = await import('./ai/adapters/ollamaAdapter.js');
|
|
187
|
+
return new OllamaAdapter(aiConfig?.ollama ?? {});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (providerType === 'openai') {
|
|
191
|
+
const { OpenAiAdapter } = await import('./ai/adapters/openaiAdapter.js');
|
|
192
|
+
return new OpenAiAdapter(aiConfig?.openai ?? {});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (providerType === 'custom' && typeof aiConfig?.customProvider === 'function') {
|
|
196
|
+
const { CustomAdapter } = await import('./ai/adapters/customAdapter.js');
|
|
197
|
+
return new CustomAdapter(aiConfig.customProvider);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error(
|
|
201
|
+
`[greg:ai] Unknown provider: "${providerType}". ` +
|
|
202
|
+
`Supported values: "ollama", "openai", "custom".`,
|
|
203
|
+
);
|
|
204
|
+
}
|