From df9d91f1acea7d8ed896ac9c92a52824cad5274b Mon Sep 17 00:00:00 2001 From: ewood Date: Wed, 11 Feb 2026 22:53:59 -0500 Subject: [PATCH] Add a game export endpoint --- src/ai/simulate/simulate-workflow.ts | 2 +- src/api/internal/handler.ts | 114 +++++++++++++++++++++++++++ src/api/internal/routes.ts | 5 +- 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/ai/simulate/simulate-workflow.ts b/src/ai/simulate/simulate-workflow.ts index 253ec87..58432ad 100644 --- a/src/ai/simulate/simulate-workflow.ts +++ b/src/ai/simulate/simulate-workflow.ts @@ -128,7 +128,7 @@ export interface SpecArtifacts { * Get cached spec processing artifacts from checkpoint. * Similar to getCachedDesignSpecification in design-workflow. */ -async function getCachedSpecArtifacts( +export async function getCachedSpecArtifacts( specKey: string, ): Promise { const saver = await getSaver(specKey, getConfig("simulation-graph-type")); diff --git a/src/api/internal/handler.ts b/src/api/internal/handler.ts index 42152e3..b2d6324 100644 --- a/src/api/internal/handler.ts +++ b/src/api/internal/handler.ts @@ -5,6 +5,8 @@ import { unlink } from 'fs/promises'; import { Pool } from 'pg'; import { cleanup } from '#chaincraft/ai/memory/checkpoint-memory.js'; import { getConfig } from '#chaincraft/config.js'; +import { getCachedDesign, getDesignByVersion } from '#chaincraft/ai/design/design-workflow.js'; +import { getCachedSpecArtifacts } from '#chaincraft/ai/simulate/simulate-workflow.js'; /** * Authenticate internal API requests using X-Internal-Token header @@ -250,3 +252,115 @@ export async function handleDbStats( await pool.end(); } } + +/** + * Game export endpoint - exports game design state and optionally artifacts + * GET /internal/game-export?gameId=xxx&version=N&artifacts=true + * + * Query parameters: + * - gameId (required): The conversation/game ID + * - version (optional): Specific version number to export. If not provided, exports latest version + * - artifacts (optional): If "true", includes spec processing artifacts (schema, transitions, instructions) + * + * Returns JSON with: + * - metadata: { gameId, version, timestamp, hasArtifacts } + * - design: { title, specification, specNarratives, pendingSpecChanges } + * - artifacts: { gameRules, stateSchema, stateTransitions, playerPhaseInstructions, transitionInstructions } (if artifacts=true and available) + */ +export async function handleGameExport( + request: FastifyRequest<{ Querystring: { gameId?: string; version?: string; artifacts?: string } }>, + reply: FastifyReply +) { + if (!authenticateInternal(request, reply)) return; + + const { gameId, version: versionParam, artifacts: artifactsParam } = request.query; + + if (!gameId) { + reply.code(400); + return { error: 'gameId query parameter required' }; + } + + const includeArtifacts = artifactsParam === 'true'; + const specificVersion = versionParam ? parseInt(versionParam, 10) : undefined; + + if (versionParam && isNaN(specificVersion!)) { + reply.code(400); + return { error: 'version must be a valid number' }; + } + + try { + console.log(`[internal/game-export] Exporting game ${gameId}`, { + version: specificVersion ?? 'latest', + includeArtifacts + }); + + // Get design state (specific version or latest) + const design = specificVersion !== undefined + ? await getDesignByVersion(gameId, specificVersion) + : await getCachedDesign(gameId); + + if (!design) { + reply.code(404); + return { + error: 'Game not found', + message: specificVersion !== undefined + ? `No design found for game ${gameId} version ${specificVersion}` + : `No design found for game ${gameId}` + }; + } + + // Extract version from the design + const exportVersion = design.specification?.version ?? 0; + + // Optionally get artifacts + let artifacts = null; + if (includeArtifacts) { + const specKey = `${gameId}-v${exportVersion}`; + artifacts = await getCachedSpecArtifacts(specKey); + + // If artifacts requested but not found, just don't include them (per user request) + if (!artifacts) { + console.log(`[internal/game-export] No artifacts found for ${specKey}`); + } + } + + // Build export response + const exportData = { + metadata: { + gameId, + version: exportVersion, + timestamp: new Date().toISOString(), + hasArtifacts: !!artifacts + }, + design: { + title: design.title, + specification: design.specification, + specNarratives: design.specNarratives, + pendingSpecChanges: design.pendingSpecChanges, + consolidationThreshold: design.consolidationThreshold, + consolidationCharLimit: design.consolidationCharLimit + }, + ...(artifacts && { + artifacts: { + gameRules: artifacts.gameRules, + stateSchema: artifacts.stateSchema, + stateTransitions: artifacts.stateTransitions, + playerPhaseInstructions: artifacts.playerPhaseInstructions, + transitionInstructions: artifacts.transitionInstructions, + specNarratives: artifacts.specNarratives + } + }) + }; + + console.log(`[internal/game-export] Successfully exported game ${gameId} version ${exportVersion}`); + return exportData; + + } catch (error) { + console.error('[internal/game-export] Error exporting game:', error); + reply.code(500); + return { + error: 'Export failed', + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} diff --git a/src/api/internal/routes.ts b/src/api/internal/routes.ts index 258a4a5..02773de 100644 --- a/src/api/internal/routes.ts +++ b/src/api/internal/routes.ts @@ -1,5 +1,5 @@ import { FastifyInstance } from 'fastify'; -import { handleCleanup, handleHeapSnapshot, handleMemoryStats, handleDbStats } from './handler.js'; +import { handleCleanup, handleHeapSnapshot, handleMemoryStats, handleDbStats, handleGameExport } from './handler.js'; export async function registerInternalRoutes(server: FastifyInstance) { // Cleanup endpoint - removes old checkpoints @@ -13,4 +13,7 @@ export async function registerInternalRoutes(server: FastifyInstance) { // Database stats endpoint - returns checkpoint storage statistics server.get('/db-stats', handleDbStats); + + // Game export endpoint - exports game design and artifacts for local import + server.get('/game-export', handleGameExport); }