diff --git a/.gitignore b/.gitignore index 38443a1..3795180 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ vite.config.d.ts # Rust target/ +# Agentic orchestrator worktrees +.worktrees/ + # Database files (SQLite, SpacetimeDB, etc.) *.db *.db-shm diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..1a7d0d7 --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,3 @@ +# Local SQLite database and runtime artifacts +.data/ +dist/ diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..18f62c6 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,24 @@ +{ + "name": "@void-nav/api", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "Backend API for the VOID::NAV docs site (Implementation Board persistence).", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "check": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.5", + "better-sqlite3": "^12.11.1", + "hono": "^4.12.25" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^25.9.3", + "tsx": "^4.22.4", + "typescript": "^5.8.3" + } +} diff --git a/apps/api/src/ai.ts b/apps/api/src/ai.ts new file mode 100644 index 0000000..63fdc59 --- /dev/null +++ b/apps/api/src/ai.ts @@ -0,0 +1,124 @@ +/** + * AI "beautify" helper. + * + * Turns a user's raw text into structured HTML documentation with embedded + * Mermaid diagram blocks. + * + * Uses the **same model pi uses**: GLM-5.2 via the z.ai coding endpoint + * (`https://api.z.ai/api/coding/paas/v4`), an OpenAI-compatible Chat + * Completions API, with a 1,000,000-token context window and 131,072 max + * output tokens. See `~/.pi/agent/models.json` (provider `zai-coding`) for the + * canonical configuration this mirrors. + * + * The API key is resolved the same way pi resolves it: from + * `~/.pi/agent/auth.json` under `zai.key`. It can be overridden with the + * `ZAI_API_KEY` env var (useful for CI / containers). Other aspects are + * overridable too: + * - ZAI_BASE_URL (defaults to the z.ai coding v4 endpoint) + * - ZAI_MODEL (defaults to `glm-5.2`) + * - ZAI_MAX_TOKENS (defaults to 131072, matching pi's maxTokens for glm-5.2) + * + * When no key is available, `beautify` throws `AiNotConfiguredError` so the + * route layer can return a clear 503 instead of a generic failure. + */ + +import { readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import path from 'node:path'; + +const DEFAULT_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'; +const DEFAULT_MODEL = 'glm-5.2'; +const DEFAULT_MAX_TOKENS = 131_072; + +/** GLM-5.2 supports a 1,000,000-token context window (pi: contextWindow: 1000000). */ +export const GLM_CONTEXT_WINDOW = 1_000_000; + +export class AiNotConfiguredError extends Error { + constructor() { + super( + 'AI beautify is not configured — add a key to ~/.pi/agent/auth.json (zai.key) or set ZAI_API_KEY', + ); + this.name = 'AiNotConfiguredError'; + } +} + +/** + * Resolve the z.ai API key the same way pi does: prefer the explicit env var, + * then fall back to `~/.pi/agent/auth.json` → `zai.key`. + */ +function resolveApiKey(): string | undefined { + if (process.env.ZAI_API_KEY) return process.env.ZAI_API_KEY; + try { + const authPath = path.join(homedir(), '.pi', 'agent', 'auth.json'); + const raw = readFileSync(authPath, 'utf8'); + const parsed = JSON.parse(raw) as { zai?: { key?: string } }; + return parsed.zai?.key; + } catch { + return undefined; + } +} + +export function isAiConfigured(): boolean { + return Boolean(resolveApiKey()); +} + +const SYSTEM_PROMPT = `You are a technical documentation writer for VOID::NAV, a single-player +narrative-driven space exploration RPG. Convert the user's rough notes into polished, +well-structured HTML documentation that fits a dark, monospace-accented sci-fi UI. + +Rules: +- Return a SINGLE HTML fragment (no , , , or