@nordsym/apiclaw 1.5.12 → 1.5.14

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 (70) hide show
  1. package/dist/bin.js +1 -1
  2. package/dist/cli/commands/mcp-install.js +6 -6
  3. package/dist/cli/index.js +7 -0
  4. package/dist/convex/adminActivate.js +46 -0
  5. package/dist/convex/adminStats.js +41 -0
  6. package/dist/convex/agents.js +498 -0
  7. package/dist/convex/analytics.js +165 -0
  8. package/dist/convex/billing.js +654 -0
  9. package/dist/convex/capabilities.js +144 -0
  10. package/dist/convex/chains.js +1041 -0
  11. package/dist/convex/credits.js +185 -0
  12. package/dist/convex/crons.js +16 -0
  13. package/dist/convex/directCall.js +626 -0
  14. package/dist/convex/earnProgress.js +648 -0
  15. package/dist/convex/email.js +299 -0
  16. package/dist/convex/feedback.js +226 -0
  17. package/dist/convex/http.js +909 -0
  18. package/dist/convex/logs.js +486 -0
  19. package/dist/convex/mou.js +81 -0
  20. package/dist/convex/providerKeys.js +256 -0
  21. package/dist/convex/providers.js +755 -0
  22. package/dist/convex/purchases.js +156 -0
  23. package/dist/convex/ratelimit.js +90 -0
  24. package/dist/convex/schema.js +709 -0
  25. package/dist/convex/searchLogs.js +128 -0
  26. package/dist/convex/spendAlerts.js +379 -0
  27. package/dist/convex/stripeActions.js +410 -0
  28. package/dist/convex/teams.js +214 -0
  29. package/dist/convex/telemetry.js +73 -0
  30. package/dist/convex/usage.js +228 -0
  31. package/dist/convex/waitlist.js +48 -0
  32. package/dist/convex/webhooks.js +409 -0
  33. package/dist/convex/workspaces.js +879 -0
  34. package/dist/src/analytics.js +129 -0
  35. package/dist/src/bin.js +17 -0
  36. package/dist/src/capability-router.js +240 -0
  37. package/dist/src/chainExecutor.js +451 -0
  38. package/dist/src/chainResolver.js +518 -0
  39. package/dist/src/cli/commands/doctor.js +324 -0
  40. package/dist/src/cli/commands/mcp-install.js +255 -0
  41. package/dist/src/cli/commands/restore.js +259 -0
  42. package/dist/src/cli/commands/setup.js +205 -0
  43. package/dist/src/cli/commands/uninstall.js +188 -0
  44. package/dist/src/cli/index.js +111 -0
  45. package/dist/src/cli.js +302 -0
  46. package/dist/src/confirmation.js +240 -0
  47. package/dist/src/credentials.js +357 -0
  48. package/dist/src/credits.js +260 -0
  49. package/dist/src/crypto.js +66 -0
  50. package/dist/src/discovery.js +504 -0
  51. package/dist/src/enterprise/env.js +123 -0
  52. package/dist/src/enterprise/script-generator.js +460 -0
  53. package/dist/src/execute-dynamic.js +473 -0
  54. package/dist/src/execute.js +1727 -0
  55. package/dist/src/index.js +2062 -0
  56. package/dist/src/metered.js +80 -0
  57. package/dist/src/open-apis.js +276 -0
  58. package/dist/src/proxy.js +28 -0
  59. package/dist/src/session.js +86 -0
  60. package/dist/src/stripe.js +407 -0
  61. package/dist/src/telemetry.js +49 -0
  62. package/dist/src/types.js +2 -0
  63. package/dist/src/utils/backup.js +181 -0
  64. package/dist/src/utils/config.js +220 -0
  65. package/dist/src/utils/os.js +105 -0
  66. package/dist/src/utils/paths.js +159 -0
  67. package/package.json +1 -1
  68. package/src/bin.ts +1 -1
  69. package/src/cli/commands/mcp-install.ts +6 -6
  70. package/src/cli/index.ts +8 -0
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * - No args or MCP-related args → Run MCP server
6
6
  * - setup/doctor/restore/uninstall → Run CLI
7
7
  */
8
- const cliCommands = ['setup', 'mcp-install', 'doctor', 'restore', 'uninstall', 'help', '--help', '-h', '--version', '-V'];
8
+ const cliCommands = ['setup', 'mcp-install', 'mcp-uninstall', 'doctor', 'restore', 'uninstall', 'help', '--help', '-h', '--version', '-V'];
9
9
  const firstArg = process.argv[2];
10
10
  if (!firstArg || !cliCommands.includes(firstArg)) {
11
11
  // Run MCP server
@@ -234,9 +234,9 @@ async function mcpInstallCommand(options) {
234
234
  else if (successCount > 0) {
235
235
  console.log(chalk_1.default.green('\n✅ APIClaw installed successfully!\n'));
236
236
  console.log(chalk_1.default.bold('What you get:\n'));
237
- console.log(chalk_1.default.cyan(' 🔍 Search') + ' 22,000+ APIs indexed');
238
- console.log(chalk_1.default.cyan(' 🌐 Open APIs') + ' 1,600 APIs - no keys needed');
239
- console.log(chalk_1.default.cyan(' 🔑 Direct Call') + ' 1,500+ endpoints - no keys needed');
237
+ console.log(chalk_1.default.cyan(' 🔍 Search') + ' 22,000+ APIs to discover');
238
+ console.log(chalk_1.default.cyan(' 🌐 Open APIs') + ' 1,600 free APIs');
239
+ console.log(chalk_1.default.cyan(' 🔑 Direct Call') + ' 1,500+ premium (APIClaw manages keys)');
240
240
  console.log('');
241
241
  console.log('Next:');
242
242
  console.log(' 1. Restart your MCP client');
@@ -247,9 +247,9 @@ async function mcpInstallCommand(options) {
247
247
  else {
248
248
  console.log(chalk_1.default.yellow('\n✅ APIClaw already installed in all clients.\n'));
249
249
  console.log(chalk_1.default.bold('What you have:\n'));
250
- console.log(chalk_1.default.cyan(' 🔍 Search') + ' 22,000+ APIs indexed');
251
- console.log(chalk_1.default.cyan(' 🌐 Open APIs') + ' 1,600 APIs - no keys needed');
252
- console.log(chalk_1.default.cyan(' 🔑 Direct Call') + ' 1,500+ endpoints - no keys needed');
250
+ console.log(chalk_1.default.cyan(' 🔍 Search') + ' 22,000+ APIs to discover');
251
+ console.log(chalk_1.default.cyan(' 🌐 Open APIs') + ' 1,600 free APIs');
252
+ console.log(chalk_1.default.cyan(' 🔑 Direct Call') + ' 1,500+ premium (APIClaw manages keys)');
253
253
  console.log('');
254
254
  console.log('Run with --force to reinstall (coming soon).\n');
255
255
  }
package/dist/cli/index.js CHANGED
@@ -96,6 +96,13 @@ program
96
96
  .option('--dry-run', 'Show what would be done without making changes')
97
97
  .option('-f, --force', 'Remove even if not configured')
98
98
  .action(uninstallCommand);
99
+ // MCP Uninstall alias - same as uninstall but for consistency with mcp-install
100
+ program
101
+ .command('mcp-uninstall')
102
+ .description('Remove APIClaw from Claude Desktop or Claude Code MCP config')
103
+ .option('-c, --client <client>', 'Target specific client (claude-desktop, claude-code)')
104
+ .option('--dry-run', 'Show what would be done without making changes')
105
+ .action(uninstallCommand);
99
106
  // Parse and execute
100
107
  program.parse();
101
108
  // Show help if no command provided
@@ -0,0 +1,46 @@
1
+ import { mutation } from "./_generated/server";
2
+ import { v } from "convex/values";
3
+ export const activateWorkspace = mutation({
4
+ args: { workspaceId: v.id("workspaces") },
5
+ handler: async (ctx, { workspaceId }) => {
6
+ const workspace = await ctx.db.get(workspaceId);
7
+ if (!workspace) {
8
+ return { success: false, error: "not_found" };
9
+ }
10
+ await ctx.db.patch(workspaceId, {
11
+ status: "active",
12
+ tier: "backer", // Give Hivr bees backer status
13
+ weeklyUsageLimit: 999999,
14
+ usageLimit: 999999,
15
+ backerUntil: new Date("2026-12-31T23:59:59Z").getTime(), // Founding Backer until end of 2026
16
+ updatedAt: Date.now(),
17
+ });
18
+ return { success: true };
19
+ },
20
+ });
21
+ function generateToken() {
22
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
23
+ let result = '';
24
+ for (let i = 0; i < 32; i++) {
25
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
26
+ }
27
+ return result;
28
+ }
29
+ export const createSessionForWorkspace = mutation({
30
+ args: { workspaceId: v.id("workspaces") },
31
+ handler: async (ctx, { workspaceId }) => {
32
+ const workspace = await ctx.db.get(workspaceId);
33
+ if (!workspace || workspace.status !== "active") {
34
+ return { success: false, error: "workspace_not_active" };
35
+ }
36
+ const sessionToken = "apiclaw_" + generateToken();
37
+ await ctx.db.insert("agentSessions", {
38
+ workspaceId,
39
+ sessionToken,
40
+ fingerprint: "hivr-bees",
41
+ lastUsedAt: Date.now(),
42
+ createdAt: Date.now(),
43
+ });
44
+ return { success: true, sessionToken };
45
+ },
46
+ });
@@ -0,0 +1,41 @@
1
+ import { query } from "./_generated/server";
2
+ // Get total user/workspace count
3
+ export const getTotalWorkspaces = query({
4
+ args: {},
5
+ handler: async (ctx) => {
6
+ const workspaces = await ctx.db.query("workspaces").collect();
7
+ const providers = await ctx.db.query("providers").collect();
8
+ return {
9
+ totalWorkspaces: workspaces.length,
10
+ totalProviders: providers.length,
11
+ activeWorkspaces: workspaces.filter(w => w.status === "active").length,
12
+ backers: workspaces.filter(w => w.tier === "backer").length,
13
+ workspaceBreakdown: {
14
+ free: workspaces.filter(w => w.tier === "free").length,
15
+ pro: workspaces.filter(w => w.tier === "pro").length,
16
+ enterprise: workspaces.filter(w => w.tier === "enterprise").length,
17
+ backer: workspaces.filter(w => w.tier === "backer").length,
18
+ },
19
+ providerBreakdown: {
20
+ pending: providers.filter(p => p.status === "pending").length,
21
+ approved: providers.filter(p => p.status === "approved").length,
22
+ rejected: providers.filter(p => p.status === "rejected").length,
23
+ }
24
+ };
25
+ },
26
+ });
27
+ // List all workspace emails (for inspection)
28
+ export const listWorkspaces = query({
29
+ args: {},
30
+ handler: async (ctx) => {
31
+ const workspaces = await ctx.db.query("workspaces").collect();
32
+ return workspaces.map(w => ({
33
+ email: w.email,
34
+ status: w.status,
35
+ tier: w.tier,
36
+ usageCount: w.usageCount,
37
+ createdAt: w.createdAt,
38
+ lastActiveAt: w.lastActiveAt,
39
+ }));
40
+ },
41
+ });
@@ -0,0 +1,498 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+ // ============================================
4
+ // AGENT NAME GENERATION
5
+ // ============================================
6
+ const ADJECTIVES = [
7
+ "Crimson", "Azure", "Golden", "Silver", "Obsidian",
8
+ "Emerald", "Sapphire", "Violet", "Amber", "Jade",
9
+ "Scarlet", "Cobalt", "Onyx", "Ruby", "Pearl",
10
+ "Shadow", "Storm", "Frost", "Blaze", "Thunder",
11
+ "Swift", "Silent", "Bright", "Dark", "Wild",
12
+ "Noble", "Fierce", "Cosmic", "Quantum", "Neural",
13
+ ];
14
+ const NOUNS = [
15
+ "Phoenix", "Falcon", "Dragon", "Wolf", "Raven",
16
+ "Serpent", "Tiger", "Hawk", "Panther", "Lynx",
17
+ "Cipher", "Vector", "Prism", "Nexus", "Core",
18
+ "Agent", "Oracle", "Sentinel", "Phantom", "Vanguard",
19
+ "Forge", "Spark", "Pulse", "Echo", "Byte",
20
+ "Matrix", "Vertex", "Helix", "Nova", "Zenith",
21
+ ];
22
+ function generateAgentName() {
23
+ const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
24
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
25
+ return `${adj} ${noun}`;
26
+ }
27
+ function generateUUID() {
28
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
29
+ const r = Math.random() * 16 | 0;
30
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
31
+ return v.toString(16);
32
+ });
33
+ }
34
+ // ============================================
35
+ // MAIN AGENT QUERIES
36
+ // ============================================
37
+ /**
38
+ * Get main agent info for a workspace
39
+ */
40
+ export const getMainAgent = query({
41
+ args: { token: v.string() },
42
+ handler: async (ctx, { token }) => {
43
+ const session = await ctx.db
44
+ .query("agentSessions")
45
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
46
+ .first();
47
+ if (!session) {
48
+ return null;
49
+ }
50
+ const workspace = await ctx.db.get(session.workspaceId);
51
+ if (!workspace) {
52
+ return null;
53
+ }
54
+ return {
55
+ workspaceId: workspace._id,
56
+ email: workspace.email,
57
+ mainAgentId: workspace.mainAgentId || null,
58
+ mainAgentName: workspace.mainAgentName || null,
59
+ aiBackend: workspace.aiBackend || null,
60
+ usageCount: workspace.usageCount,
61
+ createdAt: workspace.createdAt,
62
+ };
63
+ },
64
+ });
65
+ /**
66
+ * Rename the main agent
67
+ */
68
+ export const renameMainAgent = mutation({
69
+ args: {
70
+ token: v.string(),
71
+ name: v.string(),
72
+ },
73
+ handler: async (ctx, { token, name }) => {
74
+ const session = await ctx.db
75
+ .query("agentSessions")
76
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
77
+ .first();
78
+ if (!session) {
79
+ throw new Error("Invalid session");
80
+ }
81
+ const workspace = await ctx.db.get(session.workspaceId);
82
+ if (!workspace) {
83
+ throw new Error("Workspace not found");
84
+ }
85
+ // Validate name length
86
+ const trimmedName = name.trim();
87
+ if (trimmedName.length < 2 || trimmedName.length > 50) {
88
+ throw new Error("Name must be between 2 and 50 characters");
89
+ }
90
+ await ctx.db.patch(workspace._id, {
91
+ mainAgentName: trimmedName,
92
+ updatedAt: Date.now(),
93
+ });
94
+ return { success: true, name: trimmedName };
95
+ },
96
+ });
97
+ /**
98
+ * Initialize main agent (auto-generate name and ID if not set)
99
+ * Called on first API call
100
+ */
101
+ export const ensureMainAgent = mutation({
102
+ args: { workspaceId: v.id("workspaces") },
103
+ handler: async (ctx, { workspaceId }) => {
104
+ const workspace = await ctx.db.get(workspaceId);
105
+ if (!workspace) {
106
+ throw new Error("Workspace not found");
107
+ }
108
+ // Already initialized
109
+ if (workspace.mainAgentId && workspace.mainAgentName) {
110
+ return {
111
+ mainAgentId: workspace.mainAgentId,
112
+ mainAgentName: workspace.mainAgentName,
113
+ created: false,
114
+ };
115
+ }
116
+ const mainAgentId = workspace.mainAgentId || generateUUID();
117
+ const mainAgentName = workspace.mainAgentName || generateAgentName();
118
+ await ctx.db.patch(workspaceId, {
119
+ mainAgentId,
120
+ mainAgentName,
121
+ updatedAt: Date.now(),
122
+ });
123
+ return {
124
+ mainAgentId,
125
+ mainAgentName,
126
+ created: true,
127
+ };
128
+ },
129
+ });
130
+ // ============================================
131
+ // SUBAGENT QUERIES
132
+ // ============================================
133
+ /**
134
+ * Get all subagents for a workspace
135
+ */
136
+ export const getSubagents = query({
137
+ args: {
138
+ token: v.string(),
139
+ limit: v.optional(v.number()),
140
+ },
141
+ handler: async (ctx, { token, limit = 50 }) => {
142
+ const session = await ctx.db
143
+ .query("agentSessions")
144
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
145
+ .first();
146
+ if (!session) {
147
+ return { subagents: [], total: 0 };
148
+ }
149
+ const subagents = await ctx.db
150
+ .query("subagents")
151
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
152
+ .order("desc")
153
+ .take(limit);
154
+ // Sort by lastActiveAt descending
155
+ const sorted = subagents.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
156
+ return {
157
+ subagents: sorted.map((s) => ({
158
+ id: s._id,
159
+ subagentId: s.subagentId,
160
+ name: s.name || s.subagentId,
161
+ callCount: s.callCount,
162
+ firstSeenAt: s.firstSeenAt,
163
+ lastActiveAt: s.lastActiveAt,
164
+ })),
165
+ total: subagents.length,
166
+ };
167
+ },
168
+ });
169
+ /**
170
+ * Get stats for a specific subagent
171
+ */
172
+ export const getSubagentStats = query({
173
+ args: {
174
+ token: v.string(),
175
+ subagentId: v.string(),
176
+ },
177
+ handler: async (ctx, { token, subagentId }) => {
178
+ const session = await ctx.db
179
+ .query("agentSessions")
180
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
181
+ .first();
182
+ if (!session) {
183
+ return null;
184
+ }
185
+ const subagent = await ctx.db
186
+ .query("subagents")
187
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", session.workspaceId).eq("subagentId", subagentId))
188
+ .first();
189
+ if (!subagent) {
190
+ return null;
191
+ }
192
+ // Get recent logs for this subagent
193
+ const logs = await ctx.db
194
+ .query("apiLogs")
195
+ .withIndex("by_subagentId", (q) => q.eq("subagentId", subagentId))
196
+ .order("desc")
197
+ .take(100);
198
+ const successCount = logs.filter((l) => l.status === "success").length;
199
+ const errorCount = logs.filter((l) => l.status === "error").length;
200
+ const avgLatency = logs.length > 0
201
+ ? Math.round(logs.reduce((sum, l) => sum + l.latencyMs, 0) / logs.length)
202
+ : 0;
203
+ // Group by provider
204
+ const byProvider = {};
205
+ for (const log of logs) {
206
+ byProvider[log.provider] = (byProvider[log.provider] || 0) + 1;
207
+ }
208
+ return {
209
+ subagentId: subagent.subagentId,
210
+ name: subagent.name || subagent.subagentId,
211
+ callCount: subagent.callCount,
212
+ successCount,
213
+ errorCount,
214
+ successRate: logs.length > 0 ? Math.round((successCount / logs.length) * 100) : 0,
215
+ avgLatency,
216
+ firstSeenAt: subagent.firstSeenAt,
217
+ lastActiveAt: subagent.lastActiveAt,
218
+ byProvider: Object.entries(byProvider)
219
+ .map(([provider, count]) => ({ provider, count }))
220
+ .sort((a, b) => b.count - a.count),
221
+ };
222
+ },
223
+ });
224
+ /**
225
+ * Rename a subagent
226
+ */
227
+ export const renameSubagent = mutation({
228
+ args: {
229
+ token: v.string(),
230
+ subagentId: v.string(),
231
+ name: v.string(),
232
+ },
233
+ handler: async (ctx, { token, subagentId, name }) => {
234
+ const session = await ctx.db
235
+ .query("agentSessions")
236
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
237
+ .first();
238
+ if (!session) {
239
+ throw new Error("Invalid session");
240
+ }
241
+ const subagent = await ctx.db
242
+ .query("subagents")
243
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", session.workspaceId).eq("subagentId", subagentId))
244
+ .first();
245
+ if (!subagent) {
246
+ throw new Error("Subagent not found");
247
+ }
248
+ const trimmedName = name.trim();
249
+ if (trimmedName.length < 1 || trimmedName.length > 100) {
250
+ throw new Error("Name must be between 1 and 100 characters");
251
+ }
252
+ await ctx.db.patch(subagent._id, { name: trimmedName });
253
+ return { success: true, name: trimmedName };
254
+ },
255
+ });
256
+ /**
257
+ * Track a subagent call (upsert subagent record)
258
+ * Called when X-APIClaw-Subagent header is present
259
+ */
260
+ export const trackSubagentCall = mutation({
261
+ args: {
262
+ workspaceId: v.id("workspaces"),
263
+ subagentId: v.string(),
264
+ },
265
+ handler: async (ctx, { workspaceId, subagentId }) => {
266
+ const now = Date.now();
267
+ // Find existing subagent record
268
+ const existing = await ctx.db
269
+ .query("subagents")
270
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", workspaceId).eq("subagentId", subagentId))
271
+ .first();
272
+ if (existing) {
273
+ // Increment call count
274
+ await ctx.db.patch(existing._id, {
275
+ callCount: existing.callCount + 1,
276
+ lastActiveAt: now,
277
+ });
278
+ return { id: existing._id, created: false };
279
+ }
280
+ // Create new subagent record
281
+ const id = await ctx.db.insert("subagents", {
282
+ workspaceId,
283
+ subagentId,
284
+ callCount: 1,
285
+ firstSeenAt: now,
286
+ lastActiveAt: now,
287
+ });
288
+ return { id, created: true };
289
+ },
290
+ });
291
+ // ============================================
292
+ // AGGREGATE STATS
293
+ // ============================================
294
+ // ============================================
295
+ // AGENT REGISTRATION & AI BACKEND TRACKING
296
+ // ============================================
297
+ /**
298
+ * Pre-register a task agent (subagent)
299
+ * Allows agents to be registered before they make their first call
300
+ */
301
+ export const registerTaskAgent = mutation({
302
+ args: {
303
+ token: v.string(),
304
+ subagentId: v.string(),
305
+ name: v.optional(v.string()),
306
+ description: v.optional(v.string()),
307
+ },
308
+ handler: async (ctx, { token, subagentId, name, description }) => {
309
+ const session = await ctx.db
310
+ .query("agentSessions")
311
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
312
+ .first();
313
+ if (!session) {
314
+ throw new Error("Invalid session");
315
+ }
316
+ // Validate subagentId
317
+ const trimmedId = subagentId.trim();
318
+ if (trimmedId.length < 1 || trimmedId.length > 100) {
319
+ throw new Error("Subagent ID must be between 1 and 100 characters");
320
+ }
321
+ const now = Date.now();
322
+ // Check if already exists
323
+ const existing = await ctx.db
324
+ .query("subagents")
325
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", session.workspaceId).eq("subagentId", trimmedId))
326
+ .first();
327
+ if (existing) {
328
+ // Update existing record
329
+ await ctx.db.patch(existing._id, {
330
+ name: name || existing.name,
331
+ description: description || existing.description,
332
+ isRegistered: true,
333
+ lastActiveAt: now,
334
+ });
335
+ return { id: existing._id, created: false };
336
+ }
337
+ // Create new subagent record
338
+ const id = await ctx.db.insert("subagents", {
339
+ workspaceId: session.workspaceId,
340
+ subagentId: trimmedId,
341
+ name: name,
342
+ description: description,
343
+ callCount: 0,
344
+ isRegistered: true,
345
+ firstSeenAt: now,
346
+ lastActiveAt: now,
347
+ });
348
+ return { id, created: true };
349
+ },
350
+ });
351
+ /**
352
+ * Update AI backend for workspace or subagent
353
+ * Called when X-APIClaw-AI-Backend header is present
354
+ */
355
+ export const updateAIBackend = mutation({
356
+ args: {
357
+ workspaceId: v.id("workspaces"),
358
+ subagentId: v.optional(v.string()),
359
+ aiBackend: v.string(),
360
+ },
361
+ handler: async (ctx, { workspaceId, subagentId, aiBackend }) => {
362
+ const now = Date.now();
363
+ if (subagentId) {
364
+ // Update subagent's AI backend
365
+ const subagent = await ctx.db
366
+ .query("subagents")
367
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", workspaceId).eq("subagentId", subagentId))
368
+ .first();
369
+ if (subagent) {
370
+ await ctx.db.patch(subagent._id, {
371
+ aiBackend,
372
+ lastActiveAt: now,
373
+ });
374
+ }
375
+ }
376
+ else {
377
+ // Update workspace's main agent AI backend
378
+ await ctx.db.patch(workspaceId, {
379
+ aiBackend,
380
+ aiBackendLastSeen: now,
381
+ updatedAt: now,
382
+ });
383
+ }
384
+ return { success: true };
385
+ },
386
+ });
387
+ // ============================================
388
+ // AGGREGATE STATS
389
+ // ============================================
390
+ /**
391
+ * Get agent overview for workspace (main + subagents summary)
392
+ */
393
+ export const getAgentOverview = query({
394
+ args: { token: v.string() },
395
+ handler: async (ctx, { token }) => {
396
+ const session = await ctx.db
397
+ .query("agentSessions")
398
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
399
+ .first();
400
+ if (!session) {
401
+ return null;
402
+ }
403
+ const workspace = await ctx.db.get(session.workspaceId);
404
+ if (!workspace) {
405
+ return null;
406
+ }
407
+ // Get all subagents
408
+ const subagents = await ctx.db
409
+ .query("subagents")
410
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
411
+ .collect();
412
+ // Calculate totals
413
+ const totalSubagentCalls = subagents.reduce((sum, s) => sum + s.callCount, 0);
414
+ const mainAgentCalls = workspace.usageCount - totalSubagentCalls;
415
+ // Get most active subagents (top 5)
416
+ const topSubagents = subagents
417
+ .sort((a, b) => b.callCount - a.callCount)
418
+ .slice(0, 5)
419
+ .map((s) => ({
420
+ subagentId: s.subagentId,
421
+ name: s.name || s.subagentId,
422
+ callCount: s.callCount,
423
+ lastActiveAt: s.lastActiveAt,
424
+ }));
425
+ return {
426
+ mainAgent: {
427
+ id: workspace.mainAgentId || null,
428
+ name: workspace.mainAgentName || "Unnamed Agent",
429
+ callCount: Math.max(0, mainAgentCalls), // Ensure non-negative
430
+ },
431
+ subagents: {
432
+ total: subagents.length,
433
+ totalCalls: totalSubagentCalls,
434
+ topActive: topSubagents,
435
+ },
436
+ totalCalls: workspace.usageCount,
437
+ };
438
+ },
439
+ });
440
+ /**
441
+ * Delete a subagent
442
+ */
443
+ export const deleteSubagent = mutation({
444
+ args: {
445
+ token: v.string(),
446
+ subagentId: v.string(),
447
+ },
448
+ handler: async (ctx, { token, subagentId }) => {
449
+ const session = await ctx.db
450
+ .query("agentSessions")
451
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
452
+ .first();
453
+ if (!session) {
454
+ throw new Error("Invalid session");
455
+ }
456
+ const subagent = await ctx.db
457
+ .query("subagents")
458
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", session.workspaceId).eq("subagentId", subagentId))
459
+ .first();
460
+ if (!subagent) {
461
+ throw new Error("Subagent not found");
462
+ }
463
+ await ctx.db.delete(subagent._id);
464
+ return { success: true };
465
+ },
466
+ });
467
+ /**
468
+ * Update subagent stats (call count, last active)
469
+ * Internal helper for tracking
470
+ */
471
+ export const updateSubagentStats = mutation({
472
+ args: {
473
+ token: v.string(),
474
+ subagentId: v.string(),
475
+ incrementCalls: v.optional(v.number()),
476
+ },
477
+ handler: async (ctx, { token, subagentId, incrementCalls = 1 }) => {
478
+ const session = await ctx.db
479
+ .query("agentSessions")
480
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
481
+ .first();
482
+ if (!session) {
483
+ throw new Error("Invalid session");
484
+ }
485
+ const subagent = await ctx.db
486
+ .query("subagents")
487
+ .withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", session.workspaceId).eq("subagentId", subagentId))
488
+ .first();
489
+ if (!subagent) {
490
+ throw new Error("Subagent not found");
491
+ }
492
+ await ctx.db.patch(subagent._id, {
493
+ callCount: (subagent.callCount || 0) + incrementCalls,
494
+ lastActiveAt: Date.now(),
495
+ });
496
+ return { success: true, newCallCount: (subagent.callCount || 0) + incrementCalls };
497
+ },
498
+ });