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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +78 -37
  2. package/dist/modules/ai_assistant/api/chat/route.js +104 -13
  3. package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
  4. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +97 -0
  5. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
  6. package/dist/modules/ai_assistant/lib/codemode-tools.js +610 -0
  7. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +7 -0
  8. package/dist/modules/ai_assistant/lib/http-server.js +65 -7
  9. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  10. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +16 -1
  11. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  12. package/dist/modules/ai_assistant/lib/mcp-server.js +17 -0
  13. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  14. package/dist/modules/ai_assistant/lib/opencode-handlers.js +32 -0
  15. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +2 -2
  16. package/dist/modules/ai_assistant/lib/sandbox.js +124 -0
  17. package/dist/modules/ai_assistant/lib/sandbox.js.map +7 -0
  18. package/dist/modules/ai_assistant/lib/session-memory.js +103 -0
  19. package/dist/modules/ai_assistant/lib/session-memory.js.map +7 -0
  20. package/dist/modules/ai_assistant/lib/tool-loader.js +4 -114
  21. package/dist/modules/ai_assistant/lib/tool-loader.js.map +3 -3
  22. package/dist/modules/ai_assistant/lib/truncate.js +26 -0
  23. package/dist/modules/ai_assistant/lib/truncate.js.map +7 -0
  24. package/jest.config.cjs +23 -0
  25. package/package.json +6 -5
  26. package/src/modules/ai_assistant/api/chat/route.ts +110 -14
  27. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +129 -0
  28. package/src/modules/ai_assistant/lib/__tests__/sandbox.test.ts +642 -0
  29. package/src/modules/ai_assistant/lib/__tests__/session-memory.test.ts +82 -0
  30. package/src/modules/ai_assistant/lib/__tests__/truncate.test.ts +76 -0
  31. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +143 -0
  32. package/src/modules/ai_assistant/lib/codemode-tools.ts +864 -0
  33. package/src/modules/ai_assistant/lib/http-server.ts +86 -9
  34. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +19 -0
  35. package/src/modules/ai_assistant/lib/mcp-server.ts +21 -0
  36. package/src/modules/ai_assistant/lib/opencode-handlers.ts +40 -0
  37. package/src/modules/ai_assistant/lib/sandbox.ts +192 -0
  38. package/src/modules/ai_assistant/lib/session-memory.ts +174 -0
  39. package/src/modules/ai_assistant/lib/tool-loader.ts +11 -145
  40. package/src/modules/ai_assistant/lib/truncate.ts +45 -0
  41. package/src/modules/ai_assistant/lib/types.ts +2 -0
package/AGENTS.md CHANGED
@@ -8,15 +8,13 @@
8
8
  - Expose module tools to an AI agent via MCP (Model Context Protocol)
9
9
  - Enable dynamic API discovery so the agent can call any endpoint without hardcoded tools
10
10
  - Build the Raycast-style Command Palette UI (Cmd+K) for user interaction
11
- - Combine search-based and OpenAPI-based tool discovery
12
11
 
13
- Five core components to understand:
12
+ Four core components to understand:
14
13
 
15
14
  1. **OpenCode Agent** — AI backend that processes natural language and executes tools
16
15
  2. **MCP HTTP Server** — Exposes tools to OpenCode via HTTP on port 3001
17
- 3. **API Discovery Tools** — 3 meta-tools that replace 600+ individual endpoint tools
16
+ 3. **Code Mode Tools** — 2 meta-tools (`search` + `execute`) where the AI writes JavaScript that runs in a `node:vm` sandbox
18
17
  4. **Command Palette UI** — Raycast-style frontend interface
19
- 5. **Hybrid Tool Discovery** — Merges search-based and OpenAPI introspection results
20
18
 
21
19
  ## Common Tasks
22
20
 
@@ -57,11 +55,11 @@ registerMcpTool({
57
55
 
58
56
  ### Add New API Endpoints to Discovery
59
57
 
60
- APIs are automatically discovered from the OpenAPI spec (`openapi.yaml`). Follow these steps:
58
+ APIs are automatically available via the Code Mode `search` tool (reads the OpenAPI spec at runtime). To add new endpoints:
61
59
 
62
60
  1. Define the endpoint in your module's route file with an `openApi` export
63
- 2. Regenerate the OpenAPI spec
64
- 3. Restart the MCP server
61
+ 2. Regenerate the OpenAPI spec (`yarn modules:prepare`)
62
+ 3. Restart the MCP server — the `search` tool's `spec.paths` will include the new endpoint
65
63
 
66
64
  ### Debug Tool Calls
67
65
 
@@ -125,9 +123,9 @@ When modifying this stack, follow these constraints:
125
123
  │ ▼ │
126
124
  │ ┌─────────────────────────────────────────────────────────────────────┐ │
127
125
  │ │ MCP HTTP Server (:3001) │ │
128
- │ │ • Exposes 10 tools to OpenCode │ │
129
- │ │ • API discovery tools (api_discover, api_execute, api_schema) │ │
130
- │ │ • Search tools (search, search_status, etc.) │ │
126
+ │ │ • Exposes 3 tools: context_whoami + Code Mode (search, execute) │ │
127
+ │ │ • search: AI writes JS to query OpenAPI spec + entity schemas │ │
128
+ │ │ • execute: AI writes JS to make API calls via api.request() │ │
131
129
  │ │ • Authentication via x-api-key header │ │
132
130
  │ └─────────────────────────────────────────────────────────────────────┘ │
133
131
  │ │
@@ -159,8 +157,12 @@ packages/ai-assistant/
159
157
  │ │ ├── lib/
160
158
  │ │ │ ├── opencode-client.ts # OpenCode server client
161
159
  │ │ │ ├── opencode-handlers.ts # Request handlers for OpenCode
162
- │ │ │ ├── api-discovery-tools.ts # api_discover, api_execute, api_schema
163
- │ │ │ ├── api-endpoint-index.ts # OpenAPI endpoint indexing
160
+ │ │ │ ├── codemode-tools.ts # Code Mode search + execute tools
161
+ │ │ │ ├── sandbox.ts # node:vm sandbox executor
162
+ │ │ │ ├── truncate.ts # Response size limiter
163
+ │ │ │ ├── api-endpoint-index.ts # OpenAPI endpoint indexing + raw spec cache
164
+ │ │ │ ├── api-discovery-tools.ts # (legacy, unused) old find_api/call_api
165
+ │ │ │ ├── entity-graph-tools.ts # (legacy, unused) old discover_schema
164
166
  │ │ │ ├── http-server.ts # MCP HTTP server implementation
165
167
  │ │ │ ├── mcp-server.ts # MCP stdio server implementation
166
168
  │ │ │ ├── tool-registry.ts # Global tool registration
@@ -225,30 +227,40 @@ When you need to interact with OpenCode, follow these rules:
225
227
  }
226
228
  ```
227
229
 
228
- ## Rules for Working with API Discovery Tools
230
+ ## Rules for Code Mode Tools
229
231
 
230
- Use 3 meta-tools instead of 600+ individual tools:
232
+ Use 2 meta-tools instead of individual endpoint/schema tools. The AI writes JavaScript that runs in a `node:vm` sandbox:
231
233
 
232
- | Tool | When to use |
233
- |------|-------------|
234
- | `api_discover` | When you need to find APIs by keyword, module, or HTTP method |
235
- | `api_schema` | When you need detailed schema for a specific endpoint before calling it |
236
- | `api_execute` | When you need to execute an API call with parameters |
234
+ | Tool | Sandbox globals | When to use |
235
+ |------|----------------|-------------|
236
+ | `search` | `spec` (OpenAPI paths + entity schemas) | When discovering endpoints, understanding schemas, or exploring the API surface |
237
+ | `execute` | `api.request()`, `context` | When making API calls to read or write data |
237
238
 
238
239
  **Example workflow the agent follows**:
239
240
  1. Agent receives: "Find all customers in New York"
240
- 2. Agent calls `api_discover("customers search")`
241
- 3. Agent calls `api_schema("/api/v1/customers")` to see parameters
242
- 4. Agent calls `api_execute({ method: "GET", path: "/api/v1/customers", query: { city: "New York" } })`
241
+ 2. Agent calls `search({ code: 'async () => Object.keys(spec.paths).filter(p => p.includes("customer"))' })`
242
+ 3. Agent calls `search({ code: 'async () => spec.paths["/api/customers/companies"]?.get' })` to see endpoint details
243
+ 4. Agent calls `execute({ code: 'async () => api.request({ method: "GET", path: "/api/customers/companies", query: { city: "New York" } })' })`
243
244
 
244
- ## Rules for Hybrid Tool Discovery
245
+ **Sandbox safety**: Code runs in `node:vm` with only whitelisted globals. `fetch`, `require`, `process`, `fs`, `Buffer`, and network APIs are blocked. Execution times out after 30 seconds. API calls are capped at 50 per execution.
245
246
 
246
- Tools are discovered through two combined sources:
247
+ **When modifying Code Mode tools**: Edit `lib/codemode-tools.ts` for tool definitions, `lib/sandbox.ts` for the sandbox engine, `lib/truncate.ts` for response size limiting.
247
248
 
248
- 1. **Search-based**: Semantic search over tool descriptions
249
- 2. **OpenAPI-based**: Direct introspection of API endpoints
249
+ ## MANDATORY: Use AskUserQuestion for Confirmations
250
250
 
251
- When you need to modify discovery behavior, edit `api_discover` in `lib/api-discovery-tools.ts` it merges both sources.
251
+ > **This is the MOST IMPORTANT rule. NEVER skip this.**
252
+
253
+ Before ANY operation that modifies data (CREATE, UPDATE, DELETE):
254
+
255
+ 1. **YOU MUST USE** the `AskUserQuestion` tool
256
+ 2. Do NOT just write "Proceed?" in text
257
+ 3. The `AskUserQuestion` tool will show buttons and WAIT for user response
258
+ 4. Only proceed after user selects confirmation option
259
+
260
+ **Why This Matters:**
261
+ - Text like "Shall I proceed?" does NOT pause execution
262
+ - Only the `AskUserQuestion` tool actually waits for user input
263
+ - Without it, the AI may proceed without real confirmation
252
264
 
253
265
  ## Rules for the Chat Flow
254
266
 
@@ -503,21 +515,24 @@ async function handleOpenCodeMessage(options: {
503
515
  function extractTextFromResponse(result: OpenCodeMessage): string
504
516
  ```
505
517
 
506
- ## Rules for API Discovery Internals
518
+ ## Rules for Code Mode Internals
507
519
 
508
- Located in `lib/api-discovery-tools.ts`. When modifying discovery logic:
520
+ Located in `lib/codemode-tools.ts`, `lib/sandbox.ts`, `lib/truncate.ts`.
509
521
 
510
522
  ```typescript
511
- // Registered tools:
512
- // - api_discover: Search endpoints by keyword
513
- // - api_schema: Get endpoint details
514
- // - api_execute: Execute API call
515
-
516
- // Use these internal functions when extending discovery:
517
- function searchEndpoints(query: string, options?: SearchOptions): EndpointMatch[]
518
- function executeApiCall(params: ExecuteParams, ctx: McpToolContext): Promise<unknown>
523
+ // lib/codemode-tools.ts — Tool definitions
524
+ loadCodeModeTools(): Promise<number> // Registers search + execute, returns 2
525
+
526
+ // lib/sandbox.ts Sandbox engine
527
+ createSandbox(globals, options?): { execute: (code: string) => Promise<SandboxResult> }
528
+ normalizeCode(code: string): string // Strip markdown fences, validate shape
529
+
530
+ // lib/truncate.ts Response limiting
531
+ truncateResult(value, maxChars?): string // Default 40K chars (~10K tokens)
519
532
  ```
520
533
 
534
+ **Legacy files kept but unused**: `lib/api-discovery-tools.ts` (old find_api/call_api) and `lib/entity-graph-tools.ts` (old discover_schema) remain in the tree but are no longer imported.
535
+
521
536
  ## Rules for the API Endpoint Index
522
537
 
523
538
  Located in `lib/api-endpoint-index.ts`. Use the singleton pattern — never instantiate directly:
@@ -1034,6 +1049,32 @@ if (tool.requiredFeatures?.length) {
1034
1049
 
1035
1050
  ## Changelog
1036
1051
 
1052
+ ### 2026-02-22 - Code Mode Tools (search + execute)
1053
+
1054
+ **Major change**: Replaced all individual API/schema/module tools with 2 Code Mode meta-tools following Cloudflare's Code Mode pattern.
1055
+
1056
+ **What changed**:
1057
+ - Added `search` tool: AI writes JavaScript to query the OpenAPI spec + entity schemas via a `spec` global
1058
+ - Added `execute` tool: AI writes JavaScript to make API calls via `api.request()` in a `node:vm` sandbox
1059
+ - Removed `find_api`, `call_api`, `discover_schema` tools (files kept but no longer imported)
1060
+ - Removed auto-discovered module AI tools from `ai-tools.generated.ts`
1061
+ - Token savings: from ~10+ tool schemas to exactly 2, with fixed footprint regardless of API surface growth
1062
+
1063
+ **Files created**:
1064
+ - `lib/codemode-tools.ts` — `search` and `execute` tool definitions
1065
+ - `lib/sandbox.ts` — `node:vm` sandbox executor with security restrictions
1066
+ - `lib/truncate.ts` — Response size limiter (40K chars / ~10K tokens)
1067
+
1068
+ **Files modified**:
1069
+ - `lib/api-endpoint-index.ts` — Added `getRawOpenApiSpec()` for raw spec caching
1070
+ - `lib/tool-loader.ts` — Loads Code Mode tools instead of legacy tools + module tools
1071
+ - `lib/http-server.ts` — Pre-caches raw OpenAPI spec at startup
1072
+ - `lib/mcp-server.ts` — Generates entity graph and caches spec for stdio mode
1073
+
1074
+ **Files kept but unused**:
1075
+ - `lib/api-discovery-tools.ts` — Old find_api/call_api (no longer imported)
1076
+ - `lib/entity-graph-tools.ts` — Old discover_schema (no longer imported)
1077
+
1037
1078
  ### 2026-01-17 - Session Persistence Fix
1038
1079
 
1039
1080
  **Lesson learned:** Never use `Promise.race` for SSE completion — the HTTP response resolves before SSE can emit the `done` event. Always await only the SSE event promise.
@@ -14,22 +14,102 @@ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
14
14
  const CHAT_SYSTEM_INSTRUCTIONS = `
15
15
  You are a helpful business assistant for Open Mercato.
16
16
 
17
- EFFICIENCY - CRITICAL:
18
- - MINIMIZE tool calls. If you already have the information, DO NOT call tools again.
19
- - When you retrieve entity info or search results, REMEMBER and REUSE that data.
20
- - For simple queries, use ONE search and present results - don't over-fetch.
21
- - For updates: search once to find the record, then call_api once to update.
17
+ \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
18
+ ABSOLUTE RULES \u2014 FOLLOW THESE OR BE CUT OFF
19
+ \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
20
+ 1. 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.
21
+ 2. 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.
22
+ 3. 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.
23
+ 4. Maximum 4 tool calls per message. Hard limit is 10.
22
24
 
23
- STATUS UPDATES: Before each tool call, output a brief status line:
24
- - "\u{1F50D} Searching..." before search_query
25
- - "\u{1F4CB} Getting details..." before search_get or understand_entity
26
- - "\u{1F517} Calling API..." before call_api
25
+ You have 2 tools \u2014 both accept a "code" parameter with an async JavaScript arrow function.
26
+
27
+ TOOL: search \u2014 discover endpoints and schemas (READ-ONLY, fast)
28
+ - spec.findEndpoints(keyword) \u2192 [{ path, methods }]
29
+ - spec.describeEndpoint(path, method) \u2192 COMPACT: { requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }
30
+ - spec.describeEntity(keyword) \u2192 { className, fields, relationships }
31
+
32
+ TOOL: execute \u2014 make API calls (reads and writes)
33
+ - api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }
34
+
35
+ COMMON API PATHS (use directly \u2014 do NOT call findEndpoints for these):
36
+ /api/customers/companies \u2014 companies (GET list, POST create, PUT update)
37
+ /api/customers/people \u2014 contacts/people
38
+ /api/customers/deals \u2014 deals/opportunities
39
+ /api/customers/activities \u2014 activities/tasks
40
+ /api/sales/orders \u2014 sales orders
41
+ /api/sales/quotes \u2014 quotes
42
+ /api/sales/invoices \u2014 invoices
43
+ /api/catalog/products \u2014 products
44
+ /api/catalog/categories \u2014 categories
45
+
46
+ \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
47
+ RECIPES \u2014 follow EXACTLY for each task type
48
+ \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
49
+
50
+ FIND/LIST records (1 call):
51
+ For COMMON PATHS: skip describeEndpoint, go straight to execute.
52
+ 1. execute: api.request({ method: 'GET', path: '/api/<module>/<resource>' })
53
+ The "search" query param only matches indexed text fields \u2014 it will NOT match concepts like "Polish" or "large".
54
+ For conceptual/subjective queries, fetch ALL records and use YOUR reasoning to identify matches from the returned data.
55
+ For unknown paths: 1 search + 1 execute.
56
+
57
+ UPDATE a record (3-4 calls):
58
+ 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'PUT') \u2192 learn requestBody fields AND relatedEntity
59
+ 2. execute: GET the record \u2192 find it, get its ID
60
+ 3. execute: PUT to the COLLECTION path with id IN THE BODY:
61
+ api.request({ method: 'PUT', path: '/api/<module>/<resource>', body: { id: '<uuid>', ...changes } })
62
+ NOTE: All CRUD endpoints use the COLLECTION path. The id goes in the request BODY, not the URL. There are NO /{id} path segments.
63
+
64
+ CREATE a record (2-3 calls):
65
+ 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 gives requiredFields, optionalFields, nestedCollections, and a working example
66
+ 2. Ask user for confirmation with the field values
67
+ 3. execute: api.request({ method: 'POST', ...body })
68
+ If the endpoint has nestedCollections (like lines), include them INLINE in the body \u2014 do NOT create them separately.
69
+ Use the "example" from describeEndpoint as your template \u2014 fill in real values.
70
+ Example \u2014 create a quote with line items:
71
+ api.request({ method: 'POST', path: '/api/sales/quotes', body: {
72
+ currencyCode: 'EUR', customerEntityId: '<company-uuid>',
73
+ lines: [{ currencyCode: 'EUR', quantity: 1, productId: '<product-uuid>', name: 'Product Name', kind: 'product' }]
74
+ }})
75
+ Do NOT create lines separately. Do NOT include id, quoteId, or total fields \u2014 the server generates them.
76
+
77
+ CREATE MULTIPLE records (2-3 calls):
78
+ 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 learn fields + example
79
+ 2. execute: loop in one call:
80
+ async () => {
81
+ const results = [];
82
+ for (const item of items) {
83
+ results.push(await api.request({ method: 'POST', path: '...', body: item }));
84
+ }
85
+ return results;
86
+ }
87
+
88
+ DISCOVER (what endpoints/entities exist) (1 call):
89
+ 1. search: spec.findEndpoints('<keyword>') or spec.describeEntity('<keyword>')
90
+
91
+ \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
92
+ HARD RULES
93
+ \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
94
+ - MAXIMUM 4 tool calls per user message. You WILL be cut off after 10.
95
+ - NEVER call findEndpoints or describeEndpoint for COMMON PATHS listed above \u2014 use them directly with execute.
96
+ - NEVER call describeEntity if describeEndpoint already returned relatedEntity.
97
+ - NEVER repeat a search from earlier in the conversation \u2014 reuse previous results.
98
+ - NEVER make N+1 API calls (1 call per record). Fetch a list and reason about the results yourself.
99
+ - When you already have the data you need from a previous call, use it \u2014 do NOT fetch more data to "enrich" it.
100
+ - 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.
101
+ - The "search" query param is fulltext only \u2014 it won't match nationalities, categories, or subjective criteria. For those, fetch all and reason.
102
+ - 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.
103
+ - For fields you don't know, OMIT them \u2014 the API uses defaults for optional fields.
104
+ - NEVER try to set computed/total fields (amounts, totals, counts) \u2014 the server calculates them.
105
+ - For updates: describeEndpoint gives you the field names. Go straight to GET + PUT. PUT path is the COLLECTION path, id in BODY.
106
+ - For creates with children (e.g. quote + lines): include children INLINE in the body using the nestedCollections field name.
27
107
 
28
108
  RESPONSE RULES:
29
- - Be proactive - fetch data and present results, don't ask what the user wants to see
30
- - Never show technical terms, IDs, JSON, or internal reasoning
31
- - Present results in clean business language with **bold names** and bullet points
32
- - Only ask for confirmation before create/update/delete operations
109
+ - Be proactive \u2014 fetch data and present results, don't ask what the user wants to see.
110
+ - Never show technical terms, IDs, JSON, or internal reasoning.
111
+ - Present results in clean business language with **bold names** and bullet points.
112
+ - Only ask for confirmation before create/update/delete operations.
33
113
  `.trim();
34
114
  const metadata = {
35
115
  POST: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
@@ -97,6 +177,9 @@ async function POST(req) {
97
177
  if (!lastUserMessage) {
98
178
  return NextResponse.json({ error: "No user message found" }, { status: 400 });
99
179
  }
180
+ const chatStartTime = Date.now();
181
+ const messagePreview = lastUserMessage.slice(0, 80).replace(/\n/g, " ");
182
+ console.error(`[AI Usage] Chat request: user=${auth.sub} session=${sessionId ? sessionId.slice(0, 16) + "..." : "new"} message="${messagePreview}${lastUserMessage.length > 80 ? "..." : ""}"`);
100
183
  let sessionToken = null;
101
184
  if (!sessionId) {
102
185
  try {
@@ -130,6 +213,9 @@ async function POST(req) {
130
213
  }
131
214
  messageToSend += lastUserMessage;
132
215
  (async () => {
216
+ let toolCallCount = 0;
217
+ let lastTokens;
218
+ let resultSessionId;
133
219
  try {
134
220
  if (sessionToken) {
135
221
  console.log("[AI Chat] Emitting session-authorized event");
@@ -145,6 +231,9 @@ async function POST(req) {
145
231
  sessionId
146
232
  },
147
233
  async (event) => {
234
+ if (event.type === "tool-call") toolCallCount++;
235
+ if (event.type === "metadata" && "tokens" in event) lastTokens = event.tokens;
236
+ if (event.type === "done" && "sessionId" in event) resultSessionId = event.sessionId;
148
237
  await writeSSE(event);
149
238
  }
150
239
  );
@@ -155,6 +244,8 @@ async function POST(req) {
155
244
  error: error instanceof Error ? error.message : "OpenCode request failed"
156
245
  });
157
246
  } finally {
247
+ const durationMs = Date.now() - chatStartTime;
248
+ 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}}` : ""}`);
158
249
  await closeWriter();
159
250
  }
160
251
  })();
@@ -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\nEFFICIENCY - CRITICAL:\n- MINIMIZE tool calls. If you already have the information, DO NOT call tools again.\n- When you retrieve entity info or search results, REMEMBER and REUSE that data.\n- For simple queries, use ONE search and present results - don't over-fetch.\n- For updates: search once to find the record, then call_api once to update.\n\nSTATUS UPDATES: Before each tool call, output a brief status line:\n- \"\uD83D\uDD0D Searching...\" before search_query\n- \"\uD83D\uDCCB Getting details...\" before search_get or understand_entity\n- \"\uD83D\uDD17 Calling API...\" before call_api\n\nRESPONSE RULES:\n- Be proactive - 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 (api_discover, api_execute, api_schema).\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 // 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 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 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 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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",
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": "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",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,7 @@ import {
9
9
  const API_ENDPOINT_ENTITY = API_ENDPOINT_ENTITY_ID;
10
10
  let endpointsCache = null;
11
11
  let endpointsByOperationId = null;
12
+ let rawSpecCache = null;
12
13
  async function getApiEndpoints() {
13
14
  if (endpointsCache) {
14
15
  return endpointsCache;
@@ -21,6 +22,97 @@ async function getEndpointByOperationId(operationId) {
21
22
  await getApiEndpoints();
22
23
  return endpointsByOperationId?.get(operationId) ?? null;
23
24
  }
25
+ async function getRawOpenApiSpec() {
26
+ if (rawSpecCache) return rawSpecCache;
27
+ rawSpecCache = await loadRawOpenApiSpec();
28
+ return rawSpecCache;
29
+ }
30
+ function setRawSpecCache(doc) {
31
+ rawSpecCache = doc;
32
+ }
33
+ function clearRawSpecCache() {
34
+ rawSpecCache = null;
35
+ }
36
+ async function loadRichOpenApiSpec() {
37
+ if (rawSpecCache) return rawSpecCache;
38
+ try {
39
+ const { getModules } = await import("@open-mercato/shared/lib/modules/registry");
40
+ const modules = getModules();
41
+ const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0);
42
+ if (modulesWithApis.length > 0) {
43
+ const doc = buildOpenApiDocument(modules, {
44
+ title: "Open Mercato API",
45
+ version: "1.0.0",
46
+ servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" }]
47
+ });
48
+ console.error(`[API Index] Rich OpenAPI spec built from ${modulesWithApis.length} modules (Tier 2)`);
49
+ rawSpecCache = doc;
50
+ return doc;
51
+ }
52
+ } catch {
53
+ }
54
+ rawSpecCache = await loadRawOpenApiSpec();
55
+ return rawSpecCache;
56
+ }
57
+ async function loadRawOpenApiSpec() {
58
+ try {
59
+ const fs = await import("node:fs");
60
+ const path = await import("node:path");
61
+ const { findAppRoot, findAllApps } = await import("@open-mercato/shared/lib/bootstrap/appResolver");
62
+ let appRoot = findAppRoot();
63
+ if (!appRoot) {
64
+ let current = process.cwd();
65
+ while (current !== path.dirname(current)) {
66
+ const appsDir = path.join(current, "apps");
67
+ if (fs.existsSync(appsDir)) {
68
+ const apps = findAllApps(current);
69
+ if (apps.length > 0) {
70
+ appRoot = apps[0];
71
+ break;
72
+ }
73
+ }
74
+ current = path.dirname(current);
75
+ }
76
+ }
77
+ if (appRoot) {
78
+ const jsonPath = path.join(appRoot.generatedDir, "openapi.generated.json");
79
+ if (fs.existsSync(jsonPath)) {
80
+ const doc = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
81
+ console.error(`[API Index] Raw OpenAPI spec loaded from ${jsonPath}`);
82
+ return doc;
83
+ }
84
+ }
85
+ } catch (error) {
86
+ console.error("[API Index] Raw spec from JSON failed:", error instanceof Error ? error.message : error);
87
+ }
88
+ try {
89
+ const { getModules } = await import("@open-mercato/shared/lib/modules/registry");
90
+ const modules = getModules();
91
+ const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0);
92
+ if (modulesWithApis.length > 0) {
93
+ const doc = buildOpenApiDocument(modules, {
94
+ title: "Open Mercato API",
95
+ version: "1.0.0",
96
+ servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" }]
97
+ });
98
+ console.error(`[API Index] Raw OpenAPI spec built from ${modulesWithApis.length} modules`);
99
+ return doc;
100
+ }
101
+ } catch {
102
+ }
103
+ const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
104
+ try {
105
+ const response = await fetch(`${baseUrl}/api/docs/openapi`);
106
+ if (response.ok) {
107
+ const doc = await response.json();
108
+ console.error("[API Index] Raw OpenAPI spec fetched via HTTP");
109
+ return doc;
110
+ }
111
+ } catch (error) {
112
+ console.error("[API Index] Raw spec HTTP fetch failed:", error instanceof Error ? error.message : error);
113
+ }
114
+ return null;
115
+ }
24
116
  async function parseApiEndpointsFromGeneratedJson() {
25
117
  try {
26
118
  const fs = await import("node:fs");
@@ -270,6 +362,7 @@ function searchEndpointsFallback(query, options = {}) {
270
362
  function clearEndpointCache() {
271
363
  endpointsCache = null;
272
364
  endpointsByOperationId = null;
365
+ rawSpecCache = null;
273
366
  }
274
367
  function simplifyRequestBodySchema(schema) {
275
368
  if (!schema) return null;
@@ -293,10 +386,14 @@ function simplifyRequestBodySchema(schema) {
293
386
  export {
294
387
  API_ENDPOINT_ENTITY,
295
388
  clearEndpointCache,
389
+ clearRawSpecCache,
296
390
  getApiEndpoints,
297
391
  getEndpointByOperationId,
392
+ getRawOpenApiSpec,
298
393
  indexApiEndpoints,
394
+ loadRichOpenApiSpec,
299
395
  searchEndpoints,
396
+ setRawSpecCache,
300
397
  simplifyRequestBodySchema
301
398
  };
302
399
  //# sourceMappingURL=api-endpoint-index.js.map