@agentforge-ai/cli 0.4.3 → 0.5.1

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 (67) hide show
  1. package/dist/default/convex/agents.ts +204 -0
  2. package/dist/default/convex/apiKeys.ts +133 -0
  3. package/dist/default/convex/cronJobs.ts +224 -0
  4. package/dist/default/convex/files.ts +103 -0
  5. package/dist/default/convex/folders.ts +110 -0
  6. package/dist/default/convex/heartbeat.ts +371 -0
  7. package/dist/default/convex/logs.ts +66 -0
  8. package/dist/default/convex/mastraIntegration.ts +185 -0
  9. package/dist/default/convex/mcpConnections.ts +127 -0
  10. package/dist/default/convex/messages.ts +90 -0
  11. package/dist/default/convex/projects.ts +114 -0
  12. package/dist/default/convex/schema.ts +150 -83
  13. package/dist/default/convex/sessions.ts +174 -0
  14. package/dist/default/convex/settings.ts +79 -0
  15. package/dist/default/convex/skills.ts +178 -0
  16. package/dist/default/convex/threads.ts +100 -0
  17. package/dist/default/convex/usage.ts +195 -0
  18. package/dist/default/convex/vault.ts +397 -0
  19. package/dist/default/dashboard/app/main.tsx +7 -3
  20. package/dist/default/dashboard/app/routes/agents.tsx +103 -161
  21. package/dist/default/dashboard/app/routes/chat.tsx +163 -317
  22. package/dist/default/dashboard/app/routes/connections.tsx +247 -386
  23. package/dist/default/dashboard/app/routes/cron.tsx +127 -286
  24. package/dist/default/dashboard/app/routes/files.tsx +184 -167
  25. package/dist/default/dashboard/app/routes/index.tsx +63 -96
  26. package/dist/default/dashboard/app/routes/projects.tsx +106 -225
  27. package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
  28. package/dist/default/dashboard/app/routes/settings.tsx +316 -532
  29. package/dist/default/dashboard/app/routes/skills.tsx +329 -216
  30. package/dist/default/dashboard/app/routes/usage.tsx +107 -150
  31. package/dist/default/dashboard/tsconfig.json +3 -2
  32. package/dist/default/dashboard/vite.config.ts +6 -0
  33. package/dist/index.js +256 -49
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/default/convex/agents.ts +204 -0
  37. package/templates/default/convex/apiKeys.ts +133 -0
  38. package/templates/default/convex/cronJobs.ts +224 -0
  39. package/templates/default/convex/files.ts +103 -0
  40. package/templates/default/convex/folders.ts +110 -0
  41. package/templates/default/convex/heartbeat.ts +371 -0
  42. package/templates/default/convex/logs.ts +66 -0
  43. package/templates/default/convex/mastraIntegration.ts +185 -0
  44. package/templates/default/convex/mcpConnections.ts +127 -0
  45. package/templates/default/convex/messages.ts +90 -0
  46. package/templates/default/convex/projects.ts +114 -0
  47. package/templates/default/convex/schema.ts +150 -83
  48. package/templates/default/convex/sessions.ts +174 -0
  49. package/templates/default/convex/settings.ts +79 -0
  50. package/templates/default/convex/skills.ts +178 -0
  51. package/templates/default/convex/threads.ts +100 -0
  52. package/templates/default/convex/usage.ts +195 -0
  53. package/templates/default/convex/vault.ts +397 -0
  54. package/templates/default/dashboard/app/main.tsx +7 -3
  55. package/templates/default/dashboard/app/routes/agents.tsx +103 -161
  56. package/templates/default/dashboard/app/routes/chat.tsx +163 -317
  57. package/templates/default/dashboard/app/routes/connections.tsx +247 -386
  58. package/templates/default/dashboard/app/routes/cron.tsx +127 -286
  59. package/templates/default/dashboard/app/routes/files.tsx +184 -167
  60. package/templates/default/dashboard/app/routes/index.tsx +63 -96
  61. package/templates/default/dashboard/app/routes/projects.tsx +106 -225
  62. package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
  63. package/templates/default/dashboard/app/routes/settings.tsx +316 -532
  64. package/templates/default/dashboard/app/routes/skills.tsx +329 -216
  65. package/templates/default/dashboard/app/routes/usage.tsx +107 -150
  66. package/templates/default/dashboard/tsconfig.json +3 -2
  67. package/templates/default/dashboard/vite.config.ts +6 -0
@@ -1,24 +1,15 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
2
  import { v } from "convex/values";
3
3
 
4
- /**
5
- * AgentForge Database Schema
6
- *
7
- * This schema defines all the tables needed for your AgentForge project.
8
- * Customize it to fit your needs — add new tables, fields, or indexes.
9
- *
10
- * IMPORTANT: Index names cannot be "by_id" or "by_creation_time" (reserved by Convex).
11
- * Use camelCase names like "byAgentId", "byUserId", etc.
12
- */
13
4
  export default defineSchema({
14
- // ─── Agent Definitions ───────────────────────────────────────────────
5
+ // Core agent definitions
15
6
  agents: defineTable({
16
7
  id: v.string(),
17
8
  name: v.string(),
18
9
  description: v.optional(v.string()),
19
10
  instructions: v.string(),
20
11
  model: v.string(),
21
- provider: v.string(),
12
+ provider: v.string(), // "openai", "openrouter", "anthropic", etc.
22
13
  tools: v.optional(v.any()),
23
14
  temperature: v.optional(v.number()),
24
15
  maxTokens: v.optional(v.number()),
@@ -32,22 +23,21 @@ export default defineSchema({
32
23
  .index("byUserId", ["userId"])
33
24
  .index("byIsActive", ["isActive"]),
34
25
 
35
- // ─── Conversation Threads ────────────────────────────────────────────
26
+ // Conversation threads
36
27
  threads: defineTable({
37
28
  name: v.optional(v.string()),
38
29
  agentId: v.string(),
39
30
  userId: v.optional(v.string()),
40
- projectId: v.optional(v.string()),
41
- status: v.string(),
31
+ projectId: v.optional(v.id("projects")),
42
32
  metadata: v.optional(v.any()),
43
33
  createdAt: v.number(),
44
34
  updatedAt: v.number(),
45
35
  })
46
36
  .index("byAgentId", ["agentId"])
47
37
  .index("byUserId", ["userId"])
48
- .index("byStatus", ["status"]),
38
+ .index("byProjectId", ["projectId"]),
49
39
 
50
- // ─── Messages ────────────────────────────────────────────────────────
40
+ // Messages in threads
51
41
  messages: defineTable({
52
42
  threadId: v.id("threads"),
53
43
  role: v.union(
@@ -57,113 +47,143 @@ export default defineSchema({
57
47
  v.literal("tool")
58
48
  ),
59
49
  content: v.string(),
60
- toolCalls: v.optional(v.any()),
61
- toolResults: v.optional(v.any()),
62
- tokenUsage: v.optional(v.any()),
63
- model: v.optional(v.string()),
64
- provider: v.optional(v.string()),
65
- timestamp: v.number(),
66
- })
67
- .index("byThreadId", ["threadId"])
68
- .index("byTimestamp", ["timestamp"]),
50
+ tool_calls: v.optional(v.any()),
51
+ tool_results: v.optional(v.any()),
52
+ metadata: v.optional(v.any()),
53
+ createdAt: v.number(),
54
+ }).index("byThread", ["threadId"]),
69
55
 
70
- // ─── Sessions ────────────────────────────────────────────────────────
56
+ // Active sessions
71
57
  sessions: defineTable({
72
- name: v.string(),
58
+ sessionId: v.string(),
59
+ threadId: v.id("threads"),
73
60
  agentId: v.string(),
74
- threadId: v.optional(v.id("threads")),
75
- status: v.string(),
76
61
  userId: v.optional(v.string()),
62
+ status: v.union(
63
+ v.literal("active"),
64
+ v.literal("paused"),
65
+ v.literal("completed"),
66
+ v.literal("error")
67
+ ),
68
+ channel: v.optional(v.string()), // "dashboard", "api", "webhook", etc.
69
+ metadata: v.optional(v.any()),
77
70
  startedAt: v.number(),
78
71
  lastActivityAt: v.number(),
79
- metadata: v.optional(v.any()),
72
+ completedAt: v.optional(v.number()),
80
73
  })
74
+ .index("bySessionId", ["sessionId"])
75
+ .index("byThreadId", ["threadId"])
81
76
  .index("byAgentId", ["agentId"])
82
- .index("byUserId", ["userId"])
83
- .index("byStatus", ["status"]),
77
+ .index("byStatus", ["status"])
78
+ .index("byUserId", ["userId"]),
84
79
 
85
- // ─── Files ───────────────────────────────────────────────────────────
80
+ // File storage metadata (files stored in Cloudflare R2)
86
81
  files: defineTable({
87
82
  name: v.string(),
88
- folderId: v.optional(v.string()),
83
+ originalName: v.string(),
89
84
  mimeType: v.string(),
90
85
  size: v.number(),
91
- storageId: v.optional(v.string()),
92
- url: v.optional(v.string()),
86
+ url: v.string(), // Cloudflare R2 URL
87
+ folderId: v.optional(v.id("folders")),
88
+ projectId: v.optional(v.id("projects")),
93
89
  userId: v.optional(v.string()),
94
- projectId: v.optional(v.string()),
95
- createdAt: v.number(),
90
+ uploadedAt: v.number(),
91
+ metadata: v.optional(v.any()),
96
92
  })
97
93
  .index("byFolderId", ["folderId"])
98
- .index("byUserId", ["userId"])
99
- .index("byProjectId", ["projectId"]),
94
+ .index("byProjectId", ["projectId"])
95
+ .index("byUserId", ["userId"]),
100
96
 
101
- // ─── Folders ─────────────────────────────────────────────────────────
97
+ // Folder organization
102
98
  folders: defineTable({
103
99
  name: v.string(),
104
- parentId: v.optional(v.string()),
100
+ parentId: v.optional(v.id("folders")),
101
+ projectId: v.optional(v.id("projects")),
105
102
  userId: v.optional(v.string()),
106
- projectId: v.optional(v.string()),
107
103
  createdAt: v.number(),
104
+ updatedAt: v.number(),
108
105
  })
109
106
  .index("byParentId", ["parentId"])
107
+ .index("byProjectId", ["projectId"])
110
108
  .index("byUserId", ["userId"]),
111
109
 
112
- // ─── Projects / Workspaces ───────────────────────────────────────────
110
+ // Projects/Workspaces
113
111
  projects: defineTable({
114
112
  name: v.string(),
115
113
  description: v.optional(v.string()),
116
- status: v.string(),
117
114
  userId: v.optional(v.string()),
118
115
  settings: v.optional(v.any()),
119
116
  createdAt: v.number(),
120
117
  updatedAt: v.number(),
121
- })
122
- .index("byUserId", ["userId"])
123
- .index("byStatus", ["status"]),
118
+ }).index("byUserId", ["userId"]),
124
119
 
125
- // ─── Skills ──────────────────────────────────────────────────────────
120
+ // Skills/Tools marketplace
126
121
  skills: defineTable({
127
122
  name: v.string(),
128
- description: v.optional(v.string()),
123
+ displayName: v.string(),
124
+ description: v.string(),
129
125
  category: v.string(),
130
126
  version: v.string(),
127
+ author: v.optional(v.string()),
128
+ repository: v.optional(v.string()),
129
+ documentation: v.optional(v.string()),
130
+ code: v.string(), // The actual skill code
131
+ schema: v.optional(v.any()), // JSON schema for skill parameters
131
132
  isInstalled: v.boolean(),
132
- configuration: v.optional(v.any()),
133
- agentId: v.optional(v.string()),
133
+ isEnabled: v.boolean(),
134
134
  userId: v.optional(v.string()),
135
+ installedAt: v.optional(v.number()),
135
136
  createdAt: v.number(),
136
137
  updatedAt: v.number(),
137
138
  })
138
- .index("byAgentId", ["agentId"])
139
- .index("byCategory", ["category"])
140
- .index("byIsInstalled", ["isInstalled"]),
139
+ .index("byUserId", ["userId"])
140
+ .index("byIsInstalled", ["isInstalled"])
141
+ .index("byCategory", ["category"]),
141
142
 
142
- // ─── Cron Jobs ───────────────────────────────────────────────────────
143
+ // Cron jobs/scheduled tasks
143
144
  cronJobs: defineTable({
144
145
  name: v.string(),
145
- schedule: v.string(),
146
+ description: v.optional(v.string()),
147
+ schedule: v.string(), // Cron expression
146
148
  agentId: v.string(),
147
- action: v.string(),
149
+ prompt: v.string(), // What to execute
148
150
  isEnabled: v.boolean(),
149
- lastRunAt: v.optional(v.number()),
150
- nextRunAt: v.optional(v.number()),
151
+ lastRun: v.optional(v.number()),
152
+ nextRun: v.optional(v.number()),
151
153
  userId: v.optional(v.string()),
154
+ metadata: v.optional(v.any()),
152
155
  createdAt: v.number(),
153
156
  updatedAt: v.number(),
154
157
  })
155
158
  .index("byAgentId", ["agentId"])
159
+ .index("byUserId", ["userId"])
156
160
  .index("byIsEnabled", ["isEnabled"])
157
- .index("byUserId", ["userId"]),
161
+ .index("byNextRun", ["nextRun"]),
162
+
163
+ // Cron job execution history
164
+ cronJobRuns: defineTable({
165
+ cronJobId: v.id("cronJobs"),
166
+ status: v.union(
167
+ v.literal("success"),
168
+ v.literal("failed"),
169
+ v.literal("running")
170
+ ),
171
+ startedAt: v.number(),
172
+ completedAt: v.optional(v.number()),
173
+ output: v.optional(v.string()),
174
+ error: v.optional(v.string()),
175
+ })
176
+ .index("byCronJobId", ["cronJobId"])
177
+ .index("byStatus", ["status"]),
158
178
 
159
- // ─── MCP Connections ─────────────────────────────────────────────────
179
+ // MCP (Model Context Protocol) connections
160
180
  mcpConnections: defineTable({
161
181
  name: v.string(),
162
- type: v.string(),
163
- endpoint: v.string(),
182
+ serverUrl: v.string(),
183
+ protocol: v.string(), // "mcp", "custom"
164
184
  isConnected: v.boolean(),
165
185
  isEnabled: v.boolean(),
166
- credentials: v.optional(v.any()),
186
+ credentials: v.optional(v.any()), // Encrypted
167
187
  capabilities: v.optional(v.any()),
168
188
  userId: v.optional(v.string()),
169
189
  lastConnectedAt: v.optional(v.number()),
@@ -173,9 +193,9 @@ export default defineSchema({
173
193
  .index("byUserId", ["userId"])
174
194
  .index("byIsEnabled", ["isEnabled"]),
175
195
 
176
- // ─── API Keys ────────────────────────────────────────────────────────
196
+ // API keys and credentials (encrypted)
177
197
  apiKeys: defineTable({
178
- provider: v.string(),
198
+ provider: v.string(), // "openai", "openrouter", "anthropic", etc.
179
199
  keyName: v.string(),
180
200
  encryptedKey: v.string(),
181
201
  isActive: v.boolean(),
@@ -187,7 +207,7 @@ export default defineSchema({
187
207
  .index("byUserId", ["userId"])
188
208
  .index("byIsActive", ["isActive"]),
189
209
 
190
- // ─── Usage Tracking ──────────────────────────────────────────────────
210
+ // Usage tracking for metrics
191
211
  usage: defineTable({
192
212
  agentId: v.string(),
193
213
  sessionId: v.optional(v.string()),
@@ -196,7 +216,7 @@ export default defineSchema({
196
216
  promptTokens: v.number(),
197
217
  completionTokens: v.number(),
198
218
  totalTokens: v.number(),
199
- cost: v.optional(v.number()),
219
+ cost: v.optional(v.number()), // Estimated cost in USD
200
220
  userId: v.optional(v.string()),
201
221
  timestamp: v.number(),
202
222
  })
@@ -205,7 +225,7 @@ export default defineSchema({
205
225
  .index("byTimestamp", ["timestamp"])
206
226
  .index("byProvider", ["provider"]),
207
227
 
208
- // ─── Settings ────────────────────────────────────────────────────────
228
+ // User settings and configuration
209
229
  settings: defineTable({
210
230
  userId: v.string(),
211
231
  key: v.string(),
@@ -215,7 +235,7 @@ export default defineSchema({
215
235
  .index("byUserId", ["userId"])
216
236
  .index("byUserIdAndKey", ["userId", "key"]),
217
237
 
218
- // ─── System Logs ─────────────────────────────────────────────────────
238
+ // System logs for debugging
219
239
  logs: defineTable({
220
240
  level: v.union(
221
241
  v.literal("debug"),
@@ -223,7 +243,7 @@ export default defineSchema({
223
243
  v.literal("warn"),
224
244
  v.literal("error")
225
245
  ),
226
- source: v.string(),
246
+ source: v.string(), // "agent", "system", "api", etc.
227
247
  message: v.string(),
228
248
  metadata: v.optional(v.any()),
229
249
  userId: v.optional(v.string()),
@@ -231,16 +251,31 @@ export default defineSchema({
231
251
  })
232
252
  .index("byLevel", ["level"])
233
253
  .index("bySource", ["source"])
234
- .index("byTimestamp", ["timestamp"]),
254
+ .index("byTimestamp", ["timestamp"])
255
+ .index("byUserId", ["userId"]),
256
+
257
+ // Channels for multi-platform support
258
+ channels: defineTable({
259
+ name: v.string(),
260
+ type: v.string(), // "dashboard", "api", "webhook", "whatsapp", etc.
261
+ isEnabled: v.boolean(),
262
+ configuration: v.optional(v.any()),
263
+ userId: v.optional(v.string()),
264
+ createdAt: v.number(),
265
+ updatedAt: v.number(),
266
+ })
267
+ .index("byType", ["type"])
268
+ .index("byUserId", ["userId"])
269
+ .index("byIsEnabled", ["isEnabled"]),
235
270
 
236
- // ─── Heartbeat (Task Continuation) ───────────────────────────────────
271
+ // Heartbeat system for ongoing task tracking
237
272
  heartbeats: defineTable({
238
273
  agentId: v.string(),
239
274
  threadId: v.optional(v.id("threads")),
240
- status: v.string(),
275
+ status: v.string(), // "active", "waiting", "completed", "error"
241
276
  currentTask: v.optional(v.string()),
242
277
  pendingTasks: v.array(v.string()),
243
- context: v.string(),
278
+ context: v.string(), // Markdown-formatted context
244
279
  lastCheck: v.number(),
245
280
  nextCheck: v.number(),
246
281
  metadata: v.optional(v.any()),
@@ -249,14 +284,14 @@ export default defineSchema({
249
284
  .index("byStatus", ["status"])
250
285
  .index("byNextCheck", ["nextCheck"]),
251
286
 
252
- // ─── Secure Vault ────────────────────────────────────────────────────
287
+ // Secure Vault for encrypted secrets storage
253
288
  vault: defineTable({
254
- name: v.string(),
255
- category: v.string(),
256
- provider: v.optional(v.string()),
257
- encryptedValue: v.string(),
258
- iv: v.string(),
259
- maskedValue: v.string(),
289
+ name: v.string(), // Display name (e.g., "OpenAI API Key")
290
+ category: v.string(), // "api_key", "token", "secret", "credential"
291
+ provider: v.optional(v.string()), // Associated provider
292
+ encryptedValue: v.string(), // AES-256-GCM encrypted value
293
+ iv: v.string(), // Initialization vector for decryption
294
+ maskedValue: v.string(), // e.g., "sk-...abc123"
260
295
  isActive: v.boolean(),
261
296
  expiresAt: v.optional(v.number()),
262
297
  lastAccessedAt: v.optional(v.number()),
@@ -269,4 +304,36 @@ export default defineSchema({
269
304
  .index("byCategory", ["category"])
270
305
  .index("byProvider", ["provider"])
271
306
  .index("byIsActive", ["isActive"]),
307
+
308
+ // Audit log for vault access
309
+ vaultAuditLog: defineTable({
310
+ vaultEntryId: v.id("vault"),
311
+ action: v.string(), // "created", "accessed", "updated", "deleted", "auto_captured"
312
+ source: v.string(), // "dashboard", "chat", "api", "agent"
313
+ userId: v.optional(v.string()),
314
+ ipAddress: v.optional(v.string()),
315
+ timestamp: v.number(),
316
+ })
317
+ .index("byVaultEntryId", ["vaultEntryId"])
318
+ .index("byUserId", ["userId"])
319
+ .index("byTimestamp", ["timestamp"]),
320
+
321
+ // Agent instances for multi-agent workflows
322
+ instances: defineTable({
323
+ agentId: v.string(),
324
+ instanceId: v.string(),
325
+ status: v.union(
326
+ v.literal("running"),
327
+ v.literal("stopped"),
328
+ v.literal("error")
329
+ ),
330
+ configuration: v.optional(v.any()),
331
+ userId: v.optional(v.string()),
332
+ startedAt: v.number(),
333
+ stoppedAt: v.optional(v.number()),
334
+ })
335
+ .index("byAgentId", ["agentId"])
336
+ .index("byInstanceId", ["instanceId"])
337
+ .index("byStatus", ["status"])
338
+ .index("byUserId", ["userId"]),
272
339
  });
@@ -0,0 +1,174 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+
4
+ // Query: Get all sessions
5
+ export const list = query({
6
+ args: {
7
+ userId: v.optional(v.string()),
8
+ agentId: v.optional(v.string()),
9
+ status: v.optional(v.string()),
10
+ },
11
+ handler: async (ctx, args) => {
12
+ if (args.status) {
13
+ const sessions = await ctx.db
14
+ .query("sessions")
15
+ .withIndex("byStatus", (q) => q.eq("status", args.status! as any))
16
+ .collect();
17
+
18
+ if (args.userId) {
19
+ return sessions.filter((s) => s.userId === args.userId);
20
+ }
21
+ if (args.agentId) {
22
+ return sessions.filter((s) => s.agentId === args.agentId);
23
+ }
24
+ return sessions;
25
+ }
26
+
27
+ if (args.agentId) {
28
+ return await ctx.db
29
+ .query("sessions")
30
+ .withIndex("byAgentId", (q) => q.eq("agentId", args.agentId!))
31
+ .collect();
32
+ }
33
+
34
+ if (args.userId) {
35
+ return await ctx.db
36
+ .query("sessions")
37
+ .withIndex("byUserId", (q) => q.eq("userId", args.userId!))
38
+ .collect();
39
+ }
40
+
41
+ return await ctx.db.query("sessions").collect();
42
+ },
43
+ });
44
+
45
+ // Query: Get session by ID
46
+ export const get = query({
47
+ args: { sessionId: v.string() },
48
+ handler: async (ctx, args) => {
49
+ return await ctx.db
50
+ .query("sessions")
51
+ .withIndex("bySessionId", (q) => q.eq("sessionId", args.sessionId!))
52
+ .first();
53
+ },
54
+ });
55
+
56
+ // Query: Get active sessions
57
+ export const listActive = query({
58
+ args: {
59
+ userId: v.optional(v.string()),
60
+ },
61
+ handler: async (ctx, args) => {
62
+ const sessions = await ctx.db
63
+ .query("sessions")
64
+ .withIndex("byStatus", (q) => q.eq("status", "active"))
65
+ .collect();
66
+
67
+ if (args.userId) {
68
+ return sessions.filter((s) => s.userId === args.userId);
69
+ }
70
+
71
+ return sessions;
72
+ },
73
+ });
74
+
75
+ // Mutation: Create a new session
76
+ export const create = mutation({
77
+ args: {
78
+ sessionId: v.string(),
79
+ threadId: v.id("threads"),
80
+ agentId: v.string(),
81
+ userId: v.optional(v.string()),
82
+ channel: v.optional(v.string()),
83
+ metadata: v.optional(v.any()),
84
+ },
85
+ handler: async (ctx, args) => {
86
+ const now = Date.now();
87
+ const sessionId = await ctx.db.insert("sessions", {
88
+ ...args,
89
+ status: "active",
90
+ startedAt: now,
91
+ lastActivityAt: now,
92
+ });
93
+ return sessionId;
94
+ },
95
+ });
96
+
97
+ // Mutation: Update session activity
98
+ export const updateActivity = mutation({
99
+ args: {
100
+ sessionId: v.string(),
101
+ metadata: v.optional(v.any()),
102
+ },
103
+ handler: async (ctx, args) => {
104
+ const session = await ctx.db
105
+ .query("sessions")
106
+ .withIndex("bySessionId", (q) => q.eq("sessionId", args.sessionId!))
107
+ .first();
108
+
109
+ if (!session) {
110
+ throw new Error(`Session ${args.sessionId} not found`);
111
+ }
112
+
113
+ await ctx.db.patch(session._id, {
114
+ lastActivityAt: Date.now(),
115
+ ...(args.metadata && { metadata: args.metadata }),
116
+ });
117
+
118
+ return session._id;
119
+ },
120
+ });
121
+
122
+ // Mutation: Update session status
123
+ export const updateStatus = mutation({
124
+ args: {
125
+ sessionId: v.string(),
126
+ status: v.union(
127
+ v.literal("active"),
128
+ v.literal("paused"),
129
+ v.literal("completed"),
130
+ v.literal("error")
131
+ ),
132
+ },
133
+ handler: async (ctx, args) => {
134
+ const session = await ctx.db
135
+ .query("sessions")
136
+ .withIndex("bySessionId", (q) => q.eq("sessionId", args.sessionId!))
137
+ .first();
138
+
139
+ if (!session) {
140
+ throw new Error(`Session ${args.sessionId} not found`);
141
+ }
142
+
143
+ const updates: any = {
144
+ status: args.status,
145
+ lastActivityAt: Date.now(),
146
+ };
147
+
148
+ if (args.status === "completed" || args.status === "error") {
149
+ updates.completedAt = Date.now();
150
+ }
151
+
152
+ await ctx.db.patch(session._id, updates);
153
+
154
+ return session._id;
155
+ },
156
+ });
157
+
158
+ // Mutation: Delete session
159
+ export const remove = mutation({
160
+ args: { sessionId: v.string() },
161
+ handler: async (ctx, args) => {
162
+ const session = await ctx.db
163
+ .query("sessions")
164
+ .withIndex("bySessionId", (q) => q.eq("sessionId", args.sessionId!))
165
+ .first();
166
+
167
+ if (!session) {
168
+ throw new Error(`Session ${args.sessionId} not found`);
169
+ }
170
+
171
+ await ctx.db.delete(session._id);
172
+ return { success: true };
173
+ },
174
+ });
@@ -0,0 +1,79 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+
4
+ // Query: Get a setting by key
5
+ export const get = query({
6
+ args: { userId: v.string(), key: v.string() },
7
+ handler: async (ctx, args) => {
8
+ const setting = await ctx.db
9
+ .query("settings")
10
+ .withIndex("byUserIdAndKey", (q) =>
11
+ q.eq("userId", args.userId!).eq("key", args.key!)
12
+ )
13
+ .first();
14
+ return setting;
15
+ },
16
+ });
17
+
18
+ // Query: List all settings for a user
19
+ export const list = query({
20
+ args: { userId: v.optional(v.string()) },
21
+ handler: async (ctx, args) => {
22
+ if (args.userId) {
23
+ return await ctx.db
24
+ .query("settings")
25
+ .withIndex("byUserId", (q) => q.eq("userId", args.userId!))
26
+ .collect();
27
+ }
28
+ return await ctx.db.query("settings").collect();
29
+ },
30
+ });
31
+
32
+ // Mutation: Set a setting (upsert)
33
+ export const set = mutation({
34
+ args: {
35
+ userId: v.string(),
36
+ key: v.string(),
37
+ value: v.any(),
38
+ },
39
+ handler: async (ctx, args) => {
40
+ const existing = await ctx.db
41
+ .query("settings")
42
+ .withIndex("byUserIdAndKey", (q) =>
43
+ q.eq("userId", args.userId!).eq("key", args.key!)
44
+ )
45
+ .first();
46
+
47
+ if (existing) {
48
+ await ctx.db.patch(existing._id, {
49
+ value: args.value,
50
+ updatedAt: Date.now(),
51
+ });
52
+ return existing._id;
53
+ }
54
+
55
+ return await ctx.db.insert("settings", {
56
+ userId: args.userId,
57
+ key: args.key,
58
+ value: args.value,
59
+ updatedAt: Date.now(),
60
+ });
61
+ },
62
+ });
63
+
64
+ // Mutation: Delete a setting
65
+ export const remove = mutation({
66
+ args: { userId: v.string(), key: v.string() },
67
+ handler: async (ctx, args) => {
68
+ const existing = await ctx.db
69
+ .query("settings")
70
+ .withIndex("byUserIdAndKey", (q) =>
71
+ q.eq("userId", args.userId!).eq("key", args.key!)
72
+ )
73
+ .first();
74
+
75
+ if (existing) {
76
+ await ctx.db.delete(existing._id);
77
+ }
78
+ },
79
+ });