@open-mercato/ai-assistant 0.4.9-develop-8c36c096d5 → 0.4.9-develop-be6e1cf49c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +78 -37
  2. package/dist/modules/ai_assistant/api/chat/route.js +104 -13
  3. package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
  4. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +97 -0
  5. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
  6. package/dist/modules/ai_assistant/lib/codemode-tools.js +610 -0
  7. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +7 -0
  8. package/dist/modules/ai_assistant/lib/http-server.js +65 -7
  9. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  10. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +16 -1
  11. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  12. package/dist/modules/ai_assistant/lib/mcp-server.js +17 -0
  13. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  14. package/dist/modules/ai_assistant/lib/opencode-handlers.js +32 -0
  15. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +2 -2
  16. package/dist/modules/ai_assistant/lib/sandbox.js +124 -0
  17. package/dist/modules/ai_assistant/lib/sandbox.js.map +7 -0
  18. package/dist/modules/ai_assistant/lib/session-memory.js +103 -0
  19. package/dist/modules/ai_assistant/lib/session-memory.js.map +7 -0
  20. package/dist/modules/ai_assistant/lib/tool-loader.js +4 -114
  21. package/dist/modules/ai_assistant/lib/tool-loader.js.map +3 -3
  22. package/dist/modules/ai_assistant/lib/truncate.js +26 -0
  23. package/dist/modules/ai_assistant/lib/truncate.js.map +7 -0
  24. package/jest.config.cjs +23 -0
  25. package/package.json +6 -5
  26. package/src/modules/ai_assistant/api/chat/route.ts +110 -14
  27. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +129 -0
  28. package/src/modules/ai_assistant/lib/__tests__/sandbox.test.ts +642 -0
  29. package/src/modules/ai_assistant/lib/__tests__/session-memory.test.ts +82 -0
  30. package/src/modules/ai_assistant/lib/__tests__/truncate.test.ts +76 -0
  31. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +143 -0
  32. package/src/modules/ai_assistant/lib/codemode-tools.ts +864 -0
  33. package/src/modules/ai_assistant/lib/http-server.ts +86 -9
  34. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +19 -0
  35. package/src/modules/ai_assistant/lib/mcp-server.ts +21 -0
  36. package/src/modules/ai_assistant/lib/opencode-handlers.ts +40 -0
  37. package/src/modules/ai_assistant/lib/sandbox.ts +192 -0
  38. package/src/modules/ai_assistant/lib/session-memory.ts +174 -0
  39. package/src/modules/ai_assistant/lib/tool-loader.ts +11 -145
  40. package/src/modules/ai_assistant/lib/truncate.ts +45 -0
  41. package/src/modules/ai_assistant/lib/types.ts +2 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/mcp-server.ts"],
4
- "sourcesContent": ["import { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, hasRequiredFeatures } from './auth'\nimport type { McpServerOptions, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\n/**\n * Create and configure an MCP server instance.\n */\nexport async function createMcpServer(options: McpServerOptions): Promise<Server> {\n const { config, container, context, apiKeySecret } = options\n\n let tenantId: string | null = null\n let organizationId: string | null = null\n let userId: string | null = null\n let userFeatures: string[] = []\n let isSuperAdmin = false\n\n // API key authentication takes precedence\n if (apiKeySecret) {\n const authResult = await authenticateMcpRequest(apiKeySecret, container)\n if (!authResult.success) {\n throw new Error(`API key authentication failed: ${authResult.error}`)\n }\n tenantId = authResult.tenantId\n organizationId = authResult.organizationId\n userId = authResult.userId\n userFeatures = authResult.features\n isSuperAdmin = authResult.isSuperAdmin\n console.error(`[MCP Server] Authenticated via API key: ${authResult.keyName}`)\n } else if (context) {\n // Manual context provided\n tenantId = context.tenantId\n organizationId = context.organizationId\n userId = context.userId\n\n if (userId) {\n try {\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n }>\n }\n const acl = await rbacService.loadAcl(userId, {\n tenantId,\n organizationId,\n })\n userFeatures = acl.features\n isSuperAdmin = acl.isSuperAdmin\n } catch (error) {\n console.error('[MCP Server] Failed to load user ACL:', error)\n }\n } else {\n // No user specified - grant superadmin access for development/testing\n isSuperAdmin = true\n console.error('[MCP Server] No user specified, running with superadmin access')\n }\n } else {\n // No context and no API key - superadmin for dev/testing\n isSuperAdmin = true\n console.error('[MCP Server] No auth context, running with superadmin access')\n }\n\n const toolContext: McpToolContext = {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures,\n isSuperAdmin,\n apiKeySecret,\n }\n\n const server = new Server(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n // List tools handler\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on user permissions\n const rbacService = container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, userFeatures, isSuperAdmin, rbacService)\n )\n\n if (config.debug) {\n console.error(\n `[MCP Server] Listing ${accessibleTools.length}/${tools.length} tools (filtered by ACL)`\n )\n }\n\n return {\n tools: accessibleTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n // Cast to any for Zod v4 compatibility with zod-to-json-schema\n inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,\n })),\n }\n })\n\n // Call tool handler\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n\n if (config.debug) {\n console.error(`[MCP Server] Calling tool: ${name}`, JSON.stringify(args))\n }\n\n const result = await executeTool(name, args ?? {}, toolContext)\n\n if (!result.success) {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n })\n\n return server\n}\n\n/**\n * Run MCP server with stdio transport.\n * This keeps the process running until terminated.\n *\n * Supports two authentication modes:\n * 1. API key: Provide `apiKeySecret` option\n * 2. Manual context: Provide `context` with tenant/org/user\n */\nexport async function runMcpServer(options: McpServerOptions): Promise<void> {\n // Load tools from all modules before starting\n await loadAllModuleTools()\n\n // Index tools and API endpoints for hybrid search discovery (if search service available)\n try {\n const searchService = options.container.resolve('searchService') as SearchService\n\n // Index MCP tools\n await indexToolsForSearch(searchService)\n\n // Index API endpoints for api_discover\n const { indexApiEndpoints } = await import('./api-endpoint-index')\n const endpointCount = await indexApiEndpoints(searchService)\n if (endpointCount > 0) {\n console.error(`[MCP Server] Indexed ${endpointCount} API endpoints for hybrid search`)\n }\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n console.error('[MCP Server] Search indexing skipped (search service not available)')\n }\n\n const server = await createMcpServer(options)\n const transport = new StdioServerTransport()\n\n const toolCount = getToolRegistry().listToolNames().length\n\n console.error(`[MCP Server] Starting ${options.config.name} v${options.config.version}`)\n\n if (options.apiKeySecret) {\n console.error(`[MCP Server] Authentication: API key`)\n } else if (options.context) {\n console.error(`[MCP Server] Tenant: ${options.context.tenantId ?? '(none)'}`)\n console.error(`[MCP Server] Organization: ${options.context.organizationId ?? '(none)'}`)\n console.error(`[MCP Server] User: ${options.context.userId ?? '(superadmin)'}`)\n } else {\n console.error(`[MCP Server] Authentication: none (superadmin mode)`)\n }\n\n console.error(`[MCP Server] Tools registered: ${toolCount}`)\n\n await server.connect(transport)\n\n console.error('[MCP Server] Connected and ready for requests')\n\n // Handle shutdown gracefully\n const shutdown = async () => {\n console.error('[MCP Server] Shutting down...')\n await server.close()\n process.exit(0)\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB,2BAA2B;AACxD,SAAS,wBAAwB,2BAA2B;AAQ5D,eAAsB,gBAAgB,SAA4C;AAChF,QAAM,EAAE,QAAQ,WAAW,SAAS,aAAa,IAAI;AAErD,MAAI,WAA0B;AAC9B,MAAI,iBAAgC;AACpC,MAAI,SAAwB;AAC5B,MAAI,eAAyB,CAAC;AAC9B,MAAI,eAAe;AAGnB,MAAI,cAAc;AAChB,UAAM,aAAa,MAAM,uBAAuB,cAAc,SAAS;AACvE,QAAI,CAAC,WAAW,SAAS;AACvB,YAAM,IAAI,MAAM,kCAAkC,WAAW,KAAK,EAAE;AAAA,IACtE;AACA,eAAW,WAAW;AACtB,qBAAiB,WAAW;AAC5B,aAAS,WAAW;AACpB,mBAAe,WAAW;AAC1B,mBAAe,WAAW;AAC1B,YAAQ,MAAM,2CAA2C,WAAW,OAAO,EAAE;AAAA,EAC/E,WAAW,SAAS;AAElB,eAAW,QAAQ;AACnB,qBAAiB,QAAQ;AACzB,aAAS,QAAQ;AAEjB,QAAI,QAAQ;AACV,UAAI;AACF,cAAM,cAAc,UAAU,QAAQ,aAAa;AASnD,cAAM,MAAM,MAAM,YAAY,QAAQ,QAAQ;AAAA,UAC5C;AAAA,UACA;AAAA,QACF,CAAC;AACD,uBAAe,IAAI;AACnB,uBAAe,IAAI;AAAA,MACrB,SAAS,OAAO;AACd,gBAAQ,MAAM,yCAAyC,KAAK;AAAA,MAC9D;AAAA,IACF,OAAO;AAEL,qBAAe;AACf,cAAQ,MAAM,gEAAgE;AAAA,IAChF;AAAA,EACF,OAAO;AAEL,mBAAe;AACf,YAAQ,MAAM,8DAA8D;AAAA,EAC9E;AAEA,QAAM,cAA8B;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,SAAS,IAAI;AAAA,IACjB,EAAE,MAAM,OAAO,MAAM,SAAS,OAAO,QAAQ;AAAA,IAC7C,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,EAChC;AAGA,SAAO,kBAAkB,wBAAwB,YAAY;AAC3D,UAAM,WAAW,gBAAgB;AACjC,UAAM,QAAQ,MAAM,KAAK,SAAS,SAAS,EAAE,OAAO,CAAC;AAGrD,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,kBAAkB,MAAM;AAAA,MAAO,CAAC,SACpC,oBAAoB,KAAK,kBAAkB,cAAc,cAAc,WAAW;AAAA,IACpF;AAEA,QAAI,OAAO,OAAO;AAChB,cAAQ;AAAA,QACN,wBAAwB,gBAAgB,MAAM,IAAI,MAAM,MAAM;AAAA,MAChE;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO,gBAAgB,IAAI,CAAC,UAAU;AAAA,QACpC,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA;AAAA,QAElB,aAAa,gBAAgB,KAAK,WAAkB;AAAA,MACtD,EAAE;AAAA,IACJ;AAAA,EACF,CAAC;AAGD,SAAO,kBAAkB,uBAAuB,OAAO,YAAY;AACjE,UAAM,EAAE,MAAM,WAAW,KAAK,IAAI,QAAQ;AAE1C,QAAI,OAAO,OAAO;AAChB,cAAQ,MAAM,8BAA8B,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,SAAS,MAAM,YAAY,MAAM,QAAQ,CAAC,GAAG,WAAW;AAE9D,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU,EAAE,OAAO,OAAO,OAAO,MAAM,OAAO,UAAU,CAAC;AAAA,UACtE;AAAA,QACF;AAAA,QACA,SAAS;AAAA,MACX;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM,KAAK,UAAU,OAAO,QAAQ,MAAM,CAAC;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAUA,eAAsB,aAAa,SAA0C;AAE3E,QAAM,mBAAmB;AAGzB,MAAI;AACF,UAAM,gBAAgB,QAAQ,UAAU,QAAQ,eAAe;AAG/D,UAAM,oBAAoB,aAAa;AAGvC,UAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,sBAAsB;AACjE,UAAM,gBAAgB,MAAM,kBAAkB,aAAa;AAC3D,QAAI,gBAAgB,GAAG;AACrB,cAAQ,MAAM,wBAAwB,aAAa,kCAAkC;AAAA,IACvF;AAAA,EACF,SAAS,OAAO;AAEd,YAAQ,MAAM,qEAAqE;AAAA,EACrF;AAEA,QAAM,SAAS,MAAM,gBAAgB,OAAO;AAC5C,QAAM,YAAY,IAAI,qBAAqB;AAE3C,QAAM,YAAY,gBAAgB,EAAE,cAAc,EAAE;AAEpD,UAAQ,MAAM,yBAAyB,QAAQ,OAAO,IAAI,KAAK,QAAQ,OAAO,OAAO,EAAE;AAEvF,MAAI,QAAQ,cAAc;AACxB,YAAQ,MAAM,sCAAsC;AAAA,EACtD,WAAW,QAAQ,SAAS;AAC1B,YAAQ,MAAM,wBAAwB,QAAQ,QAAQ,YAAY,QAAQ,EAAE;AAC5E,YAAQ,MAAM,8BAA8B,QAAQ,QAAQ,kBAAkB,QAAQ,EAAE;AACxF,YAAQ,MAAM,sBAAsB,QAAQ,QAAQ,UAAU,cAAc,EAAE;AAAA,EAChF,OAAO;AACL,YAAQ,MAAM,qDAAqD;AAAA,EACrE;AAEA,UAAQ,MAAM,kCAAkC,SAAS,EAAE;AAE3D,QAAM,OAAO,QAAQ,SAAS;AAE9B,UAAQ,MAAM,+CAA+C;AAG7D,QAAM,WAAW,YAAY;AAC3B,YAAQ,MAAM,+BAA+B;AAC7C,UAAM,OAAO,MAAM;AACnB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,GAAG,UAAU,QAAQ;AAC7B,UAAQ,GAAG,WAAW,QAAQ;AAChC;",
4
+ "sourcesContent": ["import { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, hasRequiredFeatures } from './auth'\nimport type { McpServerOptions, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\n/**\n * Create and configure an MCP server instance.\n */\nexport async function createMcpServer(options: McpServerOptions): Promise<Server> {\n const { config, container, context, apiKeySecret } = options\n\n let tenantId: string | null = null\n let organizationId: string | null = null\n let userId: string | null = null\n let userFeatures: string[] = []\n let isSuperAdmin = false\n\n // API key authentication takes precedence\n if (apiKeySecret) {\n const authResult = await authenticateMcpRequest(apiKeySecret, container)\n if (!authResult.success) {\n throw new Error(`API key authentication failed: ${authResult.error}`)\n }\n tenantId = authResult.tenantId\n organizationId = authResult.organizationId\n userId = authResult.userId\n userFeatures = authResult.features\n isSuperAdmin = authResult.isSuperAdmin\n console.error(`[MCP Server] Authenticated via API key: ${authResult.keyName}`)\n } else if (context) {\n // Manual context provided\n tenantId = context.tenantId\n organizationId = context.organizationId\n userId = context.userId\n\n if (userId) {\n try {\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n }>\n }\n const acl = await rbacService.loadAcl(userId, {\n tenantId,\n organizationId,\n })\n userFeatures = acl.features\n isSuperAdmin = acl.isSuperAdmin\n } catch (error) {\n console.error('[MCP Server] Failed to load user ACL:', error)\n }\n } else {\n // No user specified - grant superadmin access for development/testing\n isSuperAdmin = true\n console.error('[MCP Server] No user specified, running with superadmin access')\n }\n } else {\n // No context and no API key - superadmin for dev/testing\n isSuperAdmin = true\n console.error('[MCP Server] No auth context, running with superadmin access')\n }\n\n const toolContext: McpToolContext = {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures,\n isSuperAdmin,\n apiKeySecret,\n }\n\n const server = new Server(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n // List tools handler\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on user permissions\n const rbacService = container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, userFeatures, isSuperAdmin, rbacService)\n )\n\n if (config.debug) {\n console.error(\n `[MCP Server] Listing ${accessibleTools.length}/${tools.length} tools (filtered by ACL)`\n )\n }\n\n return {\n tools: accessibleTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n // Cast to any for Zod v4 compatibility with zod-to-json-schema\n inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,\n })),\n }\n })\n\n // Call tool handler\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n\n if (config.debug) {\n console.error(`[MCP Server] Calling tool: ${name}`, JSON.stringify(args))\n }\n\n const result = await executeTool(name, args ?? {}, toolContext)\n\n if (!result.success) {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n })\n\n return server\n}\n\n/**\n * Run MCP server with stdio transport.\n * This keeps the process running until terminated.\n *\n * Supports two authentication modes:\n * 1. API key: Provide `apiKeySecret` option\n * 2. Manual context: Provide `context` with tenant/org/user\n */\nexport async function runMcpServer(options: McpServerOptions): Promise<void> {\n // Generate entity graph for Code Mode search tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n console.error(`[MCP Server] Entity graph: ${graph.nodes.length} entities`)\n } catch (error) {\n console.error('[MCP Server] Entity graph skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache raw OpenAPI spec for Code Mode search tool\n try {\n const { getRawOpenApiSpec } = await import('./api-endpoint-index')\n await getRawOpenApiSpec()\n console.error('[MCP Server] Raw OpenAPI spec cached for Code Mode')\n } catch (error) {\n console.error('[MCP Server] Raw OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Load tools from all modules before starting\n await loadAllModuleTools()\n\n // Index tools and API endpoints for hybrid search discovery (if search service available)\n try {\n const searchService = options.container.resolve('searchService') as SearchService\n\n // Index MCP tools\n await indexToolsForSearch(searchService)\n\n // Index API endpoints for api_discover\n const { indexApiEndpoints } = await import('./api-endpoint-index')\n const endpointCount = await indexApiEndpoints(searchService)\n if (endpointCount > 0) {\n console.error(`[MCP Server] Indexed ${endpointCount} API endpoints for hybrid search`)\n }\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n console.error('[MCP Server] Search indexing skipped (search service not available)')\n }\n\n const server = await createMcpServer(options)\n const transport = new StdioServerTransport()\n\n const toolCount = getToolRegistry().listToolNames().length\n\n console.error(`[MCP Server] Starting ${options.config.name} v${options.config.version}`)\n\n if (options.apiKeySecret) {\n console.error(`[MCP Server] Authentication: API key`)\n } else if (options.context) {\n console.error(`[MCP Server] Tenant: ${options.context.tenantId ?? '(none)'}`)\n console.error(`[MCP Server] Organization: ${options.context.organizationId ?? '(none)'}`)\n console.error(`[MCP Server] User: ${options.context.userId ?? '(superadmin)'}`)\n } else {\n console.error(`[MCP Server] Authentication: none (superadmin mode)`)\n }\n\n console.error(`[MCP Server] Tools registered: ${toolCount}`)\n\n await server.connect(transport)\n\n console.error('[MCP Server] Connected and ready for requests')\n\n // Handle shutdown gracefully\n const shutdown = async () => {\n console.error('[MCP Server] Shutting down...')\n await server.close()\n process.exit(0)\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -95,6 +95,13 @@ async function handleOpenCodeMessageStreaming(request, onEvent) {
95
95
  const client = getClient();
96
96
  const { message, sessionId, model } = request;
97
97
  const startTime = Date.now();
98
+ const usageStats = {
99
+ toolCalls: 0,
100
+ toolNames: [],
101
+ totalInputTokens: 0,
102
+ totalOutputTokens: 0,
103
+ messageCount: 0
104
+ };
98
105
  if (!message) {
99
106
  await onEvent({ type: "error", error: "Message is required" });
100
107
  return;
@@ -151,6 +158,8 @@ async function handleOpenCodeMessageStreaming(request, onEvent) {
151
158
  lastActivityTime = Date.now();
152
159
  } else if (status.status === "idle") {
153
160
  resolved = true;
161
+ const durationMs = Date.now() - startTime;
162
+ console.error(`[AI Usage] Session complete (heartbeat): sessionId=${targetSessionId.slice(0, 16)}... duration=${durationMs}ms tokens={in:${usageStats.totalInputTokens},out:${usageStats.totalOutputTokens}} toolCalls=${usageStats.toolCalls} tools=[${usageStats.toolNames.join(",")}] messages=${usageStats.messageCount}`);
154
163
  try {
155
164
  await onEvent({ type: "done", sessionId: targetSessionId });
156
165
  } catch (err) {
@@ -234,6 +243,8 @@ async function handleOpenCodeMessageStreaming(request, onEvent) {
234
243
  lastActivityTime = Date.now();
235
244
  } else {
236
245
  resolved = true;
246
+ const durationMs = Date.now() - startTime;
247
+ console.error(`[AI Usage] Session complete: sessionId=${targetSessionId.slice(0, 16)}... duration=${durationMs}ms tokens={in:${usageStats.totalInputTokens},out:${usageStats.totalOutputTokens}} toolCalls=${usageStats.toolCalls} tools=[${usageStats.toolNames.join(",")}] messages=${usageStats.messageCount}`);
237
248
  await onEvent({ type: "done", sessionId: targetSessionId });
238
249
  cleanup();
239
250
  clearTimeout(timeout);
@@ -280,6 +291,12 @@ async function handleOpenCodeMessageStreaming(request, onEvent) {
280
291
  provider: info.providerID,
281
292
  tokens: info.tokens
282
293
  };
294
+ usageStats.messageCount++;
295
+ if (info.tokens) {
296
+ usageStats.totalInputTokens += info.tokens.input || 0;
297
+ usageStats.totalOutputTokens += info.tokens.output || 0;
298
+ console.error(`[AI Usage] Tokens (message ${usageStats.messageCount}): input=${info.tokens.input} output=${info.tokens.output} model=${info.modelID || "unknown"}`);
299
+ }
283
300
  await onEvent({
284
301
  type: "debug",
285
302
  partType: "message-completed",
@@ -298,8 +315,15 @@ async function handleOpenCodeMessageStreaming(request, onEvent) {
298
315
  await onEvent({ type: "text", content: delta });
299
316
  }
300
317
  break;
318
+ case "thinking":
319
+ console.error(`[OpenCode SSE] Thinking block received (${(delta || part.text || "").length} chars)`);
320
+ await onEvent({ type: "debug", partType: "thinking", data: { text: delta || part.text } });
321
+ break;
301
322
  case "tool_use":
302
323
  if (part.name) {
324
+ usageStats.toolCalls++;
325
+ usageStats.toolNames.push(part.name);
326
+ console.error(`[AI Usage] Tool call #${usageStats.toolCalls}: ${part.name}`);
303
327
  await onEvent({
304
328
  type: "tool-call",
305
329
  id: part.id,
@@ -322,6 +346,14 @@ async function handleOpenCodeMessageStreaming(request, onEvent) {
322
346
  }
323
347
  break;
324
348
  }
349
+ case "message.part.delta": {
350
+ const part = properties.part;
351
+ const delta = properties.delta;
352
+ if (delta && part?.type !== "thinking") {
353
+ await onEvent({ type: "text", content: delta });
354
+ }
355
+ break;
356
+ }
325
357
  }
326
358
  } catch (err) {
327
359
  console.error("[OpenCode SSE] Error processing event:", err);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/opencode-handlers.ts"],
4
- "sourcesContent": ["/**\n * OpenCode API Route Handlers\n *\n * These handlers can be used by Next.js API routes to interact with OpenCode.\n */\n\nimport {\n createOpenCodeClient,\n type OpenCodeClient,\n type OpenCodeQuestion,\n} from './opencode-client'\n\nlet clientInstance: OpenCodeClient | null = null\n\nfunction getClient(): OpenCodeClient {\n if (!clientInstance) {\n clientInstance = createOpenCodeClient()\n }\n return clientInstance\n}\n\nexport type OpenCodeTestRequest = {\n message: string\n sessionId?: string\n model?: {\n providerID: string\n modelID: string\n }\n}\n\nexport type OpenCodeTestResponse = {\n sessionId: string\n result: unknown\n}\n\nexport type OpenCodeHealthResponse = {\n status: 'ok' | 'error'\n opencode?: {\n healthy: boolean\n version: string\n }\n mcp?: Record<string, { status: string; error?: string }>\n search?: {\n available: boolean\n driver: string | null // 'meilisearch' or null\n url: string | null // Meilisearch URL\n }\n url: string\n mcpUrl: string\n message?: string\n}\n\n/**\n * Handle POST request to send a message to OpenCode.\n */\nexport async function handleOpenCodeMessage(\n request: OpenCodeTestRequest\n): Promise<OpenCodeTestResponse> {\n const client = getClient()\n\n const { message, sessionId, model } = request\n\n if (!message) {\n throw new Error('Message is required')\n }\n\n // Create or get session\n let session\n if (sessionId) {\n session = await client.getSession(sessionId)\n } else {\n session = await client.createSession()\n }\n\n // Send message\n const result = await client.sendMessage(session.id, message, { model })\n\n return {\n sessionId: session.id,\n result,\n }\n}\n\n/**\n * Handle GET request to check OpenCode health.\n */\nexport async function handleOpenCodeHealth(): Promise<OpenCodeHealthResponse> {\n const client = getClient()\n const url = process.env.OPENCODE_URL ?? 'http://localhost:4096'\n // MCP_URL is the full URL (e.g., https://mcp.example.com), fallback to localhost with port\n const mcpUrl = process.env.MCP_URL ?? `http://localhost:${process.env.MCP_DEV_PORT ?? '3001'}`\n const meilisearchHost = process.env.MEILISEARCH_HOST ?? null\n\n // Check search service availability\n let searchStatus: { available: boolean; driver: string | null; url: string | null } = {\n available: false,\n driver: null,\n url: meilisearchHost,\n }\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const searchService = container.resolve<{\n isStrategyAvailable: (strategy: string) => boolean\n }>('searchService')\n const available = searchService.isStrategyAvailable('fulltext')\n searchStatus = {\n available,\n driver: available ? 'meilisearch' : null,\n url: meilisearchHost,\n }\n } catch {\n // Search service not available\n }\n\n try {\n const [health, mcp] = await Promise.all([client.health(), client.mcpStatus()])\n\n return {\n status: 'ok',\n opencode: health,\n mcp,\n search: searchStatus,\n url,\n mcpUrl,\n }\n } catch (error) {\n return {\n status: 'error',\n search: searchStatus,\n message: error instanceof Error ? error.message : 'OpenCode not reachable',\n url,\n mcpUrl,\n }\n }\n}\n\n/**\n * Extract text content from OpenCode message response.\n */\nexport function extractTextFromResponse(result: unknown): string | null {\n if (!result || typeof result !== 'object') return null\n\n const message = result as { parts?: Array<{ type: string; text?: string }> }\n if (!message.parts) return null\n\n const textParts = message.parts.filter((p) => p.type === 'text' && p.text)\n return textParts.map((p) => p.text).join('\\n') || null\n}\n\n/**\n * Response part from OpenCode - can be text, tool-call, tool-result, etc.\n */\nexport interface OpenCodeResponsePart {\n id: string\n type: string\n text?: string\n // Tool call fields (OpenCode uses 'tool_use' type)\n name?: string\n input?: unknown\n // Tool result fields (OpenCode uses 'tool_result' type)\n tool_use_id?: string\n content?: unknown\n // Step fields (step-start, step-finish)\n sessionID?: string\n messageID?: string\n reason?: string\n cost?: number\n tokens?: {\n input: number\n output: number\n reasoning?: number\n cache?: { read: number; write: number }\n }\n // Generic catch-all\n [key: string]: unknown\n}\n\n/**\n * Metadata about the OpenCode response.\n */\nexport interface OpenCodeResponseMetadata {\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n timing?: { created: number; completed?: number }\n}\n\n/**\n * Extract all parts from OpenCode response for verbose debugging.\n */\nexport function extractAllPartsFromResponse(result: unknown): OpenCodeResponsePart[] {\n if (!result || typeof result !== 'object') return []\n\n const message = result as { parts?: OpenCodeResponsePart[] }\n return message.parts || []\n}\n\n/**\n * Extract metadata (model, tokens, timing) from OpenCode response.\n */\nexport function extractMetadataFromResponse(result: unknown): OpenCodeResponseMetadata | null {\n if (!result || typeof result !== 'object') return null\n\n const message = result as {\n info?: {\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n time?: { created: number; completed?: number }\n }\n }\n\n if (!message.info) return null\n\n return {\n modelID: message.info.modelID,\n providerID: message.info.providerID,\n tokens: message.info.tokens,\n timing: message.info.time,\n }\n}\n\n/**\n * Event types emitted during streaming message handling.\n */\nexport type OpenCodeStreamEvent =\n | { type: 'thinking' }\n | { type: 'text'; content: string }\n | { type: 'tool-call'; id: string; toolName: string; args: unknown }\n | { type: 'tool-result'; id: string; result: unknown }\n | { type: 'question'; question: OpenCodeQuestion }\n | { type: 'metadata'; model?: string; provider?: string; tokens?: { input: number; output: number }; durationMs?: number }\n | { type: 'debug'; partType: string; data: unknown }\n | { type: 'done'; sessionId: string }\n | { type: 'error'; error: string }\n\n/**\n * Handle OpenCode message with real-time SSE streaming.\n * Uses OpenCode's /event SSE endpoint for live updates.\n *\n * OpenCode does agentic loops - it may generate multiple assistant messages\n * with tool calls in between. We complete only when the session becomes \"idle\"\n * after being \"busy\", indicating the full agentic loop is done.\n */\nexport async function handleOpenCodeMessageStreaming(\n request: OpenCodeTestRequest,\n onEvent: (event: OpenCodeStreamEvent) => Promise<void>\n): Promise<void> {\n const client = getClient()\n const { message, sessionId, model } = request\n const startTime = Date.now()\n\n if (!message) {\n await onEvent({ type: 'error', error: 'Message is required' })\n return\n }\n\n try {\n // Create or get session\n let session\n if (sessionId) {\n session = await client.getSession(sessionId)\n } else {\n session = await client.createSession()\n }\n\n const targetSessionId = session.id\n let unsubscribe: (() => void) | null = null\n let emittedThinking = false\n let wasBusy = false // Track if session was ever busy\n let resolved = false // Track if we've already completed\n let lastActivityTime = Date.now() // Track last event for heartbeat\n let heartbeatInterval: NodeJS.Timeout | null = null\n let lastMetadata: {\n model?: string\n provider?: string\n tokens?: { input: number; output: number }\n } | null = null\n\n // Helper to clean up resources\n const cleanup = () => {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval)\n heartbeatInterval = null\n }\n }\n\n // Set up SSE subscription for real-time events\n const eventPromise = new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n cleanup()\n unsubscribe?.()\n reject(new Error('OpenCode request timed out'))\n }, 300000) // 5 minute timeout for complex agentic tasks\n\n // Heartbeat: Check every second for completion conditions\n heartbeatInterval = setInterval(async () => {\n if (resolved) return\n\n const idleTime = Date.now() - lastActivityTime\n\n // If no activity for 5 seconds and we were busy, check session status\n if (idleTime >= 5000 && wasBusy && !resolved) {\n try {\n // Check actual session status before completing\n const status = await client.getSessionStatus(targetSessionId)\n\n if (status.status === 'busy') {\n // Session is still busy - wait\n return\n }\n\n if (status.status === 'waiting' && status.questionId) {\n // Session is waiting for a question answer\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.id === status.questionId)\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now() // Reset timer after emitting question\n return\n }\n }\n\n // Check for any pending questions for this session\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now() // Reset timer after emitting question\n } else if (status.status === 'idle') {\n // Session is explicitly idle and no questions - complete\n resolved = true\n try {\n await onEvent({ type: 'done', sessionId: targetSessionId })\n } catch (err) {\n console.error('[OpenCode SSE] Heartbeat: Failed to emit done event:', err)\n }\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n // Status is 'unknown' or something else - wait for SSE events\n } catch (err) {\n console.error('[OpenCode SSE] Heartbeat error:', err)\n }\n }\n }, 1000)\n\n unsubscribe = client.subscribeToEvents(\n async (sseEvent) => {\n try {\n const { type, properties } = sseEvent\n\n // Update activity timestamp for heartbeat\n lastActivityTime = Date.now()\n\n // Filter events for our session\n const eventSessionId =\n (properties.sessionID as string) ||\n (properties.info as { sessionID?: string })?.sessionID ||\n (properties.part as { sessionID?: string })?.sessionID ||\n (properties.question as { sessionID?: string })?.sessionID ||\n (properties.session as { id?: string })?.id ||\n (properties.status as { sessionID?: string })?.sessionID\n\n if (eventSessionId && eventSessionId !== targetSessionId) {\n return // Ignore events from other sessions\n }\n\n switch (type) {\n case 'question.asked': {\n // OpenCode is asking a question - use the data directly from the SSE event\n await onEvent({ type: 'debug', partType: 'question-asked', data: properties })\n\n // The question data is in properties.question (from SSE event)\n // This is more reliable than fetching from API which may return incomplete data\n const questionFromEvent = properties.question as OpenCodeQuestion | undefined\n\n if (questionFromEvent && questionFromEvent.sessionID === targetSessionId) {\n await onEvent({ type: 'question', question: questionFromEvent })\n } else {\n // Fallback to fetching from API if event doesn't have full question\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n }\n }\n break\n }\n\n case 'session.status': {\n const status = properties.status as { type: string; questionId?: string }\n\n if (status?.type === 'busy') {\n wasBusy = true\n if (!emittedThinking) {\n emittedThinking = true\n await onEvent({ type: 'thinking' })\n }\n } else if (status?.type === 'waiting' && !resolved) {\n // Session is waiting for user to answer a question\n const questions = await client.getPendingQuestions()\n const sessionQuestion = status.questionId\n ? questions.find((q) => q.id === status.questionId)\n : questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now()\n }\n } else if (status?.type === 'idle' && wasBusy && !resolved) {\n // Session went from busy to idle - check if there are pending questions\n const endTime = Date.now()\n\n // Emit final metadata if we have it\n if (lastMetadata) {\n await onEvent({\n type: 'metadata',\n model: lastMetadata.model,\n provider: lastMetadata.provider,\n tokens: lastMetadata.tokens,\n durationMs: endTime - startTime,\n })\n }\n\n // Check for pending questions before declaring done\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n // Question found - emit it but keep stream open for answer\n await onEvent({ type: 'question', question: sessionQuestion })\n // Reset activity time so heartbeat doesn't close prematurely\n lastActivityTime = Date.now()\n // Don't set resolved - let heartbeat handle completion after user answers\n } else {\n // No questions found - but give OpenCode a moment to register one\n // (race condition prevention)\n setTimeout(async () => {\n try {\n if (resolved) {\n return\n }\n\n // Check one more time for questions\n const finalQuestions = await client.getPendingQuestions()\n const finalQuestion = finalQuestions.find((q) => q.sessionID === targetSessionId)\n\n if (finalQuestion) {\n await onEvent({ type: 'question', question: finalQuestion })\n lastActivityTime = Date.now()\n } else {\n // Truly idle - complete the stream\n resolved = true\n await onEvent({ type: 'done', sessionId: targetSessionId })\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n } catch (err) {\n console.error('[OpenCode SSE] Error in timeout callback:', err)\n // Still try to complete even if there was an error\n if (!resolved) {\n resolved = true\n try {\n await onEvent({ type: 'done', sessionId: targetSessionId })\n } catch (e2) {\n console.error('[OpenCode SSE] Failed to emit done event:', e2)\n }\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n }\n }, 2000)\n }\n }\n break\n }\n\n case 'message.updated': {\n const info = properties.info as {\n id: string\n role: string\n time?: { completed?: number }\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n error?: { name: string; message?: string }\n }\n\n if (info.role === 'assistant') {\n // Check for error\n if (info.error) {\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n await onEvent({\n type: 'error',\n error: `${info.error.name}: ${info.error.message || 'Unknown error'}`,\n })\n resolve()\n return\n }\n\n // Track metadata from completed messages\n if (info.time?.completed) {\n lastMetadata = {\n model: info.modelID,\n provider: info.providerID,\n tokens: info.tokens,\n }\n // Emit intermediate metadata for visibility\n await onEvent({\n type: 'debug',\n partType: 'message-completed',\n data: { messageId: info.id, tokens: info.tokens },\n })\n // Note: Completion is now handled by heartbeat interval\n }\n }\n break\n }\n\n case 'message.part.updated': {\n const part = properties.part as {\n type: string\n text?: string\n name?: string\n input?: unknown\n tool_use_id?: string\n content?: unknown\n id: string\n }\n const delta = properties.delta as string | undefined\n\n switch (part.type) {\n case 'text':\n // Use delta for streaming text if available\n if (delta) {\n await onEvent({ type: 'text', content: delta })\n }\n break\n case 'tool_use':\n if (part.name) {\n await onEvent({\n type: 'tool-call',\n id: part.id,\n toolName: part.name,\n args: part.input,\n })\n }\n break\n case 'tool_result':\n await onEvent({\n type: 'tool-result',\n id: part.tool_use_id || part.id,\n result: part.content,\n })\n break\n case 'step-start':\n case 'step-finish':\n await onEvent({ type: 'debug', partType: part.type, data: part })\n break\n }\n break\n }\n }\n } catch (err) {\n console.error('[OpenCode SSE] Error processing event:', err)\n }\n },\n (error) => {\n clearTimeout(timeout)\n reject(error)\n }\n )\n })\n\n // Send message (don't await - let SSE handle the response via events)\n // We only catch errors here - successful completion is signaled via SSE session.status: idle\n client.sendMessage(session.id, message, { model }).catch((err) => {\n // Log send errors - SSE should also receive an error event\n console.error('[OpenCode] Send error (SSE should handle):', err)\n })\n\n // Wait for SSE to indicate completion (session.status: idle or error)\n await eventPromise\n } catch (error) {\n await onEvent({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n }\n}\n\n/**\n * Answer a pending question and continue processing.\n * Uses polling to check for completion/next question.\n */\nexport async function handleOpenCodeAnswer(\n questionId: string,\n answer: number,\n sessionId: string,\n onEvent: (event: OpenCodeStreamEvent) => Promise<void>\n): Promise<void> {\n const client = getClient()\n\n try {\n // Answer the question\n await client.answerQuestion(questionId, answer)\n await onEvent({ type: 'thinking' })\n\n // Poll for completion using session status (max 20 seconds for same-question wait, 60 seconds total)\n const maxAttempts = 30\n const pollInterval = 2000\n let sameQuestionWaitCount = 0\n const maxSameQuestionWait = 5 // Give up after 10 seconds of waiting on same question\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n await new Promise((resolve) => setTimeout(resolve, pollInterval))\n\n // Check session status - most reliable way to know if processing is done\n const status = await client.getSessionStatus(sessionId)\n\n if (status.status === 'idle' || status.status === 'unknown') {\n // Session is idle or unknown - processing complete\n await onEvent({ type: 'done', sessionId })\n return\n }\n\n if (status.status === 'waiting' && status.questionId && status.questionId !== questionId) {\n // A new question appeared - fetch and emit it\n const allQuestions = await client.getPendingQuestions()\n const newQuestion = allQuestions.find((q) => q.id === status.questionId)\n if (newQuestion) {\n await onEvent({ type: 'question', question: newQuestion })\n return\n }\n }\n\n // If waiting on the same question we answered, track how long\n if (status.status === 'waiting' && status.questionId === questionId) {\n sameQuestionWaitCount++\n if (sameQuestionWaitCount >= maxSameQuestionWait) {\n // OpenCode didn't properly clear the question - assume answered and complete\n await onEvent({ type: 'done', sessionId })\n return\n }\n } else {\n // Reset counter if status changed\n sameQuestionWaitCount = 0\n }\n\n // Session is busy - keep polling\n }\n\n // Timeout - assume complete\n await onEvent({ type: 'done', sessionId })\n } catch (error) {\n console.error('[OpenCode Answer] Error:', error)\n await onEvent({\n type: 'error',\n error: error instanceof Error ? error.message : 'Failed to answer question',\n })\n }\n}\n\n/**\n * Get pending questions for a session.\n */\nexport async function getPendingQuestions(): Promise<OpenCodeQuestion[]> {\n const client = getClient()\n return client.getPendingQuestions()\n}\n\n// Re-export the question type\nexport type { OpenCodeQuestion }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * OpenCode API Route Handlers\n *\n * These handlers can be used by Next.js API routes to interact with OpenCode.\n */\n\nimport {\n createOpenCodeClient,\n type OpenCodeClient,\n type OpenCodeQuestion,\n} from './opencode-client'\n\nlet clientInstance: OpenCodeClient | null = null\n\nfunction getClient(): OpenCodeClient {\n if (!clientInstance) {\n clientInstance = createOpenCodeClient()\n }\n return clientInstance\n}\n\nexport type OpenCodeTestRequest = {\n message: string\n sessionId?: string\n model?: {\n providerID: string\n modelID: string\n }\n}\n\nexport type OpenCodeTestResponse = {\n sessionId: string\n result: unknown\n}\n\nexport type OpenCodeHealthResponse = {\n status: 'ok' | 'error'\n opencode?: {\n healthy: boolean\n version: string\n }\n mcp?: Record<string, { status: string; error?: string }>\n search?: {\n available: boolean\n driver: string | null // 'meilisearch' or null\n url: string | null // Meilisearch URL\n }\n url: string\n mcpUrl: string\n message?: string\n}\n\n/**\n * Handle POST request to send a message to OpenCode.\n */\nexport async function handleOpenCodeMessage(\n request: OpenCodeTestRequest\n): Promise<OpenCodeTestResponse> {\n const client = getClient()\n\n const { message, sessionId, model } = request\n\n if (!message) {\n throw new Error('Message is required')\n }\n\n // Create or get session\n let session\n if (sessionId) {\n session = await client.getSession(sessionId)\n } else {\n session = await client.createSession()\n }\n\n // Send message\n const result = await client.sendMessage(session.id, message, { model })\n\n return {\n sessionId: session.id,\n result,\n }\n}\n\n/**\n * Handle GET request to check OpenCode health.\n */\nexport async function handleOpenCodeHealth(): Promise<OpenCodeHealthResponse> {\n const client = getClient()\n const url = process.env.OPENCODE_URL ?? 'http://localhost:4096'\n // MCP_URL is the full URL (e.g., https://mcp.example.com), fallback to localhost with port\n const mcpUrl = process.env.MCP_URL ?? `http://localhost:${process.env.MCP_DEV_PORT ?? '3001'}`\n const meilisearchHost = process.env.MEILISEARCH_HOST ?? null\n\n // Check search service availability\n let searchStatus: { available: boolean; driver: string | null; url: string | null } = {\n available: false,\n driver: null,\n url: meilisearchHost,\n }\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const searchService = container.resolve<{\n isStrategyAvailable: (strategy: string) => boolean\n }>('searchService')\n const available = searchService.isStrategyAvailable('fulltext')\n searchStatus = {\n available,\n driver: available ? 'meilisearch' : null,\n url: meilisearchHost,\n }\n } catch {\n // Search service not available\n }\n\n try {\n const [health, mcp] = await Promise.all([client.health(), client.mcpStatus()])\n\n return {\n status: 'ok',\n opencode: health,\n mcp,\n search: searchStatus,\n url,\n mcpUrl,\n }\n } catch (error) {\n return {\n status: 'error',\n search: searchStatus,\n message: error instanceof Error ? error.message : 'OpenCode not reachable',\n url,\n mcpUrl,\n }\n }\n}\n\n/**\n * Extract text content from OpenCode message response.\n */\nexport function extractTextFromResponse(result: unknown): string | null {\n if (!result || typeof result !== 'object') return null\n\n const message = result as { parts?: Array<{ type: string; text?: string }> }\n if (!message.parts) return null\n\n const textParts = message.parts.filter((p) => p.type === 'text' && p.text)\n return textParts.map((p) => p.text).join('\\n') || null\n}\n\n/**\n * Response part from OpenCode - can be text, tool-call, tool-result, etc.\n */\nexport interface OpenCodeResponsePart {\n id: string\n type: string\n text?: string\n // Tool call fields (OpenCode uses 'tool_use' type)\n name?: string\n input?: unknown\n // Tool result fields (OpenCode uses 'tool_result' type)\n tool_use_id?: string\n content?: unknown\n // Step fields (step-start, step-finish)\n sessionID?: string\n messageID?: string\n reason?: string\n cost?: number\n tokens?: {\n input: number\n output: number\n reasoning?: number\n cache?: { read: number; write: number }\n }\n // Generic catch-all\n [key: string]: unknown\n}\n\n/**\n * Metadata about the OpenCode response.\n */\nexport interface OpenCodeResponseMetadata {\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n timing?: { created: number; completed?: number }\n}\n\n/**\n * Extract all parts from OpenCode response for verbose debugging.\n */\nexport function extractAllPartsFromResponse(result: unknown): OpenCodeResponsePart[] {\n if (!result || typeof result !== 'object') return []\n\n const message = result as { parts?: OpenCodeResponsePart[] }\n return message.parts || []\n}\n\n/**\n * Extract metadata (model, tokens, timing) from OpenCode response.\n */\nexport function extractMetadataFromResponse(result: unknown): OpenCodeResponseMetadata | null {\n if (!result || typeof result !== 'object') return null\n\n const message = result as {\n info?: {\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n time?: { created: number; completed?: number }\n }\n }\n\n if (!message.info) return null\n\n return {\n modelID: message.info.modelID,\n providerID: message.info.providerID,\n tokens: message.info.tokens,\n timing: message.info.time,\n }\n}\n\n/**\n * Event types emitted during streaming message handling.\n */\nexport type OpenCodeStreamEvent =\n | { type: 'thinking' }\n | { type: 'text'; content: string }\n | { type: 'tool-call'; id: string; toolName: string; args: unknown }\n | { type: 'tool-result'; id: string; result: unknown }\n | { type: 'question'; question: OpenCodeQuestion }\n | { type: 'metadata'; model?: string; provider?: string; tokens?: { input: number; output: number }; durationMs?: number }\n | { type: 'debug'; partType: string; data: unknown }\n | { type: 'done'; sessionId: string }\n | { type: 'error'; error: string }\n\n/**\n * Handle OpenCode message with real-time SSE streaming.\n * Uses OpenCode's /event SSE endpoint for live updates.\n *\n * OpenCode does agentic loops - it may generate multiple assistant messages\n * with tool calls in between. We complete only when the session becomes \"idle\"\n * after being \"busy\", indicating the full agentic loop is done.\n */\nexport async function handleOpenCodeMessageStreaming(\n request: OpenCodeTestRequest,\n onEvent: (event: OpenCodeStreamEvent) => Promise<void>\n): Promise<void> {\n const client = getClient()\n const { message, sessionId, model } = request\n const startTime = Date.now()\n\n // Accumulators for usage summary\n const usageStats = {\n toolCalls: 0,\n toolNames: [] as string[],\n totalInputTokens: 0,\n totalOutputTokens: 0,\n messageCount: 0,\n }\n\n if (!message) {\n await onEvent({ type: 'error', error: 'Message is required' })\n return\n }\n\n try {\n // Create or get session\n let session\n if (sessionId) {\n session = await client.getSession(sessionId)\n } else {\n session = await client.createSession()\n }\n\n const targetSessionId = session.id\n let unsubscribe: (() => void) | null = null\n let emittedThinking = false\n let wasBusy = false // Track if session was ever busy\n let resolved = false // Track if we've already completed\n let lastActivityTime = Date.now() // Track last event for heartbeat\n let heartbeatInterval: NodeJS.Timeout | null = null\n let lastMetadata: {\n model?: string\n provider?: string\n tokens?: { input: number; output: number }\n } | null = null\n\n // Helper to clean up resources\n const cleanup = () => {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval)\n heartbeatInterval = null\n }\n }\n\n // Set up SSE subscription for real-time events\n const eventPromise = new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n cleanup()\n unsubscribe?.()\n reject(new Error('OpenCode request timed out'))\n }, 300000) // 5 minute timeout for complex agentic tasks\n\n // Heartbeat: Check every second for completion conditions\n heartbeatInterval = setInterval(async () => {\n if (resolved) return\n\n const idleTime = Date.now() - lastActivityTime\n\n // If no activity for 5 seconds and we were busy, check session status\n if (idleTime >= 5000 && wasBusy && !resolved) {\n try {\n // Check actual session status before completing\n const status = await client.getSessionStatus(targetSessionId)\n\n if (status.status === 'busy') {\n // Session is still busy - wait\n return\n }\n\n if (status.status === 'waiting' && status.questionId) {\n // Session is waiting for a question answer\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.id === status.questionId)\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now() // Reset timer after emitting question\n return\n }\n }\n\n // Check for any pending questions for this session\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now() // Reset timer after emitting question\n } else if (status.status === 'idle') {\n // Session is explicitly idle and no questions - complete\n resolved = true\n const durationMs = Date.now() - startTime\n console.error(`[AI Usage] Session complete (heartbeat): sessionId=${targetSessionId.slice(0, 16)}... duration=${durationMs}ms tokens={in:${usageStats.totalInputTokens},out:${usageStats.totalOutputTokens}} toolCalls=${usageStats.toolCalls} tools=[${usageStats.toolNames.join(',')}] messages=${usageStats.messageCount}`)\n try {\n await onEvent({ type: 'done', sessionId: targetSessionId })\n } catch (err) {\n console.error('[OpenCode SSE] Heartbeat: Failed to emit done event:', err)\n }\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n // Status is 'unknown' or something else - wait for SSE events\n } catch (err) {\n console.error('[OpenCode SSE] Heartbeat error:', err)\n }\n }\n }, 1000)\n\n unsubscribe = client.subscribeToEvents(\n async (sseEvent) => {\n try {\n const { type, properties } = sseEvent\n\n // Update activity timestamp for heartbeat\n lastActivityTime = Date.now()\n\n // Filter events for our session\n const eventSessionId =\n (properties.sessionID as string) ||\n (properties.info as { sessionID?: string })?.sessionID ||\n (properties.part as { sessionID?: string })?.sessionID ||\n (properties.question as { sessionID?: string })?.sessionID ||\n (properties.session as { id?: string })?.id ||\n (properties.status as { sessionID?: string })?.sessionID\n\n if (eventSessionId && eventSessionId !== targetSessionId) {\n return // Ignore events from other sessions\n }\n\n switch (type) {\n case 'question.asked': {\n // OpenCode is asking a question - use the data directly from the SSE event\n await onEvent({ type: 'debug', partType: 'question-asked', data: properties })\n\n // The question data is in properties.question (from SSE event)\n // This is more reliable than fetching from API which may return incomplete data\n const questionFromEvent = properties.question as OpenCodeQuestion | undefined\n\n if (questionFromEvent && questionFromEvent.sessionID === targetSessionId) {\n await onEvent({ type: 'question', question: questionFromEvent })\n } else {\n // Fallback to fetching from API if event doesn't have full question\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n }\n }\n break\n }\n\n case 'session.status': {\n const status = properties.status as { type: string; questionId?: string }\n\n if (status?.type === 'busy') {\n wasBusy = true\n if (!emittedThinking) {\n emittedThinking = true\n await onEvent({ type: 'thinking' })\n }\n } else if (status?.type === 'waiting' && !resolved) {\n // Session is waiting for user to answer a question\n const questions = await client.getPendingQuestions()\n const sessionQuestion = status.questionId\n ? questions.find((q) => q.id === status.questionId)\n : questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n await onEvent({ type: 'question', question: sessionQuestion })\n lastActivityTime = Date.now()\n }\n } else if (status?.type === 'idle' && wasBusy && !resolved) {\n // Session went from busy to idle - check if there are pending questions\n const endTime = Date.now()\n\n // Emit final metadata if we have it\n if (lastMetadata) {\n await onEvent({\n type: 'metadata',\n model: lastMetadata.model,\n provider: lastMetadata.provider,\n tokens: lastMetadata.tokens,\n durationMs: endTime - startTime,\n })\n }\n\n // Check for pending questions before declaring done\n const questions = await client.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === targetSessionId)\n\n if (sessionQuestion) {\n // Question found - emit it but keep stream open for answer\n await onEvent({ type: 'question', question: sessionQuestion })\n // Reset activity time so heartbeat doesn't close prematurely\n lastActivityTime = Date.now()\n // Don't set resolved - let heartbeat handle completion after user answers\n } else {\n // No questions found - but give OpenCode a moment to register one\n // (race condition prevention)\n setTimeout(async () => {\n try {\n if (resolved) {\n return\n }\n\n // Check one more time for questions\n const finalQuestions = await client.getPendingQuestions()\n const finalQuestion = finalQuestions.find((q) => q.sessionID === targetSessionId)\n\n if (finalQuestion) {\n await onEvent({ type: 'question', question: finalQuestion })\n lastActivityTime = Date.now()\n } else {\n // Truly idle - complete the stream\n resolved = true\n const durationMs = Date.now() - startTime\n console.error(`[AI Usage] Session complete: sessionId=${targetSessionId.slice(0, 16)}... duration=${durationMs}ms tokens={in:${usageStats.totalInputTokens},out:${usageStats.totalOutputTokens}} toolCalls=${usageStats.toolCalls} tools=[${usageStats.toolNames.join(',')}] messages=${usageStats.messageCount}`)\n await onEvent({ type: 'done', sessionId: targetSessionId })\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n } catch (err) {\n console.error('[OpenCode SSE] Error in timeout callback:', err)\n // Still try to complete even if there was an error\n if (!resolved) {\n resolved = true\n try {\n await onEvent({ type: 'done', sessionId: targetSessionId })\n } catch (e2) {\n console.error('[OpenCode SSE] Failed to emit done event:', e2)\n }\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n resolve()\n }\n }\n }, 2000)\n }\n }\n break\n }\n\n case 'message.updated': {\n const info = properties.info as {\n id: string\n role: string\n time?: { completed?: number }\n modelID?: string\n providerID?: string\n tokens?: { input: number; output: number }\n error?: { name: string; message?: string }\n }\n\n if (info.role === 'assistant') {\n // Check for error\n if (info.error) {\n cleanup()\n clearTimeout(timeout)\n unsubscribe?.()\n await onEvent({\n type: 'error',\n error: `${info.error.name}: ${info.error.message || 'Unknown error'}`,\n })\n resolve()\n return\n }\n\n // Track metadata from completed messages\n if (info.time?.completed) {\n lastMetadata = {\n model: info.modelID,\n provider: info.providerID,\n tokens: info.tokens,\n }\n\n // Accumulate token usage\n usageStats.messageCount++\n if (info.tokens) {\n usageStats.totalInputTokens += info.tokens.input || 0\n usageStats.totalOutputTokens += info.tokens.output || 0\n console.error(`[AI Usage] Tokens (message ${usageStats.messageCount}): input=${info.tokens.input} output=${info.tokens.output} model=${info.modelID || 'unknown'}`)\n }\n\n // Emit intermediate metadata for visibility\n await onEvent({\n type: 'debug',\n partType: 'message-completed',\n data: { messageId: info.id, tokens: info.tokens },\n })\n // Note: Completion is now handled by heartbeat interval\n }\n }\n break\n }\n\n case 'message.part.updated': {\n const part = properties.part as {\n type: string\n text?: string\n name?: string\n input?: unknown\n tool_use_id?: string\n content?: unknown\n id: string\n }\n const delta = properties.delta as string | undefined\n\n switch (part.type) {\n case 'text':\n // Use delta for streaming text if available\n if (delta) {\n await onEvent({ type: 'text', content: delta })\n }\n break\n case 'thinking':\n // Extended thinking blocks \u2014 route to debug panel only, never to chat\n console.error(`[OpenCode SSE] Thinking block received (${(delta || part.text || '').length} chars)`)\n await onEvent({ type: 'debug', partType: 'thinking', data: { text: delta || part.text } })\n break\n case 'tool_use':\n if (part.name) {\n usageStats.toolCalls++\n usageStats.toolNames.push(part.name)\n console.error(`[AI Usage] Tool call #${usageStats.toolCalls}: ${part.name}`)\n await onEvent({\n type: 'tool-call',\n id: part.id,\n toolName: part.name,\n args: part.input,\n })\n }\n break\n case 'tool_result':\n await onEvent({\n type: 'tool-result',\n id: part.tool_use_id || part.id,\n result: part.content,\n })\n break\n case 'step-start':\n case 'step-finish':\n await onEvent({ type: 'debug', partType: part.type, data: part })\n break\n }\n break\n }\n\n case 'message.part.delta': {\n const part = properties.part as { type?: string } | undefined\n const delta = properties.delta as string | undefined\n // Filter out thinking deltas \u2014 only stream text part deltas to chat\n if (delta && part?.type !== 'thinking') {\n await onEvent({ type: 'text', content: delta })\n }\n break\n }\n }\n } catch (err) {\n console.error('[OpenCode SSE] Error processing event:', err)\n }\n },\n (error) => {\n clearTimeout(timeout)\n reject(error)\n }\n )\n })\n\n // Send message (don't await - let SSE handle the response via events)\n // We only catch errors here - successful completion is signaled via SSE session.status: idle\n client.sendMessage(session.id, message, { model }).catch((err) => {\n // Log send errors - SSE should also receive an error event\n console.error('[OpenCode] Send error (SSE should handle):', err)\n })\n\n // Wait for SSE to indicate completion (session.status: idle or error)\n await eventPromise\n } catch (error) {\n await onEvent({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n }\n}\n\n/**\n * Answer a pending question and continue processing.\n * Uses polling to check for completion/next question.\n */\nexport async function handleOpenCodeAnswer(\n questionId: string,\n answer: number,\n sessionId: string,\n onEvent: (event: OpenCodeStreamEvent) => Promise<void>\n): Promise<void> {\n const client = getClient()\n\n try {\n // Answer the question\n await client.answerQuestion(questionId, answer)\n await onEvent({ type: 'thinking' })\n\n // Poll for completion using session status (max 20 seconds for same-question wait, 60 seconds total)\n const maxAttempts = 30\n const pollInterval = 2000\n let sameQuestionWaitCount = 0\n const maxSameQuestionWait = 5 // Give up after 10 seconds of waiting on same question\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n await new Promise((resolve) => setTimeout(resolve, pollInterval))\n\n // Check session status - most reliable way to know if processing is done\n const status = await client.getSessionStatus(sessionId)\n\n if (status.status === 'idle' || status.status === 'unknown') {\n // Session is idle or unknown - processing complete\n await onEvent({ type: 'done', sessionId })\n return\n }\n\n if (status.status === 'waiting' && status.questionId && status.questionId !== questionId) {\n // A new question appeared - fetch and emit it\n const allQuestions = await client.getPendingQuestions()\n const newQuestion = allQuestions.find((q) => q.id === status.questionId)\n if (newQuestion) {\n await onEvent({ type: 'question', question: newQuestion })\n return\n }\n }\n\n // If waiting on the same question we answered, track how long\n if (status.status === 'waiting' && status.questionId === questionId) {\n sameQuestionWaitCount++\n if (sameQuestionWaitCount >= maxSameQuestionWait) {\n // OpenCode didn't properly clear the question - assume answered and complete\n await onEvent({ type: 'done', sessionId })\n return\n }\n } else {\n // Reset counter if status changed\n sameQuestionWaitCount = 0\n }\n\n // Session is busy - keep polling\n }\n\n // Timeout - assume complete\n await onEvent({ type: 'done', sessionId })\n } catch (error) {\n console.error('[OpenCode Answer] Error:', error)\n await onEvent({\n type: 'error',\n error: error instanceof Error ? error.message : 'Failed to answer question',\n })\n }\n}\n\n/**\n * Get pending questions for a session.\n */\nexport async function getPendingQuestions(): Promise<OpenCodeQuestion[]> {\n const client = getClient()\n return client.getPendingQuestions()\n}\n\n// Re-export the question type\nexport type { OpenCodeQuestion }\n"],
5
+ "mappings": "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",
6
6
  "names": ["questions", "sessionQuestion"]
7
7
  }
@@ -0,0 +1,124 @@
1
+ import vm from "node:vm";
2
+ const MAX_LOG_ENTRIES = 100;
3
+ const MAX_LOG_ENTRY_LENGTH = 1e3;
4
+ function createSandbox(globals, options = {}) {
5
+ const { timeout = 3e4, maxApiCalls = 50 } = options;
6
+ return {
7
+ async execute(code) {
8
+ const logs = [];
9
+ const start = Date.now();
10
+ const consolProxy = {
11
+ log: (...args) => pushLog(logs, args),
12
+ info: (...args) => pushLog(logs, args),
13
+ warn: (...args) => pushLog(logs, args),
14
+ error: (...args) => pushLog(logs, args),
15
+ debug: (...args) => pushLog(logs, args)
16
+ };
17
+ const contextGlobals = {
18
+ // Safe built-ins
19
+ JSON,
20
+ Object,
21
+ Array,
22
+ Map,
23
+ Set,
24
+ Promise,
25
+ Math,
26
+ Date,
27
+ RegExp,
28
+ String,
29
+ Number,
30
+ Boolean,
31
+ parseInt,
32
+ parseFloat,
33
+ isNaN,
34
+ isFinite,
35
+ encodeURIComponent,
36
+ decodeURIComponent,
37
+ Error,
38
+ TypeError,
39
+ RangeError,
40
+ undefined: void 0,
41
+ NaN: NaN,
42
+ Infinity: Infinity,
43
+ // Sandboxed console
44
+ console: consolProxy,
45
+ // Blocked — explicitly set to undefined
46
+ require: void 0,
47
+ process: void 0,
48
+ global: void 0,
49
+ globalThis: void 0,
50
+ fetch: void 0,
51
+ XMLHttpRequest: void 0,
52
+ WebSocket: void 0,
53
+ Buffer: void 0,
54
+ setTimeout: void 0,
55
+ setInterval: void 0,
56
+ __dirname: void 0,
57
+ __filename: void 0,
58
+ // Caller-provided globals (spec, api, context, etc.)
59
+ ...globals
60
+ };
61
+ const ctx = vm.createContext(contextGlobals);
62
+ try {
63
+ const normalized = normalizeCode(code);
64
+ const wrapped = `(${normalized})()`;
65
+ const script = new vm.Script(wrapped, {
66
+ filename: "sandbox.js"
67
+ });
68
+ const promise = script.runInContext(ctx, { timeout });
69
+ const result = await Promise.race([
70
+ promise,
71
+ new Promise(
72
+ (_, reject) => (
73
+ // Use global setTimeout (not the blocked sandbox one)
74
+ globalThis.setTimeout(
75
+ () => reject(new Error(`Execution timed out after ${timeout}ms`)),
76
+ timeout
77
+ )
78
+ )
79
+ )
80
+ ]);
81
+ return {
82
+ result,
83
+ logs,
84
+ durationMs: Date.now() - start
85
+ };
86
+ } catch (error) {
87
+ return {
88
+ result: null,
89
+ error: error instanceof Error ? error.message : String(error),
90
+ logs,
91
+ durationMs: Date.now() - start
92
+ };
93
+ }
94
+ }
95
+ };
96
+ }
97
+ function normalizeCode(code) {
98
+ let normalized = code.trim();
99
+ normalized = normalized.replace(/^```(?:javascript|js|typescript|ts)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
100
+ if (!/^\s*async\s*\(/.test(normalized)) {
101
+ const isStatement = /^\s*(const|let|var|for|while|if|try|switch|return|throw|class|function)\b/.test(normalized);
102
+ normalized = isStatement ? `async () => { ${normalized} }` : `async () => { return ${normalized} }`;
103
+ }
104
+ return normalized;
105
+ }
106
+ function pushLog(logs, args) {
107
+ if (logs.length >= MAX_LOG_ENTRIES) return;
108
+ const message = args.map((arg) => {
109
+ if (typeof arg === "string") return arg;
110
+ try {
111
+ return JSON.stringify(arg);
112
+ } catch {
113
+ return String(arg);
114
+ }
115
+ }).join(" ");
116
+ logs.push(
117
+ message.length > MAX_LOG_ENTRY_LENGTH ? message.slice(0, MAX_LOG_ENTRY_LENGTH) + "..." : message
118
+ );
119
+ }
120
+ export {
121
+ createSandbox,
122
+ normalizeCode
123
+ };
124
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/sandbox.ts"],
4
+ "sourcesContent": ["/**\n * Sandboxed Code Execution Engine\n *\n * Uses node:vm to run AI-generated JavaScript in a restricted sandbox.\n * Only whitelisted globals are available \u2014 no file system, network, or process access.\n */\n\nimport vm from 'node:vm'\n\nexport interface SandboxOptions {\n /** Execution timeout in milliseconds (default: 30_000) */\n timeout?: number\n /** Maximum output size in bytes (default: 1_048_576 / 1MB) */\n maxOutputSize?: number\n /** Maximum number of api.request() calls allowed (default: 50) */\n maxApiCalls?: number\n}\n\nexport interface SandboxResult {\n result: unknown\n error?: string\n logs: string[]\n durationMs: number\n apiCallCount?: number\n}\n\nconst MAX_LOG_ENTRIES = 100\nconst MAX_LOG_ENTRY_LENGTH = 1000\n\n/**\n * Create a sandboxed execution environment.\n *\n * @param globals - Custom globals to inject (e.g., spec, api, context)\n * @param options - Sandbox configuration\n */\nexport function createSandbox(\n globals: Record<string, unknown>,\n options: SandboxOptions = {}\n) {\n const { timeout = 30_000, maxApiCalls = 50 } = options\n\n return {\n async execute(code: string): Promise<SandboxResult> {\n const logs: string[] = []\n const start = Date.now()\n\n // Capture console output\n const consolProxy = {\n log: (...args: unknown[]) => pushLog(logs, args),\n info: (...args: unknown[]) => pushLog(logs, args),\n warn: (...args: unknown[]) => pushLog(logs, args),\n error: (...args: unknown[]) => pushLog(logs, args),\n debug: (...args: unknown[]) => pushLog(logs, args),\n }\n\n // Build context with safe globals + caller-provided globals\n const contextGlobals: Record<string, unknown> = {\n // Safe built-ins\n JSON,\n Object,\n Array,\n Map,\n Set,\n Promise,\n Math,\n Date,\n RegExp,\n String,\n Number,\n Boolean,\n parseInt,\n parseFloat,\n isNaN,\n isFinite,\n encodeURIComponent,\n decodeURIComponent,\n Error,\n TypeError,\n RangeError,\n undefined,\n NaN,\n Infinity,\n\n // Sandboxed console\n console: consolProxy,\n\n // Blocked \u2014 explicitly set to undefined\n require: undefined,\n process: undefined,\n global: undefined,\n globalThis: undefined,\n fetch: undefined,\n XMLHttpRequest: undefined,\n WebSocket: undefined,\n Buffer: undefined,\n setTimeout: undefined,\n setInterval: undefined,\n __dirname: undefined,\n __filename: undefined,\n\n // Caller-provided globals (spec, api, context, etc.)\n ...globals,\n }\n\n const ctx = vm.createContext(contextGlobals)\n\n try {\n const normalized = normalizeCode(code)\n\n // Invoke the normalized async function directly\n const wrapped = `(${normalized})()`\n\n const script = new vm.Script(wrapped, {\n filename: 'sandbox.js',\n })\n\n // Run the script \u2014 returns a Promise\n const promise = script.runInContext(ctx, { timeout })\n\n // Await with secondary timeout (for async operations like api.request)\n const result = await Promise.race([\n promise,\n new Promise((_, reject) =>\n // Use global setTimeout (not the blocked sandbox one)\n globalThis.setTimeout(\n () => reject(new Error(`Execution timed out after ${timeout}ms`)),\n timeout\n )\n ),\n ])\n\n return {\n result,\n logs,\n durationMs: Date.now() - start,\n }\n } catch (error) {\n return {\n result: null,\n error: error instanceof Error ? error.message : String(error),\n logs,\n durationMs: Date.now() - start,\n }\n }\n },\n }\n}\n\n/**\n * Normalize AI-generated code: strip markdown fencing and validate shape.\n */\nexport function normalizeCode(code: string): string {\n let normalized = code.trim()\n\n // Strip markdown code fences\n normalized = normalized\n .replace(/^```(?:javascript|js|typescript|ts)?\\s*\\n?/i, '')\n .replace(/\\n?```\\s*$/, '')\n .trim()\n\n // Auto-wrap bare code into async arrow functions\n if (!/^\\s*async\\s*\\(/.test(normalized)) {\n // Detect statement-leading keywords \u2014 these cannot follow `return`\n const isStatement = /^\\s*(const|let|var|for|while|if|try|switch|return|throw|class|function)\\b/.test(normalized)\n normalized = isStatement\n ? `async () => { ${normalized} }`\n : `async () => { return ${normalized} }`\n }\n\n return normalized\n}\n\nfunction pushLog(logs: string[], args: unknown[]): void {\n if (logs.length >= MAX_LOG_ENTRIES) return\n\n const message = args\n .map((arg) => {\n if (typeof arg === 'string') return arg\n try {\n return JSON.stringify(arg)\n } catch {\n return String(arg)\n }\n })\n .join(' ')\n\n logs.push(\n message.length > MAX_LOG_ENTRY_LENGTH\n ? message.slice(0, MAX_LOG_ENTRY_LENGTH) + '...'\n : message\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,103 @@
1
+ import { createHash } from "node:crypto";
2
+ const SESSION_TTL_MS = 5 * 60 * 1e3;
3
+ const CLEANUP_INTERVAL_MS = 2 * 60 * 1e3;
4
+ const MAX_SEARCH_ENTRIES = 50;
5
+ const MAX_MEMORY_CONTEXT_LENGTH = 500;
6
+ const TOOL_CALL_HARD_CAP = 10;
7
+ const TOOL_CALL_WINDOW_MS = 60 * 1e3;
8
+ const sessions = /* @__PURE__ */ new Map();
9
+ function getOrCreateSession(token) {
10
+ let session = sessions.get(token);
11
+ if (!session) {
12
+ session = {
13
+ searchCache: /* @__PURE__ */ new Map(),
14
+ createdAt: Date.now(),
15
+ lastAccessedAt: Date.now(),
16
+ toolCallCount: 0,
17
+ toolCallWindowStart: Date.now()
18
+ };
19
+ sessions.set(token, session);
20
+ }
21
+ session.lastAccessedAt = Date.now();
22
+ return session;
23
+ }
24
+ function hashCode(code) {
25
+ return createHash("sha256").update(code).digest("hex").slice(0, 16);
26
+ }
27
+ function evictOldest(map, maxSize) {
28
+ if (map.size <= maxSize) return;
29
+ let oldestKey = null;
30
+ let oldestTime = Infinity;
31
+ for (const [key, entry] of map) {
32
+ if (entry.timestamp < oldestTime) {
33
+ oldestTime = entry.timestamp;
34
+ oldestKey = key;
35
+ }
36
+ }
37
+ if (oldestKey) {
38
+ map.delete(oldestKey);
39
+ }
40
+ }
41
+ function lookupSearchCache(token, code) {
42
+ const session = sessions.get(token);
43
+ if (!session) return null;
44
+ session.lastAccessedAt = Date.now();
45
+ const key = hashCode(code);
46
+ return session.searchCache.get(key) ?? null;
47
+ }
48
+ function storeSearchResult(token, code, result, label) {
49
+ const session = getOrCreateSession(token);
50
+ const key = hashCode(code);
51
+ evictOldest(session.searchCache, MAX_SEARCH_ENTRIES);
52
+ session.searchCache.set(key, { result, timestamp: Date.now(), label });
53
+ }
54
+ function incrementToolCallCount(token) {
55
+ const session = getOrCreateSession(token);
56
+ const now = Date.now();
57
+ if (now - session.toolCallWindowStart > TOOL_CALL_WINDOW_MS) {
58
+ session.toolCallCount = 0;
59
+ session.toolCallWindowStart = now;
60
+ }
61
+ session.toolCallCount++;
62
+ return {
63
+ count: session.toolCallCount,
64
+ exceeded: session.toolCallCount > TOOL_CALL_HARD_CAP
65
+ };
66
+ }
67
+ function buildMemoryContext(token) {
68
+ const session = sessions.get(token);
69
+ if (!session) return "";
70
+ const searchCount = session.searchCache.size;
71
+ if (searchCount === 0) return "";
72
+ const labels = Array.from(session.searchCache.values()).slice(-3).map((e) => e.label).join(", ");
73
+ const context = `[Memory: ${searchCount} schema searches cached (${labels}). Reuse previous schema results instead of re-calling.]`;
74
+ if (context.length > MAX_MEMORY_CONTEXT_LENGTH) {
75
+ return context.slice(0, MAX_MEMORY_CONTEXT_LENGTH - 3) + "...]";
76
+ }
77
+ return context;
78
+ }
79
+ function buildSearchLabel(code) {
80
+ const helperMatch = code.match(/spec\.(findEndpoints|describeEndpoint|describeEntity)\s*\(([^)]*)\)/);
81
+ if (helperMatch) {
82
+ return `${helperMatch[1]}(${helperMatch[2].slice(0, 30)})`;
83
+ }
84
+ const cleaned = code.replace(/async\s*\(\)\s*=>\s*/, "").trim();
85
+ return cleaned.slice(0, 40);
86
+ }
87
+ const cleanupTimer = setInterval(() => {
88
+ const now = Date.now();
89
+ for (const [token, session] of sessions) {
90
+ if (now - session.lastAccessedAt > SESSION_TTL_MS) {
91
+ sessions.delete(token);
92
+ }
93
+ }
94
+ }, CLEANUP_INTERVAL_MS);
95
+ cleanupTimer.unref();
96
+ export {
97
+ buildMemoryContext,
98
+ buildSearchLabel,
99
+ incrementToolCallCount,
100
+ lookupSearchCache,
101
+ storeSearchResult
102
+ };
103
+ //# sourceMappingURL=session-memory.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/session-memory.ts"],
4
+ "sourcesContent": ["/**\n * Session Memory\n *\n * Process-level in-memory cache keyed by session token (`sess_xxx`).\n * Deduplicates schema/spec search queries within a single conversation,\n * reducing redundant tool calls by the AI agent.\n *\n * - Search cache: exact code string match \u2192 return cached result (max 50 entries)\n * - Memory context summary appended to every response to remind the agent\n * - Sessions auto-expire after 5 minutes (short-lived cache for active conversations)\n *\n * NOTE: Only schema/spec lookups are cached. API responses are NEVER cached\n * because data can change from external sources between calls.\n */\n\nimport { createHash } from 'node:crypto'\n\nconst SESSION_TTL_MS = 5 * 60 * 1000 // 5 minutes\nconst CLEANUP_INTERVAL_MS = 2 * 60 * 1000 // 2 minutes\nconst MAX_SEARCH_ENTRIES = 50\nconst MAX_MEMORY_CONTEXT_LENGTH = 500\n\ninterface CacheEntry {\n result: unknown\n timestamp: number\n label: string\n}\n\nconst TOOL_CALL_HARD_CAP = 10\nconst TOOL_CALL_WINDOW_MS = 60 * 1000 // 60 seconds \u2014 resets for new user message\n\ninterface SessionMemory {\n searchCache: Map<string, CacheEntry>\n createdAt: number\n lastAccessedAt: number\n toolCallCount: number\n toolCallWindowStart: number\n}\n\nconst sessions = new Map<string, SessionMemory>()\n\nfunction getOrCreateSession(token: string): SessionMemory {\n let session = sessions.get(token)\n if (!session) {\n session = {\n searchCache: new Map(),\n createdAt: Date.now(),\n lastAccessedAt: Date.now(),\n toolCallCount: 0,\n toolCallWindowStart: Date.now(),\n }\n sessions.set(token, session)\n }\n session.lastAccessedAt = Date.now()\n return session\n}\n\nfunction hashCode(code: string): string {\n return createHash('sha256').update(code).digest('hex').slice(0, 16)\n}\n\nfunction evictOldest(map: Map<string, CacheEntry>, maxSize: number): void {\n if (map.size <= maxSize) return\n\n let oldestKey: string | null = null\n let oldestTime = Infinity\n\n for (const [key, entry] of map) {\n if (entry.timestamp < oldestTime) {\n oldestTime = entry.timestamp\n oldestKey = key\n }\n }\n\n if (oldestKey) {\n map.delete(oldestKey)\n }\n}\n\n/**\n * Look up a cached search result by exact code string match.\n */\nexport function lookupSearchCache(token: string, code: string): CacheEntry | null {\n const session = sessions.get(token)\n if (!session) return null\n session.lastAccessedAt = Date.now()\n\n const key = hashCode(code)\n return session.searchCache.get(key) ?? null\n}\n\n/**\n * Store a search result in the session cache.\n */\nexport function storeSearchResult(token: string, code: string, result: unknown, label: string): void {\n const session = getOrCreateSession(token)\n const key = hashCode(code)\n\n evictOldest(session.searchCache, MAX_SEARCH_ENTRIES)\n session.searchCache.set(key, { result, timestamp: Date.now(), label })\n}\n\n/**\n * Increment the tool call counter for a session.\n * Resets the counter if more than TOOL_CALL_WINDOW_MS has elapsed (new user message).\n * Returns the current count and whether the hard cap has been exceeded.\n */\nexport function incrementToolCallCount(token: string): { count: number; exceeded: boolean } {\n const session = getOrCreateSession(token)\n const now = Date.now()\n\n // Reset counter if the window has expired (likely a new user message)\n if (now - session.toolCallWindowStart > TOOL_CALL_WINDOW_MS) {\n session.toolCallCount = 0\n session.toolCallWindowStart = now\n }\n\n session.toolCallCount++\n\n return {\n count: session.toolCallCount,\n exceeded: session.toolCallCount > TOOL_CALL_HARD_CAP,\n }\n}\n\n/**\n * Build a summary string of what this session has already discovered.\n * Appended to tool responses so the agent knows what's cached.\n */\nexport function buildMemoryContext(token: string): string {\n const session = sessions.get(token)\n if (!session) return ''\n\n const searchCount = session.searchCache.size\n if (searchCount === 0) return ''\n\n const labels = Array.from(session.searchCache.values())\n .slice(-3)\n .map((e) => e.label)\n .join(', ')\n\n const context = `[Memory: ${searchCount} schema searches cached (${labels}). Reuse previous schema results instead of re-calling.]`\n if (context.length > MAX_MEMORY_CONTEXT_LENGTH) {\n return context.slice(0, MAX_MEMORY_CONTEXT_LENGTH - 3) + '...]'\n }\n return context\n}\n\n/**\n * Build a short label from search code for the memory context summary.\n */\nexport function buildSearchLabel(code: string): string {\n // Try to extract the helper call: spec.findEndpoints('companies')\n const helperMatch = code.match(/spec\\.(findEndpoints|describeEndpoint|describeEntity)\\s*\\(([^)]*)\\)/)\n if (helperMatch) {\n return `${helperMatch[1]}(${helperMatch[2].slice(0, 30)})`\n }\n\n // Fallback: first meaningful portion\n const cleaned = code.replace(/async\\s*\\(\\)\\s*=>\\s*/, '').trim()\n return cleaned.slice(0, 40)\n}\n\n// Periodic cleanup of expired sessions\nconst cleanupTimer = setInterval(() => {\n const now = Date.now()\n for (const [token, session] of sessions) {\n if (now - session.lastAccessedAt > SESSION_TTL_MS) {\n sessions.delete(token)\n }\n }\n}, CLEANUP_INTERVAL_MS)\n\ncleanupTimer.unref()\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }