diff --git a/.changeset/codebase-wiki-pack-description-length.md b/.changeset/codebase-wiki-pack-description-length.md new file mode 100644 index 00000000..abd5ec03 --- /dev/null +++ b/.changeset/codebase-wiki-pack-description-length.md @@ -0,0 +1,6 @@ +--- +"@inkeep/open-knowledge-server": patch +"@inkeep/open-knowledge": patch +--- + +Shorten the "Codebase wiki" starter-pack description to a single short clause ("Architecture, modules, and flows.") so its card matches the length of the other packs on the starter-pack picker instead of overflowing with a full paragraph. diff --git a/docs/next.config.ts b/docs/next.config.ts index 3a1c8850..c454fcab 100644 --- a/docs/next.config.ts +++ b/docs/next.config.ts @@ -13,21 +13,37 @@ const nextConfig: NextConfig = { // through the rewrites below. skipTrailingSlashRedirect: true, async rewrites() { - return [ - { - source: '/ingest/static/:path*', - destination: 'https://us-assets.i.posthog.com/static/:path*', - }, - { - source: '/ingest/array/:path*', - destination: 'https://us-assets.i.posthog.com/array/:path*', - }, - // Catch-all must come last — the static/array asset rules above must win. - { - source: '/ingest/:path*', - destination: 'https://us.i.posthog.com/:path*', - }, - ]; + return { + // Per-page raw Markdown for agents: `/docs/.md` (and `.mdx`) maps + // to the markdown route handler at `/llms.mdx/`. Must run in + // `beforeFiles` so it wins before the `/docs/[...slug]` page catch-all, + // which would otherwise match `…/overview.md` as a slug segment and 404. + beforeFiles: [ + { + source: '/docs/:path*.md', + destination: '/llms.mdx/:path*', + }, + { + source: '/docs/:path*.mdx', + destination: '/llms.mdx/:path*', + }, + ], + afterFiles: [ + { + source: '/ingest/static/:path*', + destination: 'https://us-assets.i.posthog.com/static/:path*', + }, + { + source: '/ingest/array/:path*', + destination: 'https://us-assets.i.posthog.com/array/:path*', + }, + // Catch-all must come last — the static/array asset rules above must win. + { + source: '/ingest/:path*', + destination: 'https://us.i.posthog.com/:path*', + }, + ], + }; }, // HSTS with `includeSubDomains; preload` (Vercel's injected default is // max-age only). Chrome blocks a download when ANY hop in its redirect diff --git a/docs/src/app/docs/[...slug]/page.tsx b/docs/src/app/docs/[...slug]/page.tsx index 7015c63a..01ef25f0 100644 --- a/docs/src/app/docs/[...slug]/page.tsx +++ b/docs/src/app/docs/[...slug]/page.tsx @@ -1,7 +1,8 @@ import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; -import { metaDescription, SITE_NAME, TWITTER_HANDLE } from '@/lib/site'; +import { PageMarkdownActions } from '@/components/page-markdown-actions'; +import { metaDescription, SITE_NAME, SITE_URL, TWITTER_HANDLE } from '@/lib/site'; import { source } from '@/lib/source'; import { getMDXComponents } from '@/mdx-components'; @@ -20,7 +21,14 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) { footer={hideFooter ? { enabled: false } : undefined} article={hideFooter ? { className: 'pb-12' } : undefined} > - {page.data.title} +
+ {page.data.title} + +
{page.data.description} diff --git a/docs/src/app/llms-full.txt/route.ts b/docs/src/app/llms-full.txt/route.ts index 5e7275ff..13e04892 100644 --- a/docs/src/app/llms-full.txt/route.ts +++ b/docs/src/app/llms-full.txt/route.ts @@ -1,20 +1,11 @@ +import { getLLMText } from '@/lib/get-llm-text'; import { source } from '@/lib/source'; export const revalidate = false; export async function GET() { const pages = source.getPages(); - - const scan = pages.map(async (page) => { - const processed = await page.data.getText('processed'); - - return `# ${page.data.title} (${page.url}) - -${page.data.description || ''} - -${processed}`; - }); - const scanned = await Promise.all(scan); + const scanned = await Promise.all(pages.map((page) => getLLMText(page))); return new Response(scanned.join('\n\n')); } diff --git a/docs/src/app/llms.mdx/[...slug]/route.test.ts b/docs/src/app/llms.mdx/[...slug]/route.test.ts new file mode 100644 index 00000000..f2c9c044 --- /dev/null +++ b/docs/src/app/llms.mdx/[...slug]/route.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, mock, test } from 'bun:test'; + +const overviewPage = { + url: '/docs/get-started/overview', + data: { + title: 'Overview', + description: 'What OpenKnowledge is.', + getText: async () => 'PROCESSED BODY', + }, +}; + +mock.module('@/lib/source', () => ({ + source: { + getPage: (slug: string[]) => + slug.join('/') === 'get-started/overview' ? overviewPage : undefined, + generateParams: () => [{ slug: ['get-started', 'overview'] }], + }, +})); + +mock.module('next/navigation', () => ({ + notFound: () => { + throw new Error('NEXT_HTTP_ERROR_FALLBACK;404'); + }, +})); + +const { GET, generateStaticParams } = await import('./route.ts'); + +function props(slug: string[]) { + return { params: Promise.resolve({ slug }) }; +} + +describe('GET /docs/.md (markdown route handler)', () => { + test('serves text/markdown with the page rendered by getLLMText', async () => { + const res = await GET(new Request('https://openknowledge.ai/docs/get-started/overview.md'), { + ...props(['get-started', 'overview']), + }); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8'); + + const body = await res.text(); + expect(body).toContain('# Overview (/docs/get-started/overview)'); + expect(body).toContain('PROCESSED BODY'); + }); + + test('calls notFound() for an unknown page', async () => { + await expect( + GET(new Request('https://openknowledge.ai/docs/nope.md'), { ...props(['nope']) }), + ).rejects.toThrow('404'); + }); + + test('generateStaticParams delegates to the loader and yields slug-shaped params', () => { + const params = generateStaticParams(); + expect(params.length).toBeGreaterThan(0); + for (const entry of params) { + expect(Array.isArray(entry.slug)).toBe(true); + } + }); +}); diff --git a/docs/src/app/llms.mdx/[...slug]/route.ts b/docs/src/app/llms.mdx/[...slug]/route.ts new file mode 100644 index 00000000..b12aa707 --- /dev/null +++ b/docs/src/app/llms.mdx/[...slug]/route.ts @@ -0,0 +1,23 @@ +import { notFound } from 'next/navigation'; +import { getLLMText } from '@/lib/get-llm-text'; +import { source } from '@/lib/source'; + +export const dynamic = 'force-static'; + +interface RouteProps { + params: Promise<{ slug: string[] }>; +} + +export async function GET(_request: Request, props: RouteProps) { + const { slug } = await props.params; + const page = source.getPage(slug); + if (!page) notFound(); + + return new Response(await getLLMText(page), { + headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, + }); +} + +export function generateStaticParams() { + return source.generateParams(); +} diff --git a/docs/src/app/llms.txt/route.ts b/docs/src/app/llms.txt/route.ts index 0102fcc2..a2e621b9 100644 --- a/docs/src/app/llms.txt/route.ts +++ b/docs/src/app/llms.txt/route.ts @@ -9,7 +9,7 @@ export async function GET() { [ '# OpenKnowledge', '## Docs', - ...pages.map((page) => `- [${page.data.title}](${SITE_URL}${page.url})`), + ...pages.map((page) => `- [${page.data.title}](${SITE_URL}${page.url}.md)`), ].join('\n\n'), ); } diff --git a/docs/src/components/page-markdown-actions.tsx b/docs/src/components/page-markdown-actions.tsx new file mode 100644 index 00000000..46d39f08 --- /dev/null +++ b/docs/src/components/page-markdown-actions.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { Check, ChevronDown, Copy, FileText } from 'lucide-react'; +import { useState } from 'react'; +import { ClaudeIcon } from '@/components/icons/claude'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { cn } from '@/lib/utils'; + +interface PageMarkdownActionsProps { + markdownPath: string; + markdownUrl: string; + className?: string; +} + +const menuContentClass = 'border-fd-border bg-fd-popover text-fd-popover-foreground'; +const menuItemClass = 'gap-2 focus:bg-fd-accent focus:text-fd-accent-foreground'; + +export function PageMarkdownActions({ + markdownPath, + markdownUrl, + className, +}: PageMarkdownActionsProps) { + const [copied, setCopied] = useState(false); + + const copyMarkdown = async () => { + try { + const res = await fetch(markdownPath); + if (!res.ok) return; + await navigator.clipboard.writeText(await res.text()); + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + } catch {} + }; + + const prompt = `Read ${markdownUrl} so I can ask questions about it.`; + const chatGptUrl = `https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`; + const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`; + + return ( +
+ + + + + + + + + + + + + + Open in ChatGPT + + + + + {/* Decorative — the "Open in Claude" label is the accessible name. */} + + + + +
+ ); +} diff --git a/docs/src/lib/get-llm-text.test.ts b/docs/src/lib/get-llm-text.test.ts new file mode 100644 index 00000000..91adc492 --- /dev/null +++ b/docs/src/lib/get-llm-text.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'bun:test'; +import { getLLMText } from './get-llm-text.ts'; + +type Page = Parameters[0]; + +function fakePage(data: Partial = {}, url = '/docs/get-started/overview'): Page { + return { + url, + data: { + title: 'Overview', + description: 'What OpenKnowledge is.', + getText: async (type: 'raw' | 'processed') => `BODY(${type})`, + ...data, + }, + } as unknown as Page; +} + +describe('getLLMText', () => { + test('renders title + URL header, description, then processed body', async () => { + const md = await getLLMText(fakePage()); + expect(md).toBe( + `# Overview (/docs/get-started/overview) + +What OpenKnowledge is. + +BODY(processed)`, + ); + }); + + test('requests the processed Markdown variant (snippets resolved), not raw', async () => { + let requested: string | undefined; + await getLLMText( + fakePage({ + getText: async (type) => { + requested = type; + return ''; + }, + }), + ); + expect(requested).toBe('processed'); + }); + + test('tolerates a missing description without emitting "undefined"', async () => { + const md = await getLLMText(fakePage({ description: undefined })); + expect(md).not.toContain('undefined'); + expect(md).toContain('# Overview (/docs/get-started/overview)'); + }); +}); diff --git a/docs/src/lib/get-llm-text.ts b/docs/src/lib/get-llm-text.ts new file mode 100644 index 00000000..8b96b731 --- /dev/null +++ b/docs/src/lib/get-llm-text.ts @@ -0,0 +1,12 @@ +import type { InferPageType } from 'fumadocs-core/source'; +import type { source } from '@/lib/source'; + +export async function getLLMText(page: InferPageType): Promise { + const processed = await page.data.getText('processed'); + + return `# ${page.data.title} (${page.url}) + +${page.data.description || ''} + +${processed}`; +} diff --git a/packages/server/src/seed/starter.ts b/packages/server/src/seed/starter.ts index f50b3ad5..ea92b4b6 100644 --- a/packages/server/src/seed/starter.ts +++ b/packages/server/src/seed/starter.ts @@ -1390,8 +1390,7 @@ export const STARTER_PACKS: Readonly> = { 'codebase-wiki': { id: 'codebase-wiki', name: 'Codebase wiki', - description: - 'An agent-authored, navigable wiki of your codebase — architecture, modules, flows, concepts, and guides, with diagrams, cross-links, and source references. Generated and refreshed by the `wiki` workflow; version-controlled and private by default.', + description: 'Architecture, modules, and flows.', defaultSubfolder: undefined, folders: CODEBASE_WIKI_FOLDERS, templates: CODEBASE_WIKI_TEMPLATES,