superlore-cli 0.3.0 → 0.4.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 CHANGED
@@ -27,10 +27,10 @@ function banner() {
27
27
  }
28
28
  const seam = dim("\u258F");
29
29
  const lines = [
30
- ` ${accent("\u2597\u259F\u2588")}${accentSoft("\u2599\u2596")}`,
31
- ` ${accent("\u2590\u2588")}${seam}${accentSoft("\u2592\u258C")} ${wordmark("superlore")}`,
32
- ` ${accent("\u2590\u2588")}${seam}${accentSoft("\u2592\u258C")} ${dim("the company knowledge base your agents run on")}`,
33
- ` ${accent("\u259D\u259C\u2588")}${accentSoft("\u259B\u2598")}`
30
+ ` ${accent("\u2597\u2584")}${seam}${accentSoft("\u2584\u2596")}`,
31
+ ` ${accent("\u2588\u2588")}${seam}${accentSoft("\u2588\u2588")} ${wordmark("superlore")}`,
32
+ ` ${accent("\u2588\u2588")}${seam}${accentSoft("\u2588\u2588")} ${dim("the company knowledge base your agents run on")}`,
33
+ ` ${accent("\u259D\u2580")}${seam}${accentSoft("\u2580\u2598")}`
34
34
  ];
35
35
  process.stdout.write(`
36
36
  ${lines.join("\n")}
@@ -213,14 +213,15 @@ function detectPackageManager(root) {
213
213
  function isInstalled(root) {
214
214
  return existsSync(join(root, "node_modules"));
215
215
  }
216
- function runScript(root, script, extraArgs = []) {
216
+ function runScript(root, script, extraArgs = [], env) {
217
217
  const pm = detectPackageManager(root);
218
218
  const args = ["run", script, ...extraArgs.length ? ["--", ...extraArgs] : []];
219
219
  return new Promise((resolvePromise, reject) => {
220
220
  const child = spawn(pm, args, {
221
221
  cwd: root,
222
222
  stdio: "inherit",
223
- shell: process.platform === "win32"
223
+ shell: process.platform === "win32",
224
+ env: env ? { ...process.env, ...env } : process.env
224
225
  });
225
226
  child.on("error", reject);
226
227
  child.on("close", (code) => resolvePromise(code ?? 0));
@@ -417,7 +418,9 @@ function report(result) {
417
418
  log.success(`${bold(result.editor.label)} ${dim("\u2014 extension installed.")}`);
418
419
  break;
419
420
  case "already-installed":
420
- log.info(`${accent("\u203A")} ${bold(result.editor.label)} ${dim("\u2014 already installed, up to date.")}`);
421
+ log.info(
422
+ `${accent("\u203A")} ${bold(result.editor.label)} ${dim("\u2014 already installed, up to date.")}`
423
+ );
421
424
  break;
422
425
  case "failed":
423
426
  log.error(`${bold(result.editor.label)} ${dim("\u2014 install failed:")} ${result.error}`);
@@ -437,12 +440,15 @@ function printManualInstall() {
437
440
  function printMcpNextStep() {
438
441
  log.blank();
439
442
  log.info(bold("Next: connect the MCP"));
443
+ log.info(` ${dim("superlore's docs + help, in your agent \u2014 always current:")}`);
440
444
  log.info(
441
- ` ${dim("Let your agent read the same corpus. Ask Claude")} ${cyan('"connect my superlore MCP"')}${dim(",")}`
445
+ ` ${cyan("claude mcp add --transport http -s user superlore-docs https://superlore.vercel.app/api/mcp")}`
442
446
  );
447
+ log.blank();
443
448
  log.info(
444
- ` ${dim("or register it yourself:")} ${cyan("claude mcp add --transport http -s user superlore <url>/api/mcp")}`
449
+ ` ${dim("And your own KB once it's live (or ask Claude")} ${cyan('"connect my superlore MCP"')}${dim("):")}`
445
450
  );
451
+ log.info(` ${cyan("claude mcp add --transport http -s user my-kb <your-kb-url>/api/mcp")}`);
446
452
  }
447
453
 
448
454
  // src/commands/deploy.ts
@@ -518,108 +524,30 @@ async function devCommand(flags) {
518
524
  );
519
525
  }
520
526
  log.blank();
521
- const args = flags.port ? ["--port", String(flags.port)] : [];
522
- const code = await runScript(project.root, "dev", args);
527
+ const code = await runScript(project.root, "dev", [], { PORT: String(port) });
523
528
  process.exit(code);
524
529
  }
525
530
 
526
531
  // src/commands/init.ts
527
- import { existsSync as existsSync4 } from "fs";
528
- import { basename, resolve as resolve3 } from "path";
532
+ import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
533
+ import { basename, join as join4, resolve as resolve3 } from "path";
529
534
  import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
530
535
 
531
536
  // src/lib/scaffold.ts
532
537
  import { cpSync, existsSync as existsSync3, mkdirSync, readdirSync, writeFileSync as writeFileSync2 } from "fs";
533
538
  import { fileURLToPath } from "url";
534
539
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
535
- var here = dirname2(fileURLToPath(import.meta.url));
536
- function findStarterTemplate() {
537
- let dir = here;
538
- for (; ; ) {
539
- const candidate = join3(dir, "templates", "starter");
540
- if (existsSync3(candidate)) return candidate;
541
- const bundled = join3(dir, "template");
542
- if (existsSync3(bundled)) return bundled;
543
- const parent = dirname2(dir);
544
- if (parent === dir) return void 0;
545
- dir = parent;
546
- }
547
- }
548
- function isUsableTemplate(dir) {
549
- if (!existsSync3(dir)) return false;
550
- const entries = readdirSync(dir).filter((name) => name !== "README.md" && !name.startsWith("."));
551
- return entries.length > 0;
552
- }
553
- function isEmptyDir(dir) {
554
- if (!existsSync3(dir)) return true;
555
- return readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0;
556
- }
557
- function scaffold(options) {
558
- const root = resolve2(options.dir);
559
- mkdirSync(root, { recursive: true });
560
- const template = findStarterTemplate();
561
- let source;
562
- if (template && isUsableTemplate(template)) {
563
- cpSync(template, root, { recursive: true });
564
- source = "template";
565
- } else {
566
- writeSkeleton(root, options.config);
567
- source = "skeleton";
568
- }
569
- writeFileSync2(join3(root, "superlore.json"), serializeSuperloreJson(options.config), "utf8");
570
- return { root, source };
571
- }
572
- function writeSkeleton(root, config) {
573
- const accent2 = config.accent ?? SUPERLORE_VIOLET;
574
- const mcpEnabled = config.mcp?.enabled ?? true;
575
- const authEnabled = config.auth?.enabled ?? false;
576
- const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "superlore-kb";
577
- const write = (rel, body) => {
578
- const file = join3(root, rel);
579
- mkdirSync(dirname2(file), { recursive: true });
580
- writeFileSync2(file, body, "utf8");
581
- };
540
+
541
+ // src/lib/content/company.ts
542
+ function writeCompanyContent(write, config) {
582
543
  write(
583
- "package.json",
544
+ "content/docs/meta.json",
584
545
  `${JSON.stringify(
585
546
  {
586
- name: slug,
587
- version: "0.1.0",
588
- private: true,
589
- type: "module",
590
- scripts: {
591
- dev: "next dev",
592
- build: "next build",
593
- start: "next start",
594
- postinstall: "fumadocs-mdx"
595
- },
596
- dependencies: {
597
- "fumadocs-core": "16.8.2",
598
- "fumadocs-mdx": "14.3.1",
599
- "fumadocs-ui": "16.8.2",
600
- superlore: "^0.5.1",
601
- "lucide-react": "^1.21.0",
602
- // superlore peers the rendered components pull in: Mermaid (Diagram), themes.
603
- mermaid: "^11.15.0",
604
- ...mcpEnabled ? { "@modelcontextprotocol/sdk": "^1.29.0", "mcp-handler": "^1.1.0" } : {},
605
- // Auth.js v5 powers the optional Google SSO gate (superlore/auth). Self-disabling
606
- // without AUTH_GOOGLE_ID, so it's harmless until the env is set.
607
- ...authEnabled ? { "next-auth": "^5.0.0-beta.25" } : {},
608
- next: "16.2.4",
609
- "next-themes": "^0.4.6",
610
- react: "^19.2.5",
611
- "react-dom": "^19.2.5",
612
- zod: "^4.4.3"
613
- },
614
- devDependencies: {
615
- "@tailwindcss/postcss": "^4.2.2",
616
- "@types/node": "^25.6.0",
617
- "@types/react": "^19.2.14",
618
- "@types/react-dom": "^19.2.3",
619
- postcss: "^8.5.10",
620
- tailwindcss: "^4.2.2",
621
- typescript: "6.0.3"
622
- }
547
+ title: config.name,
548
+ root: true,
549
+ icon: "BookOpen",
550
+ pages: ["index", "engineering", "product", "team"]
623
551
  },
624
552
  null,
625
553
  2
@@ -627,408 +555,830 @@ function writeSkeleton(root, config) {
627
555
  `
628
556
  );
629
557
  write(
630
- "tsconfig.json",
558
+ "content/docs/index.mdx",
559
+ `---
560
+ title: Home
561
+ description: The home of the ${config.name} knowledge base \u2014 for humans and the agents working beside them.
562
+ summary: Overview of the ${config.name} company knowledge base \u2014 what lives here, how it's organized, and how an agent should use it to answer questions and act on the team's behalf.
563
+ tags: [overview, home, getting-started]
564
+ ---
565
+
566
+ <PageHero
567
+ kicker="Company knowledge base"
568
+ title="${config.name}"
569
+ description="One corpus, two readers. Humans get this clean, navigable site; agents read the same structured content over MCP and act the way the team would."
570
+ icon="book-open"
571
+ />
572
+
573
+ <Note title="This is a starter structure">
574
+ Everything here is **placeholder content** that shows the shape of a good company KB. Keep the
575
+ structure, then replace each page with what's true for your team. Run \`superlore dev\` to preview as
576
+ you go.
577
+ </Note>
578
+
579
+ ## What lives here
580
+
581
+ <FeatureList
582
+ items={[
583
+ { icon: "layers", title: "Engineering", description: "System architecture, the decisions behind it, and the runbooks that keep it up.", href: "/docs/engineering/architecture" },
584
+ { icon: "rocket", title: "Product", description: "The roadmap and what shipped \u2014 now, next, and later.", href: "/docs/product/roadmap" },
585
+ { icon: "users", title: "Team", description: "How to onboard, and who owns what.", href: "/docs/team/onboarding" },
586
+ ]}
587
+ />
588
+
589
+ ## How the company fits together
590
+
591
+ A map of the domains and how they depend on each other. Edit the canvas \u2014 the agent reads the same
592
+ graph the diagram is drawn from.
593
+
594
+ \`\`\`superlore-canvas
595
+ {
596
+ "title": "${config.name} \u2014 domains",
597
+ "layout": "flow",
598
+ "direction": "down",
599
+ "groups": [
600
+ { "id": "surface", "label": "Customer surface", "frame": true, "intent": "blue" },
601
+ { "id": "core", "label": "Core platform", "frame": true, "intent": "purple" },
602
+ { "id": "data", "label": "Data & insight", "frame": true, "intent": "green" },
603
+ { "id": "ops", "label": "Operations", "frame": true, "intent": "gray" }
604
+ ],
605
+ "nodes": [
606
+ { "id": "web", "kind": "rect", "intent": "blue", "group": "surface", "icon": "globe", "label": "Web app" },
607
+ { "id": "api", "kind": "rect", "intent": "blue", "group": "surface", "icon": "plug", "label": "Public API" },
608
+ { "id": "billing", "kind": "rect", "intent": "purple", "group": "core", "icon": "credit-card", "label": "Billing" },
609
+ { "id": "accounts", "kind": "rect", "intent": "purple", "group": "core", "icon": "users", "label": "Accounts" },
610
+ { "id": "workflow", "kind": "rect", "intent": "purple", "group": "core", "icon": "workflow", "label": "Workflow engine" },
611
+ { "id": "warehouse", "kind": "cylinder", "intent": "green", "group": "data", "label": "Warehouse" },
612
+ { "id": "insights", "kind": "rect", "intent": "green", "group": "data", "icon": "line-chart", "label": "Insights" },
613
+ { "id": "oncall", "kind": "rect", "intent": "gray", "group": "ops", "icon": "siren", "label": "On-call" }
614
+ ],
615
+ "edges": [
616
+ { "from": "web", "to": "api", "intent": "blue" },
617
+ { "from": "api", "to": "accounts", "rel": "depends-on" },
618
+ { "from": "api", "to": "workflow", "rel": "depends-on" },
619
+ { "from": "workflow", "to": "billing", "rel": "links" },
620
+ { "from": "workflow", "to": "warehouse", "intent": "green", "rel": "depends-on" },
621
+ { "from": "warehouse", "to": "insights", "intent": "green" },
622
+ { "from": "oncall", "to": "workflow", "kind": "dashed", "intent": "gray", "label": "watches" }
623
+ ]
624
+ }
625
+ \`\`\`
626
+
627
+ ## How to use this KB
628
+
629
+ <KeyFacts
630
+ items={[
631
+ { label: "For humans", value: "Browse the sections, or search. Every diagram links to the page behind it." },
632
+ { label: "For agents", value: "Query the MCP \u2014 search, get a page, follow relations, read a component's data." },
633
+ { label: "Source of truth", value: "If it isn't written here, it isn't decided. Author once; both readers stay in sync." },
634
+ { label: "Keep it current", value: "Update the doc in the same PR as the change. Stale knowledge is worse than none." },
635
+ ]}
636
+ />
637
+ `
638
+ );
639
+ write(
640
+ "content/docs/engineering/meta.json",
631
641
  `${JSON.stringify(
632
- {
633
- compilerOptions: {
634
- target: "ES2022",
635
- lib: ["dom", "dom.iterable", "esnext"],
636
- module: "esnext",
637
- moduleResolution: "bundler",
638
- strict: true,
639
- noEmit: true,
640
- jsx: "preserve",
641
- esModuleInterop: true,
642
- resolveJsonModule: true,
643
- isolatedModules: true,
644
- skipLibCheck: true,
645
- incremental: true,
646
- paths: { "@/*": ["./*"], "collections/*": ["./.source/*"] },
647
- plugins: [{ name: "next" }]
648
- },
649
- include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
650
- exclude: ["node_modules", ".next", "out"]
651
- },
642
+ { title: "Engineering", icon: "Layers", pages: ["architecture", "decisions", "runbooks"] },
652
643
  null,
653
644
  2
654
645
  )}
655
646
  `
656
647
  );
657
648
  write(
658
- "next.config.mjs",
659
- `import { createMDX } from "fumadocs-mdx/next";
649
+ "content/docs/engineering/architecture.mdx",
650
+ `---
651
+ title: Architecture
652
+ description: How the platform is built \u2014 the services, the request path, and where the data lives.
653
+ summary: The system architecture \u2014 services and their tiers, the synchronous request path, and the data stores. The canvas is the authoritative diagram; the agent reads the same nodes and edges.
654
+ tags: [engineering, architecture, system-design]
655
+ ---
660
656
 
661
- const withMDX = createMDX();
657
+ <SectionHead
658
+ eyebrow="Engineering"
659
+ title="Architecture"
660
+ description="The system, as a diagram the humans read and the agent queries. Replace the services with yours."
661
+ />
662
662
 
663
- /** @type {import('next').NextConfig} */
664
- const config = {
665
- reactStrictMode: true,
666
- // superlore ships compiled ESM \u2014 consume it as a normal package (no transpilePackages).
667
- };
663
+ This is placeholder architecture for a generic SaaS platform. Redraw the canvas to match your real
664
+ system \u2014 the agent answers "how does X work?" from this graph, so keep it honest.
665
+
666
+ \`\`\`superlore-canvas
667
+ {
668
+ "title": "Request path",
669
+ "direction": "right",
670
+ "groups": [
671
+ { "id": "client", "label": "Client", "frame": true, "intent": "gray" },
672
+ { "id": "edge", "label": "Edge", "frame": true, "intent": "blue" },
673
+ { "id": "app", "label": "Services", "frame": true, "intent": "purple" },
674
+ { "id": "data", "label": "Data", "frame": true, "intent": "green" },
675
+ { "id": "async", "label": "Async", "frame": true, "intent": "orange" }
676
+ ],
677
+ "nodes": [
678
+ { "id": "user", "kind": "circle", "group": "client", "label": "User" },
679
+ { "id": "cdn", "kind": "icon", "icon": "globe", "group": "edge", "label": "CDN + WAF" },
680
+ { "id": "gw", "kind": "icon", "icon": "shield", "group": "edge", "label": "API Gateway" },
681
+ { "id": "api", "kind": "rect", "intent": "purple", "group": "app", "label": "API service" },
682
+ { "id": "workflow", "kind": "rect", "intent": "purple", "group": "app", "label": "Workflow engine" },
683
+ { "id": "cachehit", "kind": "diamond", "group": "app", "label": "Cache hit?" },
684
+ { "id": "worker", "kind": "rect", "intent": "orange", "group": "app", "label": "Background worker" },
685
+ { "id": "cache", "kind": "cylinder", "intent": "red", "group": "data", "label": "Redis" },
686
+ { "id": "db", "kind": "cylinder", "intent": "green", "group": "data", "label": "Postgres" },
687
+ { "id": "queue", "kind": "parallelogram", "intent": "orange", "group": "async", "label": "Job queue" }
688
+ ],
689
+ "edges": [
690
+ { "from": "user", "to": "cdn", "label": "HTTPS", "intent": "blue" },
691
+ { "from": "cdn", "to": "gw", "intent": "blue" },
692
+ { "from": "gw", "to": "api", "label": "authenticated", "rel": "links" },
693
+ { "from": "api", "to": "cachehit", "intent": "purple" },
694
+ { "from": "cachehit", "to": "cache", "label": "lookup", "intent": "red", "rel": "depends-on" },
695
+ { "from": "cachehit", "to": "db", "label": "miss", "kind": "dashed", "intent": "green" },
696
+ { "from": "api", "to": "workflow", "rel": "depends-on" },
697
+ { "from": "workflow", "to": "queue", "label": "enqueue", "intent": "orange" },
698
+ { "from": "queue", "to": "worker", "intent": "orange" },
699
+ { "from": "worker", "to": "db", "label": "write", "intent": "green", "rel": "depends-on" }
700
+ ]
701
+ }
702
+ \`\`\`
668
703
 
669
- export default withMDX(config);
704
+ ## Services
705
+
706
+ <Table
707
+ caption="The services in the request path above"
708
+ columns={[
709
+ { key: "service", label: "Service", type: "text" },
710
+ { key: "owner", label: "Owner", type: "text" },
711
+ { key: "store", label: "Data store", type: "text" },
712
+ { key: "sla", label: "SLA", type: "text" }
713
+ ]}
714
+ rows={[
715
+ { service: "API service", owner: "Platform", store: "Postgres + Redis", sla: "99.95%" },
716
+ { service: "Workflow engine", owner: "Platform", store: "Postgres", sla: "99.9%" },
717
+ { service: "Background worker", owner: "Platform", store: "Job queue", sla: "best effort" }
718
+ ]}
719
+ />
720
+
721
+ <KeyFacts
722
+ items={[
723
+ { label: "Language", value: "Replace with yours \u2014 e.g. TypeScript / Go" },
724
+ { label: "Runtime", value: "Where it runs \u2014 e.g. containers on your cloud" },
725
+ { label: "Primary store", value: "Postgres" },
726
+ { label: "Cache", value: "Redis" },
727
+ ]}
728
+ />
670
729
  `
671
730
  );
672
731
  write(
673
- "source.config.ts",
674
- `import { defineConfig, defineDocs } from "fumadocs-mdx/config";
675
- import { superloreFrontmatterSchema } from "superlore/frontmatter";
732
+ "content/docs/engineering/decisions.mdx",
733
+ `---
734
+ title: Decisions
735
+ description: The architectural decisions behind how things are built \u2014 and why.
736
+ summary: Architecture decision records (ADRs). Each captures the context, the call, and its consequences so an agent can reason about why the system is the way it is.
737
+ tags: [engineering, decisions, adr]
738
+ ---
676
739
 
677
- // Extend the superlore frontmatter schema in ONE place if you add custom fields.
678
- export const docs = defineDocs({
679
- dir: "content/docs",
680
- docs: { schema: superloreFrontmatterSchema },
681
- });
740
+ <SectionHead
741
+ eyebrow="Engineering"
742
+ title="Decisions"
743
+ description="Why the system looks the way it does. Add an ADR whenever a choice is hard to reverse."
744
+ />
745
+
746
+ These are placeholder ADRs. Keep the format \u2014 context, decision, consequences \u2014 so both a new hire
747
+ and an agent can understand not just *what* was chosen but *why*.
748
+
749
+ <Decision
750
+ title="Postgres as the primary store"
751
+ status="accepted"
752
+ identifier="ADR-001"
753
+ date="2024-01-15"
754
+ context={<>We needed a primary store with strong consistency, relational queries, and an operational track record the team already had.</>}
755
+ decision={<>Use a single Postgres cluster as the system of record; reach for other stores only when a workload clearly outgrows it.</>}
756
+ consequences={[
757
+ "One backup, failover, and migration story to learn.",
758
+ "Relational integrity by default; JSONB where we need flexibility.",
759
+ "We accept vertical-scaling limits until a workload proves it needs more.",
760
+ ]}
761
+ />
682
762
 
683
- export default defineConfig();
763
+ <Decision
764
+ title="Redis cache in front of reads"
765
+ status="accepted"
766
+ identifier="ADR-002"
767
+ date="2024-03-02"
768
+ context={<>Hot read paths were hitting Postgres harder than necessary as traffic grew.</>}
769
+ decision={<>Add a Redis cache with a read-through pattern on the hottest endpoints; treat it as disposable.</>}
770
+ consequences={[
771
+ "Lower p99 on cached endpoints.",
772
+ "A cache-invalidation discipline we have to keep honest.",
773
+ "The system must stay correct when the cache is cold or down.",
774
+ ]}
775
+ refs={[{ rel: "see", target: "/docs/engineering/architecture", label: "Where the cache sits" }]}
776
+ />
684
777
  `
685
778
  );
686
779
  write(
687
- "postcss.config.mjs",
688
- `const config = {
689
- plugins: { "@tailwindcss/postcss": {} },
690
- };
780
+ "content/docs/engineering/runbooks.mdx",
781
+ `---
782
+ title: Runbooks
783
+ description: What to do when things break \u2014 and who's on call.
784
+ summary: Operational runbooks for common incidents, as ordered checklists, plus the on-call rotation. An agent can walk these steps or hand them to the person on call.
785
+ tags: [engineering, operations, runbooks, on-call]
786
+ ---
691
787
 
692
- export default config;
788
+ <SectionHead
789
+ eyebrow="Engineering"
790
+ title="Runbooks"
791
+ description="Calm, ordered steps for the bad days. Replace with your real incident procedures."
792
+ />
793
+
794
+ ## Runbook: elevated error rate
795
+
796
+ <Checklist
797
+ label="Elevated 5xx on the API"
798
+ items={[
799
+ { text: "Confirm the alert against the dashboard \u2014 is it real and ongoing?", group: "Assess" },
800
+ { text: "Check the most recent deploy; roll back if the timeline lines up", group: "Assess" },
801
+ { text: "Check Redis and Postgres health and connection counts", group: "Assess" },
802
+ { text: "If a dependency is down, shed load and post status", group: "Mitigate" },
803
+ { text: "Once stable, capture a timeline for the post-incident review", group: "Recover" },
804
+ ]}
805
+ />
806
+
807
+ ## On-call rotation
808
+
809
+ <Schedule
810
+ label="On-call this cycle"
811
+ events={[
812
+ { date: "2024-06-03", title: "Primary \u2014 replace with a name", body: "Carries the pager; first responder." },
813
+ { date: "2024-06-10", title: "Secondary \u2014 replace with a name", body: "Backup; escalation point." },
814
+ { date: "2024-06-17", title: "Primary \u2014 replace with a name", body: "Carries the pager; first responder." },
815
+ ]}
816
+ />
817
+
818
+ <Tip title="Keep runbooks executable">
819
+ A good runbook is a list someone half-asleep can follow. If a step needs judgement, say what to
820
+ weigh. The agent can read these too \u2014 keep them concrete.
821
+ </Tip>
693
822
  `
694
823
  );
695
824
  write(
696
- "app/global.css",
697
- `@import "tailwindcss";
698
- /* superlore theme tokens (light + dark, co-equal). Re-skin the whole KB from one accent. */
699
- @import "superlore/css";
700
-
701
- :root {
702
- /* Brand accent \u2014 superlore derives the hover/weak/border/text family for both themes. */
703
- --kp-accent: ${accent2};
704
- }
825
+ "content/docs/product/meta.json",
826
+ `${JSON.stringify(
827
+ { title: "Product", icon: "Rocket", pages: ["roadmap", "releases"] },
828
+ null,
829
+ 2
830
+ )}
705
831
  `
706
832
  );
707
833
  write(
708
- "app/layout.tsx",
709
- `import type { ReactNode } from "react";
710
- import { RootProvider } from "superlore/ui";
711
- import "./global.css";
834
+ "content/docs/product/roadmap.mdx",
835
+ `---
836
+ title: Roadmap
837
+ description: What we're building \u2014 now, next, and later.
838
+ summary: The product roadmap as a board (now / next / later) and a timeline of milestones. An agent can answer "what's planned" and "what's in flight" from this structured data.
839
+ tags: [product, roadmap, planning]
840
+ ---
712
841
 
713
- export default function RootLayout({ children }: { children: ReactNode }) {
714
- // Light and dark are co-equal; default to the system preference.
715
- return (
716
- <html lang="en" suppressHydrationWarning>
717
- <body>
718
- <RootProvider>{children}</RootProvider>
719
- </body>
720
- </html>
721
- );
722
- }
842
+ <SectionHead
843
+ eyebrow="Product"
844
+ title="Roadmap"
845
+ description="Where the work is heading. Replace the cards and milestones with yours."
846
+ />
847
+
848
+ <Board
849
+ label="Now / Next / Later"
850
+ columns={[
851
+ { title: "Now", status: "in-progress", cards: [
852
+ { title: "Faster onboarding", body: "Cut time-to-first-value for new accounts.", status: "in-progress", tags: ["growth"] },
853
+ { title: "Audit log", body: "Per-account, exportable.", status: "in-progress", tags: ["enterprise"] }
854
+ ] },
855
+ { title: "Next", status: "planned", cards: [
856
+ { title: "SSO for teams", body: "SAML + SCIM.", status: "planned", tags: ["enterprise"] },
857
+ { title: "Usage-based billing", body: "Meter and invoice on usage.", status: "planned", tags: ["billing"] }
858
+ ] },
859
+ { title: "Later", status: "planned", cards: [
860
+ { title: "Public API v2", body: "Cleaner resources, better pagination.", status: "planned", tags: ["platform"] }
861
+ ] }
862
+ ]}
863
+ />
864
+
865
+ ## Milestones
866
+
867
+ <Timeline
868
+ label="Product milestones"
869
+ items={[
870
+ { date: "2024-Q1", title: "GA launch", body: "First generally-available release.", status: "done", tags: ["launch"] },
871
+ { date: "2024-Q2", title: "Enterprise readiness", body: "Audit log, roles, SSO groundwork.", status: "in-progress", tags: ["enterprise"] },
872
+ { date: "2024-Q3", title: "Self-serve growth", body: "Onboarding and usage-based billing.", status: "planned", tags: ["growth"] },
873
+ ]}
874
+ />
723
875
  `
724
876
  );
725
877
  write(
726
- "app/page.tsx",
727
- `import Link from "next/link";
878
+ "content/docs/product/releases.mdx",
879
+ `---
880
+ title: Releases
881
+ description: What shipped, version by version.
882
+ summary: The release history with versioned, categorized changes (added / changed / fixed). An agent can answer "what changed in 1.2?" from this structured changelog.
883
+ tags: [product, releases, changelog]
884
+ ---
728
885
 
729
- export default function HomePage() {
730
- return (
731
- <main style={{ maxWidth: "42rem", margin: "0 auto", padding: "6rem 1.5rem" }}>
732
- <h1 style={{ fontSize: "2rem", fontWeight: 700 }}>${escapeForJsx(config.name)}</h1>
733
- <p style={{ marginTop: "0.75rem", opacity: 0.7 }}>
734
- One corpus. Humans and agents. Start in <Link href="/docs">the docs</Link>.
735
- </p>
736
- </main>
737
- );
738
- }
739
- `
740
- );
741
- write(
742
- "app/docs/layout.tsx",
743
- `import type { ReactNode } from "react";
744
- import { DocsLayout } from "fumadocs-ui/layouts/docs";
745
- import { source } from "@/lib/source";
746
-
747
- export default function Layout({ children }: { children: ReactNode }) {
748
- return (
749
- <DocsLayout tree={source.pageTree} nav={{ title: "${escapeForJsx(config.name)}" }}>
750
- {children}
751
- </DocsLayout>
752
- );
753
- }
754
- `
755
- );
756
- write(
757
- "app/docs/[[...slug]]/page.tsx",
758
- `import { notFound } from "next/navigation";
759
- import { DocsBody, DocsPage } from "fumadocs-ui/page";
760
- import { getMDXComponents } from "superlore";
761
- import { source } from "@/lib/source";
886
+ <SectionHead
887
+ eyebrow="Product"
888
+ title="Releases"
889
+ description="The changelog, as structured data. Replace with your real release notes."
890
+ />
762
891
 
763
- export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
764
- const params = await props.params;
765
- const page = source.getPage(params.slug);
766
- if (!page) notFound();
767
- const MDX = page.data.body;
768
- return (
769
- <DocsPage toc={page.data.toc}>
770
- <DocsBody>
771
- <MDX components={getMDXComponents()} />
772
- </DocsBody>
773
- </DocsPage>
774
- );
775
- }
892
+ <Releases
893
+ version="1.2.0"
894
+ date="2024-05-20"
895
+ status="done"
896
+ title="Audit log (beta)"
897
+ summary="Per-account audit logging, plus reliability fixes."
898
+ changes={[
899
+ { type: "added", text: "Exportable audit log for account admins." },
900
+ { type: "changed", text: "Faster account switching." },
901
+ { type: "fixed", text: "Rare double-charge on retried payments." },
902
+ ]}
903
+ />
776
904
 
777
- export function generateStaticParams() {
778
- return source.generateParams();
779
- }
905
+ <Releases
906
+ version="1.1.0"
907
+ date="2024-04-02"
908
+ status="done"
909
+ title="Onboarding refresh"
910
+ changes={[
911
+ { type: "added", text: "Guided setup for new accounts." },
912
+ { type: "fixed", text: "Invite emails occasionally landing in spam." },
913
+ ]}
914
+ />
780
915
  `
781
916
  );
782
917
  write(
783
- "lib/source.ts",
784
- `import { docs } from "collections/server";
785
- import { loader, lucideIconsPlugin } from "superlore/source";
786
-
787
- export const source = loader({
788
- baseUrl: "/docs",
789
- source: docs.toFumadocsSource(),
790
- plugins: [lucideIconsPlugin()],
791
- });
918
+ "content/docs/team/meta.json",
919
+ `${JSON.stringify(
920
+ { title: "Team", icon: "Users", pages: ["onboarding", "people"] },
921
+ null,
922
+ 2
923
+ )}
792
924
  `
793
925
  );
794
- if (config.type === "personal-kb") {
795
- writePersonalContent(write, config);
796
- } else {
797
- write(
798
- "content/docs/index.mdx",
799
- `---
800
- title: Welcome
801
- description: The home of your superlore knowledge base.
802
- summary: Landing page for the ${config.name} knowledge base.
803
- tags: [getting-started]
926
+ write(
927
+ "content/docs/team/onboarding.mdx",
928
+ `---
929
+ title: Onboarding
930
+ description: Your first week \u2014 what to set up, who to meet, and where to start.
931
+ summary: The new-hire onboarding path as ordered steps and a first-week checklist. An agent can guide a new teammate through setup using these steps.
932
+ tags: [team, onboarding, getting-started]
804
933
  ---
805
934
 
806
- # Welcome to ${config.name}
935
+ <SectionHead
936
+ eyebrow="Team"
937
+ title="Onboarding"
938
+ description="Welcome. This is the path from day one to your first shipped change. Replace with yours."
939
+ />
807
940
 
808
- This is your superlore KB. Author once in MDX \u2014 humans get this clean, interactive site, and
809
- agents read the same structured content over MCP.
941
+ <Steps>
942
+ <Step>
943
+ ### Get access
944
+ Accounts, repos, and the password manager. If something's missing, ask in the team channel.
945
+ </Step>
946
+ <Step>
947
+ ### Set up your machine
948
+ Clone the repo, install dependencies, and get the app running locally.
949
+ </Step>
950
+ <Step>
951
+ ### Read the architecture
952
+ Skim [Architecture](/docs/engineering/architecture) and the [Decisions](/docs/engineering/decisions) so the system isn't a black box.
953
+ </Step>
954
+ <Step>
955
+ ### Ship something small
956
+ Pick a starter issue. The goal of week one is a merged PR, however small.
957
+ </Step>
958
+ </Steps>
959
+
960
+ ## First-week checklist
810
961
 
811
- Edit \`content/docs/index.mdx\` to make it yours, then run \`superlore dev\`.
962
+ <Checklist
963
+ label="Your first week"
964
+ items={[
965
+ { text: "Access to repos, cloud, and the password manager", group: "Day 1" },
966
+ { text: "App running locally", group: "Day 1" },
967
+ { text: "Met your onboarding buddy", group: "Day 1" },
968
+ { text: "Read the architecture and recent decisions", group: "Week 1" },
969
+ { text: "Opened your first PR", group: "Week 1" },
970
+ { text: "Added yourself to the team page", group: "Week 1" },
971
+ ]}
972
+ />
812
973
  `
813
- );
814
- }
815
- if (authEnabled) {
816
- writeAuth(write, config);
817
- }
818
- if (mcpEnabled) {
819
- const mcpPath = config.mcp?.path ?? "/api/mcp";
820
- write(
821
- "app/api/[transport]/route.ts",
822
- `import { createMcpHandler } from "mcp-handler";
823
- import { z } from "zod";
824
- import { getComponentData, getPage, list, navigate, search } from "superlore/mcp";
825
- import { buildIndexFromSource } from "superlore/source";
826
- import type { KKind } from "superlore";
827
- import { source } from "@/lib/source";
974
+ );
975
+ write(
976
+ "content/docs/team/people.mdx",
977
+ `---
978
+ title: People
979
+ description: Who's on the team and what they own.
980
+ summary: The team roster \u2014 who's here, their role, and who they report to \u2014 plus an example entity card. An agent can answer "who owns X?" from this structured directory.
981
+ tags: [team, people, directory]
982
+ ---
828
983
 
829
- // Your KB's MCP endpoint. Served at ${mcpPath} \u2014 the same structured content the site renders,
830
- // exposed to agents. The index is built straight from your content \`source\`: author once, and
831
- // humans read the pages while agents query this corpus. No scraping, no drift.
832
- const index = buildIndexFromSource(source);
984
+ <SectionHead
985
+ eyebrow="Team"
986
+ title="People"
987
+ description="Who to ask about what. Replace these placeholders with your real team."
988
+ />
833
989
 
834
- const json = (data: unknown) => ({
835
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
836
- });
990
+ <Roster
991
+ label="The team"
992
+ people={[
993
+ { name: "Replace Me", role: "Engineering lead", tags: ["platform"] },
994
+ { name: "Replace Me", role: "Product", reportsTo: "Replace Me", tags: ["roadmap"] },
995
+ { name: "Replace Me", role: "Engineer", reportsTo: "Replace Me", tags: ["platform"] },
996
+ { name: "Replace Me", role: "Design", reportsTo: "Replace Me", tags: ["product"] },
997
+ ]}
998
+ />
837
999
 
838
- const handler = createMcpHandler(
839
- (server) => {
840
- server.tool(
841
- "search",
842
- "Full-text search across the knowledge base.",
843
- { query: z.string(), limit: z.number().int().positive().optional() },
844
- async ({ query, limit }) => json(search(index, query, limit)),
845
- );
846
- server.tool(
847
- "get_page",
848
- "Get a page's full structured content by path.",
849
- { path: z.string() },
850
- async ({ path }) => json(getPage(index, path)),
851
- );
852
- server.tool(
853
- "list",
854
- "List knowledge nodes, filtered by kind / tag / entityType.",
855
- { kind: z.string().optional(), tag: z.string().optional(), entityType: z.string().optional() },
856
- async ({ kind, tag, entityType }) =>
857
- json(list(index, { kind: kind as KKind | undefined, tag, entityType })),
858
- );
859
- server.tool(
860
- "navigate",
861
- "Follow relations from a page path / node id / entity ref.",
862
- { target: z.string() },
863
- async ({ target }) => json(navigate(index, target)),
864
- );
865
- server.tool(
866
- "get_component_data",
867
- "Get the structured data behind a rendered component (its knowledge face).",
868
- { id: z.string() },
869
- async ({ id }) => json(getComponentData(index, id)),
870
- );
871
- },
872
- {},
873
- { basePath: "/api" },
874
- );
1000
+ ## Example: a person, as structured data
875
1001
 
876
- export { handler as GET, handler as POST };
1002
+ <EntityCard
1003
+ type="person"
1004
+ slug="engineering-lead"
1005
+ title="Engineering lead"
1006
+ summary="One line on what they own and how to reach them."
1007
+ icon="user"
1008
+ fields={[
1009
+ { key: "Owns", value: "Platform & architecture" },
1010
+ { key: "Time zone", value: "UTC+0" },
1011
+ { key: "Best reached", value: "Async, in the team channel" },
1012
+ ]}
1013
+ refs={[{ rel: "see", target: "/docs/engineering/architecture", label: "What they own" }]}
1014
+ />
877
1015
  `
878
- );
879
- }
1016
+ );
1017
+ }
1018
+
1019
+ // src/lib/content/product.ts
1020
+ function writeProductContent(write, config) {
880
1021
  write(
881
- ".gitignore",
882
- `node_modules
883
- .next
884
- .source
885
- out
886
- *.tsbuildinfo
887
- .env*.local
1022
+ "content/docs/meta.json",
1023
+ `${JSON.stringify(
1024
+ {
1025
+ title: config.name,
1026
+ root: true,
1027
+ icon: "BookOpen",
1028
+ pages: ["index", "getting-started", "concepts", "guides", "reference", "changelog"]
1029
+ },
1030
+ null,
1031
+ 2
1032
+ )}
888
1033
  `
889
1034
  );
890
- if (authEnabled) {
891
- write(
892
- ".env.example",
893
- `# Auth.js v5 + Google SSO. The gate is OFF until AUTH_GOOGLE_ID is set, so local dev works with
894
- # this file empty. Copy to .env.local and fill in to enable it on a deploy.
895
- AUTH_SECRET= # \`openssl rand -base64 32\`
896
- AUTH_GOOGLE_ID= # presence of this turns the gate ON
897
- AUTH_GOOGLE_SECRET=
898
- AUTH_URL= # the deploy's canonical URL, e.g. https://your-kb.vercel.app
899
- AUTH_TRUST_HOST=true
900
- ${config.auth?.allowedDomain ? `AUTH_ALLOWED_DOMAIN=${config.auth.allowedDomain}` : "# AUTH_ALLOWED_DOMAIN=example.com # restrict sign-in to one workspace domain (optional)"}
901
- # AUTH_ALLOWED_EMAILS=you@example.com # comma-separated allowlist that bypasses the domain check
902
- # LOCAL=true # force the gate OFF locally even when configured
903
- `
904
- );
905
- }
906
- const authReadme = authEnabled ? `
907
-
908
- ## Auth
909
-
910
- This KB ships a Google SSO gate (Auth.js v5). It is **off until you set \`AUTH_GOOGLE_ID\`**, so local dev runs open. Copy \`.env.example\` to \`.env.local\` and fill it in to enable it. The gate lives in \`proxy.ts\`, in front of every route \u2014 so the MCP inherits it too.` : "";
911
1035
  write(
912
- "README.md",
913
- `# ${config.name}
1036
+ "content/docs/index.mdx",
1037
+ `---
1038
+ title: Overview
1039
+ description: What ${config.name} is, who it's for, and where to start.
1040
+ summary: Overview of ${config.name} \u2014 what it does, the value it delivers, and the paths into the docs. The starting point for both a reader and an agent answering "what is this?".
1041
+ tags: [overview, getting-started]
1042
+ ---
914
1043
 
915
- A superlore knowledge base. One corpus. Humans and agents.
1044
+ <PageHero
1045
+ kicker="Documentation"
1046
+ title="${config.name}"
1047
+ description="What it is, in one line you'll replace with your own. Author once in MDX \u2014 readers get this site, agents get the same content over MCP."
1048
+ icon="book-open"
1049
+ />
916
1050
 
917
- ## Develop
1051
+ <Note title="This is a starter structure">
1052
+ Placeholder docs that show the shape of a good product site. Keep the structure, replace the
1053
+ content. Preview with \`superlore dev\`.
1054
+ </Note>
918
1055
 
919
- \`\`\`sh
920
- pnpm install # or npm / yarn / bun
921
- superlore dev # preview the site locally
922
- \`\`\`
1056
+ <StatGrid
1057
+ stats={[
1058
+ { label: "Set up in", value: "5 min", hint: "From install to first call" },
1059
+ { label: "Works with", value: "Any stack", hint: "Replace with your real surface" },
1060
+ { label: "Agent-ready", value: "MCP", hint: "The docs, queryable" },
1061
+ ]}
1062
+ />
923
1063
 
924
- ## Build
1064
+ ## Start here
925
1065
 
926
- \`\`\`sh
927
- superlore build
928
- \`\`\`
1066
+ <FeatureList
1067
+ items={[
1068
+ { icon: "rocket", title: "Getting started", description: "Install, configure, and make your first call.", href: "/docs/getting-started" },
1069
+ { icon: "lightbulb", title: "Concepts", description: "How it works and the core ideas.", href: "/docs/concepts/how-it-works" },
1070
+ { icon: "book-open", title: "Guides", description: "Step-by-step for common tasks.", href: "/docs/guides/first-integration" },
1071
+ { icon: "settings", title: "Reference", description: "Every option, in one place.", href: "/docs/reference/configuration" },
1072
+ ]}
1073
+ />
1074
+ `
1075
+ );
1076
+ write(
1077
+ "content/docs/getting-started.mdx",
1078
+ `---
1079
+ title: Getting started
1080
+ description: Install ${config.name}, configure it, and make your first call.
1081
+ summary: The quickstart for ${config.name} \u2014 install, minimal configuration, and a first working call, as ordered steps an agent can also follow.
1082
+ tags: [getting-started, quickstart, install]
1083
+ ---
929
1084
 
930
- Config lives in \`superlore.json\`. Author content in \`content/docs/\`.${mcpEnabled ? `
1085
+ <SectionHead
1086
+ eyebrow="Getting started"
1087
+ title="Quickstart"
1088
+ description="From nothing to a working call. Replace the commands and snippet with your real ones."
1089
+ />
931
1090
 
932
- The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}${authReadme}
1091
+ <Steps>
1092
+ <Step>
1093
+ ### Install
1094
+
1095
+ \`\`\`bash
1096
+ npm install your-package
1097
+ \`\`\`
1098
+ </Step>
1099
+ <Step>
1100
+ ### Configure
1101
+
1102
+ Set your API key in the environment:
1103
+
1104
+ \`\`\`bash
1105
+ export YOUR_API_KEY=sk_...
1106
+ \`\`\`
1107
+ </Step>
1108
+ <Step>
1109
+ ### Make your first call
1110
+
1111
+ \`\`\`ts
1112
+ import { Client } from "your-package";
1113
+
1114
+ const client = new Client({ apiKey: process.env.YOUR_API_KEY });
1115
+ const result = await client.ping();
1116
+ console.log(result);
1117
+ \`\`\`
1118
+ </Step>
1119
+ </Steps>
1120
+
1121
+ <Tip title="That's it">
1122
+ You've made your first call. Next, read [how it works](/docs/concepts/how-it-works) or jump into a
1123
+ [guide](/docs/guides/first-integration).
1124
+ </Tip>
933
1125
  `
934
1126
  );
935
- }
936
- function writeAuth(write, config) {
937
- const allowedDomain = config.auth?.allowedDomain;
938
1127
  write(
939
- "auth.ts",
940
- `import { createSuperloreAuth } from "superlore/auth";
941
-
942
- // Auth.js v5 + Google SSO. Allowlists can come from env (AUTH_ALLOWED_DOMAIN / AUTH_ALLOWED_EMAILS)
943
- // or be passed explicitly here. Off until AUTH_GOOGLE_ID is set, so local dev needs no config.
944
- export const { handlers, auth, signIn, signOut } = createSuperloreAuth(${allowedDomain ? `{
945
- allowedDomain: ${JSON.stringify(allowedDomain)},
946
- }` : "{}"});
1128
+ "content/docs/concepts/meta.json",
1129
+ `${JSON.stringify(
1130
+ { title: "Concepts", icon: "Lightbulb", pages: ["how-it-works", "core-concepts"] },
1131
+ null,
1132
+ 2
1133
+ )}
947
1134
  `
948
1135
  );
949
1136
  write(
950
- "app/api/auth/[...nextauth]/route.ts",
951
- `import { handlers } from "@/auth";
1137
+ "content/docs/concepts/how-it-works.mdx",
1138
+ `---
1139
+ title: How it works
1140
+ description: The path a request takes through ${config.name}, end to end.
1141
+ summary: How ${config.name} works under the hood \u2014 the request path from the caller's app through the API to the result. The canvas is the authoritative diagram; the agent reads the same graph.
1142
+ tags: [concepts, architecture, how-it-works]
1143
+ ---
952
1144
 
953
- export const { GET, POST } = handlers;
1145
+ <SectionHead
1146
+ eyebrow="Concepts"
1147
+ title="How it works"
1148
+ description="The journey of a request. Redraw the canvas to match your real flow."
1149
+ />
1150
+
1151
+ \`\`\`superlore-canvas
1152
+ {
1153
+ "title": "Request lifecycle",
1154
+ "direction": "right",
1155
+ "groups": [
1156
+ { "id": "you", "label": "Your app", "frame": true, "intent": "gray" },
1157
+ { "id": "svc", "label": "${config.name}", "frame": true, "intent": "accent" },
1158
+ { "id": "out", "label": "Result", "frame": true, "intent": "green" }
1159
+ ],
1160
+ "nodes": [
1161
+ { "id": "sdk", "kind": "rect", "intent": "blue", "group": "you", "icon": "code", "label": "SDK / HTTP call" },
1162
+ { "id": "auth", "kind": "icon", "icon": "shield", "group": "svc", "label": "Auth" },
1163
+ { "id": "validate", "kind": "diamond", "group": "svc", "label": "Valid request?" },
1164
+ { "id": "process", "kind": "rect", "intent": "purple", "group": "svc", "label": "Process" },
1165
+ { "id": "store", "kind": "cylinder", "intent": "green", "group": "svc", "label": "Store" },
1166
+ { "id": "result", "kind": "rounded", "intent": "green", "group": "out", "label": "Response" },
1167
+ { "id": "error", "kind": "callout", "intent": "red", "group": "out", "label": "Typed error" }
1168
+ ],
1169
+ "edges": [
1170
+ { "from": "sdk", "to": "auth", "label": "request", "intent": "blue" },
1171
+ { "from": "auth", "to": "validate", "rel": "links" },
1172
+ { "from": "validate", "to": "process", "label": "yes", "intent": "purple" },
1173
+ { "from": "validate", "to": "error", "label": "no", "kind": "dashed", "intent": "red" },
1174
+ { "from": "process", "to": "store", "intent": "green", "rel": "depends-on" },
1175
+ { "from": "process", "to": "result", "intent": "green" }
1176
+ ]
1177
+ }
1178
+ \`\`\`
1179
+
1180
+ <KeyFacts
1181
+ items={[
1182
+ { label: "Transport", value: "HTTPS / your SDK" },
1183
+ { label: "Auth", value: "API key \u2014 replace with your real scheme" },
1184
+ { label: "Errors", value: "Typed and predictable, never a bare 500" },
1185
+ { label: "Idempotency", value: "Safe to retry \u2014 say how, here" },
1186
+ ]}
1187
+ />
954
1188
  `
955
1189
  );
956
1190
  write(
957
- "proxy.ts",
958
- `import { auth } from "@/auth";
959
- import { createAuthProxy } from "superlore/auth";
1191
+ "content/docs/concepts/core-concepts.mdx",
1192
+ `---
1193
+ title: Core concepts
1194
+ description: The handful of ideas that explain everything else.
1195
+ summary: The core concepts of ${config.name} \u2014 the vocabulary a reader (or agent) needs, plus how the approaches compare.
1196
+ tags: [concepts, glossary]
1197
+ ---
960
1198
 
961
- // Next.js 16 middleware lives in proxy.ts. The gate is self-disabling: with no AUTH_GOOGLE_ID
962
- // (or LOCAL=true) every request passes through, so local dev and public deploys stay open.
963
- export default createAuthProxy(auth);
1199
+ <SectionHead
1200
+ eyebrow="Concepts"
1201
+ title="Core concepts"
1202
+ description="Learn these five words and the rest of the docs make sense. Replace with yours."
1203
+ />
964
1204
 
965
- export const config = {
966
- // Run on everything except static assets (the helper also skips the auth dance + icons).
967
- matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
968
- };
1205
+ <FeatureList
1206
+ items={[
1207
+ { icon: "box", title: "Resource", description: "The main thing you create and operate on. Define yours." },
1208
+ { icon: "key", title: "API key", description: "How a request is authenticated." },
1209
+ { icon: "webhook", title: "Webhook", description: "How you hear about events asynchronously." },
1210
+ { icon: "repeat", title: "Idempotency key", description: "How a retry stays safe." },
1211
+ ]}
1212
+ />
1213
+
1214
+ ## Which approach fits?
1215
+
1216
+ <Comparison
1217
+ caption="Choosing how to integrate"
1218
+ options={["SDK", "Raw HTTP"]}
1219
+ rows={[
1220
+ { criterion: "Setup time", cells: ["Minutes", "A bit more"] },
1221
+ { criterion: "Type safety", cells: [true, false] },
1222
+ { criterion: "Control", cells: ["partial", true] },
1223
+ { criterion: "Best for", cells: ["Most apps", "Exotic runtimes"] },
1224
+ ]}
1225
+ />
969
1226
  `
970
1227
  );
971
1228
  write(
972
- "app/auth/signin/page.tsx",
973
- `import { signIn } from "@/auth";
1229
+ "content/docs/guides/meta.json",
1230
+ `${JSON.stringify(
1231
+ { title: "Guides", icon: "BookOpen", pages: ["first-integration"] },
1232
+ null,
1233
+ 2
1234
+ )}
1235
+ `
1236
+ );
1237
+ write(
1238
+ "content/docs/guides/first-integration.mdx",
1239
+ `---
1240
+ title: Your first integration
1241
+ description: A worked, end-to-end example of integrating ${config.name}.
1242
+ summary: A step-by-step guide to a first real integration with ${config.name}, with a verification checklist an agent can run through.
1243
+ tags: [guides, integration, tutorial]
1244
+ ---
974
1245
 
975
- export default async function SignInPage(props: {
976
- searchParams: Promise<{ callbackUrl?: string }>;
977
- }) {
978
- const { callbackUrl } = await props.searchParams;
979
- return (
980
- <main className="grid min-h-screen place-items-center p-6">
981
- <form
982
- action={async () => {
983
- "use server";
984
- await signIn("google", { redirectTo: callbackUrl ?? "/" });
985
- }}
986
- className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center"
987
- >
988
- <h1 className="text-lg font-semibold text-fd-foreground">Sign in</h1>
989
- <p className="mt-1 text-sm text-fd-muted-foreground">
990
- This knowledge base is private. Continue with Google to read it.
991
- </p>
992
- <button
993
- type="submit"
994
- className="mt-5 inline-flex w-full items-center justify-center rounded-md border border-kp-accent-border bg-kp-accent px-4 py-2 text-sm font-medium text-white"
995
- >
996
- Continue with Google
997
- </button>
998
- </form>
999
- </main>
1246
+ <SectionHead
1247
+ eyebrow="Guides"
1248
+ title="Your first integration"
1249
+ description="A realistic, end-to-end task. Replace with a real flow your users care about."
1250
+ />
1251
+
1252
+ <Steps>
1253
+ <Step>
1254
+ ### Create a resource
1255
+
1256
+ \`\`\`ts
1257
+ const item = await client.items.create({ name: "Example" });
1258
+ \`\`\`
1259
+ </Step>
1260
+ <Step>
1261
+ ### Handle the response
1262
+
1263
+ Check for the typed error before using the result. Never assume success.
1264
+ </Step>
1265
+ <Step>
1266
+ ### Listen for events
1267
+
1268
+ Register a webhook so you hear about changes you didn't initiate.
1269
+ </Step>
1270
+ </Steps>
1271
+
1272
+ ## Before you ship
1273
+
1274
+ <Checklist
1275
+ label="Integration checklist"
1276
+ items={[
1277
+ { text: "API key stored as a secret, never in code", group: "Security" },
1278
+ { text: "Errors handled, not swallowed", group: "Correctness" },
1279
+ { text: "Retries use an idempotency key", group: "Correctness" },
1280
+ { text: "Webhook signature verified", group: "Security" },
1281
+ ]}
1282
+ />
1283
+ `
1000
1284
  );
1001
- }
1285
+ write(
1286
+ "content/docs/reference/meta.json",
1287
+ `${JSON.stringify(
1288
+ { title: "Reference", icon: "Settings2", pages: ["configuration"] },
1289
+ null,
1290
+ 2
1291
+ )}
1002
1292
  `
1003
1293
  );
1004
1294
  write(
1005
- "app/auth/error/page.tsx",
1006
- `export default async function AuthErrorPage(props: {
1007
- searchParams: Promise<{ error?: string }>;
1008
- }) {
1009
- const { error } = await props.searchParams;
1010
- return (
1011
- <main className="grid min-h-screen place-items-center p-6">
1012
- <div className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center">
1013
- <h1 className="text-lg font-semibold text-fd-foreground">Can't sign in</h1>
1014
- <p className="mt-1 text-sm text-fd-muted-foreground">
1015
- {error === "AccessDenied"
1016
- ? "That account isn't allowed to access this knowledge base."
1017
- : "Something went wrong signing in. Try again."}
1018
- </p>
1019
- <a
1020
- href="/auth/signin"
1021
- className="mt-5 inline-flex w-full items-center justify-center rounded-md border border-fd-border px-4 py-2 text-sm font-medium text-fd-foreground no-underline"
1022
- >
1023
- Back to sign in
1024
- </a>
1025
- </div>
1026
- </main>
1295
+ "content/docs/reference/configuration.mdx",
1296
+ `---
1297
+ title: Configuration
1298
+ description: Every configuration option for ${config.name}, in one table.
1299
+ summary: The full configuration reference for ${config.name} \u2014 each option, its type, default, and what it does. An agent can answer "what does option X do?" from this table.
1300
+ tags: [reference, configuration, options]
1301
+ ---
1302
+
1303
+ <SectionHead
1304
+ eyebrow="Reference"
1305
+ title="Configuration"
1306
+ description="The complete list of options. Replace these placeholders with your real surface."
1307
+ />
1308
+
1309
+ <Table
1310
+ caption="Client options"
1311
+ columns={[
1312
+ { key: "option", label: "Option", type: "code" },
1313
+ { key: "type", label: "Type", type: "text" },
1314
+ { key: "default", label: "Default", type: "code" },
1315
+ { key: "desc", label: "Description", type: "text" }
1316
+ ]}
1317
+ rows={[
1318
+ { option: "apiKey", type: "string", default: "\u2014", desc: "Required. Your secret API key." },
1319
+ { option: "baseUrl", type: "string", default: "https://api\u2026", desc: "Override for self-hosted or staging." },
1320
+ { option: "timeout", type: "number", default: "30000", desc: "Request timeout in milliseconds." },
1321
+ { option: "maxRetries", type: "number", default: "2", desc: "Automatic retries on transient errors." }
1322
+ ]}
1323
+ />
1324
+
1325
+ ## A design note
1326
+
1327
+ <Decision
1328
+ title="Sensible defaults over required config"
1329
+ status="accepted"
1330
+ identifier="DESIGN-01"
1331
+ context={<>Every required option is friction between a developer and their first success.</>}
1332
+ decision={<>Only \`apiKey\` is required; everything else has a default that works for most apps.</>}
1333
+ consequences={[
1334
+ "A working call needs one line of config.",
1335
+ "Power users override exactly what they need.",
1336
+ ]}
1337
+ />
1338
+ `
1027
1339
  );
1028
- }
1340
+ write(
1341
+ "content/docs/changelog.mdx",
1342
+ `---
1343
+ title: Changelog
1344
+ description: What changed in ${config.name}, version by version.
1345
+ summary: The release history for ${config.name} with versioned, categorized changes. An agent can answer "what changed in version X?" from this structured changelog.
1346
+ tags: [changelog, releases]
1347
+ ---
1348
+
1349
+ <SectionHead
1350
+ eyebrow="Changelog"
1351
+ title="What's new"
1352
+ description="Versioned, categorized changes. Replace with your real release notes."
1353
+ />
1354
+
1355
+ <Releases
1356
+ version="1.1.0"
1357
+ date="2024-05-20"
1358
+ status="done"
1359
+ title="Webhooks"
1360
+ summary="Asynchronous events, plus reliability fixes."
1361
+ changes={[
1362
+ { type: "added", text: "Webhooks for resource events." },
1363
+ { type: "changed", text: "Lower default timeout to 30s." },
1364
+ { type: "fixed", text: "Retries no longer double-create on timeout." },
1365
+ ]}
1366
+ />
1367
+
1368
+ <Releases
1369
+ version="1.0.0"
1370
+ date="2024-04-01"
1371
+ status="done"
1372
+ title="General availability"
1373
+ changes={[
1374
+ { type: "added", text: "Stable v1 API and the official SDK." },
1375
+ ]}
1376
+ />
1029
1377
  `
1030
1378
  );
1031
1379
  }
1380
+
1381
+ // src/lib/content/personal.ts
1032
1382
  function writePersonalContent(write, config) {
1033
1383
  write(
1034
1384
  "content/docs/meta.json",
@@ -1036,6 +1386,7 @@ function writePersonalContent(write, config) {
1036
1386
  {
1037
1387
  title: config.name,
1038
1388
  root: true,
1389
+ icon: "User",
1039
1390
  pages: ["index", "beliefs", "working-style", "pr-comments", "voice", "stories"]
1040
1391
  },
1041
1392
  null,
@@ -1168,6 +1519,34 @@ would.
1168
1519
  ]}
1169
1520
  />
1170
1521
 
1522
+ ## How I approach a task
1523
+
1524
+ This is the loop I run on anything non-trivial. An agent working for me should follow the same path.
1525
+
1526
+ \`\`\`superlore-canvas
1527
+ {
1528
+ "title": "How I approach a task",
1529
+ "layout": "row",
1530
+ "nodes": [
1531
+ { "id": "outcome", "kind": "rounded", "intent": "accent", "icon": "target", "label": "Name the outcome", "body": "Goal + done-condition, in writing." },
1532
+ { "id": "decompose", "kind": "rounded", "intent": "blue", "icon": "split", "label": "Decompose", "body": "Reversible steps, sequenced." },
1533
+ { "id": "risk", "kind": "diamond", "intent": "orange", "label": "Riskiest part?" },
1534
+ { "id": "spike", "kind": "rounded", "intent": "orange", "icon": "flask-conical", "label": "Spike it cheap", "body": "Prove the unknown first." },
1535
+ { "id": "ship", "kind": "rounded", "intent": "green", "icon": "rocket", "label": "Ship small", "body": "Smallest reversible change." },
1536
+ { "id": "reflect", "kind": "rounded", "intent": "purple", "icon": "repeat", "label": "Reflect", "body": "What did I learn? Write it down." }
1537
+ ],
1538
+ "edges": [
1539
+ { "from": "outcome", "to": "decompose", "intent": "accent" },
1540
+ { "from": "decompose", "to": "risk", "intent": "blue" },
1541
+ { "from": "risk", "to": "spike", "label": "unknown", "intent": "orange" },
1542
+ { "from": "risk", "to": "ship", "label": "clear", "intent": "green" },
1543
+ { "from": "spike", "to": "ship", "intent": "green" },
1544
+ { "from": "ship", "to": "reflect", "intent": "purple" },
1545
+ { "from": "reflect", "to": "outcome", "kind": "dashed", "intent": "muted", "label": "next slice" }
1546
+ ]
1547
+ }
1548
+ \`\`\`
1549
+
1171
1550
  ## How I decide
1172
1551
 
1173
1552
  <Decision
@@ -1195,156 +1574,655 @@ would.
1195
1574
  />
1196
1575
  `
1197
1576
  );
1198
- write(
1199
- "content/docs/pr-comments.mdx",
1200
- `---
1201
- title: How I give PR comments
1202
- description: My review bar, the tone I use, and concrete examples of comments I leave.
1203
- summary: How this person reviews code \u2014 what they block on versus nudge, the tone of their comments, and worked examples an agent can imitate when reviewing on their behalf.
1204
- tags: [code-review, feedback, how-to]
1205
- ---
1577
+ write(
1578
+ "content/docs/pr-comments.mdx",
1579
+ `---
1580
+ title: How I give PR comments
1581
+ description: My review bar, the tone I use, and concrete examples of comments I leave.
1582
+ summary: How this person reviews code \u2014 what they block on versus nudge, the tone of their comments, and worked examples an agent can imitate when reviewing on their behalf.
1583
+ tags: [code-review, feedback, how-to]
1584
+ ---
1585
+
1586
+ <SectionHead
1587
+ eyebrow="Code review"
1588
+ title="How I give PR comments"
1589
+ description="What I block on, what I just nudge, and how I phrase it. Replace with your own."
1590
+ />
1591
+
1592
+ ## My review bar
1593
+
1594
+ <Checklist
1595
+ label="What I look for, in order"
1596
+ items={[
1597
+ { text: "Correctness \u2014 does it do the thing, including the edge cases?", group: "Block on" },
1598
+ { text: "Tests that would fail without the change", group: "Block on" },
1599
+ { text: "Names and structure I can read in one pass", group: "Block on" },
1600
+ { text: "Unnecessary abstraction or dead code", group: "Nudge on" },
1601
+ { text: "Comments that explain why, not what", group: "Nudge on" },
1602
+ { text: "Nits \u2014 formatting, ordering, taste", group: "Optional" },
1603
+ ]}
1604
+ />
1605
+
1606
+ ## How I phrase comments
1607
+
1608
+ I prefix to signal weight: **blocking:** must change, **suggestion:** take it or leave it,
1609
+ **nit:** ignore freely, **question:** I genuinely don't know. Examples:
1610
+
1611
+ <Example title="A blocking comment">
1612
+ **blocking:** This drops the error on the floor \u2014 if the fetch fails we return \`undefined\` and
1613
+ the caller renders an empty state as if it were success. Surface it, or handle it explicitly.
1614
+ </Example>
1615
+
1616
+ <Example title="A suggestion">
1617
+ **suggestion:** This loop reads cleanly, but \`items.flatMap\` would say the same thing in one line.
1618
+ Your call \u2014 not blocking.
1619
+ </Example>
1620
+
1621
+ <Example title="A nit">
1622
+ **nit:** tiny \u2014 can we name this \`pendingCount\` so it matches the others? Ignore if you disagree.
1623
+ </Example>
1624
+
1625
+ <Tip title="Tone">
1626
+ Critique the code, never the author. Lead with the why. If I'd want it softened when it lands on
1627
+ my own PR, I soften it.
1628
+ </Tip>
1629
+ `
1630
+ );
1631
+ write(
1632
+ "content/docs/voice.mdx",
1633
+ `---
1634
+ title: Voice & writing
1635
+ description: How I sound in writing \u2014 tone, defaults, and what I avoid.
1636
+ summary: This person's writing voice \u2014 tone, structure defaults, and explicit do/don't rules an agent should follow when drafting in their name.
1637
+ tags: [voice, writing, style]
1638
+ ---
1639
+
1640
+ <SectionHead
1641
+ eyebrow="How I write"
1642
+ title="Voice & writing"
1643
+ description="So an agent drafting in my name sounds like me, not like a template."
1644
+ />
1645
+
1646
+ Placeholder \u2014 capture your actual voice. The more specific the do/don't list, the better an agent
1647
+ can match you.
1648
+
1649
+ <KeyFacts
1650
+ items={[
1651
+ { label: "Tone", value: "Direct, warm, low ceremony" },
1652
+ { label: "Sentence length", value: "Short. Then one longer one to breathe." },
1653
+ { label: "Person", value: "First person, active voice" },
1654
+ { label: "Jargon", value: "Only when it's the precise word" },
1655
+ ]}
1656
+ />
1657
+
1658
+ ## Do / don't
1659
+
1660
+ <Comparison
1661
+ caption="My writing defaults"
1662
+ options={["Do", "Don't"]}
1663
+ rows={[
1664
+ { criterion: "Openers", cells: ["Get to the point in the first line", "Open with filler pleasantries"] },
1665
+ { criterion: "Hedging", cells: ["Say what I think", "It might possibly be worth considering"] },
1666
+ { criterion: "Structure", cells: ["Lead with the answer, then the why", "Bury the ask at the end"] },
1667
+ { criterion: "Emoji", cells: ["Rarely, and never in serious writing", "Decorate every line"] },
1668
+ ]}
1669
+ />
1670
+
1671
+ <Example title="A message in my voice">
1672
+ Shipping the import fix today. It was dropping rows when a column was empty \u2014 now we skip the row
1673
+ and log it. One follow-up: we should validate on upload so this can't happen again. Want me to take
1674
+ that next?
1675
+ </Example>
1676
+ `
1677
+ );
1678
+ write(
1679
+ "content/docs/stories.mdx",
1680
+ `---
1681
+ title: Stories
1682
+ description: Formative moments that explain how I got my defaults.
1683
+ summary: Formative experiences that shaped this person's beliefs and working style, as a dated timeline an agent can reference for context on why they think the way they do.
1684
+ tags: [stories, background, timeline]
1685
+ ---
1686
+
1687
+ <SectionHead
1688
+ eyebrow="Where it comes from"
1689
+ title="Stories"
1690
+ description="The moments behind the takes. Replace these with your own \u2014 dates can be approximate."
1691
+ />
1692
+
1693
+ An agent that knows *why* you believe something reasons better than one that only knows *what*.
1694
+ These are placeholders.
1695
+
1696
+ <Timeline
1697
+ label="Formative moments"
1698
+ items={[
1699
+ {
1700
+ date: "2016",
1701
+ title: "The outage that taught me to write things down",
1702
+ body: "A fix lived only in one person's head. When they were out, we relearned it the hard way. I've defaulted to docs ever since.",
1703
+ status: "done",
1704
+ tags: ["process", "writing"],
1705
+ },
1706
+ {
1707
+ date: "2019",
1708
+ title: "Shipped small for the first time",
1709
+ body: "Replaced a six-month rewrite with weekly reversible changes. It landed. I stopped believing in big-bang.",
1710
+ status: "done",
1711
+ tags: ["shipping"],
1712
+ },
1713
+ {
1714
+ date: "2022",
1715
+ title: "A review that changed how I review",
1716
+ body: "Someone critiqued my code without making me feel small. I've tried to give every review that way since.",
1717
+ status: "done",
1718
+ tags: ["code-review", "tone"],
1719
+ },
1720
+ ]}
1721
+ />
1722
+ `
1723
+ );
1724
+ }
1725
+
1726
+ // src/lib/content/util.ts
1727
+ function escapeForJsx(value) {
1728
+ return value.replace(/[\\"]/g, "\\$&").replace(/[{}]/g, "");
1729
+ }
1730
+
1731
+ // src/lib/content/index.ts
1732
+ function writeContent(write, config) {
1733
+ switch (config.type) {
1734
+ case "company-kb":
1735
+ writeCompanyContent(write, config);
1736
+ return;
1737
+ case "product-docs":
1738
+ writeProductContent(write, config);
1739
+ return;
1740
+ case "personal-kb":
1741
+ writePersonalContent(write, config);
1742
+ return;
1743
+ }
1744
+ }
1745
+
1746
+ // src/lib/scaffold.ts
1747
+ var here = dirname2(fileURLToPath(import.meta.url));
1748
+ function findStarterTemplate() {
1749
+ let dir = here;
1750
+ for (; ; ) {
1751
+ const candidate = join3(dir, "templates", "starter");
1752
+ if (existsSync3(candidate)) return candidate;
1753
+ const bundled = join3(dir, "template");
1754
+ if (existsSync3(bundled)) return bundled;
1755
+ const parent = dirname2(dir);
1756
+ if (parent === dir) return void 0;
1757
+ dir = parent;
1758
+ }
1759
+ }
1760
+ function isUsableTemplate(dir) {
1761
+ if (!existsSync3(dir)) return false;
1762
+ const entries = readdirSync(dir).filter((name) => name !== "README.md" && !name.startsWith("."));
1763
+ return entries.length > 0;
1764
+ }
1765
+ function isEmptyDir(dir) {
1766
+ if (!existsSync3(dir)) return true;
1767
+ return readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0;
1768
+ }
1769
+ function scaffold(options) {
1770
+ const root = resolve2(options.dir);
1771
+ mkdirSync(root, { recursive: true });
1772
+ const template = findStarterTemplate();
1773
+ let source;
1774
+ if (template && isUsableTemplate(template)) {
1775
+ cpSync(template, root, { recursive: true });
1776
+ source = "template";
1777
+ } else {
1778
+ writeSkeleton(root, options.config);
1779
+ source = "skeleton";
1780
+ }
1781
+ writeFileSync2(join3(root, "superlore.json"), serializeSuperloreJson(options.config), "utf8");
1782
+ return { root, source };
1783
+ }
1784
+ function writeSkeleton(root, config) {
1785
+ const accent2 = config.accent ?? SUPERLORE_VIOLET;
1786
+ const mcpEnabled = config.mcp?.enabled ?? true;
1787
+ const authEnabled = config.auth?.enabled ?? false;
1788
+ const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "superlore-kb";
1789
+ const write = (rel, body) => {
1790
+ const file = join3(root, rel);
1791
+ mkdirSync(dirname2(file), { recursive: true });
1792
+ writeFileSync2(file, body, "utf8");
1793
+ };
1794
+ write(
1795
+ "package.json",
1796
+ `${JSON.stringify(
1797
+ {
1798
+ name: slug,
1799
+ version: "0.1.0",
1800
+ private: true,
1801
+ type: "module",
1802
+ scripts: {
1803
+ dev: "next dev",
1804
+ build: "next build",
1805
+ start: "next start",
1806
+ postinstall: "fumadocs-mdx"
1807
+ },
1808
+ dependencies: {
1809
+ "fumadocs-core": "16.8.2",
1810
+ "fumadocs-mdx": "14.3.1",
1811
+ "fumadocs-ui": "16.8.2",
1812
+ superlore: "^0.5.3",
1813
+ "lucide-react": "^1.21.0",
1814
+ // superlore peers the rendered components pull in: Mermaid (Diagram), themes.
1815
+ mermaid: "^11.15.0",
1816
+ ...mcpEnabled ? { "@modelcontextprotocol/sdk": "^1.29.0", "mcp-handler": "^1.1.0" } : {},
1817
+ // Auth.js v5 powers the optional Google SSO gate (superlore/auth). Self-disabling
1818
+ // without AUTH_GOOGLE_ID, so it's harmless until the env is set.
1819
+ ...authEnabled ? { "next-auth": "^5.0.0-beta.25" } : {},
1820
+ next: "16.2.4",
1821
+ "next-themes": "^0.4.6",
1822
+ react: "^19.2.5",
1823
+ "react-dom": "^19.2.5",
1824
+ zod: "^4.4.3"
1825
+ },
1826
+ devDependencies: {
1827
+ "@tailwindcss/postcss": "^4.2.2",
1828
+ "@types/node": "^25.6.0",
1829
+ "@types/react": "^19.2.14",
1830
+ "@types/react-dom": "^19.2.3",
1831
+ postcss: "^8.5.10",
1832
+ tailwindcss: "^4.2.2",
1833
+ typescript: "6.0.3"
1834
+ }
1835
+ },
1836
+ null,
1837
+ 2
1838
+ )}
1839
+ `
1840
+ );
1841
+ write(
1842
+ "tsconfig.json",
1843
+ `${JSON.stringify(
1844
+ {
1845
+ compilerOptions: {
1846
+ target: "ES2022",
1847
+ lib: ["dom", "dom.iterable", "esnext"],
1848
+ module: "esnext",
1849
+ moduleResolution: "bundler",
1850
+ strict: true,
1851
+ noEmit: true,
1852
+ jsx: "preserve",
1853
+ esModuleInterop: true,
1854
+ resolveJsonModule: true,
1855
+ isolatedModules: true,
1856
+ skipLibCheck: true,
1857
+ incremental: true,
1858
+ paths: { "@/*": ["./*"], "collections/*": ["./.source/*"] },
1859
+ plugins: [{ name: "next" }]
1860
+ },
1861
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1862
+ exclude: ["node_modules", ".next", "out"]
1863
+ },
1864
+ null,
1865
+ 2
1866
+ )}
1867
+ `
1868
+ );
1869
+ write(
1870
+ "next.config.mjs",
1871
+ `import { createMDX } from "fumadocs-mdx/next";
1872
+
1873
+ const withMDX = createMDX();
1874
+
1875
+ /** @type {import('next').NextConfig} */
1876
+ const config = {
1877
+ reactStrictMode: true,
1878
+ // superlore ships compiled ESM \u2014 consume it as a normal package (no transpilePackages).
1879
+ };
1880
+
1881
+ export default withMDX(config);
1882
+ `
1883
+ );
1884
+ write(
1885
+ "source.config.ts",
1886
+ `import { defineConfig, defineDocs } from "fumadocs-mdx/config";
1887
+ import { superloreFrontmatterSchema } from "superlore/frontmatter";
1888
+ import { remarkSuperloreCanvas } from "superlore/mdx";
1889
+
1890
+ // Extend the superlore frontmatter schema in ONE place if you add custom fields.
1891
+ export const docs = defineDocs({
1892
+ dir: "content/docs",
1893
+ docs: { schema: superloreFrontmatterSchema },
1894
+ });
1895
+
1896
+ export default defineConfig({
1897
+ mdxOptions: {
1898
+ // Turn fenced \`\`\`superlore-canvas JSON blocks into <Canvas json="\u2026" /> \u2014 the headline
1899
+ // authoring path: write a whiteboard as a code block, get a rendered + serialized canvas.
1900
+ remarkPlugins: [remarkSuperloreCanvas],
1901
+ },
1902
+ });
1903
+ `
1904
+ );
1905
+ write(
1906
+ "postcss.config.mjs",
1907
+ `const config = {
1908
+ plugins: { "@tailwindcss/postcss": {} },
1909
+ };
1910
+
1911
+ export default config;
1912
+ `
1913
+ );
1914
+ write(
1915
+ "app/global.css",
1916
+ `@import "tailwindcss";
1917
+ /* superlore theme tokens (light + dark, co-equal). Re-skin the whole KB from one accent. */
1918
+ @import "superlore/css";
1919
+
1920
+ :root {
1921
+ /* Brand accent \u2014 superlore derives the hover/weak/border/text family for both themes. */
1922
+ --kp-accent: ${accent2};
1923
+ }
1924
+ `
1925
+ );
1926
+ write(
1927
+ "app/layout.tsx",
1928
+ `import type { ReactNode } from "react";
1929
+ import { RootProvider } from "superlore/ui";
1930
+ import "./global.css";
1931
+
1932
+ export default function RootLayout({ children }: { children: ReactNode }) {
1933
+ // Light and dark are co-equal; default to the system preference.
1934
+ return (
1935
+ <html lang="en" suppressHydrationWarning>
1936
+ <body>
1937
+ <RootProvider>{children}</RootProvider>
1938
+ </body>
1939
+ </html>
1940
+ );
1941
+ }
1942
+ `
1943
+ );
1944
+ write(
1945
+ "app/page.tsx",
1946
+ `import { redirect } from "next/navigation";
1947
+
1948
+ // A superlore KB *is* its docs \u2014 there's no separate marketing home to maintain. Land visitors
1949
+ // straight in the docs (full nav, header, sidebar, search). Replace this with a custom landing
1950
+ // only if you actually want one.
1951
+ export default function HomePage() {
1952
+ redirect("/docs");
1953
+ }
1954
+ `
1955
+ );
1956
+ write(
1957
+ "app/docs/layout.tsx",
1958
+ `import type { ReactNode } from "react";
1959
+ import { DocsLayout } from "fumadocs-ui/layouts/docs";
1960
+ import { source } from "@/lib/source";
1961
+
1962
+ export default function Layout({ children }: { children: ReactNode }) {
1963
+ return (
1964
+ <DocsLayout tree={source.pageTree} nav={{ title: "${escapeForJsx(config.name)}" }}>
1965
+ {children}
1966
+ </DocsLayout>
1967
+ );
1968
+ }
1969
+ `
1970
+ );
1971
+ write(
1972
+ "app/docs/[[...slug]]/page.tsx",
1973
+ `import { notFound } from "next/navigation";
1974
+ import { DocsBody, DocsPage } from "fumadocs-ui/page";
1975
+ import { getMDXComponents } from "superlore";
1976
+ import { source } from "@/lib/source";
1977
+
1978
+ export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
1979
+ const params = await props.params;
1980
+ const page = source.getPage(params.slug);
1981
+ if (!page) notFound();
1982
+ const MDX = page.data.body;
1983
+ return (
1984
+ <DocsPage toc={page.data.toc}>
1985
+ <DocsBody>
1986
+ <MDX components={getMDXComponents()} />
1987
+ </DocsBody>
1988
+ </DocsPage>
1989
+ );
1990
+ }
1991
+
1992
+ export function generateStaticParams() {
1993
+ return source.generateParams();
1994
+ }
1995
+ `
1996
+ );
1997
+ write(
1998
+ "lib/source.ts",
1999
+ `import { docs } from "collections/server";
2000
+ import { loader, lucideIconsPlugin } from "superlore/source";
2001
+
2002
+ export const source = loader({
2003
+ baseUrl: "/docs",
2004
+ source: docs.toFumadocsSource(),
2005
+ plugins: [lucideIconsPlugin()],
2006
+ });
2007
+ `
2008
+ );
2009
+ writeContent(write, config);
2010
+ if (authEnabled) {
2011
+ writeAuth(write, config);
2012
+ }
2013
+ if (mcpEnabled) {
2014
+ const mcpPath = config.mcp?.path ?? "/api/mcp";
2015
+ write(
2016
+ "app/api/[transport]/route.ts",
2017
+ `import { createMcpHandler } from "mcp-handler";
2018
+ import { z } from "zod";
2019
+ import { getComponentData, getPage, list, navigate, search } from "superlore/mcp";
2020
+ import { buildIndexFromSource } from "superlore/source";
2021
+ import type { KKind } from "superlore";
2022
+ import { source } from "@/lib/source";
2023
+
2024
+ // Your KB's MCP endpoint. Served at ${mcpPath} \u2014 the same structured content the site renders,
2025
+ // exposed to agents. The index is built straight from your content \`source\`: author once, and
2026
+ // humans read the pages while agents query this corpus. No scraping, no drift.
2027
+ const index = buildIndexFromSource(source);
2028
+
2029
+ const json = (data: unknown) => ({
2030
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
2031
+ });
2032
+
2033
+ const handler = createMcpHandler(
2034
+ (server) => {
2035
+ server.tool(
2036
+ "search",
2037
+ "Full-text search across the knowledge base.",
2038
+ { query: z.string(), limit: z.number().int().positive().optional() },
2039
+ async ({ query, limit }) => json(search(index, query, limit)),
2040
+ );
2041
+ server.tool(
2042
+ "get_page",
2043
+ "Get a page's full structured content by path.",
2044
+ { path: z.string() },
2045
+ async ({ path }) => json(getPage(index, path)),
2046
+ );
2047
+ server.tool(
2048
+ "list",
2049
+ "List knowledge nodes, filtered by kind / tag / entityType.",
2050
+ { kind: z.string().optional(), tag: z.string().optional(), entityType: z.string().optional() },
2051
+ async ({ kind, tag, entityType }) =>
2052
+ json(list(index, { kind: kind as KKind | undefined, tag, entityType })),
2053
+ );
2054
+ server.tool(
2055
+ "navigate",
2056
+ "Follow relations from a page path / node id / entity ref.",
2057
+ { target: z.string() },
2058
+ async ({ target }) => json(navigate(index, target)),
2059
+ );
2060
+ server.tool(
2061
+ "get_component_data",
2062
+ "Get the structured data behind a rendered component (its knowledge face).",
2063
+ { id: z.string() },
2064
+ async ({ id }) => json(getComponentData(index, id)),
2065
+ );
2066
+ },
2067
+ {},
2068
+ { basePath: "/api" },
2069
+ );
2070
+
2071
+ export { handler as GET, handler as POST };
2072
+ `
2073
+ );
2074
+ }
2075
+ write(
2076
+ ".gitignore",
2077
+ `node_modules
2078
+ .next
2079
+ .source
2080
+ out
2081
+ *.tsbuildinfo
2082
+ .env*.local
2083
+ `
2084
+ );
2085
+ if (authEnabled) {
2086
+ write(
2087
+ ".env.example",
2088
+ `# Auth.js v5 + Google SSO. The gate is OFF until AUTH_GOOGLE_ID is set, so local dev works with
2089
+ # this file empty. Copy to .env.local and fill in to enable it on a deploy.
2090
+ AUTH_SECRET= # \`openssl rand -base64 32\`
2091
+ AUTH_GOOGLE_ID= # presence of this turns the gate ON
2092
+ AUTH_GOOGLE_SECRET=
2093
+ AUTH_URL= # the deploy's canonical URL, e.g. https://your-kb.vercel.app
2094
+ AUTH_TRUST_HOST=true
2095
+ ${config.auth?.allowedDomain ? `AUTH_ALLOWED_DOMAIN=${config.auth.allowedDomain}` : "# AUTH_ALLOWED_DOMAIN=example.com # restrict sign-in to one workspace domain (optional)"}
2096
+ # AUTH_ALLOWED_EMAILS=you@example.com # comma-separated allowlist that bypasses the domain check
2097
+ # LOCAL=true # force the gate OFF locally even when configured
2098
+ `
2099
+ );
2100
+ }
2101
+ const authReadme = authEnabled ? `
1206
2102
 
1207
- <SectionHead
1208
- eyebrow="Code review"
1209
- title="How I give PR comments"
1210
- description="What I block on, what I just nudge, and how I phrase it. Replace with your own."
1211
- />
2103
+ ## Auth
1212
2104
 
1213
- ## My review bar
2105
+ This KB ships a Google SSO gate (Auth.js v5). It is **off until you set \`AUTH_GOOGLE_ID\`**, so local dev runs open. Copy \`.env.example\` to \`.env.local\` and fill it in to enable it. The gate lives in \`proxy.ts\`, in front of every route \u2014 so the MCP inherits it too.` : "";
2106
+ write(
2107
+ "README.md",
2108
+ `# ${config.name}
1214
2109
 
1215
- <Checklist
1216
- label="What I look for, in order"
1217
- items={[
1218
- { text: "Correctness \u2014 does it do the thing, including the edge cases?", group: "Block on" },
1219
- { text: "Tests that would fail without the change", group: "Block on" },
1220
- { text: "Names and structure I can read in one pass", group: "Block on" },
1221
- { text: "Unnecessary abstraction or dead code", group: "Nudge on" },
1222
- { text: "Comments that explain why, not what", group: "Nudge on" },
1223
- { text: "Nits \u2014 formatting, ordering, taste", group: "Optional" },
1224
- ]}
1225
- />
2110
+ A superlore knowledge base. One corpus. Humans and agents.
1226
2111
 
1227
- ## How I phrase comments
2112
+ ## Develop
1228
2113
 
1229
- I prefix to signal weight: **blocking:** must change, **suggestion:** take it or leave it,
1230
- **nit:** ignore freely, **question:** I genuinely don't know. Examples:
2114
+ \`\`\`sh
2115
+ pnpm install # or npm / yarn / bun
2116
+ superlore dev # preview the site locally
2117
+ \`\`\`
1231
2118
 
1232
- <Example title="A blocking comment">
1233
- **blocking:** This drops the error on the floor \u2014 if the fetch fails we return \`undefined\` and
1234
- the caller renders an empty state as if it were success. Surface it, or handle it explicitly.
1235
- </Example>
2119
+ ## Build
1236
2120
 
1237
- <Example title="A suggestion">
1238
- **suggestion:** This loop reads cleanly, but \`items.flatMap\` would say the same thing in one line.
1239
- Your call \u2014 not blocking.
1240
- </Example>
2121
+ \`\`\`sh
2122
+ superlore build
2123
+ \`\`\`
1241
2124
 
1242
- <Example title="A nit">
1243
- **nit:** tiny \u2014 can we name this \`pendingCount\` so it matches the others? Ignore if you disagree.
1244
- </Example>
2125
+ Config lives in \`superlore.json\`. Author content in \`content/docs/\`.${mcpEnabled ? `
1245
2126
 
1246
- <Tip title="Tone">
1247
- Critique the code, never the author. Lead with the why. If I'd want it softened when it lands on
1248
- my own PR, I soften it.
1249
- </Tip>
2127
+ The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}${authReadme}
1250
2128
  `
1251
2129
  );
2130
+ }
2131
+ function writeAuth(write, config) {
2132
+ const allowedDomain = config.auth?.allowedDomain;
1252
2133
  write(
1253
- "content/docs/voice.mdx",
1254
- `---
1255
- title: Voice & writing
1256
- description: How I sound in writing \u2014 tone, defaults, and what I avoid.
1257
- summary: This person's writing voice \u2014 tone, structure defaults, and explicit do/don't rules an agent should follow when drafting in their name.
1258
- tags: [voice, writing, style]
1259
- ---
1260
-
1261
- <SectionHead
1262
- eyebrow="How I write"
1263
- title="Voice & writing"
1264
- description="So an agent drafting in my name sounds like me, not like a template."
1265
- />
1266
-
1267
- Placeholder \u2014 capture your actual voice. The more specific the do/don't list, the better an agent
1268
- can match you.
1269
-
1270
- <KeyFacts
1271
- items={[
1272
- { label: "Tone", value: "Direct, warm, low ceremony" },
1273
- { label: "Sentence length", value: "Short. Then one longer one to breathe." },
1274
- { label: "Person", value: "First person, active voice" },
1275
- { label: "Jargon", value: "Only when it's the precise word" },
1276
- ]}
1277
- />
1278
-
1279
- ## Do / don't
2134
+ "auth.ts",
2135
+ `import { createSuperloreAuth } from "superlore/auth";
1280
2136
 
1281
- <Comparison
1282
- caption="My writing defaults"
1283
- options={["Do", "Don't"]}
1284
- rows={[
1285
- { criterion: "Openers", cells: ["Get to the point in the first line", "Open with filler pleasantries"] },
1286
- { criterion: "Hedging", cells: ["Say what I think", "It might possibly be worth considering"] },
1287
- { criterion: "Structure", cells: ["Lead with the answer, then the why", "Bury the ask at the end"] },
1288
- { criterion: "Emoji", cells: ["Rarely, and never in serious writing", "Decorate every line"] },
1289
- ]}
1290
- />
2137
+ // Auth.js v5 + Google SSO. Allowlists can come from env (AUTH_ALLOWED_DOMAIN / AUTH_ALLOWED_EMAILS)
2138
+ // or be passed explicitly here. Off until AUTH_GOOGLE_ID is set, so local dev needs no config.
2139
+ export const { handlers, auth, signIn, signOut } = createSuperloreAuth(${allowedDomain ? `{
2140
+ allowedDomain: ${JSON.stringify(allowedDomain)},
2141
+ }` : "{}"});
2142
+ `
2143
+ );
2144
+ write(
2145
+ "app/api/auth/[...nextauth]/route.ts",
2146
+ `import { handlers } from "@/auth";
1291
2147
 
1292
- <Example title="A message in my voice">
1293
- Shipping the import fix today. It was dropping rows when a column was empty \u2014 now we skip the row
1294
- and log it. One follow-up: we should validate on upload so this can't happen again. Want me to take
1295
- that next?
1296
- </Example>
2148
+ export const { GET, POST } = handlers;
1297
2149
  `
1298
2150
  );
1299
2151
  write(
1300
- "content/docs/stories.mdx",
1301
- `---
1302
- title: Stories
1303
- description: Formative moments that explain how I got my defaults.
1304
- summary: Formative experiences that shaped this person's beliefs and working style, as a dated timeline an agent can reference for context on why they think the way they do.
1305
- tags: [stories, background, timeline]
1306
- ---
2152
+ "proxy.ts",
2153
+ `import { auth } from "@/auth";
2154
+ import { createAuthProxy } from "superlore/auth";
1307
2155
 
1308
- <SectionHead
1309
- eyebrow="Where it comes from"
1310
- title="Stories"
1311
- description="The moments behind the takes. Replace these with your own \u2014 dates can be approximate."
1312
- />
2156
+ // Next.js 16 middleware lives in proxy.ts. The gate is self-disabling: with no AUTH_GOOGLE_ID
2157
+ // (or LOCAL=true) every request passes through, so local dev and public deploys stay open.
2158
+ export default createAuthProxy(auth);
1313
2159
 
1314
- An agent that knows *why* you believe something reasons better than one that only knows *what*.
1315
- These are placeholders.
2160
+ export const config = {
2161
+ // Run on everything except static assets (the helper also skips the auth dance + icons).
2162
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
2163
+ };
2164
+ `
2165
+ );
2166
+ write(
2167
+ "app/auth/signin/page.tsx",
2168
+ `import { signIn } from "@/auth";
1316
2169
 
1317
- <Timeline
1318
- label="Formative moments"
1319
- items={[
1320
- {
1321
- date: "2016",
1322
- title: "The outage that taught me to write things down",
1323
- body: "A fix lived only in one person's head. When they were out, we relearned it the hard way. I've defaulted to docs ever since.",
1324
- status: "done",
1325
- tags: ["process", "writing"],
1326
- },
1327
- {
1328
- date: "2019",
1329
- title: "Shipped small for the first time",
1330
- body: "Replaced a six-month rewrite with weekly reversible changes. It landed. I stopped believing in big-bang.",
1331
- status: "done",
1332
- tags: ["shipping"],
1333
- },
1334
- {
1335
- date: "2022",
1336
- title: "A review that changed how I review",
1337
- body: "Someone critiqued my code without making me feel small. I've tried to give every review that way since.",
1338
- status: "done",
1339
- tags: ["code-review", "tone"],
1340
- },
1341
- ]}
1342
- />
2170
+ export default async function SignInPage(props: {
2171
+ searchParams: Promise<{ callbackUrl?: string }>;
2172
+ }) {
2173
+ const { callbackUrl } = await props.searchParams;
2174
+ return (
2175
+ <main className="grid min-h-screen place-items-center p-6">
2176
+ <form
2177
+ action={async () => {
2178
+ "use server";
2179
+ await signIn("google", { redirectTo: callbackUrl ?? "/" });
2180
+ }}
2181
+ className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center"
2182
+ >
2183
+ <h1 className="text-lg font-semibold text-fd-foreground">Sign in</h1>
2184
+ <p className="mt-1 text-sm text-fd-muted-foreground">
2185
+ This knowledge base is private. Continue with Google to read it.
2186
+ </p>
2187
+ <button
2188
+ type="submit"
2189
+ className="mt-5 inline-flex w-full items-center justify-center rounded-md border border-kp-accent-border bg-kp-accent px-4 py-2 text-sm font-medium text-white"
2190
+ >
2191
+ Continue with Google
2192
+ </button>
2193
+ </form>
2194
+ </main>
2195
+ );
2196
+ }
1343
2197
  `
1344
2198
  );
2199
+ write(
2200
+ "app/auth/error/page.tsx",
2201
+ `export default async function AuthErrorPage(props: {
2202
+ searchParams: Promise<{ error?: string }>;
2203
+ }) {
2204
+ const { error } = await props.searchParams;
2205
+ return (
2206
+ <main className="grid min-h-screen place-items-center p-6">
2207
+ <div className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center">
2208
+ <h1 className="text-lg font-semibold text-fd-foreground">Can't sign in</h1>
2209
+ <p className="mt-1 text-sm text-fd-muted-foreground">
2210
+ {error === "AccessDenied"
2211
+ ? "That account isn't allowed to access this knowledge base."
2212
+ : "Something went wrong signing in. Try again."}
2213
+ </p>
2214
+ <a
2215
+ href="/auth/signin"
2216
+ className="mt-5 inline-flex w-full items-center justify-center rounded-md border border-fd-border px-4 py-2 text-sm font-medium text-fd-foreground no-underline"
2217
+ >
2218
+ Back to sign in
2219
+ </a>
2220
+ </div>
2221
+ </main>
2222
+ );
1345
2223
  }
1346
- function escapeForJsx(value) {
1347
- return value.replace(/[\\"]/g, "\\$&").replace(/[{}]/g, "");
2224
+ `
2225
+ );
1348
2226
  }
1349
2227
 
1350
2228
  // src/commands/init.ts
@@ -1459,9 +2337,41 @@ ${result.issues.map((i) => ` - ${i.path} ${i.message}`).join("\n")}`
1459
2337
  `This is a ${bold(kind)} but auth is ${bold("not enabled")}. A ${kind} should gate access before you deploy \u2014 re-run with ${cyan("--auth")} or set ${cyan('"auth": { "enabled": true }')} in superlore.json.`
1460
2338
  );
1461
2339
  }
2340
+ printStructure(root);
1462
2341
  printNextSteps(root, config);
1463
2342
  await maybeConnectEditor(flags, interactive);
1464
2343
  }
2344
+ function printStructure(root) {
2345
+ const docs = join4(root, "content", "docs");
2346
+ if (!existsSync4(docs)) return;
2347
+ const lines = [];
2348
+ const walk = (dir, depth) => {
2349
+ let names;
2350
+ try {
2351
+ names = readdirSync2(dir).sort((a, b) => a.localeCompare(b));
2352
+ } catch {
2353
+ return;
2354
+ }
2355
+ const dirs = names.filter((n) => !n.startsWith(".") && statSync(join4(dir, n)).isDirectory());
2356
+ const pages = names.filter((n) => n.endsWith(".mdx"));
2357
+ for (const name of dirs) {
2358
+ lines.push(`${" ".repeat(depth)}${cyan(`${name}/`)}`);
2359
+ walk(join4(dir, name), depth + 1);
2360
+ }
2361
+ for (const name of pages) {
2362
+ lines.push(`${" ".repeat(depth)}${name}`);
2363
+ }
2364
+ };
2365
+ walk(docs, 1);
2366
+ log.blank();
2367
+ log.info(bold("Your starter structure"));
2368
+ log.info(`${dim(" content/docs/")} ${dim("\u2014 a full KB, pre-filled with placeholder content:")}`);
2369
+ for (const line of lines) log.info(line);
2370
+ log.blank();
2371
+ log.info(
2372
+ `${bold("Now bring your content in.")} Keep the structure and replace the placeholder pages with what's true for you \u2014 page by page, or ask your agent: ${cyan('"fill in my superlore KB"')}.`
2373
+ );
2374
+ }
1465
2375
  async function maybeConnectEditor(flags, interactive) {
1466
2376
  if (flags.connect === false) return;
1467
2377
  const editors = detectEditors();
@@ -1508,7 +2418,7 @@ function printNextSteps(root, config) {
1508
2418
  }
1509
2419
 
1510
2420
  // src/index.ts
1511
- var VERSION = "0.3.0";
2421
+ var VERSION = "0.4.0";
1512
2422
  function buildCli(argv = process3.argv) {
1513
2423
  const cli = cac("superlore");
1514
2424
  cli.command("init [dir]", "Scaffold a new superlore knowledge base").option("--name <name>", "KB name").option("--type <type>", "KB type: company-kb | product-docs | personal-kb").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("--connect", "Install the editor extension after scaffolding (skip the prompt)").option("--no-connect", "Don't set up the editor extension").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").example("superlore init me --type personal-kb").action(