@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 +8 -0
- package/README.md +21 -1
- package/convex/http.ts +112 -18
- package/convex/logs.ts +25 -0
- package/convex/mou.ts +17 -0
- package/convex/searchLogs.ts +111 -106
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +45 -32
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/setup.js +2 -2
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/stripe.js +2 -2
- package/dist/stripe.js.map +1 -1
- package/dist/ui/errors.js +17 -17
- package/dist/ui/errors.js.map +1 -1
- package/dist/ui/prompts.js +1 -1
- package/dist/ui/prompts.js.map +1 -1
- package/landing/public/.well-known/ai-plugin.json +6 -6
- package/landing/public/.well-known/openapi.json +295 -0
- package/landing/public/sitemap.xml +15 -0
- package/landing/src/app/api/mou/sign/route.ts +166 -168
- package/landing/src/app/founding-backer/success/page.tsx +115 -0
- package/landing/src/app/layout.tsx +35 -0
- package/landing/src/app/mou/coaccept/page.tsx +13 -13
- package/landing/src/app/page.tsx +3 -4
- package/package.json +1 -1
- package/src/cli/commands/doctor.ts +49 -36
- package/src/cli/commands/setup.ts +2 -2
- package/src/stripe.ts +2 -2
- package/src/ui/errors.ts +17 -17
- package/src/ui/prompts.ts +1 -1
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:** [
|
|
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 (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
660
|
-
|
|
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":
|
|
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
|
+
});
|
package/convex/searchLogs.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
+
resultsCount: v.number(),
|
|
17
9
|
matchedProviders: v.optional(v.array(v.string())),
|
|
18
|
-
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
*/
|
|
45
|
-
export const getStats = query({
|
|
53
|
+
// Get top search queries (for analytics)
|
|
54
|
+
export const getTopQueries = query({
|
|
46
55
|
args: {
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
limit: v.optional(v.number()),
|
|
57
|
+
since: v.optional(v.number()), // timestamp
|
|
49
58
|
},
|
|
50
|
-
handler: async (ctx,
|
|
51
|
-
const
|
|
52
|
-
|
|
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("
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
+
.withIndex("by_timestamp")
|
|
66
|
+
.filter((q) => q.gte(q.field("timestamp"), since))
|
|
65
67
|
.collect();
|
|
66
68
|
|
|
67
|
-
// Aggregate
|
|
68
|
-
const
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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,
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
-
|
|
116
|
+
const logs = await ctx.db
|
|
134
117
|
.query("searchLogs")
|
|
135
|
-
.withIndex("
|
|
136
|
-
|
|
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
|
-
.
|
|
139
|
-
|
|
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;
|
|
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"}
|