@chaaskit/server 0.1.0

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 (189) hide show
  1. package/dist/api/admin.js +438 -0
  2. package/dist/api/admin.js.map +1 -0
  3. package/dist/api/agents.js +21 -0
  4. package/dist/api/agents.js.map +1 -0
  5. package/dist/api/api-keys.js +122 -0
  6. package/dist/api/api-keys.js.map +1 -0
  7. package/dist/api/auth.js +399 -0
  8. package/dist/api/auth.js.map +1 -0
  9. package/dist/api/chat.js +900 -0
  10. package/dist/api/chat.js.map +1 -0
  11. package/dist/api/config.js +91 -0
  12. package/dist/api/config.js.map +1 -0
  13. package/dist/api/documents.js +237 -0
  14. package/dist/api/documents.js.map +1 -0
  15. package/dist/api/export.js +107 -0
  16. package/dist/api/export.js.map +1 -0
  17. package/dist/api/health.js +25 -0
  18. package/dist/api/health.js.map +1 -0
  19. package/dist/api/mcp-server.js +84 -0
  20. package/dist/api/mcp-server.js.map +1 -0
  21. package/dist/api/mcp.js +400 -0
  22. package/dist/api/mcp.js.map +1 -0
  23. package/dist/api/mentions.js +94 -0
  24. package/dist/api/mentions.js.map +1 -0
  25. package/dist/api/oauth.js +366 -0
  26. package/dist/api/oauth.js.map +1 -0
  27. package/dist/api/payments.js +473 -0
  28. package/dist/api/payments.js.map +1 -0
  29. package/dist/api/projects.js +301 -0
  30. package/dist/api/projects.js.map +1 -0
  31. package/dist/api/scheduled-prompts.js +617 -0
  32. package/dist/api/scheduled-prompts.js.map +1 -0
  33. package/dist/api/search.js +85 -0
  34. package/dist/api/search.js.map +1 -0
  35. package/dist/api/share.js +188 -0
  36. package/dist/api/share.js.map +1 -0
  37. package/dist/api/slack.js +468 -0
  38. package/dist/api/slack.js.map +1 -0
  39. package/dist/api/teams.js +693 -0
  40. package/dist/api/teams.js.map +1 -0
  41. package/dist/api/templates.js +134 -0
  42. package/dist/api/templates.js.map +1 -0
  43. package/dist/api/threads.js +323 -0
  44. package/dist/api/threads.js.map +1 -0
  45. package/dist/api/upload.js +57 -0
  46. package/dist/api/upload.js.map +1 -0
  47. package/dist/api/user.js +111 -0
  48. package/dist/api/user.js.map +1 -0
  49. package/dist/api/v1/openai.js +245 -0
  50. package/dist/api/v1/openai.js.map +1 -0
  51. package/dist/app.js +168 -0
  52. package/dist/app.js.map +1 -0
  53. package/dist/bin/cli.js +57 -0
  54. package/dist/bin/cli.js.map +1 -0
  55. package/dist/commands/db-sync.js +108 -0
  56. package/dist/commands/db-sync.js.map +1 -0
  57. package/dist/config/loader.js +374 -0
  58. package/dist/config/loader.js.map +1 -0
  59. package/dist/documents/extractors.js +136 -0
  60. package/dist/documents/extractors.js.map +1 -0
  61. package/dist/extensions/glob.js +53 -0
  62. package/dist/extensions/glob.js.map +1 -0
  63. package/dist/extensions/loader.js +72 -0
  64. package/dist/extensions/loader.js.map +1 -0
  65. package/dist/index.js +25 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/loaders/index.js +75 -0
  68. package/dist/loaders/index.js.map +1 -0
  69. package/dist/mcp/client.js +551 -0
  70. package/dist/mcp/client.js.map +1 -0
  71. package/dist/mcp/server.js +335 -0
  72. package/dist/mcp/server.js.map +1 -0
  73. package/dist/middleware/apiKeyAuth.js +136 -0
  74. package/dist/middleware/apiKeyAuth.js.map +1 -0
  75. package/dist/middleware/auth.js +192 -0
  76. package/dist/middleware/auth.js.map +1 -0
  77. package/dist/middleware/errorHandler.js +41 -0
  78. package/dist/middleware/errorHandler.js.map +1 -0
  79. package/dist/middleware/mcpServerAuth.js +164 -0
  80. package/dist/middleware/mcpServerAuth.js.map +1 -0
  81. package/dist/middleware/requestLogger.js +9 -0
  82. package/dist/middleware/requestLogger.js.map +1 -0
  83. package/dist/middleware/team.js +132 -0
  84. package/dist/middleware/team.js.map +1 -0
  85. package/dist/oauth/server.js +410 -0
  86. package/dist/oauth/server.js.map +1 -0
  87. package/dist/queue/cli.js +93 -0
  88. package/dist/queue/cli.js.map +1 -0
  89. package/dist/queue/handlers/index.js +91 -0
  90. package/dist/queue/handlers/index.js.map +1 -0
  91. package/dist/queue/handlers/scheduled-prompt.js +270 -0
  92. package/dist/queue/handlers/scheduled-prompt.js.map +1 -0
  93. package/dist/queue/index.js +91 -0
  94. package/dist/queue/index.js.map +1 -0
  95. package/dist/queue/providers/memory.js +296 -0
  96. package/dist/queue/providers/memory.js.map +1 -0
  97. package/dist/queue/providers/sqs.js +275 -0
  98. package/dist/queue/providers/sqs.js.map +1 -0
  99. package/dist/queue/scheduler.js +355 -0
  100. package/dist/queue/scheduler.js.map +1 -0
  101. package/dist/queue/types.js +5 -0
  102. package/dist/queue/types.js.map +1 -0
  103. package/dist/queue/worker.js +230 -0
  104. package/dist/queue/worker.js.map +1 -0
  105. package/dist/registry/index.js +40 -0
  106. package/dist/registry/index.js.map +1 -0
  107. package/dist/server.js +207 -0
  108. package/dist/server.js.map +1 -0
  109. package/dist/services/agent.js +530 -0
  110. package/dist/services/agent.js.map +1 -0
  111. package/dist/services/agents.js +194 -0
  112. package/dist/services/agents.js.map +1 -0
  113. package/dist/services/documents.js +507 -0
  114. package/dist/services/documents.js.map +1 -0
  115. package/dist/services/email/index.js +91 -0
  116. package/dist/services/email/index.js.map +1 -0
  117. package/dist/services/email/providers/ses.js +97 -0
  118. package/dist/services/email/providers/ses.js.map +1 -0
  119. package/dist/services/email/templates.js +194 -0
  120. package/dist/services/email/templates.js.map +1 -0
  121. package/dist/services/email/types.js +5 -0
  122. package/dist/services/email/types.js.map +1 -0
  123. package/dist/services/encryption.js +69 -0
  124. package/dist/services/encryption.js.map +1 -0
  125. package/dist/services/oauth-discovery.js +226 -0
  126. package/dist/services/oauth-discovery.js.map +1 -0
  127. package/dist/services/pendingConfirmation.js +105 -0
  128. package/dist/services/pendingConfirmation.js.map +1 -0
  129. package/dist/services/scheduledPrompts.js +70 -0
  130. package/dist/services/scheduledPrompts.js.map +1 -0
  131. package/dist/services/slack/client.js +174 -0
  132. package/dist/services/slack/client.js.map +1 -0
  133. package/dist/services/slack/events.js +189 -0
  134. package/dist/services/slack/events.js.map +1 -0
  135. package/dist/services/slack/index.js +6 -0
  136. package/dist/services/slack/index.js.map +1 -0
  137. package/dist/services/slack/notifications.js +124 -0
  138. package/dist/services/slack/notifications.js.map +1 -0
  139. package/dist/services/slack/signature.js +74 -0
  140. package/dist/services/slack/signature.js.map +1 -0
  141. package/dist/services/slack/thread-context.js +191 -0
  142. package/dist/services/slack/thread-context.js.map +1 -0
  143. package/dist/services/toolConfirmation.js +55 -0
  144. package/dist/services/toolConfirmation.js.map +1 -0
  145. package/dist/services/usage.js +241 -0
  146. package/dist/services/usage.js.map +1 -0
  147. package/dist/ssr/build.js +90 -0
  148. package/dist/ssr/build.js.map +1 -0
  149. package/dist/ssr/components/SSRMessageList.js +120 -0
  150. package/dist/ssr/components/SSRMessageList.js.map +1 -0
  151. package/dist/ssr/entry.client.js +8 -0
  152. package/dist/ssr/entry.client.js.map +1 -0
  153. package/dist/ssr/entry.server.js +71 -0
  154. package/dist/ssr/entry.server.js.map +1 -0
  155. package/dist/ssr/handler.js +51 -0
  156. package/dist/ssr/handler.js.map +1 -0
  157. package/dist/ssr/root.js +184 -0
  158. package/dist/ssr/root.js.map +1 -0
  159. package/dist/ssr/routes/login.js +140 -0
  160. package/dist/ssr/routes/login.js.map +1 -0
  161. package/dist/ssr/routes/pricing.js +195 -0
  162. package/dist/ssr/routes/pricing.js.map +1 -0
  163. package/dist/ssr/routes/privacy.js +39 -0
  164. package/dist/ssr/routes/privacy.js.map +1 -0
  165. package/dist/ssr/routes/register.js +148 -0
  166. package/dist/ssr/routes/register.js.map +1 -0
  167. package/dist/ssr/routes/shared.$shareId.js +153 -0
  168. package/dist/ssr/routes/shared.$shareId.js.map +1 -0
  169. package/dist/ssr/routes/terms.js +39 -0
  170. package/dist/ssr/routes/terms.js.map +1 -0
  171. package/dist/storage/index.js +43 -0
  172. package/dist/storage/index.js.map +1 -0
  173. package/dist/storage/providers/database.js +38 -0
  174. package/dist/storage/providers/database.js.map +1 -0
  175. package/dist/storage/providers/filesystem.js +51 -0
  176. package/dist/storage/providers/filesystem.js.map +1 -0
  177. package/dist/storage/types.js +2 -0
  178. package/dist/storage/types.js.map +1 -0
  179. package/dist/tools/documents.js +336 -0
  180. package/dist/tools/documents.js.map +1 -0
  181. package/dist/tools/get-plan-usage.js +82 -0
  182. package/dist/tools/get-plan-usage.js.map +1 -0
  183. package/dist/tools/index.js +106 -0
  184. package/dist/tools/index.js.map +1 -0
  185. package/dist/tools/types.js +2 -0
  186. package/dist/tools/types.js.map +1 -0
  187. package/dist/tools/web-scrape.js +145 -0
  188. package/dist/tools/web-scrape.js.map +1 -0
  189. package/package.json +93 -0
@@ -0,0 +1,900 @@
1
+ import { Router } from 'express';
2
+ import { db } from '@chaaskit/db';
3
+ import { HTTP_STATUS, sendMessageSchema } from '@chaaskit/shared';
4
+ import { optionalAuth, requireAuth, optionalVerifiedEmail } from '../middleware/auth.js';
5
+ import { AppError } from '../middleware/errorHandler.js';
6
+ import { getConfig } from '../config/loader.js';
7
+ import { createAgentService } from '../services/agent.js';
8
+ import { checkUsageLimits, incrementUsage } from '../services/usage.js';
9
+ import { mcpManager } from '../mcp/client.js';
10
+ import { pendingConfirmations } from '../services/pendingConfirmation.js';
11
+ import { checkToolConfirmationRequired, createDenialToolResult } from '../services/toolConfirmation.js';
12
+ import { getAgentById, getDefaultAgent, filterToolsForAgent, toAgentConfig, isBuiltInAgent, getNativeToolsForAgentFiltered } from '../services/agents.js';
13
+ import { executeNativeTool, isNativeTool, resolveNativeToolTemplate } from '../tools/index.js';
14
+ import { documentService } from '../services/documents.js';
15
+ import { notifyMessageLiked } from '../services/slack/notifications.js';
16
+ export const chatRouter = Router();
17
+ // Send message with streaming response
18
+ chatRouter.post('/', optionalAuth, optionalVerifiedEmail, async (req, res, next) => {
19
+ try {
20
+ const config = getConfig();
21
+ const parsed = sendMessageSchema.parse(req.body);
22
+ const { threadId, content, files, agentId: requestAgentId } = parsed;
23
+ // Extract teamId and projectId from request body (not in schema, optional)
24
+ // Only use teamId if teams feature is enabled
25
+ const teamsEnabled = config.teams?.enabled ?? false;
26
+ const projectsEnabled = config.projects?.enabled ?? false;
27
+ const requestTeamId = teamsEnabled ? req.body.teamId : undefined;
28
+ const requestProjectId = projectsEnabled ? req.body.projectId : undefined;
29
+ console.log(`[Chat] Received message: threadId=${threadId || 'new'}, agentId=${requestAgentId || 'none'}, content="${content.slice(0, 50)}${content.length > 50 ? '...' : ''}", files=${files?.length || 0}`);
30
+ // Check usage limits (will be done again after we know the threadId/teamId)
31
+ // Initial check uses personal limits - team limits checked after thread is resolved
32
+ // Get or create thread
33
+ let thread;
34
+ let threadAgentId = null;
35
+ if (threadId) {
36
+ thread = await db.thread.findUnique({
37
+ where: { id: threadId },
38
+ include: {
39
+ messages: {
40
+ orderBy: { createdAt: 'asc' },
41
+ },
42
+ },
43
+ });
44
+ if (!thread) {
45
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Thread not found');
46
+ }
47
+ // Check access - team threads require membership with write permission
48
+ if (thread.teamId) {
49
+ if (!req.user) {
50
+ throw new AppError(HTTP_STATUS.UNAUTHORIZED, 'Authentication required');
51
+ }
52
+ const membership = await db.teamMember.findUnique({
53
+ where: {
54
+ teamId_userId: {
55
+ teamId: thread.teamId,
56
+ userId: req.user.id,
57
+ },
58
+ },
59
+ });
60
+ if (!membership) {
61
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
62
+ }
63
+ // Viewers can read but not send messages
64
+ if (membership.role === 'viewer') {
65
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Viewers cannot send messages');
66
+ }
67
+ }
68
+ else if (thread.userId && thread.userId !== req.user?.id) {
69
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
70
+ }
71
+ // Use thread's agent (locked per thread)
72
+ threadAgentId = thread.agentId;
73
+ console.log(`[Chat] Existing thread ${threadId}, using locked agentId=${threadAgentId}`);
74
+ }
75
+ else {
76
+ // New thread - use requested agent or default
77
+ if (requestAgentId) {
78
+ // Validate agent access
79
+ const agent = getAgentById(requestAgentId);
80
+ if (!agent) {
81
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Invalid agent');
82
+ }
83
+ threadAgentId = requestAgentId;
84
+ console.log(`[Chat] New thread with requested agentId=${requestAgentId}`);
85
+ }
86
+ else {
87
+ const defaultAgent = getDefaultAgent();
88
+ threadAgentId = defaultAgent.id;
89
+ console.log(`[Chat] New thread with no agentId requested, using default: ${defaultAgent.id}`);
90
+ }
91
+ // Validate team membership if creating a team thread
92
+ if (requestTeamId) {
93
+ if (!req.user) {
94
+ throw new AppError(HTTP_STATUS.UNAUTHORIZED, 'Authentication required for team threads');
95
+ }
96
+ const membership = await db.teamMember.findUnique({
97
+ where: {
98
+ teamId_userId: {
99
+ teamId: requestTeamId,
100
+ userId: req.user.id,
101
+ },
102
+ },
103
+ });
104
+ if (!membership) {
105
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'You are not a member of this team');
106
+ }
107
+ if (membership.role === 'viewer') {
108
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Viewers cannot create team threads');
109
+ }
110
+ }
111
+ // Validate project access if creating a project thread
112
+ let effectiveTeamId = requestTeamId || null;
113
+ if (requestProjectId) {
114
+ if (!req.user) {
115
+ throw new AppError(HTTP_STATUS.UNAUTHORIZED, 'Authentication required for project threads');
116
+ }
117
+ const project = await db.project.findUnique({
118
+ where: { id: requestProjectId },
119
+ });
120
+ if (!project || project.archivedAt) {
121
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Project not found');
122
+ }
123
+ // Check project access
124
+ if (project.userId !== req.user.id) {
125
+ if (!project.teamId || project.sharing === 'private') {
126
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
127
+ }
128
+ // For team-shared projects, verify team membership and write permission
129
+ const membership = await db.teamMember.findUnique({
130
+ where: {
131
+ teamId_userId: {
132
+ teamId: project.teamId,
133
+ userId: req.user.id,
134
+ },
135
+ },
136
+ });
137
+ if (!membership) {
138
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
139
+ }
140
+ if (membership.role === 'viewer') {
141
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Viewers cannot create threads');
142
+ }
143
+ }
144
+ // Thread inherits teamId from project
145
+ effectiveTeamId = project.teamId;
146
+ }
147
+ thread = await db.thread.create({
148
+ data: {
149
+ title: content.slice(0, 50) + (content.length > 50 ? '...' : ''),
150
+ userId: req.user?.id,
151
+ teamId: effectiveTeamId,
152
+ projectId: requestProjectId || null,
153
+ agentId: threadAgentId,
154
+ },
155
+ include: { messages: true },
156
+ });
157
+ }
158
+ // Get agent definition for this thread
159
+ const agentDef = getAgentById(threadAgentId);
160
+ console.log(`[Chat] Using agent: id=${agentDef?.id}, name=${agentDef?.name}, allowedTools=${agentDef?.allowedTools?.join(', ') || 'none'}`);
161
+ // Check usage limits with team context now that we have the thread
162
+ if (req.user) {
163
+ const canSend = await checkUsageLimits(req.user.id, thread.teamId || undefined);
164
+ if (!canSend) {
165
+ const context = thread.teamId ? 'team' : 'personal';
166
+ console.log(`[Chat] Usage limit exceeded for user ${req.user.id} (${context} billing)`);
167
+ throw new AppError(HTTP_STATUS.TOO_MANY_REQUESTS, `Usage limit exceeded for ${context} plan`);
168
+ }
169
+ }
170
+ // Create user message
171
+ await db.message.create({
172
+ data: {
173
+ threadId: thread.id,
174
+ role: 'user',
175
+ content,
176
+ files: files || undefined,
177
+ },
178
+ });
179
+ // Update thread title if it's the first message and title is default
180
+ const isFirstMessage = thread.messages.length === 0;
181
+ if (isFirstMessage && thread.title === 'New Chat') {
182
+ const newTitle = content.slice(0, 50) + (content.length > 50 ? '...' : '');
183
+ await db.thread.update({
184
+ where: { id: thread.id },
185
+ data: { title: newTitle },
186
+ });
187
+ thread.title = newTitle;
188
+ console.log(`[Chat] Updated thread title to: ${newTitle}`);
189
+ }
190
+ console.log(`[Chat] User message saved to thread ${thread.id}`);
191
+ // Set up SSE
192
+ res.setHeader('Content-Type', 'text/event-stream');
193
+ res.setHeader('Cache-Control', 'no-cache');
194
+ res.setHeader('Connection', 'keep-alive');
195
+ res.flushHeaders();
196
+ // Send thread info (including title for sidebar update)
197
+ res.write(`data: ${JSON.stringify({ type: 'thread', threadId: thread.id, title: thread.title })}\n\n`);
198
+ // Create agent service from thread's agent definition
199
+ const agentConfig = agentDef ? toAgentConfig(agentDef) : config.agent;
200
+ console.log(`[Chat] Creating agent service for agent: ${agentDef?.name || 'default'} (${agentDef?.id || 'legacy'})`);
201
+ const agentService = createAgentService(agentConfig);
202
+ // Build conversation history
203
+ const history = thread.messages.map((msg) => ({
204
+ role: msg.role,
205
+ content: msg.content,
206
+ }));
207
+ history.push({ role: 'user', content });
208
+ // Get user context from settings
209
+ const userSettings = req.user
210
+ ? await db.user.findUnique({
211
+ where: { id: req.user.id },
212
+ select: { settings: true },
213
+ })
214
+ : null;
215
+ const userContext = userSettings?.settings;
216
+ // Get team context if this is a team thread and teams are enabled
217
+ let teamContext = null;
218
+ if (teamsEnabled && thread.teamId) {
219
+ const team = await db.team.findUnique({
220
+ where: { id: thread.teamId },
221
+ select: { context: true },
222
+ });
223
+ teamContext = team?.context || null;
224
+ }
225
+ // Get project context if this thread belongs to a project and projects are enabled
226
+ let projectContext = null;
227
+ if (projectsEnabled && thread.projectId) {
228
+ const project = await db.project.findUnique({
229
+ where: { id: thread.projectId },
230
+ select: { context: true },
231
+ });
232
+ projectContext = project?.context || null;
233
+ }
234
+ // Parse mentions from message content and build mention context
235
+ let mentionContext = null;
236
+ const documentsEnabled = config.documents?.enabled ?? false;
237
+ const hybridThreshold = config.documents?.hybridThreshold ?? 1000;
238
+ if (documentsEnabled && req.user) {
239
+ const mentions = documentService.parseMentions(content);
240
+ if (mentions.length > 0) {
241
+ console.log(`[Chat] Found ${mentions.length} document mention(s) in message`);
242
+ const paths = mentions.map((m) => m.path);
243
+ const resolvedDocs = await documentService.resolveForContext(paths, req.user.id);
244
+ // Separate small docs (inject) and large docs (tools)
245
+ const smallDocs = resolvedDocs.filter((d) => d.charCount <= hybridThreshold);
246
+ const largeDocs = resolvedDocs.filter((d) => d.charCount > hybridThreshold);
247
+ // Build mention context for small docs
248
+ if (smallDocs.length > 0) {
249
+ mentionContext = 'Referenced documents:\n' +
250
+ smallDocs.map((d) => `--- ${d.path} ---\n${d.content}\n--- End ${d.path} ---`).join('\n\n');
251
+ console.log(`[Chat] Injected ${smallDocs.length} small document(s) into context`);
252
+ }
253
+ // Add hint about large docs that need tool access
254
+ if (largeDocs.length > 0) {
255
+ const largeDocHint = `\n\nLarge documents referenced (use read_document, search_in_document tools to access):\n` +
256
+ largeDocs.map((d) => `- ${d.path} (${d.charCount} chars)`).join('\n');
257
+ mentionContext = mentionContext
258
+ ? mentionContext + largeDocHint
259
+ : largeDocHint;
260
+ console.log(`[Chat] ${largeDocs.length} large document(s) available via tools`);
261
+ }
262
+ }
263
+ }
264
+ // Track tools allowed for this thread (via "allow for this thread" scope)
265
+ const threadAllowedTools = [];
266
+ // Generate a visitor ID for this request (used for SSE connection lookup)
267
+ const visitorId = `visitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
268
+ // Get available MCP tools (including user-credential servers)
269
+ const mcpServers = config.mcp?.servers || [];
270
+ console.log(`[Chat] Fetching MCP tools for user ${req.user?.id || 'anonymous'}...`);
271
+ const allMcpTools = await mcpManager.listAllToolsForUser(req.user?.id, mcpServers);
272
+ // Get native tools available to this agent
273
+ const nativeTools = getNativeToolsForAgentFiltered(threadAgentId);
274
+ console.log(`[Chat] ${nativeTools.length} native tools available to agent`);
275
+ // Combine MCP and native tools, then filter based on agent's allowedTools
276
+ const allTools = [...allMcpTools, ...nativeTools];
277
+ const filteredTools = filterToolsForAgent(threadAgentId, allTools);
278
+ console.log(`[Chat] ${filteredTools.length} total tools available to agent (${allMcpTools.length} MCP + ${nativeTools.length} native, filtered by agent config)`);
279
+ if (filteredTools.length > 0) {
280
+ console.log(`[Chat] Tools: ${JSON.stringify(filteredTools.map(t => ({ name: t.name, server: t.serverId, desc: t.description?.slice(0, 50) })), null, 2)}`);
281
+ }
282
+ // Build a lookup for tool metadata (to find UI resources and identify native tools)
283
+ const toolMetaLookup = new Map();
284
+ for (const tool of filteredTools) {
285
+ toolMetaLookup.set(tool.name, { serverId: tool.serverId, _meta: '_meta' in tool ? tool._meta : undefined });
286
+ }
287
+ // Stream response with tool loop
288
+ let fullContent = '';
289
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
290
+ const toolCalls = [];
291
+ try {
292
+ console.log(`[Chat] Starting stream with ${history.length} messages in history`);
293
+ // Send start event
294
+ res.write(`data: ${JSON.stringify({ type: 'start' })}\n\n`);
295
+ console.log(`[Chat] Stream started, receiving chunks...`);
296
+ // Agentic loop - continues until no more tool calls
297
+ let conversationMessages = [...history];
298
+ let loopCount = 0;
299
+ const maxLoops = 10; // Prevent infinite loops
300
+ while (loopCount < maxLoops) {
301
+ loopCount++;
302
+ let currentLoopText = '';
303
+ const currentLoopToolCalls = [];
304
+ let stopReason;
305
+ // Get system prompt from agent definition
306
+ const systemPrompt = agentDef && isBuiltInAgent(agentDef) ? agentDef.systemPrompt : undefined;
307
+ const stream = agentService.chat(conversationMessages, {
308
+ systemPrompt,
309
+ teamContext,
310
+ projectContext,
311
+ mentionContext,
312
+ userContext,
313
+ files: loopCount === 1 ? files : undefined, // Only include files on first iteration
314
+ tools: filteredTools.length > 0 ? filteredTools : undefined,
315
+ });
316
+ let chunkCount = 0;
317
+ for await (const chunk of stream) {
318
+ if (chunk.type === 'text' && chunk.content) {
319
+ currentLoopText += chunk.content;
320
+ fullContent += chunk.content;
321
+ res.write(`data: ${JSON.stringify({ type: 'delta', content: chunk.content })}\n\n`);
322
+ chunkCount++;
323
+ }
324
+ else if (chunk.type === 'tool_use' && chunk.toolUse) {
325
+ // Send tool_use event to frontend
326
+ console.log(`[Chat] Sending tool_use event: ${chunk.toolUse.name} (id: ${chunk.toolUse.id})`);
327
+ res.write(`data: ${JSON.stringify({
328
+ type: 'tool_use',
329
+ id: chunk.toolUse.id,
330
+ name: chunk.toolUse.name,
331
+ serverId: chunk.toolUse.serverId,
332
+ input: chunk.toolUse.input,
333
+ })}\n\n`);
334
+ currentLoopToolCalls.push({
335
+ id: chunk.toolUse.id,
336
+ name: chunk.toolUse.name,
337
+ serverId: chunk.toolUse.serverId,
338
+ input: chunk.toolUse.input,
339
+ });
340
+ }
341
+ else if (chunk.type === 'stop' && chunk.stopReason) {
342
+ stopReason = chunk.stopReason;
343
+ }
344
+ else if (chunk.type === 'usage' && chunk.usage) {
345
+ totalUsage.inputTokens += chunk.usage.inputTokens;
346
+ totalUsage.outputTokens += chunk.usage.outputTokens;
347
+ }
348
+ }
349
+ console.log(`[Chat] Loop ${loopCount}: ${chunkCount} text chunks, ${currentLoopToolCalls.length} tool calls, stop=${stopReason}`);
350
+ // If no tool calls or stop reason is not tool_use, we're done
351
+ if (currentLoopToolCalls.length === 0 || stopReason !== 'tool_use') {
352
+ break;
353
+ }
354
+ // Execute tool calls and collect results
355
+ const toolResultBlocks = [];
356
+ for (const toolCall of currentLoopToolCalls) {
357
+ console.log(`[Chat] Executing tool: ${toolCall.name} on server ${toolCall.serverId}`);
358
+ const isNativeToolCall = isNativeTool(toolCall.serverId);
359
+ // Find server config for user-credential support (not needed for native tools)
360
+ const serverConfig = isNativeToolCall ? undefined : mcpServers.find((s) => s.id === toolCall.serverId);
361
+ if (!isNativeToolCall && !serverConfig) {
362
+ console.error(`[Chat] Server config not found for ${toolCall.serverId}`);
363
+ continue;
364
+ }
365
+ const toolId = `${toolCall.serverId}:${toolCall.name}`;
366
+ // Check if tool confirmation is required
367
+ const confirmCheck = checkToolConfirmationRequired({
368
+ serverId: toolCall.serverId,
369
+ toolName: toolCall.name,
370
+ userId: req.user?.id || '',
371
+ userSettings: userContext || undefined,
372
+ threadAllowedTools,
373
+ });
374
+ let toolResult;
375
+ if (confirmCheck.required && req.user) {
376
+ // Tool requires confirmation - create pending and wait for user response
377
+ console.log(`[Chat] Tool ${toolId} requires confirmation`);
378
+ // Send pending confirmation event to frontend
379
+ const { id: confirmationId, promise: confirmationPromise } = pendingConfirmations.create({
380
+ visitorId,
381
+ threadId: thread.id,
382
+ userId: req.user.id,
383
+ serverId: toolCall.serverId,
384
+ toolName: toolCall.name,
385
+ toolArgs: toolCall.input,
386
+ });
387
+ res.write(`data: ${JSON.stringify({
388
+ type: 'tool_pending_confirmation',
389
+ confirmationId,
390
+ serverId: toolCall.serverId,
391
+ toolName: toolCall.name,
392
+ toolArgs: toolCall.input,
393
+ })}\n\n`);
394
+ // Wait for user response
395
+ const confirmationResult = await confirmationPromise;
396
+ // Send confirmation result event
397
+ res.write(`data: ${JSON.stringify({
398
+ type: 'tool_confirmed',
399
+ confirmationId,
400
+ approved: confirmationResult.approved,
401
+ })}\n\n`);
402
+ if (!confirmationResult.approved) {
403
+ // User denied - create denial result
404
+ console.log(`[Chat] User denied tool ${toolId}`);
405
+ toolResult = {
406
+ content: [{ type: 'text', text: createDenialToolResult(toolCall.name) }],
407
+ isError: false, // Not an error, just a denial
408
+ };
409
+ }
410
+ else {
411
+ // User approved - check scope and execute
412
+ if (confirmationResult.scope === 'thread') {
413
+ // Add to thread-level allowlist
414
+ threadAllowedTools.push(toolId);
415
+ console.log(`[Chat] Added ${toolId} to thread allowlist`);
416
+ }
417
+ // Note: 'always' scope is handled in the /confirm-tool endpoint
418
+ // Execute the tool
419
+ if (isNativeToolCall) {
420
+ toolResult = await executeNativeTool(toolCall.name, toolCall.input, {
421
+ userId: req.user?.id,
422
+ threadId: thread.id,
423
+ agentId: threadAgentId || undefined,
424
+ });
425
+ }
426
+ else {
427
+ toolResult = await mcpManager.callToolForUser(req.user?.id, toolCall.serverId, toolCall.name, toolCall.input, serverConfig);
428
+ }
429
+ }
430
+ }
431
+ else {
432
+ // Tool is auto-approved or no user context
433
+ if (confirmCheck.autoApproveReason && req.user) {
434
+ console.log(`[Chat] Tool ${toolId} auto-approved: ${confirmCheck.autoApproveReason}`);
435
+ res.write(`data: ${JSON.stringify({
436
+ type: 'tool_auto_approved',
437
+ toolCallId: toolCall.id,
438
+ serverId: toolCall.serverId,
439
+ toolName: toolCall.name,
440
+ reason: confirmCheck.autoApproveReason,
441
+ })}\n\n`);
442
+ }
443
+ // Execute the tool
444
+ if (isNativeToolCall) {
445
+ toolResult = await executeNativeTool(toolCall.name, toolCall.input, {
446
+ userId: req.user?.id,
447
+ threadId: thread.id,
448
+ agentId: threadAgentId || undefined,
449
+ });
450
+ }
451
+ else {
452
+ toolResult = await mcpManager.callToolForUser(req.user?.id, toolCall.serverId, toolCall.name, toolCall.input, serverConfig);
453
+ }
454
+ }
455
+ const result = toolResult;
456
+ // Log the full result from callToolForUser
457
+ console.log(`[Chat] Tool ${toolCall.name} result:`, {
458
+ contentLength: result.content?.length,
459
+ contentPreview: result.content?.map(c => c.text?.slice(0, 100) || c.type),
460
+ hasStructuredContent: !!result.structuredContent,
461
+ structuredContentKeys: result.structuredContent ? Object.keys(result.structuredContent) : null,
462
+ isError: result.isError,
463
+ });
464
+ // Check if tool has a UI resource template to fetch
465
+ const toolMeta = toolMetaLookup.get(toolCall.name);
466
+ console.log(`[Chat] Tool meta lookup for ${toolCall.name}:`, toolMeta ? { hasMetadata: !!toolMeta._meta, keys: toolMeta._meta ? Object.keys(toolMeta._meta) : [] } : 'NOT FOUND');
467
+ let uiResource = null;
468
+ let resourceUri;
469
+ let isOpenAiFormat = false;
470
+ // Check for native tool templates first
471
+ if (isNativeToolCall) {
472
+ const nativeTemplate = await resolveNativeToolTemplate(toolCall.name);
473
+ if (nativeTemplate) {
474
+ uiResource = nativeTemplate;
475
+ resourceUri = `native://${toolCall.name}/template`;
476
+ isOpenAiFormat = true; // Use OpenAI format for consistent rendering
477
+ console.log(`[Chat] Resolved native tool template for ${toolCall.name}, length=${nativeTemplate.text.length}`);
478
+ }
479
+ }
480
+ else {
481
+ // MCP tool - check for resource URI
482
+ const openaiOutputTemplate = toolMeta?._meta?.['openai/outputTemplate'];
483
+ const uiResourceUri = toolMeta?._meta?.['ui/resourceUri'];
484
+ resourceUri = (openaiOutputTemplate || uiResourceUri);
485
+ console.log(`[Chat] UI resource URI for ${toolCall.name}: ${resourceUri || 'none'} (openai: ${openaiOutputTemplate}, ui: ${uiResourceUri})`);
486
+ isOpenAiFormat = !!openaiOutputTemplate;
487
+ if (resourceUri && typeof resourceUri === 'string') {
488
+ console.log(`[Chat] Tool ${toolCall.name} has UI resource: ${resourceUri} (openai format: ${isOpenAiFormat})`);
489
+ uiResource = await mcpManager.readResourceForUser(req.user?.id || '', toolCall.serverId, resourceUri, serverConfig);
490
+ if (uiResource) {
491
+ console.log(`[Chat] Fetched UI resource: ${resourceUri}, mimeType=${uiResource.mimeType}, length=${uiResource.text?.length || uiResource.blob?.length || 0}`);
492
+ }
493
+ }
494
+ }
495
+ // Send tool_result event to frontend (include UI resource if available)
496
+ // Use structuredContent as toolOutput if available, otherwise fall back to content
497
+ const toolOutput = result.structuredContent || result.content;
498
+ const toolResultEvent = {
499
+ type: 'tool_result',
500
+ id: toolCall.id,
501
+ name: toolCall.name,
502
+ serverId: toolCall.serverId,
503
+ input: toolCall.input,
504
+ content: result.content,
505
+ isError: result.isError,
506
+ uiResource: uiResource ? {
507
+ uri: resourceUri,
508
+ text: uiResource.text,
509
+ blob: uiResource.blob,
510
+ mimeType: uiResource.mimeType,
511
+ isOpenAiFormat,
512
+ toolInput: toolCall.input,
513
+ toolOutput,
514
+ } : undefined,
515
+ };
516
+ console.log(`[Chat] Sending tool_result event:`, {
517
+ id: toolResultEvent.id,
518
+ hasContent: !!toolResultEvent.content?.length,
519
+ hasUiResource: !!toolResultEvent.uiResource,
520
+ uiResourceMimeType: toolResultEvent.uiResource?.mimeType,
521
+ uiResourceTextLength: toolResultEvent.uiResource?.text?.length,
522
+ });
523
+ res.write(`data: ${JSON.stringify(toolResultEvent)}\n\n`);
524
+ // Store for message history (including UI resource for persistence)
525
+ toolCalls.push({
526
+ ...toolCall,
527
+ result: result.content,
528
+ isError: result.isError,
529
+ uiResource: uiResource ? {
530
+ uri: resourceUri,
531
+ text: uiResource.text,
532
+ blob: uiResource.blob,
533
+ mimeType: uiResource.mimeType,
534
+ isOpenAiFormat,
535
+ toolInput: toolCall.input,
536
+ toolOutput,
537
+ } : undefined,
538
+ });
539
+ // Build tool result block for next iteration
540
+ // Include both content text and structuredContent for full context
541
+ let resultText = result.content
542
+ .map((c) => c.text || JSON.stringify(c))
543
+ .join('\n');
544
+ // Append structuredContent if available (this is the rich data the widget renders)
545
+ if (result.structuredContent) {
546
+ resultText += '\n\nThe user has been shown this structured data:\n' + JSON.stringify(result.structuredContent, null, 2);
547
+ }
548
+ console.log(`[Chat] Tool result text for agent (length=${resultText.length}):`, resultText.slice(0, 500) + (resultText.length > 500 ? '...' : ''));
549
+ toolResultBlocks.push({
550
+ type: 'tool_result',
551
+ tool_use_id: toolCall.id,
552
+ content: resultText,
553
+ is_error: result.isError,
554
+ });
555
+ }
556
+ // Build assistant message with tool uses for context
557
+ const assistantContent = [];
558
+ if (currentLoopText) {
559
+ assistantContent.push({ type: 'text', text: currentLoopText });
560
+ }
561
+ for (const tc of currentLoopToolCalls) {
562
+ assistantContent.push({
563
+ type: 'tool_use',
564
+ id: tc.id,
565
+ name: tc.name,
566
+ input: tc.input,
567
+ });
568
+ }
569
+ // Add assistant message with tool uses
570
+ conversationMessages.push({
571
+ role: 'assistant',
572
+ content: assistantContent,
573
+ });
574
+ // Add tool results as user message
575
+ conversationMessages.push({
576
+ role: 'user',
577
+ content: toolResultBlocks,
578
+ });
579
+ }
580
+ console.log(`[Chat] Stream complete after ${loopCount} loops: ${fullContent.length} chars, ${toolCalls.length} tool calls`);
581
+ // Create assistant message with tool calls
582
+ const assistantMessage = await db.message.create({
583
+ data: {
584
+ threadId: thread.id,
585
+ role: 'assistant',
586
+ content: fullContent,
587
+ inputTokens: totalUsage.inputTokens,
588
+ outputTokens: totalUsage.outputTokens,
589
+ toolCalls: toolCalls.length > 0 ? JSON.parse(JSON.stringify(toolCalls.map((tc) => ({
590
+ id: tc.id,
591
+ serverId: tc.serverId,
592
+ toolName: tc.name,
593
+ arguments: tc.input,
594
+ status: tc.isError ? 'error' : 'completed',
595
+ })))) : undefined,
596
+ toolResults: toolCalls.length > 0
597
+ ? JSON.parse(JSON.stringify(toolCalls.map((tc) => ({
598
+ toolCallId: tc.id,
599
+ content: tc.result,
600
+ isError: tc.isError,
601
+ uiResource: tc.uiResource,
602
+ }))))
603
+ : undefined,
604
+ },
605
+ });
606
+ // Update thread timestamp
607
+ await db.thread.update({
608
+ where: { id: thread.id },
609
+ data: { updatedAt: new Date() },
610
+ });
611
+ // Increment usage on the correct entity (user or team)
612
+ if (req.user) {
613
+ await incrementUsage(req.user.id, thread.teamId || undefined);
614
+ }
615
+ // Send done event
616
+ res.write(`data: ${JSON.stringify({
617
+ type: 'done',
618
+ messageId: assistantMessage.id,
619
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
620
+ usage: {
621
+ inputTokens: totalUsage.inputTokens,
622
+ outputTokens: totalUsage.outputTokens,
623
+ totalTokens: totalUsage.inputTokens + totalUsage.outputTokens,
624
+ },
625
+ })}\n\n`);
626
+ }
627
+ catch (error) {
628
+ console.error('Chat error:', error);
629
+ res.write(`data: ${JSON.stringify({
630
+ type: 'error',
631
+ error: error instanceof Error ? error.message : 'Unknown error',
632
+ })}\n\n`);
633
+ }
634
+ res.end();
635
+ }
636
+ catch (error) {
637
+ next(error);
638
+ }
639
+ });
640
+ // Regenerate assistant message
641
+ chatRouter.post('/regenerate/:messageId', requireAuth, async (req, res, next) => {
642
+ try {
643
+ const { messageId } = req.params;
644
+ const config = getConfig();
645
+ const message = await db.message.findUnique({
646
+ where: { id: messageId },
647
+ include: {
648
+ thread: {
649
+ include: {
650
+ messages: {
651
+ orderBy: { createdAt: 'asc' },
652
+ },
653
+ },
654
+ },
655
+ },
656
+ });
657
+ if (!message) {
658
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Message not found');
659
+ }
660
+ if (message.role !== 'assistant') {
661
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Can only regenerate assistant messages');
662
+ }
663
+ if (message.thread.userId !== req.user.id) {
664
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
665
+ }
666
+ // Check usage limits
667
+ const canSend = await checkUsageLimits(req.user.id);
668
+ if (!canSend) {
669
+ throw new AppError(HTTP_STATUS.TOO_MANY_REQUESTS, 'Usage limit exceeded');
670
+ }
671
+ // Set up SSE
672
+ res.setHeader('Content-Type', 'text/event-stream');
673
+ res.setHeader('Cache-Control', 'no-cache');
674
+ res.setHeader('Connection', 'keep-alive');
675
+ res.flushHeaders();
676
+ // Build history up to this message (excluding the message to regenerate)
677
+ const messageIndex = message.thread.messages.findIndex((m) => m.id === messageId);
678
+ const history = message.thread.messages.slice(0, messageIndex).map((msg) => ({
679
+ role: msg.role,
680
+ content: msg.content,
681
+ }));
682
+ // Get agent for this thread
683
+ const agentDef = getAgentById(message.thread.agentId);
684
+ const agentConfig = agentDef ? toAgentConfig(agentDef) : config.agent;
685
+ const agentService = createAgentService(agentConfig);
686
+ let fullContent = '';
687
+ let usage = { inputTokens: 0, outputTokens: 0 };
688
+ try {
689
+ // Get system prompt from agent definition
690
+ const systemPrompt = agentDef && isBuiltInAgent(agentDef) ? agentDef.systemPrompt : undefined;
691
+ const stream = await agentService.chat(history, {
692
+ systemPrompt,
693
+ });
694
+ res.write(`data: ${JSON.stringify({ type: 'start' })}\n\n`);
695
+ for await (const chunk of stream) {
696
+ if (chunk.type === 'text') {
697
+ fullContent += chunk.content;
698
+ res.write(`data: ${JSON.stringify({ type: 'delta', content: chunk.content })}\n\n`);
699
+ }
700
+ else if (chunk.type === 'usage' && chunk.usage) {
701
+ usage = chunk.usage;
702
+ }
703
+ }
704
+ // Update the message
705
+ await db.message.update({
706
+ where: { id: messageId },
707
+ data: {
708
+ content: fullContent,
709
+ inputTokens: usage.inputTokens,
710
+ outputTokens: usage.outputTokens,
711
+ },
712
+ });
713
+ // Increment usage on the correct entity (user or team)
714
+ await incrementUsage(req.user.id, message.thread.teamId || undefined);
715
+ res.write(`data: ${JSON.stringify({
716
+ type: 'done',
717
+ messageId,
718
+ usage: {
719
+ inputTokens: usage.inputTokens,
720
+ outputTokens: usage.outputTokens,
721
+ totalTokens: usage.inputTokens + usage.outputTokens,
722
+ },
723
+ })}\n\n`);
724
+ }
725
+ catch (error) {
726
+ res.write(`data: ${JSON.stringify({
727
+ type: 'error',
728
+ error: error instanceof Error ? error.message : 'Unknown error',
729
+ })}\n\n`);
730
+ }
731
+ res.end();
732
+ }
733
+ catch (error) {
734
+ next(error);
735
+ }
736
+ });
737
+ // Branch from message
738
+ chatRouter.post('/branch/:messageId', requireAuth, async (req, res, next) => {
739
+ try {
740
+ const { messageId } = req.params;
741
+ const message = await db.message.findUnique({
742
+ where: { id: messageId },
743
+ include: {
744
+ thread: {
745
+ include: {
746
+ messages: {
747
+ orderBy: { createdAt: 'asc' },
748
+ },
749
+ },
750
+ },
751
+ },
752
+ });
753
+ if (!message) {
754
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Message not found');
755
+ }
756
+ if (message.thread.userId !== req.user.id) {
757
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
758
+ }
759
+ // Create new thread as branch, inheriting the agent from parent
760
+ const newThread = await db.thread.create({
761
+ data: {
762
+ title: `Branch from: ${message.thread.title}`,
763
+ userId: req.user.id,
764
+ agentId: message.thread.agentId,
765
+ parentThreadId: message.thread.id,
766
+ parentMessageId: messageId,
767
+ },
768
+ });
769
+ // Copy messages up to and including the branch point
770
+ const messageIndex = message.thread.messages.findIndex((m) => m.id === messageId);
771
+ const messagesToCopy = message.thread.messages.slice(0, messageIndex + 1);
772
+ for (const msg of messagesToCopy) {
773
+ await db.message.create({
774
+ data: {
775
+ threadId: newThread.id,
776
+ role: msg.role,
777
+ content: msg.content,
778
+ files: msg.files || undefined,
779
+ },
780
+ });
781
+ }
782
+ // Note: We don't add the new user message here - the frontend will send it
783
+ // via the normal chat endpoint, which triggers the AI response
784
+ // Fetch the thread with all messages for the response
785
+ const threadWithMessages = await db.thread.findUnique({
786
+ where: { id: newThread.id },
787
+ include: {
788
+ messages: {
789
+ orderBy: { createdAt: 'asc' },
790
+ },
791
+ },
792
+ });
793
+ // Get agent info
794
+ const agent = getAgentById(newThread.agentId);
795
+ res.status(HTTP_STATUS.CREATED).json({
796
+ thread: {
797
+ ...threadWithMessages,
798
+ agentName: agent?.name,
799
+ },
800
+ });
801
+ }
802
+ catch (error) {
803
+ next(error);
804
+ }
805
+ });
806
+ // Submit feedback for a message
807
+ chatRouter.post('/feedback/:messageId', requireAuth, async (req, res, next) => {
808
+ try {
809
+ const { messageId } = req.params;
810
+ const { type, comment } = req.body;
811
+ if (!['up', 'down'].includes(type)) {
812
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Invalid feedback type');
813
+ }
814
+ const message = await db.message.findUnique({
815
+ where: { id: messageId },
816
+ include: { thread: true },
817
+ });
818
+ if (!message) {
819
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Message not found');
820
+ }
821
+ // Upsert feedback
822
+ const feedback = await db.messageFeedback.upsert({
823
+ where: {
824
+ messageId_userId: {
825
+ messageId,
826
+ userId: req.user.id,
827
+ },
828
+ },
829
+ update: { type, comment },
830
+ create: {
831
+ messageId,
832
+ userId: req.user.id,
833
+ type,
834
+ comment,
835
+ },
836
+ });
837
+ // Send Slack notification for positive feedback on team threads
838
+ if (type === 'up' && message.thread.teamId) {
839
+ notifyMessageLiked(message.thread.teamId, req.user.name, req.user.email, message.thread.title).catch(err => console.error('[Feedback] Slack notification failed:', err));
840
+ }
841
+ res.json({ feedback });
842
+ }
843
+ catch (error) {
844
+ next(error);
845
+ }
846
+ });
847
+ // Confirm or deny a tool call
848
+ chatRouter.post('/confirm-tool', requireAuth, async (req, res, next) => {
849
+ try {
850
+ const { confirmationId, approved, scope } = req.body;
851
+ if (!confirmationId) {
852
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'confirmationId is required');
853
+ }
854
+ if (typeof approved !== 'boolean') {
855
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'approved must be a boolean');
856
+ }
857
+ const pending = pendingConfirmations.get(confirmationId);
858
+ if (!pending) {
859
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Confirmation not found or expired');
860
+ }
861
+ // Verify the user owns this confirmation
862
+ if (pending.userId !== req.user.id) {
863
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Not authorized to confirm this tool call');
864
+ }
865
+ // If "always allow", update user settings
866
+ if (approved && scope === 'always') {
867
+ const toolId = `${pending.serverId}:${pending.toolName}`;
868
+ const user = await db.user.findUnique({
869
+ where: { id: req.user.id },
870
+ select: { settings: true },
871
+ });
872
+ const currentSettings = user?.settings || {};
873
+ const allowedTools = currentSettings.allowedTools || [];
874
+ // Only add if not already present
875
+ if (!allowedTools.includes(toolId)) {
876
+ await db.user.update({
877
+ where: { id: req.user.id },
878
+ data: {
879
+ settings: {
880
+ ...currentSettings,
881
+ allowedTools: [...allowedTools, toolId],
882
+ },
883
+ },
884
+ });
885
+ console.log(`[ConfirmTool] Added ${toolId} to user ${req.user.id}'s allowedTools`);
886
+ }
887
+ }
888
+ // Resolve the pending confirmation
889
+ const resolved = pendingConfirmations.resolve(confirmationId, approved, scope);
890
+ if (!resolved) {
891
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Failed to resolve confirmation');
892
+ }
893
+ console.log(`[ConfirmTool] User ${req.user.id} ${approved ? 'approved' : 'denied'} tool ${pending.serverId}:${pending.toolName} (scope: ${scope || 'once'})`);
894
+ res.json({ success: true });
895
+ }
896
+ catch (error) {
897
+ next(error);
898
+ }
899
+ });
900
+ //# sourceMappingURL=chat.js.map