superlore-cli 0.3.1 → 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
@@ -529,102 +529,25 @@ async function devCommand(flags) {
529
529
  }
530
530
 
531
531
  // src/commands/init.ts
532
- import { existsSync as existsSync4 } from "fs";
533
- 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";
534
534
  import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
535
535
 
536
536
  // src/lib/scaffold.ts
537
537
  import { cpSync, existsSync as existsSync3, mkdirSync, readdirSync, writeFileSync as writeFileSync2 } from "fs";
538
538
  import { fileURLToPath } from "url";
539
539
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
540
- var here = dirname2(fileURLToPath(import.meta.url));
541
- function findStarterTemplate() {
542
- let dir = here;
543
- for (; ; ) {
544
- const candidate = join3(dir, "templates", "starter");
545
- if (existsSync3(candidate)) return candidate;
546
- const bundled = join3(dir, "template");
547
- if (existsSync3(bundled)) return bundled;
548
- const parent = dirname2(dir);
549
- if (parent === dir) return void 0;
550
- dir = parent;
551
- }
552
- }
553
- function isUsableTemplate(dir) {
554
- if (!existsSync3(dir)) return false;
555
- const entries = readdirSync(dir).filter((name) => name !== "README.md" && !name.startsWith("."));
556
- return entries.length > 0;
557
- }
558
- function isEmptyDir(dir) {
559
- if (!existsSync3(dir)) return true;
560
- return readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0;
561
- }
562
- function scaffold(options) {
563
- const root = resolve2(options.dir);
564
- mkdirSync(root, { recursive: true });
565
- const template = findStarterTemplate();
566
- let source;
567
- if (template && isUsableTemplate(template)) {
568
- cpSync(template, root, { recursive: true });
569
- source = "template";
570
- } else {
571
- writeSkeleton(root, options.config);
572
- source = "skeleton";
573
- }
574
- writeFileSync2(join3(root, "superlore.json"), serializeSuperloreJson(options.config), "utf8");
575
- return { root, source };
576
- }
577
- function writeSkeleton(root, config) {
578
- const accent2 = config.accent ?? SUPERLORE_VIOLET;
579
- const mcpEnabled = config.mcp?.enabled ?? true;
580
- const authEnabled = config.auth?.enabled ?? false;
581
- const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "superlore-kb";
582
- const write = (rel, body) => {
583
- const file = join3(root, rel);
584
- mkdirSync(dirname2(file), { recursive: true });
585
- writeFileSync2(file, body, "utf8");
586
- };
540
+
541
+ // src/lib/content/company.ts
542
+ function writeCompanyContent(write, config) {
587
543
  write(
588
- "package.json",
544
+ "content/docs/meta.json",
589
545
  `${JSON.stringify(
590
546
  {
591
- name: slug,
592
- version: "0.1.0",
593
- private: true,
594
- type: "module",
595
- scripts: {
596
- dev: "next dev",
597
- build: "next build",
598
- start: "next start",
599
- postinstall: "fumadocs-mdx"
600
- },
601
- dependencies: {
602
- "fumadocs-core": "16.8.2",
603
- "fumadocs-mdx": "14.3.1",
604
- "fumadocs-ui": "16.8.2",
605
- superlore: "^0.5.1",
606
- "lucide-react": "^1.21.0",
607
- // superlore peers the rendered components pull in: Mermaid (Diagram), themes.
608
- mermaid: "^11.15.0",
609
- ...mcpEnabled ? { "@modelcontextprotocol/sdk": "^1.29.0", "mcp-handler": "^1.1.0" } : {},
610
- // Auth.js v5 powers the optional Google SSO gate (superlore/auth). Self-disabling
611
- // without AUTH_GOOGLE_ID, so it's harmless until the env is set.
612
- ...authEnabled ? { "next-auth": "^5.0.0-beta.25" } : {},
613
- next: "16.2.4",
614
- "next-themes": "^0.4.6",
615
- react: "^19.2.5",
616
- "react-dom": "^19.2.5",
617
- zod: "^4.4.3"
618
- },
619
- devDependencies: {
620
- "@tailwindcss/postcss": "^4.2.2",
621
- "@types/node": "^25.6.0",
622
- "@types/react": "^19.2.14",
623
- "@types/react-dom": "^19.2.3",
624
- postcss: "^8.5.10",
625
- tailwindcss: "^4.2.2",
626
- typescript: "6.0.3"
627
- }
547
+ title: config.name,
548
+ root: true,
549
+ icon: "BookOpen",
550
+ pages: ["index", "engineering", "product", "team"]
628
551
  },
629
552
  null,
630
553
  2
@@ -632,408 +555,830 @@ function writeSkeleton(root, config) {
632
555
  `
633
556
  );
634
557
  write(
635
- "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",
636
641
  `${JSON.stringify(
637
- {
638
- compilerOptions: {
639
- target: "ES2022",
640
- lib: ["dom", "dom.iterable", "esnext"],
641
- module: "esnext",
642
- moduleResolution: "bundler",
643
- strict: true,
644
- noEmit: true,
645
- jsx: "preserve",
646
- esModuleInterop: true,
647
- resolveJsonModule: true,
648
- isolatedModules: true,
649
- skipLibCheck: true,
650
- incremental: true,
651
- paths: { "@/*": ["./*"], "collections/*": ["./.source/*"] },
652
- plugins: [{ name: "next" }]
653
- },
654
- include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
655
- exclude: ["node_modules", ".next", "out"]
656
- },
642
+ { title: "Engineering", icon: "Layers", pages: ["architecture", "decisions", "runbooks"] },
657
643
  null,
658
644
  2
659
645
  )}
660
646
  `
661
647
  );
662
648
  write(
663
- "next.config.mjs",
664
- `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
+ ---
665
656
 
666
- 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
+ />
667
662
 
668
- /** @type {import('next').NextConfig} */
669
- const config = {
670
- reactStrictMode: true,
671
- // superlore ships compiled ESM \u2014 consume it as a normal package (no transpilePackages).
672
- };
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
+ \`\`\`
673
703
 
674
- export default withMDX(config);
675
- `
676
- );
677
- write(
678
- "source.config.ts",
679
- `import { defineConfig, defineDocs } from "fumadocs-mdx/config";
680
- import { superloreFrontmatterSchema } from "superlore/frontmatter";
704
+ ## Services
681
705
 
682
- // Extend the superlore frontmatter schema in ONE place if you add custom fields.
683
- export const docs = defineDocs({
684
- dir: "content/docs",
685
- docs: { schema: superloreFrontmatterSchema },
686
- });
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
+ />
687
720
 
688
- export default defineConfig();
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
+ />
689
729
  `
690
730
  );
691
731
  write(
692
- "postcss.config.mjs",
693
- `const config = {
694
- plugins: { "@tailwindcss/postcss": {} },
695
- };
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
+ ---
696
739
 
697
- export default config;
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
+ />
762
+
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
+ />
698
777
  `
699
778
  );
700
779
  write(
701
- "app/global.css",
702
- `@import "tailwindcss";
703
- /* superlore theme tokens (light + dark, co-equal). Re-skin the whole KB from one accent. */
704
- @import "superlore/css";
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
+ ---
705
787
 
706
- :root {
707
- /* Brand accent \u2014 superlore derives the hover/weak/border/text family for both themes. */
708
- --kp-accent: ${accent2};
709
- }
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>
710
822
  `
711
823
  );
712
824
  write(
713
- "app/layout.tsx",
714
- `import type { ReactNode } from "react";
715
- import { RootProvider } from "superlore/ui";
716
- import "./global.css";
717
-
718
- export default function RootLayout({ children }: { children: ReactNode }) {
719
- // Light and dark are co-equal; default to the system preference.
720
- return (
721
- <html lang="en" suppressHydrationWarning>
722
- <body>
723
- <RootProvider>{children}</RootProvider>
724
- </body>
725
- </html>
726
- );
727
- }
825
+ "content/docs/product/meta.json",
826
+ `${JSON.stringify(
827
+ { title: "Product", icon: "Rocket", pages: ["roadmap", "releases"] },
828
+ null,
829
+ 2
830
+ )}
728
831
  `
729
832
  );
730
833
  write(
731
- "app/page.tsx",
732
- `import Link from "next/link";
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
+ ---
733
841
 
734
- export default function HomePage() {
735
- return (
736
- <main style={{ maxWidth: "42rem", margin: "0 auto", padding: "6rem 1.5rem" }}>
737
- <h1 style={{ fontSize: "2rem", fontWeight: 700 }}>${escapeForJsx(config.name)}</h1>
738
- <p style={{ marginTop: "0.75rem", opacity: 0.7 }}>
739
- One corpus. Humans and agents. Start in <Link href="/docs">the docs</Link>.
740
- </p>
741
- </main>
742
- );
743
- }
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
+ />
744
875
  `
745
876
  );
746
877
  write(
747
- "app/docs/layout.tsx",
748
- `import type { ReactNode } from "react";
749
- import { DocsLayout } from "fumadocs-ui/layouts/docs";
750
- import { source } from "@/lib/source";
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
+ ---
751
885
 
752
- export default function Layout({ children }: { children: ReactNode }) {
753
- return (
754
- <DocsLayout tree={source.pageTree} nav={{ title: "${escapeForJsx(config.name)}" }}>
755
- {children}
756
- </DocsLayout>
757
- );
758
- }
759
- `
760
- );
761
- write(
762
- "app/docs/[[...slug]]/page.tsx",
763
- `import { notFound } from "next/navigation";
764
- import { DocsBody, DocsPage } from "fumadocs-ui/page";
765
- import { getMDXComponents } from "superlore";
766
- 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
+ />
767
891
 
768
- export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
769
- const params = await props.params;
770
- const page = source.getPage(params.slug);
771
- if (!page) notFound();
772
- const MDX = page.data.body;
773
- return (
774
- <DocsPage toc={page.data.toc}>
775
- <DocsBody>
776
- <MDX components={getMDXComponents()} />
777
- </DocsBody>
778
- </DocsPage>
779
- );
780
- }
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
+ />
781
904
 
782
- export function generateStaticParams() {
783
- return source.generateParams();
784
- }
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
+ />
785
915
  `
786
916
  );
787
917
  write(
788
- "lib/source.ts",
789
- `import { docs } from "collections/server";
790
- import { loader, lucideIconsPlugin } from "superlore/source";
791
-
792
- export const source = loader({
793
- baseUrl: "/docs",
794
- source: docs.toFumadocsSource(),
795
- plugins: [lucideIconsPlugin()],
796
- });
918
+ "content/docs/team/meta.json",
919
+ `${JSON.stringify(
920
+ { title: "Team", icon: "Users", pages: ["onboarding", "people"] },
921
+ null,
922
+ 2
923
+ )}
797
924
  `
798
925
  );
799
- if (config.type === "personal-kb") {
800
- writePersonalContent(write, config);
801
- } else {
802
- write(
803
- "content/docs/index.mdx",
804
- `---
805
- title: Welcome
806
- description: The home of your superlore knowledge base.
807
- summary: Landing page for the ${config.name} knowledge base.
808
- 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]
809
933
  ---
810
934
 
811
- # 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
+ />
812
940
 
813
- This is your superlore KB. Author once in MDX \u2014 humans get this clean, interactive site, and
814
- 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
815
961
 
816
- 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
+ />
817
973
  `
818
- );
819
- }
820
- if (authEnabled) {
821
- writeAuth(write, config);
822
- }
823
- if (mcpEnabled) {
824
- const mcpPath = config.mcp?.path ?? "/api/mcp";
825
- write(
826
- "app/api/[transport]/route.ts",
827
- `import { createMcpHandler } from "mcp-handler";
828
- import { z } from "zod";
829
- import { getComponentData, getPage, list, navigate, search } from "superlore/mcp";
830
- import { buildIndexFromSource } from "superlore/source";
831
- import type { KKind } from "superlore";
832
- 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
+ ---
833
983
 
834
- // Your KB's MCP endpoint. Served at ${mcpPath} \u2014 the same structured content the site renders,
835
- // exposed to agents. The index is built straight from your content \`source\`: author once, and
836
- // humans read the pages while agents query this corpus. No scraping, no drift.
837
- 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
+ />
838
989
 
839
- const json = (data: unknown) => ({
840
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
841
- });
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
+ />
842
999
 
843
- const handler = createMcpHandler(
844
- (server) => {
845
- server.tool(
846
- "search",
847
- "Full-text search across the knowledge base.",
848
- { query: z.string(), limit: z.number().int().positive().optional() },
849
- async ({ query, limit }) => json(search(index, query, limit)),
850
- );
851
- server.tool(
852
- "get_page",
853
- "Get a page's full structured content by path.",
854
- { path: z.string() },
855
- async ({ path }) => json(getPage(index, path)),
856
- );
857
- server.tool(
858
- "list",
859
- "List knowledge nodes, filtered by kind / tag / entityType.",
860
- { kind: z.string().optional(), tag: z.string().optional(), entityType: z.string().optional() },
861
- async ({ kind, tag, entityType }) =>
862
- json(list(index, { kind: kind as KKind | undefined, tag, entityType })),
863
- );
864
- server.tool(
865
- "navigate",
866
- "Follow relations from a page path / node id / entity ref.",
867
- { target: z.string() },
868
- async ({ target }) => json(navigate(index, target)),
869
- );
870
- server.tool(
871
- "get_component_data",
872
- "Get the structured data behind a rendered component (its knowledge face).",
873
- { id: z.string() },
874
- async ({ id }) => json(getComponentData(index, id)),
875
- );
876
- },
877
- {},
878
- { basePath: "/api" },
879
- );
1000
+ ## Example: a person, as structured data
880
1001
 
881
- 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
+ />
882
1015
  `
883
- );
884
- }
1016
+ );
1017
+ }
1018
+
1019
+ // src/lib/content/product.ts
1020
+ function writeProductContent(write, config) {
885
1021
  write(
886
- ".gitignore",
887
- `node_modules
888
- .next
889
- .source
890
- out
891
- *.tsbuildinfo
892
- .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
+ )}
893
1033
  `
894
1034
  );
895
- if (authEnabled) {
896
- write(
897
- ".env.example",
898
- `# Auth.js v5 + Google SSO. The gate is OFF until AUTH_GOOGLE_ID is set, so local dev works with
899
- # this file empty. Copy to .env.local and fill in to enable it on a deploy.
900
- AUTH_SECRET= # \`openssl rand -base64 32\`
901
- AUTH_GOOGLE_ID= # presence of this turns the gate ON
902
- AUTH_GOOGLE_SECRET=
903
- AUTH_URL= # the deploy's canonical URL, e.g. https://your-kb.vercel.app
904
- AUTH_TRUST_HOST=true
905
- ${config.auth?.allowedDomain ? `AUTH_ALLOWED_DOMAIN=${config.auth.allowedDomain}` : "# AUTH_ALLOWED_DOMAIN=example.com # restrict sign-in to one workspace domain (optional)"}
906
- # AUTH_ALLOWED_EMAILS=you@example.com # comma-separated allowlist that bypasses the domain check
907
- # LOCAL=true # force the gate OFF locally even when configured
908
- `
909
- );
910
- }
911
- const authReadme = authEnabled ? `
912
-
913
- ## Auth
914
-
915
- 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.` : "";
916
1035
  write(
917
- "README.md",
918
- `# ${config.name}
919
-
920
- A superlore knowledge base. One corpus. Humans and agents.
921
-
922
- ## Develop
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
+ ---
923
1043
 
924
- \`\`\`sh
925
- pnpm install # or npm / yarn / bun
926
- superlore dev # preview the site locally
927
- \`\`\`
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
+ />
928
1050
 
929
- ## Build
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>
930
1055
 
931
- \`\`\`sh
932
- superlore build
933
- \`\`\`
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
+ />
934
1063
 
935
- Config lives in \`superlore.json\`. Author content in \`content/docs/\`.${mcpEnabled ? `
1064
+ ## Start here
936
1065
 
937
- The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}${authReadme}
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
+ />
938
1074
  `
939
1075
  );
940
- }
941
- function writeAuth(write, config) {
942
- const allowedDomain = config.auth?.allowedDomain;
943
1076
  write(
944
- "auth.ts",
945
- `import { createSuperloreAuth } from "superlore/auth";
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
+ ---
946
1084
 
947
- // Auth.js v5 + Google SSO. Allowlists can come from env (AUTH_ALLOWED_DOMAIN / AUTH_ALLOWED_EMAILS)
948
- // or be passed explicitly here. Off until AUTH_GOOGLE_ID is set, so local dev needs no config.
949
- export const { handlers, auth, signIn, signOut } = createSuperloreAuth(${allowedDomain ? `{
950
- allowedDomain: ${JSON.stringify(allowedDomain)},
951
- }` : "{}"});
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
+ />
1090
+
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>
952
1125
  `
953
1126
  );
954
1127
  write(
955
- "app/api/auth/[...nextauth]/route.ts",
956
- `import { handlers } from "@/auth";
957
-
958
- export const { GET, POST } = handlers;
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
+ )}
959
1134
  `
960
1135
  );
961
1136
  write(
962
- "proxy.ts",
963
- `import { auth } from "@/auth";
964
- import { createAuthProxy } from "superlore/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
+ ---
965
1144
 
966
- // Next.js 16 middleware lives in proxy.ts. The gate is self-disabling: with no AUTH_GOOGLE_ID
967
- // (or LOCAL=true) every request passes through, so local dev and public deploys stay open.
968
- export default createAuthProxy(auth);
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
+ />
969
1150
 
970
- export const config = {
971
- // Run on everything except static assets (the helper also skips the auth dance + icons).
972
- matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
973
- };
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
+ />
974
1188
  `
975
1189
  );
976
1190
  write(
977
- "app/auth/signin/page.tsx",
978
- `import { signIn } from "@/auth";
979
-
980
- export default async function SignInPage(props: {
981
- searchParams: Promise<{ callbackUrl?: string }>;
982
- }) {
983
- const { callbackUrl } = await props.searchParams;
984
- return (
985
- <main className="grid min-h-screen place-items-center p-6">
986
- <form
987
- action={async () => {
988
- "use server";
989
- await signIn("google", { redirectTo: callbackUrl ?? "/" });
990
- }}
991
- className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center"
992
- >
993
- <h1 className="text-lg font-semibold text-fd-foreground">Sign in</h1>
994
- <p className="mt-1 text-sm text-fd-muted-foreground">
995
- This knowledge base is private. Continue with Google to read it.
996
- </p>
997
- <button
998
- type="submit"
999
- 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"
1000
- >
1001
- Continue with Google
1002
- </button>
1003
- </form>
1004
- </main>
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
+ ---
1198
+
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
+ />
1204
+
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
+ />
1226
+ `
1005
1227
  );
1006
- }
1228
+ write(
1229
+ "content/docs/guides/meta.json",
1230
+ `${JSON.stringify(
1231
+ { title: "Guides", icon: "BookOpen", pages: ["first-integration"] },
1232
+ null,
1233
+ 2
1234
+ )}
1007
1235
  `
1008
1236
  );
1009
1237
  write(
1010
- "app/auth/error/page.tsx",
1011
- `export default async function AuthErrorPage(props: {
1012
- searchParams: Promise<{ error?: string }>;
1013
- }) {
1014
- const { error } = await props.searchParams;
1015
- return (
1016
- <main className="grid min-h-screen place-items-center p-6">
1017
- <div className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center">
1018
- <h1 className="text-lg font-semibold text-fd-foreground">Can't sign in</h1>
1019
- <p className="mt-1 text-sm text-fd-muted-foreground">
1020
- {error === "AccessDenied"
1021
- ? "That account isn't allowed to access this knowledge base."
1022
- : "Something went wrong signing in. Try again."}
1023
- </p>
1024
- <a
1025
- href="/auth/signin"
1026
- 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"
1027
- >
1028
- Back to sign in
1029
- </a>
1030
- </div>
1031
- </main>
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
+ ---
1245
+
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
+ `
1032
1284
  );
1033
- }
1285
+ write(
1286
+ "content/docs/reference/meta.json",
1287
+ `${JSON.stringify(
1288
+ { title: "Reference", icon: "Settings2", pages: ["configuration"] },
1289
+ null,
1290
+ 2
1291
+ )}
1292
+ `
1293
+ );
1294
+ write(
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
+ `
1339
+ );
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
+ />
1034
1377
  `
1035
1378
  );
1036
1379
  }
1380
+
1381
+ // src/lib/content/personal.ts
1037
1382
  function writePersonalContent(write, config) {
1038
1383
  write(
1039
1384
  "content/docs/meta.json",
@@ -1041,6 +1386,7 @@ function writePersonalContent(write, config) {
1041
1386
  {
1042
1387
  title: config.name,
1043
1388
  root: true,
1389
+ icon: "User",
1044
1390
  pages: ["index", "beliefs", "working-style", "pr-comments", "voice", "stories"]
1045
1391
  },
1046
1392
  null,
@@ -1173,6 +1519,34 @@ would.
1173
1519
  ]}
1174
1520
  />
1175
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
+
1176
1550
  ## How I decide
1177
1551
 
1178
1552
  <Decision
@@ -1200,156 +1574,655 @@ would.
1200
1574
  />
1201
1575
  `
1202
1576
  );
1203
- write(
1204
- "content/docs/pr-comments.mdx",
1205
- `---
1206
- title: How I give PR comments
1207
- description: My review bar, the tone I use, and concrete examples of comments I leave.
1208
- 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.
1209
- tags: [code-review, feedback, how-to]
1210
- ---
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 ? `
1211
2102
 
1212
- <SectionHead
1213
- eyebrow="Code review"
1214
- title="How I give PR comments"
1215
- description="What I block on, what I just nudge, and how I phrase it. Replace with your own."
1216
- />
2103
+ ## Auth
1217
2104
 
1218
- ## 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}
1219
2109
 
1220
- <Checklist
1221
- label="What I look for, in order"
1222
- items={[
1223
- { text: "Correctness \u2014 does it do the thing, including the edge cases?", group: "Block on" },
1224
- { text: "Tests that would fail without the change", group: "Block on" },
1225
- { text: "Names and structure I can read in one pass", group: "Block on" },
1226
- { text: "Unnecessary abstraction or dead code", group: "Nudge on" },
1227
- { text: "Comments that explain why, not what", group: "Nudge on" },
1228
- { text: "Nits \u2014 formatting, ordering, taste", group: "Optional" },
1229
- ]}
1230
- />
2110
+ A superlore knowledge base. One corpus. Humans and agents.
1231
2111
 
1232
- ## How I phrase comments
2112
+ ## Develop
1233
2113
 
1234
- I prefix to signal weight: **blocking:** must change, **suggestion:** take it or leave it,
1235
- **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
+ \`\`\`
1236
2118
 
1237
- <Example title="A blocking comment">
1238
- **blocking:** This drops the error on the floor \u2014 if the fetch fails we return \`undefined\` and
1239
- the caller renders an empty state as if it were success. Surface it, or handle it explicitly.
1240
- </Example>
2119
+ ## Build
1241
2120
 
1242
- <Example title="A suggestion">
1243
- **suggestion:** This loop reads cleanly, but \`items.flatMap\` would say the same thing in one line.
1244
- Your call \u2014 not blocking.
1245
- </Example>
2121
+ \`\`\`sh
2122
+ superlore build
2123
+ \`\`\`
1246
2124
 
1247
- <Example title="A nit">
1248
- **nit:** tiny \u2014 can we name this \`pendingCount\` so it matches the others? Ignore if you disagree.
1249
- </Example>
2125
+ Config lives in \`superlore.json\`. Author content in \`content/docs/\`.${mcpEnabled ? `
1250
2126
 
1251
- <Tip title="Tone">
1252
- Critique the code, never the author. Lead with the why. If I'd want it softened when it lands on
1253
- my own PR, I soften it.
1254
- </Tip>
2127
+ The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}${authReadme}
1255
2128
  `
1256
2129
  );
2130
+ }
2131
+ function writeAuth(write, config) {
2132
+ const allowedDomain = config.auth?.allowedDomain;
1257
2133
  write(
1258
- "content/docs/voice.mdx",
1259
- `---
1260
- title: Voice & writing
1261
- description: How I sound in writing \u2014 tone, defaults, and what I avoid.
1262
- 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.
1263
- tags: [voice, writing, style]
1264
- ---
1265
-
1266
- <SectionHead
1267
- eyebrow="How I write"
1268
- title="Voice & writing"
1269
- description="So an agent drafting in my name sounds like me, not like a template."
1270
- />
1271
-
1272
- Placeholder \u2014 capture your actual voice. The more specific the do/don't list, the better an agent
1273
- can match you.
1274
-
1275
- <KeyFacts
1276
- items={[
1277
- { label: "Tone", value: "Direct, warm, low ceremony" },
1278
- { label: "Sentence length", value: "Short. Then one longer one to breathe." },
1279
- { label: "Person", value: "First person, active voice" },
1280
- { label: "Jargon", value: "Only when it's the precise word" },
1281
- ]}
1282
- />
1283
-
1284
- ## Do / don't
2134
+ "auth.ts",
2135
+ `import { createSuperloreAuth } from "superlore/auth";
1285
2136
 
1286
- <Comparison
1287
- caption="My writing defaults"
1288
- options={["Do", "Don't"]}
1289
- rows={[
1290
- { criterion: "Openers", cells: ["Get to the point in the first line", "Open with filler pleasantries"] },
1291
- { criterion: "Hedging", cells: ["Say what I think", "It might possibly be worth considering"] },
1292
- { criterion: "Structure", cells: ["Lead with the answer, then the why", "Bury the ask at the end"] },
1293
- { criterion: "Emoji", cells: ["Rarely, and never in serious writing", "Decorate every line"] },
1294
- ]}
1295
- />
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";
1296
2147
 
1297
- <Example title="A message in my voice">
1298
- Shipping the import fix today. It was dropping rows when a column was empty \u2014 now we skip the row
1299
- and log it. One follow-up: we should validate on upload so this can't happen again. Want me to take
1300
- that next?
1301
- </Example>
2148
+ export const { GET, POST } = handlers;
1302
2149
  `
1303
2150
  );
1304
2151
  write(
1305
- "content/docs/stories.mdx",
1306
- `---
1307
- title: Stories
1308
- description: Formative moments that explain how I got my defaults.
1309
- 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.
1310
- tags: [stories, background, timeline]
1311
- ---
2152
+ "proxy.ts",
2153
+ `import { auth } from "@/auth";
2154
+ import { createAuthProxy } from "superlore/auth";
1312
2155
 
1313
- <SectionHead
1314
- eyebrow="Where it comes from"
1315
- title="Stories"
1316
- description="The moments behind the takes. Replace these with your own \u2014 dates can be approximate."
1317
- />
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);
1318
2159
 
1319
- An agent that knows *why* you believe something reasons better than one that only knows *what*.
1320
- 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";
1321
2169
 
1322
- <Timeline
1323
- label="Formative moments"
1324
- items={[
1325
- {
1326
- date: "2016",
1327
- title: "The outage that taught me to write things down",
1328
- 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.",
1329
- status: "done",
1330
- tags: ["process", "writing"],
1331
- },
1332
- {
1333
- date: "2019",
1334
- title: "Shipped small for the first time",
1335
- body: "Replaced a six-month rewrite with weekly reversible changes. It landed. I stopped believing in big-bang.",
1336
- status: "done",
1337
- tags: ["shipping"],
1338
- },
1339
- {
1340
- date: "2022",
1341
- title: "A review that changed how I review",
1342
- body: "Someone critiqued my code without making me feel small. I've tried to give every review that way since.",
1343
- status: "done",
1344
- tags: ["code-review", "tone"],
1345
- },
1346
- ]}
1347
- />
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
+ }
1348
2197
  `
1349
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
+ );
1350
2223
  }
1351
- function escapeForJsx(value) {
1352
- return value.replace(/[\\"]/g, "\\$&").replace(/[{}]/g, "");
2224
+ `
2225
+ );
1353
2226
  }
1354
2227
 
1355
2228
  // src/commands/init.ts
@@ -1464,9 +2337,41 @@ ${result.issues.map((i) => ` - ${i.path} ${i.message}`).join("\n")}`
1464
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.`
1465
2338
  );
1466
2339
  }
2340
+ printStructure(root);
1467
2341
  printNextSteps(root, config);
1468
2342
  await maybeConnectEditor(flags, interactive);
1469
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
+ }
1470
2375
  async function maybeConnectEditor(flags, interactive) {
1471
2376
  if (flags.connect === false) return;
1472
2377
  const editors = detectEditors();
@@ -1513,7 +2418,7 @@ function printNextSteps(root, config) {
1513
2418
  }
1514
2419
 
1515
2420
  // src/index.ts
1516
- var VERSION = "0.3.1";
2421
+ var VERSION = "0.4.0";
1517
2422
  function buildCli(argv = process3.argv) {
1518
2423
  const cli = cac("superlore");
1519
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(