Repository navigation
fix(web): point agents at Cap Infra in llms files and serve docs as Markdown #4170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
58ef229
fix(web): one poll interval for Infra video status and transcripts
richiemcilroy dd9a2e8
fix(web): point agents at Cap Infra in the llms files
richiemcilroy 0b7e700
feat(web): serve docs pages as Markdown to agents
richiemcilroy 28edfea
docs(web): drop the webhook claim and the hls.js start level setup
richiemcilroy 1fe9aa1
fix(web): one embed snippet and Get Started as the Infra docs link
richiemcilroy c2e1a5f
fix(web): give the final Infra rate limits in the llms files
richiemcilroy ea58c75
refactor(web): serve docs Markdown outside /api and drop narration co…
richiemcilroy 3b4b848
style(web): format the createTranscript description
richiemcilroy 1f3e55e
fix(web): tell agents which Infra errors to wait out
richiemcilroy 9cbeedd
docs(web): poll several transcripts with one list call per status
richiemcilroy 095731d
docs(web): stop promising 0.08 second word timing steps
richiemcilroy 7e50b96
docs(web): list queued transcripts before processing ones when batch …
richiemcilroy 8506f85
test(web): count h4 headings as link targets in the API docs
richiemcilroy d97f9a4
fix(web): keep the agent prompt's rate limit sentence as #4160 writes it
richiemcilroy fdcf48b
docs(web): point agents at the hosted Cap Infra MCP server in the llm…
richiemcilroy 337c23f
revert(web): leave the Infra embed snippets as main has them
richiemcilroy 86f4b52
docs(web): say what the hosted player records
richiemcilroy b904539
Merge remote-tracking branch 'origin/main' into fix/infra-agent-disco…
richiemcilroy 4b98682
docs(web): mention Infra webhooks now that they have shipped
richiemcilroy b1ba110
Merge remote-tracking branch 'origin/main' into fix/infra-agent-disco…
richiemcilroy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import { NextRequest } from "next/server"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
|
|
||
| vi.mock("@cap/database", () => ({ | ||
| db: () => ({ | ||
| select: () => ({ from: () => ({ where: async () => [] }) }), | ||
| }), | ||
| })); | ||
| vi.mock("@cap/database/schema", () => ({ organizations: {} })); | ||
| vi.mock("@cap/env", () => ({ | ||
| buildEnv: { NEXT_PUBLIC_IS_CAP: "true" }, | ||
| serverEnv: () => ({ WEB_URL: "https://cap.so" }), | ||
| })); | ||
|
|
||
| import { GET } from "@/app/docs-markdown/[...slug]/route"; | ||
| import { CAP_AGENT_PROMPT } from "@/data/agent-prompt"; | ||
| import { DOCS_MARKDOWN_ROUTE, docsMarkdownSlug } from "@/lib/agent-markdown"; | ||
| import { docToMarkdown } from "@/lib/docs-markdown"; | ||
| import { getAllDocs, getDocBySlug } from "@/utils/docs"; | ||
| import { proxy } from "../../proxy"; | ||
|
|
||
| const getMarkdown = (slug: string, method = "GET") => | ||
| GET(new Request(`https://cap.so/docs/${slug}.md`, { method }), { | ||
| params: Promise.resolve({ slug: slug.split("/") }), | ||
| }); | ||
|
|
||
| const proseLines = (markdown: string) => { | ||
| const lines: string[] = []; | ||
| let fence: string | null = null; | ||
| for (const line of markdown.split("\n")) { | ||
| const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1]; | ||
| if (fence) { | ||
| if (marker && marker[0] === fence[0] && marker.length >= fence.length) | ||
| fence = null; | ||
| } else if (marker) fence = marker; | ||
| else lines.push(line); | ||
| } | ||
| return lines; | ||
| }; | ||
|
|
||
| describe("docsMarkdownSlug", () => { | ||
| it.each([ | ||
| ["/docs/api/rest-api.md", "text/html", "api/rest-api"], | ||
| ["/docs/api/rest-api.mdx", null, "api/rest-api"], | ||
| ["/docs/api/rest-api", "text/markdown", "api/rest-api"], | ||
| ["/docs/api/rest-api/", "text/markdown", "api/rest-api"], | ||
| ["/docs", "text/markdown", "introduction"], | ||
| ["/docs.md", null, "introduction"], | ||
| ["/docs/api/rest-api", "text/html,*/*;q=0.8", null], | ||
| ["/docs/api/rest-api", null, null], | ||
| ["/pricing.md", null, null], | ||
| ["/docsearch", "text/markdown", null], | ||
| ])("%s with Accept %s gives %s", (path, accept, slug) => { | ||
| expect(docsMarkdownSlug(path, accept)).toBe(slug); | ||
| }); | ||
| }); | ||
|
|
||
| describe("docToMarkdown", () => { | ||
| const docs = getAllDocs(); | ||
|
|
||
| it.each(docs.map((doc) => [doc.slug, doc] as const))( | ||
| "turns %s into plain Markdown", | ||
| (_slug, doc) => { | ||
| const markdown = docToMarkdown(doc, "https://cap.so"); | ||
| expect(markdown.startsWith(`# ${doc.metadata.title}\n`)).toBe(true); | ||
| expect(markdown).not.toMatch(/^---\n/); | ||
| for (const line of proseLines(markdown)) { | ||
| expect(line).not.toMatch(/^\s*<\/?[A-Z]/); | ||
| expect(line).not.toMatch(/\]\(\/[^/]/); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| it("keeps every code block exactly as written", () => { | ||
| const doc = getDocBySlug("api/get-started"); | ||
| if (!doc) throw new Error("missing get-started doc"); | ||
| const markdown = docToMarkdown(doc, "https://cap.so"); | ||
| const blocks = doc.content.match(/```[\s\S]*?\n```/g) ?? []; | ||
| expect(blocks.length).toBeGreaterThan(5); | ||
| for (const block of blocks) expect(markdown).toContain(block); | ||
| }); | ||
|
|
||
| it("shows the copyable prompt and warnings as text", () => { | ||
| const agents = getDocBySlug("agents"); | ||
| const selfHosting = getDocBySlug("self-hosting"); | ||
| if (!agents || !selfHosting) throw new Error("missing docs"); | ||
| expect(docToMarkdown(agents, "https://cap.so")).toContain(CAP_AGENT_PROMPT); | ||
| const hosting = docToMarkdown(selfHosting, "https://cap.so"); | ||
| expect(hosting).toContain( | ||
| "> **Security Notice for Production Deployments**", | ||
| ); | ||
| expect(hosting).toContain( | ||
| "[Watch the video](https://cap.so/embed/k310v2vf41d6ffp)", | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("docs Markdown route", () => { | ||
| it("serves a docs page as Markdown", async () => { | ||
| const response = await getMarkdown("api/rest-api"); | ||
| expect(response.status).toBe(200); | ||
| expect(response.headers.get("content-type")).toBe( | ||
| "text/markdown; charset=utf-8", | ||
| ); | ||
| expect(response.headers.get("vary")).toBe("Accept"); | ||
| const body = await response.text(); | ||
| expect(body.startsWith("# REST API\n")).toBe(true); | ||
| expect(body).toContain("every 3 seconds"); | ||
| expect(body).not.toContain("<html"); | ||
| }); | ||
|
|
||
| it("answers a missing page with a Markdown 404", async () => { | ||
| const response = await getMarkdown("api/nope"); | ||
| expect(response.status).toBe(404); | ||
| expect(response.headers.get("content-type")).toBe( | ||
| "text/markdown; charset=utf-8", | ||
| ); | ||
| expect(await response.text()).toContain("/docs/api/nope"); | ||
| }); | ||
|
|
||
| it("refuses a path outside the docs folder", async () => { | ||
| const response = await getMarkdown("../../package"); | ||
| expect(response.status).toBe(404); | ||
| }); | ||
| }); | ||
|
|
||
| describe("proxy routes docs Markdown requests", () => { | ||
| const send = (path: string, accept: string, method = "GET") => | ||
| proxy( | ||
| new NextRequest(`https://cap.so${path}`, { | ||
| method, | ||
| headers: { accept }, | ||
| }), | ||
| ); | ||
| const rewriteOf = (response: Response) => | ||
| response.headers.get("x-middleware-rewrite"); | ||
|
|
||
| it.each([ | ||
| ["/docs/api/get-started", "text/markdown"], | ||
| ["/docs/api/get-started.md", "text/html,*/*;q=0.8"], | ||
| ])("rewrites %s (Accept %s)", async (path, accept) => { | ||
| const response = await send(path, accept); | ||
| expect(rewriteOf(response)).toBe( | ||
| `https://cap.so${DOCS_MARKDOWN_ROUTE}/api/get-started`, | ||
| ); | ||
| }); | ||
|
|
||
| it("leaves the HTML page alone for a browser", async () => { | ||
| const response = await send( | ||
| "/docs/api/get-started", | ||
| "text/html,application/xhtml+xml,*/*;q=0.8", | ||
| ); | ||
| expect(rewriteOf(response)).toBeNull(); | ||
| expect(response.headers.get("x-middleware-next")).toBe("1"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { markdownHeaders, markdownNotFoundBody } from "@/lib/agent-markdown"; | ||
| import { docToMarkdown } from "@/lib/docs-markdown"; | ||
| import { getDocBySlug } from "@/utils/docs"; | ||
|
|
||
| export async function GET( | ||
| request: Request, | ||
| props: RouteContext<"/docs-markdown/[...slug]">, | ||
| ) { | ||
| const { slug } = await props.params; | ||
| const { origin } = new URL(request.url); | ||
| const doc = getDocBySlug(slug.join("/")); | ||
|
|
||
| if (!doc) { | ||
| return new Response( | ||
| markdownNotFoundBody(origin, `/docs/${slug.join("/")}`), | ||
| { status: 404, headers: markdownHeaders("private, no-store") }, | ||
| ); | ||
| } | ||
|
|
||
| return new Response(docToMarkdown(doc, origin), { | ||
| headers: markdownHeaders("public, max-age=300"), | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.