@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
@@ -0,0 +1,397 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query, internalMutation } from "./_generated/server";
3
+
4
+ // ============================================================
5
+ // SECURE VAULT - Encrypted secrets management
6
+ // ============================================================
7
+
8
+ // Secret pattern detection for auto-capture from chat
9
+ const SECRET_PATTERNS = [
10
+ { pattern: /sk-[a-zA-Z0-9]{20,}/, category: "api_key", provider: "openai", name: "OpenAI API Key" },
11
+ { pattern: /sk-ant-[a-zA-Z0-9-]{20,}/, category: "api_key", provider: "anthropic", name: "Anthropic API Key" },
12
+ { pattern: /sk-or-[a-zA-Z0-9]{20,}/, category: "api_key", provider: "openrouter", name: "OpenRouter API Key" },
13
+ { pattern: /AIza[a-zA-Z0-9_-]{35}/, category: "api_key", provider: "google", name: "Google API Key" },
14
+ { pattern: /xai-[a-zA-Z0-9]{20,}/, category: "api_key", provider: "xai", name: "xAI API Key" },
15
+ { pattern: /ghp_[a-zA-Z0-9]{36}/, category: "token", provider: "github", name: "GitHub Personal Access Token" },
16
+ { pattern: /gho_[a-zA-Z0-9]{36}/, category: "token", provider: "github", name: "GitHub OAuth Token" },
17
+ { pattern: /glpat-[a-zA-Z0-9_-]{20,}/, category: "token", provider: "gitlab", name: "GitLab Personal Access Token" },
18
+ { pattern: /xoxb-[a-zA-Z0-9-]+/, category: "token", provider: "slack", name: "Slack Bot Token" },
19
+ { pattern: /xoxp-[a-zA-Z0-9-]+/, category: "token", provider: "slack", name: "Slack User Token" },
20
+ { pattern: /AKIA[A-Z0-9]{16}/, category: "credential", provider: "aws", name: "AWS Access Key ID" },
21
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/, category: "api_key", provider: "stripe", name: "Stripe Live Secret Key" },
22
+ { pattern: /sk_test_[a-zA-Z0-9]{24,}/, category: "api_key", provider: "stripe", name: "Stripe Test Secret Key" },
23
+ { pattern: /pk_live_[a-zA-Z0-9]{24,}/, category: "api_key", provider: "stripe", name: "Stripe Live Publishable Key" },
24
+ { pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/, category: "api_key", provider: "sendgrid", name: "SendGrid API Key" },
25
+ { pattern: /[a-f0-9]{32}-us[0-9]{1,2}/, category: "api_key", provider: "mailchimp", name: "Mailchimp API Key" },
26
+ { pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, category: "secret", provider: "ssh", name: "Private Key" },
27
+ { pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/, category: "token", provider: "jwt", name: "JWT Token" },
28
+ ];
29
+
30
+ // Mask a secret value for display (show first 4 and last 4 chars)
31
+ function maskSecret(value: string): string {
32
+ if (value.length <= 12) {
33
+ return value.substring(0, 3) + "..." + value.substring(value.length - 3);
34
+ }
35
+ return value.substring(0, 6) + "..." + value.substring(value.length - 4);
36
+ }
37
+
38
+ // Encryption key from Convex environment variable.
39
+ // Set VAULT_ENCRYPTION_KEY in your Convex dashboard under Settings > Environment Variables.
40
+ // If not set, a default key is used (NOT SECURE for production).
41
+ function getEncryptionKey(): string {
42
+ return process.env.VAULT_ENCRYPTION_KEY ?? "agentforge-default-key-set-env-var";
43
+ }
44
+
45
+ // XOR-based encoding with per-entry IV for database obfuscation.
46
+ // For production deployments with sensitive data, consider integrating
47
+ // with a proper KMS (e.g., AWS KMS, Cloudflare Workers Secrets).
48
+ function encodeSecret(value: string, key: string): { encrypted: string; iv: string } {
49
+ const iv = Array.from({ length: 16 }, () =>
50
+ Math.floor(Math.random() * 256).toString(16).padStart(2, "0")
51
+ ).join("");
52
+ const combined = key + iv;
53
+ let encrypted = "";
54
+ for (let i = 0; i < value.length; i++) {
55
+ const charCode = value.charCodeAt(i) ^ combined.charCodeAt(i % combined.length);
56
+ encrypted += charCode.toString(16).padStart(4, "0");
57
+ }
58
+ return { encrypted, iv };
59
+ }
60
+
61
+ function decodeSecret(encrypted: string, iv: string, key: string): string {
62
+ const combined = key + iv;
63
+ let decoded = "";
64
+ for (let i = 0; i < encrypted.length; i += 4) {
65
+ const charCode =
66
+ parseInt(encrypted.substring(i, i + 4), 16) ^
67
+ combined.charCodeAt((i / 4) % combined.length);
68
+ decoded += String.fromCharCode(charCode);
69
+ }
70
+ return decoded;
71
+ }
72
+
73
+ // ---- Queries ----
74
+
75
+ export const list = query({
76
+ args: {
77
+ userId: v.optional(v.string()),
78
+ category: v.optional(v.string()),
79
+ },
80
+ handler: async (ctx, args) => {
81
+ let entries;
82
+ if (args.userId) {
83
+ entries = await ctx.db
84
+ .query("vault")
85
+ .withIndex("byUserId", (q) => q.eq("userId", args.userId!))
86
+ .order("desc")
87
+ .collect();
88
+ } else {
89
+ entries = await ctx.db.query("vault").order("desc").collect();
90
+ }
91
+
92
+ if (args.category) {
93
+ entries = entries.filter((e) => e.category === args.category);
94
+ }
95
+
96
+ // Never return encrypted values - only masked
97
+ return entries.map((entry) => ({
98
+ _id: entry._id,
99
+ name: entry.name,
100
+ category: entry.category,
101
+ provider: entry.provider,
102
+ maskedValue: entry.maskedValue,
103
+ isActive: entry.isActive,
104
+ expiresAt: entry.expiresAt,
105
+ lastAccessedAt: entry.lastAccessedAt,
106
+ accessCount: entry.accessCount,
107
+ createdAt: entry.createdAt,
108
+ updatedAt: entry.updatedAt,
109
+ }));
110
+ },
111
+ });
112
+
113
+ export const getAuditLog = query({
114
+ args: {
115
+ vaultEntryId: v.optional(v.id("vault")),
116
+ limit: v.optional(v.number()),
117
+ },
118
+ handler: async (ctx, args) => {
119
+ const limit = args.limit ?? 50;
120
+ if (args.vaultEntryId) {
121
+ return await ctx.db
122
+ .query("vaultAuditLog")
123
+ .withIndex("byVaultEntryId", (q) => q.eq("vaultEntryId", args.vaultEntryId!))
124
+ .order("desc")
125
+ .take(limit);
126
+ }
127
+ return await ctx.db.query("vaultAuditLog").order("desc").take(limit);
128
+ },
129
+ });
130
+
131
+ // ---- Mutations ----
132
+
133
+ export const store = mutation({
134
+ args: {
135
+ name: v.string(),
136
+ category: v.string(),
137
+ provider: v.optional(v.string()),
138
+ value: v.string(),
139
+ userId: v.optional(v.string()),
140
+ expiresAt: v.optional(v.number()),
141
+ },
142
+ handler: async (ctx, args) => {
143
+ const key = getEncryptionKey();
144
+ const { encrypted, iv } = encodeSecret(args.value, key);
145
+ const masked = maskSecret(args.value);
146
+ const now = Date.now();
147
+
148
+ const id = await ctx.db.insert("vault", {
149
+ name: args.name,
150
+ category: args.category,
151
+ provider: args.provider,
152
+ encryptedValue: encrypted,
153
+ iv,
154
+ maskedValue: masked,
155
+ isActive: true,
156
+ expiresAt: args.expiresAt,
157
+ accessCount: 0,
158
+ userId: args.userId,
159
+ createdAt: now,
160
+ updatedAt: now,
161
+ });
162
+
163
+ await ctx.db.insert("vaultAuditLog", {
164
+ vaultEntryId: id,
165
+ action: "created",
166
+ source: "dashboard",
167
+ userId: args.userId,
168
+ timestamp: now,
169
+ });
170
+
171
+ return id;
172
+ },
173
+ });
174
+
175
+ export const storeFromChat = mutation({
176
+ args: {
177
+ name: v.string(),
178
+ category: v.string(),
179
+ provider: v.optional(v.string()),
180
+ value: v.string(),
181
+ userId: v.optional(v.string()),
182
+ },
183
+ handler: async (ctx, args) => {
184
+ const key = getEncryptionKey();
185
+ const { encrypted, iv } = encodeSecret(args.value, key);
186
+ const masked = maskSecret(args.value);
187
+ const now = Date.now();
188
+
189
+ const id = await ctx.db.insert("vault", {
190
+ name: args.name,
191
+ category: args.category,
192
+ provider: args.provider,
193
+ encryptedValue: encrypted,
194
+ iv,
195
+ maskedValue: masked,
196
+ isActive: true,
197
+ accessCount: 0,
198
+ userId: args.userId,
199
+ createdAt: now,
200
+ updatedAt: now,
201
+ });
202
+
203
+ await ctx.db.insert("vaultAuditLog", {
204
+ vaultEntryId: id,
205
+ action: "auto_captured",
206
+ source: "chat",
207
+ userId: args.userId,
208
+ timestamp: now,
209
+ });
210
+
211
+ return { id, masked };
212
+ },
213
+ });
214
+
215
+ export const update = mutation({
216
+ args: {
217
+ id: v.id("vault"),
218
+ name: v.optional(v.string()),
219
+ value: v.optional(v.string()),
220
+ isActive: v.optional(v.boolean()),
221
+ expiresAt: v.optional(v.number()),
222
+ userId: v.optional(v.string()),
223
+ },
224
+ handler: async (ctx, args) => {
225
+ const existing = await ctx.db.get(args.id);
226
+ if (!existing) throw new Error("Vault entry not found");
227
+
228
+ const updates: Record<string, unknown> = { updatedAt: Date.now() };
229
+
230
+ if (args.name !== undefined) updates.name = args.name;
231
+ if (args.isActive !== undefined) updates.isActive = args.isActive;
232
+ if (args.expiresAt !== undefined) updates.expiresAt = args.expiresAt;
233
+
234
+ if (args.value) {
235
+ const key = getEncryptionKey();
236
+ const { encrypted, iv } = encodeSecret(args.value, key);
237
+ updates.encryptedValue = encrypted;
238
+ updates.iv = iv;
239
+ updates.maskedValue = maskSecret(args.value);
240
+ }
241
+
242
+ await ctx.db.patch(args.id, updates);
243
+
244
+ await ctx.db.insert("vaultAuditLog", {
245
+ vaultEntryId: args.id,
246
+ action: "updated",
247
+ source: "dashboard",
248
+ userId: args.userId,
249
+ timestamp: Date.now(),
250
+ });
251
+ },
252
+ });
253
+
254
+ export const remove = mutation({
255
+ args: {
256
+ id: v.id("vault"),
257
+ userId: v.optional(v.string()),
258
+ },
259
+ handler: async (ctx, args) => {
260
+ const existing = await ctx.db.get(args.id);
261
+ if (!existing) throw new Error("Vault entry not found");
262
+
263
+ await ctx.db.insert("vaultAuditLog", {
264
+ vaultEntryId: args.id,
265
+ action: "deleted",
266
+ source: "dashboard",
267
+ userId: args.userId,
268
+ timestamp: Date.now(),
269
+ });
270
+
271
+ await ctx.db.delete(args.id);
272
+ },
273
+ });
274
+
275
+ // Internal-only: Retrieve decrypted value (only callable from other Convex functions)
276
+ export const retrieveSecret = internalMutation({
277
+ args: {
278
+ id: v.id("vault"),
279
+ userId: v.optional(v.string()),
280
+ },
281
+ handler: async (ctx, args) => {
282
+ const entry = await ctx.db.get(args.id);
283
+ if (!entry) throw new Error("Vault entry not found");
284
+ if (!entry.isActive) throw new Error("Vault entry is disabled");
285
+
286
+ await ctx.db.patch(args.id, {
287
+ lastAccessedAt: Date.now(),
288
+ accessCount: entry.accessCount + 1,
289
+ });
290
+
291
+ await ctx.db.insert("vaultAuditLog", {
292
+ vaultEntryId: args.id,
293
+ action: "accessed",
294
+ source: "agent",
295
+ userId: args.userId,
296
+ timestamp: Date.now(),
297
+ });
298
+
299
+ const key = getEncryptionKey();
300
+ const decrypted = decodeSecret(entry.encryptedValue, entry.iv, key);
301
+ return decrypted;
302
+ },
303
+ });
304
+
305
+ // ---- Secret Detection Utility ----
306
+
307
+ export const detectSecrets = query({
308
+ args: { text: v.string() },
309
+ handler: async (_ctx, args) => {
310
+ const detected: Array<{
311
+ match: string;
312
+ category: string;
313
+ provider: string;
314
+ name: string;
315
+ startIndex: number;
316
+ endIndex: number;
317
+ }> = [];
318
+
319
+ for (const { pattern, category, provider, name } of SECRET_PATTERNS) {
320
+ const regex = new RegExp(pattern, "g");
321
+ let match;
322
+ while ((match = regex.exec(args.text)) !== null) {
323
+ detected.push({
324
+ match: match[0],
325
+ category,
326
+ provider,
327
+ name,
328
+ startIndex: match.index,
329
+ endIndex: match.index + match[0].length,
330
+ });
331
+ }
332
+ }
333
+
334
+ return detected;
335
+ },
336
+ });
337
+
338
+ // Censor a message by replacing detected secrets with masked versions
339
+ export const censorMessage = mutation({
340
+ args: {
341
+ text: v.string(),
342
+ userId: v.optional(v.string()),
343
+ autoStore: v.optional(v.boolean()),
344
+ },
345
+ handler: async (ctx, args) => {
346
+ let censoredText = args.text;
347
+ const storedSecrets: Array<{ name: string; masked: string; id: unknown }> = [];
348
+
349
+ for (const { pattern, category, provider, name } of SECRET_PATTERNS) {
350
+ const regex = new RegExp(pattern, "g");
351
+ let match;
352
+ while ((match = regex.exec(args.text)) !== null) {
353
+ const secretValue = match[0];
354
+ const masked = maskSecret(secretValue);
355
+
356
+ censoredText = censoredText.replace(secretValue, `[REDACTED: ${masked}]`);
357
+
358
+ if (args.autoStore !== false) {
359
+ const key = getEncryptionKey();
360
+ const { encrypted, iv } = encodeSecret(secretValue, key);
361
+ const now = Date.now();
362
+
363
+ const id = await ctx.db.insert("vault", {
364
+ name: `${name} (auto-captured)`,
365
+ category,
366
+ provider,
367
+ encryptedValue: encrypted,
368
+ iv,
369
+ maskedValue: masked,
370
+ isActive: true,
371
+ accessCount: 0,
372
+ userId: args.userId,
373
+ createdAt: now,
374
+ updatedAt: now,
375
+ });
376
+
377
+ await ctx.db.insert("vaultAuditLog", {
378
+ vaultEntryId: id,
379
+ action: "auto_captured",
380
+ source: "chat",
381
+ userId: args.userId,
382
+ timestamp: now,
383
+ });
384
+
385
+ storedSecrets.push({ name, masked, id });
386
+ }
387
+ }
388
+ }
389
+
390
+ return {
391
+ censoredText,
392
+ secretsDetected: storedSecrets.length > 0,
393
+ storedSecrets,
394
+ originalHadSecrets: censoredText !== args.text,
395
+ };
396
+ },
397
+ });
@@ -16,9 +16,13 @@ declare module "@tanstack/react-router" {
16
16
  }
17
17
 
18
18
  // Initialize Convex client
19
- const convexUrl =
20
- (import.meta as any).env?.VITE_CONVEX_URL ||
21
- "https://hip-cardinal-943.convex.cloud";
19
+ const convexUrl = (import.meta as any).env?.VITE_CONVEX_URL;
20
+ if (!convexUrl) {
21
+ throw new Error(
22
+ "Missing VITE_CONVEX_URL environment variable. " +
23
+ "Run 'agentforge dashboard' from your project root, or create dashboard/.env.local with VITE_CONVEX_URL=<your-url>"
24
+ );
25
+ }
22
26
  const convex = new ConvexReactClient(convexUrl);
23
27
 
24
28
  // Render