camelmind 0.2.1 → 0.2.3

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.
@@ -132,7 +132,7 @@ export function TopNav({ nav, userRoles, userName, authEnabled = false, versions
132
132
  currentVersionId={currentVersionId}
133
133
  currentSlug={currentSlug}
134
134
  versionSlugs={versionSlugs}
135
- isLoggedIn={!!userName}
135
+ isLoggedIn={!!userName || !authEnabled}
136
136
  />
137
137
  {authEnabled && (
138
138
  userName ? (
@@ -21,6 +21,14 @@ export function VersionSelector({ versions, currentVersionId, currentSlug, versi
21
21
 
22
22
  function navigateToVersion(targetId: string) {
23
23
  setOpen(false)
24
+
25
+ // API reference pages use a different URL scheme (/api-reference/[version]/...)
26
+ // so navigate directly to the target version's API reference overview
27
+ if (currentSlug.startsWith("/api-reference")) {
28
+ router.push(`/api-reference/${targetId}`)
29
+ return
30
+ }
31
+
24
32
  const bare = currentVersionId
25
33
  ? currentSlug.replace(new RegExp(`^/${currentVersionId}`), "") || "/"
26
34
  : currentSlug
@@ -23,10 +23,12 @@ const defaultIcons: Record<CalloutType, string> = {
23
23
  export function Callout({
24
24
  type = "note",
25
25
  icon,
26
+ title,
26
27
  children,
27
28
  }: {
28
29
  type?: CalloutType
29
30
  icon?: string
31
+ title?: string
30
32
  children: React.ReactNode
31
33
  }) {
32
34
  const iconName = icon ?? defaultIcons[type]
@@ -35,7 +37,7 @@ export function Callout({
35
37
  <div className={`callout ${type} my-4`}>
36
38
  <div className="callout-header flex items-center gap-1.5">
37
39
  <Icon name={iconName} size={13} />
38
- {labels[type]}
40
+ {title ?? labels[type]}
39
41
  </div>
40
42
  <div className="callout-body">{children}</div>
41
43
  </div>
@@ -0,0 +1,4 @@
1
+ // Renders normally for humans — stripped from .md responses served to LLMs.
2
+ export function LLMIgnore({ children }: { children?: React.ReactNode }) {
3
+ return <>{children}</>
4
+ }
@@ -0,0 +1,4 @@
1
+ // Hidden from the rendered page — visible only in .md responses served to LLMs.
2
+ export function LLMOnly({ children }: { children?: React.ReactNode }) {
3
+ return null
4
+ }
@@ -3,6 +3,8 @@ import { Steps, Step } from "./Steps"
3
3
  import { Tabs, Tab } from "./Tabs"
4
4
  import { Icon } from "./Icon"
5
5
  import { Details } from "./Details"
6
+ import { LLMOnly } from "./LLMOnly"
7
+ import { LLMIgnore } from "./LLMIgnore"
6
8
  import { CodeBlock } from "@/components/Code/CodeBlock"
7
9
 
8
10
  export const mdxComponents = {
@@ -13,6 +15,8 @@ export const mdxComponents = {
13
15
  Tab,
14
16
  Icon,
15
17
  Details,
18
+ LLMOnly,
19
+ LLMIgnore,
16
20
  pre: ({ children }: { children: React.ReactNode }) => {
17
21
  // If the child code block has a language class, let CodeBlock handle it
18
22
  const child = children as React.ReactElement<{ className?: string }>
@@ -15,22 +15,21 @@ import type {
15
15
  HttpMethod,
16
16
  } from "./api-types"
17
17
 
18
- let _cache: ParsedSpec | null = null
19
- let _cachedLanguages: string | null = null
18
+ const _specCache = new Map<string, ParsedSpec>()
20
19
 
21
20
  export function loadApiSpec(specPath: string, languages?: string[]): ParsedSpec {
22
- const langKey = JSON.stringify(languages ?? null)
23
- if (_cache && _cachedLanguages === langKey) return _cache
21
+ const cacheKey = `${specPath}|${JSON.stringify(languages ?? null)}`
22
+ const cached = _specCache.get(cacheKey)
23
+ if (cached) return cached
24
24
  const raw = fs.readFileSync(path.join(process.cwd(), specPath), "utf-8")
25
25
  const doc = yaml.load(raw) as Record<string, unknown>
26
- _cache = parseOpenApi(doc, languages)
27
- _cachedLanguages = langKey
28
- return _cache
26
+ const parsed = parseOpenApi(doc, languages)
27
+ _specCache.set(cacheKey, parsed)
28
+ return parsed
29
29
  }
30
30
 
31
31
  export function clearApiCache() {
32
- _cache = null
33
- _cachedLanguages = null
32
+ _specCache.clear()
34
33
  }
35
34
 
36
35
  function slugify(str: string): string {
@@ -22,14 +22,24 @@ export type SiteFeatures = {
22
22
  showFeedbackWidget?: boolean
23
23
  }
24
24
 
25
+ export type ApiSpecEntry = {
26
+ label: string
27
+ file: string
28
+ }
29
+
25
30
  export type ApiReferenceConfig = {
26
31
  enabled: boolean
27
- spec: string
32
+ specs: Record<string, ApiSpecEntry>
28
33
  navLabel?: string
29
34
  languages?: string[]
30
35
  roles?: string[]
31
36
  }
32
37
 
38
+ export type LlmsConfig = {
39
+ enabled?: boolean
40
+ directive?: string
41
+ }
42
+
33
43
  export type CamelMindConfig = {
34
44
  title: string
35
45
  tagline: string
@@ -43,4 +53,5 @@ export type CamelMindConfig = {
43
53
  }
44
54
  site?: SiteFeatures
45
55
  apiReference?: ApiReferenceConfig
56
+ llms?: LlmsConfig
46
57
  }
@@ -1,7 +1,7 @@
1
1
  import siteConfig from "@/camelmind.config"
2
2
  import type { AuthConfig, CamelMindConfig } from "./config-types"
3
3
 
4
- export type { AuthConfig, AuthProvider, CamelMindConfig, OidcConfig } from "./config-types"
4
+ export type { AuthConfig, AuthProvider, CamelMindConfig, LlmsConfig, OidcConfig } from "./config-types"
5
5
 
6
6
  let _config: CamelMindConfig | null = null
7
7
 
@@ -44,6 +44,19 @@ function extractToc(source: string): TocEntry[] {
44
44
  return entries
45
45
  }
46
46
 
47
+ export function processForLLM(source: string): string {
48
+ let result = source.replace(/<LLMIgnore>[\s\S]*?<\/LLMIgnore>/g, "")
49
+ result = result.replace(/<LLMOnly>([\s\S]*?)<\/LLMOnly>/g, "$1")
50
+ return result.replace(/\n{3,}/g, "\n\n").trim()
51
+ }
52
+
53
+ export function loadFrontmatterOnly(filePath: string): FrontMatter {
54
+ const fullPath = path.join(process.cwd(), filePath)
55
+ const raw = fs.readFileSync(fullPath, "utf-8")
56
+ const { data } = matter(raw)
57
+ return data as FrontMatter
58
+ }
59
+
47
60
  export function loadMdxFile(filePath: string): DocContent {
48
61
  const fullPath = path.join(process.cwd(), filePath)
49
62
  const raw = fs.readFileSync(fullPath, "utf-8")
@@ -8,11 +8,59 @@ export { isNavGroup } from "./nav-types"
8
8
 
9
9
  let _navCache: NavConfig | null = null
10
10
 
11
+ function stripPrefix(slug: string, prefix: string): string {
12
+ if (slug.startsWith(prefix + "/")) return slug.slice(prefix.length)
13
+ if (slug === prefix) return "/"
14
+ return slug
15
+ }
16
+
17
+ function stripNavPrefix(nav: NavConfig, prefix: string): NavConfig {
18
+ const s = (slug: string) => stripPrefix(slug, prefix)
19
+
20
+ const walkChildren = (children: NavChild[]): NavChild[] =>
21
+ children.map((c) => ({
22
+ ...c,
23
+ slug: s(c.slug),
24
+ children: c.children ? walkChildren(c.children) : undefined,
25
+ }))
26
+
27
+ const walkEntries = (entries: NavEntry[]): NavEntry[] =>
28
+ entries.map((e) => ({
29
+ ...e,
30
+ slug: s(e.slug),
31
+ section: e.section ? walkChildren(e.section) : undefined,
32
+ }))
33
+
34
+ return {
35
+ nav: nav.nav.map((item) => {
36
+ if ("dropdown" in item) {
37
+ const group = item as NavGroup
38
+ return {
39
+ ...group,
40
+ ...(group.slug ? { slug: s(group.slug) } : {}),
41
+ items: group.items ? walkEntries(group.items) : undefined,
42
+ }
43
+ }
44
+ const entry = item as NavEntry
45
+ return { ...entry, slug: s(entry.slug) }
46
+ }),
47
+ } as NavConfig
48
+ }
49
+
11
50
  export function loadNav(): NavConfig {
12
51
  if (process.env.NODE_ENV !== "development" && _navCache) return _navCache
13
- const navPath = path.join(process.cwd(), "nav", "nav.yml")
14
- const raw = fs.readFileSync(navPath, "utf-8")
15
- _navCache = yaml.load(raw) as NavConfig
52
+
53
+ // Derive the unversioned nav from the first stable version in versions.yml,
54
+ // stripping the version prefix from all slugs so /v2/foo becomes /foo.
55
+ const versionsPath = path.join(process.cwd(), "versions.yml")
56
+ const { versions } = yaml.load(fs.readFileSync(versionsPath, "utf-8")) as {
57
+ versions: { id: string; stable: boolean; nav: string }[]
58
+ }
59
+ const latest = versions.find((v) => v.stable) ?? versions[0]
60
+ const navPath = path.join(process.cwd(), latest.nav)
61
+ const nav = yaml.load(fs.readFileSync(navPath, "utf-8")) as NavConfig
62
+
63
+ _navCache = stripNavPrefix(nav, `/${latest.id}`)
16
64
  return _navCache
17
65
  }
18
66
 
@@ -123,3 +171,24 @@ export function getSectionForSlugFromConfig(nav: NavConfig, slug: string): NavEn
123
171
  export function getSectionForSlug(slug: string): NavEntry | null {
124
172
  return getSectionForSlugFromConfig(loadNav(), slug)
125
173
  }
174
+
175
+ export function getAllPublicEntries(nav: NavConfig): (NavEntry | NavChild)[] {
176
+ const entries: (NavEntry | NavChild)[] = []
177
+ for (const item of nav.nav) {
178
+ if ("dropdown" in item) {
179
+ const group = item as NavGroup
180
+ for (const entry of (group.items ?? [])) {
181
+ if (entry.roles.length === 0) entries.push(entry)
182
+ if (entry.section) {
183
+ for (const child of flattenChildren(entry.section)) {
184
+ if (child.roles.length === 0) entries.push(child)
185
+ }
186
+ }
187
+ }
188
+ } else if ("slug" in item) {
189
+ const entry = item as NavEntry
190
+ if (entry.roles.length === 0) entries.push(entry)
191
+ }
192
+ }
193
+ return entries
194
+ }
@@ -3,20 +3,24 @@ import path from "path"
3
3
  import yaml from "js-yaml"
4
4
  import { loadNav } from "./nav"
5
5
  import type { NavConfig } from "./nav-types"
6
+ import type { ApiReferenceConfig } from "./config-types"
6
7
 
7
- export type VersionApiReference = {
8
- spec: string
9
- nav_label?: string
10
- languages?: string[]
8
+ export type VersionApiReferenceTab = {
9
+ id: string
10
+ spec: string // key into camelmind.config.ts apiReference.specs
11
11
  }
12
12
 
13
+ export type VersionApiReference =
14
+ | { spec: string } // single named spec
15
+ | { tabs: VersionApiReferenceTab[] } // multi-spec tabs
16
+
13
17
  export type Version = {
14
18
  id: string
15
19
  label: string
16
20
  stable: boolean
17
21
  badge?: string
18
22
  nav: string
19
- // true/omitted = use site-level spec, false = hide, object = version-specific spec
23
+ // false = disabled, omitted/true = use first spec in registry, object = explicit config
20
24
  api_reference?: boolean | VersionApiReference
21
25
  }
22
26
 
@@ -24,6 +28,18 @@ export type VersionsConfig = {
24
28
  versions: Version[]
25
29
  }
26
30
 
31
+ // Resolved output types
32
+ export type ResolvedTab = {
33
+ id: string
34
+ label: string
35
+ file: string
36
+ languages?: string[]
37
+ }
38
+
39
+ export type ResolvedApiReference =
40
+ | { mode: "single"; label: string; file: string; languages?: string[] }
41
+ | { mode: "tabs"; tabs: ResolvedTab[] }
42
+
27
43
  let _versionsCache: VersionsConfig | null = null
28
44
 
29
45
  export function loadVersions(): VersionsConfig {
@@ -33,12 +49,52 @@ export function loadVersions(): VersionsConfig {
33
49
  return _versionsCache
34
50
  }
35
51
 
52
+ // Resolve which spec(s) to show for a given version.
53
+ // Resolution: versions.yml api_reference → apiReference.specs registry → first spec in registry
54
+ // Returns null if api_reference is explicitly disabled for this version.
55
+ export function getApiReferenceForVersion(
56
+ versionId: string | null,
57
+ config: ApiReferenceConfig
58
+ ): ResolvedApiReference | null {
59
+ const { versions } = loadVersions()
60
+
61
+ let versionAr: boolean | VersionApiReference | undefined
62
+ if (versionId) {
63
+ const v = versions.find((v) => v.id === versionId)
64
+ versionAr = v?.api_reference
65
+ }
66
+
67
+ if (versionAr === false) return null
68
+
69
+ // Default: use first spec in registry
70
+ if (!versionAr || versionAr === true) {
71
+ const firstEntry = Object.entries(config.specs)[0]
72
+ if (!firstEntry) return null
73
+ const [, entry] = firstEntry
74
+ return { mode: "single", label: entry.label, file: entry.file, languages: config.languages }
75
+ }
76
+
77
+ // Tabs mode
78
+ if ("tabs" in versionAr) {
79
+ const tabs: ResolvedTab[] = versionAr.tabs.flatMap((tab) => {
80
+ const entry = config.specs[tab.spec]
81
+ if (!entry) return []
82
+ return [{ id: tab.id, label: entry.label, file: entry.file, languages: config.languages }]
83
+ })
84
+ return tabs.length ? { mode: "tabs", tabs } : null
85
+ }
86
+
87
+ // Single named spec
88
+ const entry = config.specs[versionAr.spec]
89
+ if (!entry) return null
90
+ return { mode: "single", label: entry.label, file: entry.file, languages: config.languages }
91
+ }
92
+
36
93
  export function getVersionFromSlug(slug: string): string | null {
37
94
  const { versions } = loadVersions()
38
95
  for (const v of versions) {
39
96
  if (slug.startsWith(`/${v.id}/`) || slug === `/${v.id}`) return v.id
40
97
  }
41
- // no version prefix = default (latest)
42
98
  return null
43
99
  }
44
100
 
@@ -57,30 +113,10 @@ export function getLatestVersion(): Version {
57
113
  return versions[0]
58
114
  }
59
115
 
60
- // Resolve which OpenAPI spec to use for a given version, with site-level fallback.
61
- // Returns null if api_reference is explicitly disabled for this version.
62
- export function getApiSpecForVersion(
63
- versionId: string | null,
64
- siteSpec: string,
65
- siteLanguages?: string[]
66
- ): { spec: string; languages?: string[] } | null {
67
- if (!versionId) return { spec: siteSpec, languages: siteLanguages }
68
- const { versions } = loadVersions()
69
- const v = versions.find((v) => v.id === versionId)
70
- if (!v) return { spec: siteSpec, languages: siteLanguages }
71
-
72
- const ar = v.api_reference
73
- if (ar === false) return null
74
- if (!ar || ar === true) return { spec: siteSpec, languages: siteLanguages }
75
- return { spec: ar.spec, languages: ar.languages ?? siteLanguages }
76
- }
77
-
78
- // Given a slug like /v1.9/getting-started/overview, return /getting-started/overview
79
116
  export function stripVersionPrefix(slug: string, versionId: string): string {
80
117
  return slug.replace(new RegExp(`^/${versionId}`), "") || "/"
81
118
  }
82
119
 
83
- // Given /getting-started/overview and a target version, return /v1.9/getting-started/overview
84
120
  export function swapVersion(slug: string, currentVersionId: string | null, targetVersionId: string): string {
85
121
  const bare = currentVersionId ? stripVersionPrefix(slug, currentVersionId) : slug
86
122
  return `/${targetVersionId}${bare}`
package/template/proxy.ts CHANGED
@@ -6,6 +6,14 @@ import { isAuthEnabled, isPrivateSite, isPublicPath } from "@/lib/config"
6
6
  export async function proxy(req: NextRequest) {
7
7
  const { pathname } = req.nextUrl
8
8
 
9
+ // Rewrite /<slug>.md to /api/llms/<slug> before auth runs.
10
+ // /api/llms is in publicPaths so the handler is always reachable.
11
+ if (pathname.endsWith(".md") && !pathname.startsWith("/api/")) {
12
+ const url = req.nextUrl.clone()
13
+ url.pathname = `/api/llms${pathname.slice(0, -3)}`
14
+ return NextResponse.rewrite(url)
15
+ }
16
+
9
17
  // Auth disabled — all traffic passes through
10
18
  if (!isAuthEnabled()) {
11
19
  return NextResponse.next()
@@ -46,5 +54,6 @@ export async function proxy(req: NextRequest) {
46
54
  }
47
55
 
48
56
  export const config = {
49
- matcher: ["/((?!_next/static|_next/image|favicon.ico|public/).*)"],
57
+ // Exclude Next.js internals and common static asset extensions served from public/
58
+ matcher: ["/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|eot|otf)).*)"],
50
59
  }
@@ -10,7 +10,7 @@
10
10
 
11
11
  set -euo pipefail
12
12
 
13
- VERSION=${1:-v2.0}
13
+ VERSION=${1:-latest}
14
14
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15
15
  OUT_DIR="$ROOT/out"
16
16
  BUILDS_DIR="$ROOT/offline-builds"
@@ -21,6 +21,8 @@ SERVER_ROUTES=(
21
21
  "app/api/raw/route.ts"
22
22
  "app/api/download/route.ts"
23
23
  "app/api/search/route.ts"
24
+ "app/api/feedback/route.ts"
25
+ "app/api/llms"
24
26
  "app/api/auth"
25
27
  )
26
28
 
@@ -48,7 +50,17 @@ restore_routes() {
48
50
  }
49
51
 
50
52
  # Always restore on exit (even on error)
51
- trap restore_routes EXIT
53
+ PAGES_DIR="$ROOT/.pdf-pages"
54
+
55
+ cleanup() {
56
+ restore_routes
57
+ rm -rf "$PAGES_DIR"
58
+ # Kill the static server if still running
59
+ if [[ -n "${STATIC_PID:-}" ]]; then
60
+ kill "$STATIC_PID" 2>/dev/null || true
61
+ fi
62
+ }
63
+ trap cleanup EXIT
52
64
 
53
65
  echo "▶ Building offline package for version: $VERSION"
54
66
 
@@ -69,10 +81,14 @@ OFFLINE_MODE=true TARGET_VERSION="$VERSION" npx next build
69
81
 
70
82
  cat > "$OUT_DIR/launch.sh" <<'LAUNCHER'
71
83
  #!/usr/bin/env bash
72
- # Game Warden Help Center — offline launcher (Mac / Linux)
84
+ # CamelMind offline launcher (Mac / Linux)
85
+ cd "$(dirname "$0")"
73
86
  PORT=8765
74
87
  URL="http://localhost:$PORT/home/"
75
88
 
89
+ # Free the port if a previous server is still running
90
+ lsof -ti:$PORT 2>/dev/null | xargs kill 2>/dev/null || true
91
+
76
92
  # Try Python 3 first (available on most systems without internet)
77
93
  if command -v python3 &>/dev/null; then
78
94
  echo "Starting local server on $URL"
@@ -113,7 +129,8 @@ chmod +x "$OUT_DIR/launch.sh"
113
129
 
114
130
  cat > "$OUT_DIR/launch.bat" <<'LAUNCHER'
115
131
  @echo off
116
- REM Game Warden Help Center — offline launcher (Windows)
132
+ REM CamelMind offline launcher (Windows)
133
+ cd /D "%~dp0"
117
134
  set PORT=8765
118
135
  set URL=http://localhost:%PORT%/home/
119
136
 
@@ -180,7 +197,28 @@ NOTES
180
197
  Do not redistribute without authorization.
181
198
  EOF
182
199
 
183
- # 4. Zip the output
200
+ # 4. Generate PDF from the static export
201
+ echo " → Generating PDF..."
202
+ STATIC_PORT=8766
203
+
204
+ # Serve the static output on a temporary port
205
+ python3 -m http.server "$STATIC_PORT" --directory "$OUT_DIR" >/dev/null 2>&1 &
206
+ STATIC_PID=$!
207
+
208
+ # Wait up to 10s for the server to be ready
209
+ for i in $(seq 1 20); do
210
+ if curl -s -o /dev/null "http://localhost:$STATIC_PORT/home/"; then break; fi
211
+ sleep 0.5
212
+ done
213
+
214
+ npx tsx scripts/generate-pdfs.ts --port="$STATIC_PORT" --out="$PAGES_DIR" --no-auth
215
+ npx tsx scripts/generate-master-pdf.ts --version="$VERSION" --pages="$PAGES_DIR" --out="$BUILDS_DIR"
216
+
217
+ kill "$STATIC_PID" 2>/dev/null || true
218
+ unset STATIC_PID
219
+ rm -rf "$PAGES_DIR"
220
+
221
+ # 5. Zip the output
184
222
  echo " → Packaging..."
185
223
  mkdir -p "$BUILDS_DIR"
186
224
  cd "$OUT_DIR"
@@ -9,7 +9,7 @@
9
9
 
10
10
  set -euo pipefail
11
11
 
12
- VERSION=${1:-v2.0}
12
+ VERSION=${1:-latest}
13
13
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
14
14
  PAGES_DIR="$ROOT/.pdf-pages"
15
15
  PORT=3000
@@ -37,5 +37,5 @@ npx tsx scripts/generate-pdfs.ts --port="$PORT" --out="$PAGES_DIR"
37
37
  echo " → Assembling master PDF..."
38
38
  npx tsx scripts/generate-master-pdf.ts --version="$VERSION" --pages="$PAGES_DIR" --out="$ROOT/offline-builds"
39
39
 
40
- SIZE=$(du -sh "$ROOT/offline-builds/Game-Warden-Help-Center-${VERSION}.pdf" | cut -f1)
41
- echo "✓ Done: offline-builds/Game-Warden-Help-Center-${VERSION}.pdf ($SIZE)"
40
+ SIZE=$(du -sh "$ROOT/offline-builds/camelmind-${VERSION}.pdf" | cut -f1)
41
+ echo "✓ Done: offline-builds/camelmind-${VERSION}.pdf ($SIZE)"
@@ -10,9 +10,15 @@ import path from "path"
10
10
  import matter from "gray-matter"
11
11
 
12
12
  const ROOT = path.resolve(process.cwd())
13
- const NAV_FILE = path.join(ROOT, "nav", "nav.yml")
14
13
  const OUT_FILE = path.join(ROOT, "public", "search-index.json")
15
14
 
15
+ // Resolve the latest stable version's nav file from versions.yml
16
+ const { versions } = yaml.load(fs.readFileSync(path.join(ROOT, "versions.yml"), "utf-8")) as {
17
+ versions: { id: string; stable: boolean; nav: string }[]
18
+ }
19
+ const latestVersion = versions.find((v) => v.stable) ?? versions[0]
20
+ const NAV_FILE = path.join(ROOT, latestVersion.nav)
21
+
16
22
  // Minimal YAML parser for our nav structure (avoids pulling in the full Next.js lib graph)
17
23
  import yaml from "js-yaml"
18
24
 
@@ -12,6 +12,9 @@
12
12
  import { PDFDocument, StandardFonts, rgb, PDFPage } from "pdf-lib"
13
13
  import fs from "fs"
14
14
  import path from "path"
15
+ import config from "../camelmind.config"
16
+
17
+ const SITE_TITLE = config.title
15
18
 
16
19
  const ROOT = path.resolve(process.cwd())
17
20
 
@@ -49,25 +52,15 @@ async function buildCoverPage(doc: PDFDocument): Promise<PDFPage> {
49
52
  // Thin accent line
50
53
  page.drawRectangle({ x: MARGIN, y: PH * 0.45 - 2, width: PW - MARGIN * 2, height: 2, color: rgb(0.3, 0.35, 0.45) })
51
54
 
52
- // "SECOND FRONT SYSTEMS" label
53
- page.drawText("SECOND FRONT SYSTEMS", {
54
- x: MARGIN,
55
- y: PH * 0.45 + 180,
56
- size: 9,
57
- font: regularFont,
58
- color: rgb(0.6, 0.65, 0.75),
59
- characterSpacing: 2,
60
- })
61
-
62
55
  // Main title
63
- page.drawText("Game Warden", {
56
+ page.drawText(SITE_TITLE, {
64
57
  x: MARGIN,
65
58
  y: PH * 0.45 + 130,
66
59
  size: 42,
67
60
  font: boldFont,
68
61
  color: WHITE,
69
62
  })
70
- page.drawText("Help Center", {
63
+ page.drawText("Documentation", {
71
64
  x: MARGIN,
72
65
  y: PH * 0.45 + 78,
73
66
  size: 42,
@@ -94,13 +87,12 @@ async function buildCoverPage(doc: PDFDocument): Promise<PDFPage> {
94
87
 
95
88
  // Footer bar
96
89
  page.drawRectangle({ x: 0, y: 0, width: PW, height: 36, color: LIGHT })
97
- page.drawText("game warden help center · secondfront.com", {
90
+ page.drawText(`${SITE_TITLE.toLowerCase()} documentation`, {
98
91
  x: MARGIN,
99
92
  y: 12,
100
93
  size: 8,
101
94
  font: regularFont,
102
95
  color: MID,
103
- characterSpacing: 0.5,
104
96
  })
105
97
  page.drawText("CONFIDENTIAL — FOR AUTHORIZED USERS ONLY", {
106
98
  x: PW - MARGIN - 220,
@@ -126,7 +118,7 @@ async function buildTocPage(
126
118
 
127
119
  const drawPageHeader = (p: PDFPage) => {
128
120
  p.drawText("TABLE OF CONTENTS", {
129
- x: MARGIN, y: PH - MARGIN + 10, size: 8, font: regularFont, color: MID, characterSpacing: 1.5
121
+ x: MARGIN, y: PH - MARGIN + 10, size: 8, font: regularFont, color: MID,
130
122
  })
131
123
  p.drawLine({ start: { x: MARGIN, y: PH - MARGIN - 4 }, end: { x: PW - MARGIN, y: PH - MARGIN - 4 }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) })
132
124
  }
@@ -152,7 +144,7 @@ async function buildTocPage(
152
144
  if (entry.group !== lastGroup) {
153
145
  if (lastGroup !== "") y -= 6
154
146
  page.drawText(entry.group.toUpperCase(), {
155
- x: MARGIN, y, size: 7.5, font: boldFont, color: MID, characterSpacing: 1,
147
+ x: MARGIN, y, size: 7.5, font: boldFont, color: MID,
156
148
  })
157
149
  y -= 18
158
150
  lastGroup = entry.group
@@ -249,7 +241,7 @@ async function main() {
249
241
 
250
242
  // ── Write output ───────────────────────────────────────────────────────────
251
243
  fs.mkdirSync(OUT_DIR, { recursive: true })
252
- const outFile = path.join(OUT_DIR, `Game-Warden-Help-Center-${VERSION}.pdf`)
244
+ const outFile = path.join(OUT_DIR, `camelmind-${VERSION}.pdf`)
253
245
  const pdfBytes = await masterDoc.save()
254
246
  fs.writeFileSync(outFile, pdfBytes)
255
247