@open-mercato/ai-assistant 0.4.11-develop.1917.b6098e78cb → 0.4.11-develop.1922.46b23ba3b5
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.
- package/dist/frontend/components/DockableChat/DockableChat.js +5 -0
- package/dist/frontend/components/DockableChat/DockableChat.js.map +2 -2
- package/dist/frontend/hooks/useCommandPalette.js +16 -12
- package/dist/frontend/hooks/useCommandPalette.js.map +2 -2
- package/dist/frontend/hooks/useMcpTools.js +22 -21
- package/dist/frontend/hooks/useMcpTools.js.map +2 -2
- package/dist/modules/ai_assistant/api/chat/route.js +9 -1
- package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/health/route.js +9 -1
- package/dist/modules/ai_assistant/api/health/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +9 -1
- package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +9 -1
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/tools/execute/route.js +9 -1
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/tools/route.js +9 -1
- package/dist/modules/ai_assistant/api/tools/route.js.map +2 -2
- package/dist/modules/ai_assistant/lib/opencode-client.js +24 -8
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +2 -2
- package/package.json +4 -4
- package/src/frontend/components/DockableChat/DockableChat.tsx +5 -0
- package/src/frontend/hooks/useCommandPalette.ts +17 -12
- package/src/frontend/hooks/useMcpTools.ts +22 -22
- package/src/modules/ai_assistant/api/chat/route.ts +9 -0
- package/src/modules/ai_assistant/api/health/route.ts +9 -0
- package/src/modules/ai_assistant/api/route/route.ts +9 -0
- package/src/modules/ai_assistant/api/settings/route.ts +9 -0
- package/src/modules/ai_assistant/api/tools/execute/route.ts +9 -0
- package/src/modules/ai_assistant/api/tools/route.ts +9 -0
- package/src/modules/ai_assistant/lib/opencode-client.ts +24 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/chat/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n handleOpenCodeMessageStreaming,\n type OpenCodeStreamEvent,\n} from '../../lib/opencode-handlers'\nimport { createOpenCodeClient } from '../../lib/opencode-client'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n generateSessionToken,\n createSessionApiKey,\n} from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\n/**\n * System instructions injected at the start of new chat sessions.\n * These ensure the AI follows the correct workflow for data operations.\n */\nconst CHAT_SYSTEM_INSTRUCTIONS = `\nYou are a helpful business assistant for Open Mercato.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nABSOLUTE RULES \u2014 FOLLOW THESE OR BE CUT OFF\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n1. READ = GET only. If the user says find/list/show/search/get \u2192 use only GET. NEVER call PUT/POST/DELETE for a read query.\n2. PUT path = collection path. id goes in the BODY, not the URL. Example: PUT /api/customers/companies with { id: '...', name: 'New' }. There are NO /{id} path segments.\n3. Confirm before ANY write. Before POST/PUT/DELETE: present your plan in business language, then STOP and wait for user to say \"yes\". Do NOT execute the write in the same turn.\n4. Maximum 4 tool calls per message. Hard limit is 10.\n\nYou have 2 tools \u2014 both accept a \"code\" parameter with an async JavaScript arrow function.\n\nTOOL: search \u2014 discover endpoints and schemas (READ-ONLY, fast)\n - spec.findEndpoints(keyword) \u2192 [{ path, methods }]\n - spec.describeEndpoint(path, method) \u2192 COMPACT: { requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }\n - spec.describeEntity(keyword) \u2192 { className, fields, relationships }\n\nTOOL: execute \u2014 make API calls (reads and writes)\n - api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }\n\nCOMMON API PATHS (use directly \u2014 do NOT call findEndpoints for these):\n /api/customers/companies \u2014 companies (GET list, POST create, PUT update)\n /api/customers/people \u2014 contacts/people\n /api/customers/deals \u2014 deals/opportunities\n /api/customers/activities \u2014 activities/tasks\n /api/sales/orders \u2014 sales orders\n /api/sales/quotes \u2014 quotes\n /api/sales/invoices \u2014 invoices\n /api/catalog/products \u2014 products\n /api/catalog/categories \u2014 categories\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nRECIPES \u2014 follow EXACTLY for each task type\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nFIND/LIST records (1 call):\n For COMMON PATHS: skip describeEndpoint, go straight to execute.\n 1. execute: api.request({ method: 'GET', path: '/api/<module>/<resource>' })\n The \"search\" query param only matches indexed text fields \u2014 it will NOT match concepts like \"Polish\" or \"large\".\n For conceptual/subjective queries, fetch ALL records and use YOUR reasoning to identify matches from the returned data.\n For unknown paths: 1 search + 1 execute.\n\nUPDATE a record (3-4 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'PUT') \u2192 learn requestBody fields AND relatedEntity\n 2. execute: GET the record \u2192 find it, get its ID\n 3. execute: PUT to the COLLECTION path with id IN THE BODY:\n api.request({ method: 'PUT', path: '/api/<module>/<resource>', body: { id: '<uuid>', ...changes } })\n NOTE: All CRUD endpoints use the COLLECTION path. The id goes in the request BODY, not the URL. There are NO /{id} path segments.\n\nCREATE a record (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 gives requiredFields, optionalFields, nestedCollections, and a working example\n 2. Ask user for confirmation with the field values\n 3. execute: api.request({ method: 'POST', ...body })\n If the endpoint has nestedCollections (like lines), include them INLINE in the body \u2014 do NOT create them separately.\n Use the \"example\" from describeEndpoint as your template \u2014 fill in real values.\n Example \u2014 create a quote with line items:\n api.request({ method: 'POST', path: '/api/sales/quotes', body: {\n currencyCode: 'EUR', customerEntityId: '<company-uuid>',\n lines: [{ currencyCode: 'EUR', quantity: 1, productId: '<product-uuid>', name: 'Product Name', kind: 'product' }]\n }})\n Do NOT create lines separately. Do NOT include id, quoteId, or total fields \u2014 the server generates them.\n\nCREATE MULTIPLE records (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 learn fields + example\n 2. execute: loop in one call:\n async () => {\n const results = [];\n for (const item of items) {\n results.push(await api.request({ method: 'POST', path: '...', body: item }));\n }\n return results;\n }\n\nDISCOVER (what endpoints/entities exist) (1 call):\n 1. search: spec.findEndpoints('<keyword>') or spec.describeEntity('<keyword>')\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nHARD RULES\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n- MAXIMUM 4 tool calls per user message. You WILL be cut off after 10.\n- NEVER call findEndpoints or describeEndpoint for COMMON PATHS listed above \u2014 use them directly with execute.\n- NEVER call describeEntity if describeEndpoint already returned relatedEntity.\n- NEVER repeat a search from earlier in the conversation \u2014 reuse previous results.\n- NEVER make N+1 API calls (1 call per record). Fetch a list and reason about the results yourself.\n- When you already have the data you need from a previous call, use it \u2014 do NOT fetch more data to \"enrich\" it.\n- Do NOT write JavaScript filters/regex to match records. Fetch data with a simple api.request() call and use YOUR knowledge to interpret the results.\n- The \"search\" query param is fulltext only \u2014 it won't match nationalities, categories, or subjective criteria. For those, fetch all and reason.\n- describeEndpoint returns a COMPACT summary with requiredFields, optionalFields, and an example. Use the example as your template \u2014 fill in real values and send it.\n- For fields you don't know, OMIT them \u2014 the API uses defaults for optional fields.\n- NEVER try to set computed/total fields (amounts, totals, counts) \u2014 the server calculates them.\n- For updates: describeEndpoint gives you the field names. Go straight to GET + PUT. PUT path is the COLLECTION path, id in BODY.\n- For creates with children (e.g. quote + lines): include children INLINE in the body using the nestedCollections field name.\n\nRESPONSE RULES:\n- Be proactive \u2014 fetch data and present results, don't ask what the user wants to see.\n- Never show technical terms, IDs, JSON, or internal reasoning.\n- Present results in clean business language with **bold names** and bullet points.\n- Only ask for confirmation before create/update/delete operations.\n`.trim()\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * Get user's role IDs from the database.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null\n): Promise<string[]> {\n if (!tenantId) return []\n\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((l) => (l.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\n/**\n * Chat endpoint that routes messages to OpenCode agent.\n * OpenCode connects to MCP server for tool access (search, execute, context_whoami).\n *\n * Emits verbose SSE events for debugging:\n * - thinking: Agent started processing\n * - metadata: Model, tokens, timing info\n * - tool-call: Tool invocation with args\n * - tool-result: Tool response\n * - text: Response text\n * - question: Confirmation question from agent\n * - done: Complete with session ID\n * - error: Error occurred\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { messages, sessionId, answerQuestion } = body as {\n messages?: Array<{ role: string; content: string }>\n sessionId?: string\n // For answering a question\n answerQuestion?: {\n questionId: string\n answer: number\n sessionId: string\n }\n }\n\n // Create SSE stream for frontend compatibility\n const encoder = new TextEncoder()\n const stream = new TransformStream()\n const writer = stream.writable.getWriter()\n let writerClosed = false\n\n const writeSSE = async (event: OpenCodeStreamEvent | { type: string; [key: string]: unknown }) => {\n if (writerClosed) return // Guard against writes after close\n try {\n const jsonStr = JSON.stringify(event)\n await writer.write(encoder.encode(`data: ${jsonStr}\\n\\n`))\n } catch (err) {\n // Writer may have been closed by client disconnect\n console.warn('[AI Chat] Failed to write SSE event:', event.type)\n }\n }\n\n const closeWriter = async () => {\n if (writerClosed) return\n writerClosed = true\n try {\n await writer.close()\n } catch {\n // Already closed\n }\n }\n\n // Handle question answer - simple JSON response, not SSE\n // The original SSE stream continues and will receive the follow-up response\n if (answerQuestion) {\n try {\n const client = createOpenCodeClient()\n await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer)\n return NextResponse.json({ success: true })\n } catch (error) {\n console.error('[AI Chat] Answer error:', error)\n return NextResponse.json(\n { error: error instanceof Error ? error.message : 'Failed to answer question' },\n { status: 500 }\n )\n }\n }\n\n // Handle regular message\n if (!messages || !Array.isArray(messages)) {\n return NextResponse.json({ error: 'messages array is required' }, { status: 400 })\n }\n\n // Get the latest user message\n const lastUserMessage = messages.filter((m) => m.role === 'user').pop()?.content\n if (!lastUserMessage) {\n return NextResponse.json({ error: 'No user message found' }, { status: 400 })\n }\n\n const chatStartTime = Date.now()\n const messagePreview = lastUserMessage.slice(0, 80).replace(/\\n/g, ' ')\n console.error(`[AI Usage] Chat request: user=${auth.sub} session=${sessionId ? sessionId.slice(0, 16) + '...' : 'new'} message=\"${messagePreview}${lastUserMessage.length > 80 ? '...' : ''}\"`)\n\n\n // For new sessions, create an ephemeral API key that inherits user permissions\n // The API key secret is encrypted and stored; MCP server recovers it via session token\n let sessionToken: string | null = null\n if (!sessionId) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n // Get user's role IDs from database\n const userRoleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n // Generate session token and create ephemeral key\n sessionToken = generateSessionToken()\n await createSessionApiKey(em, {\n sessionToken,\n userId: auth.sub,\n userRoles: userRoleIds,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n ttlMinutes: 120,\n })\n console.log('[AI Chat] Created session token:', sessionToken.slice(0, 12) + '...')\n } catch (error) {\n console.error('[AI Chat] Failed to create session key:', error)\n // Continue without session key - tools will use static API key auth\n }\n }\n\n // Build the message to send to OpenCode\n // For NEW sessions: inject system instructions + session token\n // For existing sessions: only inject session token if available\n let messageToSend = ''\n\n // For NEW sessions only, prepend system instructions\n if (!sessionId) {\n messageToSend = `${CHAT_SYSTEM_INSTRUCTIONS}\\n\\n`\n }\n\n // If we have a session token, prepend explicit instructions for the AI to include it in tool calls\n if (sessionToken) {\n messageToSend += `[Session Authorization: ${sessionToken}. Include \"_sessionToken\": \"${sessionToken}\" in EVERY tool call.]\\n\\n`\n }\n\n messageToSend += lastUserMessage\n\n // Process in background - starts AFTER Response is returned so there's a reader for the stream\n ;(async () => {\n let toolCallCount = 0\n let lastTokens: { input?: number; output?: number } | undefined\n let resultSessionId: string | undefined\n\n try {\n // Emit session-authorized event first (if we have a token)\n if (sessionToken) {\n console.log('[AI Chat] Emitting session-authorized event')\n await writeSSE({\n type: 'session-authorized',\n sessionToken: sessionToken.slice(0, 12) + '...',\n })\n }\n\n // Emit thinking event for UX feedback\n await writeSSE({ type: 'thinking' })\n\n // Use streaming handler that supports questions\n await handleOpenCodeMessageStreaming(\n {\n message: messageToSend,\n sessionId,\n },\n async (event) => {\n // Track usage from stream events\n if (event.type === 'tool-call') toolCallCount++\n if (event.type === 'metadata' && 'tokens' in event) lastTokens = event.tokens\n if (event.type === 'done' && 'sessionId' in event) resultSessionId = event.sessionId\n\n await writeSSE(event)\n }\n )\n } catch (error) {\n console.error('[AI Chat] OpenCode error:', error)\n await writeSSE({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n } finally {\n const durationMs = Date.now() - chatStartTime\n console.error(`[AI Usage] Chat complete: user=${auth.sub} session=${(resultSessionId || sessionId || 'unknown').slice(0, 16)}... duration=${durationMs}ms toolCalls=${toolCallCount}${lastTokens ? ` tokens={in:${lastTokens.input || 0},out:${lastTokens.output || 0}}` : ''}`)\n await closeWriter()\n }\n })()\n\n return new Response(stream.readable, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n })\n } catch (error) {\n console.error('[AI Chat] Error:', error)\n return NextResponse.json({ error: 'Chat request failed' }, { status: 500 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n handleOpenCodeMessageStreaming,\n type OpenCodeStreamEvent,\n} from '../../lib/opencode-handlers'\nimport { createOpenCodeClient } from '../../lib/opencode-client'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n generateSessionToken,\n createSessionApiKey,\n} from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\n/**\n * System instructions injected at the start of new chat sessions.\n * These ensure the AI follows the correct workflow for data operations.\n */\nconst CHAT_SYSTEM_INSTRUCTIONS = `\nYou are a helpful business assistant for Open Mercato.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nABSOLUTE RULES \u2014 FOLLOW THESE OR BE CUT OFF\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n1. READ = GET only. If the user says find/list/show/search/get \u2192 use only GET. NEVER call PUT/POST/DELETE for a read query.\n2. PUT path = collection path. id goes in the BODY, not the URL. Example: PUT /api/customers/companies with { id: '...', name: 'New' }. There are NO /{id} path segments.\n3. Confirm before ANY write. Before POST/PUT/DELETE: present your plan in business language, then STOP and wait for user to say \"yes\". Do NOT execute the write in the same turn.\n4. Maximum 4 tool calls per message. Hard limit is 10.\n\nYou have 2 tools \u2014 both accept a \"code\" parameter with an async JavaScript arrow function.\n\nTOOL: search \u2014 discover endpoints and schemas (READ-ONLY, fast)\n - spec.findEndpoints(keyword) \u2192 [{ path, methods }]\n - spec.describeEndpoint(path, method) \u2192 COMPACT: { requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }\n - spec.describeEntity(keyword) \u2192 { className, fields, relationships }\n\nTOOL: execute \u2014 make API calls (reads and writes)\n - api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }\n\nCOMMON API PATHS (use directly \u2014 do NOT call findEndpoints for these):\n /api/customers/companies \u2014 companies (GET list, POST create, PUT update)\n /api/customers/people \u2014 contacts/people\n /api/customers/deals \u2014 deals/opportunities\n /api/customers/activities \u2014 activities/tasks\n /api/sales/orders \u2014 sales orders\n /api/sales/quotes \u2014 quotes\n /api/sales/invoices \u2014 invoices\n /api/catalog/products \u2014 products\n /api/catalog/categories \u2014 categories\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nRECIPES \u2014 follow EXACTLY for each task type\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nFIND/LIST records (1 call):\n For COMMON PATHS: skip describeEndpoint, go straight to execute.\n 1. execute: api.request({ method: 'GET', path: '/api/<module>/<resource>' })\n The \"search\" query param only matches indexed text fields \u2014 it will NOT match concepts like \"Polish\" or \"large\".\n For conceptual/subjective queries, fetch ALL records and use YOUR reasoning to identify matches from the returned data.\n For unknown paths: 1 search + 1 execute.\n\nUPDATE a record (3-4 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'PUT') \u2192 learn requestBody fields AND relatedEntity\n 2. execute: GET the record \u2192 find it, get its ID\n 3. execute: PUT to the COLLECTION path with id IN THE BODY:\n api.request({ method: 'PUT', path: '/api/<module>/<resource>', body: { id: '<uuid>', ...changes } })\n NOTE: All CRUD endpoints use the COLLECTION path. The id goes in the request BODY, not the URL. There are NO /{id} path segments.\n\nCREATE a record (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 gives requiredFields, optionalFields, nestedCollections, and a working example\n 2. Ask user for confirmation with the field values\n 3. execute: api.request({ method: 'POST', ...body })\n If the endpoint has nestedCollections (like lines), include them INLINE in the body \u2014 do NOT create them separately.\n Use the \"example\" from describeEndpoint as your template \u2014 fill in real values.\n Example \u2014 create a quote with line items:\n api.request({ method: 'POST', path: '/api/sales/quotes', body: {\n currencyCode: 'EUR', customerEntityId: '<company-uuid>',\n lines: [{ currencyCode: 'EUR', quantity: 1, productId: '<product-uuid>', name: 'Product Name', kind: 'product' }]\n }})\n Do NOT create lines separately. Do NOT include id, quoteId, or total fields \u2014 the server generates them.\n\nCREATE MULTIPLE records (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 learn fields + example\n 2. execute: loop in one call:\n async () => {\n const results = [];\n for (const item of items) {\n results.push(await api.request({ method: 'POST', path: '...', body: item }));\n }\n return results;\n }\n\nDISCOVER (what endpoints/entities exist) (1 call):\n 1. search: spec.findEndpoints('<keyword>') or spec.describeEntity('<keyword>')\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nHARD RULES\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n- MAXIMUM 4 tool calls per user message. You WILL be cut off after 10.\n- NEVER call findEndpoints or describeEndpoint for COMMON PATHS listed above \u2014 use them directly with execute.\n- NEVER call describeEntity if describeEndpoint already returned relatedEntity.\n- NEVER repeat a search from earlier in the conversation \u2014 reuse previous results.\n- NEVER make N+1 API calls (1 call per record). Fetch a list and reason about the results yourself.\n- When you already have the data you need from a previous call, use it \u2014 do NOT fetch more data to \"enrich\" it.\n- Do NOT write JavaScript filters/regex to match records. Fetch data with a simple api.request() call and use YOUR knowledge to interpret the results.\n- The \"search\" query param is fulltext only \u2014 it won't match nationalities, categories, or subjective criteria. For those, fetch all and reason.\n- describeEndpoint returns a COMPACT summary with requiredFields, optionalFields, and an example. Use the example as your template \u2014 fill in real values and send it.\n- For fields you don't know, OMIT them \u2014 the API uses defaults for optional fields.\n- NEVER try to set computed/total fields (amounts, totals, counts) \u2014 the server calculates them.\n- For updates: describeEndpoint gives you the field names. Go straight to GET + PUT. PUT path is the COLLECTION path, id in BODY.\n- For creates with children (e.g. quote + lines): include children INLINE in the body using the nestedCollections field name.\n\nRESPONSE RULES:\n- Be proactive \u2014 fetch data and present results, don't ask what the user wants to see.\n- Never show technical terms, IDs, JSON, or internal reasoning.\n- Present results in clean business language with **bold names** and bullet points.\n- Only ask for confirmation before create/update/delete operations.\n`.trim()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI chat',\n methods: {\n POST: { summary: 'Send message to AI agent via SSE stream' },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * Get user's role IDs from the database.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null\n): Promise<string[]> {\n if (!tenantId) return []\n\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((l) => (l.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\n/**\n * Chat endpoint that routes messages to OpenCode agent.\n * OpenCode connects to MCP server for tool access (search, execute, context_whoami).\n *\n * Emits verbose SSE events for debugging:\n * - thinking: Agent started processing\n * - metadata: Model, tokens, timing info\n * - tool-call: Tool invocation with args\n * - tool-result: Tool response\n * - text: Response text\n * - question: Confirmation question from agent\n * - done: Complete with session ID\n * - error: Error occurred\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { messages, sessionId, answerQuestion } = body as {\n messages?: Array<{ role: string; content: string }>\n sessionId?: string\n // For answering a question\n answerQuestion?: {\n questionId: string\n answer: number\n sessionId: string\n }\n }\n\n // Create SSE stream for frontend compatibility\n const encoder = new TextEncoder()\n const stream = new TransformStream()\n const writer = stream.writable.getWriter()\n let writerClosed = false\n\n const writeSSE = async (event: OpenCodeStreamEvent | { type: string; [key: string]: unknown }) => {\n if (writerClosed) return // Guard against writes after close\n try {\n const jsonStr = JSON.stringify(event)\n await writer.write(encoder.encode(`data: ${jsonStr}\\n\\n`))\n } catch (err) {\n // Writer may have been closed by client disconnect\n console.warn('[AI Chat] Failed to write SSE event:', event.type)\n }\n }\n\n const closeWriter = async () => {\n if (writerClosed) return\n writerClosed = true\n try {\n await writer.close()\n } catch {\n // Already closed\n }\n }\n\n // Handle question answer - simple JSON response, not SSE\n // The original SSE stream continues and will receive the follow-up response\n if (answerQuestion) {\n try {\n const client = createOpenCodeClient()\n await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer)\n return NextResponse.json({ success: true })\n } catch (error) {\n console.error('[AI Chat] Answer error:', error)\n return NextResponse.json(\n { error: error instanceof Error ? error.message : 'Failed to answer question' },\n { status: 500 }\n )\n }\n }\n\n // Handle regular message\n if (!messages || !Array.isArray(messages)) {\n return NextResponse.json({ error: 'messages array is required' }, { status: 400 })\n }\n\n // Get the latest user message\n const lastUserMessage = messages.filter((m) => m.role === 'user').pop()?.content\n if (!lastUserMessage) {\n return NextResponse.json({ error: 'No user message found' }, { status: 400 })\n }\n\n const chatStartTime = Date.now()\n const messagePreview = lastUserMessage.slice(0, 80).replace(/\\n/g, ' ')\n console.error(`[AI Usage] Chat request: user=${auth.sub} session=${sessionId ? sessionId.slice(0, 16) + '...' : 'new'} message=\"${messagePreview}${lastUserMessage.length > 80 ? '...' : ''}\"`)\n\n\n // For new sessions, create an ephemeral API key that inherits user permissions\n // The API key secret is encrypted and stored; MCP server recovers it via session token\n let sessionToken: string | null = null\n if (!sessionId) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n // Get user's role IDs from database\n const userRoleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n // Generate session token and create ephemeral key\n sessionToken = generateSessionToken()\n await createSessionApiKey(em, {\n sessionToken,\n userId: auth.sub,\n userRoles: userRoleIds,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n ttlMinutes: 120,\n })\n console.log('[AI Chat] Created session token:', sessionToken.slice(0, 12) + '...')\n } catch (error) {\n console.error('[AI Chat] Failed to create session key:', error)\n // Continue without session key - tools will use static API key auth\n }\n }\n\n // Build the message to send to OpenCode\n // For NEW sessions: inject system instructions + session token\n // For existing sessions: only inject session token if available\n let messageToSend = ''\n\n // For NEW sessions only, prepend system instructions\n if (!sessionId) {\n messageToSend = `${CHAT_SYSTEM_INSTRUCTIONS}\\n\\n`\n }\n\n // If we have a session token, prepend explicit instructions for the AI to include it in tool calls\n if (sessionToken) {\n messageToSend += `[Session Authorization: ${sessionToken}. Include \"_sessionToken\": \"${sessionToken}\" in EVERY tool call.]\\n\\n`\n }\n\n messageToSend += lastUserMessage\n\n // Process in background - starts AFTER Response is returned so there's a reader for the stream\n ;(async () => {\n let toolCallCount = 0\n let lastTokens: { input?: number; output?: number } | undefined\n let resultSessionId: string | undefined\n\n try {\n // Emit session-authorized event first (if we have a token)\n if (sessionToken) {\n console.log('[AI Chat] Emitting session-authorized event')\n await writeSSE({\n type: 'session-authorized',\n sessionToken: sessionToken.slice(0, 12) + '...',\n })\n }\n\n // Emit thinking event for UX feedback\n await writeSSE({ type: 'thinking' })\n\n // Use streaming handler that supports questions\n await handleOpenCodeMessageStreaming(\n {\n message: messageToSend,\n sessionId,\n },\n async (event) => {\n // Track usage from stream events\n if (event.type === 'tool-call') toolCallCount++\n if (event.type === 'metadata' && 'tokens' in event) lastTokens = event.tokens\n if (event.type === 'done' && 'sessionId' in event) resultSessionId = event.sessionId\n\n await writeSSE(event)\n }\n )\n } catch (error) {\n console.error('[AI Chat] OpenCode error:', error)\n await writeSSE({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n } finally {\n const durationMs = Date.now() - chatStartTime\n console.error(`[AI Usage] Chat complete: user=${auth.sub} session=${(resultSessionId || sessionId || 'unknown').slice(0, 16)}... duration=${durationMs}ms toolCalls=${toolCallCount}${lastTokens ? ` tokens={in:${lastTokens.input || 0},out:${lastTokens.output || 0}}` : ''}`)\n await closeWriter()\n }\n })()\n\n return new Response(stream.readable, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n })\n } catch (error) {\n console.error('[AI Chat] Error:', error)\n return NextResponse.json({ error: 'Chat request failed' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
3
|
import { handleOpenCodeHealth } from "../../lib/opencode-handlers.js";
|
|
4
|
+
const openApi = {
|
|
5
|
+
tag: "AI Assistant",
|
|
6
|
+
summary: "AI assistant health check",
|
|
7
|
+
methods: {
|
|
8
|
+
GET: { summary: "Check OpenCode and MCP connection status" }
|
|
9
|
+
}
|
|
10
|
+
};
|
|
4
11
|
const metadata = {
|
|
5
12
|
GET: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
6
13
|
};
|
|
@@ -22,6 +29,7 @@ async function GET(req) {
|
|
|
22
29
|
}
|
|
23
30
|
export {
|
|
24
31
|
GET,
|
|
25
|
-
metadata
|
|
32
|
+
metadata,
|
|
33
|
+
openApi
|
|
26
34
|
};
|
|
27
35
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/health/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { handleOpenCodeHealth } from '../../lib/opencode-handlers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/health\n *\n * Returns OpenCode and MCP connection status.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await handleOpenCodeHealth()\n return NextResponse.json(health)\n } catch (error) {\n console.error('[AI Health] Error:', error)\n return NextResponse.json(\n { error: 'Failed to check health', message: error instanceof Error ? error.message : 'Unknown error' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { handleOpenCodeHealth } from '../../lib/opencode-handlers'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant health check',\n methods: {\n GET: { summary: 'Check OpenCode and MCP connection status' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/health\n *\n * Returns OpenCode and MCP connection status.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await handleOpenCodeHealth()\n return NextResponse.json(health)\n } catch (error) {\n console.error('[AI Health] Error:', error)\n return NextResponse.json(\n { error: 'Failed to check health', message: error instanceof Error ? error.message : 'Unknown error' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AAE9B,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,2CAA2C;AAAA,EAC7D;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAOA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,qBAAqB;AAC1C,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,OAAO;AACd,YAAQ,MAAM,sBAAsB,KAAK;AACzC,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,0BAA0B,SAAS,iBAAiB,QAAQ,MAAM,UAAU,gBAAgB;AAAA,MACrG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -17,6 +17,13 @@ import {
|
|
|
17
17
|
resolveChatConfig,
|
|
18
18
|
isProviderConfigured
|
|
19
19
|
} from "../../lib/chat-config.js";
|
|
20
|
+
const openApi = {
|
|
21
|
+
tag: "AI Assistant",
|
|
22
|
+
summary: "AI query routing",
|
|
23
|
+
methods: {
|
|
24
|
+
POST: { summary: "Route user query to appropriate AI handler" }
|
|
25
|
+
}
|
|
26
|
+
};
|
|
20
27
|
const metadata = {
|
|
21
28
|
POST: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
22
29
|
};
|
|
@@ -126,6 +133,7 @@ Respond with:
|
|
|
126
133
|
}
|
|
127
134
|
export {
|
|
128
135
|
POST,
|
|
129
|
-
metadata
|
|
136
|
+
metadata,
|
|
137
|
+
openApi
|
|
130
138
|
};
|
|
131
139
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/route/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { generateObject } from '../../lib/ai-sdk'\nimport {\n createOpenAI,\n createAnthropic,\n createGoogleGenerativeAI,\n} from '../../lib/ai-sdk'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n resolveOpenCodeProviderApiKey,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n resolveChatConfig,\n isProviderConfigured,\n type ChatProviderId,\n} from '../../lib/chat-config'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nconst RouteResultSchema = z.object({\n intent: z.enum(['tool', 'general_chat']),\n toolName: z.string().optional(),\n confidence: z.number().min(0).max(1),\n reasoning: z.string(),\n})\n\nfunction createRoutingModel(providerId: ChatProviderId, configuredModel?: string) {\n const { modelId, modelWithProvider } = resolveOpenCodeModel(providerId, {\n overrideModel: configuredModel,\n })\n const apiKey = resolveOpenCodeProviderApiKey(providerId)\n if (!apiKey) {\n throw new Error(`${providerId.toUpperCase()} API key not configured`)\n }\n\n switch (providerId) {\n case 'openai': {\n const openai = createOpenAI({ apiKey })\n return {\n model: openai(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],\n modelWithProvider,\n }\n }\n case 'anthropic': {\n const anthropic = createAnthropic({ apiKey })\n return {\n model: anthropic(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],\n modelWithProvider,\n }\n }\n case 'google': {\n const google = createGoogleGenerativeAI({ apiKey })\n return {\n model: google(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],\n modelWithProvider,\n }\n }\n default:\n throw new Error(`Unknown provider: ${providerId}`)\n }\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { query, availableTools } = body as {\n query: string\n availableTools: Array<{ name: string; description: string }>\n }\n\n console.log('[AI Route] Routing query:', query)\n console.log('[AI Route] Available tools count:', availableTools?.length)\n\n if (!query || typeof query !== 'string') {\n return NextResponse.json({ error: 'query is required' }, { status: 400 })\n }\n\n if (!availableTools || !Array.isArray(availableTools)) {\n return NextResponse.json({ error: 'availableTools array is required' }, { status: 400 })\n }\n\n // Get user's configured provider\n const container = await createRequestContainer()\n let config = await resolveChatConfig(container)\n\n // Fallback to first configured provider\n if (!config) {\n const configuredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (!configuredProvider) {\n return NextResponse.json(\n { error: 'No AI provider configured. Please set an API key for OpenAI, Anthropic, or Google.' },\n { status: 503 }\n )\n }\n config = { providerId: configuredProvider, model: '', updatedAt: '' }\n }\n\n console.log('[AI Route] Using provider:', config.providerId)\n\n // Verify the configured provider is still available\n if (!isProviderConfigured(config.providerId)) {\n return NextResponse.json(\n { error: `Configured provider ${config.providerId} is no longer available. Please update settings.` },\n { status: 503 }\n )\n }\n\n // Use fast model for the configured provider\n const { model, modelWithProvider } = createRoutingModel(config.providerId, config.model)\n\n const toolList = availableTools\n .map((t) => `- ${t.name}: ${t.description}`)\n .join('\\n')\n\n console.log('[AI Route] Calling generateObject with', modelWithProvider)\n\n const result = await generateObject({\n model,\n schema: RouteResultSchema,\n prompt: `You are a routing assistant. Given a user query, determine if they want to use a specific tool or have a general conversation.\n\nAvailable tools:\n${toolList}\n\nUser query: \"${query}\"\n\nRespond with:\n- intent: \"tool\" if user wants to perform an action with a specific tool, \"general_chat\" otherwise\n- toolName: the exact tool name if intent is \"tool\"\n- confidence: 0-1 how confident you are\n- reasoning: brief explanation`,\n })\n\n console.log('[AI Route] Result:', result.object)\n return NextResponse.json(result.object)\n } catch (error) {\n console.error('[AI Route] Error routing query:', error)\n return NextResponse.json(\n { error: 'Routing request failed' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { generateObject } from '../../lib/ai-sdk'\nimport {\n createOpenAI,\n createAnthropic,\n createGoogleGenerativeAI,\n} from '../../lib/ai-sdk'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n resolveOpenCodeProviderApiKey,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n resolveChatConfig,\n isProviderConfigured,\n type ChatProviderId,\n} from '../../lib/chat-config'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI query routing',\n methods: {\n POST: { summary: 'Route user query to appropriate AI handler' },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nconst RouteResultSchema = z.object({\n intent: z.enum(['tool', 'general_chat']),\n toolName: z.string().optional(),\n confidence: z.number().min(0).max(1),\n reasoning: z.string(),\n})\n\nfunction createRoutingModel(providerId: ChatProviderId, configuredModel?: string) {\n const { modelId, modelWithProvider } = resolveOpenCodeModel(providerId, {\n overrideModel: configuredModel,\n })\n const apiKey = resolveOpenCodeProviderApiKey(providerId)\n if (!apiKey) {\n throw new Error(`${providerId.toUpperCase()} API key not configured`)\n }\n\n switch (providerId) {\n case 'openai': {\n const openai = createOpenAI({ apiKey })\n return {\n model: openai(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],\n modelWithProvider,\n }\n }\n case 'anthropic': {\n const anthropic = createAnthropic({ apiKey })\n return {\n model: anthropic(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],\n modelWithProvider,\n }\n }\n case 'google': {\n const google = createGoogleGenerativeAI({ apiKey })\n return {\n model: google(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],\n modelWithProvider,\n }\n }\n default:\n throw new Error(`Unknown provider: ${providerId}`)\n }\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { query, availableTools } = body as {\n query: string\n availableTools: Array<{ name: string; description: string }>\n }\n\n console.log('[AI Route] Routing query:', query)\n console.log('[AI Route] Available tools count:', availableTools?.length)\n\n if (!query || typeof query !== 'string') {\n return NextResponse.json({ error: 'query is required' }, { status: 400 })\n }\n\n if (!availableTools || !Array.isArray(availableTools)) {\n return NextResponse.json({ error: 'availableTools array is required' }, { status: 400 })\n }\n\n // Get user's configured provider\n const container = await createRequestContainer()\n let config = await resolveChatConfig(container)\n\n // Fallback to first configured provider\n if (!config) {\n const configuredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (!configuredProvider) {\n return NextResponse.json(\n { error: 'No AI provider configured. Please set an API key for OpenAI, Anthropic, or Google.' },\n { status: 503 }\n )\n }\n config = { providerId: configuredProvider, model: '', updatedAt: '' }\n }\n\n console.log('[AI Route] Using provider:', config.providerId)\n\n // Verify the configured provider is still available\n if (!isProviderConfigured(config.providerId)) {\n return NextResponse.json(\n { error: `Configured provider ${config.providerId} is no longer available. Please update settings.` },\n { status: 503 }\n )\n }\n\n // Use fast model for the configured provider\n const { model, modelWithProvider } = createRoutingModel(config.providerId, config.model)\n\n const toolList = availableTools\n .map((t) => `- ${t.name}: ${t.description}`)\n .join('\\n')\n\n console.log('[AI Route] Calling generateObject with', modelWithProvider)\n\n const result = await generateObject({\n model,\n schema: RouteResultSchema,\n prompt: `You are a routing assistant. Given a user query, determine if they want to use a specific tool or have a general conversation.\n\nAvailable tools:\n${toolList}\n\nUser query: \"${query}\"\n\nRespond with:\n- intent: \"tool\" if user wants to perform an action with a specific tool, \"general_chat\" otherwise\n- toolName: the exact tool name if intent is \"tool\"\n- confidence: 0-1 how confident you are\n- reasoning: brief explanation`,\n })\n\n console.log('[AI Route] Result:', result.object)\n return NextResponse.json(result.object)\n } catch (error) {\n console.error('[AI Route] Error routing query:', error)\n return NextResponse.json(\n { error: 'Routing request failed' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
resolveOpenCodeModel,
|
|
9
9
|
resolveOpenCodeProviderId
|
|
10
10
|
} from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
11
|
+
const openApi = {
|
|
12
|
+
tag: "AI Assistant",
|
|
13
|
+
summary: "AI assistant settings",
|
|
14
|
+
methods: {
|
|
15
|
+
GET: { summary: "Get AI provider configuration" }
|
|
16
|
+
}
|
|
17
|
+
};
|
|
11
18
|
const metadata = {
|
|
12
19
|
GET: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
13
20
|
};
|
|
@@ -51,6 +58,7 @@ async function GET(req) {
|
|
|
51
58
|
}
|
|
52
59
|
export {
|
|
53
60
|
GET,
|
|
54
|
-
metadata
|
|
61
|
+
metadata,
|
|
62
|
+
openApi
|
|
55
63
|
};
|
|
56
64
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/settings/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n resolveOpenCodeModel,\n resolveOpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n // Read provider config from environment\n const providerId = resolveOpenCodeProviderId(process.env.OPENCODE_PROVIDER)\n const providerInfo = OPEN_CODE_PROVIDERS[providerId]\n\n // Check if the provider's API key is configured (supports multiple fallback keys)\n const apiKeyConfigured = isOpenCodeProviderConfigured(providerId)\n\n // Get model (custom or default)\n const resolvedModel = resolveOpenCodeModel(providerId)\n\n // Show the env key that's configured, or the first one as instruction\n const displayEnvKey = getOpenCodeProviderConfiguredEnvKey(providerId)\n\n // Check if MCP_SERVER_API_KEY is configured (required for MCP authentication)\n const mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerInfo.name,\n model: resolvedModel.modelWithProvider,\n defaultModel: providerInfo.defaultModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders: OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n }\n }),\n mcpKeyConfigured,\n })\n } catch (error) {\n console.error('[AI Settings] GET error:', error)\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n resolveOpenCodeModel,\n resolveOpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant settings',\n methods: {\n GET: { summary: 'Get AI provider configuration' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n // Read provider config from environment\n const providerId = resolveOpenCodeProviderId(process.env.OPENCODE_PROVIDER)\n const providerInfo = OPEN_CODE_PROVIDERS[providerId]\n\n // Check if the provider's API key is configured (supports multiple fallback keys)\n const apiKeyConfigured = isOpenCodeProviderConfigured(providerId)\n\n // Get model (custom or default)\n const resolvedModel = resolveOpenCodeModel(providerId)\n\n // Show the env key that's configured, or the first one as instruction\n const displayEnvKey = getOpenCodeProviderConfiguredEnvKey(providerId)\n\n // Check if MCP_SERVER_API_KEY is configured (required for MCP authentication)\n const mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerInfo.name,\n model: resolvedModel.modelWithProvider,\n defaultModel: providerInfo.defaultModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders: OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n }\n }),\n mcpKeyConfigured,\n })\n } catch (error) {\n console.error('[AI Settings] GET error:', error)\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,gCAAgC;AAAA,EAClD;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAOA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AAEF,UAAM,aAAa,0BAA0B,QAAQ,IAAI,iBAAiB;AAC1E,UAAM,eAAe,oBAAoB,UAAU;AAGnD,UAAM,mBAAmB,6BAA6B,UAAU;AAGhE,UAAM,gBAAgB,qBAAqB,UAAU;AAGrD,UAAM,gBAAgB,oCAAoC,UAAU;AAGpE,UAAM,mBAAmB,CAAC,CAAC,QAAQ,IAAI,oBAAoB,KAAK;AAEhE,WAAO,aAAa,KAAK;AAAA,MACvB,UAAU;AAAA,QACR,IAAI;AAAA,QACJ,MAAM,aAAa;AAAA,QACnB,OAAO,cAAc;AAAA,QACrB,cAAc,aAAa;AAAA,QAC3B,QAAQ;AAAA,QACR,YAAY;AAAA,MACd;AAAA,MACA,oBAAoB,uBAAuB,IAAI,CAAC,OAAO;AACrD,cAAM,OAAO,oBAAoB,EAAE;AACnC,eAAO;AAAA,UACL;AAAA,UACA,MAAM,KAAK;AAAA,UACX,cAAc,KAAK;AAAA,UACnB,QAAQ,oCAAoC,EAAE;AAAA,UAC9C,YAAY,6BAA6B,EAAE;AAAA,QAC7C;AAAA,MACF,CAAC;AAAA,MACD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,4BAA4B,KAAK;AAC/C,WAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,13 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
4
|
import { executeTool } from "../../../lib/tool-executor.js";
|
|
5
5
|
import { loadAllModuleTools } from "../../../lib/tool-loader.js";
|
|
6
|
+
const openApi = {
|
|
7
|
+
tag: "AI Assistant",
|
|
8
|
+
summary: "Execute AI tool",
|
|
9
|
+
methods: {
|
|
10
|
+
POST: { summary: "Execute a specific MCP tool by name" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
6
13
|
const metadata = {
|
|
7
14
|
POST: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
8
15
|
};
|
|
@@ -53,6 +60,7 @@ async function POST(req) {
|
|
|
53
60
|
}
|
|
54
61
|
export {
|
|
55
62
|
POST,
|
|
56
|
-
metadata
|
|
63
|
+
metadata,
|
|
64
|
+
openApi
|
|
57
65
|
};
|
|
58
66
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ai_assistant/api/tools/execute/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { executeTool } from '../../../lib/tool-executor'\nimport { loadAllModuleTools } from '../../../lib/tool-loader'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { McpToolContext } from '../../../lib/types'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { toolName, args = {} } = body\n\n if (!toolName || typeof toolName !== 'string') {\n return NextResponse.json({ error: 'toolName is required' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n\n // Load ACL for user\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // Ensure tools are loaded\n await loadAllModuleTools()\n\n // Build tool context\n const toolContext: McpToolContext = {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n }\n\n // Execute the tool\n const result = await executeTool(toolName, args, toolContext)\n\n if (!result.success) {\n return NextResponse.json(\n { success: false, error: result.error },\n { status: result.errorCode === 'UNAUTHORIZED' ? 403 : 400 }\n )\n }\n\n return NextResponse.json({\n success: true,\n result: result.result,\n })\n } catch (error) {\n console.error('[AI Tools] Error executing tool:', error)\n return NextResponse.json(\n { success: false, error: 'Tool execution failed' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { executeTool } from '../../../lib/tool-executor'\nimport { loadAllModuleTools } from '../../../lib/tool-loader'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { McpToolContext } from '../../../lib/types'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Execute AI tool',\n methods: {\n POST: { summary: 'Execute a specific MCP tool by name' },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { toolName, args = {} } = body\n\n if (!toolName || typeof toolName !== 'string') {\n return NextResponse.json({ error: 'toolName is required' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n\n // Load ACL for user\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // Ensure tools are loaded\n await loadAllModuleTools()\n\n // Build tool context\n const toolContext: McpToolContext = {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n }\n\n // Execute the tool\n const result = await executeTool(toolName, args, toolContext)\n\n if (!result.success) {\n return NextResponse.json(\n { success: false, error: result.error },\n { status: result.errorCode === 'UNAUTHORIZED' ? 403 : 400 }\n )\n }\n\n return NextResponse.json({\n success: true,\n result: result.result,\n })\n } catch (error) {\n console.error('[AI Tools] Error executing tool:', error)\n return NextResponse.json(\n { success: false, error: 'Tool execution failed' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AAI5B,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM,EAAE,SAAS,sCAAsC;AAAA,EACzD;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACpE;AAEA,eAAsB,KAAK,KAAkB;AAC3C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AAEzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,EAAE,UAAU,OAAO,CAAC,EAAE,IAAI;AAEhC,QAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC7C,aAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7E;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAGhE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AAGD,UAAM,mBAAmB;AAGzB,UAAM,cAA8B;AAAA,MAClC,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb;AAAA,MACA,cAAc,IAAI;AAAA,MAClB,cAAc,IAAI;AAAA,IACpB;AAGA,UAAM,SAAS,MAAM,YAAY,UAAU,MAAM,WAAW;AAE5D,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa;AAAA,QAClB,EAAE,SAAS,OAAO,OAAO,OAAO,MAAM;AAAA,QACtC,EAAE,QAAQ,OAAO,cAAc,iBAAiB,MAAM,IAAI;AAAA,MAC5D;AAAA,IACF;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,SAAS;AAAA,MACT,QAAQ,OAAO;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,oCAAoC,KAAK;AACvD,WAAO,aAAa;AAAA,MAClB,EAAE,SAAS,OAAO,OAAO,wBAAwB;AAAA,MACjD,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,13 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
5
5
|
import { getToolRegistry } from "../../lib/tool-registry.js";
|
|
6
6
|
import { loadAllModuleTools } from "../../lib/tool-loader.js";
|
|
7
7
|
import { hasRequiredFeatures } from "../../lib/auth.js";
|
|
8
|
+
const openApi = {
|
|
9
|
+
tag: "AI Assistant",
|
|
10
|
+
summary: "List AI tools",
|
|
11
|
+
methods: {
|
|
12
|
+
GET: { summary: "List available MCP tools filtered by user permissions" }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
8
15
|
const metadata = {
|
|
9
16
|
GET: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
10
17
|
};
|
|
@@ -43,6 +50,7 @@ async function GET(req) {
|
|
|
43
50
|
}
|
|
44
51
|
export {
|
|
45
52
|
GET,
|
|
46
|
-
metadata
|
|
53
|
+
metadata,
|
|
54
|
+
openApi
|
|
47
55
|
};
|
|
48
56
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/tools/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getToolRegistry } from '../../lib/tool-registry'\nimport { loadAllModuleTools } from '../../lib/tool-loader'\nimport { hasRequiredFeatures } from '../../lib/auth'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n\n // Load ACL for user\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // Ensure tools are loaded\n await loadAllModuleTools()\n\n // Get tools filtered by ACL\n const registry = getToolRegistry()\n const allTools = Array.from(registry.getTools().values())\n\n const accessibleTools = allTools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService)\n )\n\n const tools = accessibleTools.map((tool) => {\n const nameParts = tool.name.split('.')\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,\n module: nameParts[0] || 'other',\n }\n })\n\n return NextResponse.json({ tools })\n } catch (error) {\n console.error('[AI Tools] Error listing tools:', error)\n return NextResponse.json({ error: 'Failed to list tools' }, { status: 500 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getToolRegistry } from '../../lib/tool-registry'\nimport { loadAllModuleTools } from '../../lib/tool-loader'\nimport { hasRequiredFeatures } from '../../lib/auth'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'List AI tools',\n methods: {\n GET: { summary: 'List available MCP tools filtered by user permissions' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n\n // Load ACL for user\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // Ensure tools are loaded\n await loadAllModuleTools()\n\n // Get tools filtered by ACL\n const registry = getToolRegistry()\n const allTools = Array.from(registry.getTools().values())\n\n const accessibleTools = allTools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService)\n )\n\n const tools = accessibleTools.map((tool) => {\n const nameParts = tool.name.split('.')\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,\n module: nameParts[0] || 'other',\n }\n })\n\n return NextResponse.json({ tools })\n } catch (error) {\n console.error('[AI Tools] Error listing tools:', error)\n return NextResponse.json({ error: 'Failed to list tools' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAG7B,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,wDAAwD;AAAA,EAC1E;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAEA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AAEzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAGhE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AAGD,UAAM,mBAAmB;AAGzB,UAAM,WAAW,gBAAgB;AACjC,UAAM,WAAW,MAAM,KAAK,SAAS,SAAS,EAAE,OAAO,CAAC;AAExD,UAAM,kBAAkB,SAAS;AAAA,MAAO,CAAC,SACvC,oBAAoB,KAAK,kBAAkB,IAAI,UAAU,IAAI,cAAc,WAAW;AAAA,IACxF;AAEA,UAAM,QAAQ,gBAAgB,IAAI,CAAC,SAAS;AAC1C,YAAM,YAAY,KAAK,KAAK,MAAM,GAAG;AACrC,aAAO;AAAA,QACL,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA,QAClB,aAAa,gBAAgB,KAAK,WAAkB;AAAA,QACpD,QAAQ,UAAU,CAAC,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAED,WAAO,aAAa,KAAK,EAAE,MAAM,CAAC;AAAA,EACpC,SAAS,OAAO;AACd,YAAQ,MAAM,mCAAmC,KAAK;AACtD,WAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7E;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
1
2
|
class OpenCodeClient {
|
|
2
3
|
constructor(config) {
|
|
3
4
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
@@ -65,7 +66,9 @@ class OpenCodeClient {
|
|
|
65
66
|
if (!res.ok) {
|
|
66
67
|
throw new Error(`Health check failed: ${res.status}`);
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
const data = await readJsonSafe(res, null);
|
|
70
|
+
if (!data) throw new Error("Health check returned invalid JSON response");
|
|
71
|
+
return data;
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
71
74
|
* Get MCP server connection status.
|
|
@@ -77,7 +80,9 @@ class OpenCodeClient {
|
|
|
77
80
|
if (!res.ok) {
|
|
78
81
|
throw new Error(`MCP status check failed: ${res.status}`);
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
const data = await readJsonSafe(res, null);
|
|
84
|
+
if (!data) throw new Error("MCP status check returned invalid JSON response");
|
|
85
|
+
return data;
|
|
81
86
|
}
|
|
82
87
|
/**
|
|
83
88
|
* Create a new conversation session.
|
|
@@ -92,7 +97,9 @@ class OpenCodeClient {
|
|
|
92
97
|
const error = await res.text();
|
|
93
98
|
throw new Error(`Failed to create session: ${error}`);
|
|
94
99
|
}
|
|
95
|
-
|
|
100
|
+
const data = await readJsonSafe(res, null);
|
|
101
|
+
if (!data) throw new Error("Create session returned invalid JSON response");
|
|
102
|
+
return data;
|
|
96
103
|
}
|
|
97
104
|
/**
|
|
98
105
|
* Get an existing session by ID.
|
|
@@ -104,7 +111,9 @@ class OpenCodeClient {
|
|
|
104
111
|
if (!res.ok) {
|
|
105
112
|
throw new Error(`Failed to get session: ${res.status}`);
|
|
106
113
|
}
|
|
107
|
-
|
|
114
|
+
const data = await readJsonSafe(res, null);
|
|
115
|
+
if (!data) throw new Error("Get session returned invalid JSON response");
|
|
116
|
+
return data;
|
|
108
117
|
}
|
|
109
118
|
/**
|
|
110
119
|
* Send a message to a session and wait for response.
|
|
@@ -125,7 +134,9 @@ class OpenCodeClient {
|
|
|
125
134
|
const error = await res.text();
|
|
126
135
|
throw new Error(`Failed to send message: ${error}`);
|
|
127
136
|
}
|
|
128
|
-
|
|
137
|
+
const data = await readJsonSafe(res, null);
|
|
138
|
+
if (!data) throw new Error("Send message returned invalid JSON response");
|
|
139
|
+
return data;
|
|
129
140
|
}
|
|
130
141
|
/**
|
|
131
142
|
* Set authentication credentials for a provider.
|
|
@@ -150,7 +161,9 @@ class OpenCodeClient {
|
|
|
150
161
|
if (!res.ok) {
|
|
151
162
|
throw new Error(`Failed to get config: ${res.status}`);
|
|
152
163
|
}
|
|
153
|
-
|
|
164
|
+
const data = await readJsonSafe(res, null);
|
|
165
|
+
if (!data) throw new Error("Get config returned invalid JSON response");
|
|
166
|
+
return data;
|
|
154
167
|
}
|
|
155
168
|
/**
|
|
156
169
|
* Get pending questions that need user response.
|
|
@@ -162,7 +175,9 @@ class OpenCodeClient {
|
|
|
162
175
|
if (!res.ok) {
|
|
163
176
|
throw new Error(`Failed to get questions: ${res.status}`);
|
|
164
177
|
}
|
|
165
|
-
|
|
178
|
+
const data = await readJsonSafe(res, null);
|
|
179
|
+
if (!data) throw new Error("Get questions returned invalid JSON response");
|
|
180
|
+
return data;
|
|
166
181
|
}
|
|
167
182
|
/**
|
|
168
183
|
* Answer a pending question.
|
|
@@ -221,7 +236,8 @@ class OpenCodeClient {
|
|
|
221
236
|
if (res.ok) {
|
|
222
237
|
const contentType = res.headers.get("content-type");
|
|
223
238
|
if (contentType && contentType.includes("application/json")) {
|
|
224
|
-
|
|
239
|
+
const data = await readJsonSafe(res, null);
|
|
240
|
+
if (data) return data;
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/opencode-client.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * OpenCode Agent Client\n *\n * Client for communicating with OpenCode server running in headless mode.\n * OpenCode is used as an AI agent that can execute MCP tools.\n */\n\nexport type OpenCodeClientConfig = {\n baseUrl: string\n password?: string\n}\n\nexport type OpenCodeSession = {\n id: string\n slug: string\n version: string\n projectID: string\n directory: string\n title: string\n time: {\n created: number\n updated: number\n }\n}\n\nexport type OpenCodeMessagePart = {\n type: 'text'\n text: string\n}\n\nexport type OpenCodeMessageInfo = {\n id: string\n sessionID: string\n role: 'user' | 'assistant'\n time: {\n created: number\n completed?: number\n }\n modelID?: string\n providerID?: string\n tokens?: {\n input: number\n output: number\n }\n error?: {\n name: string\n data: Record<string, unknown>\n }\n}\n\nexport type OpenCodeMessage = {\n info: OpenCodeMessageInfo\n parts: Array<{\n id: string\n type: string\n text?: string\n [key: string]: unknown\n }>\n}\n\nexport type OpenCodeHealth = {\n healthy: boolean\n version: string\n}\n\nexport type OpenCodeMcpStatus = Record<\n string,\n {\n status: 'connected' | 'failed' | 'connecting'\n error?: string\n }\n>\n\nexport type OpenCodeQuestionOption = {\n label: string\n description: string\n}\n\nexport type OpenCodeQuestion = {\n id: string\n sessionID: string\n questions: Array<{\n question: string\n header: string\n options: OpenCodeQuestionOption[]\n }>\n tool: {\n messageID: string\n callID: string\n }\n}\n\n/**\n * SSE Event from OpenCode event stream.\n */\nexport type OpenCodeSSEEvent = {\n type: string\n properties: Record<string, unknown>\n}\n\n/**\n * Callback for SSE events.\n */\nexport type OpenCodeSSECallback = (event: OpenCodeSSEEvent) => void\n\n/**\n * Client for OpenCode server API.\n */\nexport class OpenCodeClient {\n private baseUrl: string\n private headers: Record<string, string>\n\n constructor(config: OpenCodeClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '')\n this.headers = {\n 'Content-Type': 'application/json',\n }\n\n if (config.password) {\n const credentials = Buffer.from(`opencode:${config.password}`).toString('base64')\n this.headers['Authorization'] = `Basic ${credentials}`\n }\n }\n\n /**\n * Subscribe to SSE event stream for real-time updates.\n * Returns an abort function to stop the stream.\n */\n subscribeToEvents(\n onEvent: OpenCodeSSECallback,\n onError?: (error: Error) => void\n ): () => void {\n const controller = new AbortController()\n\n const connect = async () => {\n try {\n const res = await fetch(`${this.baseUrl}/event`, {\n headers: {\n ...this.headers,\n Accept: 'text/event-stream',\n },\n signal: controller.signal,\n })\n\n if (!res.ok || !res.body) {\n throw new Error(`SSE connection failed: ${res.status}`)\n }\n\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n\n // Process complete SSE messages\n const lines = buffer.split('\\n')\n buffer = lines.pop() || '' // Keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6))\n onEvent(data)\n } catch {\n // Ignore parse errors\n }\n }\n }\n }\n } catch (error) {\n if ((error as Error).name !== 'AbortError') {\n onError?.(error as Error)\n }\n }\n }\n\n connect()\n\n return () => controller.abort()\n }\n\n /**\n * Check OpenCode server health.\n */\n async health(): Promise<OpenCodeHealth> {\n const res = await fetch(`${this.baseUrl}/global/health`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Health check failed: ${res.status}`)\n }\n\n return res.json()\n }\n\n /**\n * Get MCP server connection status.\n */\n async mcpStatus(): Promise<OpenCodeMcpStatus> {\n const res = await fetch(`${this.baseUrl}/mcp`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`MCP status check failed: ${res.status}`)\n }\n\n return res.json()\n }\n\n /**\n * Create a new conversation session.\n */\n async createSession(): Promise<OpenCodeSession> {\n const res = await fetch(`${this.baseUrl}/session`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({}),\n })\n\n if (!res.ok) {\n const error = await res.text()\n throw new Error(`Failed to create session: ${error}`)\n }\n\n return res.json()\n }\n\n /**\n * Get an existing session by ID.\n */\n async getSession(sessionId: string): Promise<OpenCodeSession> {\n const res = await fetch(`${this.baseUrl}/session/${sessionId}`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Failed to get session: ${res.status}`)\n }\n\n return res.json()\n }\n\n /**\n * Send a message to a session and wait for response.\n */\n async sendMessage(\n sessionId: string,\n message: string,\n options?: {\n model?: { providerID: string; modelID: string }\n }\n ): Promise<OpenCodeMessage> {\n const body: Record<string, unknown> = {\n parts: [{ type: 'text', text: message }],\n }\n\n if (options?.model) {\n body.model = options.model\n }\n\n const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n const error = await res.text()\n throw new Error(`Failed to send message: ${error}`)\n }\n\n return res.json()\n }\n\n /**\n * Set authentication credentials for a provider.\n */\n async setAuth(providerId: string, apiKey: string): Promise<void> {\n const res = await fetch(`${this.baseUrl}/auth/${providerId}`, {\n method: 'PUT',\n headers: this.headers,\n body: JSON.stringify({ type: 'api', key: apiKey }),\n })\n\n if (!res.ok) {\n throw new Error(`Failed to set auth: ${res.status}`)\n }\n }\n\n /**\n * Get current configuration.\n */\n async getConfig(): Promise<Record<string, unknown>> {\n const res = await fetch(`${this.baseUrl}/config`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Failed to get config: ${res.status}`)\n }\n\n return res.json()\n }\n\n /**\n * Get pending questions that need user response.\n */\n async getPendingQuestions(): Promise<OpenCodeQuestion[]> {\n const res = await fetch(`${this.baseUrl}/question`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Failed to get questions: ${res.status}`)\n }\n\n return res.json()\n }\n\n /**\n * Answer a pending question.\n * OpenCode expects: POST /question/{requestID}/reply with { answers: [[\"label\"]] }\n * Each answer is an array of selected option labels (for multi-select support).\n */\n async answerQuestion(questionId: string, answerIndex: number): Promise<void> {\n // First get the question to find the selected option label\n const questions = await this.getPendingQuestions()\n const question = questions.find((q) => q.id === questionId)\n\n if (!question) {\n throw new Error(`Question ${questionId} not found`)\n }\n\n // Build answers array - each question's answer is an array of selected labels\n const answers: string[][] = []\n for (const q of question.questions) {\n const selectedOption = q.options[answerIndex]\n if (selectedOption) {\n // Each answer is an array of selected labels (supports multi-select)\n answers.push([selectedOption.label])\n }\n }\n\n const body = { answers }\n\n console.log('[OpenCode Client] Answering question', questionId, 'with body:', JSON.stringify(body))\n\n const res = await fetch(`${this.baseUrl}/question/${questionId}/reply`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(body),\n })\n\n const responseText = await res.text()\n console.log('[OpenCode Client] Answer response:', res.status, responseText.substring(0, 200))\n\n if (!res.ok) {\n throw new Error(`Failed to answer question: ${res.status} - ${responseText}`)\n }\n }\n\n /**\n * Reject a pending question.\n */\n async rejectQuestion(questionId: string): Promise<void> {\n console.log('[OpenCode Client] Rejecting question', questionId)\n\n const res = await fetch(`${this.baseUrl}/question/${questionId}/reject`, {\n method: 'POST',\n headers: this.headers,\n })\n\n if (!res.ok) {\n const responseText = await res.text()\n throw new Error(`Failed to reject question: ${res.status} - ${responseText}`)\n }\n }\n\n /**\n * Get session status (idle, busy, waiting for question).\n * Falls back to inferring status from pending questions if endpoint doesn't exist.\n */\n async getSessionStatus(sessionId: string): Promise<{ status: string; questionId?: string }> {\n try {\n const res = await fetch(`${this.baseUrl}/session/${sessionId}/status`, {\n headers: this.headers,\n })\n\n if (res.ok) {\n const contentType = res.headers.get('content-type')\n if (contentType && contentType.includes('application/json')) {\n return res.json()\n }\n }\n } catch {\n // Endpoint doesn't exist or network error - fall through to inference\n }\n\n // Fall back to inferring status from pending questions\n // Note: We can't tell if OpenCode is busy without the status endpoint\n // Return 'unknown' to let SSE events determine actual state\n const questions = await this.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === sessionId)\n if (sessionQuestion) {\n return { status: 'waiting', questionId: sessionQuestion.id }\n }\n // Don't assume idle - we can't know without SSE events\n return { status: 'unknown' }\n }\n}\n\n/**\n * Create an OpenCode client with default configuration from environment.\n */\nexport function createOpenCodeClient(config?: Partial<OpenCodeClientConfig>): OpenCodeClient {\n return new OpenCodeClient({\n baseUrl: config?.baseUrl ?? process.env.OPENCODE_URL ?? 'http://localhost:4096',\n password: config?.password ?? process.env.OPENCODE_PASSWORD,\n })\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * OpenCode Agent Client\n *\n * Client for communicating with OpenCode server running in headless mode.\n * OpenCode is used as an AI agent that can execute MCP tools.\n */\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\n\nexport type OpenCodeClientConfig = {\n baseUrl: string\n password?: string\n}\n\nexport type OpenCodeSession = {\n id: string\n slug: string\n version: string\n projectID: string\n directory: string\n title: string\n time: {\n created: number\n updated: number\n }\n}\n\nexport type OpenCodeMessagePart = {\n type: 'text'\n text: string\n}\n\nexport type OpenCodeMessageInfo = {\n id: string\n sessionID: string\n role: 'user' | 'assistant'\n time: {\n created: number\n completed?: number\n }\n modelID?: string\n providerID?: string\n tokens?: {\n input: number\n output: number\n }\n error?: {\n name: string\n data: Record<string, unknown>\n }\n}\n\nexport type OpenCodeMessage = {\n info: OpenCodeMessageInfo\n parts: Array<{\n id: string\n type: string\n text?: string\n [key: string]: unknown\n }>\n}\n\nexport type OpenCodeHealth = {\n healthy: boolean\n version: string\n}\n\nexport type OpenCodeMcpStatus = Record<\n string,\n {\n status: 'connected' | 'failed' | 'connecting'\n error?: string\n }\n>\n\nexport type OpenCodeQuestionOption = {\n label: string\n description: string\n}\n\nexport type OpenCodeQuestion = {\n id: string\n sessionID: string\n questions: Array<{\n question: string\n header: string\n options: OpenCodeQuestionOption[]\n }>\n tool: {\n messageID: string\n callID: string\n }\n}\n\n/**\n * SSE Event from OpenCode event stream.\n */\nexport type OpenCodeSSEEvent = {\n type: string\n properties: Record<string, unknown>\n}\n\n/**\n * Callback for SSE events.\n */\nexport type OpenCodeSSECallback = (event: OpenCodeSSEEvent) => void\n\n/**\n * Client for OpenCode server API.\n */\nexport class OpenCodeClient {\n private baseUrl: string\n private headers: Record<string, string>\n\n constructor(config: OpenCodeClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '')\n this.headers = {\n 'Content-Type': 'application/json',\n }\n\n if (config.password) {\n const credentials = Buffer.from(`opencode:${config.password}`).toString('base64')\n this.headers['Authorization'] = `Basic ${credentials}`\n }\n }\n\n /**\n * Subscribe to SSE event stream for real-time updates.\n * Returns an abort function to stop the stream.\n */\n subscribeToEvents(\n onEvent: OpenCodeSSECallback,\n onError?: (error: Error) => void\n ): () => void {\n const controller = new AbortController()\n\n const connect = async () => {\n try {\n const res = await fetch(`${this.baseUrl}/event`, {\n headers: {\n ...this.headers,\n Accept: 'text/event-stream',\n },\n signal: controller.signal,\n })\n\n if (!res.ok || !res.body) {\n throw new Error(`SSE connection failed: ${res.status}`)\n }\n\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n\n // Process complete SSE messages\n const lines = buffer.split('\\n')\n buffer = lines.pop() || '' // Keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6))\n onEvent(data)\n } catch {\n // Ignore parse errors\n }\n }\n }\n }\n } catch (error) {\n if ((error as Error).name !== 'AbortError') {\n onError?.(error as Error)\n }\n }\n }\n\n connect()\n\n return () => controller.abort()\n }\n\n /**\n * Check OpenCode server health.\n */\n async health(): Promise<OpenCodeHealth> {\n const res = await fetch(`${this.baseUrl}/global/health`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Health check failed: ${res.status}`)\n }\n\n const data = await readJsonSafe<OpenCodeHealth>(res, null)\n if (!data) throw new Error('Health check returned invalid JSON response')\n return data\n }\n\n /**\n * Get MCP server connection status.\n */\n async mcpStatus(): Promise<OpenCodeMcpStatus> {\n const res = await fetch(`${this.baseUrl}/mcp`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`MCP status check failed: ${res.status}`)\n }\n\n const data = await readJsonSafe<OpenCodeMcpStatus>(res, null)\n if (!data) throw new Error('MCP status check returned invalid JSON response')\n return data\n }\n\n /**\n * Create a new conversation session.\n */\n async createSession(): Promise<OpenCodeSession> {\n const res = await fetch(`${this.baseUrl}/session`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({}),\n })\n\n if (!res.ok) {\n const error = await res.text()\n throw new Error(`Failed to create session: ${error}`)\n }\n\n const data = await readJsonSafe<OpenCodeSession>(res, null)\n if (!data) throw new Error('Create session returned invalid JSON response')\n return data\n }\n\n /**\n * Get an existing session by ID.\n */\n async getSession(sessionId: string): Promise<OpenCodeSession> {\n const res = await fetch(`${this.baseUrl}/session/${sessionId}`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Failed to get session: ${res.status}`)\n }\n\n const data = await readJsonSafe<OpenCodeSession>(res, null)\n if (!data) throw new Error('Get session returned invalid JSON response')\n return data\n }\n\n /**\n * Send a message to a session and wait for response.\n */\n async sendMessage(\n sessionId: string,\n message: string,\n options?: {\n model?: { providerID: string; modelID: string }\n }\n ): Promise<OpenCodeMessage> {\n const body: Record<string, unknown> = {\n parts: [{ type: 'text', text: message }],\n }\n\n if (options?.model) {\n body.model = options.model\n }\n\n const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n const error = await res.text()\n throw new Error(`Failed to send message: ${error}`)\n }\n\n const data = await readJsonSafe<OpenCodeMessage>(res, null)\n if (!data) throw new Error('Send message returned invalid JSON response')\n return data\n }\n\n /**\n * Set authentication credentials for a provider.\n */\n async setAuth(providerId: string, apiKey: string): Promise<void> {\n const res = await fetch(`${this.baseUrl}/auth/${providerId}`, {\n method: 'PUT',\n headers: this.headers,\n body: JSON.stringify({ type: 'api', key: apiKey }),\n })\n\n if (!res.ok) {\n throw new Error(`Failed to set auth: ${res.status}`)\n }\n }\n\n /**\n * Get current configuration.\n */\n async getConfig(): Promise<Record<string, unknown>> {\n const res = await fetch(`${this.baseUrl}/config`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Failed to get config: ${res.status}`)\n }\n\n const data = await readJsonSafe<Record<string, unknown>>(res, null)\n if (!data) throw new Error('Get config returned invalid JSON response')\n return data\n }\n\n /**\n * Get pending questions that need user response.\n */\n async getPendingQuestions(): Promise<OpenCodeQuestion[]> {\n const res = await fetch(`${this.baseUrl}/question`, {\n headers: this.headers,\n })\n\n if (!res.ok) {\n throw new Error(`Failed to get questions: ${res.status}`)\n }\n\n const data = await readJsonSafe<OpenCodeQuestion[]>(res, null)\n if (!data) throw new Error('Get questions returned invalid JSON response')\n return data\n }\n\n /**\n * Answer a pending question.\n * OpenCode expects: POST /question/{requestID}/reply with { answers: [[\"label\"]] }\n * Each answer is an array of selected option labels (for multi-select support).\n */\n async answerQuestion(questionId: string, answerIndex: number): Promise<void> {\n // First get the question to find the selected option label\n const questions = await this.getPendingQuestions()\n const question = questions.find((q) => q.id === questionId)\n\n if (!question) {\n throw new Error(`Question ${questionId} not found`)\n }\n\n // Build answers array - each question's answer is an array of selected labels\n const answers: string[][] = []\n for (const q of question.questions) {\n const selectedOption = q.options[answerIndex]\n if (selectedOption) {\n // Each answer is an array of selected labels (supports multi-select)\n answers.push([selectedOption.label])\n }\n }\n\n const body = { answers }\n\n console.log('[OpenCode Client] Answering question', questionId, 'with body:', JSON.stringify(body))\n\n const res = await fetch(`${this.baseUrl}/question/${questionId}/reply`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(body),\n })\n\n const responseText = await res.text()\n console.log('[OpenCode Client] Answer response:', res.status, responseText.substring(0, 200))\n\n if (!res.ok) {\n throw new Error(`Failed to answer question: ${res.status} - ${responseText}`)\n }\n }\n\n /**\n * Reject a pending question.\n */\n async rejectQuestion(questionId: string): Promise<void> {\n console.log('[OpenCode Client] Rejecting question', questionId)\n\n const res = await fetch(`${this.baseUrl}/question/${questionId}/reject`, {\n method: 'POST',\n headers: this.headers,\n })\n\n if (!res.ok) {\n const responseText = await res.text()\n throw new Error(`Failed to reject question: ${res.status} - ${responseText}`)\n }\n }\n\n /**\n * Get session status (idle, busy, waiting for question).\n * Falls back to inferring status from pending questions if endpoint doesn't exist.\n */\n async getSessionStatus(sessionId: string): Promise<{ status: string; questionId?: string }> {\n try {\n const res = await fetch(`${this.baseUrl}/session/${sessionId}/status`, {\n headers: this.headers,\n })\n\n if (res.ok) {\n const contentType = res.headers.get('content-type')\n if (contentType && contentType.includes('application/json')) {\n const data = await readJsonSafe<{ status: string; questionId?: string }>(res, null)\n if (data) return data\n }\n }\n } catch {\n // Endpoint doesn't exist or network error - fall through to inference\n }\n\n // Fall back to inferring status from pending questions\n // Note: We can't tell if OpenCode is busy without the status endpoint\n // Return 'unknown' to let SSE events determine actual state\n const questions = await this.getPendingQuestions()\n const sessionQuestion = questions.find((q) => q.sessionID === sessionId)\n if (sessionQuestion) {\n return { status: 'waiting', questionId: sessionQuestion.id }\n }\n // Don't assume idle - we can't know without SSE events\n return { status: 'unknown' }\n }\n}\n\n/**\n * Create an OpenCode client with default configuration from environment.\n */\nexport function createOpenCodeClient(config?: Partial<OpenCodeClientConfig>): OpenCodeClient {\n return new OpenCodeClient({\n baseUrl: config?.baseUrl ?? process.env.OPENCODE_URL ?? 'http://localhost:4096',\n password: config?.password ?? process.env.OPENCODE_PASSWORD,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.4.11-develop.
|
|
3
|
+
"version": "0.4.11-develop.1922.46b23ba3b5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -94,12 +94,12 @@
|
|
|
94
94
|
"zod-to-json-schema": "^3.25.1"
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
|
-
"@open-mercato/shared": "0.4.11-develop.
|
|
98
|
-
"@open-mercato/ui": "0.4.11-develop.
|
|
97
|
+
"@open-mercato/shared": "0.4.11-develop.1922.46b23ba3b5",
|
|
98
|
+
"@open-mercato/ui": "0.4.11-develop.1922.46b23ba3b5",
|
|
99
99
|
"zod": ">=3.23.0"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
|
-
"@open-mercato/cli": "0.4.11-develop.
|
|
102
|
+
"@open-mercato/cli": "0.4.11-develop.1922.46b23ba3b5",
|
|
103
103
|
"tsx": "^4.21.0"
|
|
104
104
|
},
|
|
105
105
|
"publishConfig": {
|
|
@@ -204,6 +204,11 @@ export function DockableChat() {
|
|
|
204
204
|
const handleChatKeyDown = (e: React.KeyboardEvent) => {
|
|
205
205
|
if (e.key === 'Escape') {
|
|
206
206
|
e.stopPropagation()
|
|
207
|
+
if (phase !== 'idle') {
|
|
208
|
+
reset()
|
|
209
|
+
} else {
|
|
210
|
+
close()
|
|
211
|
+
}
|
|
207
212
|
}
|
|
208
213
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
209
214
|
e.preventDefault()
|