@homenshum/convex-mcp-nodebench 0.4.1 → 0.7.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.
- package/dist/index.js +52 -2
- package/dist/tools/actionAuditTools.d.ts +2 -0
- package/dist/tools/actionAuditTools.js +180 -0
- package/dist/tools/authorizationTools.d.ts +2 -0
- package/dist/tools/authorizationTools.js +201 -0
- package/dist/tools/dataModelingTools.d.ts +2 -0
- package/dist/tools/dataModelingTools.js +168 -0
- package/dist/tools/deploymentTools.js +42 -2
- package/dist/tools/devSetupTools.d.ts +2 -0
- package/dist/tools/devSetupTools.js +170 -0
- package/dist/tools/embeddingProvider.d.ts +6 -0
- package/dist/tools/embeddingProvider.js +3 -0
- package/dist/tools/functionTools.js +24 -1
- package/dist/tools/httpTools.js +128 -48
- package/dist/tools/migrationTools.d.ts +2 -0
- package/dist/tools/migrationTools.js +133 -0
- package/dist/tools/paginationTools.d.ts +2 -0
- package/dist/tools/paginationTools.js +125 -0
- package/dist/tools/queryEfficiencyTools.d.ts +2 -0
- package/dist/tools/queryEfficiencyTools.js +191 -0
- package/dist/tools/schemaTools.js +95 -1
- package/dist/tools/storageAuditTools.d.ts +2 -0
- package/dist/tools/storageAuditTools.js +148 -0
- package/dist/tools/toolRegistry.d.ts +4 -0
- package/dist/tools/toolRegistry.js +200 -11
- package/dist/tools/transactionSafetyTools.d.ts +2 -0
- package/dist/tools/transactionSafetyTools.js +166 -0
- package/dist/tools/typeSafetyTools.d.ts +2 -0
- package/dist/tools/typeSafetyTools.js +146 -0
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
|
@@ -20,7 +20,12 @@ function runPreDeployChecks(projectDir) {
|
|
|
20
20
|
if (!convexDir) {
|
|
21
21
|
checks.push({ name: "convex_dir_exists", passed: false, message: "No convex/ directory found" });
|
|
22
22
|
blockers.push("No convex/ directory found");
|
|
23
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
passed: false,
|
|
25
|
+
checks,
|
|
26
|
+
blockers: blockers.map((b, i) => ({ priority: i + 1, blocker: b, fixFirst: i === 0 })),
|
|
27
|
+
fixOrder: "Fix #1: Create a convex/ directory. Then re-run convex_pre_deploy_gate.",
|
|
28
|
+
};
|
|
24
29
|
}
|
|
25
30
|
checks.push({ name: "convex_dir_exists", passed: true, message: `Found at ${convexDir}` });
|
|
26
31
|
// Check 2: schema.ts exists
|
|
@@ -101,10 +106,19 @@ function runPreDeployChecks(projectDir) {
|
|
|
101
106
|
checks.push({ name: "generated_dir_exists", passed: false, message: "_generated/ not found - run 'npx convex dev' first" });
|
|
102
107
|
blockers.push("Run 'npx convex dev' to initialize the project before deploying");
|
|
103
108
|
}
|
|
109
|
+
// Add priority ordering to blockers
|
|
110
|
+
const prioritizedBlockers = blockers.map((b, i) => ({
|
|
111
|
+
priority: i + 1,
|
|
112
|
+
blocker: b,
|
|
113
|
+
fixFirst: i === 0,
|
|
114
|
+
}));
|
|
104
115
|
return {
|
|
105
116
|
passed: blockers.length === 0,
|
|
106
117
|
checks,
|
|
107
|
-
blockers,
|
|
118
|
+
blockers: prioritizedBlockers,
|
|
119
|
+
fixOrder: blockers.length > 0
|
|
120
|
+
? `Fix ${blockers.length} blocker(s) in order: ${blockers.map((_, i) => `#${i + 1}`).join(" → ")}. Then re-run convex_pre_deploy_gate.`
|
|
121
|
+
: "All checks passed. Safe to deploy.",
|
|
108
122
|
};
|
|
109
123
|
}
|
|
110
124
|
function checkEnvVars(projectDir) {
|
|
@@ -222,8 +236,34 @@ export const deploymentTools = [
|
|
|
222
236
|
handler: async (args) => {
|
|
223
237
|
const projectDir = resolve(args.projectDir);
|
|
224
238
|
const result = checkEnvVars(projectDir);
|
|
239
|
+
// Group missing vars by service for actionable output
|
|
240
|
+
const serviceGroups = {};
|
|
241
|
+
for (const v of result.missingInEnvFile) {
|
|
242
|
+
const vUp = v.toUpperCase();
|
|
243
|
+
const svc = vUp.includes("OPENAI") ? "OpenAI" :
|
|
244
|
+
vUp.includes("GEMINI") || vUp.includes("GOOGLE") ? "Google" :
|
|
245
|
+
vUp.includes("OPENBB") ? "OpenBB" :
|
|
246
|
+
vUp.includes("TWILIO") ? "Twilio" :
|
|
247
|
+
vUp.includes("LINKEDIN") ? "LinkedIn" :
|
|
248
|
+
vUp.includes("GITHUB") ? "GitHub" :
|
|
249
|
+
vUp.includes("STRIPE") ? "Stripe" :
|
|
250
|
+
vUp.includes("OPENROUTER") ? "OpenRouter" :
|
|
251
|
+
vUp.includes("RESEARCH") ? "Research MCP" :
|
|
252
|
+
vUp.includes("MCP") ? "MCP" :
|
|
253
|
+
vUp.includes("NTFY") ? "Ntfy" :
|
|
254
|
+
vUp.includes("XAI") ? "xAI" :
|
|
255
|
+
vUp.includes("CLERK") ? "Clerk" :
|
|
256
|
+
vUp.includes("CONVEX") ? "Convex" :
|
|
257
|
+
"Other";
|
|
258
|
+
if (!serviceGroups[svc])
|
|
259
|
+
serviceGroups[svc] = [];
|
|
260
|
+
serviceGroups[svc].push(v);
|
|
261
|
+
}
|
|
225
262
|
return {
|
|
226
263
|
...result,
|
|
264
|
+
missingByService: Object.entries(serviceGroups)
|
|
265
|
+
.sort(([, a], [, b]) => b.length - a.length)
|
|
266
|
+
.map(([service, vars]) => ({ service, count: vars.length, vars })),
|
|
227
267
|
quickRef: getQuickRef("convex_check_env_vars"),
|
|
228
268
|
};
|
|
229
269
|
},
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getDb, genId } from "../db.js";
|
|
4
|
+
import { getQuickRef } from "./toolRegistry.js";
|
|
5
|
+
function auditDevSetup(projectDir) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const checks = [];
|
|
8
|
+
// Check 1: .gitignore includes _generated/
|
|
9
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
10
|
+
if (existsSync(gitignorePath)) {
|
|
11
|
+
const gitignore = readFileSync(gitignorePath, "utf-8");
|
|
12
|
+
if (gitignore.includes("_generated") || gitignore.includes("convex/_generated")) {
|
|
13
|
+
checks.push({ area: "gitignore", status: "pass", detail: "_generated/ is in .gitignore" });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
checks.push({ area: "gitignore", status: "warn", detail: "_generated/ not in .gitignore — generated files may be committed" });
|
|
17
|
+
issues.push({
|
|
18
|
+
severity: "warning",
|
|
19
|
+
area: "gitignore",
|
|
20
|
+
message: "_generated/ directory is not in .gitignore. These files are auto-generated and should not be committed.",
|
|
21
|
+
fix: "Add `convex/_generated/` to .gitignore",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
checks.push({ area: "gitignore", status: "fail", detail: "No .gitignore file found" });
|
|
27
|
+
issues.push({
|
|
28
|
+
severity: "warning",
|
|
29
|
+
area: "gitignore",
|
|
30
|
+
message: "No .gitignore file found. Generated and environment files may be committed.",
|
|
31
|
+
fix: "Create a .gitignore with at least: node_modules/, convex/_generated/, .env.local",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Check 2: .env.example exists
|
|
35
|
+
const envExamplePath = join(projectDir, ".env.example");
|
|
36
|
+
if (existsSync(envExamplePath)) {
|
|
37
|
+
checks.push({ area: "env_example", status: "pass", detail: ".env.example exists for onboarding" });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Check if there are env vars in use
|
|
41
|
+
const envLocalPath = join(projectDir, ".env.local");
|
|
42
|
+
const envPath = join(projectDir, ".env");
|
|
43
|
+
if (existsSync(envLocalPath) || existsSync(envPath)) {
|
|
44
|
+
checks.push({ area: "env_example", status: "warn", detail: ".env files exist but no .env.example for new developers" });
|
|
45
|
+
issues.push({
|
|
46
|
+
severity: "info",
|
|
47
|
+
area: "env_example",
|
|
48
|
+
message: "No .env.example file. New developers won't know which env vars to set.",
|
|
49
|
+
fix: "Create .env.example with placeholder values for all required environment variables",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Check 3: convex.json exists and points to valid deployment
|
|
54
|
+
const convexJsonPath = join(projectDir, "convex.json");
|
|
55
|
+
if (existsSync(convexJsonPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const convexJson = JSON.parse(readFileSync(convexJsonPath, "utf-8"));
|
|
58
|
+
if (convexJson.project) {
|
|
59
|
+
checks.push({ area: "convex_json", status: "pass", detail: `convex.json configured for project: ${convexJson.project}` });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
checks.push({ area: "convex_json", status: "warn", detail: "convex.json exists but no project configured" });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
checks.push({ area: "convex_json", status: "fail", detail: "convex.json exists but is invalid JSON" });
|
|
67
|
+
issues.push({
|
|
68
|
+
severity: "critical",
|
|
69
|
+
area: "convex_json",
|
|
70
|
+
message: "convex.json is invalid JSON. Convex CLI won't work.",
|
|
71
|
+
fix: "Fix the JSON syntax in convex.json or delete and run `npx convex dev` to regenerate",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Check 4: package.json has convex as dependency
|
|
76
|
+
const pkgJsonPath = join(projectDir, "package.json");
|
|
77
|
+
if (existsSync(pkgJsonPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
80
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
81
|
+
if (deps.convex) {
|
|
82
|
+
checks.push({ area: "convex_dep", status: "pass", detail: `convex@${deps.convex} installed` });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
checks.push({ area: "convex_dep", status: "fail", detail: "convex not in dependencies" });
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: "critical",
|
|
88
|
+
area: "convex_dep",
|
|
89
|
+
message: "convex package is not in dependencies. Install it first.",
|
|
90
|
+
fix: "Run: npm install convex",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* ignore parse errors */ }
|
|
95
|
+
}
|
|
96
|
+
// Check 5: tsconfig.json configured for Convex
|
|
97
|
+
const tsconfigPath = join(projectDir, "tsconfig.json");
|
|
98
|
+
if (existsSync(tsconfigPath)) {
|
|
99
|
+
const tsconfig = readFileSync(tsconfigPath, "utf-8");
|
|
100
|
+
if (/\"strict\"\s*:\s*true/.test(tsconfig)) {
|
|
101
|
+
checks.push({ area: "tsconfig", status: "pass", detail: "TypeScript strict mode enabled" });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
checks.push({ area: "tsconfig", status: "info", detail: "TypeScript strict mode not enabled — recommended for Convex" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check 6: _generated/ directory exists (project initialized)
|
|
108
|
+
const convexDir = join(projectDir, "convex");
|
|
109
|
+
const generatedDir = join(convexDir, "_generated");
|
|
110
|
+
if (existsSync(generatedDir)) {
|
|
111
|
+
checks.push({ area: "initialization", status: "pass", detail: "_generated/ exists — project is initialized" });
|
|
112
|
+
}
|
|
113
|
+
else if (existsSync(convexDir)) {
|
|
114
|
+
checks.push({ area: "initialization", status: "warn", detail: "_generated/ not found — run `npx convex dev` to initialize" });
|
|
115
|
+
issues.push({
|
|
116
|
+
severity: "warning",
|
|
117
|
+
area: "initialization",
|
|
118
|
+
message: "Convex project not initialized — _generated/ directory is missing.",
|
|
119
|
+
fix: "Run `npx convex dev` to generate types and initialize the project",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Check 7: Node modules installed
|
|
123
|
+
const nodeModulesPath = join(projectDir, "node_modules", "convex");
|
|
124
|
+
if (existsSync(nodeModulesPath)) {
|
|
125
|
+
checks.push({ area: "node_modules", status: "pass", detail: "convex package installed in node_modules" });
|
|
126
|
+
}
|
|
127
|
+
else if (existsSync(pkgJsonPath)) {
|
|
128
|
+
checks.push({ area: "node_modules", status: "warn", detail: "convex not in node_modules — run npm install" });
|
|
129
|
+
}
|
|
130
|
+
return { issues, checks };
|
|
131
|
+
}
|
|
132
|
+
// ── Tool Definition ─────────────────────────────────────────────────
|
|
133
|
+
export const devSetupTools = [
|
|
134
|
+
{
|
|
135
|
+
name: "convex_audit_dev_setup",
|
|
136
|
+
description: "Audit Convex project development setup: .gitignore includes _generated/, .env.example exists, convex.json is valid, convex is in dependencies, TypeScript strict mode, project initialization status.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
projectDir: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Absolute path to the project root",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ["projectDir"],
|
|
146
|
+
},
|
|
147
|
+
handler: async (args) => {
|
|
148
|
+
const projectDir = resolve(args.projectDir);
|
|
149
|
+
const { issues, checks } = auditDevSetup(projectDir);
|
|
150
|
+
const db = getDb();
|
|
151
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "dev_setup", JSON.stringify(issues), issues.length);
|
|
152
|
+
const passed = checks.filter((c) => c.status === "pass").length;
|
|
153
|
+
const warned = checks.filter((c) => c.status === "warn").length;
|
|
154
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
155
|
+
return {
|
|
156
|
+
summary: {
|
|
157
|
+
totalChecks: checks.length,
|
|
158
|
+
passed,
|
|
159
|
+
warned,
|
|
160
|
+
failed,
|
|
161
|
+
totalIssues: issues.length,
|
|
162
|
+
},
|
|
163
|
+
checks,
|
|
164
|
+
issues,
|
|
165
|
+
quickRef: getQuickRef("convex_audit_dev_setup"),
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
//# sourceMappingURL=devSetupTools.js.map
|
|
@@ -11,19 +11,25 @@ export interface EmbeddingProvider {
|
|
|
11
11
|
dimensions: number;
|
|
12
12
|
embed(texts: string[]): Promise<Float32Array[]>;
|
|
13
13
|
}
|
|
14
|
+
/** Node type in the bipartite graph: tool nodes vs domain (agent) nodes */
|
|
15
|
+
export type GraphNodeType = "tool" | "domain";
|
|
14
16
|
interface EmbeddingIndexEntry {
|
|
15
17
|
name: string;
|
|
16
18
|
vector: Float32Array;
|
|
19
|
+
/** Node type for Agent-as-a-Graph bipartite scoring */
|
|
20
|
+
nodeType: GraphNodeType;
|
|
17
21
|
}
|
|
18
22
|
export declare function getEmbeddingProvider(): Promise<EmbeddingProvider | null>;
|
|
19
23
|
export declare function initEmbeddingIndex(corpus: Array<{
|
|
20
24
|
name: string;
|
|
21
25
|
text: string;
|
|
26
|
+
nodeType?: GraphNodeType;
|
|
22
27
|
}>): Promise<void>;
|
|
23
28
|
export declare function embedQuery(text: string): Promise<Float32Array | null>;
|
|
24
29
|
export declare function embeddingSearch(queryVec: Float32Array, limit?: number): Array<{
|
|
25
30
|
name: string;
|
|
26
31
|
similarity: number;
|
|
32
|
+
nodeType: GraphNodeType;
|
|
27
33
|
}>;
|
|
28
34
|
export declare function isEmbeddingReady(): boolean;
|
|
29
35
|
export declare function getProviderName(): string | null;
|
|
@@ -120,6 +120,7 @@ async function _doInit(corpus) {
|
|
|
120
120
|
_embeddingIndex = corpus.map((c) => ({
|
|
121
121
|
name: c.name,
|
|
122
122
|
vector: new Float32Array(cached.entries[c.name]),
|
|
123
|
+
nodeType: c.nodeType ?? "tool",
|
|
123
124
|
}));
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
@@ -129,6 +130,7 @@ async function _doInit(corpus) {
|
|
|
129
130
|
_embeddingIndex = corpus.map((c, i) => ({
|
|
130
131
|
name: c.name,
|
|
131
132
|
vector: vectors[i],
|
|
133
|
+
nodeType: c.nodeType ?? "tool",
|
|
132
134
|
}));
|
|
133
135
|
const cacheData = {
|
|
134
136
|
providerName: provider.name,
|
|
@@ -165,6 +167,7 @@ export function embeddingSearch(queryVec, limit = 30) {
|
|
|
165
167
|
const scored = _embeddingIndex.map((entry) => ({
|
|
166
168
|
name: entry.name,
|
|
167
169
|
similarity: cosineSim(queryVec, entry.vector),
|
|
170
|
+
nodeType: entry.nodeType,
|
|
168
171
|
}));
|
|
169
172
|
scored.sort((a, b) => b.similarity - a.similarity);
|
|
170
173
|
return scored.slice(0, limit);
|
|
@@ -287,6 +287,22 @@ export const functionTools = [
|
|
|
287
287
|
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "function_audit", JSON.stringify(issues), issues.length);
|
|
288
288
|
const critical = issues.filter((i) => i.severity === "critical");
|
|
289
289
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
290
|
+
// Aggregate issues by category for cleaner output
|
|
291
|
+
const categories = {};
|
|
292
|
+
for (const issue of issues) {
|
|
293
|
+
const cat = issue.message.includes("missing args") ? "missing_args_validator" :
|
|
294
|
+
issue.message.includes("missing returns") ? "missing_returns_validator" :
|
|
295
|
+
issue.message.includes("missing handler") ? "missing_handler_old_syntax" :
|
|
296
|
+
issue.message.includes("sensitive") ? "sensitive_function_public" :
|
|
297
|
+
issue.message.includes("queries cannot") ? "query_cross_call_violation" :
|
|
298
|
+
issue.message.includes("multiple actions") ? "action_from_action" :
|
|
299
|
+
"other";
|
|
300
|
+
if (!categories[cat])
|
|
301
|
+
categories[cat] = { severity: issue.severity, count: 0, examples: [] };
|
|
302
|
+
categories[cat].count++;
|
|
303
|
+
if (categories[cat].examples.length < 5)
|
|
304
|
+
categories[cat].examples.push(issue);
|
|
305
|
+
}
|
|
290
306
|
return {
|
|
291
307
|
summary: {
|
|
292
308
|
totalFunctions: functions.length,
|
|
@@ -296,7 +312,14 @@ export const functionTools = [
|
|
|
296
312
|
critical: critical.length,
|
|
297
313
|
warnings: warnings.length,
|
|
298
314
|
},
|
|
299
|
-
|
|
315
|
+
issuesByCategory: Object.entries(categories)
|
|
316
|
+
.sort(([, a], [, b]) => (b.severity === "critical" ? 1 : 0) - (a.severity === "critical" ? 1 : 0) || b.count - a.count)
|
|
317
|
+
.map(([cat, data]) => ({
|
|
318
|
+
category: cat,
|
|
319
|
+
severity: data.severity,
|
|
320
|
+
count: data.count,
|
|
321
|
+
examples: data.examples,
|
|
322
|
+
})),
|
|
300
323
|
quickRef: getQuickRef("convex_audit_functions"),
|
|
301
324
|
};
|
|
302
325
|
},
|
package/dist/tools/httpTools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { join, resolve, dirname } from "node:path";
|
|
3
3
|
import { getQuickRef } from "./toolRegistry.js";
|
|
4
4
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
5
5
|
function findConvexDir(projectDir) {
|
|
@@ -10,21 +10,28 @@ function findConvexDir(projectDir) {
|
|
|
10
10
|
}
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/** Resolve a relative import to a .ts file path */
|
|
14
|
+
function resolveImport(fromFile, importPath) {
|
|
15
|
+
const dir = dirname(fromFile);
|
|
16
|
+
const candidates = [
|
|
17
|
+
join(dir, importPath + ".ts"),
|
|
18
|
+
join(dir, importPath, "index.ts"),
|
|
19
|
+
join(dir, importPath + ".tsx"),
|
|
20
|
+
];
|
|
21
|
+
for (const c of candidates) {
|
|
22
|
+
if (existsSync(c))
|
|
23
|
+
return c;
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** Extract http.route() calls from a file's content, supporting both path and pathPrefix */
|
|
28
|
+
function extractRoutesFromContent(content, sourceFile) {
|
|
20
29
|
const routes = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const routeBlockPattern = /http\.route\s*\(\s*\{/g;
|
|
30
|
+
// Match any variable name followed by .route({ ... })
|
|
31
|
+
const routeBlockPattern = /\w+\.route\s*\(\s*\{/g;
|
|
24
32
|
let routeMatch;
|
|
25
33
|
while ((routeMatch = routeBlockPattern.exec(content)) !== null) {
|
|
26
34
|
const startIdx = routeMatch.index;
|
|
27
|
-
// Find the closing of this route block (rough — find next })
|
|
28
35
|
let depth = 0;
|
|
29
36
|
let endIdx = startIdx;
|
|
30
37
|
for (let i = startIdx; i < content.length; i++) {
|
|
@@ -39,21 +46,91 @@ function analyzeHttpEndpoints(convexDir) {
|
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
const block = content.slice(startIdx, endIdx + 1);
|
|
42
|
-
|
|
49
|
+
// Support both path: and pathPrefix:
|
|
50
|
+
const pathMatch = block.match(/(?:path|pathPrefix)\s*:\s*["']([^"']+)["']/);
|
|
43
51
|
const methodMatch = block.match(/method\s*:\s*["']([^"']+)["']/);
|
|
52
|
+
const isWildcard = /pathPrefix\s*:/.test(block) || /\/\*/.test(block) || /:\w+/.test(pathMatch?.[1] || "");
|
|
44
53
|
const line = content.slice(0, startIdx).split("\n").length;
|
|
45
54
|
if (pathMatch && methodMatch) {
|
|
46
55
|
routes.push({
|
|
47
56
|
path: pathMatch[1],
|
|
48
57
|
method: methodMatch[1].toUpperCase(),
|
|
49
58
|
line,
|
|
59
|
+
sourceFile,
|
|
50
60
|
handlerType: /handler\s*:\s*httpAction/.test(block) ? "inline" : "imported",
|
|
61
|
+
isWildcard,
|
|
51
62
|
});
|
|
52
63
|
}
|
|
53
64
|
}
|
|
65
|
+
return routes;
|
|
66
|
+
}
|
|
67
|
+
/** Follow imports from http.ts to find all router files */
|
|
68
|
+
function findRouterFiles(convexDir, httpPath) {
|
|
69
|
+
const files = [httpPath];
|
|
70
|
+
const visited = new Set([httpPath]);
|
|
71
|
+
const content = readFileSync(httpPath, "utf-8");
|
|
72
|
+
// Find relative imports that look like router sources
|
|
73
|
+
const importPattern = /import\s+(?:[\w{},\s]+\s+from\s+)?["'](\.[^"']+)["']/g;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = importPattern.exec(content)) !== null) {
|
|
76
|
+
const resolved = resolveImport(httpPath, m[1]);
|
|
77
|
+
if (resolved && !visited.has(resolved)) {
|
|
78
|
+
visited.add(resolved);
|
|
79
|
+
// Only include files that contain .route( or httpRouter or httpAction
|
|
80
|
+
try {
|
|
81
|
+
const fc = readFileSync(resolved, "utf-8");
|
|
82
|
+
if (/\.route\s*\(/.test(fc) || /httpRouter/.test(fc) || /httpAction/.test(fc)) {
|
|
83
|
+
files.push(resolved);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* skip unreadable */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
/** Detect .registerRoutes(http), .addHttpRoutes(http) and similar composite calls */
|
|
92
|
+
function detectCompositeRouteSources(content, sourceFile) {
|
|
93
|
+
const composites = [];
|
|
94
|
+
const pattern = /(\w+)\.(registerRoutes|addHttpRoutes)\s*\(\s*\w+\s*\)/g;
|
|
95
|
+
let m;
|
|
96
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
97
|
+
const line = content.slice(0, m.index).split("\n").length;
|
|
98
|
+
composites.push({
|
|
99
|
+
callee: `${m[1]}.${m[2]}()`,
|
|
100
|
+
sourceFile,
|
|
101
|
+
line,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return composites;
|
|
105
|
+
}
|
|
106
|
+
function analyzeHttpEndpoints(convexDir) {
|
|
107
|
+
const httpPath = join(convexDir, "http.ts");
|
|
108
|
+
if (!existsSync(httpPath)) {
|
|
109
|
+
return { hasHttp: false, routes: [], compositeRouteSources: [], issues: [], hasCors: false, hasOptionsHandler: false, filesScanned: [] };
|
|
110
|
+
}
|
|
111
|
+
const routerFiles = findRouterFiles(convexDir, httpPath);
|
|
112
|
+
const allRoutes = [];
|
|
113
|
+
const allComposites = [];
|
|
114
|
+
const issues = [];
|
|
115
|
+
let hasCorsInAny = false;
|
|
116
|
+
for (const filePath of routerFiles) {
|
|
117
|
+
const content = readFileSync(filePath, "utf-8");
|
|
118
|
+
const relPath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
119
|
+
// Extract routes
|
|
120
|
+
const routes = extractRoutesFromContent(content, relPath);
|
|
121
|
+
allRoutes.push(...routes);
|
|
122
|
+
// Detect composite route sources
|
|
123
|
+
const composites = detectCompositeRouteSources(content, relPath);
|
|
124
|
+
allComposites.push(...composites);
|
|
125
|
+
// Check for CORS in any file
|
|
126
|
+
if (/Access-Control-Allow-Origin/i.test(content) || /cors/i.test(content)) {
|
|
127
|
+
hasCorsInAny = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const hasOptionsHandler = allRoutes.some((r) => r.method === "OPTIONS");
|
|
54
131
|
// Check for duplicate routes (same path + method)
|
|
55
132
|
const routeKeys = new Map();
|
|
56
|
-
for (const route of
|
|
133
|
+
for (const route of allRoutes) {
|
|
57
134
|
const key = `${route.method} ${route.path}`;
|
|
58
135
|
const count = (routeKeys.get(key) || 0) + 1;
|
|
59
136
|
routeKeys.set(key, count);
|
|
@@ -61,57 +138,46 @@ function analyzeHttpEndpoints(convexDir) {
|
|
|
61
138
|
issues.push({
|
|
62
139
|
severity: "critical",
|
|
63
140
|
message: `Duplicate route: ${key} — only the last registration will be used`,
|
|
64
|
-
location:
|
|
141
|
+
location: `${route.sourceFile}:${route.line}`,
|
|
65
142
|
});
|
|
66
143
|
}
|
|
67
144
|
}
|
|
68
|
-
|
|
69
|
-
const hasCors = /Access-Control-Allow-Origin/i.test(content) ||
|
|
70
|
-
/cors/i.test(content);
|
|
71
|
-
const hasOptionsHandler = routes.some((r) => r.method === "OPTIONS");
|
|
72
|
-
if (!hasCors && routes.length > 0) {
|
|
145
|
+
if (!hasCorsInAny && allRoutes.length > 0) {
|
|
73
146
|
issues.push({
|
|
74
147
|
severity: "warning",
|
|
75
|
-
message: "No CORS headers detected. Browser requests from different origins will fail.
|
|
148
|
+
message: "No CORS headers detected in any HTTP router file. Browser requests from different origins will fail.",
|
|
76
149
|
});
|
|
77
150
|
}
|
|
78
|
-
if (
|
|
151
|
+
if (hasCorsInAny && !hasOptionsHandler) {
|
|
79
152
|
issues.push({
|
|
80
153
|
severity: "warning",
|
|
81
|
-
message: "CORS headers found but no OPTIONS handler registered. Preflight requests will fail.
|
|
154
|
+
message: "CORS headers found but no OPTIONS handler registered. Preflight requests will fail.",
|
|
82
155
|
});
|
|
83
156
|
}
|
|
84
|
-
// Check
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const parts = route.path.split("/").filter(Boolean);
|
|
88
|
-
if (parts.length >= 2) {
|
|
89
|
-
const prefix = `/${parts[0]}/${parts[1]}`;
|
|
90
|
-
pathPrefixes.set(prefix, (pathPrefixes.get(prefix) || 0) + 1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Check for missing httpRouter import
|
|
94
|
-
if (!/httpRouter/.test(content)) {
|
|
157
|
+
// Check http.ts specifically for required exports
|
|
158
|
+
const httpContent = readFileSync(httpPath, "utf-8");
|
|
159
|
+
if (!/export\s+default/.test(httpContent)) {
|
|
95
160
|
issues.push({
|
|
96
161
|
severity: "critical",
|
|
97
|
-
message: "Missing
|
|
162
|
+
message: "Missing 'export default' in http.ts. The httpRouter must be exported as default.",
|
|
98
163
|
});
|
|
99
164
|
}
|
|
100
|
-
//
|
|
101
|
-
if (
|
|
165
|
+
// Info about composite routes that can't be statically analyzed
|
|
166
|
+
if (allComposites.length > 0) {
|
|
102
167
|
issues.push({
|
|
103
|
-
severity: "
|
|
104
|
-
message:
|
|
168
|
+
severity: "info",
|
|
169
|
+
message: `${allComposites.length} composite route source(s) detected (${allComposites.map(c => c.callee).join(", ")}). These add routes dynamically — actual route count may be higher than statically detected.`,
|
|
105
170
|
});
|
|
106
171
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
172
|
+
return {
|
|
173
|
+
hasHttp: true,
|
|
174
|
+
routes: allRoutes,
|
|
175
|
+
compositeRouteSources: allComposites,
|
|
176
|
+
issues,
|
|
177
|
+
hasCors: hasCorsInAny,
|
|
178
|
+
hasOptionsHandler,
|
|
179
|
+
filesScanned: routerFiles.map(f => f.replace(convexDir, "").replace(/^[\\/]/, "")),
|
|
180
|
+
};
|
|
115
181
|
}
|
|
116
182
|
// ── Tool Definitions ────────────────────────────────────────────────
|
|
117
183
|
export const httpTools = [
|
|
@@ -142,18 +208,32 @@ export const httpTools = [
|
|
|
142
208
|
quickRef: getQuickRef("convex_analyze_http"),
|
|
143
209
|
};
|
|
144
210
|
}
|
|
145
|
-
// Group routes by
|
|
211
|
+
// Group routes by method
|
|
146
212
|
const byMethod = {};
|
|
147
213
|
for (const r of result.routes) {
|
|
148
214
|
byMethod[r.method] = (byMethod[r.method] || 0) + 1;
|
|
149
215
|
}
|
|
216
|
+
// Group routes by source file
|
|
217
|
+
const byFile = {};
|
|
218
|
+
for (const r of result.routes) {
|
|
219
|
+
byFile[r.sourceFile] = (byFile[r.sourceFile] || 0) + 1;
|
|
220
|
+
}
|
|
150
221
|
return {
|
|
151
222
|
hasHttp: true,
|
|
152
|
-
|
|
223
|
+
totalStaticRoutes: result.routes.length,
|
|
224
|
+
compositeRouteSources: result.compositeRouteSources.length,
|
|
225
|
+
estimatedTotalRoutes: result.compositeRouteSources.length > 0
|
|
226
|
+
? `${result.routes.length}+ (${result.compositeRouteSources.length} dynamic source(s) add additional routes)`
|
|
227
|
+
: result.routes.length,
|
|
228
|
+
filesScanned: result.filesScanned,
|
|
153
229
|
byMethod,
|
|
230
|
+
byFile,
|
|
154
231
|
hasCors: result.hasCors,
|
|
155
232
|
hasOptionsHandler: result.hasOptionsHandler,
|
|
156
233
|
routes: result.routes,
|
|
234
|
+
composites: result.compositeRouteSources.length > 0
|
|
235
|
+
? result.compositeRouteSources
|
|
236
|
+
: undefined,
|
|
157
237
|
issues: {
|
|
158
238
|
total: result.issues.length,
|
|
159
239
|
critical: result.issues.filter((i) => i.severity === "critical").length,
|