From 63a764fee2fe793123826c02c35e82ae21a15fd2 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Mon, 6 Jul 2026 03:40:53 +0200 Subject: [PATCH] feat(docs): interactive API reference on /docs/rest-api --- .../src/components/docs/endpoint-list.tsx | 152 ++++++++ .../src/components/docs/json-highlight.tsx | 95 +++++ .../src/docs/components/doc-article.tsx | 4 + packages/landing/src/docs/content/rest-api.ts | 97 +---- packages/landing/src/docs/types.ts | 47 ++- packages/landing/src/lib/api-endpoints.ts | 363 ++++++++++++++++++ 6 files changed, 669 insertions(+), 89 deletions(-) create mode 100644 packages/landing/src/components/docs/endpoint-list.tsx create mode 100644 packages/landing/src/components/docs/json-highlight.tsx create mode 100644 packages/landing/src/lib/api-endpoints.ts diff --git a/packages/landing/src/components/docs/endpoint-list.tsx b/packages/landing/src/components/docs/endpoint-list.tsx new file mode 100644 index 0000000..0860619 --- /dev/null +++ b/packages/landing/src/components/docs/endpoint-list.tsx @@ -0,0 +1,152 @@ +'use client'; + +// Interactive, grouped endpoint accordion for /docs/rest-api. Reproduces the +// approved prototype (_archive/proto/api-reference-page) against the landing's +// semantic tokens. Rows collapse via a CSS toggle so the contract stays in the +// prerendered HTML; each row opens independently. + +import * as React from 'react'; +import { ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { CodeBlock } from '@/docs/components/ui'; +import { JsonBlock } from './json-highlight'; +import type { Endpoint, EndpointArg, EndpointGroup, HttpMethod } from '@/docs/types'; + +const METHOD_CLASS: Record = { + GET: 'text-success bg-success/10 border-success/20', + POST: 'text-info bg-info/10 border-info/20', + PUT: 'text-warning bg-warning/10 border-warning/20', + PATCH: 'text-tag-schedules bg-tag-schedules/10 border-tag-schedules/20', + DELETE: 'text-destructive bg-destructive/10 border-destructive/20', +}; + +function SectionLabel({ children }: { children: React.ReactNode }): React.JSX.Element { + return ( +

+ {children} +

+ ); +} + +function ArgTable({ rows }: { rows: EndpointArg[] }): React.JSX.Element { + return ( + + + {rows.map((r) => ( + + + + + + ))} + +
+ {r.name} + + {r.type} + + {r.description} +
+ ); +} + +function EndpointRow({ endpoint }: { endpoint: Endpoint }): React.JSX.Element { + const [open, setOpen] = React.useState(false); + const bodyId = React.useId(); + const planned = endpoint.status === 'planned'; + return ( +
+ +
+

+ {endpoint.description} +

+ {planned && ( +

+ Draft contract from the north-star spec. Not callable yet. +

+ )} + {endpoint.params && ( + <> + Parameters + + + )} + Example + + Response 200 + + Fields + + Errors +

+ {endpoint.errors.map((e, i) => ( + + {i > 0 && · } + + {e.status} + {' '} + {e.meaning} + + ))} +

+
+
+ ); +} + +export function EndpointList({ groups }: { groups: EndpointGroup[] }): React.JSX.Element { + return ( +
+ {groups.map((g) => ( +
+

+ {g.group} +

+
+ {g.items.map((e) => ( + + ))} +
+
+ ))} +
+ ); +} diff --git a/packages/landing/src/components/docs/json-highlight.tsx b/packages/landing/src/components/docs/json-highlight.tsx new file mode 100644 index 0000000..e02012a --- /dev/null +++ b/packages/landing/src/components/docs/json-highlight.tsx @@ -0,0 +1,95 @@ +'use client'; + +// Structured, syntax-highlighted JSON for the endpoint response blocks. We avoid +// pulling shiki into this client island (async + heavy); a tiny tokenizer colors +// keys/strings/numbers/booleans/null via the landing's semantic tokens, so it +// works in both light and dark themes. Responses are normalized through +// JSON.parse/stringify (2-space) - nothing is hand-aligned. + +import * as React from 'react'; + +interface Piece { + text: string; + className?: string; +} + +// Alternation: quoted string (optionally a key when followed by a colon), +// literal keyword, or number. +const TOKEN = + /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g; + +function tokenize(json: string): Piece[] { + const pieces: Piece[] = []; + let last = 0; + TOKEN.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = TOKEN.exec(json)) !== null) { + if (m.index > last) pieces.push({ text: json.slice(last, m.index) }); + const [full, str, colon, keyword] = m; + if (str !== undefined) { + if (colon !== undefined) { + pieces.push({ text: str, className: 'text-info' }); + pieces.push({ text: colon }); + } else { + pieces.push({ text: str, className: 'text-success' }); + } + } else if (keyword !== undefined) { + pieces.push({ text: full, className: 'text-tag-schedules' }); + } else { + pieces.push({ text: full, className: 'text-warning' }); + } + last = m.index + full.length; + } + if (last < json.length) pieces.push({ text: json.slice(last) }); + return pieces; +} + +function normalize(code: string): { text: string; highlighted: boolean } { + try { + return { text: JSON.stringify(JSON.parse(code), null, 2), highlighted: true }; + } catch { + return { text: code, highlighted: false }; + } +} + +export function JsonBlock({ code }: { code: string }): React.JSX.Element { + const [copied, setCopied] = React.useState(false); + const { text, highlighted } = normalize(code); + const copy = async (): Promise => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + return ( +
+
+ + {highlighted ? 'json' : 'text'} + + +
+
+        
+          {highlighted
+            ? tokenize(text).map((p, i) =>
+                p.className ? (
+                  
+                    {p.text}
+                  
+                ) : (
+                  {p.text}
+                )
+              )
+            : text}
+        
+      
+
+ ); +} diff --git a/packages/landing/src/docs/components/doc-article.tsx b/packages/landing/src/docs/components/doc-article.tsx index 9cb6356..6f8466d 100644 --- a/packages/landing/src/docs/components/doc-article.tsx +++ b/packages/landing/src/docs/components/doc-article.tsx @@ -2,6 +2,7 @@ import { Info, TriangleAlert, CircleCheck } from 'lucide-react'; import { Alert, CodeBlock, Md, Tabs, TabsContent, TabsList, TabsTrigger } from './ui'; +import { EndpointList } from '@/components/docs/endpoint-list'; import type { CalloutVariant, DocBlock, DocPage } from '../types'; const calloutMeta: Record< @@ -77,6 +78,9 @@ function Block({ block }: { block: DocBlock }): React.JSX.Element { ); } + case 'endpoints': + return ; + case 'steps': return (
    diff --git a/packages/landing/src/docs/content/rest-api.ts b/packages/landing/src/docs/content/rest-api.ts index a96b708..58ea96d 100644 --- a/packages/landing/src/docs/content/rest-api.ts +++ b/packages/landing/src/docs/content/rest-api.ts @@ -1,8 +1,10 @@ import type { DocPage } from '../types'; import { MCP_AUTH_ORIGIN, REST_RATE_LIMIT_PER_MIN, REST_VAULTS_ENDPOINT } from '@/lib/mcp-docs'; +import { API_ENDPOINTS } from '@/lib/api-endpoints'; -// REST API reference - one read-only endpoint that lists the vaults a token can -// see. Facts (base URL, endpoint, rate limit) come from lib/mcp-docs.ts. +// REST API reference. One live endpoint (GET /v1/vaults) plus the north-star +// draft contracts, rendered as an interactive list from lib/api-endpoints.ts. +// Facts (base URL, endpoint, rate limit) come from lib/mcp-docs.ts. export const restApiDoc: DocPage = { slug: 'rest-api', title: 'REST API', @@ -51,97 +53,16 @@ export const restApiDoc: DocPage = { ], }, { - id: 'example', - title: 'Example', - blocks: [ - { - type: 'code', - language: 'bash', - code: `curl -s \\ - -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ - ${REST_VAULTS_ENDPOINT}`, - }, - { - type: 'code', - language: 'json', - code: `{ - "vaults": [ - { - "name": "default", - "files": 412, - "folders": 37, - "updated": "2026-07-06T07:31:02+00:00", - "empty": false - }, - { - "name": "work", - "files": 128, - "folders": 12, - "updated": "2026-07-05T21:47:55+00:00", - "empty": false - } - ] -}`, - }, - ], - }, - { - id: 'response-schema', - title: 'Response schema', + id: 'endpoints', + title: 'Endpoints', blocks: [ { type: 'p', - md: [ - '| Field | Type | Meaning |', - '| --- | --- | --- |', - '| `vaults` | array | Vaults visible to the presented token, one object per vault. |', - '| `vaults[].name` | string | Vault slug, 1-64 chars of `a-z 0-9 _ -`. |', - '| `vaults[].files` | integer | Number of notes currently in the vault. |', - '| `vaults[].folders` | integer | Number of folders in the vault. |', - '| `vaults[].updated` | string (ISO 8601) or null | Last write to the vault; `null` when the vault has no content yet. |', - '| `vaults[].empty` | boolean | True when the vault has no notes. |', - ].join('\n'), - }, - { - type: 'code', - language: 'json', - code: `{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "required": ["vaults"], - "properties": { - "vaults": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "files", "folders", "updated", "empty"], - "properties": { - "name": { "type": "string", "pattern": "^[a-z0-9_-]{1,64}$" }, - "files": { "type": "integer", "minimum": 0 }, - "folders": { "type": "integer", "minimum": 0 }, - "updated": { "type": ["string", "null"], "format": "date-time" }, - "empty": { "type": "boolean" } - } - } - } - } -}`, + md: 'Every endpoint, grouped by resource. Click a row to expand its contract - parameters, a curl example, the 200 response, response fields, and error codes. `Live` endpoints are callable today; `Planned` rows are draft contracts from the north-star spec and are not callable yet.', }, - ], - }, - { - id: 'errors', - title: 'Errors', - blocks: [ { - type: 'p', - md: [ - '| Status | Meaning | What to do |', - '| --- | --- | --- |', - '| `401` | Missing, expired, or invalid token. | Re-authenticate and retry with a fresh token. |', - '| `429` | Rate limit exceeded. | Back off and retry after the window resets. |', - '| `503` | Auth service temporarily unavailable (never means a bad token). | Retry with backoff. |', - ].join('\n'), + type: 'endpoints', + groups: API_ENDPOINTS, }, ], }, diff --git a/packages/landing/src/docs/types.ts b/packages/landing/src/docs/types.ts index 1f02aa2..ff8fef6 100644 --- a/packages/landing/src/docs/types.ts +++ b/packages/landing/src/docs/types.ts @@ -29,13 +29,58 @@ export interface ClientTab { md: string; } +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + +/** Live = callable today; planned = draft contract from the north-star spec. */ +export type EndpointStatus = 'live' | 'planned'; + +export interface EndpointArg { + /** Parameter or response field name, e.g. `vault` or `vaults[].name`. */ + name: string; + /** Type + location, e.g. "string, body", "integer, query", "array". */ + type: string; + description: string; +} + +export interface EndpointError { + /** HTTP status code, e.g. "401". */ + status: string; + meaning: string; +} + +export interface Endpoint { + method: HttpMethod; + path: string; + /** One-line summary shown on the collapsed row. */ + summary: string; + status: EndpointStatus; + /** Rollout wave for planned endpoints, e.g. 2 or 3. */ + wave?: number; + description: string; + params?: EndpointArg[]; + /** curl example. */ + curl: string; + /** 200 response body. */ + response: string; + /** Response field reference. */ + fields: EndpointArg[]; + errors: EndpointError[]; +} + +export interface EndpointGroup { + /** Group heading, e.g. "Vaults", "Notes", "Search & export". */ + group: string; + items: Endpoint[]; +} + export type DocBlock = | { type: 'p'; md: string } | { type: 'code'; code: string; language?: string; caption?: string } | { type: 'tabs'; tabs: CodeTab[] } | { type: 'clienttabs'; tabs: ClientTab[] } | { type: 'callout'; variant: CalloutVariant; title?: string; md: string } - | { type: 'steps'; steps: Step[] }; + | { type: 'steps'; steps: Step[] } + | { type: 'endpoints'; groups: EndpointGroup[] }; export interface DocSection { /** Anchor id - also the table-of-contents target. */ diff --git a/packages/landing/src/lib/api-endpoints.ts b/packages/landing/src/lib/api-endpoints.ts new file mode 100644 index 0000000..3f2fa9b --- /dev/null +++ b/packages/landing/src/lib/api-endpoints.ts @@ -0,0 +1,363 @@ +// Endpoint reference for the interactive list on /docs/rest-api. Transcribed +// from the approved prototype at +// _archive/proto/api-reference-page/index.html - one live endpoint plus the +// north-star draft contracts. The base URL comes from lib/mcp-docs.ts. + +import { REST_API_BASE_URL } from './mcp-docs'; +import type { EndpointArg, EndpointGroup } from '@/docs/types'; + +// Shared note shape, reused by the list-notes and read-note responses. +const NOTE_FIELDS: EndpointArg[] = [ + { + name: 'path', + type: 'string', + description: 'POSIX .md path inside the vault, no leading slash.', + }, + { name: 'title', type: 'string', description: 'First heading or filename.' }, + { name: 'tags', type: 'string[]', description: 'Frontmatter tags.' }, + { name: 'sizeBytes', type: 'integer', description: 'Note size.' }, + { name: 'updated', type: 'string | null', description: 'ISO 8601, last write.' }, +]; + +export const API_ENDPOINTS: EndpointGroup[] = [ + { + group: 'Vaults', + items: [ + { + method: 'GET', + path: '/v1/vaults', + status: 'live', + summary: 'List the vaults your token can see', + description: + 'Returns every vault visible to the presented token. Visibility is decided by the token (its vaults claim), never by request parameters.', + curl: `curl -s \\ + -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + ${REST_API_BASE_URL}/v1/vaults`, + response: `{ + "vaults": [ + { "name": "default", "files": 412, "folders": 37, + "updated": "2026-07-06T07:31:02+00:00", "empty": false } + ] +}`, + fields: [ + { name: 'vaults', type: 'array', description: 'One object per visible vault.' }, + { + name: 'vaults[].name', + type: 'string', + description: 'Vault slug, 1-64 chars of a-z 0-9 _ -.', + }, + { name: 'vaults[].files', type: 'integer', description: 'Number of notes in the vault.' }, + { name: 'vaults[].folders', type: 'integer', description: 'Number of folders.' }, + { + name: 'vaults[].updated', + type: 'string | null', + description: 'ISO 8601 last write; null when empty.', + }, + { + name: 'vaults[].empty', + type: 'boolean', + description: 'True when the vault has no notes.', + }, + ], + errors: [ + { status: '401', meaning: 'missing or invalid token' }, + { status: '429', meaning: 'rate limit exceeded' }, + { status: '503', meaning: 'auth service unavailable, retry' }, + ], + }, + { + method: 'POST', + path: '/v1/vaults', + status: 'planned', + wave: 2, + summary: 'Create a vault', + description: + 'Creates a named vault. Idempotent: an existing name returns the vault instead of failing. Subject to the plan vault cap.', + params: [ + { + name: 'name', + type: 'string, body', + description: 'Vault slug, 1-64 chars of a-z 0-9 _ -. Required.', + }, + ], + curl: `curl -s -X POST \\ + -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + -H "Content-Type: application/json" \\ + -d '{"name":"work"}' \\ + ${REST_API_BASE_URL}/v1/vaults`, + response: `{ + "vault": "work", + "info": { "name": "work", "files": 0, "folders": 0, + "updated": null, "empty": true } +}`, + fields: [ + { name: 'vault', type: 'string', description: 'The created (or existing) slug.' }, + { name: 'info', type: 'object', description: 'Same shape as a vaults[] entry.' }, + ], + errors: [ + { status: '400', meaning: 'invalid name' }, + { status: '401', meaning: 'missing or invalid token' }, + { status: '409', meaning: 'plan vault cap reached' }, + ], + }, + { + method: 'GET', + path: '/v1/vaults/{vault}', + status: 'planned', + wave: 2, + summary: 'Vault stats', + description: 'Stats for one vault: counts, size and last activity.', + params: [ + { + name: 'vault', + type: 'string, path', + description: 'Vault slug. Must be granted by the token.', + }, + ], + curl: `curl -s -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + ${REST_API_BASE_URL}/v1/vaults/default`, + response: `{ "name": "default", "files": 412, "folders": 37, + "sizeBytes": 8388608, "updated": "2026-07-06T07:31:02+00:00", "empty": false }`, + fields: [ + { name: 'sizeBytes', type: 'integer', description: 'Total vault size.' }, + { name: '...', type: '', description: 'Other fields as in vaults[].' }, + ], + errors: [ + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted to this token' }, + { status: '404', meaning: 'no such vault' }, + ], + }, + ], + }, + { + group: 'Notes', + items: [ + { + method: 'GET', + path: '/v1/vaults/{vault}/notes', + status: 'planned', + wave: 2, + summary: 'List notes', + description: 'Paginated listing of notes, optionally scoped to a folder.', + params: [ + { name: 'vault', type: 'string, path', description: 'Vault slug.' }, + { name: 'folder', type: 'string, query', description: 'Folder prefix filter. Optional.' }, + { name: 'limit', type: 'integer, query', description: '1-500, default 200.' }, + { name: 'cursor', type: 'string, query', description: 'Opaque pagination cursor.' }, + ], + curl: `curl -s -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + "${REST_API_BASE_URL}/v1/vaults/default/notes?folder=work&limit=50"`, + response: `{ + "notes": [ + { "path": "work/plan.md", "title": "Plan", "tags": ["work"], + "excerpt": "Q3 focus is...", "sizeBytes": 2048, + "updated": "2026-07-05T21:47:55+00:00" } + ], + "nextCursor": null +}`, + fields: [ + { name: 'notes', type: 'array', description: 'One object per note.' }, + ...NOTE_FIELDS.map((f) => ({ ...f, name: `notes[].${f.name}` })), + { + name: 'nextCursor', + type: 'string | null', + description: 'Pass back as cursor for the next page.', + }, + ], + errors: [ + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + { status: '404', meaning: 'no such vault' }, + ], + }, + { + method: 'GET', + path: '/v1/vaults/{vault}/notes/{path}', + status: 'planned', + wave: 2, + summary: 'Read a note', + description: 'Full note by path: frontmatter, markdown body and metadata.', + params: [ + { name: 'vault', type: 'string, path', description: 'Vault slug.' }, + { name: 'path', type: 'string, path', description: 'POSIX .md path, URL-encoded.' }, + ], + curl: `curl -s -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + ${REST_API_BASE_URL}/v1/vaults/default/notes/work%2Fplan.md`, + response: `{ + "path": "work/plan.md", "title": "Plan", + "frontmatter": { "tags": ["work"] }, + "body": "# Plan\\n\\nQ3 focus is...", + "tags": ["work"], "sizeBytes": 2048, + "updated": "2026-07-05T21:47:55+00:00" +}`, + fields: [ + ...NOTE_FIELDS, + { name: 'frontmatter', type: 'object', description: 'Parsed YAML frontmatter.' }, + { name: 'body', type: 'string', description: 'Markdown body.' }, + ], + errors: [ + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + { status: '404', meaning: 'no such note' }, + ], + }, + { + method: 'PUT', + path: '/v1/vaults/{vault}/notes/{path}', + status: 'planned', + wave: 3, + summary: 'Write a note (create or replace)', + description: + 'Idempotent full write, mirroring the memory__write MCP tool. Wave 3: ships once edge rate limits and plan quotas are enforced.', + params: [ + { name: 'vault', type: 'string, path', description: 'Vault slug.' }, + { name: 'path', type: 'string, path', description: 'Target .md path.' }, + { name: 'body', type: 'string, body', description: 'Markdown body. Required.' }, + { name: 'frontmatter', type: 'object, body', description: 'Optional frontmatter.' }, + ], + curl: `curl -s -X PUT \\ + -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + -H "Content-Type: application/json" \\ + -d '{"body":"# Plan\\n..."}' \\ + ${REST_API_BASE_URL}/v1/vaults/default/notes/work%2Fplan.md`, + response: `{ "path": "work/plan.md", "updated": "2026-07-06T08:00:00+00:00" }`, + fields: [ + { name: 'path', type: 'string', description: 'Written path.' }, + { name: 'updated', type: 'string', description: 'ISO 8601 commit time.' }, + ], + errors: [ + { status: '400', meaning: 'invalid path or body' }, + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + { status: '429', meaning: 'rate limit exceeded' }, + ], + }, + { + method: 'PATCH', + path: '/v1/vaults/{vault}/notes/{path}', + status: 'planned', + wave: 3, + summary: 'Edit a note', + description: + 'Partial update, mirroring memory__edit: replace, append, or targeted string replacement.', + params: [ + { + name: 'mode', + type: 'string, body', + description: 'replace | append | str_replace. Default replace.', + }, + { name: 'body', type: 'string, body', description: 'New or appended content.' }, + { name: 'old_str / new_str', type: 'string, body', description: 'For str_replace mode.' }, + ], + curl: `curl -s -X PATCH \\ + -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + -H "Content-Type: application/json" \\ + -d '{"mode":"append","body":"\\n- new item"}' \\ + ${REST_API_BASE_URL}/v1/vaults/default/notes/work%2Fplan.md`, + response: `{ "path": "work/plan.md", "updated": "2026-07-06T08:01:00+00:00" }`, + fields: [ + { name: 'path', type: 'string', description: 'Edited path.' }, + { name: 'updated', type: 'string', description: 'ISO 8601 commit time.' }, + ], + errors: [ + { status: '400', meaning: 'bad mode or no match for old_str' }, + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + { status: '404', meaning: 'no such note' }, + ], + }, + { + method: 'DELETE', + path: '/v1/vaults/{vault}/notes/{path}', + status: 'planned', + wave: 3, + summary: 'Delete a note (recoverable)', + description: + 'Soft delete, mirroring memory__delete: the note is tombstoned in git history, not destroyed.', + params: [ + { name: 'vault', type: 'string, path', description: 'Vault slug.' }, + { name: 'path', type: 'string, path', description: 'Note path.' }, + ], + curl: `curl -s -X DELETE \\ + -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + ${REST_API_BASE_URL}/v1/vaults/default/notes/work%2Fold.md`, + response: `{ "path": "work/old.md", "deleted": true }`, + fields: [{ name: 'deleted', type: 'boolean', description: 'Always true on success.' }], + errors: [ + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + { status: '404', meaning: 'no such note' }, + ], + }, + ], + }, + { + group: 'Search & export', + items: [ + { + method: 'GET', + path: '/v1/vaults/{vault}/search', + status: 'planned', + wave: 2, + summary: 'Search notes', + description: + 'Ranked lexical search over the vault (git-native, literal keyword matching). Returns paths and snippets, never full bodies.', + params: [ + { name: 'q', type: 'string, query', description: 'Search query. Required.' }, + { name: 'folder', type: 'string, query', description: 'Scope to a folder. Optional.' }, + { name: 'limit', type: 'integer, query', description: '1-50, default 20.' }, + { name: 'cursor', type: 'string, query', description: 'Pagination cursor.' }, + ], + curl: `curl -s -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + "${REST_API_BASE_URL}/v1/vaults/default/search?q=roadmap"`, + response: `{ + "results": [ + { "path": "work/plan.md", "title": "Plan", + "snippet": "...the Q3 roadmap is...", "score": 0.92, + "updated": "2026-07-05T21:47:55+00:00" } + ], + "nextCursor": null +}`, + fields: [ + { name: 'results', type: 'array', description: 'Ranked matches.' }, + { name: 'results[].snippet', type: 'string', description: 'Match context.' }, + { name: 'results[].score', type: 'number', description: 'Relevance, 0-1.' }, + { name: 'nextCursor', type: 'string | null', description: 'Next page cursor.' }, + ], + errors: [ + { status: '400', meaning: 'missing q' }, + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + ], + }, + { + method: 'GET', + path: '/v1/vaults/{vault}/export', + status: 'planned', + wave: 2, + summary: 'Export the vault as a git bundle', + description: + 'Streams a cloneable git bundle of the whole vault: full history, plain markdown, yours. Content-Type application/x-git-bundle.', + params: [{ name: 'vault', type: 'string, path', description: 'Vault slug.' }], + curl: `curl -s -H "Authorization: Bearer $AGENTAGE_TOKEN" \\ + -o memory.bundle \\ + ${REST_API_BASE_URL}/v1/vaults/default/export +git clone memory.bundle my-memory`, + response: `(binary git bundle stream)`, + fields: [ + { + name: '-', + type: 'application/x-git-bundle', + description: 'Attachment; clone it with plain git.', + }, + ], + errors: [ + { status: '401', meaning: 'missing or invalid token' }, + { status: '403', meaning: 'vault not granted' }, + { status: '404', meaning: 'no such vault' }, + ], + }, + ], + }, +];