@open-mercato/ai-assistant 0.4.2-canary-c02407ff85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/AGENTS.md +1090 -0
  2. package/README.md +607 -0
  3. package/build.mjs +92 -0
  4. package/dist/di.js +8 -0
  5. package/dist/di.js.map +7 -0
  6. package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
  7. package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
  8. package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
  9. package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
  10. package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
  11. package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
  12. package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
  13. package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
  14. package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
  15. package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
  16. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
  17. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
  18. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
  19. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
  20. package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
  21. package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
  22. package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
  23. package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
  24. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
  25. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
  26. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
  27. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
  28. package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
  29. package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
  30. package/dist/frontend/components/CommandPalette/index.js +28 -0
  31. package/dist/frontend/components/CommandPalette/index.js.map +7 -0
  32. package/dist/frontend/constants.js +41 -0
  33. package/dist/frontend/constants.js.map +7 -0
  34. package/dist/frontend/hooks/index.js +13 -0
  35. package/dist/frontend/hooks/index.js.map +7 -0
  36. package/dist/frontend/hooks/useCommandPalette.js +1094 -0
  37. package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
  38. package/dist/frontend/hooks/useMcpTools.js +66 -0
  39. package/dist/frontend/hooks/useMcpTools.js.map +7 -0
  40. package/dist/frontend/hooks/usePageContext.js +48 -0
  41. package/dist/frontend/hooks/usePageContext.js.map +7 -0
  42. package/dist/frontend/hooks/useRecentActions.js +56 -0
  43. package/dist/frontend/hooks/useRecentActions.js.map +7 -0
  44. package/dist/frontend/hooks/useRecentTools.js +55 -0
  45. package/dist/frontend/hooks/useRecentTools.js.map +7 -0
  46. package/dist/frontend/index.js +35 -0
  47. package/dist/frontend/index.js.map +7 -0
  48. package/dist/frontend/types.js +1 -0
  49. package/dist/frontend/types.js.map +7 -0
  50. package/dist/frontend/utils/index.js +7 -0
  51. package/dist/frontend/utils/index.js.map +7 -0
  52. package/dist/frontend/utils/toolMatcher.js +95 -0
  53. package/dist/frontend/utils/toolMatcher.js.map +7 -0
  54. package/dist/index.js +57 -0
  55. package/dist/index.js.map +7 -0
  56. package/dist/modules/ai_assistant/acl.js +14 -0
  57. package/dist/modules/ai_assistant/acl.js.map +7 -0
  58. package/dist/modules/ai_assistant/api/chat/route.js +152 -0
  59. package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
  60. package/dist/modules/ai_assistant/api/health/route.js +27 -0
  61. package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
  62. package/dist/modules/ai_assistant/api/route/route.js +123 -0
  63. package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
  64. package/dist/modules/ai_assistant/api/settings/route.js +60 -0
  65. package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
  66. package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
  67. package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
  68. package/dist/modules/ai_assistant/api/tools/route.js +48 -0
  69. package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
  70. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
  71. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
  72. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
  73. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
  74. package/dist/modules/ai_assistant/cli.js +192 -0
  75. package/dist/modules/ai_assistant/cli.js.map +7 -0
  76. package/dist/modules/ai_assistant/di.js +11 -0
  77. package/dist/modules/ai_assistant/di.js.map +7 -0
  78. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
  79. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
  80. package/dist/modules/ai_assistant/index.js +13 -0
  81. package/dist/modules/ai_assistant/index.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
  83. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
  85. package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
  87. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
  89. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/auth.js +87 -0
  91. package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
  93. package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
  95. package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/http-server.js +367 -0
  97. package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
  99. package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
  101. package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
  103. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
  105. package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
  107. package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
  109. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
  111. package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
  113. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
  115. package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
  117. package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
  119. package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
  121. package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
  123. package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
  125. package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/types.js +1 -0
  127. package/dist/modules/ai_assistant/lib/types.js.map +7 -0
  128. package/package.json +108 -0
  129. package/src/di.ts +11 -0
  130. package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
  131. package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
  132. package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
  133. package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
  134. package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
  135. package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
  136. package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
  137. package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
  138. package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
  139. package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
  140. package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
  141. package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
  142. package/src/frontend/components/CommandPalette/index.ts +14 -0
  143. package/src/frontend/constants.ts +35 -0
  144. package/src/frontend/hooks/index.ts +5 -0
  145. package/src/frontend/hooks/useCommandPalette.ts +1389 -0
  146. package/src/frontend/hooks/useMcpTools.ts +73 -0
  147. package/src/frontend/hooks/usePageContext.ts +61 -0
  148. package/src/frontend/hooks/useRecentActions.ts +64 -0
  149. package/src/frontend/hooks/useRecentTools.ts +69 -0
  150. package/src/frontend/index.ts +39 -0
  151. package/src/frontend/types.ts +260 -0
  152. package/src/frontend/utils/index.ts +1 -0
  153. package/src/frontend/utils/toolMatcher.ts +127 -0
  154. package/src/index.ts +92 -0
  155. package/src/modules/ai_assistant/acl.ts +10 -0
  156. package/src/modules/ai_assistant/api/chat/route.ts +213 -0
  157. package/src/modules/ai_assistant/api/health/route.ts +30 -0
  158. package/src/modules/ai_assistant/api/route/route.ts +149 -0
  159. package/src/modules/ai_assistant/api/settings/route.ts +73 -0
  160. package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
  161. package/src/modules/ai_assistant/api/tools/route.ts +57 -0
  162. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
  163. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
  164. package/src/modules/ai_assistant/cli.ts +233 -0
  165. package/src/modules/ai_assistant/di.ts +9 -0
  166. package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
  167. package/src/modules/ai_assistant/index.ts +11 -0
  168. package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
  169. package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
  170. package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
  171. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
  172. package/src/modules/ai_assistant/lib/auth.ts +185 -0
  173. package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
  174. package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
  175. package/src/modules/ai_assistant/lib/http-server.ts +498 -0
  176. package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
  177. package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
  178. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
  179. package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
  180. package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
  181. package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
  182. package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
  183. package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
  184. package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
  185. package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
  186. package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
  187. package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
  188. package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
  189. package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
  190. package/src/modules/ai_assistant/lib/types.ts +147 -0
  191. package/test-schema.ts +37 -0
  192. package/tsconfig.json +10 -0
  193. package/watch.mjs +6 -0
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @open-mercato/ai-assistant
3
+ *
4
+ * MCP (Model Context Protocol) server module for AI assistant integration.
5
+ *
6
+ * This module provides:
7
+ * - MCP server with stdio transport for Claude Desktop integration
8
+ * - Tool registry for modules to register AI-callable tools
9
+ * - ACL-based permission filtering for tools
10
+ * - Multi-tenant execution context
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { registerMcpTool } from '@open-mercato/ai-assistant/tools'
15
+ * import { z } from 'zod'
16
+ *
17
+ * registerMcpTool({
18
+ * name: 'customers.search',
19
+ * description: 'Search for customers',
20
+ * inputSchema: z.object({ query: z.string() }),
21
+ * requiredFeatures: ['customers.people.view'],
22
+ * handler: async (input, ctx) => {
23
+ * // Implementation
24
+ * }
25
+ * }, { moduleId: 'customers' })
26
+ * ```
27
+ */
28
+
29
+ // Re-export types
30
+ export * from './modules/ai_assistant/lib/types'
31
+
32
+ // Tool registry
33
+ export {
34
+ registerMcpTool,
35
+ getToolRegistry,
36
+ unregisterMcpTool,
37
+ toolRegistry,
38
+ } from './modules/ai_assistant/lib/tool-registry'
39
+
40
+ // Tool executor
41
+ export { executeTool } from './modules/ai_assistant/lib/tool-executor'
42
+
43
+ // MCP server (stdio)
44
+ export { createMcpServer, runMcpServer } from './modules/ai_assistant/lib/mcp-server'
45
+
46
+ // MCP HTTP server
47
+ export { runMcpHttpServer, type McpHttpServerOptions } from './modules/ai_assistant/lib/http-server'
48
+
49
+ // MCP auth
50
+ export {
51
+ authenticateMcpRequest,
52
+ extractApiKeyFromHeaders,
53
+ type McpAuthResult,
54
+ type McpAuthSuccess,
55
+ type McpAuthFailure,
56
+ } from './modules/ai_assistant/lib/auth'
57
+
58
+ // Tool loader
59
+ export { loadAllModuleTools, indexToolsForSearch } from './modules/ai_assistant/lib/tool-loader'
60
+
61
+ // OpenCode client
62
+ export {
63
+ OpenCodeClient,
64
+ createOpenCodeClient,
65
+ type OpenCodeClientConfig,
66
+ type OpenCodeSession,
67
+ type OpenCodeMessage,
68
+ type OpenCodeHealth,
69
+ type OpenCodeMcpStatus,
70
+ } from './modules/ai_assistant/lib/opencode-client'
71
+
72
+ // OpenCode route handlers
73
+ export {
74
+ handleOpenCodeMessage,
75
+ handleOpenCodeHealth,
76
+ handleOpenCodeMessageStreaming,
77
+ handleOpenCodeAnswer,
78
+ getPendingQuestions,
79
+ extractTextFromResponse,
80
+ extractAllPartsFromResponse,
81
+ extractMetadataFromResponse,
82
+ type OpenCodeTestRequest,
83
+ type OpenCodeTestResponse,
84
+ type OpenCodeHealthResponse,
85
+ type OpenCodeResponsePart,
86
+ type OpenCodeResponseMetadata,
87
+ type OpenCodeStreamEvent,
88
+ type OpenCodeQuestion,
89
+ } from './modules/ai_assistant/lib/opencode-handlers'
90
+
91
+ // Module metadata
92
+ export { metadata, features } from './modules/ai_assistant'
@@ -0,0 +1,10 @@
1
+ export const features = [
2
+ { id: 'ai_assistant.view', title: 'View AI Assistant Settings', module: 'ai_assistant' },
3
+ { id: 'ai_assistant.settings.manage', title: 'Manage AI Assistant Settings', module: 'ai_assistant' },
4
+ { id: 'ai_assistant.mcp.serve', title: 'Start MCP Server', module: 'ai_assistant' },
5
+ { id: 'ai_assistant.tools.list', title: 'List MCP Tools', module: 'ai_assistant' },
6
+ { id: 'ai_assistant.mcp_servers.view', title: 'View MCP Server Configurations', module: 'ai_assistant' },
7
+ { id: 'ai_assistant.mcp_servers.manage', title: 'Manage MCP Server Configurations', module: 'ai_assistant' },
8
+ ]
9
+
10
+ export default features
@@ -0,0 +1,213 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import {
4
+ handleOpenCodeMessageStreaming,
5
+ type OpenCodeStreamEvent,
6
+ } from '../../lib/opencode-handlers'
7
+ import { createOpenCodeClient } from '../../lib/opencode-client'
8
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
9
+ import type { EntityManager } from '@mikro-orm/postgresql'
10
+ import {
11
+ generateSessionToken,
12
+ createSessionApiKey,
13
+ } from '@open-mercato/core/modules/api_keys/services/apiKeyService'
14
+ import { UserRole } from '@open-mercato/core/modules/auth/data/entities'
15
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
16
+
17
+ export const metadata = {
18
+ POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
19
+ }
20
+
21
+ /**
22
+ * Get user's role IDs from the database.
23
+ */
24
+ async function getUserRoleIds(
25
+ em: EntityManager,
26
+ userId: string,
27
+ tenantId: string | null
28
+ ): Promise<string[]> {
29
+ if (!tenantId) return []
30
+
31
+ const links = await findWithDecryption(
32
+ em,
33
+ UserRole,
34
+ { user: userId as any, role: { tenantId } } as any,
35
+ { populate: ['role'] },
36
+ { tenantId, organizationId: null },
37
+ )
38
+ const linkList = Array.isArray(links) ? links : []
39
+ return linkList
40
+ .map((l) => (l.role as any)?.id)
41
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
42
+ }
43
+
44
+ /**
45
+ * Chat endpoint that routes messages to OpenCode agent.
46
+ * OpenCode connects to MCP server for tool access (api_discover, api_execute, api_schema).
47
+ *
48
+ * Emits verbose SSE events for debugging:
49
+ * - thinking: Agent started processing
50
+ * - metadata: Model, tokens, timing info
51
+ * - tool-call: Tool invocation with args
52
+ * - tool-result: Tool response
53
+ * - text: Response text
54
+ * - question: Confirmation question from agent
55
+ * - done: Complete with session ID
56
+ * - error: Error occurred
57
+ */
58
+ export async function POST(req: NextRequest) {
59
+ const auth = await getAuthFromRequest(req)
60
+
61
+ if (!auth) {
62
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
63
+ }
64
+
65
+ try {
66
+ const body = await req.json()
67
+ const { messages, sessionId, answerQuestion } = body as {
68
+ messages?: Array<{ role: string; content: string }>
69
+ sessionId?: string
70
+ // For answering a question
71
+ answerQuestion?: {
72
+ questionId: string
73
+ answer: number
74
+ sessionId: string
75
+ }
76
+ }
77
+
78
+ // Create SSE stream for frontend compatibility
79
+ const encoder = new TextEncoder()
80
+ const stream = new TransformStream()
81
+ const writer = stream.writable.getWriter()
82
+ let writerClosed = false
83
+
84
+ const writeSSE = async (event: OpenCodeStreamEvent | { type: string; [key: string]: unknown }) => {
85
+ if (writerClosed) return // Guard against writes after close
86
+ try {
87
+ const jsonStr = JSON.stringify(event)
88
+ await writer.write(encoder.encode(`data: ${jsonStr}\n\n`))
89
+ } catch (err) {
90
+ // Writer may have been closed by client disconnect
91
+ console.warn('[AI Chat] Failed to write SSE event:', event.type)
92
+ }
93
+ }
94
+
95
+ const closeWriter = async () => {
96
+ if (writerClosed) return
97
+ writerClosed = true
98
+ try {
99
+ await writer.close()
100
+ } catch {
101
+ // Already closed
102
+ }
103
+ }
104
+
105
+ // Handle question answer - simple JSON response, not SSE
106
+ // The original SSE stream continues and will receive the follow-up response
107
+ if (answerQuestion) {
108
+ try {
109
+ const client = createOpenCodeClient()
110
+ await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer)
111
+ return NextResponse.json({ success: true })
112
+ } catch (error) {
113
+ console.error('[AI Chat] Answer error:', error)
114
+ return NextResponse.json(
115
+ { error: error instanceof Error ? error.message : 'Failed to answer question' },
116
+ { status: 500 }
117
+ )
118
+ }
119
+ }
120
+
121
+ // Handle regular message
122
+ if (!messages || !Array.isArray(messages)) {
123
+ return NextResponse.json({ error: 'messages array is required' }, { status: 400 })
124
+ }
125
+
126
+ // Get the latest user message
127
+ const lastUserMessage = messages.filter((m) => m.role === 'user').pop()?.content
128
+ if (!lastUserMessage) {
129
+ return NextResponse.json({ error: 'No user message found' }, { status: 400 })
130
+ }
131
+
132
+ // For new sessions, create an ephemeral API key that inherits user permissions
133
+ let sessionToken: string | null = null
134
+ if (!sessionId) {
135
+ try {
136
+ const container = await createRequestContainer()
137
+ const em = container.resolve<EntityManager>('em')
138
+
139
+ // Get user's role IDs from database
140
+ const userRoleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)
141
+
142
+ // Generate session token and create ephemeral key
143
+ sessionToken = generateSessionToken()
144
+ await createSessionApiKey(em, {
145
+ sessionToken,
146
+ userId: auth.sub,
147
+ userRoles: userRoleIds,
148
+ tenantId: auth.tenantId,
149
+ organizationId: auth.orgId,
150
+ ttlMinutes: 120,
151
+ })
152
+ console.log('[AI Chat] Created session token:', sessionToken.slice(0, 12) + '...')
153
+ } catch (error) {
154
+ console.error('[AI Chat] Failed to create session key:', error)
155
+ // Continue without session key - tools will use static API key auth
156
+ }
157
+ }
158
+
159
+ // Build the message to send to OpenCode
160
+ // If we have a session token, prepend explicit instructions for the AI to include it in tool calls
161
+ let messageToSend = lastUserMessage
162
+ if (sessionToken) {
163
+ messageToSend = `[SYSTEM: Your session token is "${sessionToken}". You MUST include "_sessionToken": "${sessionToken}" in EVERY tool call argument object. Without this, tools will fail with authorization errors.]\n\n${lastUserMessage}`
164
+ }
165
+
166
+ // Process in background - starts AFTER Response is returned so there's a reader for the stream
167
+ ;(async () => {
168
+ try {
169
+ // Emit session-authorized event first (if we have a token)
170
+ if (sessionToken) {
171
+ console.log('[AI Chat] Emitting session-authorized event')
172
+ await writeSSE({
173
+ type: 'session-authorized',
174
+ sessionToken: sessionToken.slice(0, 12) + '...',
175
+ })
176
+ }
177
+
178
+ // Emit thinking event for UX feedback
179
+ await writeSSE({ type: 'thinking' })
180
+
181
+ // Use streaming handler that supports questions
182
+ await handleOpenCodeMessageStreaming(
183
+ {
184
+ message: messageToSend,
185
+ sessionId,
186
+ },
187
+ async (event) => {
188
+ await writeSSE(event)
189
+ }
190
+ )
191
+ } catch (error) {
192
+ console.error('[AI Chat] OpenCode error:', error)
193
+ await writeSSE({
194
+ type: 'error',
195
+ error: error instanceof Error ? error.message : 'OpenCode request failed',
196
+ })
197
+ } finally {
198
+ await closeWriter()
199
+ }
200
+ })()
201
+
202
+ return new Response(stream.readable, {
203
+ headers: {
204
+ 'Content-Type': 'text/event-stream',
205
+ 'Cache-Control': 'no-cache',
206
+ Connection: 'keep-alive',
207
+ },
208
+ })
209
+ } catch (error) {
210
+ console.error('[AI Chat] Error:', error)
211
+ return NextResponse.json({ error: 'Chat request failed' }, { status: 500 })
212
+ }
213
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { handleOpenCodeHealth } from '../../lib/opencode-handlers'
4
+
5
+ export const metadata = {
6
+ GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
7
+ }
8
+
9
+ /**
10
+ * GET /api/ai_assistant/health
11
+ *
12
+ * Returns OpenCode and MCP connection status.
13
+ */
14
+ export async function GET(req: NextRequest) {
15
+ const auth = await getAuthFromRequest(req)
16
+ if (!auth?.sub) {
17
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
18
+ }
19
+
20
+ try {
21
+ const health = await handleOpenCodeHealth()
22
+ return NextResponse.json(health)
23
+ } catch (error) {
24
+ console.error('[AI Health] Error:', error)
25
+ return NextResponse.json(
26
+ { error: 'Failed to check health', message: error instanceof Error ? error.message : 'Unknown error' },
27
+ { status: 500 }
28
+ )
29
+ }
30
+ }
@@ -0,0 +1,149 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { generateObject } from '../../lib/ai-sdk'
3
+ import {
4
+ createOpenAI,
5
+ createAnthropic,
6
+ createGoogleGenerativeAI,
7
+ } from '../../lib/ai-sdk'
8
+ import { z } from 'zod'
9
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
10
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
11
+ import {
12
+ resolveChatConfig,
13
+ isProviderConfigured,
14
+ type ChatProviderId,
15
+ } from '../../lib/chat-config'
16
+
17
+ export const metadata = {
18
+ POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
19
+ }
20
+
21
+ const RouteResultSchema = z.object({
22
+ intent: z.enum(['tool', 'general_chat']),
23
+ toolName: z.string().optional(),
24
+ confidence: z.number().min(0).max(1),
25
+ reasoning: z.string(),
26
+ })
27
+
28
+ // Fast/cheap models for each provider
29
+ const ROUTING_MODELS: Record<ChatProviderId, string> = {
30
+ anthropic: 'claude-3-5-haiku-20241022',
31
+ openai: 'gpt-4o-mini',
32
+ google: 'gemini-1.5-flash',
33
+ }
34
+
35
+ function createRoutingModel(providerId: ChatProviderId) {
36
+ const modelId = ROUTING_MODELS[providerId]
37
+
38
+ switch (providerId) {
39
+ case 'openai': {
40
+ const apiKey = process.env.OPENAI_API_KEY
41
+ if (!apiKey) throw new Error('OPENAI_API_KEY not configured')
42
+ const openai = createOpenAI({ apiKey })
43
+ return openai(modelId) as unknown as Parameters<typeof generateObject>[0]['model']
44
+ }
45
+ case 'anthropic': {
46
+ const apiKey = process.env.ANTHROPIC_API_KEY
47
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY not configured')
48
+ const anthropic = createAnthropic({ apiKey })
49
+ return anthropic(modelId) as unknown as Parameters<typeof generateObject>[0]['model']
50
+ }
51
+ case 'google': {
52
+ const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY
53
+ if (!apiKey) throw new Error('GOOGLE_GENERATIVE_AI_API_KEY not configured')
54
+ const google = createGoogleGenerativeAI({ apiKey })
55
+ return google(modelId) as unknown as Parameters<typeof generateObject>[0]['model']
56
+ }
57
+ default:
58
+ throw new Error(`Unknown provider: ${providerId}`)
59
+ }
60
+ }
61
+
62
+ export async function POST(req: NextRequest) {
63
+ const auth = await getAuthFromRequest(req)
64
+
65
+ if (!auth) {
66
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
67
+ }
68
+
69
+ try {
70
+ const body = await req.json()
71
+ const { query, availableTools } = body as {
72
+ query: string
73
+ availableTools: Array<{ name: string; description: string }>
74
+ }
75
+
76
+ console.log('[AI Route] Routing query:', query)
77
+ console.log('[AI Route] Available tools count:', availableTools?.length)
78
+
79
+ if (!query || typeof query !== 'string') {
80
+ return NextResponse.json({ error: 'query is required' }, { status: 400 })
81
+ }
82
+
83
+ if (!availableTools || !Array.isArray(availableTools)) {
84
+ return NextResponse.json({ error: 'availableTools array is required' }, { status: 400 })
85
+ }
86
+
87
+ // Get user's configured provider
88
+ const container = await createRequestContainer()
89
+ let config = await resolveChatConfig(container)
90
+
91
+ // Fallback to first configured provider
92
+ if (!config) {
93
+ const providers: ChatProviderId[] = ['openai', 'anthropic', 'google']
94
+ const configuredProvider = providers.find((p) => isProviderConfigured(p))
95
+ if (!configuredProvider) {
96
+ return NextResponse.json(
97
+ { error: 'No AI provider configured. Please set an API key for OpenAI, Anthropic, or Google.' },
98
+ { status: 503 }
99
+ )
100
+ }
101
+ config = { providerId: configuredProvider, model: '', updatedAt: '' }
102
+ }
103
+
104
+ console.log('[AI Route] Using provider:', config.providerId)
105
+
106
+ // Verify the configured provider is still available
107
+ if (!isProviderConfigured(config.providerId)) {
108
+ return NextResponse.json(
109
+ { error: `Configured provider ${config.providerId} is no longer available. Please update settings.` },
110
+ { status: 503 }
111
+ )
112
+ }
113
+
114
+ // Use fast model for the configured provider
115
+ const model = createRoutingModel(config.providerId)
116
+
117
+ const toolList = availableTools
118
+ .map((t) => `- ${t.name}: ${t.description}`)
119
+ .join('\n')
120
+
121
+ console.log('[AI Route] Calling generateObject with', ROUTING_MODELS[config.providerId])
122
+
123
+ const result = await generateObject({
124
+ model,
125
+ schema: RouteResultSchema,
126
+ prompt: `You are a routing assistant. Given a user query, determine if they want to use a specific tool or have a general conversation.
127
+
128
+ Available tools:
129
+ ${toolList}
130
+
131
+ User query: "${query}"
132
+
133
+ Respond with:
134
+ - intent: "tool" if user wants to perform an action with a specific tool, "general_chat" otherwise
135
+ - toolName: the exact tool name if intent is "tool"
136
+ - confidence: 0-1 how confident you are
137
+ - reasoning: brief explanation`,
138
+ })
139
+
140
+ console.log('[AI Route] Result:', result.object)
141
+ return NextResponse.json(result.object)
142
+ } catch (error) {
143
+ console.error('[AI Route] Error routing query:', error)
144
+ return NextResponse.json(
145
+ { error: 'Routing request failed' },
146
+ { status: 500 }
147
+ )
148
+ }
149
+ }
@@ -0,0 +1,73 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+
4
+ export const metadata = {
5
+ GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
6
+ }
7
+
8
+ // Provider information
9
+ const PROVIDERS = {
10
+ anthropic: {
11
+ name: 'Anthropic',
12
+ defaultModel: 'claude-haiku-4-5-20251001',
13
+ envKey: 'OPENCODE_ANTHROPIC_API_KEY',
14
+ },
15
+ openai: {
16
+ name: 'OpenAI',
17
+ defaultModel: 'gpt-4o-mini',
18
+ envKey: 'OPENCODE_OPENAI_API_KEY',
19
+ },
20
+ google: {
21
+ name: 'Google',
22
+ defaultModel: 'gemini-2.0-flash',
23
+ envKey: 'OPENCODE_GOOGLE_API_KEY',
24
+ },
25
+ } as const
26
+
27
+ type ProviderId = keyof typeof PROVIDERS
28
+
29
+ /**
30
+ * GET /api/ai_assistant/settings
31
+ *
32
+ * Returns the current OpenCode provider configuration from environment variables.
33
+ */
34
+ export async function GET(req: NextRequest) {
35
+ const auth = await getAuthFromRequest(req)
36
+ if (!auth?.sub) {
37
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
38
+ }
39
+
40
+ try {
41
+ // Read provider config from environment
42
+ const providerId = (process.env.OPENCODE_PROVIDER || 'anthropic') as ProviderId
43
+ const providerInfo = PROVIDERS[providerId] || PROVIDERS.anthropic
44
+
45
+ // Check if the provider's API key is configured
46
+ const apiKeyConfigured = !!process.env[providerInfo.envKey]
47
+
48
+ // Get model (custom or default)
49
+ const customModel = process.env.OPENCODE_MODEL
50
+ const model = customModel || `${providerId}/${providerInfo.defaultModel}`
51
+
52
+ return NextResponse.json({
53
+ provider: {
54
+ id: providerId,
55
+ name: providerInfo.name,
56
+ model,
57
+ defaultModel: providerInfo.defaultModel,
58
+ envKey: providerInfo.envKey,
59
+ configured: apiKeyConfigured,
60
+ },
61
+ availableProviders: Object.entries(PROVIDERS).map(([id, info]) => ({
62
+ id,
63
+ name: info.name,
64
+ defaultModel: info.defaultModel,
65
+ envKey: info.envKey,
66
+ configured: !!process.env[info.envKey],
67
+ })),
68
+ })
69
+ } catch (error) {
70
+ console.error('[AI Settings] GET error:', error)
71
+ return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })
72
+ }
73
+ }
@@ -0,0 +1,71 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { executeTool } from '../../../lib/tool-executor'
5
+ import { loadAllModuleTools } from '../../../lib/tool-loader'
6
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
+ import type { McpToolContext } from '../../../lib/types'
8
+
9
+ export const metadata = {
10
+ POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
11
+ }
12
+
13
+ export async function POST(req: NextRequest) {
14
+ const auth = await getAuthFromRequest(req)
15
+
16
+ if (!auth) {
17
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
18
+ }
19
+
20
+ try {
21
+ const body = await req.json()
22
+ const { toolName, args = {} } = body
23
+
24
+ if (!toolName || typeof toolName !== 'string') {
25
+ return NextResponse.json({ error: 'toolName is required' }, { status: 400 })
26
+ }
27
+
28
+ const container = await createRequestContainer()
29
+ const rbacService = container.resolve<RbacService>('rbacService')
30
+
31
+ // Load ACL for user
32
+ const acl = await rbacService.loadAcl(auth.sub, {
33
+ tenantId: auth.tenantId,
34
+ organizationId: auth.orgId,
35
+ })
36
+
37
+ // Ensure tools are loaded
38
+ await loadAllModuleTools()
39
+
40
+ // Build tool context
41
+ const toolContext: McpToolContext = {
42
+ tenantId: auth.tenantId,
43
+ organizationId: auth.orgId,
44
+ userId: auth.sub,
45
+ container,
46
+ userFeatures: acl.features,
47
+ isSuperAdmin: acl.isSuperAdmin,
48
+ }
49
+
50
+ // Execute the tool
51
+ const result = await executeTool(toolName, args, toolContext)
52
+
53
+ if (!result.success) {
54
+ return NextResponse.json(
55
+ { success: false, error: result.error },
56
+ { status: result.errorCode === 'UNAUTHORIZED' ? 403 : 400 }
57
+ )
58
+ }
59
+
60
+ return NextResponse.json({
61
+ success: true,
62
+ result: result.result,
63
+ })
64
+ } catch (error) {
65
+ console.error('[AI Tools] Error executing tool:', error)
66
+ return NextResponse.json(
67
+ { success: false, error: 'Tool execution failed' },
68
+ { status: 500 }
69
+ )
70
+ }
71
+ }
@@ -0,0 +1,57 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { zodToJsonSchema } from 'zod-to-json-schema'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getToolRegistry } from '../../lib/tool-registry'
6
+ import { loadAllModuleTools } from '../../lib/tool-loader'
7
+ import { hasRequiredFeatures } from '../../lib/auth'
8
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
9
+
10
+ export const metadata = {
11
+ GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
12
+ }
13
+
14
+ export async function GET(req: NextRequest) {
15
+ const auth = await getAuthFromRequest(req)
16
+
17
+ if (!auth) {
18
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
19
+ }
20
+
21
+ try {
22
+ const container = await createRequestContainer()
23
+ const rbacService = container.resolve<RbacService>('rbacService')
24
+
25
+ // Load ACL for user
26
+ const acl = await rbacService.loadAcl(auth.sub, {
27
+ tenantId: auth.tenantId,
28
+ organizationId: auth.orgId,
29
+ })
30
+
31
+ // Ensure tools are loaded
32
+ await loadAllModuleTools()
33
+
34
+ // Get tools filtered by ACL
35
+ const registry = getToolRegistry()
36
+ const allTools = Array.from(registry.getTools().values())
37
+
38
+ const accessibleTools = allTools.filter((tool) =>
39
+ hasRequiredFeatures(tool.requiredFeatures, acl.features, acl.isSuperAdmin)
40
+ )
41
+
42
+ const tools = accessibleTools.map((tool) => {
43
+ const nameParts = tool.name.split('.')
44
+ return {
45
+ name: tool.name,
46
+ description: tool.description,
47
+ inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,
48
+ module: nameParts[0] || 'other',
49
+ }
50
+ })
51
+
52
+ return NextResponse.json({ tools })
53
+ } catch (error) {
54
+ console.error('[AI Tools] Error listing tools:', error)
55
+ return NextResponse.json({ error: 'Failed to list tools' }, { status: 500 })
56
+ }
57
+ }