@nordsym/apiclaw 1.4.3 → 1.4.4

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to APIClaw.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Fixed
8
+ - `doctor` connectivity check now uses resilient fallbacks:
9
+ - `${APICLAW_API_URL}/health`
10
+ - `https://apiclaw.nordsym.com`
11
+ - Convex auth endpoint (`/workspace/poll`)
12
+ - Convex auth endpoint `HTTP 400` is now treated as a valid reachability signal in `doctor`.
13
+ - Setup/network guidance now points to live docs and domain paths on `apiclaw.nordsym.com`.
14
+
7
15
  ---
8
16
 
9
17
  ## [0.4.0] - 2026-03
package/README.md CHANGED
@@ -166,6 +166,22 @@ npx @nordsym/apiclaw mcp-install --client claude-code
166
166
  npx @nordsym/apiclaw mcp-install --dry-run
167
167
  ```
168
168
 
169
+ ### Codex (OpenAI) Setup
170
+
171
+ Use Codex's native MCP manager:
172
+
173
+ ```bash
174
+ # Recommended for local APIClaw development (strict stdio-safe)
175
+ codex mcp add apiclaw -- node /absolute/path/to/apiclaw/dist/index.js
176
+
177
+ # Example
178
+ codex mcp add apiclaw -- node /Users/gustavhemmingsson/Projects/apiclaw/dist/index.js
179
+
180
+ # Verify
181
+ codex mcp get apiclaw
182
+ codex mcp list
183
+ ```
184
+
169
185
  ### Global Install
170
186
 
171
187
  ```bash
@@ -249,6 +265,7 @@ npx @nordsym/apiclaw uninstall --client cursor
249
265
  | **Windsurf** | ✅ | ✅ | ✅ |
250
266
  | **Cline** | ✅ | ✅ | ✅ |
251
267
  | **Continue** | ✅ | ✅ | ✅ |
268
+ | **Codex (OpenAI)** | ✅ | ✅ | ✅ |
252
269
 
253
270
  <details>
254
271
  <summary>Config Locations</summary>
@@ -271,6 +288,9 @@ npx @nordsym/apiclaw uninstall --client cursor
271
288
  **Continue**
272
289
  - All: `~/.continue/config.json`
273
290
 
291
+ **Codex (OpenAI)**
292
+ - All: `~/.codex/config.toml` (managed via `codex mcp add/get/list/remove`)
293
+
274
294
  </details>
275
295
 
276
296
  ---
@@ -316,7 +336,7 @@ Returns the exact request that *would* be sent, with mock response data.
316
336
  ## Links
317
337
 
318
338
  - **Platform:** [apiclaw.com](https://apiclaw.com)
319
- - **Docs:** [docs.apiclaw.com](https://docs.apiclaw.com)
339
+ - **Docs:** [apiclaw.nordsym.com/docs](https://apiclaw.nordsym.com/docs)
320
340
  - **GitHub:** [github.com/nordsym/apiclaw](https://github.com/nordsym/apiclaw)
321
341
  - **npm:** [@nordsym/apiclaw](https://www.npmjs.com/package/@nordsym/apiclaw)
322
342
 
package/convex/http.ts CHANGED
@@ -111,6 +111,53 @@ function jsonResponse(data: unknown, status = 200) {
111
111
  });
112
112
  }
113
113
 
114
+ // Helper to validate session and log API usage
115
+ async function validateAndLogProxyCall(
116
+ ctx: any,
117
+ request: Request,
118
+ provider: string,
119
+ action: string
120
+ ): Promise<{ valid: boolean; workspaceId?: string; subagentId?: string; error?: string }> {
121
+ const sessionToken = request.headers.get("X-APIClaw-Session");
122
+ const subagentId = request.headers.get("X-APIClaw-Subagent") || "unknown";
123
+
124
+ if (!sessionToken) {
125
+ // Allow calls without session but don't log to workspace
126
+ return { valid: true, subagentId };
127
+ }
128
+
129
+ try {
130
+ // Validate session
131
+ const session = await ctx.runQuery(api.workspaces.getSession, { token: sessionToken });
132
+
133
+ if (!session) {
134
+ // Allow call anyway but log warning
135
+ console.warn("[Proxy] Invalid session token, allowing call but not logging");
136
+ return { valid: true, subagentId };
137
+ }
138
+
139
+ // Log the API call
140
+ await ctx.runMutation(api.logs.createProxyLog, {
141
+ workspaceId: session.workspaceId,
142
+ provider,
143
+ action,
144
+ subagentId,
145
+ sessionToken,
146
+ });
147
+
148
+ // Increment usage
149
+ await ctx.runMutation(api.workspaces.incrementUsage, {
150
+ workspaceId: session.workspaceId,
151
+ });
152
+
153
+ return { valid: true, workspaceId: session.workspaceId, subagentId };
154
+ } catch (e: any) {
155
+ console.error("[Proxy] Session validation error:", e);
156
+ // Allow call but don't log on error
157
+ return { valid: true, subagentId };
158
+ }
159
+ }
160
+
114
161
  // OPTIONS handler for CORS
115
162
  http.route({
116
163
  path: "/api/discover",
@@ -148,8 +195,13 @@ http.route({
148
195
  method: "POST",
149
196
  handler: httpAction(async (ctx, request) => {
150
197
  try {
198
+ const startTime = Date.now();
151
199
  const body = await request.json();
152
200
  const query = (body.query || "").toLowerCase();
201
+
202
+ // Get optional auth context
203
+ const sessionToken = request.headers.get("X-APIClaw-Session");
204
+ const userAgent = request.headers.get("User-Agent");
153
205
 
154
206
  const results = Object.entries(PROVIDERS)
155
207
  .filter(([id, provider]) => {
@@ -166,6 +218,20 @@ http.route({
166
218
  ...provider,
167
219
  }));
168
220
 
221
+ const responseTimeMs = Date.now() - startTime;
222
+
223
+ // Log the search (fire and forget)
224
+ if (query) {
225
+ ctx.runMutation(internal.searchLogs.logSearch, {
226
+ query: body.query || "", // Original query (not lowercased)
227
+ resultsCount: results.length,
228
+ matchedProviders: results.map(r => r.providerId),
229
+ sessionToken: sessionToken || undefined,
230
+ userAgent: userAgent || undefined,
231
+ responseTimeMs,
232
+ }).catch(() => {}); // Ignore errors, don't block response
233
+ }
234
+
169
235
  return jsonResponse({ providers: results, total: results.length });
170
236
  } catch (e) {
171
237
  return jsonResponse({ error: "Invalid request" }, 400);
@@ -373,6 +439,9 @@ http.route({
373
439
  path: "/proxy/openrouter",
374
440
  method: "POST",
375
441
  handler: httpAction(async (ctx, request) => {
442
+ // Validate session and log usage
443
+ await validateAndLogProxyCall(ctx, request, "openrouter", "chat");
444
+
376
445
  const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY;
377
446
  if (!OPENROUTER_KEY) {
378
447
  return jsonResponse({ error: "OpenRouter not configured" }, 500);
@@ -405,6 +474,9 @@ http.route({
405
474
  path: "/proxy/brave_search",
406
475
  method: "POST",
407
476
  handler: httpAction(async (ctx, request) => {
477
+ // Validate session and log usage
478
+ await validateAndLogProxyCall(ctx, request, "brave_search", "search");
479
+
408
480
  const BRAVE_KEY = process.env.BRAVE_API_KEY;
409
481
  if (!BRAVE_KEY) {
410
482
  return jsonResponse({ error: "Brave Search not configured" }, 500);
@@ -435,6 +507,9 @@ http.route({
435
507
  path: "/proxy/resend",
436
508
  method: "POST",
437
509
  handler: httpAction(async (ctx, request) => {
510
+ // Validate session and log usage
511
+ await validateAndLogProxyCall(ctx, request, "resend", "send_email");
512
+
438
513
  const RESEND_KEY = process.env.RESEND_API_KEY;
439
514
  if (!RESEND_KEY) {
440
515
  return jsonResponse({ error: "Resend not configured" }, 500);
@@ -465,6 +540,9 @@ http.route({
465
540
  path: "/proxy/elevenlabs",
466
541
  method: "POST",
467
542
  handler: httpAction(async (ctx, request) => {
543
+ // Validate session and log usage
544
+ await validateAndLogProxyCall(ctx, request, "elevenlabs", "text_to_speech");
545
+
468
546
  const ELEVENLABS_KEY = process.env.ELEVENLABS_API_KEY;
469
547
  if (!ELEVENLABS_KEY) {
470
548
  return jsonResponse({ error: "ElevenLabs not configured" }, 500);
@@ -534,6 +612,9 @@ http.route({
534
612
  path: "/proxy/46elks",
535
613
  method: "POST",
536
614
  handler: httpAction(async (ctx, request) => {
615
+ // Validate session and log usage
616
+ await validateAndLogProxyCall(ctx, request, "46elks", "send_sms");
617
+
537
618
  const ELKS_USER = process.env.ELKS_API_USER;
538
619
  const ELKS_PASS = process.env.ELKS_API_PASSWORD;
539
620
  if (!ELKS_USER || !ELKS_PASS) {
@@ -568,6 +649,9 @@ http.route({
568
649
  path: "/proxy/twilio",
569
650
  method: "POST",
570
651
  handler: httpAction(async (ctx, request) => {
652
+ // Validate session and log usage
653
+ await validateAndLogProxyCall(ctx, request, "twilio", "send_sms");
654
+
571
655
  const TWILIO_SID = process.env.TWILIO_ACCOUNT_SID;
572
656
  const TWILIO_TOKEN = process.env.TWILIO_AUTH_TOKEN;
573
657
  if (!TWILIO_SID || !TWILIO_TOKEN) {
@@ -640,27 +724,27 @@ http.route({
640
724
  fingerprint,
641
725
  });
642
726
 
643
- // Send email directly (bypassing action)
644
- var verifyUrl = "https://apiclaw.nordsym.com/auth/verify?token=" + result.token;
645
- var html = "<!DOCTYPE html><html><head><meta charset='utf-8'></head>";
646
- html += "<body style='margin:0;padding:40px;background:#f5f5f5;font-family:Arial,sans-serif;'>";
647
- html += "<table width='100%' cellpadding='0' cellspacing='0'><tr><td align='center'>";
648
- html += "<table width='500' cellpadding='0' cellspacing='0' style='background:#fff;border-radius:12px;'>";
649
- html += "<tr><td style='padding:32px;text-align:center;'>";
650
- html += "<div style='font-size:48px;'>🦞</div>";
651
- html += "<h1 style='margin:16px 0;color:#0a0a0a;'>APIClaw</h1>";
652
- html += "<h2 style='margin:0 0 16px;font-size:20px;color:#0a0a0a;'>An AI Agent Wants to Connect</h2>";
653
- html += "<p style='margin:0 0 24px;color:#525252;'>Click below to verify your email and activate your workspace.</p>";
654
- html += "<a href='" + verifyUrl + "' style='display:inline-block;background:#ef4444;color:white;padding:14px 32px;border-radius:8px;text-decoration:none;font-weight:600;'>Verify Email</a>";
655
- html += "<p style='margin:24px 0 0;font-size:13px;color:#737373;'>Free tier: 50 API calls. This link expires in 1 hour.</p>";
656
- html += "</td></tr></table>";
657
- html += "</td></tr></table></body></html>";
727
+ // Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
728
+ const verifyUrl = `https://apiclaw.nordsym.com/auth/verify?token=${result.token}`;
729
+ const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
730
+ <h1>🦞 APIClaw</h1>
731
+ <h2>An AI Agent Wants to Connect</h2>
732
+ <p>Click below to verify your email and activate your workspace.</p>
733
+ <p><a href="${verifyUrl}" style="background:#ef4444;color:white;padding:14px 32px;border-radius:8px;text-decoration:none;display:inline-block;">Verify Email</a></p>
734
+ <p style="color:#666;font-size:13px;">Free tier: 50 API calls. This link expires in 1 hour.</p>
735
+ <p style="color:#999;font-size:11px;">Or copy this link: ${verifyUrl}</p>
736
+ </div>`;
658
737
 
659
- var RESEND_KEY = process.env.RESEND_API_KEY;
660
- await fetch("https://api.resend.com/emails", {
738
+ const RESEND_KEY = process.env.RESEND_API_KEY;
739
+ if (!RESEND_KEY) {
740
+ console.error("RESEND_API_KEY not configured");
741
+ return jsonResponse({ error: "Email service not configured" }, 500);
742
+ }
743
+
744
+ const emailResponse = await fetch("https://api.resend.com/emails", {
661
745
  method: "POST",
662
746
  headers: {
663
- "Authorization": "Bearer " + RESEND_KEY,
747
+ "Authorization": `Bearer ${RESEND_KEY}`,
664
748
  "Content-Type": "application/json",
665
749
  },
666
750
  body: JSON.stringify({
@@ -670,12 +754,22 @@ http.route({
670
754
  html: html,
671
755
  }),
672
756
  });
757
+
758
+ if (!emailResponse.ok) {
759
+ const errorText = await emailResponse.text();
760
+ console.error("Resend error:", emailResponse.status, errorText);
761
+ return jsonResponse({ error: "Failed to send email", details: errorText }, 500);
762
+ }
763
+
764
+ const emailResult = await emailResponse.json();
765
+ console.log("Email sent successfully:", emailResult.id);
673
766
 
674
767
  return jsonResponse({
675
768
  success: true,
676
769
  token: result.token,
677
770
  expiresAt: result.expiresAt,
678
771
  message: "Magic link sent! Check your email.",
772
+ emailId: emailResult.id,
679
773
  });
680
774
  } catch (e: any) {
681
775
  console.error("Magic link error:", e);
package/convex/logs.ts CHANGED
@@ -503,3 +503,28 @@ export const clearWorkspaceLogs = mutation({
503
503
  };
504
504
  },
505
505
  });
506
+
507
+ // Log proxy API calls from external agents (Hivr bees)
508
+ export const createProxyLog = mutation({
509
+ args: {
510
+ workspaceId: v.id("workspaces"),
511
+ provider: v.string(),
512
+ action: v.string(),
513
+ subagentId: v.optional(v.string()),
514
+ sessionToken: v.optional(v.string()),
515
+ },
516
+ handler: async (ctx, { workspaceId, provider, action, subagentId, sessionToken }) => {
517
+ await ctx.db.insert("apiLogs", {
518
+ workspaceId,
519
+ provider,
520
+ action,
521
+ subagentId: subagentId || "unknown",
522
+ sessionToken: sessionToken || "proxy",
523
+ status: "success",
524
+ latencyMs: 0, // Proxy calls don't track latency
525
+ createdAt: Date.now(),
526
+ });
527
+
528
+ return { success: true };
529
+ },
530
+ });
package/convex/mou.ts CHANGED
@@ -72,3 +72,20 @@ export const list = query({
72
72
  return await ctx.db.query("mouDocuments").collect();
73
73
  },
74
74
  });
75
+
76
+ // Delete MOU (admin)
77
+ export const remove = mutation({
78
+ args: { partnerId: v.string() },
79
+ handler: async (ctx, args) => {
80
+ const mou = await ctx.db
81
+ .query("mouDocuments")
82
+ .withIndex("by_partnerId", (q) => q.eq("partnerId", args.partnerId))
83
+ .first();
84
+
85
+ if (mou) {
86
+ await ctx.db.delete(mou._id);
87
+ return { success: true, deleted: args.partnerId };
88
+ }
89
+ return { success: false, message: "MOU not found" };
90
+ },
91
+ });
@@ -1,141 +1,146 @@
1
+ import { internalMutation, query } from "./_generated/server";
1
2
  import { v } from "convex/values";
2
- import { mutation, query } from "./_generated/server";
3
3
 
4
- // ============================================
5
- // SEARCH LOGGING
6
- // ============================================
7
-
8
- /**
9
- * Log a search (called from MCP server)
10
- */
11
- export const log = mutation({
4
+ // Log a search query (uses existing searchLogs table schema)
5
+ export const logSearch = internalMutation({
12
6
  args: {
13
- sessionToken: v.string(),
14
- subagentId: v.optional(v.string()),
15
7
  query: v.string(),
16
- resultCount: v.number(),
8
+ resultsCount: v.number(),
17
9
  matchedProviders: v.optional(v.array(v.string())),
18
- responseTimeMs: v.number(),
10
+ sessionToken: v.optional(v.string()),
11
+ userAgent: v.optional(v.string()),
12
+ responseTimeMs: v.optional(v.number()),
19
13
  },
20
14
  handler: async (ctx, args) => {
21
- // Get workspace from session
22
- const session = await ctx.db
23
- .query("agentSessions")
24
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.sessionToken))
25
- .first();
26
-
27
- if (!session) return null;
15
+ // Try to get workspaceId from session token
16
+ let workspaceId = undefined;
17
+ let subagentId = undefined;
18
+
19
+ if (args.sessionToken) {
20
+ try {
21
+ const token = args.sessionToken;
22
+ const session = await ctx.db
23
+ .query("agentSessions")
24
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
25
+ .first();
26
+ if (session) {
27
+ workspaceId = session.workspaceId;
28
+ // No agentId in agentSessions, subagentId stays undefined
29
+ }
30
+ } catch (e) {
31
+ // Ignore - just skip workspace linking
32
+ }
33
+ }
28
34
 
29
- return await ctx.db.insert("searchLogs", {
30
- workspaceId: session.workspaceId,
31
- subagentId: args.subagentId,
32
- query: args.query,
33
- resultCount: args.resultCount,
34
- hasResults: args.resultCount > 0,
35
- matchedProviders: args.matchedProviders,
36
- responseTimeMs: args.responseTimeMs,
37
- timestamp: Date.now(),
38
- });
35
+ // Only log if we have a workspace (existing schema requires it)
36
+ if (workspaceId) {
37
+ await ctx.db.insert("searchLogs", {
38
+ workspaceId,
39
+ subagentId,
40
+ query: args.query,
41
+ resultCount: args.resultsCount,
42
+ hasResults: args.resultsCount > 0,
43
+ matchedProviders: args.matchedProviders,
44
+ responseTimeMs: args.responseTimeMs || 0,
45
+ timestamp: Date.now(),
46
+ });
47
+ }
48
+
49
+ // TODO: Also log anonymous searches somewhere (for product insights)
39
50
  },
40
51
  });
41
52
 
42
- /**
43
- * Get search stats for workspace
44
- */
45
- export const getStats = query({
53
+ // Get top search queries (for analytics)
54
+ export const getTopQueries = query({
46
55
  args: {
47
- token: v.string(),
48
- hoursBack: v.optional(v.number()),
56
+ limit: v.optional(v.number()),
57
+ since: v.optional(v.number()), // timestamp
49
58
  },
50
- handler: async (ctx, { token, hoursBack = 24 }) => {
51
- const session = await ctx.db
52
- .query("agentSessions")
53
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
54
- .first();
55
-
56
- if (!session) return null;
57
-
58
- const since = Date.now() - hoursBack * 3600000;
59
+ handler: async (ctx, args) => {
60
+ const limit = args.limit || 50;
61
+ const since = args.since || Date.now() - 7 * 24 * 60 * 60 * 1000; // Last 7 days
59
62
 
60
63
  const logs = await ctx.db
61
64
  .query("searchLogs")
62
- .withIndex("by_workspaceId_timestamp", (q) =>
63
- q.eq("workspaceId", session.workspaceId).gte("timestamp", since)
64
- )
65
+ .withIndex("by_timestamp")
66
+ .filter((q) => q.gte(q.field("timestamp"), since))
65
67
  .collect();
66
68
 
67
- // Aggregate
68
- const totalSearches = logs.length;
69
- const zeroResults = logs.filter((l) => !l.hasResults).length;
70
- const avgResponseTime =
71
- logs.reduce((a, l) => a + l.responseTimeMs, 0) / logs.length || 0;
72
-
73
- // Top queries
74
- const queryCounts: Record<string, number> = {};
69
+ // Aggregate by query
70
+ const queryCounts: Record<string, { count: number; avgResults: number; totalResults: number }> = {};
71
+
75
72
  for (const log of logs) {
76
- queryCounts[log.query] = (queryCounts[log.query] || 0) + 1;
73
+ const q = log.query.toLowerCase().trim();
74
+ if (!q) continue;
75
+
76
+ if (!queryCounts[q]) {
77
+ queryCounts[q] = { count: 0, avgResults: 0, totalResults: 0 };
78
+ }
79
+ queryCounts[q].count++;
80
+ queryCounts[q].totalResults += log.resultCount;
77
81
  }
78
- const topQueries = Object.entries(queryCounts)
79
- .sort(([, a], [, b]) => b - a)
80
- .slice(0, 20)
81
- .map(([query, count]) => ({ query, count }));
82
-
83
- // Zero-result queries (gold data for improvement)
84
- const zeroResultQueries = logs
85
- .filter((l) => !l.hasResults)
86
- .reduce(
87
- (acc, l) => {
88
- acc[l.query] = (acc[l.query] || 0) + 1;
89
- return acc;
90
- },
91
- {} as Record<string, number>
92
- );
93
- const topZeroResults = Object.entries(zeroResultQueries)
94
- .sort(([, a], [, b]) => b - a)
95
- .slice(0, 20)
96
- .map(([query, count]) => ({ query, count }));
97
82
 
98
- // By subagent
99
- const bySubagent: Record<string, number> = {};
100
- for (const log of logs) {
101
- const key = log.subagentId || "primary";
102
- bySubagent[key] = (bySubagent[key] || 0) + 1;
103
- }
83
+ // Calculate averages and sort
84
+ const sorted = Object.entries(queryCounts)
85
+ .map(([query, data]) => ({
86
+ query,
87
+ count: data.count,
88
+ avgResults: Math.round(data.totalResults / data.count * 10) / 10,
89
+ noResults: data.totalResults === 0,
90
+ }))
91
+ .sort((a, b) => b.count - a.count)
92
+ .slice(0, limit);
104
93
 
105
94
  return {
106
- totalSearches,
107
- zeroResults,
108
- zeroResultRate: totalSearches > 0 ? zeroResults / totalSearches : 0,
109
- avgResponseTime: Math.round(avgResponseTime),
110
- topQueries,
111
- topZeroResults,
112
- bySubagent,
95
+ queries: sorted,
96
+ totalSearches: logs.length,
97
+ uniqueQueries: Object.keys(queryCounts).length,
98
+ period: {
99
+ since: new Date(since).toISOString(),
100
+ until: new Date().toISOString(),
101
+ },
113
102
  };
114
103
  },
115
104
  });
116
105
 
117
- /**
118
- * Get recent searches
119
- */
120
- export const getRecent = query({
106
+ // Get searches with no results (API gaps)
107
+ export const getZeroResultQueries = query({
121
108
  args: {
122
- token: v.string(),
123
109
  limit: v.optional(v.number()),
110
+ since: v.optional(v.number()),
124
111
  },
125
- handler: async (ctx, { token, limit = 50 }) => {
126
- const session = await ctx.db
127
- .query("agentSessions")
128
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
129
- .first();
130
-
131
- if (!session) return [];
112
+ handler: async (ctx, args) => {
113
+ const limit = args.limit || 20;
114
+ const since = args.since || Date.now() - 7 * 24 * 60 * 60 * 1000;
132
115
 
133
- return await ctx.db
116
+ const logs = await ctx.db
134
117
  .query("searchLogs")
135
- .withIndex("by_workspaceId_timestamp", (q) =>
136
- q.eq("workspaceId", session.workspaceId)
118
+ .withIndex("by_hasResults")
119
+ .filter((q) =>
120
+ q.and(
121
+ q.eq(q.field("hasResults"), false),
122
+ q.gte(q.field("timestamp"), since)
123
+ )
137
124
  )
138
- .order("desc")
139
- .take(limit);
125
+ .collect();
126
+
127
+ // Aggregate
128
+ const queryCounts: Record<string, number> = {};
129
+ for (const log of logs) {
130
+ const q = log.query.toLowerCase().trim();
131
+ if (!q) continue;
132
+ queryCounts[q] = (queryCounts[q] || 0) + 1;
133
+ }
134
+
135
+ const sorted = Object.entries(queryCounts)
136
+ .map(([query, count]) => ({ query, count }))
137
+ .sort((a, b) => b.count - a.count)
138
+ .slice(0, limit);
139
+
140
+ return {
141
+ gaps: sorted,
142
+ totalZeroResults: logs.length,
143
+ message: "These queries returned no results - potential APIs to add",
144
+ };
140
145
  },
141
146
  });
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAkOD;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAqC5F;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAyC/D;AAcD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxG"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+OD;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAqC5F;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAyC/D;AAcD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxG"}