botvisibility 1.0.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/.claude-flow/data/pending-insights.jsonl +2 -0
- package/.next/trace +1 -0
- package/.next/trace-build +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/repo-scanner.d.ts +21 -0
- package/dist/repo-scanner.d.ts.map +1 -0
- package/dist/repo-scanner.js +690 -0
- package/dist/repo-scanner.js.map +1 -0
- package/dist/scanner.d.ts +48 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +868 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scoring.d.ts +32 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +100 -0
- package/dist/scoring.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +402 -0
- package/src/repo-scanner.ts +718 -0
- package/src/scanner.ts +918 -0
- package/src/scoring.ts +105 -0
- package/src/types.ts +61 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.checkOpenApiFiles = checkOpenApiFiles;
|
|
37
|
+
exports.checkRateLimitMiddleware = checkRateLimitMiddleware;
|
|
38
|
+
exports.checkErrorPatterns = checkErrorPatterns;
|
|
39
|
+
exports.checkIdempotencyKeys = checkIdempotencyKeys;
|
|
40
|
+
exports.checkStreamingEndpoints = checkStreamingEndpoints;
|
|
41
|
+
exports.checkIntentEndpoints = checkIntentEndpoints;
|
|
42
|
+
exports.checkAgentSessions = checkAgentSessions;
|
|
43
|
+
exports.checkScopedAgentTokens = checkScopedAgentTokens;
|
|
44
|
+
exports.checkAgentAuditLogs = checkAgentAuditLogs;
|
|
45
|
+
exports.checkSandboxEnvironment = checkSandboxEnvironment;
|
|
46
|
+
exports.checkConsequenceLabels = checkConsequenceLabels;
|
|
47
|
+
exports.checkNativeToolSchemas = checkNativeToolSchemas;
|
|
48
|
+
exports.checkSparseFieldsCode = checkSparseFieldsCode;
|
|
49
|
+
exports.checkCursorPaginationCode = checkCursorPaginationCode;
|
|
50
|
+
exports.checkSearchFilteringCode = checkSearchFilteringCode;
|
|
51
|
+
exports.checkBulkOpsCode = checkBulkOpsCode;
|
|
52
|
+
exports.checkCachingHeadersCode = checkCachingHeadersCode;
|
|
53
|
+
exports.checkMcpToolQualityCode = checkMcpToolQualityCode;
|
|
54
|
+
exports.runRepoChecks = runRepoChecks;
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
// Recursive file finder
|
|
58
|
+
function findFiles(dir, pattern, results = []) {
|
|
59
|
+
try {
|
|
60
|
+
const items = fs.readdirSync(dir);
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
const fullPath = path.join(dir, item);
|
|
63
|
+
const stat = fs.statSync(fullPath);
|
|
64
|
+
if (stat.isDirectory()) {
|
|
65
|
+
if (!['node_modules', '.git', 'dist', 'build', '.next', 'vendor'].includes(item)) {
|
|
66
|
+
findFiles(fullPath, pattern, results);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (pattern.test(item)) {
|
|
70
|
+
results.push(fullPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Skip unreadable directories
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
// Search file content
|
|
80
|
+
function searchInFiles(dir, pattern, filePattern) {
|
|
81
|
+
const matches = [];
|
|
82
|
+
const files = findFiles(dir, filePattern);
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
try {
|
|
85
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
86
|
+
const lines = content.split('\n');
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
if (pattern.test(lines[i])) {
|
|
89
|
+
matches.push({
|
|
90
|
+
file,
|
|
91
|
+
line: i + 1,
|
|
92
|
+
content: lines[i].trim().slice(0, 100)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Skip unreadable files
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return matches;
|
|
102
|
+
}
|
|
103
|
+
const codeFiles = /\.(ts|js|py|rb|go|java|php)$/;
|
|
104
|
+
const allFiles = /\.(ts|js|py|rb|go|java|php|json|ya?ml|md|toml)$/;
|
|
105
|
+
// --- Supplemental checks (kept from original, remapped IDs) ---
|
|
106
|
+
function checkOpenApiFiles(repoPath) {
|
|
107
|
+
const specFiles = findFiles(repoPath, /\b(openapi|swagger)\.(json|ya?ml)$/i);
|
|
108
|
+
if (specFiles.length > 0) {
|
|
109
|
+
return {
|
|
110
|
+
id: 'repo-1.3', name: 'OpenAPI Spec Files', passed: true, status: 'pass', level: 1, category: 'Discoverable', autoDetectable: true,
|
|
111
|
+
message: `Found ${specFiles.length} OpenAPI/Swagger spec file(s)`,
|
|
112
|
+
filePath: specFiles[0],
|
|
113
|
+
details: specFiles.map(f => path.relative(repoPath, f)).join(', ')
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: 'repo-1.3', name: 'OpenAPI Spec Files', passed: false, status: 'fail', level: 1, category: 'Discoverable', autoDetectable: true,
|
|
118
|
+
message: 'No OpenAPI/Swagger spec files found in repo',
|
|
119
|
+
recommendation: 'Create an openapi.json or openapi.yaml file defining your API'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function checkRateLimitMiddleware(repoPath) {
|
|
123
|
+
const patterns = [/rate.?limit/i, /throttle/i, /RateLimiter/i, /express-rate-limit/i, /slowapi/i, /ratelimit/i];
|
|
124
|
+
const allMatches = [];
|
|
125
|
+
for (const pattern of patterns) {
|
|
126
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
127
|
+
}
|
|
128
|
+
if (allMatches.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
id: 'repo-3.5', name: 'Rate Limit Middleware', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
131
|
+
message: `Found rate limiting in ${allMatches.length} location(s)`,
|
|
132
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
id: 'repo-3.5', name: 'Rate Limit Middleware', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
137
|
+
message: 'No rate limiting middleware found',
|
|
138
|
+
recommendation: 'Add rate limiting to protect your API and return proper headers'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function checkErrorPatterns(repoPath) {
|
|
142
|
+
const goodPatterns = [/error.?code/i, /error_code/i, /"error"\s*:\s*\{/i, /json\s*\(\s*\{\s*"?error/i, /JsonResponse.*error/i];
|
|
143
|
+
const allMatches = [];
|
|
144
|
+
for (const pattern of goodPatterns) {
|
|
145
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
146
|
+
}
|
|
147
|
+
if (allMatches.length >= 3) {
|
|
148
|
+
return {
|
|
149
|
+
id: 'repo-2.7', name: 'Structured Error Responses', passed: true, status: 'pass', level: 2, category: 'Usable', autoDetectable: true,
|
|
150
|
+
message: `Found structured error patterns in ${allMatches.length} location(s)`,
|
|
151
|
+
details: 'Code uses consistent error response structure'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
else if (allMatches.length > 0) {
|
|
155
|
+
return {
|
|
156
|
+
id: 'repo-2.7', name: 'Structured Error Responses', passed: false, status: 'partial', level: 2, category: 'Usable', autoDetectable: true,
|
|
157
|
+
message: `Found some error patterns (${allMatches.length})`,
|
|
158
|
+
recommendation: 'Ensure all errors return structured JSON with error codes'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
id: 'repo-2.7', name: 'Structured Error Responses', passed: false, status: 'fail', level: 2, category: 'Usable', autoDetectable: true,
|
|
163
|
+
message: 'No structured error patterns found',
|
|
164
|
+
recommendation: 'Add consistent error response format: { "error": { "code": "...", "message": "..." } }'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function checkIdempotencyKeys(repoPath) {
|
|
168
|
+
const patterns = [/idempoten/i, /Idempotency.?Key/i, /idempotency_key/i, /x-idempotency/i];
|
|
169
|
+
const allMatches = [];
|
|
170
|
+
for (const pattern of patterns) {
|
|
171
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
172
|
+
}
|
|
173
|
+
if (allMatches.length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
id: 'repo-2.9', name: 'Idempotency Keys', passed: true, status: 'pass', level: 2, category: 'Usable', autoDetectable: true,
|
|
176
|
+
message: `Found idempotency handling in ${allMatches.length} location(s)`,
|
|
177
|
+
details: allMatches.slice(0, 2).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id: 'repo-2.9', name: 'Idempotency Keys', passed: false, status: 'fail', level: 2, category: 'Usable', autoDetectable: true,
|
|
182
|
+
message: 'No idempotency key handling found',
|
|
183
|
+
recommendation: 'Support Idempotency-Key header for write operations'
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function checkStreamingEndpoints(repoPath) {
|
|
187
|
+
const patterns = [/server.?sent.?event/i, /EventSource/i, /text\/event-stream/i, /websocket/i, /socket\.io/i, /streaming/i, /stream.?response/i];
|
|
188
|
+
const allMatches = [];
|
|
189
|
+
for (const pattern of patterns) {
|
|
190
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
191
|
+
}
|
|
192
|
+
if (allMatches.length > 0) {
|
|
193
|
+
return {
|
|
194
|
+
id: 'repo-3.6', name: 'Streaming/Event Endpoints', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
195
|
+
message: `Found streaming patterns in ${allMatches.length} location(s)`,
|
|
196
|
+
details: allMatches.slice(0, 2).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
id: 'repo-3.6', name: 'Streaming/Event Endpoints', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
201
|
+
message: 'No streaming/event endpoints found',
|
|
202
|
+
recommendation: 'Consider adding SSE or WebSocket for real-time updates'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// --- Level 4: Agent-Native checks ---
|
|
206
|
+
function checkIntentEndpoints(repoPath) {
|
|
207
|
+
// Search for intent-based/action-oriented route patterns
|
|
208
|
+
const patterns = [
|
|
209
|
+
/\/(send|process|execute|submit|trigger|run|perform|generate|analyze|convert|export|import|sync|verify|validate|approve|reject|cancel|refund)[_-]?\w+/i,
|
|
210
|
+
/router\.(post|put)\s*\(\s*['"]\/(send|process|execute|submit|trigger)/i,
|
|
211
|
+
/app\.(post|put)\s*\(\s*['"]\/(send|process|execute|submit|trigger)/i,
|
|
212
|
+
/path\s*=\s*['"]\/(send|process|execute|submit|trigger)/i,
|
|
213
|
+
];
|
|
214
|
+
const allMatches = [];
|
|
215
|
+
for (const pattern of patterns) {
|
|
216
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
217
|
+
}
|
|
218
|
+
if (allMatches.length >= 2) {
|
|
219
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
220
|
+
return {
|
|
221
|
+
id: '4.1', name: 'Intent-Based Endpoints', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
222
|
+
message: `Found intent-based endpoints in ${uniqueFiles.length} file(s)`,
|
|
223
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
else if (allMatches.length === 1) {
|
|
227
|
+
return {
|
|
228
|
+
id: '4.1', name: 'Intent-Based Endpoints', passed: false, status: 'partial', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
229
|
+
message: 'Found some intent-based patterns',
|
|
230
|
+
recommendation: 'Add more high-level intent endpoints (e.g., /send-invoice, /process-payment) alongside CRUD'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
id: '4.1', name: 'Intent-Based Endpoints', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
235
|
+
message: 'No intent-based endpoints found',
|
|
236
|
+
recommendation: 'Add high-level intent endpoints (e.g., /send-invoice, /process-payment) alongside CRUD'
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function checkAgentSessions(repoPath) {
|
|
240
|
+
const patterns = [
|
|
241
|
+
/agent.?session/i,
|
|
242
|
+
/session.?context/i,
|
|
243
|
+
/conversation.?id/i,
|
|
244
|
+
/thread.?id/i,
|
|
245
|
+
/agent.?context/i,
|
|
246
|
+
/persistent.?session/i,
|
|
247
|
+
/session.?store/i,
|
|
248
|
+
/x-session-id/i,
|
|
249
|
+
/x-agent-session/i,
|
|
250
|
+
];
|
|
251
|
+
const allMatches = [];
|
|
252
|
+
for (const pattern of patterns) {
|
|
253
|
+
allMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
254
|
+
}
|
|
255
|
+
if (allMatches.length > 0) {
|
|
256
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
257
|
+
return {
|
|
258
|
+
id: '4.2', name: 'Agent Sessions', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
259
|
+
message: `Found agent session patterns in ${uniqueFiles.length} file(s)`,
|
|
260
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
id: '4.2', name: 'Agent Sessions', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
265
|
+
message: 'No agent session management found',
|
|
266
|
+
recommendation: 'Implement persistent sessions with context for multi-step agent interactions'
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function checkScopedAgentTokens(repoPath) {
|
|
270
|
+
const patterns = [
|
|
271
|
+
/agent.?token/i,
|
|
272
|
+
/agent.?scope/i,
|
|
273
|
+
/agent.?key/i,
|
|
274
|
+
/agent.?credential/i,
|
|
275
|
+
/capability.?limit/i,
|
|
276
|
+
/scoped.?token/i,
|
|
277
|
+
/agent.?permission/i,
|
|
278
|
+
/agent.?role/i,
|
|
279
|
+
/x-agent-token/i,
|
|
280
|
+
];
|
|
281
|
+
const allMatches = [];
|
|
282
|
+
for (const pattern of patterns) {
|
|
283
|
+
allMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
284
|
+
}
|
|
285
|
+
if (allMatches.length > 0) {
|
|
286
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
287
|
+
return {
|
|
288
|
+
id: '4.3', name: 'Scoped Agent Tokens', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
289
|
+
message: `Found agent token patterns in ${uniqueFiles.length} file(s)`,
|
|
290
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
id: '4.3', name: 'Scoped Agent Tokens', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
295
|
+
message: 'No agent-specific token/scope patterns found',
|
|
296
|
+
recommendation: 'Create agent-specific tokens with capability limits (read-only, write, admin)'
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function checkAgentAuditLogs(repoPath) {
|
|
300
|
+
const patterns = [
|
|
301
|
+
/agent.?audit/i,
|
|
302
|
+
/audit.?log.*agent/i,
|
|
303
|
+
/agent.?identifier/i,
|
|
304
|
+
/agent.?id.*log/i,
|
|
305
|
+
/log.*agent.?id/i,
|
|
306
|
+
/x-agent-id/i,
|
|
307
|
+
/user.?agent.*audit/i,
|
|
308
|
+
];
|
|
309
|
+
const allMatches = [];
|
|
310
|
+
for (const pattern of patterns) {
|
|
311
|
+
allMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
312
|
+
}
|
|
313
|
+
if (allMatches.length > 0) {
|
|
314
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
315
|
+
return {
|
|
316
|
+
id: '4.4', name: 'Agent Audit Logs', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
317
|
+
message: `Found agent audit logging in ${uniqueFiles.length} file(s)`,
|
|
318
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
id: '4.4', name: 'Agent Audit Logs', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
323
|
+
message: 'No agent audit logging found',
|
|
324
|
+
recommendation: 'Log API actions with agent identifiers for traceability'
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function checkSandboxEnvironment(repoPath) {
|
|
328
|
+
const patterns = [
|
|
329
|
+
/sandbox/i,
|
|
330
|
+
/test.?environment/i,
|
|
331
|
+
/staging.?env/i,
|
|
332
|
+
/dry.?run/i,
|
|
333
|
+
/test.?mode/i,
|
|
334
|
+
];
|
|
335
|
+
const allMatches = [];
|
|
336
|
+
for (const pattern of patterns) {
|
|
337
|
+
allMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
338
|
+
}
|
|
339
|
+
// Also check for sandbox config files
|
|
340
|
+
const sandboxFiles = findFiles(repoPath, /sandbox|\.env\.test|\.env\.staging/i);
|
|
341
|
+
const total = allMatches.length + sandboxFiles.length;
|
|
342
|
+
if (total > 0) {
|
|
343
|
+
return {
|
|
344
|
+
id: '4.5', name: 'Sandbox Environment', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
345
|
+
message: `Found sandbox/test environment patterns in ${total} location(s)`,
|
|
346
|
+
details: allMatches.length > 0
|
|
347
|
+
? allMatches.slice(0, 2).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
348
|
+
: sandboxFiles.slice(0, 2).map(f => path.relative(repoPath, f)).join(', ')
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
id: '4.5', name: 'Sandbox Environment', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
353
|
+
message: 'No sandbox environment found',
|
|
354
|
+
recommendation: 'Provide a sandbox environment for agents to test operations safely'
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function checkConsequenceLabels(repoPath) {
|
|
358
|
+
const patterns = [
|
|
359
|
+
/consequen(ce|tial)/i,
|
|
360
|
+
/irreversible/i,
|
|
361
|
+
/destructive/i,
|
|
362
|
+
/dangerous/i,
|
|
363
|
+
/x-consequence/i,
|
|
364
|
+
/x-reversible/i,
|
|
365
|
+
/side.?effect/i,
|
|
366
|
+
/confirmation.?required/i,
|
|
367
|
+
];
|
|
368
|
+
const allMatches = [];
|
|
369
|
+
for (const pattern of patterns) {
|
|
370
|
+
allMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
371
|
+
}
|
|
372
|
+
if (allMatches.length > 0) {
|
|
373
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
374
|
+
return {
|
|
375
|
+
id: '4.6', name: 'Consequence Labels', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
376
|
+
message: `Found consequence annotations in ${uniqueFiles.length} file(s)`,
|
|
377
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
id: '4.6', name: 'Consequence Labels', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
382
|
+
message: 'No consequence labels found',
|
|
383
|
+
recommendation: 'Mark consequential/irreversible actions in your API docs or schema annotations'
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function checkNativeToolSchemas(repoPath) {
|
|
387
|
+
// Check for tool definition files
|
|
388
|
+
const toolFiles = findFiles(repoPath, /\.(tool|tools)\.(json|ya?ml)$|mcp\.(json|ya?ml)$|tool_?definitions?\.(json|ya?ml)$/i);
|
|
389
|
+
// Also search for tool schema patterns in code
|
|
390
|
+
const patterns = [
|
|
391
|
+
/tool.?schema/i,
|
|
392
|
+
/tool.?definition/i,
|
|
393
|
+
/function.?calling/i,
|
|
394
|
+
/tool.?manifest/i,
|
|
395
|
+
/mcp.?config/i,
|
|
396
|
+
/openai.?function/i,
|
|
397
|
+
/anthropic.?tool/i,
|
|
398
|
+
];
|
|
399
|
+
const allMatches = [];
|
|
400
|
+
for (const pattern of patterns) {
|
|
401
|
+
allMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
402
|
+
}
|
|
403
|
+
const total = toolFiles.length + allMatches.length;
|
|
404
|
+
if (toolFiles.length > 0) {
|
|
405
|
+
return {
|
|
406
|
+
id: '4.7', name: 'Native Tool Schemas', passed: true, status: 'pass', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
407
|
+
message: `Found tool definition files`,
|
|
408
|
+
filePath: toolFiles[0],
|
|
409
|
+
details: toolFiles.map(f => path.relative(repoPath, f)).join(', ')
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
else if (allMatches.length > 0) {
|
|
413
|
+
return {
|
|
414
|
+
id: '4.7', name: 'Native Tool Schemas', passed: false, status: 'partial', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
415
|
+
message: `Found tool schema references in ${allMatches.length} location(s)`,
|
|
416
|
+
details: allMatches.slice(0, 2).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', '),
|
|
417
|
+
recommendation: 'Extract tool definitions into standalone .tool.json or MCP config files'
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
id: '4.7', name: 'Native Tool Schemas', passed: false, status: 'fail', level: 4, category: 'Agent-Native', autoDetectable: true,
|
|
422
|
+
message: 'No tool definition files found',
|
|
423
|
+
recommendation: 'Create ready-to-use tool definition files (.tool.json, MCP configs) for agent frameworks'
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// --- Level 3: Optimization code checks ---
|
|
427
|
+
function checkSparseFieldsCode(repoPath) {
|
|
428
|
+
const patterns = [
|
|
429
|
+
/[?&]fields=/i,
|
|
430
|
+
/[?&]select=/i,
|
|
431
|
+
/\.select\s*\(/i,
|
|
432
|
+
/\.only\s*\(/i,
|
|
433
|
+
/\.values\s*\(/i,
|
|
434
|
+
/\.values_list\s*\(/i,
|
|
435
|
+
/fields\s*[:=]\s*req/i,
|
|
436
|
+
/query\.fields/i,
|
|
437
|
+
/params\.(fields|select)/i,
|
|
438
|
+
/projection\s*[:=]/i,
|
|
439
|
+
];
|
|
440
|
+
const allMatches = [];
|
|
441
|
+
for (const pattern of patterns) {
|
|
442
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
443
|
+
}
|
|
444
|
+
if (allMatches.length >= 2) {
|
|
445
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
446
|
+
return {
|
|
447
|
+
id: 'repo-3.1', name: 'Sparse Fields', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
448
|
+
message: `Found sparse field patterns in ${uniqueFiles.length} file(s)`,
|
|
449
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
else if (allMatches.length === 1) {
|
|
453
|
+
return {
|
|
454
|
+
id: 'repo-3.1', name: 'Sparse Fields', passed: false, status: 'partial', level: 3, category: 'Optimized', autoDetectable: true,
|
|
455
|
+
message: 'Found some sparse field patterns',
|
|
456
|
+
recommendation: 'Add a fields or select query parameter to all list/get endpoints'
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
id: 'repo-3.1', name: 'Sparse Fields', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
461
|
+
message: 'No sparse field support found in code',
|
|
462
|
+
recommendation: 'Add a fields or select parameter to endpoints so agents can request only needed data'
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function checkCursorPaginationCode(repoPath) {
|
|
466
|
+
const patterns = [
|
|
467
|
+
/cursor/i,
|
|
468
|
+
/next_?token/i,
|
|
469
|
+
/next_?page_?token/i,
|
|
470
|
+
/page_?token/i,
|
|
471
|
+
/start_?after/i,
|
|
472
|
+
/\.paginate\s*\(/i,
|
|
473
|
+
/has_?more/i,
|
|
474
|
+
/next_?cursor/i,
|
|
475
|
+
/continuation_?token/i,
|
|
476
|
+
];
|
|
477
|
+
const allMatches = [];
|
|
478
|
+
for (const pattern of patterns) {
|
|
479
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
480
|
+
}
|
|
481
|
+
if (allMatches.length >= 2) {
|
|
482
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
483
|
+
return {
|
|
484
|
+
id: 'repo-3.2', name: 'Cursor Pagination', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
485
|
+
message: `Found cursor pagination patterns in ${uniqueFiles.length} file(s)`,
|
|
486
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
else if (allMatches.length === 1) {
|
|
490
|
+
return {
|
|
491
|
+
id: 'repo-3.2', name: 'Cursor Pagination', passed: false, status: 'partial', level: 3, category: 'Optimized', autoDetectable: true,
|
|
492
|
+
message: 'Found some cursor pagination patterns',
|
|
493
|
+
recommendation: 'Implement cursor-based pagination on all list endpoints with has_more and next_cursor'
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
id: 'repo-3.2', name: 'Cursor Pagination', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
498
|
+
message: 'No cursor pagination found in code',
|
|
499
|
+
recommendation: 'Replace offset pagination with cursor-based pagination for efficient large-set traversal'
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function checkSearchFilteringCode(repoPath) {
|
|
503
|
+
const patterns = [
|
|
504
|
+
/[?&](filter|search|q)=/i,
|
|
505
|
+
/req\.query\.(filter|search|q)\b/i,
|
|
506
|
+
/params\.(filter|search|q)\b/i,
|
|
507
|
+
/query_?params.*filter/i,
|
|
508
|
+
/filter_?by/i,
|
|
509
|
+
/search_?query/i,
|
|
510
|
+
/\.filter\s*\(.*req/i,
|
|
511
|
+
/\.where\s*\(.*req/i,
|
|
512
|
+
];
|
|
513
|
+
const allMatches = [];
|
|
514
|
+
for (const pattern of patterns) {
|
|
515
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
516
|
+
}
|
|
517
|
+
if (allMatches.length >= 2) {
|
|
518
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
519
|
+
return {
|
|
520
|
+
id: 'repo-3.3', name: 'Search & Filtering', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
521
|
+
message: `Found search/filter patterns in ${uniqueFiles.length} file(s)`,
|
|
522
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
else if (allMatches.length === 1) {
|
|
526
|
+
return {
|
|
527
|
+
id: 'repo-3.3', name: 'Search & Filtering', passed: false, status: 'partial', level: 3, category: 'Optimized', autoDetectable: true,
|
|
528
|
+
message: 'Found some search/filter patterns',
|
|
529
|
+
recommendation: 'Add filter and search query parameters to all list endpoints'
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
id: 'repo-3.3', name: 'Search & Filtering', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
534
|
+
message: 'No API search or filtering found in code',
|
|
535
|
+
recommendation: 'Add filter, search, and query parameters so agents can find specific resources without over-fetching'
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function checkBulkOpsCode(repoPath) {
|
|
539
|
+
const patterns = [
|
|
540
|
+
/\/batch/i,
|
|
541
|
+
/\/bulk/i,
|
|
542
|
+
/bulkCreate/i,
|
|
543
|
+
/bulk_create/i,
|
|
544
|
+
/insertMany/i,
|
|
545
|
+
/createMany/i,
|
|
546
|
+
/updateMany/i,
|
|
547
|
+
/deleteMany/i,
|
|
548
|
+
/bulk_update/i,
|
|
549
|
+
/batch_create/i,
|
|
550
|
+
/Promise\.all\s*\(/i,
|
|
551
|
+
];
|
|
552
|
+
const allMatches = [];
|
|
553
|
+
for (const pattern of patterns) {
|
|
554
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
555
|
+
}
|
|
556
|
+
if (allMatches.length >= 1) {
|
|
557
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
558
|
+
return {
|
|
559
|
+
id: 'repo-3.4', name: 'Bulk Operations', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
560
|
+
message: `Found bulk operation patterns in ${uniqueFiles.length} file(s)`,
|
|
561
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
id: 'repo-3.4', name: 'Bulk Operations', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
566
|
+
message: 'No bulk/batch operations found in code',
|
|
567
|
+
recommendation: 'Add batch endpoints for creating, updating, or deleting multiple resources in a single request'
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function checkCachingHeadersCode(repoPath) {
|
|
571
|
+
const patterns = [
|
|
572
|
+
/ETag/i,
|
|
573
|
+
/Cache-Control/i,
|
|
574
|
+
/Last-Modified/i,
|
|
575
|
+
/If-None-Match/i,
|
|
576
|
+
/If-Modified-Since/i,
|
|
577
|
+
/stale-while-revalidate/i,
|
|
578
|
+
/max-age/i,
|
|
579
|
+
/\.cache\s*\(/i,
|
|
580
|
+
/cacheControl/i,
|
|
581
|
+
/setHeader.*cache/i,
|
|
582
|
+
];
|
|
583
|
+
const allMatches = [];
|
|
584
|
+
for (const pattern of patterns) {
|
|
585
|
+
allMatches.push(...searchInFiles(repoPath, pattern, codeFiles));
|
|
586
|
+
}
|
|
587
|
+
if (allMatches.length >= 2) {
|
|
588
|
+
const uniqueFiles = [...new Set(allMatches.map(m => m.file))];
|
|
589
|
+
return {
|
|
590
|
+
id: 'repo-3.6', name: 'Caching Headers', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
591
|
+
message: `Found caching header patterns in ${uniqueFiles.length} file(s)`,
|
|
592
|
+
details: allMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
else if (allMatches.length === 1) {
|
|
596
|
+
return {
|
|
597
|
+
id: 'repo-3.6', name: 'Caching Headers', passed: false, status: 'partial', level: 3, category: 'Optimized', autoDetectable: true,
|
|
598
|
+
message: 'Found some caching patterns',
|
|
599
|
+
recommendation: 'Add ETag, Cache-Control, and Last-Modified headers to API responses'
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
id: 'repo-3.6', name: 'Caching Headers', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
604
|
+
message: 'No caching header patterns found in code',
|
|
605
|
+
recommendation: 'Add Cache-Control, ETag, and Last-Modified headers to reduce token waste from redundant requests'
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function checkMcpToolQualityCode(repoPath) {
|
|
609
|
+
// Look for MCP server definitions
|
|
610
|
+
const mcpPatterns = [
|
|
611
|
+
/McpServer/i,
|
|
612
|
+
/server\.tool\s*\(/i,
|
|
613
|
+
/\.addTool\s*\(/i,
|
|
614
|
+
/mcp.*server/i,
|
|
615
|
+
/tool.*inputSchema/i,
|
|
616
|
+
/\"tools\"\s*:/i,
|
|
617
|
+
];
|
|
618
|
+
const mcpMatches = [];
|
|
619
|
+
for (const pattern of mcpPatterns) {
|
|
620
|
+
mcpMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
621
|
+
}
|
|
622
|
+
// Look for MCP config/manifest files
|
|
623
|
+
const mcpFiles = findFiles(repoPath, /mcp\.(json|ya?ml)$|\.well-known.*mcp/i);
|
|
624
|
+
// Check for tool descriptions
|
|
625
|
+
const descPatterns = [/description\s*[:=]\s*["'`]/i];
|
|
626
|
+
const descMatches = [];
|
|
627
|
+
for (const pattern of descPatterns) {
|
|
628
|
+
descMatches.push(...searchInFiles(repoPath, pattern, allFiles));
|
|
629
|
+
}
|
|
630
|
+
const totalMcp = mcpMatches.length + mcpFiles.length;
|
|
631
|
+
if (totalMcp === 0) {
|
|
632
|
+
return {
|
|
633
|
+
id: 'repo-3.7', name: 'MCP Tool Quality', passed: false, status: 'fail', level: 3, category: 'Optimized', autoDetectable: true,
|
|
634
|
+
message: 'No MCP server or tool definitions found in code',
|
|
635
|
+
recommendation: 'Create an MCP server with well-described tools and input schemas for AI agent integration'
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// Check quality: do the tools have descriptions?
|
|
639
|
+
const hasDescriptions = descMatches.length >= 2;
|
|
640
|
+
const schemaPatterns = [/inputSchema/i, /parameters.*type.*object/i, /json.?schema/i];
|
|
641
|
+
let schemaCount = 0;
|
|
642
|
+
for (const pattern of schemaPatterns) {
|
|
643
|
+
schemaCount += searchInFiles(repoPath, pattern, allFiles).length;
|
|
644
|
+
}
|
|
645
|
+
if (hasDescriptions && schemaCount > 0) {
|
|
646
|
+
return {
|
|
647
|
+
id: 'repo-3.7', name: 'MCP Tool Quality', passed: true, status: 'pass', level: 3, category: 'Optimized', autoDetectable: true,
|
|
648
|
+
message: `MCP tools found with descriptions and schemas in ${totalMcp} location(s)`,
|
|
649
|
+
details: mcpMatches.slice(0, 3).map(m => `${path.relative(repoPath, m.file)}:${m.line}`).join(', ')
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
id: 'repo-3.7', name: 'MCP Tool Quality', passed: false, status: 'partial', level: 3, category: 'Optimized', autoDetectable: true,
|
|
654
|
+
message: `MCP tools found but missing ${!hasDescriptions ? 'descriptions' : 'input schemas'}`,
|
|
655
|
+
recommendation: 'Ensure all MCP tools have detailed descriptions (>10 chars) and inputSchema definitions'
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
// --- Run all repo checks ---
|
|
659
|
+
function runRepoChecks(repoPath) {
|
|
660
|
+
if (!fs.existsSync(repoPath)) {
|
|
661
|
+
return [{
|
|
662
|
+
id: 'repo-error', name: 'Repository Path', passed: false, status: 'fail', level: 1, category: 'Error', autoDetectable: true,
|
|
663
|
+
message: `Path does not exist: ${repoPath}`
|
|
664
|
+
}];
|
|
665
|
+
}
|
|
666
|
+
return [
|
|
667
|
+
// Supplemental checks for Levels 1-2
|
|
668
|
+
checkOpenApiFiles(repoPath),
|
|
669
|
+
checkErrorPatterns(repoPath),
|
|
670
|
+
checkIdempotencyKeys(repoPath),
|
|
671
|
+
// Level 3: Optimization code checks
|
|
672
|
+
checkSparseFieldsCode(repoPath),
|
|
673
|
+
checkCursorPaginationCode(repoPath),
|
|
674
|
+
checkSearchFilteringCode(repoPath),
|
|
675
|
+
checkBulkOpsCode(repoPath),
|
|
676
|
+
checkRateLimitMiddleware(repoPath),
|
|
677
|
+
checkCachingHeadersCode(repoPath),
|
|
678
|
+
checkMcpToolQualityCode(repoPath),
|
|
679
|
+
checkStreamingEndpoints(repoPath),
|
|
680
|
+
// Level 4: Agent-Native checks
|
|
681
|
+
checkIntentEndpoints(repoPath),
|
|
682
|
+
checkAgentSessions(repoPath),
|
|
683
|
+
checkScopedAgentTokens(repoPath),
|
|
684
|
+
checkAgentAuditLogs(repoPath),
|
|
685
|
+
checkSandboxEnvironment(repoPath),
|
|
686
|
+
checkConsequenceLabels(repoPath),
|
|
687
|
+
checkNativeToolSchemas(repoPath),
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
//# sourceMappingURL=repo-scanner.js.map
|