@agentforge-ai/cli 0.4.2 → 0.5.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 (67) hide show
  1. package/dist/default/README.md +81 -81
  2. package/dist/default/convex/agents.ts +204 -0
  3. package/dist/default/convex/apiKeys.ts +133 -0
  4. package/dist/default/convex/cronJobs.ts +224 -0
  5. package/dist/default/convex/files.ts +103 -0
  6. package/dist/default/convex/folders.ts +110 -0
  7. package/dist/default/convex/heartbeat.ts +371 -0
  8. package/dist/default/convex/logs.ts +66 -0
  9. package/dist/default/convex/mastraIntegration.ts +184 -0
  10. package/dist/default/convex/mcpConnections.ts +127 -0
  11. package/dist/default/convex/messages.ts +90 -0
  12. package/dist/default/convex/projects.ts +114 -0
  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 +383 -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 +279 -50
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/default/README.md +81 -81
  37. package/templates/default/convex/agents.ts +204 -0
  38. package/templates/default/convex/apiKeys.ts +133 -0
  39. package/templates/default/convex/cronJobs.ts +224 -0
  40. package/templates/default/convex/files.ts +103 -0
  41. package/templates/default/convex/folders.ts +110 -0
  42. package/templates/default/convex/heartbeat.ts +371 -0
  43. package/templates/default/convex/logs.ts +66 -0
  44. package/templates/default/convex/mastraIntegration.ts +184 -0
  45. package/templates/default/convex/mcpConnections.ts +127 -0
  46. package/templates/default/convex/messages.ts +90 -0
  47. package/templates/default/convex/projects.ts +114 -0
  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 +383 -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,383 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query, action } 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
+ // Simple XOR-based encoding (in production, use proper AES-256-GCM with a KMS)
39
+ // This provides a layer of obfuscation in the database
40
+ function encodeSecret(value: string, key: string): { encrypted: string; iv: string } {
41
+ const iv = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, "0")).join("");
42
+ const combined = key + iv;
43
+ let encrypted = "";
44
+ for (let i = 0; i < value.length; i++) {
45
+ const charCode = value.charCodeAt(i) ^ combined.charCodeAt(i % combined.length);
46
+ encrypted += charCode.toString(16).padStart(4, "0");
47
+ }
48
+ return { encrypted, iv };
49
+ }
50
+
51
+ function decodeSecret(encrypted: string, iv: string, key: string): string {
52
+ const combined = key + iv;
53
+ let decoded = "";
54
+ for (let i = 0; i < encrypted.length; i += 4) {
55
+ const charCode = parseInt(encrypted.substring(i, i + 4), 16) ^ combined.charCodeAt((i / 4) % combined.length);
56
+ decoded += String.fromCharCode(charCode);
57
+ }
58
+ return decoded;
59
+ }
60
+
61
+ // The encryption key should come from environment variables in production
62
+ const VAULT_ENCRYPTION_KEY = "agentforge-vault-key-change-in-production-2026";
63
+
64
+ // ---- Queries ----
65
+
66
+ export const list = query({
67
+ args: {
68
+ userId: v.optional(v.string()),
69
+ category: v.optional(v.string()),
70
+ },
71
+ handler: async (ctx, args) => {
72
+ let q = ctx.db.query("vault");
73
+ if (args.userId) {
74
+ q = ctx.db.query("vault").withIndex("byUserId", (q) => q.eq("userId", args.userId));
75
+ }
76
+ const entries = await q.order("desc").take(100);
77
+ // Never return encrypted values - only masked
78
+ return entries.map((entry) => ({
79
+ _id: entry._id,
80
+ name: entry.name,
81
+ category: entry.category,
82
+ provider: entry.provider,
83
+ maskedValue: entry.maskedValue,
84
+ isActive: entry.isActive,
85
+ expiresAt: entry.expiresAt,
86
+ lastAccessedAt: entry.lastAccessedAt,
87
+ accessCount: entry.accessCount,
88
+ createdAt: entry.createdAt,
89
+ updatedAt: entry.updatedAt,
90
+ }));
91
+ },
92
+ });
93
+
94
+ export const getAuditLog = query({
95
+ args: {
96
+ vaultEntryId: v.optional(v.id("vault")),
97
+ limit: v.optional(v.number()),
98
+ },
99
+ handler: async (ctx, args) => {
100
+ const limit = args.limit ?? 50;
101
+ if (args.vaultEntryId) {
102
+ return ctx.db
103
+ .query("vaultAuditLog")
104
+ .withIndex("byVaultEntryId", (q) => q.eq("vaultEntryId", args.vaultEntryId!))
105
+ .order("desc")
106
+ .take(limit);
107
+ }
108
+ return ctx.db.query("vaultAuditLog").order("desc").take(limit);
109
+ },
110
+ });
111
+
112
+ // ---- Mutations ----
113
+
114
+ export const store = mutation({
115
+ args: {
116
+ name: v.string(),
117
+ category: v.string(),
118
+ provider: v.optional(v.string()),
119
+ value: v.string(), // Raw secret value - will be encrypted before storage
120
+ userId: v.optional(v.string()),
121
+ expiresAt: v.optional(v.number()),
122
+ },
123
+ handler: async (ctx, args) => {
124
+ const { encrypted, iv } = encodeSecret(args.value, VAULT_ENCRYPTION_KEY);
125
+ const masked = maskSecret(args.value);
126
+ const now = Date.now();
127
+
128
+ const id = await ctx.db.insert("vault", {
129
+ name: args.name,
130
+ category: args.category,
131
+ provider: args.provider,
132
+ encryptedValue: encrypted,
133
+ iv,
134
+ maskedValue: masked,
135
+ isActive: true,
136
+ expiresAt: args.expiresAt,
137
+ lastAccessedAt: undefined,
138
+ accessCount: 0,
139
+ userId: args.userId,
140
+ createdAt: now,
141
+ updatedAt: now,
142
+ });
143
+
144
+ // Audit log
145
+ await ctx.db.insert("vaultAuditLog", {
146
+ vaultEntryId: id,
147
+ action: "created",
148
+ source: "dashboard",
149
+ userId: args.userId,
150
+ timestamp: now,
151
+ });
152
+
153
+ return id;
154
+ },
155
+ });
156
+
157
+ export const storeFromChat = mutation({
158
+ args: {
159
+ name: v.string(),
160
+ category: v.string(),
161
+ provider: v.optional(v.string()),
162
+ value: v.string(),
163
+ userId: v.optional(v.string()),
164
+ },
165
+ handler: async (ctx, args) => {
166
+ const { encrypted, iv } = encodeSecret(args.value, VAULT_ENCRYPTION_KEY);
167
+ const masked = maskSecret(args.value);
168
+ const now = Date.now();
169
+
170
+ const id = await ctx.db.insert("vault", {
171
+ name: args.name,
172
+ category: args.category,
173
+ provider: args.provider,
174
+ encryptedValue: encrypted,
175
+ iv,
176
+ maskedValue: masked,
177
+ isActive: true,
178
+ accessCount: 0,
179
+ userId: args.userId,
180
+ createdAt: now,
181
+ updatedAt: now,
182
+ });
183
+
184
+ // Audit log with auto_captured source
185
+ await ctx.db.insert("vaultAuditLog", {
186
+ vaultEntryId: id,
187
+ action: "auto_captured",
188
+ source: "chat",
189
+ userId: args.userId,
190
+ timestamp: now,
191
+ });
192
+
193
+ return { id, masked };
194
+ },
195
+ });
196
+
197
+ export const update = mutation({
198
+ args: {
199
+ id: v.id("vault"),
200
+ name: v.optional(v.string()),
201
+ value: v.optional(v.string()),
202
+ isActive: v.optional(v.boolean()),
203
+ expiresAt: v.optional(v.number()),
204
+ userId: v.optional(v.string()),
205
+ },
206
+ handler: async (ctx, args) => {
207
+ const existing = await ctx.db.get(args.id);
208
+ if (!existing) throw new Error("Vault entry not found");
209
+
210
+ const updates: any = { updatedAt: Date.now() };
211
+
212
+ if (args.name !== undefined) updates.name = args.name;
213
+ if (args.isActive !== undefined) updates.isActive = args.isActive;
214
+ if (args.expiresAt !== undefined) updates.expiresAt = args.expiresAt;
215
+
216
+ if (args.value) {
217
+ const { encrypted, iv } = encodeSecret(args.value, VAULT_ENCRYPTION_KEY);
218
+ updates.encryptedValue = encrypted;
219
+ updates.iv = iv;
220
+ updates.maskedValue = maskSecret(args.value);
221
+ }
222
+
223
+ await ctx.db.patch(args.id, updates);
224
+
225
+ await ctx.db.insert("vaultAuditLog", {
226
+ vaultEntryId: args.id,
227
+ action: "updated",
228
+ source: "dashboard",
229
+ userId: args.userId,
230
+ timestamp: Date.now(),
231
+ });
232
+ },
233
+ });
234
+
235
+ export const remove = mutation({
236
+ args: {
237
+ id: v.id("vault"),
238
+ userId: v.optional(v.string()),
239
+ },
240
+ handler: async (ctx, args) => {
241
+ const existing = await ctx.db.get(args.id);
242
+ if (!existing) throw new Error("Vault entry not found");
243
+
244
+ // Log deletion before removing
245
+ await ctx.db.insert("vaultAuditLog", {
246
+ vaultEntryId: args.id,
247
+ action: "deleted",
248
+ source: "dashboard",
249
+ userId: args.userId,
250
+ timestamp: Date.now(),
251
+ });
252
+
253
+ await ctx.db.delete(args.id);
254
+ },
255
+ });
256
+
257
+ // Retrieve decrypted value (for internal agent use only - never expose to frontend)
258
+ export const retrieveSecret = mutation({
259
+ args: {
260
+ id: v.id("vault"),
261
+ userId: v.optional(v.string()),
262
+ },
263
+ handler: async (ctx, args) => {
264
+ const entry = await ctx.db.get(args.id);
265
+ if (!entry) throw new Error("Vault entry not found");
266
+ if (!entry.isActive) throw new Error("Vault entry is disabled");
267
+
268
+ // Update access tracking
269
+ await ctx.db.patch(args.id, {
270
+ lastAccessedAt: Date.now(),
271
+ accessCount: entry.accessCount + 1,
272
+ });
273
+
274
+ // Audit log
275
+ await ctx.db.insert("vaultAuditLog", {
276
+ vaultEntryId: args.id,
277
+ action: "accessed",
278
+ source: "agent",
279
+ userId: args.userId,
280
+ timestamp: Date.now(),
281
+ });
282
+
283
+ // Decrypt and return
284
+ const decrypted = decodeSecret(entry.encryptedValue, entry.iv, VAULT_ENCRYPTION_KEY);
285
+ return decrypted;
286
+ },
287
+ });
288
+
289
+ // ---- Secret Detection Utility ----
290
+ // This is exported for use by the chat message handler
291
+
292
+ export const detectSecrets = query({
293
+ args: { text: v.string() },
294
+ handler: async (_ctx, args) => {
295
+ const detected: Array<{
296
+ match: string;
297
+ category: string;
298
+ provider: string;
299
+ name: string;
300
+ startIndex: number;
301
+ endIndex: number;
302
+ }> = [];
303
+
304
+ for (const { pattern, category, provider, name } of SECRET_PATTERNS) {
305
+ const regex = new RegExp(pattern, "g");
306
+ let match;
307
+ while ((match = regex.exec(args.text)) !== null) {
308
+ detected.push({
309
+ match: match[0],
310
+ category,
311
+ provider,
312
+ name,
313
+ startIndex: match.index,
314
+ endIndex: match.index + match[0].length,
315
+ });
316
+ }
317
+ }
318
+
319
+ return detected;
320
+ },
321
+ });
322
+
323
+ // Censor a message by replacing detected secrets with masked versions
324
+ export const censorMessage = mutation({
325
+ args: {
326
+ text: v.string(),
327
+ userId: v.optional(v.string()),
328
+ autoStore: v.optional(v.boolean()),
329
+ },
330
+ handler: async (ctx, args) => {
331
+ let censoredText = args.text;
332
+ const storedSecrets: Array<{ name: string; masked: string; id: any }> = [];
333
+
334
+ for (const { pattern, category, provider, name } of SECRET_PATTERNS) {
335
+ const regex = new RegExp(pattern, "g");
336
+ let match;
337
+ while ((match = regex.exec(args.text)) !== null) {
338
+ const secretValue = match[0];
339
+ const masked = maskSecret(secretValue);
340
+
341
+ // Replace in censored text
342
+ censoredText = censoredText.replace(secretValue, `[REDACTED: ${masked}]`);
343
+
344
+ // Auto-store if enabled
345
+ if (args.autoStore !== false) {
346
+ const { encrypted, iv } = encodeSecret(secretValue, VAULT_ENCRYPTION_KEY);
347
+ const now = Date.now();
348
+
349
+ const id = await ctx.db.insert("vault", {
350
+ name: `${name} (auto-captured)`,
351
+ category,
352
+ provider,
353
+ encryptedValue: encrypted,
354
+ iv,
355
+ maskedValue: masked,
356
+ isActive: true,
357
+ accessCount: 0,
358
+ userId: args.userId,
359
+ createdAt: now,
360
+ updatedAt: now,
361
+ });
362
+
363
+ await ctx.db.insert("vaultAuditLog", {
364
+ vaultEntryId: id,
365
+ action: "auto_captured",
366
+ source: "chat",
367
+ userId: args.userId,
368
+ timestamp: now,
369
+ });
370
+
371
+ storedSecrets.push({ name, masked, id });
372
+ }
373
+ }
374
+ }
375
+
376
+ return {
377
+ censoredText,
378
+ secretsDetected: storedSecrets.length > 0,
379
+ storedSecrets,
380
+ originalHadSecrets: censoredText !== args.text,
381
+ };
382
+ },
383
+ });
@@ -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