@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.
Files changed (183) hide show
  1. package/README.md +397 -0
  2. package/bin/greg.js +241 -0
  3. package/bin/init.js +351 -0
  4. package/bin/templates/docs/getting-started.md +47 -0
  5. package/bin/templates/docs/index.md +11 -0
  6. package/bin/templates/greg.config.js +39 -0
  7. package/bin/templates/greg.config.ts +38 -0
  8. package/bin/templates/index.html +16 -0
  9. package/bin/templates/src/App.svelte +5 -0
  10. package/bin/templates/src/app.css +20 -0
  11. package/bin/templates/src/main.js +9 -0
  12. package/bin/templates/svelte.config.js +1 -0
  13. package/bin/templates/tsconfig.json +21 -0
  14. package/bin/templates/vite.config.js +23 -0
  15. package/docs/__partials/markdown/examples/basic.md +4 -0
  16. package/docs/__partials/markdown/examples/diff.md +10 -0
  17. package/docs/__partials/markdown/examples/focus.md +5 -0
  18. package/docs/__partials/markdown/examples/language-title.md +3 -0
  19. package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
  20. package/docs/__partials/markdown/examples/line-numbers.md +5 -0
  21. package/docs/__partials/note.md +4 -0
  22. package/docs/guide/__shared-warning.md +4 -0
  23. package/docs/guide/asset-handling.md +88 -0
  24. package/docs/guide/deploying.md +162 -0
  25. package/docs/guide/getting-started.md +334 -0
  26. package/docs/guide/index.md +23 -0
  27. package/docs/guide/localization.md +290 -0
  28. package/docs/guide/markdown/code.md +95 -0
  29. package/docs/guide/markdown/components-and-mermaid.md +43 -0
  30. package/docs/guide/markdown/containers.md +110 -0
  31. package/docs/guide/markdown/header-anchors.md +34 -0
  32. package/docs/guide/markdown/includes.md +84 -0
  33. package/docs/guide/markdown/index.md +20 -0
  34. package/docs/guide/markdown/inline-attributes.md +21 -0
  35. package/docs/guide/markdown/links-and-toc.md +64 -0
  36. package/docs/guide/markdown/math.md +54 -0
  37. package/docs/guide/markdown/syntax-highlighting.md +75 -0
  38. package/docs/guide/routing.md +150 -0
  39. package/docs/guide/using-svelte.md +88 -0
  40. package/docs/guide/versioning.md +281 -0
  41. package/docs/incompatibilities.md +48 -0
  42. package/docs/index.md +43 -0
  43. package/docs/reference/badge.md +100 -0
  44. package/docs/reference/carbon-ads.md +46 -0
  45. package/docs/reference/code-group.md +126 -0
  46. package/docs/reference/home-page.md +232 -0
  47. package/docs/reference/index.md +18 -0
  48. package/docs/reference/markdowndocs.md +275 -0
  49. package/docs/reference/outline.md +79 -0
  50. package/docs/reference/search.md +263 -0
  51. package/docs/reference/steps.md +200 -0
  52. package/docs/reference/team-page.md +189 -0
  53. package/docs/reference/theme.md +150 -0
  54. package/fakeDocsGenerator/generate_docs.js +310 -0
  55. package/package.json +92 -0
  56. package/scripts/build-versions.js +609 -0
  57. package/scripts/generate-static.js +79 -0
  58. package/scripts/render-markdown.js +420 -0
  59. package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
  60. package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
  61. package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
  62. package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
  63. package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
  64. package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
  65. package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
  66. package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
  67. package/src/lib/MarkdownDocs/Outline.svelte +238 -0
  68. package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
  69. package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
  70. package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
  71. package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
  72. package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
  73. package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
  74. package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
  75. package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
  76. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
  77. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
  78. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
  79. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
  80. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
  81. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
  82. package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
  83. package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
  84. package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
  85. package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
  86. package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
  87. package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
  88. package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
  89. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
  90. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
  91. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
  92. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
  93. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
  94. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
  95. package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
  96. package/src/lib/MarkdownDocs/ai/characters.js +52 -0
  97. package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
  98. package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
  99. package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
  100. package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
  101. package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
  102. package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
  103. package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
  104. package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
  105. package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
  106. package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
  107. package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
  108. package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
  109. package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
  110. package/src/lib/MarkdownDocs/ai/types.ts +71 -0
  111. package/src/lib/MarkdownDocs/aiServer.js +288 -0
  112. package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
  113. package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
  114. package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
  115. package/src/lib/MarkdownDocs/common.ts +47 -0
  116. package/src/lib/MarkdownDocs/docsUtils.js +281 -0
  117. package/src/lib/MarkdownDocs/index.plugins.js +22 -0
  118. package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
  119. package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
  120. package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
  121. package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
  122. package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
  123. package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
  124. package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
  125. package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
  126. package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
  127. package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
  128. package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
  129. package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
  130. package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
  131. package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
  132. package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
  133. package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
  134. package/src/lib/MarkdownDocs/remarkImports.js +461 -0
  135. package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
  136. package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
  137. package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
  138. package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
  139. package/src/lib/MarkdownDocs/searchServer.js +263 -0
  140. package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
  141. package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
  142. package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
  143. package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
  144. package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
  145. package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
  146. package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
  147. package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
  148. package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
  149. package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
  150. package/src/lib/components/Badge.svelte +59 -0
  151. package/src/lib/components/Button.svelte +138 -0
  152. package/src/lib/components/CarbonAds.svelte +99 -0
  153. package/src/lib/components/CodeGroup.svelte +102 -0
  154. package/src/lib/components/Feature.svelte +209 -0
  155. package/src/lib/components/Features.svelte +123 -0
  156. package/src/lib/components/Hero.svelte +399 -0
  157. package/src/lib/components/Image.svelte +128 -0
  158. package/src/lib/components/Link.svelte +105 -0
  159. package/src/lib/components/SocialLink.svelte +84 -0
  160. package/src/lib/components/SocialLinks.svelte +33 -0
  161. package/src/lib/components/Steps.svelte +143 -0
  162. package/src/lib/components/TeamMember.svelte +273 -0
  163. package/src/lib/components/TeamMembers.svelte +81 -0
  164. package/src/lib/components/TeamPage.svelte +65 -0
  165. package/src/lib/components/TeamPageSection.svelte +108 -0
  166. package/src/lib/components/TeamPageTitle.svelte +89 -0
  167. package/src/lib/components/index.js +24 -0
  168. package/src/lib/portal/context.js +12 -0
  169. package/src/lib/portal/index.js +3 -0
  170. package/src/lib/portal/portal.svelte +14 -0
  171. package/src/lib/portal/slot.svelte +8 -0
  172. package/src/lib/scss/__code.scss +128 -0
  173. package/src/lib/scss/__containers.scss +99 -0
  174. package/src/lib/scss/__markdown.scss +447 -0
  175. package/src/lib/scss/__scrollbar.scss +60 -0
  176. package/src/lib/scss/__steps.scss +100 -0
  177. package/src/lib/scss/__theme.scss +238 -0
  178. package/src/lib/scss/__toc.scss +55 -0
  179. package/src/lib/scss/__utilities.scss +7 -0
  180. package/src/lib/scss/greg.scss +9 -0
  181. package/src/lib/spinner/spinner.svelte +42 -0
  182. package/svelte.config.js +146 -0
  183. package/types/index.d.ts +456 -0
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Greg standalone AI server — for production deployments.
4
+ *
5
+ * Reads a pre-built search-index.json, builds the RAG pipeline, then serves:
6
+ * POST /api/ai/ask — ask a question, receive answer + source links
7
+ * GET /api/ai/characters — list available AI personas
8
+ *
9
+ * Usage:
10
+ * node src/lib/MarkdownDocs/aiServer.js [options]
11
+ * greg ai-server (via CLI)
12
+ *
13
+ * Options (CLI flags or environment variables):
14
+ * --index PATH Path to search-index.json (GREG_AI_INDEX, default: dist/search-index.json)
15
+ * --port NUMBER HTTP port (GREG_AI_PORT, default: 3200)
16
+ * --host ADDR Bind address (GREG_AI_HOST, default: localhost)
17
+ * --url PATH API base URL path (GREG_AI_URL, default: /api/ai)
18
+ * --provider NAME LLM provider (GREG_AI_PROVIDER, default: ollama)
19
+ * --model NAME LLM model name (GREG_AI_MODEL)
20
+ * --ollama-url URL Ollama base URL (GREG_AI_OLLAMA_URL, default: http://localhost:11434)
21
+ * --cors-origin VALUE CORS allowed origin (GREG_AI_CORS_ORIGIN, default: *)
22
+ * --cors-methods VALUE CORS allowed methods (GREG_AI_CORS_METHODS, default: GET, POST, OPTIONS)
23
+ * --cors-headers VALUE CORS allowed headers (GREG_AI_CORS_HEADERS, default: Content-Type)
24
+ * --cors-max-age VALUE CORS preflight cache secs (GREG_AI_CORS_MAX_AGE, default: 86400)
25
+ *
26
+ * Example — run after `greg build`:
27
+ * greg ai-server --index dist/search-index.json --provider ollama --model llama3.2
28
+ *
29
+ * Then set search.ai.serverUrl in greg.config.js → 'http://localhost:3200/api/ai'
30
+ * (or configure a reverse proxy).
31
+ */
32
+
33
+ import { createServer } from 'node:http';
34
+ import { existsSync, readFileSync } from 'node:fs';
35
+ import { resolve } from 'node:path';
36
+ import { buildChunks } from './ai/chunker.js';
37
+ import { MemoryStore } from './ai/stores/memoryStore.js';
38
+ import { resolveCharacters } from './ai/characters.js';
39
+ import { RagPipeline } from './ai/ragPipeline.js';
40
+ import { loadGregConfig } from './loadGregConfig.js';
41
+
42
+ /** @param {string} storeType @param {object} aiCfg @param {import('./ai/aiProvider.js').AiProvider} [provider] */
43
+ async function createStore(storeType, aiCfg, provider) {
44
+ if (storeType === 'sqlite') {
45
+ const { SqliteStore } = await import('./ai/stores/sqliteStore.js');
46
+ const sqliteCfg = aiCfg?.sqlite ?? {};
47
+ return new SqliteStore({
48
+ dbPath: sqliteCfg.dbPath ?? 'docs.db',
49
+ embeddingDimensions: sqliteCfg.embeddingDimensions ?? 0,
50
+ provider: sqliteCfg.embeddingDimensions ? provider : undefined,
51
+ embeddingBatchSize: sqliteCfg.embeddingBatchSize,
52
+ bm25Weight: sqliteCfg.bm25Weight,
53
+ });
54
+ }
55
+ return new MemoryStore();
56
+ }
57
+
58
+ const startupT0 = process.hrtime.bigint();
59
+
60
+ function msSince(t0) {
61
+ return Number(process.hrtime.bigint() - t0) / 1e6;
62
+ }
63
+
64
+ function fmtMs(ms) {
65
+ return `${ms.toFixed(1)}ms`;
66
+ }
67
+
68
+ // ── CLI argument parser ───────────────────────────────────────────────────────
69
+
70
+ function parseArgs(argv) {
71
+ const args = {};
72
+ for (let i = 0; i < argv.length; i++) {
73
+ const a = argv[i];
74
+ if (a.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
75
+ args[a.slice(2)] = argv[i + 1];
76
+ i++;
77
+ } else if (a.startsWith('--')) {
78
+ args[a.slice(2)] = true;
79
+ }
80
+ }
81
+ return args;
82
+ }
83
+
84
+ // ── Resolve configuration ─────────────────────────────────────────────────────
85
+
86
+ const cliArgs = parseArgs(process.argv.slice(2));
87
+ const gregConfig = await loadGregConfig();
88
+ const aiConfig = gregConfig?.search?.ai ?? {};
89
+
90
+ const port = parseInt(cliArgs.port ?? process.env.GREG_AI_PORT ?? '3200', 10);
91
+ const host = String(cliArgs.host ?? process.env.GREG_AI_HOST ?? 'localhost');
92
+ const url = String(cliArgs.url ?? process.env.GREG_AI_URL ?? '/api/ai');
93
+ const index = resolve(String(cliArgs.index ?? process.env.GREG_AI_INDEX ?? 'dist/search-index.json'));
94
+ const providerType = String(cliArgs.provider ?? process.env.GREG_AI_PROVIDER ?? aiConfig?.provider ?? 'ollama');
95
+ const corsOrigin = String(cliArgs['cors-origin'] ?? process.env.GREG_AI_CORS_ORIGIN ?? '*');
96
+ const corsMethods = String(cliArgs['cors-methods'] ?? process.env.GREG_AI_CORS_METHODS ?? 'GET, POST, OPTIONS');
97
+ const corsHeaders = String(cliArgs['cors-headers'] ?? process.env.GREG_AI_CORS_HEADERS ?? 'Content-Type');
98
+ const corsMaxAge = String(cliArgs['cors-max-age'] ?? process.env.GREG_AI_CORS_MAX_AGE ?? '86400');
99
+
100
+ function getCorsHeaders(req) {
101
+ const reflectedOrigin = req.headers.origin ? String(req.headers.origin) : '*';
102
+ const allowOrigin = corsOrigin === 'request' ? reflectedOrigin : corsOrigin;
103
+ return {
104
+ 'Access-Control-Allow-Origin': allowOrigin,
105
+ 'Access-Control-Allow-Methods': corsMethods,
106
+ 'Access-Control-Allow-Headers': corsHeaders,
107
+ 'Access-Control-Max-Age': corsMaxAge,
108
+ ...(corsOrigin === 'request' ? { 'Vary': 'Origin' } : {}),
109
+ };
110
+ }
111
+
112
+ // ── Build provider ────────────────────────────────────────────────────────────
113
+
114
+ async function createProvider() {
115
+ if (providerType === 'ollama') {
116
+ const { OllamaAdapter } = await import('./ai/adapters/ollamaAdapter.js');
117
+ const ollamaUrl = String(
118
+ cliArgs['ollama-url'] ??
119
+ process.env.GREG_AI_OLLAMA_URL ??
120
+ aiConfig?.ollama?.baseUrl ??
121
+ 'http://localhost:11434',
122
+ );
123
+ const model = String(
124
+ cliArgs.model ??
125
+ process.env.GREG_AI_MODEL ??
126
+ aiConfig?.ollama?.model ??
127
+ 'llama3.2',
128
+ );
129
+ return new OllamaAdapter({ baseUrl: ollamaUrl, model, ...(aiConfig?.ollama ?? {}) });
130
+ }
131
+
132
+ if (providerType === 'openai') {
133
+ const { OpenAiAdapter } = await import('./ai/adapters/openaiAdapter.js');
134
+ const model = String(
135
+ cliArgs.model ??
136
+ process.env.GREG_AI_MODEL ??
137
+ aiConfig?.openai?.model ??
138
+ 'gpt-4o-mini',
139
+ );
140
+ return new OpenAiAdapter({ model, ...(aiConfig?.openai ?? {}) });
141
+ }
142
+
143
+ throw new Error(`[greg-ai] Unknown provider: "${providerType}". Supported: "ollama", "openai".`);
144
+ }
145
+
146
+ // ── Load search index ─────────────────────────────────────────────────────────
147
+
148
+ console.log(`[greg-ai] Loading index: ${index}`);
149
+
150
+ if (!existsSync(index)) {
151
+ console.error(`[greg-ai] Index file not found: ${index}`);
152
+ console.error('[greg-ai] Run "greg build" first to generate dist/search-index.json.');
153
+ process.exit(1);
154
+ }
155
+
156
+ const loadT0 = process.hrtime.bigint();
157
+ let indexData;
158
+ try {
159
+ indexData = JSON.parse(readFileSync(index, 'utf-8'));
160
+ } catch (err) {
161
+ console.error(`[greg-ai] Failed to parse index: ${err.message}`);
162
+ process.exit(1);
163
+ }
164
+ const loadMs = msSince(loadT0);
165
+
166
+ // ── Build RAG pipeline ────────────────────────────────────────────────────────
167
+
168
+ const chunkT0 = process.hrtime.bigint();
169
+ const chunks = buildChunks(indexData, aiConfig?.chunking ?? {});
170
+ const chunkMs = msSince(chunkT0);
171
+
172
+ const characters = resolveCharacters(
173
+ aiConfig?.characters,
174
+ aiConfig?.customCharacters ?? [],
175
+ );
176
+
177
+ const provider = await createProvider();
178
+
179
+ const storeType = String(cliArgs.store ?? process.env.GREG_AI_STORE ?? aiConfig?.store ?? 'memory');
180
+ const storeT0 = process.hrtime.bigint();
181
+ const store = await createStore(storeType, aiConfig, provider);
182
+ await store.index(chunks);
183
+ const storeMs = msSince(storeT0);
184
+
185
+ const pipeline = new RagPipeline(provider, store, characters);
186
+
187
+ // ── HTTP server helpers ───────────────────────────────────────────────────────
188
+
189
+ function readBody(req) {
190
+ return new Promise((resolve, reject) => {
191
+ let body = '';
192
+ req.on('data', chunk => { body += chunk; });
193
+ req.on('end', () => {
194
+ try { resolve(JSON.parse(body || '{}')); }
195
+ catch { resolve({}); }
196
+ });
197
+ req.on('error', reject);
198
+ });
199
+ }
200
+
201
+ // ── HTTP server ───────────────────────────────────────────────────────────────
202
+
203
+ const server = createServer(async (req, res) => {
204
+ const rawUrl = req.url ?? '';
205
+ const qIdx = rawUrl.indexOf('?');
206
+ const pathname = qIdx >= 0 ? rawUrl.slice(0, qIdx) : rawUrl;
207
+
208
+ // CORS preflight
209
+ if (req.method === 'OPTIONS') {
210
+ res.writeHead(204, { ...getCorsHeaders(req) });
211
+ res.end();
212
+ return;
213
+ }
214
+
215
+ // GET /api/ai/characters
216
+ if (pathname === `${url}/characters` && req.method === 'GET') {
217
+ const body = JSON.stringify({ characters: pipeline.getCharacters() });
218
+ res.writeHead(200, {
219
+ 'Content-Type': 'application/json; charset=utf-8',
220
+ 'Content-Length': Buffer.byteLength(body),
221
+ ...getCorsHeaders(req),
222
+ });
223
+ res.end(body);
224
+ return;
225
+ }
226
+
227
+ // POST /api/ai/ask
228
+ if (pathname === `${url}/ask` && req.method === 'POST') {
229
+ let requestBody;
230
+ try {
231
+ requestBody = await readBody(req);
232
+ } catch {
233
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8', ...getCorsHeaders(req) });
234
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
235
+ return;
236
+ }
237
+
238
+ const query = String(requestBody.query ?? requestBody.q ?? '').trim();
239
+ if (!query) {
240
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8', ...getCorsHeaders(req) });
241
+ res.end(JSON.stringify({ error: 'query is required' }));
242
+ return;
243
+ }
244
+
245
+ const characterId = String(requestBody.character ?? aiConfig?.defaultCharacter ?? 'professional');
246
+ const locale = String(requestBody.locale ?? '');
247
+
248
+ try {
249
+ const result = await pipeline.ask(query, characterId, locale);
250
+ const body = JSON.stringify(result);
251
+ res.writeHead(200, {
252
+ 'Content-Type': 'application/json; charset=utf-8',
253
+ 'Cache-Control': 'no-cache',
254
+ 'Content-Length': Buffer.byteLength(body),
255
+ ...getCorsHeaders(req),
256
+ });
257
+ res.end(body);
258
+ } catch (err) {
259
+ console.error('[greg-ai] Query failed:', err.message);
260
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8', ...getCorsHeaders(req) });
261
+ res.end(JSON.stringify({ error: 'AI query failed', detail: err.message }));
262
+ }
263
+ return;
264
+ }
265
+
266
+ res.writeHead(404, { ...getCorsHeaders(req) });
267
+ res.end();
268
+ });
269
+
270
+ server.listen(port, host, () => {
271
+ const startupMs = msSince(startupT0);
272
+ console.log(`[greg-ai] Listening on http://${host}:${port}${url}`);
273
+ console.log(
274
+ `[greg-ai] Startup: load=${fmtMs(loadMs)}, chunk=${fmtMs(chunkMs)}, ` +
275
+ `store-index=${fmtMs(storeMs)}, total=${fmtMs(startupMs)}`,
276
+ );
277
+ console.log(
278
+ `[greg-ai] Docs: ${indexData.length} pages | Chunks: ${store.size()} | Store: ${storeType} | Provider: ${providerType}`,
279
+ );
280
+ console.log(`[greg-ai] Characters: ${characters.map(c => `${c.icon} ${c.id}`).join(', ')}`);
281
+ console.log(
282
+ `[greg-ai] CORS: origin=${corsOrigin}, methods="${corsMethods}", ` +
283
+ `headers="${corsHeaders}", max-age=${corsMaxAge}`,
284
+ );
285
+ console.log(`[greg-ai] Endpoints:`);
286
+ console.log(`[greg-ai] POST http://${host}:${port}${url}/ask`);
287
+ console.log(`[greg-ai] GET http://${host}:${port}${url}/characters`);
288
+ });
@@ -0,0 +1,191 @@
1
+ function parseLineRanges(meta) {
2
+ const text = String(meta ?? '').replace(/\[[^\]]*\]/g, '');
3
+ const match = text.match(/\{([^}]+)\}/);
4
+ if (!match?.[1]) return new Set();
5
+
6
+ const out = new Set();
7
+ const parts = match[1].split(',').map((p) => p.trim()).filter(Boolean);
8
+ for (const part of parts) {
9
+ if (/^\d+$/.test(part)) {
10
+ out.add(Number(part));
11
+ continue;
12
+ }
13
+ const range = part.match(/^(\d+)\s*-\s*(\d+)$/);
14
+ if (!range) continue;
15
+ const start = Number(range[1]);
16
+ const end = Number(range[2]);
17
+ const lo = Math.min(start, end);
18
+ const hi = Math.max(start, end);
19
+ for (let i = lo; i <= hi; i++) out.add(i);
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function parseLineNumbers(meta) {
25
+ const text = String(meta ?? '');
26
+ if (/\bno-line-numbers\b/i.test(text)) return { enabled: false, start: 1 };
27
+ const match = text.match(/\bline-numbers(?:=(\d+))?\b/i);
28
+ if (!match) return { enabled: false, start: 1 };
29
+ const start = match[1] ? Math.max(1, Number(match[1])) : 1;
30
+ return { enabled: true, start };
31
+ }
32
+
33
+ function parseMarkerToken(token, info) {
34
+ const t = token.trim().toLowerCase();
35
+ if (t === '++') info.diffAdd = true;
36
+ else if (t === '--') info.diffRemove = true;
37
+ else if (t === 'warning') info.diffWarning = true;
38
+ else if (t === 'error') info.diffError = true;
39
+ else if (t === 'focus') info.focus = true;
40
+ else if (t === 'highlight') info.highlight = true;
41
+ }
42
+
43
+ export function parseCodeDirectives(code, meta = '', lang = '') {
44
+ const normalizedCode = String(code ?? '').replace(/\r?\n$/, '');
45
+ const normalizedLang = String(lang ?? '').trim().toLowerCase();
46
+ const isMarkdownSource = normalizedLang === 'md' || normalizedLang === 'markdown';
47
+ if (isMarkdownSource) {
48
+ return {
49
+ cleanedCode: normalizedCode,
50
+ lineInfo: [],
51
+ lineNumbers: { enabled: false, start: 1 },
52
+ };
53
+ }
54
+
55
+ const highlightedLines = parseLineRanges(meta);
56
+ const lineNumbers = parseLineNumbers(meta);
57
+ const lines = normalizedCode.split(/\r?\n/);
58
+ const lineInfo = [];
59
+
60
+ const markerRe = /\[!code\s+([^\]]+)\]/gi;
61
+
62
+ for (let i = 0; i < lines.length; i++) {
63
+ let line = lines[i];
64
+ const info = {
65
+ highlight: highlightedLines.has(i + 1),
66
+ focus: false,
67
+ diffAdd: false,
68
+ diffRemove: false,
69
+ diffWarning: false,
70
+ diffError: false,
71
+ };
72
+
73
+ markerRe.lastIndex = 0;
74
+ for (const match of line.matchAll(markerRe)) {
75
+ parseMarkerToken(match[1] ?? '', info);
76
+ }
77
+
78
+ // Remove trailing marker comments and any stray marker tokens.
79
+ line = line.replace(/\s*(?:\/\/|#|<!--)?\s*\[!code\s+[^\]]+\]\s*(?:-->)?\s*$/gi, '');
80
+ line = line.replace(/\s*\[!code\s+[^\]]+\]\s*/gi, '');
81
+
82
+ lines[i] = line;
83
+ lineInfo.push(info);
84
+ }
85
+
86
+ return {
87
+ cleanedCode: lines.join('\n'),
88
+ lineInfo,
89
+ lineNumbers,
90
+ };
91
+ }
92
+
93
+ function addClassesToLineWrapper(lineHtml, classes) {
94
+ const classText = classes.join(' ');
95
+ if (!classText) return lineHtml;
96
+
97
+ if (/^<span class="[^"]*line[^"]*">/.test(lineHtml)) {
98
+ return lineHtml.replace(/^<span class="([^"]*)">/, (_m, cls) => {
99
+ const next = `${cls} ${classText}`.trim().replace(/\s+/g, ' ');
100
+ return `<span class="${next}">`;
101
+ });
102
+ }
103
+
104
+ return `<span class="line ${classText}">${lineHtml}</span>`;
105
+ }
106
+
107
+ function decorateLineSpanFragment(fragment, classes, dataLine) {
108
+ return fragment.replace(/<span class="([^"]*\bline\b[^"]*)">([\s\S]*?)<\/span>/, (_m, cls, inner) => {
109
+ const merged = `${cls} ${classes.join(' ')}`.trim().replace(/\s+/g, ' ');
110
+ const dataLineAttr = Number.isFinite(dataLine) ? ` data-line="${dataLine}"` : '';
111
+ return `<span class="${merged}"${dataLineAttr}>${inner}</span>`;
112
+ });
113
+ }
114
+
115
+ function addClassToPre(html, cls) {
116
+ if (!cls) return html;
117
+ if (/<pre\b[^>]*class="[^"]*"/i.test(html)) {
118
+ return html.replace(/<pre\b([^>]*?)class="([^"]*)"([^>]*)>/i, (_m, preA, preCls, preB) => {
119
+ const merged = `${preCls} ${cls}`.trim().replace(/\s+/g, ' ');
120
+ return `<pre${preA}class="${merged}"${preB}>`;
121
+ });
122
+ }
123
+ return html.replace(/<pre\b/i, `<pre class="${cls}"`);
124
+ }
125
+
126
+ function addAttrsToCode(html, attrs) {
127
+ const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '');
128
+ if (!entries.length) return html;
129
+
130
+ const attrsText = entries.map(([k, v]) => `${k}="${String(v)}"`).join(' ');
131
+ if (/<code\b[^>]*>/i.test(html)) {
132
+ return html.replace(/<code\b([^>]*)>/i, (_m, codeAttrs) => `<code${codeAttrs} ${attrsText}>`);
133
+ }
134
+ return html;
135
+ }
136
+
137
+ export function decorateHighlightedCodeHtml(highlightedHtml, directives) {
138
+ const lines = String(highlightedHtml ?? '').split('\n');
139
+ let hasFocused = false;
140
+ let hasDiff = false;
141
+ const lineNumbersEnabled = Boolean(directives.lineNumbers?.enabled);
142
+ const lineNumberStart = Number.isFinite(directives.lineNumbers?.start)
143
+ ? directives.lineNumbers.start
144
+ : 1;
145
+
146
+ for (let i = 0; i < lines.length; i++) {
147
+ const info = directives.lineInfo[i] ?? {
148
+ highlight: false,
149
+ focus: false,
150
+ diffAdd: false,
151
+ diffRemove: false,
152
+ diffWarning: false,
153
+ diffError: false,
154
+ };
155
+
156
+ if (/<span class="[^"]*\bline\b[^"]*">/.test(lines[i])) {
157
+ const classes = [];
158
+ if (info.highlight) classes.push('highlighted');
159
+ if (info.focus) {
160
+ classes.push('focused');
161
+ hasFocused = true;
162
+ }
163
+ if (info.diffAdd) classes.push('diff', 'add');
164
+ if (info.diffRemove) classes.push('diff', 'remove');
165
+ if (info.diffWarning) classes.push('diff', 'warning');
166
+ if (info.diffError) classes.push('diff', 'error');
167
+ if (info.diffAdd || info.diffRemove || info.diffWarning || info.diffError) hasDiff = true;
168
+
169
+ const dataLine = lineNumbersEnabled ? lineNumberStart + i : undefined;
170
+ lines[i] = decorateLineSpanFragment(lines[i], classes, dataLine);
171
+ } else if (directives.lineInfo.length && info.highlight) {
172
+ lines[i] = addClassesToLineWrapper(lines[i], ['highlighted']);
173
+ }
174
+ }
175
+
176
+ let html = lines.join('\n');
177
+ if (hasFocused) html = addClassToPre(html, 'has-focused-lines');
178
+ if (hasDiff) html = addClassToPre(html, 'has-diff');
179
+
180
+ if (lineNumbersEnabled) {
181
+ const totalLines = lines.filter((l) => /<span class="[^"]*\bline\b[^"]*">/.test(l)).length;
182
+ const maxLineNumber = Math.max(lineNumberStart + totalLines - 1, lineNumberStart);
183
+ const maxDigits = String(maxLineNumber).length;
184
+ html = addAttrsToCode(html, {
185
+ 'data-line-numbers': 'true',
186
+ 'data-line-numbers-max-digits': maxDigits,
187
+ });
188
+ }
189
+
190
+ return html;
191
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Normalize fenced code info so VitePress-style forms are supported:
3
+ * - ```ts{2}
4
+ * - ```ts:line-numbers=10 {1}
5
+ * - ```ts line-numbers=10 {1}
6
+ */
7
+ export function normalizeCodeFenceInfo(rawLang = '', rawMeta = '') {
8
+ const langToken = String(rawLang ?? '').trim();
9
+ const baseMeta = String(rawMeta ?? '').trim();
10
+
11
+ if (!langToken) {
12
+ return { lang: '', meta: baseMeta };
13
+ }
14
+
15
+ const m = langToken.match(/^([^:{\s]+)([\s\S]*)$/);
16
+ if (!m) return { lang: langToken, meta: baseMeta };
17
+
18
+ const lang = m[1].trim().toLowerCase();
19
+ const suffix = m[2] ?? '';
20
+
21
+ const metaParts = [];
22
+ if (baseMeta) metaParts.push(baseMeta);
23
+
24
+ for (const token of suffix.matchAll(/:([^\s{}]+)/g)) {
25
+ if (token[1]) metaParts.push(token[1].trim());
26
+ }
27
+ for (const block of suffix.matchAll(/\{[^}]+\}/g)) {
28
+ metaParts.push(block[0]);
29
+ }
30
+
31
+ const seen = new Set();
32
+ const mergedMeta = metaParts
33
+ .map((p) => p.trim())
34
+ .filter(Boolean)
35
+ .filter((p) => {
36
+ const key = p.toLowerCase();
37
+ if (seen.has(key)) return false;
38
+ seen.add(key);
39
+ return true;
40
+ })
41
+ .join(' ')
42
+ .trim();
43
+
44
+ return { lang, meta: mergedMeta };
45
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Delegated DOM handlers for static rehype code-group tabs.
3
+ *
4
+ * Used by mdsvex-rendered markdown pages where tab markup is emitted as
5
+ * plain HTML (<div class="rehype-code-group">...).
6
+ */
7
+ export function handleCodeGroupClick(event: MouseEvent) {
8
+ const target = event.target;
9
+ if (!(target instanceof HTMLElement)) return;
10
+
11
+ const tab = target.closest(".rcg-tab");
12
+ if (!(tab instanceof HTMLButtonElement)) return;
13
+
14
+ const group = tab.closest(".rehype-code-group");
15
+ if (!(group instanceof HTMLElement)) return;
16
+
17
+ const tabs = Array.from(group.querySelectorAll(".rcg-tab"));
18
+ const blocks = Array.from(group.querySelectorAll(".rcg-block"));
19
+ const index = tabs.indexOf(tab);
20
+ if (index < 0 || index >= blocks.length) return;
21
+
22
+ tabs.forEach((item) => {
23
+ item.classList.remove("active");
24
+ item.setAttribute("aria-selected", "false");
25
+ });
26
+ blocks.forEach((block) => {
27
+ block.classList.remove("active");
28
+ block.setAttribute("hidden", "true");
29
+ });
30
+
31
+ tab.classList.add("active");
32
+ tab.setAttribute("aria-selected", "true");
33
+
34
+ const activeBlock = blocks[index];
35
+ activeBlock.classList.add("active");
36
+ activeBlock.removeAttribute("hidden");
37
+ }
38
+
39
+ export function handleCodeGroupKeydown(event: KeyboardEvent) {
40
+ if (event.key !== "Enter" && event.key !== " ") return;
41
+ const target = event.target;
42
+ if (!(target instanceof HTMLElement)) return;
43
+ if (!target.closest(".rcg-tab")) return;
44
+ event.preventDefault();
45
+ handleCodeGroupClick(event as unknown as MouseEvent);
46
+ }
@@ -0,0 +1,47 @@
1
+ import gregConfig from "virtual:greg-config";
2
+
3
+ const EXTERNAL_RE = /^(?:[a-z][a-z\d+\-.]*:|\/\/)/i;
4
+ const SLASHES_RE = /\/+/g;
5
+
6
+ function normalizeBase(value: unknown): string {
7
+ const raw = String(value || "/").trim();
8
+ return raw === "/" ? "/" : `/${raw.replace(/^\/+|\/+$/g, "")}`;
9
+ }
10
+
11
+ export function joinPath(base: string, path: string) {
12
+ return `${base}${path}`.replace(SLASHES_RE, '/')
13
+ }
14
+
15
+ export const pathConfig = {
16
+ base: normalizeBase((gregConfig as any).base),
17
+ srcDir: String((gregConfig as any).srcDir ?? "docs").replace(SLASHES_RE, '/'),
18
+ docsBase: String(
19
+ Object.prototype.hasOwnProperty.call((gregConfig as any), "docsBase")
20
+ ? (gregConfig as any).docsBase
21
+ : "",
22
+ ).replace(SLASHES_RE, '/'),
23
+ };
24
+
25
+ export function withBase(path: string): string {
26
+ if (EXTERNAL_RE.test(path) || !path.startsWith("/")) return path;
27
+ return joinPath(pathConfig.base, path);
28
+ }
29
+
30
+ export function withoutBase(path: string): string {
31
+ if (EXTERNAL_RE.test(path) || !path.startsWith("/")) return path;
32
+ const base = pathConfig.base;
33
+ if (base && path.startsWith(base)) return joinPath('/', path.slice(base.length));
34
+ return path;
35
+ }
36
+
37
+ /**
38
+ * Normalize any path to the canonical `/something` form.
39
+ * Returns `"/"` for empty, null-ish, or root inputs.
40
+ * Mirrors `normalizeSrcDir` from `localeUtils` to avoid a circular import.
41
+ */
42
+ export function normalizePath(path: string): string {
43
+ const value = String(path || "").trim();
44
+ if (!value || value === "/") return "/";
45
+ return "/" + value.replace(/^\/+|\/+$/g, "");
46
+ }
47
+