@xalia/agent 0.5.7 → 0.6.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 (186) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +176 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -59
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/mcpServerManager.js +23 -24
  8. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  9. package/dist/agent/src/agent/nullPlatform.js +14 -0
  10. package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
  11. package/dist/agent/src/agent/promptProvider.js +63 -0
  12. package/dist/agent/src/agent/repeatLLM.js +5 -5
  13. package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
  14. package/dist/agent/src/agent/tokenAuth.js +7 -7
  15. package/dist/agent/src/agent/tools.js +1 -1
  16. package/dist/agent/src/chat/client/chatClient.js +733 -0
  17. package/dist/agent/src/chat/client/connection.js +209 -0
  18. package/dist/agent/src/chat/client/connection.test.js +188 -0
  19. package/dist/agent/src/chat/client/constants.js +5 -0
  20. package/dist/agent/src/chat/client/index.js +15 -0
  21. package/dist/agent/src/chat/client/interfaces.js +2 -0
  22. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  23. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  24. package/dist/agent/src/chat/client/teamManager.js +2 -0
  25. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  26. package/dist/agent/src/chat/data/dataModels.js +2 -0
  27. package/dist/agent/src/chat/data/database.js +749 -0
  28. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  29. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  30. package/dist/agent/src/chat/protocol/constants.js +50 -0
  31. package/dist/agent/src/chat/protocol/errors.js +22 -0
  32. package/dist/agent/src/chat/protocol/messages.js +110 -0
  33. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  34. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  35. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  36. package/dist/agent/src/chat/server/conversation.js +198 -0
  37. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  38. package/dist/agent/src/chat/server/openSession.js +869 -0
  39. package/dist/agent/src/chat/server/server.js +177 -0
  40. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  41. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  43. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  44. package/dist/agent/src/chat/server/tools.js +243 -0
  45. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  46. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  47. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  48. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  49. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  50. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  51. package/dist/agent/src/chat/utils/search.js +145 -0
  52. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  53. package/dist/agent/src/chat/utils/websocket.js +16 -0
  54. package/dist/agent/src/test/agent.test.js +332 -0
  55. package/dist/agent/src/test/approvalManager.test.js +58 -0
  56. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  57. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  58. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  59. package/dist/agent/src/test/context.test.js +83 -0
  60. package/dist/agent/src/test/conversation.test.js +89 -0
  61. package/dist/agent/src/test/db.test.js +271 -83
  62. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  63. package/dist/agent/src/test/dbTestTools.js +99 -0
  64. package/dist/agent/src/test/imageLoad.test.js +8 -7
  65. package/dist/agent/src/test/mcpServerManager.test.js +23 -20
  66. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  67. package/dist/agent/src/test/openaiStreaming.test.js +64 -35
  68. package/dist/agent/src/test/prompt.test.js +5 -4
  69. package/dist/agent/src/test/promptProvider.test.js +28 -0
  70. package/dist/agent/src/test/responseHandler.test.js +61 -0
  71. package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
  72. package/dist/agent/src/test/testTools.js +109 -0
  73. package/dist/agent/src/test/tools.test.js +31 -0
  74. package/dist/agent/src/tool/agentChat.js +21 -10
  75. package/dist/agent/src/tool/agentMain.js +1 -1
  76. package/dist/agent/src/tool/chatMain.js +241 -58
  77. package/dist/agent/src/tool/commandPrompt.js +22 -17
  78. package/dist/agent/src/tool/files.js +20 -16
  79. package/dist/agent/src/tool/nodePlatform.js +47 -3
  80. package/dist/agent/src/tool/options.js +4 -4
  81. package/dist/agent/src/tool/prompt.js +19 -13
  82. package/eslint.config.mjs +14 -1
  83. package/package.json +14 -6
  84. package/scripts/chat_server +8 -0
  85. package/scripts/setup_chat +7 -2
  86. package/scripts/shutdown_chat_server +3 -0
  87. package/scripts/test_chat +135 -17
  88. package/src/agent/agent.ts +283 -138
  89. package/src/agent/agentUtils.ts +143 -108
  90. package/src/agent/compressingContextManager.ts +164 -0
  91. package/src/agent/context.ts +268 -0
  92. package/src/agent/dummyLLM.ts +76 -8
  93. package/src/agent/iAgentEventHandler.ts +54 -0
  94. package/src/agent/iplatform.ts +1 -0
  95. package/src/agent/mcpServerManager.ts +35 -31
  96. package/src/agent/nullAgentEventHandler.ts +20 -0
  97. package/src/agent/nullPlatform.ts +13 -0
  98. package/src/agent/openAILLMStreaming.ts +26 -13
  99. package/src/agent/promptProvider.ts +87 -0
  100. package/src/agent/repeatLLM.ts +5 -5
  101. package/src/agent/sudoMcpServerManager.ts +30 -29
  102. package/src/agent/tokenAuth.ts +7 -7
  103. package/src/agent/tools.ts +3 -1
  104. package/src/chat/client/chatClient.ts +900 -0
  105. package/src/chat/client/connection.test.ts +241 -0
  106. package/src/chat/client/connection.ts +276 -0
  107. package/src/chat/client/constants.ts +3 -0
  108. package/src/chat/client/index.ts +18 -0
  109. package/src/chat/client/interfaces.ts +34 -0
  110. package/src/chat/client/responseHandler.ts +131 -0
  111. package/src/chat/client/sessionClient.ts +443 -0
  112. package/src/chat/client/teamManager.ts +29 -0
  113. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  114. package/src/chat/data/dataModels.ts +85 -0
  115. package/src/chat/data/database.ts +982 -0
  116. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  117. package/src/chat/protocol/connectionMessages.ts +49 -0
  118. package/src/chat/protocol/constants.ts +55 -0
  119. package/src/chat/protocol/errors.ts +16 -0
  120. package/src/chat/protocol/messages.ts +682 -0
  121. package/src/chat/server/README.md +127 -0
  122. package/src/chat/server/chatContextManager.ts +612 -0
  123. package/src/chat/server/connectionManager.test.ts +266 -0
  124. package/src/chat/server/connectionManager.ts +541 -0
  125. package/src/chat/server/conversation.ts +269 -0
  126. package/src/chat/server/errorUtils.ts +28 -0
  127. package/src/chat/server/openSession.ts +1332 -0
  128. package/src/chat/server/server.ts +177 -0
  129. package/src/chat/server/sessionFileManager.ts +239 -0
  130. package/src/chat/server/sessionRegistry.test.ts +138 -0
  131. package/src/chat/server/sessionRegistry.ts +1064 -0
  132. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  133. package/src/chat/server/tools.ts +265 -0
  134. package/src/chat/utils/agentSessionMap.ts +76 -0
  135. package/src/chat/utils/approvalManager.ts +111 -0
  136. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  137. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  138. package/src/chat/utils/htmlToText.ts +61 -0
  139. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  140. package/src/chat/utils/search.ts +139 -0
  141. package/src/chat/utils/userResolver.ts +48 -0
  142. package/src/chat/utils/websocket.ts +16 -0
  143. package/src/test/agent.test.ts +487 -0
  144. package/src/test/approvalManager.test.ts +73 -0
  145. package/src/test/chatContextManager.test.ts +521 -0
  146. package/src/test/clientServerConnection.test.ts +207 -0
  147. package/src/test/compressingContextManager.test.ts +82 -0
  148. package/src/test/context.test.ts +105 -0
  149. package/src/test/conversation.test.ts +109 -0
  150. package/src/test/db.test.ts +358 -89
  151. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  152. package/src/test/dbTestTools.ts +153 -0
  153. package/src/test/imageLoad.test.ts +7 -6
  154. package/src/test/mcpServerManager.test.ts +21 -16
  155. package/src/test/multiAsyncQueue.test.ts +125 -0
  156. package/src/test/openaiStreaming.test.ts +71 -36
  157. package/src/test/prompt.test.ts +4 -3
  158. package/src/test/promptProvider.test.ts +33 -0
  159. package/src/test/responseHandler.test.ts +78 -0
  160. package/src/test/sudoMcpServerManager.test.ts +32 -30
  161. package/src/test/testTools.ts +146 -0
  162. package/src/test/tools.test.ts +39 -0
  163. package/src/tool/agentChat.ts +26 -12
  164. package/src/tool/agentMain.ts +1 -1
  165. package/src/tool/chatMain.ts +292 -100
  166. package/src/tool/commandPrompt.ts +28 -19
  167. package/src/tool/files.ts +25 -19
  168. package/src/tool/nodePlatform.ts +52 -3
  169. package/src/tool/options.ts +4 -2
  170. package/src/tool/prompt.ts +22 -15
  171. package/test_data/dummyllm_script_crash.json +32 -0
  172. package/test_data/frog.png.b64 +1 -0
  173. package/vitest.config.ts +39 -0
  174. package/dist/agent/src/chat/client.js +0 -349
  175. package/dist/agent/src/chat/conversationManager.js +0 -392
  176. package/dist/agent/src/chat/db.js +0 -209
  177. package/dist/agent/src/chat/frontendClient.js +0 -74
  178. package/dist/agent/src/chat/server.js +0 -158
  179. package/src/chat/client.ts +0 -455
  180. package/src/chat/conversationManager.ts +0 -595
  181. package/src/chat/db.ts +0 -290
  182. package/src/chat/frontendClient.ts +0 -123
  183. package/src/chat/messages.ts +0 -235
  184. package/src/chat/server.ts +0 -177
  185. /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
  186. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,982 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
+
5
+ import {
6
+ TeamParticipant,
7
+ TeamRole,
8
+ SessionParticipantMap,
9
+ SessionData,
10
+ TeamInfo,
11
+ SessionMessage,
12
+ SessionCheckpoint,
13
+ UserMessageData,
14
+ SessionCreateData,
15
+ } from "./dataModels";
16
+ import { createClient, SupabaseClient } from "@supabase/supabase-js";
17
+ import type * as supabase from "../../../../supabase/database.types";
18
+ import {
19
+ AgentPreferences,
20
+ AgentProfile,
21
+ AgentTemplate,
22
+ ApiKey,
23
+ getLogger,
24
+ SavedAgentProfile,
25
+ } from "@xalia/xmcp/sdk";
26
+
27
+ const logger = getLogger();
28
+
29
+ export const SESSION_ALLOWED_PARTICIPANTS = ["owner", "participant"];
30
+
31
+ export const SUPABASE_LOCAL_URL = "http://127.0.0.1:54321";
32
+ export const SUPABASE_LOCAL_KEY =
33
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiw" +
34
+ "icm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJz" +
35
+ "dJsyH-qQwv8Hdp7fsn3W0YpN81IU";
36
+
37
+ export function createSessionParticipantMap(
38
+ participants: TeamParticipant[]
39
+ ): SessionParticipantMap {
40
+ const pmap: SessionParticipantMap = new Map();
41
+ participants.forEach((p) => {
42
+ pmap.set(p.user_uuid, p);
43
+ });
44
+
45
+ return pmap;
46
+ }
47
+
48
+ /**
49
+ * 'name' -> 'name'
50
+ * 'space/name' -> ['space', 'name']
51
+ */
52
+ export function resolveCompoundName(name: string): string | [string, string] {
53
+ const components = name.split("/");
54
+ if (components.length === 1) {
55
+ return name;
56
+ }
57
+ if (components.length !== 2) {
58
+ throw new Error("invalid compound name");
59
+ }
60
+ return components as [string, string];
61
+ }
62
+
63
+ export type UserData = {
64
+ uuid: supabase.Tables<"api_keys">["user_uuid"];
65
+ nickname: supabase.Tables<"users">["nickname"];
66
+ email: supabase.Tables<"users">["email"];
67
+ timezone: supabase.Tables<"users">["timezone"];
68
+ };
69
+
70
+ export class DbClientBase {
71
+ constructor(protected client: SupabaseClient) {}
72
+ }
73
+
74
+ export class Database {
75
+ private client: SupabaseClient;
76
+
77
+ constructor(supabaseUrl: string, supabaseKey: string) {
78
+ this.client = createClient(supabaseUrl, supabaseKey);
79
+ }
80
+
81
+ /**
82
+ * Get the underlying Supabase client for testing purposes only.
83
+ * DO NOT use in production code - use Database methods instead.
84
+ */
85
+ getClientForTesting(): SupabaseClient {
86
+ return this.client;
87
+ }
88
+
89
+ async getUserDataFromApiKey(apiKey: string): Promise<UserData | undefined> {
90
+ const { data, error } = await this.client
91
+ .from("api_keys")
92
+ .select("user_uuid, users ( nickname, timezone, email )")
93
+ .eq("api_key", apiKey)
94
+ .maybeSingle<{
95
+ user_uuid: supabase.Tables<"api_keys">["user_uuid"];
96
+ users: {
97
+ nickname: supabase.Tables<"users">["nickname"];
98
+ timezone: supabase.Tables<"users">["timezone"];
99
+ email: supabase.Tables<"users">["email"];
100
+ };
101
+ }>();
102
+
103
+ logger.debug(
104
+ `[getUserDataFromApiKey]: got ${JSON.stringify({ data, error })}`
105
+ );
106
+
107
+ if (error) {
108
+ throw error;
109
+ }
110
+
111
+ if (data === null) {
112
+ return undefined;
113
+ }
114
+
115
+ return {
116
+ uuid: data.user_uuid,
117
+ email: data.users.email,
118
+ nickname: data.users.nickname || `user ${data.user_uuid}`,
119
+ timezone: data.users.timezone || "UTC",
120
+ };
121
+ }
122
+
123
+ async getUserFromUuid(user_uuid: string): Promise<UserData | undefined> {
124
+ const { data, error } = await this.client
125
+ .from("users")
126
+ .select("*")
127
+ .eq("uuid", user_uuid)
128
+ .maybeSingle();
129
+ if (error) {
130
+ throw error;
131
+ }
132
+
133
+ return data;
134
+ }
135
+
136
+ async getUserByEmail(email: string): Promise<UserData | undefined> {
137
+ const { data, error } = await this.client
138
+ .from("users")
139
+ .select("*")
140
+ .eq("email", email)
141
+ .maybeSingle();
142
+ if (error) {
143
+ throw error;
144
+ }
145
+
146
+ return data;
147
+ }
148
+
149
+ async createUser(
150
+ user_uuid: string,
151
+ email: string,
152
+ nickname: string,
153
+ timezone?: string
154
+ ): Promise<void> {
155
+ const payload: supabase.TablesInsert<"users"> = {
156
+ uuid: user_uuid,
157
+ email,
158
+ nickname,
159
+ timezone: timezone || "UTC",
160
+ };
161
+
162
+ const { error } = await this.client.from("users").insert(payload);
163
+ if (error) {
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ async addApiKey(
169
+ user_uuid: string,
170
+ api_key: string,
171
+ name: string,
172
+ scopes: string[],
173
+ is_default: boolean = false
174
+ ): Promise<ApiKey> {
175
+ const payload: supabase.TablesInsert<"api_keys"> = {
176
+ user_uuid,
177
+ api_key,
178
+ name,
179
+ scopes,
180
+ is_default,
181
+ };
182
+ const { data, error } = await this.client
183
+ .from("api_keys")
184
+ .insert(payload)
185
+ .select("*")
186
+ .maybeSingle();
187
+ if (error) {
188
+ throw error;
189
+ }
190
+
191
+ return data;
192
+ }
193
+
194
+ /**
195
+ * Get user's default API key from user UUID
196
+ * NOTE: this should only be used for accessing
197
+ * the session owner's API key.
198
+ * */
199
+ async getUserApiKey(user_uuid: string): Promise<string | undefined> {
200
+ const { data, error } = await this.client
201
+ .from("api_keys")
202
+ .select("api_key")
203
+ .eq("user_uuid", user_uuid)
204
+ .eq("is_default", true)
205
+ .maybeSingle();
206
+
207
+ if (error) {
208
+ throw error;
209
+ }
210
+ return data?.api_key;
211
+ }
212
+
213
+ async getSavedAgentProfileById(
214
+ agentProfileId: string
215
+ ): Promise<SavedAgentProfile | undefined> {
216
+ const { data, error } = await this.client
217
+ .from("agent_profiles")
218
+ .select("*")
219
+ .eq("uuid", agentProfileId)
220
+ .maybeSingle<supabase.Tables<"agent_profiles">>();
221
+ if (error) {
222
+ throw error;
223
+ }
224
+
225
+ return data
226
+ ? SavedAgentProfile.fromJSONObj(data as Record<string, unknown>)
227
+ : undefined;
228
+ }
229
+
230
+ async getSavedAgentProfileByName(
231
+ user_uuid: string,
232
+ agentProfileName: string
233
+ ): Promise<SavedAgentProfile | undefined> {
234
+ const { data, error } = await this.client
235
+ .from("agent_profiles")
236
+ .select("*")
237
+ .eq("user_uuid", user_uuid)
238
+ .eq("profile_name", agentProfileName)
239
+ .maybeSingle<supabase.Tables<"agent_profiles">>();
240
+ if (error) {
241
+ throw error;
242
+ }
243
+
244
+ return data
245
+ ? SavedAgentProfile.fromJSONObj(data as Record<string, unknown>)
246
+ : undefined;
247
+ }
248
+
249
+ async getAgentProfileById(
250
+ agentProfileId: string
251
+ ): Promise<AgentProfile | undefined> {
252
+ const { data, error } = await this.client
253
+ .from("agent_profiles")
254
+ .select("profile")
255
+ .eq("uuid", agentProfileId)
256
+ .maybeSingle<{
257
+ profile: supabase.Tables<"agent_profiles">["profile"];
258
+ }>();
259
+ if (error) {
260
+ throw error;
261
+ }
262
+ return data
263
+ ? AgentProfile.fromJSONObj(data.profile as Record<string, unknown>)
264
+ : undefined;
265
+ }
266
+
267
+ async createAgentProfile(
268
+ user_uuid: string | undefined,
269
+ team_uuid: string | undefined,
270
+ profileName: string,
271
+ profile: AgentProfile
272
+ ): Promise<SavedAgentProfile | undefined> {
273
+ const payload: supabase.TablesInsert<"agent_profiles"> = {
274
+ profile: profile as unknown as supabase.Json,
275
+ user_uuid,
276
+ team_uuid,
277
+ profile_name: profileName,
278
+ };
279
+ const { data, error } = await this.client
280
+ .from("agent_profiles")
281
+ .upsert(payload)
282
+ .select("*");
283
+ if (error) {
284
+ throw error;
285
+ }
286
+
287
+ return data[0]
288
+ ? SavedAgentProfile.fromJSONObj(data[0] as Record<string, unknown>)
289
+ : undefined;
290
+ }
291
+
292
+ async updateAgentProfile(uuid: string, profile: AgentProfile): Promise<void> {
293
+ const payload: supabase.TablesUpdate<"agent_profiles"> = {
294
+ profile: profile as unknown as supabase.Json,
295
+ };
296
+ const { error } = await this.client
297
+ .from("agent_profiles")
298
+ .update(payload)
299
+ .eq("uuid", uuid);
300
+ if (error) {
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ async getAgentProfilePreferences(
306
+ agentProfileId: string
307
+ ): Promise<AgentPreferences | undefined> {
308
+ const { data, error } = await this.client
309
+ .from("agent_profiles")
310
+ .select("preferences")
311
+ .eq("uuid", agentProfileId)
312
+ .maybeSingle<{
313
+ preferences: supabase.Tables<"agent_profiles">["preferences"];
314
+ }>();
315
+ if (error) {
316
+ throw error;
317
+ }
318
+
319
+ return data ? (data.preferences as AgentPreferences) : undefined;
320
+ }
321
+
322
+ async updateAgentProfilePreferences(
323
+ agentProfileId: string,
324
+ preferences: AgentPreferences
325
+ ): Promise<void> {
326
+ const payload: supabase.TablesUpdate<"agent_profiles"> = { preferences };
327
+ const { error } = await this.client
328
+ .from("agent_profiles")
329
+ .update(payload)
330
+ .eq("uuid", agentProfileId);
331
+ if (error) {
332
+ throw error;
333
+ }
334
+ }
335
+
336
+ async clearAgentProfiles(): Promise<void> {
337
+ await this.client.from("agent_profiles").delete().neq("uuid", "");
338
+ }
339
+
340
+ //
341
+ // sessions
342
+ //
343
+
344
+ static sessionNewUUID(): string {
345
+ const bytes = new Uint8Array(16);
346
+ crypto.getRandomValues(bytes);
347
+
348
+ return Array.from(bytes)
349
+ .map((b) => b.toString(16).padStart(2, "0"))
350
+ .join("")
351
+ .padStart(32, "0"); // in case of leading zeros
352
+ }
353
+
354
+ async sessionsGet(): Promise<supabase.Tables<"sessions">[]> {
355
+ const { data, error } = await this.client.from("sessions").select("*");
356
+
357
+ if (error) {
358
+ throw error;
359
+ }
360
+ return data;
361
+ }
362
+
363
+ async sessionGetById(session_uuid: string): Promise<SessionData | undefined> {
364
+ const { data, error } = await this.client
365
+ .from("sessions")
366
+ .select("*")
367
+ .eq("uuid", session_uuid)
368
+ .maybeSingle();
369
+
370
+ if (error) {
371
+ throw error;
372
+ }
373
+
374
+ if (!data) {
375
+ return undefined;
376
+ }
377
+
378
+ return {
379
+ ...data,
380
+ workspace: data.workspace || undefined,
381
+ owner_uuid: data.user_uuid,
382
+ session_uuid: data.uuid,
383
+ updated_at: data.updated_at || new Date().toISOString(),
384
+ };
385
+ }
386
+
387
+ async sessionGetByName(
388
+ user_uuid: string,
389
+ session_name: string
390
+ ): Promise<SessionData | undefined> {
391
+ const { data, error } = await this.client
392
+ .from("sessions")
393
+ .select("*")
394
+ .eq("user_uuid", user_uuid)
395
+ .eq("title", session_name)
396
+ .maybeSingle();
397
+
398
+ if (error) {
399
+ logger.error(`[getSessionByName] error: ${JSON.stringify(error)}`);
400
+ throw error;
401
+ }
402
+ return {
403
+ ...data,
404
+ workspace: data.workspace || undefined,
405
+ owner_uuid: data.user_uuid,
406
+ session_uuid: data.uuid,
407
+ updated_at: data.updated_at || new Date().toISOString(),
408
+ };
409
+ }
410
+
411
+ async sessionCreate(session_data: SessionCreateData): Promise<void> {
412
+ const payload: supabase.TablesInsert<"sessions"> = {
413
+ uuid: session_data.session_uuid,
414
+ title: session_data.title,
415
+ agent_profile_uuid: session_data.agent_profile_uuid,
416
+ user_uuid: session_data.user_uuid,
417
+ team_uuid: session_data.team_uuid,
418
+ workspace: session_data.workspace,
419
+ };
420
+ const { error } = await this.client.from("sessions").insert(payload);
421
+ if (error) {
422
+ throw error;
423
+ }
424
+ }
425
+
426
+ async sessionUpdateTitle(session_uuid: string, title: string): Promise<void> {
427
+ const payload: supabase.TablesUpdate<"sessions"> = {
428
+ title,
429
+ };
430
+ const { error } = await this.client
431
+ .from("sessions")
432
+ .update(payload)
433
+ .eq("uuid", session_uuid);
434
+ if (error) {
435
+ throw error;
436
+ }
437
+ }
438
+
439
+ async sessionUpdateWorkspace(
440
+ session_uuid: string,
441
+ workspace: UserMessageData | undefined
442
+ ): Promise<void> {
443
+ const payload: supabase.TablesUpdate<"sessions"> = {
444
+ workspace: workspace || null,
445
+ };
446
+ const { error } = await this.client
447
+ .from("sessions")
448
+ .update(payload)
449
+ .eq("uuid", session_uuid);
450
+ if (error) {
451
+ throw error;
452
+ }
453
+ }
454
+
455
+ async sessionDeleteById(session_uuid: string): Promise<void> {
456
+ await this.client.from("sessions").delete().eq("uuid", session_uuid);
457
+ }
458
+
459
+ async clearSessions(): Promise<void> {
460
+ await this.client.from("sessions").delete().neq("uuid", "");
461
+ }
462
+
463
+ /**
464
+ * Get all user sessions (not including team sessions) for a user.
465
+ * @param user_uuid
466
+ * @returns SessionData[]
467
+ */
468
+ async getUserSessions(user_uuid: string): Promise<SessionData[]> {
469
+ const { data: userSessions, error: userSessionsError } = await this.client
470
+ .from("sessions")
471
+ .select(
472
+ "uuid, title, agent_profile_uuid, workspace, updated_at, user_uuid"
473
+ )
474
+ .eq("user_uuid", user_uuid)
475
+ .is("team_uuid", null);
476
+
477
+ if (userSessionsError) {
478
+ throw userSessionsError;
479
+ }
480
+
481
+ return userSessions.map((s) => ({
482
+ session_uuid: s.uuid,
483
+ title: s.title,
484
+ team_uuid: undefined,
485
+ agent_profile_uuid: s.agent_profile_uuid,
486
+ workspace: s.workspace || undefined,
487
+ updated_at: s.updated_at,
488
+ user_uuid: s.user_uuid,
489
+ }));
490
+ }
491
+
492
+ async sessionGetParticipants(
493
+ session_uuid: string
494
+ ): Promise<TeamParticipant[]> {
495
+ // check if session is a team session
496
+ const { data, error } = await this.client
497
+ .from("sessions")
498
+ .select("team_uuid, user_uuid")
499
+ .eq("uuid", session_uuid)
500
+ .maybeSingle();
501
+
502
+ if (error || !data) {
503
+ throw error || new Error("Session not found");
504
+ }
505
+
506
+ if (data.team_uuid) {
507
+ return this.teamGetMembers(data.team_uuid as string);
508
+ } else {
509
+ const userData = await this.getUserFromUuid(data.user_uuid as string);
510
+ if (!userData) {
511
+ throw new Error("Cannot find user data");
512
+ }
513
+ return [
514
+ {
515
+ user_uuid: data.user_uuid,
516
+ nickname: userData.nickname || "",
517
+ email: userData.email,
518
+ role: "owner",
519
+ },
520
+ ];
521
+ }
522
+ }
523
+
524
+ async sessionGetTeamUuid(session_uuid: string): Promise<string | undefined> {
525
+ const data = await this.sessionGetById(session_uuid);
526
+ if (data) {
527
+ return data.team_uuid;
528
+ }
529
+ return undefined;
530
+ }
531
+
532
+ //
533
+ // session_messages
534
+ //
535
+
536
+ async sessionMessagesClearConversation(session_uuid: string): Promise<void> {
537
+ const { error } = await this.client
538
+ .from("session_messages")
539
+ .delete()
540
+ .eq("session_uuid", session_uuid);
541
+ if (error) {
542
+ throw error;
543
+ }
544
+ }
545
+
546
+ async sessionMessagesGetConversation(
547
+ session_uuid: string,
548
+ numEntries: number,
549
+ beforeIndex?: number
550
+ ): Promise<SessionMessage[]> {
551
+ // Query all message for the given session, ordered high-to-low by
552
+ // message_idx, limited to `numEntries`. If `beforeIndex` is given, it
553
+ // means we get messages with `message_idx < beforeIndex`
554
+
555
+ let query = this.client
556
+ .from("session_messages")
557
+ .select("message_idx,sender_uuid,is_for_llm,content")
558
+ .eq("session_uuid", session_uuid);
559
+ if (beforeIndex) {
560
+ query = query.lt("message_idx", beforeIndex);
561
+ }
562
+ query = query.order("message_idx", { ascending: false }).limit(numEntries);
563
+
564
+ const { data, error } = await query;
565
+ if (error) {
566
+ throw error;
567
+ }
568
+
569
+ // To get the newest N messages, we've orded by index largest to smallest
570
+ // (newest first), but caller wants the message first to last, hence
571
+ // reverse the array.
572
+
573
+ return data
574
+ .map(({ sender_uuid, ...rest }) => {
575
+ return sender_uuid
576
+ ? {
577
+ sender_uuid,
578
+ ...rest,
579
+ }
580
+ : { ...rest };
581
+ })
582
+ .reverse();
583
+ }
584
+
585
+ async sessionMessagesAppend(
586
+ session_uuid: string,
587
+ messages: SessionMessage[]
588
+ ): Promise<void> {
589
+ const payload = messages.map((m) => {
590
+ return { ...m, session_uuid };
591
+ });
592
+ const { error } = await this.client
593
+ .from("session_messages")
594
+ .insert(payload);
595
+ if (error) {
596
+ throw error;
597
+ }
598
+ }
599
+
600
+ //
601
+ // session_checkpoints
602
+ //
603
+
604
+ async sessionCheckpointsClear(): Promise<void> {
605
+ const { error } = await this.client
606
+ .from("session_checkpoints")
607
+ .delete()
608
+ .neq("session_uuid", "");
609
+ if (error) {
610
+ throw error;
611
+ }
612
+ }
613
+
614
+ async sessionCheckpointGet(
615
+ session_uuid: string
616
+ ): Promise<SessionCheckpoint | undefined> {
617
+ const { error, data } = await this.client
618
+ .from("session_checkpoints")
619
+ .select("message_idx,summary")
620
+ .eq("session_uuid", session_uuid)
621
+ .maybeSingle();
622
+
623
+ if (error) {
624
+ throw error;
625
+ }
626
+
627
+ if (!data) {
628
+ return undefined;
629
+ }
630
+
631
+ return {
632
+ message_idx: data.message_idx,
633
+ summary: data.summary,
634
+ };
635
+ }
636
+
637
+ async sessionCheckpointSet(
638
+ session_uuid: string,
639
+ checkpoint: SessionCheckpoint
640
+ ): Promise<void> {
641
+ const payload: supabase.TablesUpdate<"session_checkpoints"> = {
642
+ session_uuid,
643
+ message_idx: checkpoint.message_idx,
644
+ summary: checkpoint.summary,
645
+ };
646
+
647
+ const { error } = await this.client
648
+ .from("session_checkpoints")
649
+ .upsert([payload], { onConflict: "session_uuid" });
650
+ if (error) {
651
+ throw error;
652
+ }
653
+ }
654
+
655
+ //
656
+ // agent_profiles
657
+ //
658
+
659
+ /**
660
+ * Get all agents belonging to a user.
661
+ * @param userUuid - UUID of the user
662
+ * @returns Array of agent profiles
663
+ */
664
+ async agentProfilesGetByUser(userUuid: string): Promise<SavedAgentProfile[]> {
665
+ const { data, error } = await this.client
666
+ .from("agent_profiles")
667
+ .select("*")
668
+ .eq("user_uuid", userUuid);
669
+
670
+ if (error) {
671
+ throw error;
672
+ }
673
+
674
+ return data.map((agent) =>
675
+ SavedAgentProfile.fromJSONObj(agent as Record<string, unknown>)
676
+ );
677
+ }
678
+
679
+ /**
680
+ * Get all agents belonging to a team.
681
+ * @param teamUuid - UUID of the team
682
+ * @returns Array of agent profiles
683
+ */
684
+ async AgentProfilesGetByTeam(teamUuid: string): Promise<SavedAgentProfile[]> {
685
+ const { data, error } = await this.client
686
+ .from("agent_profiles")
687
+ .select("*")
688
+ .eq("team_uuid", teamUuid);
689
+
690
+ if (error) {
691
+ throw error;
692
+ }
693
+
694
+ return data.map((agent) =>
695
+ SavedAgentProfile.fromJSONObj(agent as Record<string, unknown>)
696
+ );
697
+ }
698
+
699
+ async agentTemplateGetByName(
700
+ templateName: string
701
+ ): Promise<AgentTemplate | undefined> {
702
+ const { data, error } = await this.client
703
+ .from("agent_templates")
704
+ .select("*")
705
+ .eq("name", templateName)
706
+ .maybeSingle();
707
+ if (error) {
708
+ throw error;
709
+ }
710
+ return data
711
+ ? AgentTemplate.fromJSONObj(data as Record<string, unknown>)
712
+ : undefined;
713
+ }
714
+ //
715
+ // teams
716
+ //
717
+
718
+ async createTeam(teamName: string, owner_uuid: string): Promise<string> {
719
+ const { data, error } = await this.client.rpc("create_team_with_owner", {
720
+ p_owner_uuid: owner_uuid,
721
+ p_name: teamName,
722
+ });
723
+ if (error) {
724
+ throw error;
725
+ }
726
+ // The SQL function returns a TEXT (team UUID)
727
+ return data as string;
728
+ }
729
+
730
+ /**
731
+ * Creates a team with initial participants.
732
+ * The owner is automatically added as a team member with 'owner' role.
733
+ * @param teamName - Name of the team to create
734
+ * @param ownerUuid - UUID of the team owner
735
+ * @param initialParticipants - Array of user UUIDs to add as initial members
736
+ * @returns The UUID of the created team
737
+ */
738
+ async createTeamWithParticipants(
739
+ teamName: string,
740
+ ownerUuid: string,
741
+ initialParticipants: string[]
742
+ ): Promise<string> {
743
+ // Create the team first
744
+ const teamUuid = await this.createTeam(teamName, ownerUuid);
745
+
746
+ // If there are initial participants, add them to the team
747
+ if (initialParticipants.length > 0) {
748
+ // Filter out the owner if they're in the initial participants list
749
+ const participantsToAdd = initialParticipants.filter(
750
+ (uuid) => uuid !== ownerUuid
751
+ );
752
+
753
+ if (participantsToAdd.length > 0) {
754
+ // Add all participants to the
755
+ const teamMemberInserts = participantsToAdd.map((userUuid) => ({
756
+ team_uuid: teamUuid,
757
+ user_uuid: userUuid,
758
+ role: "participant" as const,
759
+ }));
760
+
761
+ const { error: membersError } = await this.client
762
+ .from("team_members")
763
+ .insert(teamMemberInserts);
764
+
765
+ if (membersError) {
766
+ // If adding members fails, we should clean up the team
767
+ logger.error(
768
+ `Failed to add members to team ${teamUuid}: ${membersError.message}`
769
+ );
770
+ // Optionally delete the team if member addition fails
771
+ await this.deleteTeam(teamUuid);
772
+ throw membersError;
773
+ }
774
+ }
775
+ }
776
+
777
+ logger.info(
778
+ `Created team ${teamUuid}: ${String(initialParticipants.length)} members`
779
+ );
780
+
781
+ return teamUuid;
782
+ }
783
+
784
+ /**
785
+ * Deletes a team and all associated data.
786
+ * This will also remove all team members and any team sessions.
787
+ * @param teamUuid - UUID of the team to delete
788
+ */
789
+ async deleteTeam(teamUuid: string): Promise<void> {
790
+ try {
791
+ // Delete the team - this should cascade to team_members and team_sessions
792
+ // due to foreign key constraints with ON DELETE CASCADE
793
+ const { error } = await this.client
794
+ .from("teams")
795
+ .delete()
796
+ .eq("uuid", teamUuid);
797
+
798
+ if (error) {
799
+ throw error;
800
+ }
801
+
802
+ logger.info(`Deleted team ${teamUuid}`);
803
+ } catch (error) {
804
+ logger.error(`Failed to delete team ${teamUuid}:`, error);
805
+ throw new Error(`Failed to delete team: ${String(error)}`);
806
+ }
807
+ }
808
+
809
+ async getTeamInfosByUser(user_uuid: string): Promise<TeamInfo[]> {
810
+ // Get all teams the user is a member of (including as owner)
811
+ const { data: teamMemberships, error: teamError } = await this.client
812
+ .from("team_members")
813
+ .select(
814
+ `
815
+ team_uuid,
816
+ role,
817
+ teams!inner (
818
+ uuid,
819
+ name,
820
+ owner_uuid
821
+ )
822
+ `
823
+ )
824
+ .eq("user_uuid", user_uuid);
825
+
826
+ if (teamError) {
827
+ throw teamError;
828
+ }
829
+
830
+ if (teamMemberships.length === 0) {
831
+ return [];
832
+ }
833
+
834
+ // Process all team memberships in parallel
835
+ const teamSessionPromises = teamMemberships.map(async (membership) => {
836
+ const teamUuid: string = membership.team_uuid;
837
+ const teamData = membership.teams as unknown as {
838
+ uuid: string;
839
+ name: string;
840
+ owner_uuid: string;
841
+ };
842
+
843
+ // Use the role from team_members, sanity check against teams.owner_uuid
844
+ const memberRole = membership.role as TeamRole;
845
+ const isActualOwner = teamData.owner_uuid === user_uuid;
846
+
847
+ // Sanity check: if roles are inconsistent, throw an error
848
+ if (
849
+ (memberRole === "owner" && !isActualOwner) ||
850
+ (memberRole !== "owner" && isActualOwner)
851
+ ) {
852
+ throw new Error(
853
+ `Data inconsistency: user ${user_uuid} has role '${memberRole}' ` +
854
+ `in team ${teamUuid}, but owner is ${teamData.owner_uuid}`
855
+ );
856
+ }
857
+
858
+ // Fetch team members and sessions in parallel using helper methods
859
+ const [participants, sessions, agents] = await Promise.all([
860
+ this.teamGetMembers(teamUuid),
861
+ this.teamGetSessions(teamUuid),
862
+ this.AgentProfilesGetByTeam(teamUuid),
863
+ ]);
864
+
865
+ return {
866
+ team_uuid: teamUuid,
867
+ team_name: teamData.name,
868
+ owner_uuid: teamData.owner_uuid,
869
+ participants,
870
+ sessions,
871
+ agents,
872
+ };
873
+ });
874
+
875
+ return await Promise.all(teamSessionPromises);
876
+ }
877
+
878
+ /**
879
+ * Get all members of a team.
880
+ * @param teamUuid - UUID of the team
881
+ * @returns Array of team members with their roles
882
+ */
883
+ async teamGetMembers(teamUuid: string): Promise<TeamParticipant[]> {
884
+ const { data, error } = await this.client
885
+ .from("team_members")
886
+ .select("user_uuid, role")
887
+ .eq("team_uuid", teamUuid);
888
+
889
+ if (error) {
890
+ throw error;
891
+ }
892
+
893
+ // get users' data in parallel
894
+ const usersData = await Promise.all(
895
+ data.map((member) => this.getUserFromUuid(member.user_uuid as string))
896
+ );
897
+
898
+ usersData.forEach((user) => {
899
+ if (!user) {
900
+ throw new Error("Cannot find user data");
901
+ }
902
+ });
903
+
904
+ const result = data.map((member, index) => ({
905
+ user_uuid: member.user_uuid,
906
+ nickname: usersData[index]?.nickname || "",
907
+ email: usersData[index]?.email || "",
908
+ role: member.role as TeamRole,
909
+ }));
910
+
911
+ return result;
912
+ }
913
+
914
+ /**
915
+ * Add a member to a team as a participant.
916
+ * @param teamUuid - UUID of the team
917
+ * @param userUuid - UUID of the user to add
918
+ */
919
+ async teamAddMember(teamUuid: string, userUuid: string): Promise<void> {
920
+ const { error } = await this.client.from("team_members").insert({
921
+ team_uuid: teamUuid,
922
+ user_uuid: userUuid,
923
+ role: "participant",
924
+ });
925
+
926
+ if (error) {
927
+ throw error;
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Remove a member from a team.
933
+ * @param teamUuid - UUID of the team
934
+ * @param userUuid - UUID of the user to remove
935
+ */
936
+ async teamRemoveMember(teamUuid: string, userUuid: string): Promise<void> {
937
+ const { error } = await this.client
938
+ .from("team_members")
939
+ .delete()
940
+ .eq("team_uuid", teamUuid)
941
+ .eq("user_uuid", userUuid)
942
+ .eq("role", "participant");
943
+
944
+ if (error) {
945
+ throw error;
946
+ }
947
+ }
948
+
949
+ /**
950
+ * Get all sessions belonging to a team.
951
+ * @param teamUuid - UUID of the team
952
+ * @returns Array of session data
953
+ */
954
+ async teamGetSessions(teamUuid: string): Promise<SessionData[]> {
955
+ const { data, error } = await this.client
956
+ .from("sessions")
957
+ .select(
958
+ "uuid, title, agent_profile_uuid, workspace, updated_at, user_uuid"
959
+ )
960
+ .eq("team_uuid", teamUuid);
961
+
962
+ if (error) {
963
+ throw error;
964
+ }
965
+
966
+ return data.map((session) => ({
967
+ session_uuid: session.uuid,
968
+ title: session.title,
969
+ team_uuid: teamUuid,
970
+ agent_profile_uuid: session.agent_profile_uuid,
971
+ workspace: session.workspace || undefined,
972
+ updated_at: session.updated_at || new Date().toISOString(),
973
+ user_uuid: session.user_uuid,
974
+ }));
975
+ }
976
+
977
+ createTypedClient<T extends DbClientBase>(
978
+ ctor: new (client: SupabaseClient) => T
979
+ ): T {
980
+ return new ctor(this.client);
981
+ }
982
+ }