aislop 0.8.3 → 0.9.1
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/README.md +1 -0
- package/dist/cli.js +1193 -304
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1007 -237
- package/dist/{json-D8h2EZW6.js → json-B_2_Zt7I.js} +1 -1
- package/dist/{json-BbMwrgyd.js → json-OIzja7OM.js} +1 -1
- package/dist/mcp.js +669 -155
- package/dist/{typecheck-B1MXNAy-.js → typecheck-wVSohmOX.js} +1 -1
- package/dist/{version-BynHxO1X.js → version-CBcgcofs.js} +1 -1
- package/package.json +2 -1
package/dist/mcp.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CCnnN_oQ.js";
|
|
3
3
|
import { createRequire, isBuiltin } from "node:module";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
4
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import path from "node:path";
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import path from "node:path";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import fs from "node:fs";
|
|
10
11
|
import YAML from "yaml";
|
|
11
12
|
import { z as z$1 } from "zod/v4";
|
|
12
|
-
import { performance } from "node:perf_hooks";
|
|
13
13
|
import micromatch from "micromatch";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import "typescript";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
|
|
18
19
|
//#region src/config/defaults.ts
|
|
19
20
|
const DEFAULT_CONFIG = {
|
|
@@ -25,6 +26,7 @@ const DEFAULT_CONFIG = {
|
|
|
25
26
|
"build",
|
|
26
27
|
"coverage"
|
|
27
28
|
],
|
|
29
|
+
include: [],
|
|
28
30
|
engines: {
|
|
29
31
|
format: true,
|
|
30
32
|
lint: true,
|
|
@@ -60,7 +62,7 @@ const DEFAULT_CONFIG = {
|
|
|
60
62
|
smoothing: 20
|
|
61
63
|
},
|
|
62
64
|
ci: {
|
|
63
|
-
failBelow:
|
|
65
|
+
failBelow: 70,
|
|
64
66
|
format: "json"
|
|
65
67
|
},
|
|
66
68
|
telemetry: { enabled: true }
|
|
@@ -150,7 +152,7 @@ const ScoringSchema = z$1.object({
|
|
|
150
152
|
smoothing: z$1.number().nonnegative().default(20)
|
|
151
153
|
});
|
|
152
154
|
const CiSchema = z$1.object({
|
|
153
|
-
failBelow: z$1.number().default(
|
|
155
|
+
failBelow: z$1.number().default(70),
|
|
154
156
|
format: z$1.enum(["json"]).default("json")
|
|
155
157
|
});
|
|
156
158
|
const TelemetrySchema = z$1.object({ enabled: z$1.boolean().default(true) });
|
|
@@ -184,7 +186,7 @@ const AislopConfigSchema = z$1.object({
|
|
|
184
186
|
smoothing: 20
|
|
185
187
|
})),
|
|
186
188
|
ci: CiSchema.default(() => ({
|
|
187
|
-
failBelow:
|
|
189
|
+
failBelow: 70,
|
|
188
190
|
format: "json"
|
|
189
191
|
})),
|
|
190
192
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
@@ -194,7 +196,8 @@ const AislopConfigSchema = z$1.object({
|
|
|
194
196
|
"dist",
|
|
195
197
|
"build",
|
|
196
198
|
"coverage"
|
|
197
|
-
])
|
|
199
|
+
]),
|
|
200
|
+
include: z$1.array(z$1.string()).default(() => [])
|
|
198
201
|
});
|
|
199
202
|
const defaults = AislopConfigSchema.parse({});
|
|
200
203
|
/**
|
|
@@ -274,31 +277,68 @@ const EXCLUDED_DIRS = [
|
|
|
274
277
|
"dist",
|
|
275
278
|
"build",
|
|
276
279
|
".git",
|
|
280
|
+
".agents",
|
|
277
281
|
"vendor",
|
|
282
|
+
"examples",
|
|
283
|
+
"example",
|
|
284
|
+
"demos",
|
|
285
|
+
"demo",
|
|
286
|
+
"bench",
|
|
287
|
+
"benches",
|
|
288
|
+
"benchmarks",
|
|
289
|
+
"fixtures",
|
|
290
|
+
"fixture",
|
|
291
|
+
"samples",
|
|
292
|
+
"sample",
|
|
293
|
+
"tutorials",
|
|
294
|
+
"tutorial",
|
|
295
|
+
"code_samples",
|
|
296
|
+
"code-samples",
|
|
297
|
+
"notebooks",
|
|
278
298
|
"tests",
|
|
279
299
|
"test",
|
|
280
300
|
"__tests__",
|
|
281
301
|
"__test__",
|
|
282
302
|
"spec",
|
|
283
303
|
"__mocks__",
|
|
284
|
-
"fixtures",
|
|
285
304
|
"test_data",
|
|
286
305
|
".next",
|
|
287
306
|
".nuxt",
|
|
288
307
|
"coverage",
|
|
289
|
-
".turbo"
|
|
308
|
+
".turbo",
|
|
309
|
+
"public"
|
|
290
310
|
];
|
|
291
311
|
const FIND_PRUNE_DIRS = [
|
|
292
312
|
"node_modules",
|
|
293
313
|
"dist",
|
|
294
314
|
"build",
|
|
295
315
|
".git",
|
|
316
|
+
".agents",
|
|
296
317
|
"vendor",
|
|
318
|
+
"examples",
|
|
319
|
+
"example",
|
|
320
|
+
"demos",
|
|
321
|
+
"demo",
|
|
322
|
+
"bench",
|
|
323
|
+
"benches",
|
|
324
|
+
"benchmarks",
|
|
325
|
+
"fixtures",
|
|
326
|
+
"fixture",
|
|
327
|
+
"samples",
|
|
328
|
+
"sample",
|
|
329
|
+
"tutorials",
|
|
330
|
+
"tutorial",
|
|
331
|
+
"code_samples",
|
|
332
|
+
"code-samples",
|
|
333
|
+
"notebooks",
|
|
297
334
|
".next",
|
|
298
335
|
".nuxt",
|
|
299
336
|
"coverage",
|
|
300
|
-
".turbo"
|
|
337
|
+
".turbo",
|
|
338
|
+
"public"
|
|
301
339
|
];
|
|
340
|
+
const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
|
|
341
|
+
const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
302
342
|
const TEST_FILE_PATTERNS = [
|
|
303
343
|
/(?:^|\/).*\.test\.[^/]+$/i,
|
|
304
344
|
/(?:^|\/).*\.spec\.[^/]+$/i,
|
|
@@ -323,6 +363,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
323
363
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
324
364
|
};
|
|
325
365
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
366
|
+
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
326
367
|
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
327
368
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
328
369
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
@@ -381,7 +422,7 @@ const normalizeExcludePatterns = (patterns) => {
|
|
|
381
422
|
return [p];
|
|
382
423
|
});
|
|
383
424
|
};
|
|
384
|
-
const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
|
|
425
|
+
const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
|
|
385
426
|
const extraSet = new Set(extraExtensions);
|
|
386
427
|
const normalizedFiles = files.map((file) => {
|
|
387
428
|
const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
|
|
@@ -396,8 +437,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
396
437
|
if (!normalizedExcludePatterns.length) return false;
|
|
397
438
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
398
439
|
};
|
|
440
|
+
const hasIncludePatterns = include.length > 0;
|
|
441
|
+
const isUserIncluded = (relativePath) => {
|
|
442
|
+
if (!hasIncludePatterns) return true;
|
|
443
|
+
return micromatch.isMatch(relativePath, include, { dot: true });
|
|
444
|
+
};
|
|
399
445
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
400
|
-
|
|
446
|
+
if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || ignoredPaths.has(relativePath)) return false;
|
|
447
|
+
if (!isUserIncluded(relativePath)) return false;
|
|
448
|
+
if (isUserExcluded(relativePath)) return false;
|
|
449
|
+
return hasAllowedExtension(relativePath, extraSet);
|
|
401
450
|
}).map(({ absolutePath }) => absolutePath);
|
|
402
451
|
};
|
|
403
452
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -1127,6 +1176,86 @@ const PYTHON_IMPORT_TO_PIP = {
|
|
|
1127
1176
|
redis: "redis"
|
|
1128
1177
|
};
|
|
1129
1178
|
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/engines/ai-slop/python-manifest.ts
|
|
1181
|
+
const addPyDep = (pyDeps, name) => {
|
|
1182
|
+
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1183
|
+
pyDeps.add(normalized);
|
|
1184
|
+
};
|
|
1185
|
+
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1186
|
+
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1187
|
+
if (!fs.existsSync(reqPath)) return false;
|
|
1188
|
+
try {
|
|
1189
|
+
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1190
|
+
for (const line of content.split("\n")) {
|
|
1191
|
+
const trimmed = line.trim();
|
|
1192
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1193
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1194
|
+
if (match) addPyDep(pyDeps, match[1]);
|
|
1195
|
+
}
|
|
1196
|
+
return true;
|
|
1197
|
+
} catch {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1202
|
+
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1203
|
+
if (!fs.existsSync(pyprojPath)) return false;
|
|
1204
|
+
try {
|
|
1205
|
+
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1206
|
+
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1207
|
+
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1208
|
+
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1209
|
+
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1210
|
+
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1211
|
+
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1212
|
+
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1213
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1214
|
+
}
|
|
1215
|
+
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1216
|
+
let match = poetryRe.exec(content);
|
|
1217
|
+
while (match !== null) {
|
|
1218
|
+
for (const line of match[1].split("\n")) {
|
|
1219
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1220
|
+
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1221
|
+
}
|
|
1222
|
+
match = poetryRe.exec(content);
|
|
1223
|
+
}
|
|
1224
|
+
return true;
|
|
1225
|
+
} catch {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1230
|
+
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1231
|
+
if (!fs.existsSync(pipfilePath)) return false;
|
|
1232
|
+
try {
|
|
1233
|
+
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1234
|
+
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1235
|
+
let match = sectionRe.exec(content);
|
|
1236
|
+
while (match !== null) {
|
|
1237
|
+
for (const line of match[2].split("\n")) {
|
|
1238
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1239
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1240
|
+
}
|
|
1241
|
+
match = sectionRe.exec(content);
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
} catch {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
const collectPythonDeps = (rootDir) => {
|
|
1249
|
+
const pyDeps = /* @__PURE__ */ new Set();
|
|
1250
|
+
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1251
|
+
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1252
|
+
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1253
|
+
return {
|
|
1254
|
+
pyDeps,
|
|
1255
|
+
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
1256
|
+
};
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1130
1259
|
//#endregion
|
|
1131
1260
|
//#region src/engines/ai-slop/hallucinated-imports.ts
|
|
1132
1261
|
const JS_EXTENSIONS$1 = new Set([
|
|
@@ -1263,10 +1392,26 @@ const buildAliasMatcher = (key) => {
|
|
|
1263
1392
|
};
|
|
1264
1393
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1265
1394
|
const opts = readJson(configPath)?.compilerOptions;
|
|
1266
|
-
if (!opts
|
|
1395
|
+
if (!opts) return;
|
|
1267
1396
|
const paths = opts.paths;
|
|
1268
|
-
if (
|
|
1269
|
-
|
|
1397
|
+
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1398
|
+
const baseUrl = opts.baseUrl;
|
|
1399
|
+
if (typeof baseUrl === "string") {
|
|
1400
|
+
const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
|
|
1401
|
+
let entries;
|
|
1402
|
+
try {
|
|
1403
|
+
entries = fs.readdirSync(baseUrlDir);
|
|
1404
|
+
} catch {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const baseSpecifiers = /* @__PURE__ */ new Set();
|
|
1408
|
+
for (const entry of entries) {
|
|
1409
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1410
|
+
const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
|
|
1411
|
+
if (base.length > 0) baseSpecifiers.add(base);
|
|
1412
|
+
}
|
|
1413
|
+
for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
|
|
1414
|
+
}
|
|
1270
1415
|
};
|
|
1271
1416
|
const collectTsPathAliases = (rootDir) => {
|
|
1272
1417
|
const matchers = [];
|
|
@@ -1274,97 +1419,35 @@ const collectTsPathAliases = (rootDir) => {
|
|
|
1274
1419
|
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1275
1420
|
return matchers;
|
|
1276
1421
|
};
|
|
1277
|
-
const addPyDep = (pyDeps, name) => {
|
|
1278
|
-
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1279
|
-
pyDeps.add(normalized);
|
|
1280
|
-
};
|
|
1281
|
-
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1282
|
-
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1283
|
-
if (!fs.existsSync(reqPath)) return false;
|
|
1284
|
-
try {
|
|
1285
|
-
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1286
|
-
for (const line of content.split("\n")) {
|
|
1287
|
-
const trimmed = line.trim();
|
|
1288
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1289
|
-
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1290
|
-
if (match) addPyDep(pyDeps, match[1]);
|
|
1291
|
-
}
|
|
1292
|
-
return true;
|
|
1293
|
-
} catch {
|
|
1294
|
-
return false;
|
|
1295
|
-
}
|
|
1296
|
-
};
|
|
1297
|
-
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1298
|
-
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1299
|
-
if (!fs.existsSync(pyprojPath)) return false;
|
|
1300
|
-
try {
|
|
1301
|
-
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1302
|
-
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1303
|
-
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1304
|
-
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1305
|
-
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1306
|
-
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1307
|
-
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1308
|
-
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1309
|
-
if (m) addPyDep(pyDeps, m[1]);
|
|
1310
|
-
}
|
|
1311
|
-
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1312
|
-
let match = poetryRe.exec(content);
|
|
1313
|
-
while (match !== null) {
|
|
1314
|
-
for (const line of match[1].split("\n")) {
|
|
1315
|
-
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1316
|
-
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1317
|
-
}
|
|
1318
|
-
match = poetryRe.exec(content);
|
|
1319
|
-
}
|
|
1320
|
-
return true;
|
|
1321
|
-
} catch {
|
|
1322
|
-
return false;
|
|
1323
|
-
}
|
|
1324
|
-
};
|
|
1325
|
-
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1326
|
-
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1327
|
-
if (!fs.existsSync(pipfilePath)) return false;
|
|
1328
|
-
try {
|
|
1329
|
-
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1330
|
-
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1331
|
-
let match = sectionRe.exec(content);
|
|
1332
|
-
while (match !== null) {
|
|
1333
|
-
for (const line of match[2].split("\n")) {
|
|
1334
|
-
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1335
|
-
if (m) addPyDep(pyDeps, m[1]);
|
|
1336
|
-
}
|
|
1337
|
-
match = sectionRe.exec(content);
|
|
1338
|
-
}
|
|
1339
|
-
return true;
|
|
1340
|
-
} catch {
|
|
1341
|
-
return false;
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
1422
|
const loadManifest = (rootDir) => {
|
|
1345
1423
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
1346
|
-
const pyDeps = /* @__PURE__ */ new Set();
|
|
1347
1424
|
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
1348
|
-
const
|
|
1349
|
-
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1350
|
-
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1425
|
+
const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
|
|
1351
1426
|
return {
|
|
1352
1427
|
jsDeps,
|
|
1353
1428
|
pyDeps,
|
|
1354
1429
|
hasJsManifest,
|
|
1355
|
-
hasPyManifest
|
|
1430
|
+
hasPyManifest
|
|
1356
1431
|
};
|
|
1357
1432
|
};
|
|
1358
1433
|
const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
|
|
1434
|
+
const RUNTIME_BUILTINS = new Set(["bun"]);
|
|
1359
1435
|
const isJsBuiltin = (spec) => {
|
|
1436
|
+
if (RUNTIME_BUILTINS.has(spec)) return true;
|
|
1360
1437
|
return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
|
|
1361
1438
|
};
|
|
1362
1439
|
const VIRTUAL_MODULE_PREFIXES = [
|
|
1363
1440
|
"astro:",
|
|
1364
1441
|
"virtual:",
|
|
1365
|
-
"bun:"
|
|
1442
|
+
"bun:",
|
|
1443
|
+
"~icons/"
|
|
1366
1444
|
];
|
|
1367
1445
|
const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
|
|
1446
|
+
const stripImportQuery = (spec) => {
|
|
1447
|
+
const idx = spec.indexOf("?");
|
|
1448
|
+
return idx === -1 ? spec : spec.slice(0, idx);
|
|
1449
|
+
};
|
|
1450
|
+
const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
|
|
1368
1451
|
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
1369
1452
|
const isLikelyRealImportSpec = (spec) => {
|
|
1370
1453
|
if (spec.length === 0) return false;
|
|
@@ -1431,10 +1514,14 @@ const extractPyImports = (content) => {
|
|
|
1431
1514
|
}
|
|
1432
1515
|
return results;
|
|
1433
1516
|
};
|
|
1434
|
-
const checkJsImport = (
|
|
1517
|
+
const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
1518
|
+
const spec = stripImportQuery(rawSpec);
|
|
1519
|
+
if (spec.length === 0) return null;
|
|
1435
1520
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1436
1521
|
if (isJsBuiltin(spec)) return null;
|
|
1437
1522
|
if (isJsVirtualModule(spec)) return null;
|
|
1523
|
+
const virtualOwner = VIRTUAL_ASSET_FILES[spec];
|
|
1524
|
+
if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
|
|
1438
1525
|
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
1439
1526
|
const pkg = packageNameFromImport(spec);
|
|
1440
1527
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
@@ -2789,64 +2876,88 @@ const analyzeFunctions = (content, ext) => {
|
|
|
2789
2876
|
}
|
|
2790
2877
|
return functions;
|
|
2791
2878
|
};
|
|
2792
|
-
const
|
|
2879
|
+
const FILE_LOC_MULTIPLIERS = {
|
|
2880
|
+
".tsx": 1.5,
|
|
2881
|
+
".jsx": 1.5,
|
|
2882
|
+
".rs": 2.5,
|
|
2883
|
+
".go": 1.5
|
|
2884
|
+
};
|
|
2885
|
+
const DECLARATION_FILE_RE = /\.d\.ts$/i;
|
|
2886
|
+
const fileLocBudget = (ext, relativePath, base) => {
|
|
2887
|
+
if (DECLARATION_FILE_RE.test(relativePath)) return Number.POSITIVE_INFINITY;
|
|
2888
|
+
const multiplier = FILE_LOC_MULTIPLIERS[ext] ?? 1;
|
|
2889
|
+
return Math.ceil(base * multiplier);
|
|
2890
|
+
};
|
|
2793
2891
|
const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
2794
2892
|
const results = [];
|
|
2795
2893
|
const lineCount = content.split("\n").length;
|
|
2796
2894
|
const ext = path.extname(relativePath).toLowerCase();
|
|
2797
2895
|
if (isDataFile(content)) return results;
|
|
2798
|
-
const configuredMax = ext
|
|
2896
|
+
const configuredMax = fileLocBudget(ext, relativePath, limits.maxFileLoc);
|
|
2897
|
+
if (!Number.isFinite(configuredMax)) return results;
|
|
2799
2898
|
if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
|
|
2800
2899
|
filePath: relativePath,
|
|
2801
2900
|
engine: "code-quality",
|
|
2802
2901
|
rule: "complexity/file-too-large",
|
|
2803
2902
|
severity: "warning",
|
|
2804
|
-
message: `File
|
|
2903
|
+
message: `File too large (max: ${configuredMax})`,
|
|
2805
2904
|
help: "Consider splitting this file into smaller modules",
|
|
2806
2905
|
line: 0,
|
|
2807
2906
|
column: 0,
|
|
2808
2907
|
category: "Complexity",
|
|
2809
|
-
fixable: false
|
|
2908
|
+
fixable: false,
|
|
2909
|
+
detail: `${lineCount} lines`
|
|
2810
2910
|
});
|
|
2811
2911
|
return results;
|
|
2812
2912
|
};
|
|
2813
|
-
const
|
|
2913
|
+
const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
|
|
2914
|
+
const isComponentFunction = (name, ext) => JSX_EXTENSIONS.has(ext) && /^[A-Z]/.test(name);
|
|
2915
|
+
const functionLocBudget = (fn, ext, base) => {
|
|
2916
|
+
if (isComponentFunction(fn.name, ext)) return Math.ceil(base * 2);
|
|
2917
|
+
if (ext === ".rs") return Math.ceil(base * 1.5);
|
|
2918
|
+
return base;
|
|
2919
|
+
};
|
|
2920
|
+
const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
|
|
2814
2921
|
const results = [];
|
|
2815
|
-
|
|
2922
|
+
const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
|
|
2923
|
+
if (fn.lineCount - fn.templateLines > Math.ceil(fnMax * 1.1)) results.push({
|
|
2816
2924
|
filePath: relativePath,
|
|
2817
2925
|
engine: "code-quality",
|
|
2818
2926
|
rule: "complexity/function-too-long",
|
|
2819
2927
|
severity: "warning",
|
|
2820
|
-
message: `Function
|
|
2928
|
+
message: `Function too long (max: ${fnMax})`,
|
|
2821
2929
|
help: "Consider breaking this function into smaller pieces",
|
|
2822
2930
|
line: fn.startLine,
|
|
2823
2931
|
column: 0,
|
|
2824
2932
|
category: "Complexity",
|
|
2825
|
-
fixable: false
|
|
2933
|
+
fixable: false,
|
|
2934
|
+
detail: `${fn.name} · ${fn.lineCount} lines`
|
|
2826
2935
|
});
|
|
2827
2936
|
if (fn.maxNesting > limits.maxNesting) results.push({
|
|
2828
2937
|
filePath: relativePath,
|
|
2829
2938
|
engine: "code-quality",
|
|
2830
2939
|
rule: "complexity/deep-nesting",
|
|
2831
2940
|
severity: "warning",
|
|
2832
|
-
message: `Function
|
|
2941
|
+
message: `Function nested too deeply (max: ${limits.maxNesting})`,
|
|
2833
2942
|
help: "Consider using early returns or extracting nested logic",
|
|
2834
2943
|
line: fn.startLine,
|
|
2835
2944
|
column: 0,
|
|
2836
2945
|
category: "Complexity",
|
|
2837
|
-
fixable: false
|
|
2946
|
+
fixable: false,
|
|
2947
|
+
detail: `${fn.name} · depth ${fn.maxNesting}`
|
|
2838
2948
|
});
|
|
2839
2949
|
if (fn.paramCount > limits.maxParams) results.push({
|
|
2840
2950
|
filePath: relativePath,
|
|
2841
2951
|
engine: "code-quality",
|
|
2842
2952
|
rule: "complexity/too-many-params",
|
|
2843
2953
|
severity: "warning",
|
|
2844
|
-
message: `Function
|
|
2954
|
+
message: `Function has too many parameters (max: ${limits.maxParams})`,
|
|
2845
2955
|
help: "Consider using an options object parameter",
|
|
2846
2956
|
line: fn.startLine,
|
|
2847
2957
|
column: 0,
|
|
2848
2958
|
category: "Complexity",
|
|
2849
|
-
fixable: false
|
|
2959
|
+
fixable: false,
|
|
2960
|
+
detail: `${fn.name} · ${fn.paramCount} params`
|
|
2850
2961
|
});
|
|
2851
2962
|
return results;
|
|
2852
2963
|
};
|
|
@@ -2861,7 +2972,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
|
2861
2972
|
}
|
|
2862
2973
|
const ext = path.extname(filePath).toLowerCase();
|
|
2863
2974
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
2864
|
-
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
2975
|
+
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits, ext));
|
|
2865
2976
|
return diagnostics;
|
|
2866
2977
|
};
|
|
2867
2978
|
const checkComplexity = async (context) => {
|
|
@@ -2966,17 +3077,19 @@ const findDuplicateBlocks = (content, relativePath) => {
|
|
|
2966
3077
|
});
|
|
2967
3078
|
}
|
|
2968
3079
|
return reports.map((r) => {
|
|
3080
|
+
const span = r.currentEnd - r.currentStart + 1;
|
|
2969
3081
|
return {
|
|
2970
3082
|
filePath: relativePath,
|
|
2971
3083
|
engine: "code-quality",
|
|
2972
3084
|
rule: "code-quality/duplicate-block",
|
|
2973
3085
|
severity: "warning",
|
|
2974
|
-
message:
|
|
3086
|
+
message: "Duplicate code block — extract a shared helper",
|
|
2975
3087
|
help: `Pull the shared logic into a function both sites can call. Keeps one version of the truth and makes future changes one-shot instead of N-shot.`,
|
|
2976
3088
|
line: r.currentStart,
|
|
2977
3089
|
column: 0,
|
|
2978
3090
|
category: "Complexity",
|
|
2979
|
-
fixable: false
|
|
3091
|
+
fixable: false,
|
|
3092
|
+
detail: `${span} lines duplicate block at L${r.priorStart}`
|
|
2980
3093
|
};
|
|
2981
3094
|
});
|
|
2982
3095
|
};
|
|
@@ -3518,16 +3631,34 @@ const isToolAvailable = async (toolName) => {
|
|
|
3518
3631
|
return isToolInstalled(toolName);
|
|
3519
3632
|
};
|
|
3520
3633
|
|
|
3634
|
+
//#endregion
|
|
3635
|
+
//#region src/engines/python-targets.ts
|
|
3636
|
+
const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]);
|
|
3637
|
+
const normalizeProjectPath = (filePath) => filePath.split(path.sep).join("/");
|
|
3638
|
+
const getPythonTargets = (context) => {
|
|
3639
|
+
const targets = (context.files ?? getSourceFiles(context)).filter((filePath) => PYTHON_EXTENSIONS.has(path.extname(filePath).toLowerCase())).map((filePath) => {
|
|
3640
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(context.rootDirectory, filePath);
|
|
3641
|
+
return normalizeProjectPath(path.relative(context.rootDirectory, absolutePath));
|
|
3642
|
+
}).filter((filePath) => filePath.length > 0 && !filePath.startsWith(".."));
|
|
3643
|
+
return [...new Set(targets)];
|
|
3644
|
+
};
|
|
3645
|
+
const getRuffDiagnosticPath = (rootDirectory, filePath) => {
|
|
3646
|
+
const normalizedPath = filePath.replace(/^a\//, "");
|
|
3647
|
+
return normalizeProjectPath(path.isAbsolute(normalizedPath) ? path.relative(rootDirectory, normalizedPath) : normalizedPath);
|
|
3648
|
+
};
|
|
3649
|
+
|
|
3521
3650
|
//#endregion
|
|
3522
3651
|
//#region src/engines/format/ruff-format.ts
|
|
3523
3652
|
const runRuffFormat = async (context) => {
|
|
3524
3653
|
const ruffBinary = resolveToolBinary("ruff");
|
|
3654
|
+
const targets = getPythonTargets(context);
|
|
3655
|
+
if (targets.length === 0) return [];
|
|
3525
3656
|
try {
|
|
3526
3657
|
const result = await runSubprocess(ruffBinary, [
|
|
3527
3658
|
"format",
|
|
3528
3659
|
"--check",
|
|
3529
3660
|
"--diff",
|
|
3530
|
-
|
|
3661
|
+
...targets
|
|
3531
3662
|
], {
|
|
3532
3663
|
cwd: context.rootDirectory,
|
|
3533
3664
|
timeout: 6e4
|
|
@@ -3543,9 +3674,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
3543
3674
|
const filePattern = /^--- (.+)$/gm;
|
|
3544
3675
|
let match;
|
|
3545
3676
|
while ((match = filePattern.exec(output)) !== null) {
|
|
3546
|
-
const filePath = match[1]
|
|
3677
|
+
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
3547
3678
|
diagnostics.push({
|
|
3548
|
-
filePath
|
|
3679
|
+
filePath,
|
|
3549
3680
|
engine: "format",
|
|
3550
3681
|
rule: "python-formatting",
|
|
3551
3682
|
severity: "warning",
|
|
@@ -3815,6 +3946,95 @@ const resolveOxlintBinary = () => {
|
|
|
3815
3946
|
return "oxlint";
|
|
3816
3947
|
}
|
|
3817
3948
|
};
|
|
3949
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
3950
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
3951
|
+
const AMBIENT_GLOBAL_DEPS = [
|
|
3952
|
+
"unplugin-icons",
|
|
3953
|
+
"@types/bun",
|
|
3954
|
+
"bun-types"
|
|
3955
|
+
];
|
|
3956
|
+
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
3957
|
+
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
3958
|
+
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
3959
|
+
const detectAmbientSources = (rootDir) => {
|
|
3960
|
+
const found = /* @__PURE__ */ new Set();
|
|
3961
|
+
const skipDirs = new Set([
|
|
3962
|
+
"node_modules",
|
|
3963
|
+
".git",
|
|
3964
|
+
"dist",
|
|
3965
|
+
"build",
|
|
3966
|
+
"out",
|
|
3967
|
+
"target",
|
|
3968
|
+
"coverage",
|
|
3969
|
+
".next",
|
|
3970
|
+
".turbo"
|
|
3971
|
+
]);
|
|
3972
|
+
const walk = (dir, depth) => {
|
|
3973
|
+
if (depth > 4 || found.size === AMBIENT_GLOBAL_DEPS.length) return;
|
|
3974
|
+
let entries;
|
|
3975
|
+
try {
|
|
3976
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3977
|
+
} catch {
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
for (const entry of entries) {
|
|
3981
|
+
if (found.size === AMBIENT_GLOBAL_DEPS.length) return;
|
|
3982
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
3983
|
+
if (skipDirs.has(entry.name)) continue;
|
|
3984
|
+
const full = path.join(dir, entry.name);
|
|
3985
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
3986
|
+
else if (entry.name === "package.json") try {
|
|
3987
|
+
const pkg = JSON.parse(fs.readFileSync(full, "utf-8"));
|
|
3988
|
+
const allDeps = {
|
|
3989
|
+
...pkg.dependencies ?? {},
|
|
3990
|
+
...pkg.devDependencies ?? {},
|
|
3991
|
+
...pkg.peerDependencies ?? {}
|
|
3992
|
+
};
|
|
3993
|
+
for (const dep of AMBIENT_GLOBAL_DEPS) if (dep in allDeps) found.add(dep);
|
|
3994
|
+
} catch {}
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
walk(rootDir, 0);
|
|
3998
|
+
return found;
|
|
3999
|
+
};
|
|
4000
|
+
const extractNoUndefIdentifier = (message) => {
|
|
4001
|
+
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
4002
|
+
};
|
|
4003
|
+
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
4004
|
+
if (rule !== "eslint/no-undef") return false;
|
|
4005
|
+
const ident = extractNoUndefIdentifier(message);
|
|
4006
|
+
if (!ident) return false;
|
|
4007
|
+
if (sources.has("unplugin-icons") && ICON_AUTOIMPORT_RE.test(ident)) return true;
|
|
4008
|
+
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
4009
|
+
return false;
|
|
4010
|
+
};
|
|
4011
|
+
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
4012
|
+
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4013
|
+
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4014
|
+
if (cached !== void 0) return cached;
|
|
4015
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
4016
|
+
let referenced = false;
|
|
4017
|
+
try {
|
|
4018
|
+
const fd = fs.openSync(absolute, "r");
|
|
4019
|
+
try {
|
|
4020
|
+
const buf = Buffer.alloc(512);
|
|
4021
|
+
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
|
|
4022
|
+
referenced = SST_PLATFORM_REF_RE.test(buf.toString("utf-8", 0, bytesRead));
|
|
4023
|
+
} finally {
|
|
4024
|
+
fs.closeSync(fd);
|
|
4025
|
+
}
|
|
4026
|
+
} catch {
|
|
4027
|
+
referenced = false;
|
|
4028
|
+
}
|
|
4029
|
+
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4030
|
+
return referenced;
|
|
4031
|
+
};
|
|
4032
|
+
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4033
|
+
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4034
|
+
if (rule !== "eslint/no-unused-vars") return false;
|
|
4035
|
+
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4036
|
+
return match ? match[1].startsWith("_") : false;
|
|
4037
|
+
};
|
|
3818
4038
|
const parseRuleCode = (code) => {
|
|
3819
4039
|
if (!code) return {
|
|
3820
4040
|
plugin: "eslint",
|
|
@@ -3853,6 +4073,8 @@ const runOxlint = async (context) => {
|
|
|
3853
4073
|
framework: context.frameworks.find((f) => f !== "none"),
|
|
3854
4074
|
testFramework: detectTestFramework(context.rootDirectory)
|
|
3855
4075
|
});
|
|
4076
|
+
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
4077
|
+
sstReferencedFiles.clear();
|
|
3856
4078
|
try {
|
|
3857
4079
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
3858
4080
|
const args = [
|
|
@@ -3892,6 +4114,11 @@ const runOxlint = async (context) => {
|
|
|
3892
4114
|
fixable: false
|
|
3893
4115
|
};
|
|
3894
4116
|
}).filter((d) => {
|
|
4117
|
+
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
4118
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
4119
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
4120
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
4121
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
3895
4122
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
3896
4123
|
if (seen.has(key)) return false;
|
|
3897
4124
|
seen.add(key);
|
|
@@ -3906,18 +4133,20 @@ const runOxlint = async (context) => {
|
|
|
3906
4133
|
//#region src/engines/lint/ruff.ts
|
|
3907
4134
|
const runRuffLint = async (context) => {
|
|
3908
4135
|
const ruffBinary = resolveToolBinary("ruff");
|
|
4136
|
+
const targets = getPythonTargets(context);
|
|
4137
|
+
if (targets.length === 0) return [];
|
|
3909
4138
|
try {
|
|
3910
4139
|
const output = (await runSubprocess(ruffBinary, [
|
|
3911
4140
|
"check",
|
|
3912
4141
|
"--output-format=json",
|
|
3913
|
-
|
|
4142
|
+
...targets
|
|
3914
4143
|
], {
|
|
3915
4144
|
cwd: context.rootDirectory,
|
|
3916
4145
|
timeout: 6e4
|
|
3917
4146
|
})).stdout;
|
|
3918
4147
|
if (!output) return [];
|
|
3919
4148
|
return JSON.parse(output).map((d) => ({
|
|
3920
|
-
filePath:
|
|
4149
|
+
filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
|
|
3921
4150
|
engine: "lint",
|
|
3922
4151
|
rule: `ruff/${d.code}`,
|
|
3923
4152
|
severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
|
|
@@ -4012,56 +4241,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
|
|
|
4012
4241
|
return [];
|
|
4013
4242
|
}
|
|
4014
4243
|
};
|
|
4244
|
+
const SEVERITY_RANK = {
|
|
4245
|
+
critical: 4,
|
|
4246
|
+
high: 3,
|
|
4247
|
+
moderate: 2,
|
|
4248
|
+
low: 1
|
|
4249
|
+
};
|
|
4015
4250
|
const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
|
|
4016
|
-
const
|
|
4251
|
+
const upsertVuln = (bucket, packageName, severity, recommendation) => {
|
|
4252
|
+
const existing = bucket.get(packageName);
|
|
4253
|
+
if (existing) {
|
|
4254
|
+
existing.advisories++;
|
|
4255
|
+
if ((SEVERITY_RANK[severity] ?? 0) > (SEVERITY_RANK[existing.worstSeverity] ?? 0)) existing.worstSeverity = severity;
|
|
4256
|
+
if (recommendation) existing.recommendations.add(recommendation);
|
|
4257
|
+
} else bucket.set(packageName, {
|
|
4258
|
+
packageName,
|
|
4259
|
+
worstSeverity: severity,
|
|
4260
|
+
advisories: 1,
|
|
4261
|
+
recommendations: recommendation ? new Set([recommendation]) : /* @__PURE__ */ new Set()
|
|
4262
|
+
});
|
|
4263
|
+
};
|
|
4264
|
+
const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
|
|
4265
|
+
const cmpSemver = (a, b) => {
|
|
4266
|
+
const [, a1, a2, a3] = SEMVER_RE.exec(a) ?? [
|
|
4267
|
+
"",
|
|
4268
|
+
"0",
|
|
4269
|
+
"0",
|
|
4270
|
+
"0"
|
|
4271
|
+
];
|
|
4272
|
+
const [, b1, b2, b3] = SEMVER_RE.exec(b) ?? [
|
|
4273
|
+
"",
|
|
4274
|
+
"0",
|
|
4275
|
+
"0",
|
|
4276
|
+
"0"
|
|
4277
|
+
];
|
|
4278
|
+
if (Number(a1) !== Number(b1)) return Number(a1) - Number(b1);
|
|
4279
|
+
if (Number(a2) !== Number(b2)) return Number(a2) - Number(b2);
|
|
4280
|
+
return Number(a3) - Number(b3);
|
|
4281
|
+
};
|
|
4282
|
+
const pickBestRecommendation = (recs) => {
|
|
4283
|
+
if (recs.length <= 1) return recs[0] ?? "";
|
|
4284
|
+
const versioned = recs.filter((r) => SEMVER_RE.test(r));
|
|
4285
|
+
if (versioned.length === 0) return recs[0];
|
|
4286
|
+
return versioned.reduce((best, r) => cmpSemver(r, best) > 0 ? r : best);
|
|
4287
|
+
};
|
|
4288
|
+
const cleanRecommendation = (raw) => {
|
|
4289
|
+
const t = raw.trim();
|
|
4290
|
+
if (!t || t.toLowerCase() === "none") return "no fix available";
|
|
4291
|
+
return t;
|
|
4292
|
+
};
|
|
4293
|
+
const aggregateToDiagnostic = (agg, source) => {
|
|
4294
|
+
const best = cleanRecommendation(pickBestRecommendation([...agg.recommendations]));
|
|
4295
|
+
const countLabel = agg.advisories > 1 ? ` (${agg.advisories} advisories)` : "";
|
|
4296
|
+
const recLabel = best ? ` — ${best}` : "";
|
|
4297
|
+
return {
|
|
4298
|
+
filePath: "package.json",
|
|
4299
|
+
engine: "security",
|
|
4300
|
+
rule: "security/vulnerable-dependency",
|
|
4301
|
+
severity: toSeverity(agg.worstSeverity),
|
|
4302
|
+
message: `${agg.packageName} (${agg.worstSeverity})${recLabel}${countLabel}`,
|
|
4303
|
+
help: "",
|
|
4304
|
+
line: 0,
|
|
4305
|
+
column: 0,
|
|
4306
|
+
category: "Security",
|
|
4307
|
+
fixable: false,
|
|
4308
|
+
detail: source === "npm audit" ? "npm" : "pnpm"
|
|
4309
|
+
};
|
|
4310
|
+
};
|
|
4017
4311
|
const parseLegacyAdvisories = (advisories, source) => {
|
|
4018
|
-
const
|
|
4019
|
-
for (const [key, advisory] of Object.entries(advisories))
|
|
4020
|
-
|
|
4021
|
-
const severity = (advisory.severity ?? "moderate").toLowerCase();
|
|
4022
|
-
const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
|
|
4023
|
-
diagnostics.push({
|
|
4024
|
-
filePath: "package.json",
|
|
4025
|
-
engine: "security",
|
|
4026
|
-
rule: "security/vulnerable-dependency",
|
|
4027
|
-
severity: toSeverity(severity),
|
|
4028
|
-
message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
|
|
4029
|
-
help: withFixHint(recommendation),
|
|
4030
|
-
line: 0,
|
|
4031
|
-
column: 0,
|
|
4032
|
-
category: "Security",
|
|
4033
|
-
fixable: false
|
|
4034
|
-
});
|
|
4035
|
-
}
|
|
4036
|
-
return diagnostics;
|
|
4312
|
+
const bucket = /* @__PURE__ */ new Map();
|
|
4313
|
+
for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
|
|
4314
|
+
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
4037
4315
|
};
|
|
4038
4316
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
4039
|
-
const
|
|
4317
|
+
const bucket = /* @__PURE__ */ new Map();
|
|
4040
4318
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
4041
4319
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
4042
4320
|
const fixAvailable = vulnerability.fixAvailable;
|
|
4043
4321
|
const isDirect = vulnerability.isDirect === true;
|
|
4044
|
-
let recommendation =
|
|
4045
|
-
if (fixAvailable === false) recommendation = isDirect ? "
|
|
4046
|
-
else if (!isDirect && fixAvailable === true) recommendation = "
|
|
4322
|
+
let recommendation = "";
|
|
4323
|
+
if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitive — needs override or parent upgrade";
|
|
4324
|
+
else if (!isDirect && fixAvailable === true) recommendation = "transitive — may need override or parent upgrade";
|
|
4047
4325
|
else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
|
|
4048
4326
|
const target = fixAvailable;
|
|
4049
|
-
if (target.name && target.version) recommendation = `
|
|
4327
|
+
if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
|
|
4050
4328
|
}
|
|
4051
|
-
|
|
4052
|
-
filePath: "package.json",
|
|
4053
|
-
engine: "security",
|
|
4054
|
-
rule: "security/vulnerable-dependency",
|
|
4055
|
-
severity: toSeverity(severity),
|
|
4056
|
-
message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
|
|
4057
|
-
help: withFixHint(recommendation),
|
|
4058
|
-
line: 0,
|
|
4059
|
-
column: 0,
|
|
4060
|
-
category: "Security",
|
|
4061
|
-
fixable: false
|
|
4062
|
-
});
|
|
4329
|
+
upsertVuln(bucket, packageName, severity, recommendation);
|
|
4063
4330
|
}
|
|
4064
|
-
return
|
|
4331
|
+
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
4065
4332
|
};
|
|
4066
4333
|
const parseJsAudit = (output, source) => {
|
|
4067
4334
|
if (!output) return [];
|
|
@@ -4977,10 +5244,17 @@ const runScan = async (cwd) => {
|
|
|
4977
5244
|
const config = loadConfig(cwd);
|
|
4978
5245
|
const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
|
|
4979
5246
|
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
5247
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
5248
|
+
const failBelow = config.ci.failBelow;
|
|
4980
5249
|
return {
|
|
4981
5250
|
project,
|
|
4982
5251
|
diagnostics,
|
|
4983
|
-
score
|
|
5252
|
+
score,
|
|
5253
|
+
qualityGate: {
|
|
5254
|
+
failBelow,
|
|
5255
|
+
passed: errorCount === 0 && score >= failBelow,
|
|
5256
|
+
errorCount
|
|
5257
|
+
}
|
|
4984
5258
|
};
|
|
4985
5259
|
};
|
|
4986
5260
|
const aislopScanInputSchema = z.object({ path: z.string().optional().describe("Project directory to scan. Defaults to the MCP server's cwd.") });
|
|
@@ -4990,10 +5264,11 @@ const aislopScanTool = {
|
|
|
4990
5264
|
inputSchema: aislopScanInputSchema
|
|
4991
5265
|
};
|
|
4992
5266
|
const handleAislopScan = async (input) => {
|
|
4993
|
-
const { project, diagnostics, score } = await runScan(resolveCwd(input.path));
|
|
5267
|
+
const { project, diagnostics, score, qualityGate } = await runScan(resolveCwd(input.path));
|
|
4994
5268
|
const summary = summariseDiagnostics(diagnostics, project.rootDirectory);
|
|
4995
5269
|
return {
|
|
4996
5270
|
score,
|
|
5271
|
+
qualityGate,
|
|
4997
5272
|
fileCount: project.sourceFileCount,
|
|
4998
5273
|
languages: project.languages,
|
|
4999
5274
|
frameworks: project.frameworks,
|
|
@@ -5094,7 +5369,225 @@ const handleAislopBaseline = (input) => {
|
|
|
5094
5369
|
|
|
5095
5370
|
//#endregion
|
|
5096
5371
|
//#region src/version.ts
|
|
5097
|
-
const APP_VERSION = "0.
|
|
5372
|
+
const APP_VERSION = "0.9.1";
|
|
5373
|
+
|
|
5374
|
+
//#endregion
|
|
5375
|
+
//#region src/telemetry/env.ts
|
|
5376
|
+
const detectPackageManager = (env = process.env) => {
|
|
5377
|
+
const execPath = env.npm_execpath ?? "";
|
|
5378
|
+
if (execPath.includes("npx")) return "npx";
|
|
5379
|
+
const userAgent = env.npm_config_user_agent ?? "";
|
|
5380
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm";
|
|
5381
|
+
if (userAgent.startsWith("yarn/")) return "yarn";
|
|
5382
|
+
if (userAgent.startsWith("bun/")) return "bun";
|
|
5383
|
+
if (userAgent.startsWith("npm/")) return "npm";
|
|
5384
|
+
if (execPath.includes("pnpm")) return "pnpm";
|
|
5385
|
+
if (execPath.includes("yarn")) return "yarn";
|
|
5386
|
+
if (execPath.includes("bun")) return "bun";
|
|
5387
|
+
if (execPath.includes("npm")) return "npm";
|
|
5388
|
+
return "unknown";
|
|
5389
|
+
};
|
|
5390
|
+
const CI_ENV_KEYS = [
|
|
5391
|
+
"CI",
|
|
5392
|
+
"GITHUB_ACTIONS",
|
|
5393
|
+
"GITLAB_CI",
|
|
5394
|
+
"CIRCLECI",
|
|
5395
|
+
"TRAVIS",
|
|
5396
|
+
"BUILDKITE",
|
|
5397
|
+
"DRONE",
|
|
5398
|
+
"TEAMCITY_VERSION",
|
|
5399
|
+
"TF_BUILD"
|
|
5400
|
+
];
|
|
5401
|
+
const isCiEnv = (env = process.env) => CI_ENV_KEYS.some((k) => {
|
|
5402
|
+
const v = env[k];
|
|
5403
|
+
return v === "true" || v === "1" || v != null && v.length > 0 && k !== "CI";
|
|
5404
|
+
}) || env.CI === "true" || env.CI === "1";
|
|
5405
|
+
|
|
5406
|
+
//#endregion
|
|
5407
|
+
//#region src/telemetry/identity.ts
|
|
5408
|
+
const FILE_BASENAME = "install_id";
|
|
5409
|
+
const resolveInstallIdPath = (homedir = os.homedir(), env = process.env) => {
|
|
5410
|
+
if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", FILE_BASENAME);
|
|
5411
|
+
return path.join(homedir, ".aislop", FILE_BASENAME);
|
|
5412
|
+
};
|
|
5413
|
+
const ensureInstallId = (idPath = resolveInstallIdPath()) => {
|
|
5414
|
+
if (fs.existsSync(idPath)) {
|
|
5415
|
+
const existing = fs.readFileSync(idPath, "utf-8").trim();
|
|
5416
|
+
if (existing.length > 0) return {
|
|
5417
|
+
installId: existing,
|
|
5418
|
+
created: false
|
|
5419
|
+
};
|
|
5420
|
+
}
|
|
5421
|
+
const dir = path.dirname(idPath);
|
|
5422
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
5423
|
+
const installId = randomUUID();
|
|
5424
|
+
const tmpPath = `${idPath}.${process.pid}.tmp`;
|
|
5425
|
+
fs.writeFileSync(tmpPath, `${installId}\n`, { mode: 384 });
|
|
5426
|
+
try {
|
|
5427
|
+
fs.renameSync(tmpPath, idPath);
|
|
5428
|
+
return {
|
|
5429
|
+
installId,
|
|
5430
|
+
created: true
|
|
5431
|
+
};
|
|
5432
|
+
} catch {
|
|
5433
|
+
fs.rmSync(tmpPath, { force: true });
|
|
5434
|
+
return {
|
|
5435
|
+
installId: fs.readFileSync(idPath, "utf-8").trim(),
|
|
5436
|
+
created: false
|
|
5437
|
+
};
|
|
5438
|
+
}
|
|
5439
|
+
};
|
|
5440
|
+
|
|
5441
|
+
//#endregion
|
|
5442
|
+
//#region src/telemetry/redaction.ts
|
|
5443
|
+
const SAFE_PROPERTY_NAMES = new Set([
|
|
5444
|
+
"aislop_version",
|
|
5445
|
+
"node_version",
|
|
5446
|
+
"os",
|
|
5447
|
+
"arch",
|
|
5448
|
+
"schema_version",
|
|
5449
|
+
"anonymous_install_id",
|
|
5450
|
+
"package_manager",
|
|
5451
|
+
"is_ci",
|
|
5452
|
+
"command",
|
|
5453
|
+
"language_summary",
|
|
5454
|
+
"lang_typescript",
|
|
5455
|
+
"lang_javascript",
|
|
5456
|
+
"lang_python",
|
|
5457
|
+
"lang_java",
|
|
5458
|
+
"file_count_bucket",
|
|
5459
|
+
"exit_code",
|
|
5460
|
+
"duration_ms",
|
|
5461
|
+
"error_kind",
|
|
5462
|
+
"score",
|
|
5463
|
+
"score_bucket",
|
|
5464
|
+
"finding_count",
|
|
5465
|
+
"error_count",
|
|
5466
|
+
"warning_count",
|
|
5467
|
+
"fixable_count",
|
|
5468
|
+
"fix_steps",
|
|
5469
|
+
"fix_resolved",
|
|
5470
|
+
"fix_score_delta",
|
|
5471
|
+
"engine_format_issues",
|
|
5472
|
+
"engine_format_ms",
|
|
5473
|
+
"engine_lint_issues",
|
|
5474
|
+
"engine_lint_ms",
|
|
5475
|
+
"engine_code_quality_issues",
|
|
5476
|
+
"engine_code_quality_ms",
|
|
5477
|
+
"engine_ai_slop_issues",
|
|
5478
|
+
"engine_ai_slop_ms",
|
|
5479
|
+
"engine_architecture_issues",
|
|
5480
|
+
"engine_architecture_ms",
|
|
5481
|
+
"engine_security_issues",
|
|
5482
|
+
"engine_security_ms",
|
|
5483
|
+
"tool",
|
|
5484
|
+
"ok",
|
|
5485
|
+
"agent",
|
|
5486
|
+
"score_delta"
|
|
5487
|
+
]);
|
|
5488
|
+
const redactProperties = (props) => {
|
|
5489
|
+
const clean = {};
|
|
5490
|
+
const dropped = [];
|
|
5491
|
+
for (const [key, value] of Object.entries(props)) {
|
|
5492
|
+
if (value === void 0) continue;
|
|
5493
|
+
if (SAFE_PROPERTY_NAMES.has(key)) clean[key] = value;
|
|
5494
|
+
else dropped.push(key);
|
|
5495
|
+
}
|
|
5496
|
+
return {
|
|
5497
|
+
clean,
|
|
5498
|
+
dropped
|
|
5499
|
+
};
|
|
5500
|
+
};
|
|
5501
|
+
|
|
5502
|
+
//#endregion
|
|
5503
|
+
//#region src/telemetry/client.ts
|
|
5504
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
5505
|
+
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
5506
|
+
const SCHEMA_VERSION = "v2";
|
|
5507
|
+
const REQUEST_TIMEOUT_MS = 3e3;
|
|
5508
|
+
const isTelemetryDisabled = (config) => {
|
|
5509
|
+
const env = process.env;
|
|
5510
|
+
if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
|
|
5511
|
+
if (config?.enabled === false) return true;
|
|
5512
|
+
if (config?.enabled === true) return false;
|
|
5513
|
+
if (env.CI === "true" || env.CI === "1") return true;
|
|
5514
|
+
return false;
|
|
5515
|
+
};
|
|
5516
|
+
const isDebug = () => process.env.AISLOP_TELEMETRY_DEBUG === "1";
|
|
5517
|
+
const pendingRequests = /* @__PURE__ */ new Set();
|
|
5518
|
+
let cachedInstallId = null;
|
|
5519
|
+
let installCreated = false;
|
|
5520
|
+
const baseProperties = (installId) => ({
|
|
5521
|
+
aislop_version: APP_VERSION,
|
|
5522
|
+
node_version: process.version,
|
|
5523
|
+
os: os.platform(),
|
|
5524
|
+
arch: os.arch(),
|
|
5525
|
+
schema_version: SCHEMA_VERSION,
|
|
5526
|
+
anonymous_install_id: installId,
|
|
5527
|
+
package_manager: detectPackageManager(),
|
|
5528
|
+
is_ci: isCiEnv()
|
|
5529
|
+
});
|
|
5530
|
+
const track = (input) => {
|
|
5531
|
+
if (isTelemetryDisabled(input.config)) return { installCreated: false };
|
|
5532
|
+
if (cachedInstallId == null) {
|
|
5533
|
+
const ensured = ensureInstallId(resolveInstallIdPath());
|
|
5534
|
+
cachedInstallId = ensured.installId;
|
|
5535
|
+
installCreated = ensured.created;
|
|
5536
|
+
}
|
|
5537
|
+
const { clean, dropped } = redactProperties({
|
|
5538
|
+
...baseProperties(cachedInstallId),
|
|
5539
|
+
...input.properties
|
|
5540
|
+
});
|
|
5541
|
+
if (isDebug()) {
|
|
5542
|
+
const compact = JSON.stringify({
|
|
5543
|
+
event: input.event,
|
|
5544
|
+
properties: clean
|
|
5545
|
+
});
|
|
5546
|
+
process.stderr.write(`[telemetry] ${compact}\n`);
|
|
5547
|
+
if (dropped.length > 0) for (const key of dropped) process.stderr.write(`[telemetry] dropped non-allowlisted property: ${key}\n`);
|
|
5548
|
+
}
|
|
5549
|
+
if (process.env.AISLOP_TELEMETRY_DRY_RUN === "1") return { installCreated };
|
|
5550
|
+
const payload = {
|
|
5551
|
+
api_key: POSTHOG_KEY,
|
|
5552
|
+
event: input.event,
|
|
5553
|
+
distinct_id: cachedInstallId,
|
|
5554
|
+
properties: clean,
|
|
5555
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5556
|
+
};
|
|
5557
|
+
const request = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
5558
|
+
method: "POST",
|
|
5559
|
+
headers: { "Content-Type": "application/json" },
|
|
5560
|
+
body: JSON.stringify(payload),
|
|
5561
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
5562
|
+
}).then(() => {}).catch(() => {}).finally(() => {
|
|
5563
|
+
pendingRequests.delete(request);
|
|
5564
|
+
});
|
|
5565
|
+
pendingRequests.add(request);
|
|
5566
|
+
return { installCreated };
|
|
5567
|
+
};
|
|
5568
|
+
const flushTelemetry = async () => {
|
|
5569
|
+
if (pendingRequests.size === 0) return;
|
|
5570
|
+
await Promise.all(pendingRequests);
|
|
5571
|
+
};
|
|
5572
|
+
|
|
5573
|
+
//#endregion
|
|
5574
|
+
//#region src/telemetry/events.ts
|
|
5575
|
+
const buildMcpToolCalledProps = (input) => {
|
|
5576
|
+
const props = {
|
|
5577
|
+
tool: input.tool,
|
|
5578
|
+
duration_ms: Math.round(input.durationMs),
|
|
5579
|
+
ok: input.ok
|
|
5580
|
+
};
|
|
5581
|
+
if (input.errorKind) props.error_kind = input.errorKind;
|
|
5582
|
+
return props;
|
|
5583
|
+
};
|
|
5584
|
+
const errorKindFromException = (error) => {
|
|
5585
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
5586
|
+
if (message.includes("timeout") || message.includes("timed out")) return "timeout";
|
|
5587
|
+
if (message.includes("invalid config") || message.includes("config_invalid")) return "config_invalid";
|
|
5588
|
+
if (message.includes("engine") && message.includes("crash")) return "engine_crash";
|
|
5589
|
+
return "unknown";
|
|
5590
|
+
};
|
|
5098
5591
|
|
|
5099
5592
|
//#endregion
|
|
5100
5593
|
//#region src/mcp.ts
|
|
@@ -5109,10 +5602,29 @@ const err = (message) => ({
|
|
|
5109
5602
|
}],
|
|
5110
5603
|
isError: true
|
|
5111
5604
|
});
|
|
5112
|
-
const
|
|
5605
|
+
const instrument = async (tool, fn) => {
|
|
5606
|
+
const startedAt = performance.now();
|
|
5113
5607
|
try {
|
|
5114
|
-
|
|
5608
|
+
const value = await fn();
|
|
5609
|
+
track({
|
|
5610
|
+
event: "mcp_tool_called",
|
|
5611
|
+
properties: buildMcpToolCalledProps({
|
|
5612
|
+
tool,
|
|
5613
|
+
durationMs: performance.now() - startedAt,
|
|
5614
|
+
ok: true
|
|
5615
|
+
})
|
|
5616
|
+
});
|
|
5617
|
+
return ok(value);
|
|
5115
5618
|
} catch (e) {
|
|
5619
|
+
track({
|
|
5620
|
+
event: "mcp_tool_called",
|
|
5621
|
+
properties: buildMcpToolCalledProps({
|
|
5622
|
+
tool,
|
|
5623
|
+
durationMs: performance.now() - startedAt,
|
|
5624
|
+
ok: false,
|
|
5625
|
+
errorKind: errorKindFromException(e)
|
|
5626
|
+
})
|
|
5627
|
+
});
|
|
5116
5628
|
return err(e instanceof Error ? e.message : String(e));
|
|
5117
5629
|
}
|
|
5118
5630
|
};
|
|
@@ -5124,25 +5636,27 @@ const buildServer = () => {
|
|
|
5124
5636
|
server.registerTool(aislopScanTool.name, {
|
|
5125
5637
|
description: aislopScanTool.description,
|
|
5126
5638
|
inputSchema: aislopScanInputSchema.shape
|
|
5127
|
-
}, (input) =>
|
|
5639
|
+
}, (input) => instrument("aislop_scan", () => handleAislopScan(input)));
|
|
5128
5640
|
server.registerTool(aislopFixTool.name, {
|
|
5129
5641
|
description: aislopFixTool.description,
|
|
5130
5642
|
inputSchema: aislopFixInputSchema.shape
|
|
5131
|
-
}, (input) =>
|
|
5643
|
+
}, (input) => instrument("aislop_fix", () => handleAislopFix(input)));
|
|
5132
5644
|
server.registerTool(aislopWhyTool.name, {
|
|
5133
5645
|
description: aislopWhyTool.description,
|
|
5134
5646
|
inputSchema: aislopWhyInputSchema.shape
|
|
5135
|
-
}, (input) =>
|
|
5647
|
+
}, (input) => instrument("aislop_why", () => handleAislopWhy(input)));
|
|
5136
5648
|
server.registerTool(aislopBaselineTool.name, {
|
|
5137
5649
|
description: aislopBaselineTool.description,
|
|
5138
5650
|
inputSchema: aislopBaselineInputSchema.shape
|
|
5139
|
-
}, (input) =>
|
|
5651
|
+
}, (input) => instrument("aislop_baseline", () => handleAislopBaseline(input)));
|
|
5140
5652
|
return server;
|
|
5141
5653
|
};
|
|
5142
5654
|
const main = async () => {
|
|
5143
5655
|
const server = buildServer();
|
|
5144
5656
|
const transport = new StdioServerTransport();
|
|
5145
5657
|
await server.connect(transport);
|
|
5658
|
+
track({ event: "mcp_server_started" });
|
|
5659
|
+
await flushTelemetry();
|
|
5146
5660
|
};
|
|
5147
5661
|
main().catch((e) => {
|
|
5148
5662
|
process.stderr.write(`aislop-mcp failed to start: ${e instanceof Error ? e.message : String(e)}\n`);
|