superlore-cli 0.1.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/dist/index.js ADDED
@@ -0,0 +1,899 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import process2 from "process";
6
+ import { cac } from "cac";
7
+
8
+ // src/lib/log.ts
9
+ var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
10
+ var truecolor = useColor && /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
11
+ function paint(open, text2) {
12
+ return useColor ? `${open}${text2}\x1B[0m` : text2;
13
+ }
14
+ var accent = (text2) => paint(truecolor ? "\x1B[38;2;109;92;240m" : "\x1B[38;5;99m", text2);
15
+ var accentSoft = (text2) => paint(truecolor ? "\x1B[38;2;183;172;255m" : "\x1B[38;5;147m", text2);
16
+ var bold = (text2) => paint("\x1B[1m", text2);
17
+ var dim = (text2) => paint("\x1B[2m", text2);
18
+ var green = (text2) => paint("\x1B[32m", text2);
19
+ var yellow = (text2) => paint("\x1B[33m", text2);
20
+ var red = (text2) => paint("\x1B[31m", text2);
21
+ var cyan = (text2) => paint("\x1B[36m", text2);
22
+ var wordmark = (text2) => paint(truecolor ? "\x1B[1m\x1B[38;2;109;92;240m" : "\x1B[1m\x1B[38;5;99m", text2);
23
+ function banner() {
24
+ if (!useColor) {
25
+ process.stdout.write("superlore \u2014 the company knowledge base your agents run on.\n");
26
+ return;
27
+ }
28
+ const fold = `${accent("\u2588\u2588")}${accentSoft("\u2593\u2593")}`;
29
+ const lines = [
30
+ ` ${fold}`,
31
+ ` ${fold} ${wordmark("superlore")}`,
32
+ ` ${fold} ${dim("the company knowledge base your agents run on")}`,
33
+ ` ${fold}`
34
+ ];
35
+ process.stdout.write(`
36
+ ${lines.join("\n")}
37
+
38
+ `);
39
+ }
40
+ var log = {
41
+ info(message) {
42
+ process.stdout.write(`${message}
43
+ `);
44
+ },
45
+ step(message) {
46
+ process.stdout.write(`${accent("\u203A")} ${message}
47
+ `);
48
+ },
49
+ success(message) {
50
+ process.stdout.write(`${green("\u2713")} ${message}
51
+ `);
52
+ },
53
+ warn(message) {
54
+ process.stderr.write(`${yellow("!")} ${message}
55
+ `);
56
+ },
57
+ error(message) {
58
+ process.stderr.write(`${red("\u2717")} ${message}
59
+ `);
60
+ },
61
+ blank() {
62
+ process.stdout.write("\n");
63
+ }
64
+ };
65
+
66
+ // src/lib/project.ts
67
+ import { spawn } from "child_process";
68
+ import { existsSync, readFileSync } from "fs";
69
+ import { dirname, join, resolve } from "path";
70
+
71
+ // src/config.ts
72
+ var DEFAULT_MCP_PATH = "/api/mcp";
73
+ var SUPERLORE_JSON_FILENAME = "superlore.json";
74
+ var SUPERLORE_TYPES = ["company-kb", "product-docs"];
75
+ function isRecord(value) {
76
+ return typeof value === "object" && value !== null && !Array.isArray(value);
77
+ }
78
+ function validateSuperloreJson(input) {
79
+ const issues = [];
80
+ if (!isRecord(input)) {
81
+ return { ok: false, issues: [{ path: "", message: "must be a JSON object" }] };
82
+ }
83
+ const name = input.name;
84
+ if (typeof name !== "string" || name.trim().length === 0) {
85
+ issues.push({ path: "name", message: "is required and must be a non-empty string" });
86
+ }
87
+ const type = input.type;
88
+ if (typeof type !== "string" || !SUPERLORE_TYPES.includes(type)) {
89
+ issues.push({
90
+ path: "type",
91
+ message: `is required and must be one of ${SUPERLORE_TYPES.map((t) => `"${t}"`).join(" | ")}`
92
+ });
93
+ }
94
+ const accent2 = input.accent;
95
+ if (accent2 !== void 0 && (typeof accent2 !== "string" || accent2.trim().length === 0)) {
96
+ issues.push({ path: "accent", message: "must be a non-empty string (a CSS colour)" });
97
+ }
98
+ let auth;
99
+ if (input.auth !== void 0) {
100
+ if (!isRecord(input.auth)) {
101
+ issues.push({ path: "auth", message: "must be an object" });
102
+ } else {
103
+ const a = input.auth;
104
+ if (typeof a.enabled !== "boolean") {
105
+ issues.push({ path: "auth.enabled", message: "is required and must be a boolean" });
106
+ }
107
+ if (a.provider !== void 0 && a.provider !== "google") {
108
+ issues.push({ path: "auth.provider", message: 'must be "google"' });
109
+ }
110
+ if (a.allowedDomain !== void 0 && (typeof a.allowedDomain !== "string" || a.allowedDomain.trim().length === 0)) {
111
+ issues.push({ path: "auth.allowedDomain", message: "must be a non-empty string" });
112
+ }
113
+ if (typeof a.enabled === "boolean") {
114
+ auth = {
115
+ enabled: a.enabled,
116
+ // Default the provider to Google when the gate is on.
117
+ provider: a.provider ?? (a.enabled ? "google" : void 0),
118
+ allowedDomain: typeof a.allowedDomain === "string" ? a.allowedDomain : void 0
119
+ };
120
+ }
121
+ }
122
+ }
123
+ let mcp;
124
+ if (input.mcp !== void 0) {
125
+ if (!isRecord(input.mcp)) {
126
+ issues.push({ path: "mcp", message: "must be an object" });
127
+ } else {
128
+ const m = input.mcp;
129
+ if (typeof m.enabled !== "boolean") {
130
+ issues.push({ path: "mcp.enabled", message: "is required and must be a boolean" });
131
+ }
132
+ if (m.path !== void 0 && (typeof m.path !== "string" || !m.path.startsWith("/"))) {
133
+ issues.push({ path: "mcp.path", message: 'must be a string starting with "/"' });
134
+ }
135
+ if (typeof m.enabled === "boolean") {
136
+ mcp = {
137
+ enabled: m.enabled,
138
+ path: typeof m.path === "string" ? m.path : m.enabled ? DEFAULT_MCP_PATH : void 0
139
+ };
140
+ }
141
+ }
142
+ }
143
+ if (issues.length > 0) {
144
+ return { ok: false, issues };
145
+ }
146
+ const value = {
147
+ name: name.trim(),
148
+ type
149
+ };
150
+ if (typeof accent2 === "string") value.accent = accent2.trim();
151
+ if (auth) value.auth = auth;
152
+ if (mcp) value.mcp = mcp;
153
+ return { ok: true, value };
154
+ }
155
+ function parseSuperloreJson(raw) {
156
+ let data;
157
+ try {
158
+ data = JSON.parse(raw);
159
+ } catch (error) {
160
+ const message = error instanceof Error ? error.message : "invalid JSON";
161
+ return { ok: false, issues: [{ path: "", message: `not valid JSON: ${message}` }] };
162
+ }
163
+ return validateSuperloreJson(data);
164
+ }
165
+ function serializeSuperloreJson(config) {
166
+ return `${JSON.stringify(config, null, 2)}
167
+ `;
168
+ }
169
+ function resolveMcpPath(config) {
170
+ if (!config.mcp || !config.mcp.enabled) return void 0;
171
+ return config.mcp.path ?? DEFAULT_MCP_PATH;
172
+ }
173
+
174
+ // src/lib/project.ts
175
+ function findProjectRoot(start = process.cwd()) {
176
+ let dir = resolve(start);
177
+ for (; ; ) {
178
+ if (existsSync(join(dir, SUPERLORE_JSON_FILENAME))) return dir;
179
+ const parent = dirname(dir);
180
+ if (parent === dir) return void 0;
181
+ dir = parent;
182
+ }
183
+ }
184
+ var LoadProjectError = class extends Error {
185
+ };
186
+ function loadProject(start = process.cwd()) {
187
+ const root = findProjectRoot(start);
188
+ if (!root) {
189
+ throw new LoadProjectError(
190
+ `No ${SUPERLORE_JSON_FILENAME} found here or in any parent directory. Run \`superlore init\` to scaffold a KB, or cd into one.`
191
+ );
192
+ }
193
+ const file = join(root, SUPERLORE_JSON_FILENAME);
194
+ const result = parseSuperloreJson(readFileSync(file, "utf8"));
195
+ if (!result.ok) {
196
+ const lines = result.issues.map((i) => ` - ${i.path ? `${i.path} ` : ""}${i.message}`);
197
+ throw new LoadProjectError(`Invalid ${SUPERLORE_JSON_FILENAME}:
198
+ ${lines.join("\n")}`);
199
+ }
200
+ return { root, config: result.value };
201
+ }
202
+ function detectPackageManager(root) {
203
+ if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
204
+ if (existsSync(join(root, "yarn.lock"))) return "yarn";
205
+ if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock"))) return "bun";
206
+ if (existsSync(join(root, "package-lock.json"))) return "npm";
207
+ return "pnpm";
208
+ }
209
+ function isInstalled(root) {
210
+ return existsSync(join(root, "node_modules"));
211
+ }
212
+ function runScript(root, script, extraArgs = []) {
213
+ const pm = detectPackageManager(root);
214
+ const args = ["run", script, ...extraArgs.length ? ["--", ...extraArgs] : []];
215
+ return new Promise((resolvePromise, reject) => {
216
+ const child = spawn(pm, args, {
217
+ cwd: root,
218
+ stdio: "inherit",
219
+ shell: process.platform === "win32"
220
+ });
221
+ child.on("error", reject);
222
+ child.on("close", (code) => resolvePromise(code ?? 0));
223
+ });
224
+ }
225
+
226
+ // src/commands/build.ts
227
+ async function buildCommand() {
228
+ let project;
229
+ try {
230
+ project = loadProject();
231
+ } catch (error) {
232
+ if (error instanceof LoadProjectError) {
233
+ log.error(error.message);
234
+ process.exit(1);
235
+ }
236
+ throw error;
237
+ }
238
+ if (project.config.type === "company-kb" && !project.config.auth?.enabled) {
239
+ log.warn(
240
+ `Building a company KB with auth disabled \u2014 anyone with the URL can read it. Enable ${cyan("auth in superlore.json")} before deploying.`
241
+ );
242
+ log.blank();
243
+ }
244
+ if (!isInstalled(project.root)) {
245
+ log.error(
246
+ `Dependencies aren't installed. Run ${cyan("pnpm install")} (or npm / yarn / bun) first.`
247
+ );
248
+ process.exit(1);
249
+ }
250
+ log.step(`Building ${accent(project.config.name)} for production`);
251
+ log.blank();
252
+ const code = await runScript(project.root, "build");
253
+ process.exit(code);
254
+ }
255
+
256
+ // src/commands/deploy.ts
257
+ import { spawn as spawn2 } from "child_process";
258
+
259
+ // src/lib/constants.ts
260
+ var SUPERLORE_SITE = (process.env.SUPERLORE_SITE ?? "https://superlore.vercel.app").replace(/\/$/, "");
261
+ var CLOUD_WAITLIST_URL = `${SUPERLORE_SITE}/cloud`;
262
+ var SUPERLORE_VIOLET = "#6D5CF0";
263
+
264
+ // src/commands/deploy.ts
265
+ function openInBrowser(url) {
266
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
267
+ try {
268
+ const child = spawn2(cmd, [url], {
269
+ stdio: "ignore",
270
+ detached: true,
271
+ shell: process.platform === "win32"
272
+ });
273
+ child.on("error", () => {
274
+ });
275
+ child.unref();
276
+ } catch {
277
+ }
278
+ }
279
+ async function deployCommand(flags) {
280
+ log.blank();
281
+ log.info(`${accent("superlore Cloud")} ${dim("\u2014 managed deploy")}`);
282
+ log.blank();
283
+ log.info(
284
+ `Managed deploy is in ${bold("private beta")} \u2014 one-click superlore Cloud hosting isn't open to`
285
+ );
286
+ log.info(`everyone yet.`);
287
+ log.blank();
288
+ log.info(`${bold("Join the waitlist:")} ${cyan(CLOUD_WAITLIST_URL)}`);
289
+ log.blank();
290
+ log.info(`${dim("In the meantime, superlore is open source \u2014 self-host free:")}`);
291
+ log.info(
292
+ ` ${accent("\u203A")} ${cyan("superlore build")} ${dim("then deploy the app to Vercel or any Next.js host.")}`
293
+ );
294
+ log.blank();
295
+ if (flags.open) {
296
+ log.step(`Opening ${cyan(CLOUD_WAITLIST_URL)} \u2026`);
297
+ openInBrowser(CLOUD_WAITLIST_URL);
298
+ }
299
+ process.exit(0);
300
+ }
301
+
302
+ // src/commands/dev.ts
303
+ async function devCommand(flags) {
304
+ let project;
305
+ try {
306
+ project = loadProject();
307
+ } catch (error) {
308
+ if (error instanceof LoadProjectError) {
309
+ log.error(error.message);
310
+ process.exit(1);
311
+ }
312
+ throw error;
313
+ }
314
+ if (!isInstalled(project.root)) {
315
+ log.error(
316
+ `Dependencies aren't installed. Run ${cyan("pnpm install")} (or npm / yarn / bun) first.`
317
+ );
318
+ process.exit(1);
319
+ }
320
+ const port = flags.port ?? 3e3;
321
+ log.step(`Starting ${accent(project.config.name)} dev server`);
322
+ log.info(` ${dim("Local:")} ${cyan(`http://localhost:${port}`)}`);
323
+ if (project.config.mcp?.enabled) {
324
+ log.info(
325
+ ` ${dim("MCP: ")} ${cyan(`http://localhost:${port}${project.config.mcp.path ?? "/api/mcp"}`)}`
326
+ );
327
+ }
328
+ log.blank();
329
+ const args = flags.port ? ["--port", String(flags.port)] : [];
330
+ const code = await runScript(project.root, "dev", args);
331
+ process.exit(code);
332
+ }
333
+
334
+ // src/commands/init.ts
335
+ import { existsSync as existsSync3 } from "fs";
336
+ import { basename, resolve as resolve3 } from "path";
337
+ import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
338
+
339
+ // src/lib/scaffold.ts
340
+ import { cpSync, existsSync as existsSync2, mkdirSync, readdirSync, writeFileSync } from "fs";
341
+ import { fileURLToPath } from "url";
342
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
343
+ var here = dirname2(fileURLToPath(import.meta.url));
344
+ function findStarterTemplate() {
345
+ let dir = here;
346
+ for (; ; ) {
347
+ const candidate = join2(dir, "templates", "starter");
348
+ if (existsSync2(candidate)) return candidate;
349
+ const bundled = join2(dir, "template");
350
+ if (existsSync2(bundled)) return bundled;
351
+ const parent = dirname2(dir);
352
+ if (parent === dir) return void 0;
353
+ dir = parent;
354
+ }
355
+ }
356
+ function isUsableTemplate(dir) {
357
+ if (!existsSync2(dir)) return false;
358
+ const entries = readdirSync(dir).filter((name) => name !== "README.md" && !name.startsWith("."));
359
+ return entries.length > 0;
360
+ }
361
+ function isEmptyDir(dir) {
362
+ if (!existsSync2(dir)) return true;
363
+ return readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0;
364
+ }
365
+ function scaffold(options) {
366
+ const root = resolve2(options.dir);
367
+ mkdirSync(root, { recursive: true });
368
+ const template = findStarterTemplate();
369
+ let source;
370
+ if (template && isUsableTemplate(template)) {
371
+ cpSync(template, root, { recursive: true });
372
+ source = "template";
373
+ } else {
374
+ writeSkeleton(root, options.config);
375
+ source = "skeleton";
376
+ }
377
+ writeFileSync(join2(root, "superlore.json"), serializeSuperloreJson(options.config), "utf8");
378
+ return { root, source };
379
+ }
380
+ function writeSkeleton(root, config) {
381
+ const accent2 = config.accent ?? SUPERLORE_VIOLET;
382
+ const mcpEnabled = config.mcp?.enabled ?? true;
383
+ const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "superlore-kb";
384
+ const write = (rel, body) => {
385
+ const file = join2(root, rel);
386
+ mkdirSync(dirname2(file), { recursive: true });
387
+ writeFileSync(file, body, "utf8");
388
+ };
389
+ write(
390
+ "package.json",
391
+ `${JSON.stringify(
392
+ {
393
+ name: slug,
394
+ version: "0.1.0",
395
+ private: true,
396
+ type: "module",
397
+ scripts: {
398
+ dev: "next dev",
399
+ build: "next build",
400
+ start: "next start",
401
+ postinstall: "fumadocs-mdx"
402
+ },
403
+ dependencies: {
404
+ "fumadocs-core": "16.8.2",
405
+ "fumadocs-mdx": "14.3.1",
406
+ "fumadocs-ui": "16.8.2",
407
+ superlore: "^0.1.0",
408
+ "lucide-react": "^1.21.0",
409
+ ...mcpEnabled ? { "@modelcontextprotocol/sdk": "^1.29.0", "mcp-handler": "^1.1.0" } : {},
410
+ next: "16.2.4",
411
+ "next-themes": "^0.4.6",
412
+ react: "^19.2.5",
413
+ "react-dom": "^19.2.5",
414
+ zod: "^4.4.3"
415
+ },
416
+ devDependencies: {
417
+ "@tailwindcss/postcss": "^4.2.2",
418
+ "@types/node": "^25.6.0",
419
+ "@types/react": "^19.2.14",
420
+ "@types/react-dom": "^19.2.3",
421
+ postcss: "^8.5.10",
422
+ tailwindcss: "^4.2.2",
423
+ typescript: "6.0.3"
424
+ }
425
+ },
426
+ null,
427
+ 2
428
+ )}
429
+ `
430
+ );
431
+ write(
432
+ "tsconfig.json",
433
+ `${JSON.stringify(
434
+ {
435
+ compilerOptions: {
436
+ target: "ES2022",
437
+ lib: ["dom", "dom.iterable", "esnext"],
438
+ module: "esnext",
439
+ moduleResolution: "bundler",
440
+ strict: true,
441
+ noEmit: true,
442
+ jsx: "preserve",
443
+ esModuleInterop: true,
444
+ resolveJsonModule: true,
445
+ isolatedModules: true,
446
+ skipLibCheck: true,
447
+ incremental: true,
448
+ paths: { "@/*": ["./*"] },
449
+ plugins: [{ name: "next" }]
450
+ },
451
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
452
+ exclude: ["node_modules", ".next", "out"]
453
+ },
454
+ null,
455
+ 2
456
+ )}
457
+ `
458
+ );
459
+ write(
460
+ "next.config.mjs",
461
+ `import { createMDX } from "fumadocs-mdx/next";
462
+
463
+ const withMDX = createMDX();
464
+
465
+ /** @type {import('next').NextConfig} */
466
+ const config = {
467
+ reactStrictMode: true,
468
+ // \`superlore\` ships source (.ts/.tsx) for frictionless consumption \u2014 Next transpiles it.
469
+ transpilePackages: ["superlore"],
470
+ };
471
+
472
+ export default withMDX(config);
473
+ `
474
+ );
475
+ write(
476
+ "source.config.ts",
477
+ `import { defineConfig, defineDocs } from "fumadocs-mdx/config";
478
+ import { superloreFrontmatterSchema } from "superlore/frontmatter";
479
+
480
+ // Extend the superlore frontmatter schema in ONE place if you add custom fields.
481
+ export const docs = defineDocs({
482
+ dir: "content/docs",
483
+ docs: { schema: superloreFrontmatterSchema },
484
+ });
485
+
486
+ export default defineConfig();
487
+ `
488
+ );
489
+ write(
490
+ "postcss.config.mjs",
491
+ `const config = {
492
+ plugins: { "@tailwindcss/postcss": {} },
493
+ };
494
+
495
+ export default config;
496
+ `
497
+ );
498
+ write(
499
+ "app/global.css",
500
+ `@import "tailwindcss";
501
+ /* superlore theme tokens (light + dark, co-equal). Re-skin the whole KB from one accent. */
502
+ @import "superlore/css";
503
+
504
+ :root {
505
+ /* Brand accent \u2014 superlore derives the hover/weak/border/text family for both themes. */
506
+ --kp-accent: ${accent2};
507
+ }
508
+ `
509
+ );
510
+ write(
511
+ "app/layout.tsx",
512
+ `import type { ReactNode } from "react";
513
+ import { RootProvider } from "fumadocs-ui/provider";
514
+ import "./global.css";
515
+
516
+ export default function RootLayout({ children }: { children: ReactNode }) {
517
+ // Light and dark are co-equal; default to the system preference.
518
+ return (
519
+ <html lang="en" suppressHydrationWarning>
520
+ <body>
521
+ <RootProvider>{children}</RootProvider>
522
+ </body>
523
+ </html>
524
+ );
525
+ }
526
+ `
527
+ );
528
+ write(
529
+ "app/page.tsx",
530
+ `import Link from "next/link";
531
+
532
+ export default function HomePage() {
533
+ return (
534
+ <main style={{ maxWidth: "42rem", margin: "0 auto", padding: "6rem 1.5rem" }}>
535
+ <h1 style={{ fontSize: "2rem", fontWeight: 700 }}>${escapeForJsx(config.name)}</h1>
536
+ <p style={{ marginTop: "0.75rem", opacity: 0.7 }}>
537
+ One corpus. Humans and agents. Start in <Link href="/docs">the docs</Link>.
538
+ </p>
539
+ </main>
540
+ );
541
+ }
542
+ `
543
+ );
544
+ write(
545
+ "app/docs/layout.tsx",
546
+ `import type { ReactNode } from "react";
547
+ import { DocsLayout } from "fumadocs-ui/layouts/docs";
548
+ import { source } from "@/lib/source";
549
+
550
+ export default function Layout({ children }: { children: ReactNode }) {
551
+ return (
552
+ <DocsLayout tree={source.pageTree} nav={{ title: "${escapeForJsx(config.name)}" }}>
553
+ {children}
554
+ </DocsLayout>
555
+ );
556
+ }
557
+ `
558
+ );
559
+ write(
560
+ "app/docs/[[...slug]]/page.tsx",
561
+ `import { notFound } from "next/navigation";
562
+ import { DocsBody, DocsPage } from "fumadocs-ui/page";
563
+ import { getMDXComponents } from "superlore";
564
+ import { source } from "@/lib/source";
565
+
566
+ export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
567
+ const params = await props.params;
568
+ const page = source.getPage(params.slug);
569
+ if (!page) notFound();
570
+ const MDX = page.data.body;
571
+ return (
572
+ <DocsPage toc={page.data.toc}>
573
+ <DocsBody>
574
+ <MDX components={getMDXComponents()} />
575
+ </DocsBody>
576
+ </DocsPage>
577
+ );
578
+ }
579
+
580
+ export function generateStaticParams() {
581
+ return source.generateParams();
582
+ }
583
+ `
584
+ );
585
+ write(
586
+ "lib/source.ts",
587
+ `import { loader } from "fumadocs-core/source";
588
+ import { docs } from "@/.source";
589
+
590
+ export const source = loader({
591
+ baseUrl: "/docs",
592
+ source: docs.toFumadocsSource(),
593
+ });
594
+ `
595
+ );
596
+ write(
597
+ "content/docs/index.mdx",
598
+ `---
599
+ title: Welcome
600
+ description: The home of your superlore knowledge base.
601
+ summary: Landing page for the ${config.name} knowledge base.
602
+ tags: [getting-started]
603
+ ---
604
+
605
+ # Welcome to ${config.name}
606
+
607
+ This is your superlore KB. Author once in MDX \u2014 humans get this clean, interactive site, and
608
+ agents read the same structured content over MCP.
609
+
610
+ Edit \`content/docs/index.mdx\` to make it yours, then run \`superlore dev\`.
611
+ `
612
+ );
613
+ if (mcpEnabled) {
614
+ const mcpPath = config.mcp?.path ?? "/api/mcp";
615
+ write(
616
+ "app/api/[transport]/route.ts",
617
+ `import { createMcpHandler } from "mcp-handler";
618
+ import { z } from "zod";
619
+ import { getComponentData, getPage, list, navigate, search } from "superlore/mcp";
620
+
621
+ // Your KB's MCP endpoint. Served at ${mcpPath} \u2014 the same structured content the site renders,
622
+ // exposed to agents. Build the index from your content source and pass it to each tool.
623
+ // See the superlore docs (Agents & MCP) for wiring the index from \`source\`.
624
+
625
+ const json = (data: unknown) => ({
626
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
627
+ });
628
+
629
+ const handler = createMcpHandler(
630
+ (server) => {
631
+ server.tool(
632
+ "search",
633
+ "Full-text search across the knowledge base.",
634
+ { query: z.string(), limit: z.number().int().positive().optional() },
635
+ async ({ query, limit }) => json(search(/* index */ {} as never, query, limit)),
636
+ );
637
+ server.tool(
638
+ "get_page",
639
+ "Get a page's full structured content by path.",
640
+ { path: z.string() },
641
+ async ({ path }) => json(getPage(/* index */ {} as never, path)),
642
+ );
643
+ server.tool(
644
+ "list",
645
+ "List knowledge nodes, filtered by kind / tag / entityType.",
646
+ { kind: z.string().optional(), tag: z.string().optional(), entityType: z.string().optional() },
647
+ async (args) => json(list(/* index */ {} as never, args)),
648
+ );
649
+ server.tool(
650
+ "navigate",
651
+ "Follow relations from a page path / node id / entity ref.",
652
+ { target: z.string() },
653
+ async ({ target }) => json(navigate(/* index */ {} as never, target)),
654
+ );
655
+ server.tool(
656
+ "get_component_data",
657
+ "Get the structured data behind a rendered component (its knowledge face).",
658
+ { id: z.string() },
659
+ async ({ id }) => json(getComponentData(/* index */ {} as never, id)),
660
+ );
661
+ },
662
+ {},
663
+ { basePath: "/api" },
664
+ );
665
+
666
+ export { handler as GET, handler as POST };
667
+ `
668
+ );
669
+ }
670
+ write(
671
+ ".gitignore",
672
+ `node_modules
673
+ .next
674
+ .source
675
+ out
676
+ *.tsbuildinfo
677
+ .env*.local
678
+ `
679
+ );
680
+ write(
681
+ "README.md",
682
+ `# ${config.name}
683
+
684
+ A superlore knowledge base. One corpus. Humans and agents.
685
+
686
+ ## Develop
687
+
688
+ \`\`\`sh
689
+ pnpm install # or npm / yarn / bun
690
+ superlore dev # preview the site locally
691
+ \`\`\`
692
+
693
+ ## Build
694
+
695
+ \`\`\`sh
696
+ superlore build
697
+ \`\`\`
698
+
699
+ Config lives in \`superlore.json\`. Author content in \`content/docs/\`.${mcpEnabled ? `
700
+
701
+ The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}
702
+ `
703
+ );
704
+ }
705
+ function escapeForJsx(value) {
706
+ return value.replace(/[\\"]/g, "\\$&").replace(/[{}]/g, "");
707
+ }
708
+
709
+ // src/commands/init.ts
710
+ function bail(message) {
711
+ cancel(message);
712
+ process.exit(1);
713
+ }
714
+ async function initCommand(dir, flags) {
715
+ banner();
716
+ intro(`${accent("superlore")} ${dim("init")}`);
717
+ const interactive = !flags.yes && process.stdout.isTTY;
718
+ let name = flags.name?.trim() || dir?.trim();
719
+ if (!name && interactive) {
720
+ const answer = await text({
721
+ message: "What's your knowledge base called?",
722
+ placeholder: "Acme Knowledge Base",
723
+ validate: (v) => (v ?? "").trim().length === 0 ? "A name is required." : void 0
724
+ });
725
+ if (isCancel(answer)) bail("Cancelled.");
726
+ name = answer.trim();
727
+ }
728
+ if (!name) bail("A name is required (pass --name, a [dir] argument, or run interactively).");
729
+ let type = flags.type;
730
+ if (type && type !== "company-kb" && type !== "product-docs") {
731
+ bail(`Invalid --type "${type}". Use "company-kb" or "product-docs".`);
732
+ }
733
+ if (!type && interactive) {
734
+ const answer = await select({
735
+ message: "What kind of KB is this?",
736
+ options: [
737
+ {
738
+ value: "company-kb",
739
+ label: "Company KB",
740
+ hint: "internal / private \u2014 should be gated"
741
+ },
742
+ {
743
+ value: "product-docs",
744
+ label: "Product docs",
745
+ hint: "public-facing documentation"
746
+ }
747
+ ]
748
+ });
749
+ if (isCancel(answer)) bail("Cancelled.");
750
+ type = answer;
751
+ }
752
+ if (!type) bail("A type is required (pass --type or run interactively).");
753
+ let authEnabled;
754
+ if (flags.auth !== void 0) {
755
+ authEnabled = flags.auth;
756
+ } else if (flags.allowedDomain) {
757
+ authEnabled = true;
758
+ } else if (interactive) {
759
+ const answer = await confirm({
760
+ message: "Gate the site behind Google SSO (auth)?",
761
+ initialValue: type === "company-kb"
762
+ });
763
+ if (isCancel(answer)) bail("Cancelled.");
764
+ authEnabled = answer;
765
+ } else {
766
+ authEnabled = type === "company-kb";
767
+ }
768
+ let allowedDomain = flags.allowedDomain?.trim();
769
+ if (authEnabled && !allowedDomain && interactive) {
770
+ const answer = await text({
771
+ message: "Restrict sign-in to an email domain? (optional)",
772
+ placeholder: "acme.com \u2014 leave blank for any Google account"
773
+ });
774
+ if (isCancel(answer)) bail("Cancelled.");
775
+ allowedDomain = answer.trim() || void 0;
776
+ }
777
+ const accentColor = flags.accent?.trim();
778
+ const mcpEnabled = flags.mcp ?? true;
779
+ const auth = authEnabled ? { enabled: true, provider: "google", ...allowedDomain ? { allowedDomain } : {} } : void 0;
780
+ const draft = { name, type };
781
+ if (accentColor) draft.accent = accentColor;
782
+ if (auth) draft.auth = auth;
783
+ draft.mcp = { enabled: mcpEnabled, ...mcpEnabled ? { path: DEFAULT_MCP_PATH } : {} };
784
+ const result = validateSuperloreJson(draft);
785
+ if (!result.ok) {
786
+ bail(
787
+ `Could not build a valid superlore.json:
788
+ ${result.issues.map((i) => ` - ${i.path} ${i.message}`).join("\n")}`
789
+ );
790
+ }
791
+ const config = result.value;
792
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
793
+ const targetName = dir ?? (slug || "superlore-kb");
794
+ const targetDir = resolve3(process.cwd(), targetName);
795
+ if (existsSync3(targetDir) && !isEmptyDir(targetDir)) {
796
+ if (interactive) {
797
+ const proceed = await confirm({
798
+ message: `${cyan(targetDir)} is not empty. Scaffold into it anyway?`,
799
+ initialValue: false
800
+ });
801
+ if (isCancel(proceed) || !proceed) bail("Cancelled \u2014 pick an empty directory.");
802
+ } else {
803
+ bail(`${targetDir} is not empty. Pick an empty directory.`);
804
+ }
805
+ }
806
+ const { root, source } = scaffold({ dir: targetDir, config });
807
+ outro(`${bold("Scaffolded")} ${config.name} ${dim(`(${source})`)}`);
808
+ if (config.type === "company-kb" && !config.auth?.enabled) {
809
+ log.blank();
810
+ log.warn(
811
+ `This is a ${bold("company KB")} but auth is ${bold("not enabled")}. A company KB should gate access before you deploy \u2014 re-run with ${cyan("--auth")} or set ${cyan('"auth": { "enabled": true }')} in superlore.json.`
812
+ );
813
+ }
814
+ printNextSteps(root, config);
815
+ }
816
+ function printNextSteps(root, config) {
817
+ const rel = basename(root);
818
+ const accentNote = config.accent ?? SUPERLORE_VIOLET;
819
+ log.blank();
820
+ log.info(bold("Next steps"));
821
+ log.info(` ${accent("1.")} ${cyan(`cd ${rel}`)}`);
822
+ log.info(` ${accent("2.")} ${cyan("pnpm install")} ${dim("(or npm / yarn / bun)")}`);
823
+ log.info(` ${accent("3.")} ${cyan("superlore dev")} ${dim("\u2014 preview the site locally")}`);
824
+ log.blank();
825
+ log.info(`${dim("Config:")} ${cyan("superlore.json")} ${dim("Brand accent:")} ${accentNote}`);
826
+ if (config.mcp?.enabled) {
827
+ log.info(`${dim("MCP endpoint:")} ${config.mcp.path ?? DEFAULT_MCP_PATH}`);
828
+ }
829
+ }
830
+
831
+ // src/index.ts
832
+ var VERSION = "0.1.0";
833
+ function buildCli(argv = process2.argv) {
834
+ const cli = cac("superlore");
835
+ cli.command("init [dir]", "Scaffold a new superlore knowledge base").option("--name <name>", "KB name").option("--type <type>", "KB type: company-kb | product-docs").option("--auth", "Enable the Google SSO auth gate").option("--no-auth", "Disable the auth gate").option("--allowed-domain <domain>", "Restrict SSO to one email domain (implies --auth)").option("--accent <color>", "Brand accent colour (any CSS colour)").option("--no-mcp", "Disable the MCP endpoint (on by default)").option("-y, --yes", "Skip prompts; use flags + defaults").example("superlore init my-kb --type product-docs").example("superlore init acme --type company-kb --auth --allowed-domain acme.com").action(
836
+ async (dir, flags) => {
837
+ const authExplicit = argv.includes("--auth");
838
+ const noAuthExplicit = argv.includes("--no-auth");
839
+ const auth = authExplicit ? true : noAuthExplicit ? false : void 0;
840
+ await initCommand(dir, {
841
+ name: flags.name,
842
+ type: flags.type,
843
+ auth,
844
+ allowedDomain: flags.allowedDomain,
845
+ accent: flags.accent,
846
+ // `--no-mcp` flips this to false; default true is the intended behaviour.
847
+ mcp: flags.mcp,
848
+ yes: flags.yes
849
+ });
850
+ }
851
+ );
852
+ cli.command("dev", "Run the local superlore site for preview").option("--port <port>", "Port for the dev server", { default: 3e3 }).action(async (flags) => {
853
+ await devCommand({ port: flags.port });
854
+ });
855
+ cli.command("build", "Production build of the KB").action(async () => {
856
+ await buildCommand();
857
+ });
858
+ cli.command("deploy", "Managed deploy (superlore Cloud) \u2014 private beta, joins the waitlist").option("--open", "Open the waitlist URL in your browser").action(async (flags) => {
859
+ await deployCommand({ open: flags.open });
860
+ });
861
+ cli.help();
862
+ cli.version(VERSION);
863
+ return cli;
864
+ }
865
+ async function run(argv = process2.argv) {
866
+ const cli = buildCli(argv);
867
+ const tokens = argv.slice(2);
868
+ const hasCommand = tokens.some((t) => !t.startsWith("-"));
869
+ const wantsVersion = tokens.includes("-v") || tokens.includes("--version");
870
+ if (!hasCommand && !wantsVersion) banner();
871
+ try {
872
+ cli.parse([...argv], { run: false });
873
+ if (!cli.matchedCommand && !cli.options.version) {
874
+ if (!cli.options.help) cli.outputHelp();
875
+ return;
876
+ }
877
+ await cli.runMatchedCommand();
878
+ } catch (error) {
879
+ const message = error instanceof Error ? error.message : String(error);
880
+ log.error(message);
881
+ process2.exit(1);
882
+ }
883
+ }
884
+ var isEntrypoint = Boolean(process2.argv[1]) && fileURLToPath2(import.meta.url) === process2.argv[1];
885
+ if (isEntrypoint) {
886
+ void run();
887
+ }
888
+ export {
889
+ DEFAULT_MCP_PATH,
890
+ SUPERLORE_JSON_FILENAME,
891
+ SUPERLORE_TYPES,
892
+ VERSION,
893
+ buildCli,
894
+ parseSuperloreJson,
895
+ resolveMcpPath,
896
+ run,
897
+ serializeSuperloreJson,
898
+ validateSuperloreJson
899
+ };