agentinit 1.5.0 → 1.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/CHANGELOG.md +21 -0
- package/README.md +93 -10
- package/dist/agentinit-1.7.0.tgz +0 -0
- package/dist/agents/DroidAgent.d.ts +52 -0
- package/dist/agents/DroidAgent.d.ts.map +1 -0
- package/dist/agents/DroidAgent.js +228 -0
- package/dist/agents/DroidAgent.js.map +1 -0
- package/dist/cli.js +501 -48
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +2 -2
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/verifyMcp.d.ts.map +1 -1
- package/dist/commands/verifyMcp.js +26 -4
- package/dist/commands/verifyMcp.js.map +1 -1
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/constants/index.js.map +1 -1
- package/dist/constants/mcp.d.ts +1 -0
- package/dist/constants/mcp.d.ts.map +1 -1
- package/dist/constants/mcp.js +2 -0
- package/dist/constants/mcp.js.map +1 -1
- package/dist/core/agentManager.d.ts.map +1 -1
- package/dist/core/agentManager.js +3 -1
- package/dist/core/agentManager.js.map +1 -1
- package/dist/core/mcpClient.d.ts +124 -6
- package/dist/core/mcpClient.d.ts.map +1 -1
- package/dist/core/mcpClient.js +385 -39
- package/dist/core/mcpClient.js.map +1 -1
- package/dist/lib/utils/index.d.ts +3 -1
- package/dist/lib/utils/index.d.ts.map +1 -1
- package/dist/lib/utils/index.js +4 -1
- package/dist/lib/utils/index.js.map +1 -1
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/jsonSchema.d.ts +31 -0
- package/dist/types/jsonSchema.d.ts.map +1 -0
- package/dist/types/jsonSchema.js +6 -0
- package/dist/types/jsonSchema.js.map +1 -0
- package/dist/utils/packageVersion.d.ts +105 -0
- package/dist/utils/packageVersion.d.ts.map +1 -0
- package/dist/utils/packageVersion.js +219 -0
- package/dist/utils/packageVersion.js.map +1 -0
- package/package.json +1 -1
- package/dist/agentinit-1.5.0.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -19950,27 +19950,27 @@ var require_windows = __commonJS((exports, module) => {
|
|
|
19950
19950
|
return checkPathExt(path, options2);
|
|
19951
19951
|
};
|
|
19952
19952
|
var isexe = function(path, options2, cb) {
|
|
19953
|
-
|
|
19953
|
+
fs15.stat(path, function(er, stat) {
|
|
19954
19954
|
cb(er, er ? false : checkStat(stat, path, options2));
|
|
19955
19955
|
});
|
|
19956
19956
|
};
|
|
19957
19957
|
var sync = function(path, options2) {
|
|
19958
|
-
return checkStat(
|
|
19958
|
+
return checkStat(fs15.statSync(path), path, options2);
|
|
19959
19959
|
};
|
|
19960
19960
|
module.exports = isexe;
|
|
19961
19961
|
isexe.sync = sync;
|
|
19962
|
-
var
|
|
19962
|
+
var fs15 = __require("fs");
|
|
19963
19963
|
});
|
|
19964
19964
|
|
|
19965
19965
|
// node_modules/isexe/mode.js
|
|
19966
19966
|
var require_mode = __commonJS((exports, module) => {
|
|
19967
19967
|
var isexe = function(path, options2, cb) {
|
|
19968
|
-
|
|
19968
|
+
fs15.stat(path, function(er, stat) {
|
|
19969
19969
|
cb(er, er ? false : checkStat(stat, options2));
|
|
19970
19970
|
});
|
|
19971
19971
|
};
|
|
19972
19972
|
var sync = function(path, options2) {
|
|
19973
|
-
return checkStat(
|
|
19973
|
+
return checkStat(fs15.statSync(path), options2);
|
|
19974
19974
|
};
|
|
19975
19975
|
var checkStat = function(stat, options2) {
|
|
19976
19976
|
return stat.isFile() && checkMode(stat, options2);
|
|
@@ -19990,7 +19990,7 @@ var require_mode = __commonJS((exports, module) => {
|
|
|
19990
19990
|
};
|
|
19991
19991
|
module.exports = isexe;
|
|
19992
19992
|
isexe.sync = sync;
|
|
19993
|
-
var
|
|
19993
|
+
var fs15 = __require("fs");
|
|
19994
19994
|
});
|
|
19995
19995
|
|
|
19996
19996
|
// node_modules/isexe/index.js
|
|
@@ -20035,7 +20035,7 @@ var require_isexe = __commonJS((exports, module) => {
|
|
|
20035
20035
|
}
|
|
20036
20036
|
}
|
|
20037
20037
|
};
|
|
20038
|
-
var
|
|
20038
|
+
var fs15 = __require("fs");
|
|
20039
20039
|
var core;
|
|
20040
20040
|
if (process.platform === "win32" || global.TESTING_WINDOWS) {
|
|
20041
20041
|
core = require_windows();
|
|
@@ -20242,14 +20242,14 @@ var require_readShebang = __commonJS((exports, module) => {
|
|
|
20242
20242
|
const buffer = Buffer.alloc(size);
|
|
20243
20243
|
let fd;
|
|
20244
20244
|
try {
|
|
20245
|
-
fd =
|
|
20246
|
-
|
|
20247
|
-
|
|
20245
|
+
fd = fs15.openSync(command, "r");
|
|
20246
|
+
fs15.readSync(fd, buffer, 0, size, 0);
|
|
20247
|
+
fs15.closeSync(fd);
|
|
20248
20248
|
} catch (e) {
|
|
20249
20249
|
}
|
|
20250
20250
|
return shebangCommand(buffer.toString());
|
|
20251
20251
|
};
|
|
20252
|
-
var
|
|
20252
|
+
var fs15 = __require("fs");
|
|
20253
20253
|
var shebangCommand = require_shebang_command();
|
|
20254
20254
|
module.exports = readShebang;
|
|
20255
20255
|
});
|
|
@@ -23001,6 +23001,7 @@ var getPackageVersion = function() {
|
|
|
23001
23001
|
}
|
|
23002
23002
|
};
|
|
23003
23003
|
var DEFAULT_CONNECTION_TIMEOUT_MS = 30000;
|
|
23004
|
+
var MAX_RESOURCE_CONTENT_SIZE = 10 * 1024 * 1024;
|
|
23004
23005
|
var MCP_VERIFIER_CONFIG = {
|
|
23005
23006
|
name: "agentinit-verifier",
|
|
23006
23007
|
version: getPackageVersion()
|
|
@@ -24862,6 +24863,179 @@ class CursorAgent extends Agent {
|
|
|
24862
24863
|
}
|
|
24863
24864
|
}
|
|
24864
24865
|
|
|
24866
|
+
// dist/agents/DroidAgent.js
|
|
24867
|
+
class DroidAgent extends Agent {
|
|
24868
|
+
constructor() {
|
|
24869
|
+
const definition = {
|
|
24870
|
+
id: "droid",
|
|
24871
|
+
name: "Droid (Factory)",
|
|
24872
|
+
url: "https://factory.ai",
|
|
24873
|
+
capabilities: {
|
|
24874
|
+
mcp: {
|
|
24875
|
+
stdio: true,
|
|
24876
|
+
http: false,
|
|
24877
|
+
sse: false
|
|
24878
|
+
},
|
|
24879
|
+
rules: true,
|
|
24880
|
+
hooks: false,
|
|
24881
|
+
commands: false,
|
|
24882
|
+
subagents: false,
|
|
24883
|
+
statusline: false
|
|
24884
|
+
},
|
|
24885
|
+
configFiles: [
|
|
24886
|
+
{
|
|
24887
|
+
path: "AGENTS.md",
|
|
24888
|
+
purpose: "rules",
|
|
24889
|
+
format: "markdown",
|
|
24890
|
+
type: "file",
|
|
24891
|
+
optional: true,
|
|
24892
|
+
description: "Agent instructions and rules in markdown format"
|
|
24893
|
+
}
|
|
24894
|
+
],
|
|
24895
|
+
nativeConfigPath: ".factory/mcp.json",
|
|
24896
|
+
globalConfigPath: "~/.factory/mcp.json"
|
|
24897
|
+
};
|
|
24898
|
+
super(definition);
|
|
24899
|
+
}
|
|
24900
|
+
async applyMCPConfig(projectPath, servers) {
|
|
24901
|
+
await this.applyGlobalMCPConfig(servers);
|
|
24902
|
+
}
|
|
24903
|
+
async applyGlobalMCPConfig(servers) {
|
|
24904
|
+
const globalPath = this.getGlobalMcpPath();
|
|
24905
|
+
if (!globalPath) {
|
|
24906
|
+
throw new Error(`Droid global configuration path could not be determined`);
|
|
24907
|
+
}
|
|
24908
|
+
await ensureDirectoryExists(globalPath);
|
|
24909
|
+
const existingContent = await readFileIfExists(globalPath);
|
|
24910
|
+
let existingConfig = { mcpServers: {} };
|
|
24911
|
+
if (existingContent) {
|
|
24912
|
+
try {
|
|
24913
|
+
existingConfig = JSON.parse(existingContent);
|
|
24914
|
+
if (!existingConfig.mcpServers) {
|
|
24915
|
+
existingConfig.mcpServers = {};
|
|
24916
|
+
}
|
|
24917
|
+
} catch (error) {
|
|
24918
|
+
console.warn("Warning: Existing ~/.factory/mcp.json is invalid, creating new configuration");
|
|
24919
|
+
existingConfig = { mcpServers: {} };
|
|
24920
|
+
}
|
|
24921
|
+
}
|
|
24922
|
+
for (const server of servers) {
|
|
24923
|
+
const droidServer = {
|
|
24924
|
+
type: "stdio"
|
|
24925
|
+
};
|
|
24926
|
+
if (server.command) {
|
|
24927
|
+
droidServer.command = server.command;
|
|
24928
|
+
}
|
|
24929
|
+
if (server.args && server.args.length > 0) {
|
|
24930
|
+
droidServer.args = server.args;
|
|
24931
|
+
}
|
|
24932
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
24933
|
+
droidServer.env = server.env;
|
|
24934
|
+
}
|
|
24935
|
+
droidServer.disabled = false;
|
|
24936
|
+
existingConfig.mcpServers[server.name] = droidServer;
|
|
24937
|
+
}
|
|
24938
|
+
const configJson = JSON.stringify(existingConfig, null, 2);
|
|
24939
|
+
await writeFile(globalPath, configJson);
|
|
24940
|
+
}
|
|
24941
|
+
filterMCPServers(servers) {
|
|
24942
|
+
return servers.filter((server) => server.type === "stdio");
|
|
24943
|
+
}
|
|
24944
|
+
transformMCPServers(servers) {
|
|
24945
|
+
return servers;
|
|
24946
|
+
}
|
|
24947
|
+
async applyRulesConfig(configPath, rules, existingContent) {
|
|
24948
|
+
const rulesSection = this.generateRulesContent(rules.sections);
|
|
24949
|
+
let content = existingContent;
|
|
24950
|
+
if (content && !content.endsWith("\n")) {
|
|
24951
|
+
content += "\n";
|
|
24952
|
+
}
|
|
24953
|
+
if (content) {
|
|
24954
|
+
content += "\n";
|
|
24955
|
+
}
|
|
24956
|
+
content += rulesSection;
|
|
24957
|
+
return content.trim() + "\n";
|
|
24958
|
+
}
|
|
24959
|
+
extractExistingRules(content) {
|
|
24960
|
+
const ruleLines = content.split("\n").filter((line) => line.trim().startsWith("- "));
|
|
24961
|
+
return ruleLines.map((line) => line.replace(/^- /, "").trim()).filter((rule) => rule.length > 0);
|
|
24962
|
+
}
|
|
24963
|
+
extractExistingSections(content) {
|
|
24964
|
+
const lines = content.split("\n");
|
|
24965
|
+
const sections = [];
|
|
24966
|
+
let currentSection = null;
|
|
24967
|
+
for (const line of lines) {
|
|
24968
|
+
const trimmed = line.trim();
|
|
24969
|
+
if (trimmed.startsWith("## ") && trimmed.includes(" ")) {
|
|
24970
|
+
if (currentSection) {
|
|
24971
|
+
sections.push(currentSection);
|
|
24972
|
+
}
|
|
24973
|
+
const sectionName = trimmed.replace(/^##\s*/, "");
|
|
24974
|
+
currentSection = {
|
|
24975
|
+
templateId: sectionName.toLowerCase().replace(/\s+/g, "_"),
|
|
24976
|
+
templateName: sectionName,
|
|
24977
|
+
rules: []
|
|
24978
|
+
};
|
|
24979
|
+
} else if (currentSection && trimmed.startsWith("- ")) {
|
|
24980
|
+
const rule = trimmed.replace(/^- /, "");
|
|
24981
|
+
currentSection.rules.push(rule);
|
|
24982
|
+
}
|
|
24983
|
+
}
|
|
24984
|
+
if (currentSection) {
|
|
24985
|
+
sections.push(currentSection);
|
|
24986
|
+
}
|
|
24987
|
+
return sections;
|
|
24988
|
+
}
|
|
24989
|
+
generateRulesContent(sections) {
|
|
24990
|
+
let content = "";
|
|
24991
|
+
if (sections && sections.length > 0) {
|
|
24992
|
+
for (const ruleSection of sections) {
|
|
24993
|
+
content += `## ${ruleSection.templateName}\n\n`;
|
|
24994
|
+
for (const rule of ruleSection.rules) {
|
|
24995
|
+
content += `- ${rule}\n`;
|
|
24996
|
+
}
|
|
24997
|
+
content += "\n";
|
|
24998
|
+
}
|
|
24999
|
+
}
|
|
25000
|
+
return content;
|
|
25001
|
+
}
|
|
25002
|
+
async getMCPServers(projectPath) {
|
|
25003
|
+
const globalPath = this.getGlobalMcpPath();
|
|
25004
|
+
if (!globalPath) {
|
|
25005
|
+
return [];
|
|
25006
|
+
}
|
|
25007
|
+
const configContent = await readFileIfExists(globalPath);
|
|
25008
|
+
if (!configContent) {
|
|
25009
|
+
return [];
|
|
25010
|
+
}
|
|
25011
|
+
try {
|
|
25012
|
+
const config = JSON.parse(configContent);
|
|
25013
|
+
const servers = [];
|
|
25014
|
+
if (config.mcpServers) {
|
|
25015
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
25016
|
+
const server = {
|
|
25017
|
+
name,
|
|
25018
|
+
type: "stdio"
|
|
25019
|
+
};
|
|
25020
|
+
if (serverConfig.command) {
|
|
25021
|
+
server.command = serverConfig.command;
|
|
25022
|
+
}
|
|
25023
|
+
if (serverConfig.args) {
|
|
25024
|
+
server.args = serverConfig.args;
|
|
25025
|
+
}
|
|
25026
|
+
if (serverConfig.env) {
|
|
25027
|
+
server.env = serverConfig.env;
|
|
25028
|
+
}
|
|
25029
|
+
servers.push(server);
|
|
25030
|
+
}
|
|
25031
|
+
}
|
|
25032
|
+
return servers;
|
|
25033
|
+
} catch (error) {
|
|
25034
|
+
return [];
|
|
25035
|
+
}
|
|
25036
|
+
}
|
|
25037
|
+
}
|
|
25038
|
+
|
|
24865
25039
|
// dist/core/agentManager.js
|
|
24866
25040
|
class AgentManager {
|
|
24867
25041
|
agents = [];
|
|
@@ -24874,7 +25048,8 @@ class AgentManager {
|
|
|
24874
25048
|
new ClaudeDesktopAgent,
|
|
24875
25049
|
new CodexCliAgent,
|
|
24876
25050
|
new GeminiCliAgent,
|
|
24877
|
-
new CursorAgent
|
|
25051
|
+
new CursorAgent,
|
|
25052
|
+
new DroidAgent
|
|
24878
25053
|
];
|
|
24879
25054
|
}
|
|
24880
25055
|
getAllAgents() {
|
|
@@ -32263,6 +32438,96 @@ class SSEClientTransport {
|
|
|
32263
32438
|
}
|
|
32264
32439
|
}
|
|
32265
32440
|
|
|
32441
|
+
// dist/utils/packageVersion.js
|
|
32442
|
+
function extractExplicitVersion(packageSpec) {
|
|
32443
|
+
const versionMatch = packageSpec.match(/@([\d]+\.[\d]+\.[\d]+(?:[-+].+)?$)/);
|
|
32444
|
+
if (versionMatch && versionMatch[1]) {
|
|
32445
|
+
logger.debug(`[extractExplicitVersion] Found explicit version: ${versionMatch[1]} in "${packageSpec}"`);
|
|
32446
|
+
return versionMatch[1];
|
|
32447
|
+
}
|
|
32448
|
+
return null;
|
|
32449
|
+
}
|
|
32450
|
+
function extractPackageName(packageSpec) {
|
|
32451
|
+
return packageSpec.replace(/@[\d]+\.[\d]+\.[\d]+(?:[-+].+)?$/, "").replace(/@latest$/, "").replace(/@next$/, "").replace(/@canary$/, "");
|
|
32452
|
+
}
|
|
32453
|
+
function extractPackageFromCommand(command, args) {
|
|
32454
|
+
if (!command || !args || args.length === 0) {
|
|
32455
|
+
return null;
|
|
32456
|
+
}
|
|
32457
|
+
const packageManagers = ["npx", "bunx", "pnpm", "yarn"];
|
|
32458
|
+
if (!packageManagers.includes(command)) {
|
|
32459
|
+
logger.debug(`[extractPackageFromCommand] Not a package manager command: ${command}`);
|
|
32460
|
+
return null;
|
|
32461
|
+
}
|
|
32462
|
+
for (const arg of args) {
|
|
32463
|
+
if (arg.startsWith("-")) {
|
|
32464
|
+
continue;
|
|
32465
|
+
}
|
|
32466
|
+
if (arg === "dlx" || arg === "exec") {
|
|
32467
|
+
continue;
|
|
32468
|
+
}
|
|
32469
|
+
const packagePattern = /^(@[a-z0-9-]+\/)?[a-z0-9_-]+(@[\w.-]+)?$/i;
|
|
32470
|
+
if (packagePattern.test(arg)) {
|
|
32471
|
+
logger.debug(`[extractPackageFromCommand] Found package: ${arg}`);
|
|
32472
|
+
return arg;
|
|
32473
|
+
}
|
|
32474
|
+
}
|
|
32475
|
+
logger.debug(`[extractPackageFromCommand] No package found in args: ${args.join(" ")}`);
|
|
32476
|
+
return null;
|
|
32477
|
+
}
|
|
32478
|
+
async function fetchLatestVersion(packageSpec, options2 = {}) {
|
|
32479
|
+
const { timeout = 5000, useCache = true } = options2;
|
|
32480
|
+
const explicitVersion = extractExplicitVersion(packageSpec);
|
|
32481
|
+
if (explicitVersion) {
|
|
32482
|
+
return explicitVersion;
|
|
32483
|
+
}
|
|
32484
|
+
const cleanPackageName = extractPackageName(packageSpec);
|
|
32485
|
+
if (useCache) {
|
|
32486
|
+
const cached = versionCache.get(cleanPackageName);
|
|
32487
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
32488
|
+
logger.debug(`[fetchLatestVersion] Cache hit for: ${cleanPackageName} -> ${cached.version}`);
|
|
32489
|
+
return cached.version;
|
|
32490
|
+
}
|
|
32491
|
+
}
|
|
32492
|
+
try {
|
|
32493
|
+
logger.debug(`[fetchLatestVersion] Fetching from npm registry: ${cleanPackageName}`);
|
|
32494
|
+
const controller = new AbortController;
|
|
32495
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
32496
|
+
const url = `https://registry.npmjs.org/${cleanPackageName}/latest`;
|
|
32497
|
+
const response = await fetch(url, {
|
|
32498
|
+
headers: { Accept: "application/json" },
|
|
32499
|
+
signal: controller.signal
|
|
32500
|
+
});
|
|
32501
|
+
clearTimeout(timeoutId);
|
|
32502
|
+
if (!response.ok) {
|
|
32503
|
+
logger.debug(`[fetchLatestVersion] npm registry returned ${response.status} for ${cleanPackageName}`);
|
|
32504
|
+
return null;
|
|
32505
|
+
}
|
|
32506
|
+
const data = await response.json();
|
|
32507
|
+
if (!data.version) {
|
|
32508
|
+
logger.debug(`[fetchLatestVersion] No version field in response for ${cleanPackageName}`);
|
|
32509
|
+
return null;
|
|
32510
|
+
}
|
|
32511
|
+
versionCache.set(cleanPackageName, {
|
|
32512
|
+
version: data.version,
|
|
32513
|
+
timestamp: Date.now()
|
|
32514
|
+
});
|
|
32515
|
+
logger.debug(`[fetchLatestVersion] Success: ${cleanPackageName}@${data.version}`);
|
|
32516
|
+
return data.version;
|
|
32517
|
+
} catch (error) {
|
|
32518
|
+
if (error instanceof Error) {
|
|
32519
|
+
if (error.name === "AbortError") {
|
|
32520
|
+
logger.debug(`[fetchLatestVersion] Timeout fetching version for ${cleanPackageName}`);
|
|
32521
|
+
} else {
|
|
32522
|
+
logger.debug(`[fetchLatestVersion] Error fetching version: ${error.message}`);
|
|
32523
|
+
}
|
|
32524
|
+
}
|
|
32525
|
+
return null;
|
|
32526
|
+
}
|
|
32527
|
+
}
|
|
32528
|
+
var versionCache = new Map;
|
|
32529
|
+
var CACHE_TTL_MS = 5 * 60 * 1000;
|
|
32530
|
+
|
|
32266
32531
|
// dist/core/mcpClient.js
|
|
32267
32532
|
class MCPVerificationError extends Error {
|
|
32268
32533
|
serverName;
|
|
@@ -32285,6 +32550,83 @@ class MCPVerifier {
|
|
|
32285
32550
|
return yellow(tokenCount.toString());
|
|
32286
32551
|
return red(tokenCount.toString());
|
|
32287
32552
|
}
|
|
32553
|
+
hasValidInputSchema(tool) {
|
|
32554
|
+
return tool.inputSchema !== undefined && typeof tool.inputSchema === "object" && Object.keys(tool.inputSchema.properties || {}).length > 0;
|
|
32555
|
+
}
|
|
32556
|
+
formatType(schema) {
|
|
32557
|
+
const type = schema.type || "any";
|
|
32558
|
+
return Array.isArray(type) ? type.join(" | ") : type;
|
|
32559
|
+
}
|
|
32560
|
+
formatNumberRange(schema) {
|
|
32561
|
+
const min = schema.minimum !== undefined ? schema.minimum : "-\u221E";
|
|
32562
|
+
const max = schema.maximum !== undefined ? schema.maximum : "\u221E";
|
|
32563
|
+
return `(${min}-${max})`;
|
|
32564
|
+
}
|
|
32565
|
+
formatStringLength(schema) {
|
|
32566
|
+
const minLen = schema.minLength !== undefined ? schema.minLength : 0;
|
|
32567
|
+
const maxLen = schema.maxLength !== undefined ? schema.maxLength : "\u221E";
|
|
32568
|
+
return `length: ${minLen}-${maxLen}`;
|
|
32569
|
+
}
|
|
32570
|
+
formatArrayConstraints(schema) {
|
|
32571
|
+
const constraints = [];
|
|
32572
|
+
if (schema.items) {
|
|
32573
|
+
const itemType = schema.items.type;
|
|
32574
|
+
const itemTypeStr = itemType ? Array.isArray(itemType) ? itemType.join(" | ") : itemType : "any";
|
|
32575
|
+
constraints.push(`items: ${itemTypeStr}`);
|
|
32576
|
+
if (schema.minItems !== undefined || schema.maxItems !== undefined) {
|
|
32577
|
+
const minItems = schema.minItems !== undefined ? schema.minItems : 0;
|
|
32578
|
+
const maxItems = schema.maxItems !== undefined ? schema.maxItems : "\u221E";
|
|
32579
|
+
constraints.push(`(${minItems}-${maxItems} items)`);
|
|
32580
|
+
}
|
|
32581
|
+
}
|
|
32582
|
+
return constraints;
|
|
32583
|
+
}
|
|
32584
|
+
formatConstraints(schema, typeInfo) {
|
|
32585
|
+
const constraints = [];
|
|
32586
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
32587
|
+
constraints.push(`[${schema.enum.join(", ")}]`);
|
|
32588
|
+
}
|
|
32589
|
+
if (schema.minimum !== undefined || schema.maximum !== undefined) {
|
|
32590
|
+
constraints.push(this.formatNumberRange(schema));
|
|
32591
|
+
}
|
|
32592
|
+
if (schema.default !== undefined) {
|
|
32593
|
+
constraints.push(`default: ${JSON.stringify(schema.default)}`);
|
|
32594
|
+
}
|
|
32595
|
+
if (schema.minLength !== undefined || schema.maxLength !== undefined) {
|
|
32596
|
+
constraints.push(this.formatStringLength(schema));
|
|
32597
|
+
}
|
|
32598
|
+
if (schema.pattern) {
|
|
32599
|
+
constraints.push(`pattern: ${schema.pattern}`);
|
|
32600
|
+
}
|
|
32601
|
+
const isArray = Array.isArray(schema.type) ? schema.type.includes("array") : schema.type === "array";
|
|
32602
|
+
if (isArray) {
|
|
32603
|
+
constraints.push(...this.formatArrayConstraints(schema));
|
|
32604
|
+
}
|
|
32605
|
+
return constraints;
|
|
32606
|
+
}
|
|
32607
|
+
formatParameter(name, schema, isRequired) {
|
|
32608
|
+
const parts = [];
|
|
32609
|
+
const typeInfo = this.formatType(schema);
|
|
32610
|
+
parts.push(`${name} (${isRequired ? "required" : "optional"})`);
|
|
32611
|
+
parts.push(typeInfo);
|
|
32612
|
+
if (schema.description) {
|
|
32613
|
+
parts.push(schema.description);
|
|
32614
|
+
}
|
|
32615
|
+
const constraints = this.formatConstraints(schema, typeInfo);
|
|
32616
|
+
if (constraints.length > 0) {
|
|
32617
|
+
parts.push(constraints.join(", "));
|
|
32618
|
+
}
|
|
32619
|
+
return ` \u2022 ${parts.join(" - ")}`;
|
|
32620
|
+
}
|
|
32621
|
+
formatToolParameters(tool) {
|
|
32622
|
+
if (!this.hasValidInputSchema(tool)) {
|
|
32623
|
+
return [];
|
|
32624
|
+
}
|
|
32625
|
+
const schema = tool.inputSchema;
|
|
32626
|
+
const properties = schema.properties || {};
|
|
32627
|
+
const required = schema.required || [];
|
|
32628
|
+
return Object.entries(properties).map(([name, prop]) => this.formatParameter(name, prop, required.includes(name)));
|
|
32629
|
+
}
|
|
32288
32630
|
calculateToolTokens(tools, serverName) {
|
|
32289
32631
|
const toolTokenCounts = new Map;
|
|
32290
32632
|
let totalToolTokens = 0;
|
|
@@ -32316,19 +32658,19 @@ class MCPVerifier {
|
|
|
32316
32658
|
};
|
|
32317
32659
|
const functionDefinition = JSON.stringify(toolForCounting);
|
|
32318
32660
|
const claudeToolRepresentation = `<function>${functionDefinition}</function>`;
|
|
32319
|
-
const tokenCount = countTokens(claudeToolRepresentation) *
|
|
32661
|
+
const tokenCount = countTokens(claudeToolRepresentation) * 9;
|
|
32320
32662
|
toolTokenCounts.set(tool.name, tokenCount);
|
|
32321
32663
|
totalToolTokens += tokenCount;
|
|
32322
32664
|
} catch (error) {
|
|
32323
|
-
|
|
32665
|
+
logger.error(`Failed to count tokens for tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
32324
32666
|
toolTokenCounts.set(tool.name, 0);
|
|
32325
32667
|
}
|
|
32326
32668
|
}
|
|
32327
32669
|
return { toolTokenCounts, totalToolTokens };
|
|
32328
32670
|
}
|
|
32329
|
-
async verifyServer(server,
|
|
32671
|
+
async verifyServer(server, options2) {
|
|
32330
32672
|
const startTime = Date.now();
|
|
32331
|
-
const timeoutMs = timeout || this.defaultTimeout;
|
|
32673
|
+
const timeoutMs = options2?.timeout || this.defaultTimeout;
|
|
32332
32674
|
const abortController = new AbortController;
|
|
32333
32675
|
let client2 = null;
|
|
32334
32676
|
let transport = null;
|
|
@@ -32356,7 +32698,7 @@ class MCPVerifier {
|
|
|
32356
32698
|
clearTimeout(timeoutId);
|
|
32357
32699
|
});
|
|
32358
32700
|
});
|
|
32359
|
-
const connectPromise = this.connectAndVerify(client2, transport, server, abortController.signal);
|
|
32701
|
+
const connectPromise = this.connectAndVerify(client2, transport, server, abortController.signal, options2);
|
|
32360
32702
|
const capabilities = await Promise.race([connectPromise, timeoutPromise]);
|
|
32361
32703
|
const connectionTime = Date.now() - startTime;
|
|
32362
32704
|
return {
|
|
@@ -32388,8 +32730,8 @@ class MCPVerifier {
|
|
|
32388
32730
|
}
|
|
32389
32731
|
}
|
|
32390
32732
|
}
|
|
32391
|
-
async verifyServers(servers,
|
|
32392
|
-
const results = await Promise.allSettled(servers.map((server) => this.verifyServer(server,
|
|
32733
|
+
async verifyServers(servers, options2) {
|
|
32734
|
+
const results = await Promise.allSettled(servers.map((server) => this.verifyServer(server, options2)));
|
|
32393
32735
|
return results.map((result, index) => {
|
|
32394
32736
|
if (result.status === "fulfilled") {
|
|
32395
32737
|
return result.value;
|
|
@@ -32440,17 +32782,39 @@ class MCPVerifier {
|
|
|
32440
32782
|
throw new MCPVerificationError(`Unsupported server type: ${server.type}`, server.name);
|
|
32441
32783
|
}
|
|
32442
32784
|
}
|
|
32443
|
-
async connectAndVerify(client2, transport, server, abortSignal) {
|
|
32785
|
+
async connectAndVerify(client2, transport, server, abortSignal, options2) {
|
|
32444
32786
|
try {
|
|
32445
32787
|
if (abortSignal?.aborted) {
|
|
32446
32788
|
throw new Error("Operation aborted");
|
|
32447
32789
|
}
|
|
32448
32790
|
await client2.connect(transport);
|
|
32791
|
+
const serverVersion = client2.getServerVersion();
|
|
32792
|
+
logger.debug(`[Version Detection] MCP protocol returned: ${JSON.stringify(serverVersion)}`);
|
|
32449
32793
|
const serverInfo = {
|
|
32450
|
-
name: server.name,
|
|
32451
|
-
version: "unknown",
|
|
32794
|
+
name: serverVersion?.name || server.name,
|
|
32795
|
+
version: serverVersion?.version || "unknown",
|
|
32452
32796
|
protocolVersion: "unknown"
|
|
32453
32797
|
};
|
|
32798
|
+
if (serverInfo.version === "unknown" && server.type === MCPServerType.STDIO) {
|
|
32799
|
+
logger.debug(`[Version Detection] Version unknown, attempting npm registry fallback for STDIO server`);
|
|
32800
|
+
const packageSpec = extractPackageFromCommand(server.command, server.args);
|
|
32801
|
+
if (packageSpec) {
|
|
32802
|
+
logger.debug(`[Version Detection] Extracted package: ${packageSpec}`);
|
|
32803
|
+
try {
|
|
32804
|
+
const registryVersion = await fetchLatestVersion(packageSpec, { timeout: 3000 });
|
|
32805
|
+
if (registryVersion) {
|
|
32806
|
+
serverInfo.version = registryVersion;
|
|
32807
|
+
logger.debug(`[Version Detection] npm registry returned: ${registryVersion}`);
|
|
32808
|
+
} else {
|
|
32809
|
+
logger.debug(`[Version Detection] npm registry returned no version for: ${packageSpec}`);
|
|
32810
|
+
}
|
|
32811
|
+
} catch (error) {
|
|
32812
|
+
logger.debug(`[Version Detection] npm registry lookup failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
32813
|
+
}
|
|
32814
|
+
} else {
|
|
32815
|
+
logger.debug(`[Version Detection] Could not extract package name from command: ${server.command} ${server.args?.join(" ")}`);
|
|
32816
|
+
}
|
|
32817
|
+
}
|
|
32454
32818
|
if (abortSignal?.aborted) {
|
|
32455
32819
|
throw new Error("Operation aborted");
|
|
32456
32820
|
}
|
|
@@ -32473,7 +32837,7 @@ class MCPVerifier {
|
|
|
32473
32837
|
const resources = [];
|
|
32474
32838
|
try {
|
|
32475
32839
|
const resourcesResponse = await client2.listResources();
|
|
32476
|
-
|
|
32840
|
+
const resourcePromises = resourcesResponse.resources.map(async (resource) => {
|
|
32477
32841
|
const mcpResource = { uri: resource.uri };
|
|
32478
32842
|
if (resource.name !== undefined)
|
|
32479
32843
|
mcpResource.name = resource.name;
|
|
@@ -32481,8 +32845,34 @@ class MCPVerifier {
|
|
|
32481
32845
|
mcpResource.description = resource.description;
|
|
32482
32846
|
if (resource.mimeType !== undefined)
|
|
32483
32847
|
mcpResource.mimeType = resource.mimeType;
|
|
32848
|
+
if (options2?.includeResourceContents) {
|
|
32849
|
+
try {
|
|
32850
|
+
const resourceData = await client2.readResource({ uri: resource.uri });
|
|
32851
|
+
if (resourceData.contents && resourceData.contents.length > 0) {
|
|
32852
|
+
const content = resourceData.contents[0];
|
|
32853
|
+
if (content && "text" in content && typeof content.text === "string") {
|
|
32854
|
+
const contentSize = Buffer.byteLength(content.text, "utf8");
|
|
32855
|
+
if (contentSize > MAX_RESOURCE_CONTENT_SIZE) {
|
|
32856
|
+
logger.debug(`Resource content too large for ${resource.uri}: ${contentSize} bytes (max: ${MAX_RESOURCE_CONTENT_SIZE})`);
|
|
32857
|
+
} else {
|
|
32858
|
+
mcpResource.contents = content.text;
|
|
32859
|
+
}
|
|
32860
|
+
} else if (content && "blob" in content && typeof content.blob === "string") {
|
|
32861
|
+
const blobBuffer = Buffer.from(content.blob, "base64");
|
|
32862
|
+
if (blobBuffer.length > MAX_RESOURCE_CONTENT_SIZE) {
|
|
32863
|
+
logger.debug(`Resource content too large for ${resource.uri}: ${blobBuffer.length} bytes (max: ${MAX_RESOURCE_CONTENT_SIZE})`);
|
|
32864
|
+
} else {
|
|
32865
|
+
mcpResource.contents = new Uint8Array(blobBuffer);
|
|
32866
|
+
}
|
|
32867
|
+
}
|
|
32868
|
+
}
|
|
32869
|
+
} catch (resourceError) {
|
|
32870
|
+
logger.debug(`Failed to fetch resource content for ${resource.uri}: ${resourceError instanceof Error ? resourceError.message : "Unknown error"}`);
|
|
32871
|
+
}
|
|
32872
|
+
}
|
|
32484
32873
|
return mcpResource;
|
|
32485
|
-
})
|
|
32874
|
+
});
|
|
32875
|
+
resources.push(...await Promise.all(resourcePromises));
|
|
32486
32876
|
} catch (error) {
|
|
32487
32877
|
}
|
|
32488
32878
|
if (abortSignal?.aborted) {
|
|
@@ -32491,7 +32881,7 @@ class MCPVerifier {
|
|
|
32491
32881
|
const prompts2 = [];
|
|
32492
32882
|
try {
|
|
32493
32883
|
const promptsResponse = await client2.listPrompts();
|
|
32494
|
-
|
|
32884
|
+
const promptPromises = promptsResponse.prompts.map(async (prompt) => {
|
|
32495
32885
|
const mcpPrompt = { name: prompt.name };
|
|
32496
32886
|
if (prompt.description !== undefined)
|
|
32497
32887
|
mcpPrompt.description = prompt.description;
|
|
@@ -32505,19 +32895,40 @@ class MCPVerifier {
|
|
|
32505
32895
|
return mcpArg;
|
|
32506
32896
|
});
|
|
32507
32897
|
}
|
|
32898
|
+
if (options2?.includePromptDetails) {
|
|
32899
|
+
try {
|
|
32900
|
+
const promptData = await client2.getPrompt({ name: prompt.name, arguments: {} });
|
|
32901
|
+
if (promptData.messages && promptData.messages.length > 0) {
|
|
32902
|
+
mcpPrompt.template = promptData.messages.map((msg) => {
|
|
32903
|
+
if (typeof msg.content === "string")
|
|
32904
|
+
return msg.content;
|
|
32905
|
+
if (typeof msg.content === "object" && "text" in msg.content)
|
|
32906
|
+
return msg.content.text;
|
|
32907
|
+
return JSON.stringify(msg.content);
|
|
32908
|
+
}).join("\n\n");
|
|
32909
|
+
}
|
|
32910
|
+
} catch (promptError) {
|
|
32911
|
+
logger.debug(`Failed to fetch prompt template for ${prompt.name}: ${promptError instanceof Error ? promptError.message : "Unknown error"}`);
|
|
32912
|
+
}
|
|
32913
|
+
}
|
|
32508
32914
|
return mcpPrompt;
|
|
32509
|
-
})
|
|
32915
|
+
});
|
|
32916
|
+
prompts2.push(...await Promise.all(promptPromises));
|
|
32510
32917
|
} catch (error) {
|
|
32511
32918
|
}
|
|
32512
|
-
const
|
|
32513
|
-
|
|
32919
|
+
const shouldIncludeTokenCounts = options2?.includeTokenCounts !== false;
|
|
32920
|
+
const tokenData = shouldIncludeTokenCounts ? this.calculateToolTokens(tools, server.name) : undefined;
|
|
32921
|
+
const capabilities = {
|
|
32514
32922
|
tools,
|
|
32515
32923
|
resources,
|
|
32516
32924
|
prompts: prompts2,
|
|
32517
|
-
serverInfo
|
|
32518
|
-
toolTokenCounts,
|
|
32519
|
-
totalToolTokens
|
|
32925
|
+
serverInfo
|
|
32520
32926
|
};
|
|
32927
|
+
if (tokenData) {
|
|
32928
|
+
capabilities.toolTokenCounts = tokenData.toolTokenCounts;
|
|
32929
|
+
capabilities.totalToolTokens = tokenData.totalToolTokens;
|
|
32930
|
+
}
|
|
32931
|
+
return capabilities;
|
|
32521
32932
|
} finally {
|
|
32522
32933
|
try {
|
|
32523
32934
|
await client2.close();
|
|
@@ -32543,20 +32954,42 @@ class MCPVerifier {
|
|
|
32543
32954
|
const tokenCount = capabilities.toolTokenCounts?.get(tool.name) || 0;
|
|
32544
32955
|
const tokenDisplay = tokenCount > 0 ? ` (${tokenCount} tokens)` : "";
|
|
32545
32956
|
output.push(` \u2022 ${tool.name}${tokenDisplay}${tool.description ? ` - ${tool.description}` : ""}`);
|
|
32957
|
+
const parameters = this.formatToolParameters(tool);
|
|
32958
|
+
if (parameters.length > 0) {
|
|
32959
|
+
parameters.forEach((param) => output.push(param));
|
|
32960
|
+
}
|
|
32546
32961
|
});
|
|
32547
32962
|
}
|
|
32548
|
-
if (
|
|
32549
|
-
|
|
32550
|
-
|
|
32551
|
-
|
|
32552
|
-
|
|
32553
|
-
|
|
32963
|
+
if (capabilities.resources.length > 0) {
|
|
32964
|
+
const hasContents = capabilities.resources.some((r) => r.contents !== undefined);
|
|
32965
|
+
if (isDebugMode || hasContents) {
|
|
32966
|
+
output.push(` \n Resources (${capabilities.resources.length}):`);
|
|
32967
|
+
capabilities.resources.forEach((resource) => {
|
|
32968
|
+
const resourceName = resource.name || resource.uri.split("/").pop() || resource.uri;
|
|
32969
|
+
output.push(` \u2022 ${resourceName}${resource.description ? ` - ${resource.description}` : ""}`);
|
|
32970
|
+
if (resource.contents) {
|
|
32971
|
+
const contentStr = typeof resource.contents === "string" ? resource.contents : `<binary data: ${resource.contents.length} bytes>`;
|
|
32972
|
+
const preview = contentStr.length > 100 ? contentStr.slice(0, 100) + "..." : contentStr;
|
|
32973
|
+
output.push(` Content: ${preview}`);
|
|
32974
|
+
}
|
|
32975
|
+
});
|
|
32976
|
+
}
|
|
32554
32977
|
}
|
|
32555
|
-
if (
|
|
32556
|
-
|
|
32557
|
-
|
|
32558
|
-
output.push(` \
|
|
32559
|
-
|
|
32978
|
+
if (capabilities.prompts.length > 0) {
|
|
32979
|
+
const hasTemplates = capabilities.prompts.some((p) => p.template !== undefined);
|
|
32980
|
+
if (isDebugMode || hasTemplates) {
|
|
32981
|
+
output.push(` \n Prompts (${capabilities.prompts.length}):`);
|
|
32982
|
+
capabilities.prompts.forEach((prompt) => {
|
|
32983
|
+
output.push(` \u2022 ${prompt.name}${prompt.description ? ` - ${prompt.description}` : ""}`);
|
|
32984
|
+
if (prompt.template) {
|
|
32985
|
+
const preview = prompt.template.length > 100 ? prompt.template.slice(0, 100) + "..." : prompt.template;
|
|
32986
|
+
output.push(` Template: ${preview}`);
|
|
32987
|
+
}
|
|
32988
|
+
if (prompt.arguments && prompt.arguments.length > 0) {
|
|
32989
|
+
output.push(` Arguments: ${prompt.arguments.map((a) => `${a.name}${a.required ? "*" : ""}`).join(", ")}`);
|
|
32990
|
+
}
|
|
32991
|
+
});
|
|
32992
|
+
}
|
|
32560
32993
|
}
|
|
32561
32994
|
if (capabilities.tools.length === 0 && capabilities.resources.length === 0 && capabilities.prompts.length === 0) {
|
|
32562
32995
|
output.push(` \u26A0\uFE0F No tools, resources, or prompts available`);
|
|
@@ -32839,8 +33272,8 @@ async function applyCommand(args) {
|
|
|
32839
33272
|
logger.info("");
|
|
32840
33273
|
const verifySpinner = ora(`Verifying ${mcpParsed.servers.length} MCP server(s)...`).start();
|
|
32841
33274
|
try {
|
|
32842
|
-
const verifier = new MCPVerifier
|
|
32843
|
-
const verificationResults = await verifier.verifyServers(mcpParsed.servers, timeout);
|
|
33275
|
+
const verifier = new MCPVerifier;
|
|
33276
|
+
const verificationResults = await verifier.verifyServers(mcpParsed.servers, timeout ? { timeout } : undefined);
|
|
32844
33277
|
const successCount = verificationResults.filter((r) => r.status === "success").length;
|
|
32845
33278
|
const errorCount = verificationResults.filter((r) => r.status === "error").length;
|
|
32846
33279
|
const timeoutCount = verificationResults.filter((r) => r.status === "timeout").length;
|
|
@@ -33001,6 +33434,9 @@ async function verifyMcpCommand(args) {
|
|
|
33001
33434
|
const timeoutArg = timeoutIndex >= 0 && timeoutIndex + 1 < args.length ? args[timeoutIndex + 1] : null;
|
|
33002
33435
|
const parsedTimeout = timeoutArg ? parseInt(timeoutArg, 10) : NaN;
|
|
33003
33436
|
const timeout = timeoutArg && Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? parsedTimeout : undefined;
|
|
33437
|
+
const includeResources = args.includes("--include-resources");
|
|
33438
|
+
const includePrompts = args.includes("--include-prompts");
|
|
33439
|
+
const noTokens = args.includes("--no-tokens");
|
|
33004
33440
|
const hasMcpArgs = args.some((arg) => arg.startsWith("--mcp-"));
|
|
33005
33441
|
if (mcpName && hasAll) {
|
|
33006
33442
|
logger.error("Cannot use --mcp-name and --all together. Choose one option.");
|
|
@@ -33014,9 +33450,9 @@ async function verifyMcpCommand(args) {
|
|
|
33014
33450
|
logger.info("Usage: agentinit verify_mcp [options]");
|
|
33015
33451
|
logger.info("");
|
|
33016
33452
|
logger.info("Verify existing configurations:");
|
|
33017
|
-
logger.info(" --mcp-name <name>
|
|
33018
|
-
logger.info(" --all
|
|
33019
|
-
logger.info(` --timeout <ms>
|
|
33453
|
+
logger.info(" --mcp-name <name> Verify specific MCP server by name");
|
|
33454
|
+
logger.info(" --all Verify all configured MCP servers");
|
|
33455
|
+
logger.info(` --timeout <ms> Connection timeout in milliseconds (default: ${DEFAULT_CONNECTION_TIMEOUT_MS})`);
|
|
33020
33456
|
logger.info("");
|
|
33021
33457
|
logger.info("Verify direct MCP configuration:");
|
|
33022
33458
|
logger.info(" --mcp-stdio <name> <command> Verify STDIO MCP server");
|
|
@@ -33026,6 +33462,11 @@ async function verifyMcpCommand(args) {
|
|
|
33026
33462
|
logger.info(" --env <env_vars> Environment variables for server");
|
|
33027
33463
|
logger.info(" --auth <token> Authentication token for HTTP/SSE");
|
|
33028
33464
|
logger.info("");
|
|
33465
|
+
logger.info("Advanced options:");
|
|
33466
|
+
logger.info(" --include-resources Fetch actual resource contents (may be slow)");
|
|
33467
|
+
logger.info(" --include-prompts Fetch prompt templates");
|
|
33468
|
+
logger.info(" --no-tokens Skip token counting");
|
|
33469
|
+
logger.info("");
|
|
33029
33470
|
logger.info("Examples:");
|
|
33030
33471
|
logger.info(" # Verify existing configurations");
|
|
33031
33472
|
logger.info(" agentinit verify_mcp --all");
|
|
@@ -33034,6 +33475,9 @@ async function verifyMcpCommand(args) {
|
|
|
33034
33475
|
logger.info(" # Verify direct configuration");
|
|
33035
33476
|
logger.info(' agentinit verify_mcp --mcp-stdio everything "npx -y @modelcontextprotocol/server-everything"');
|
|
33036
33477
|
logger.info(' agentinit verify_mcp --mcp-http github "https://api.github.com/mcp" --auth "Bearer token"');
|
|
33478
|
+
logger.info("");
|
|
33479
|
+
logger.info(" # Fetch resource contents and prompt templates");
|
|
33480
|
+
logger.info(" agentinit verify_mcp --all --include-resources --include-prompts");
|
|
33037
33481
|
return;
|
|
33038
33482
|
}
|
|
33039
33483
|
let spinner;
|
|
@@ -33110,7 +33554,16 @@ async function verifyMcpCommand(args) {
|
|
|
33110
33554
|
}
|
|
33111
33555
|
}
|
|
33112
33556
|
const verifier = new MCPVerifier(timeout);
|
|
33113
|
-
const
|
|
33557
|
+
const options2 = {};
|
|
33558
|
+
if (timeout)
|
|
33559
|
+
options2.timeout = timeout;
|
|
33560
|
+
if (includeResources)
|
|
33561
|
+
options2.includeResourceContents = true;
|
|
33562
|
+
if (includePrompts)
|
|
33563
|
+
options2.includePromptDetails = true;
|
|
33564
|
+
if (noTokens)
|
|
33565
|
+
options2.includeTokenCounts = false;
|
|
33566
|
+
const results = await verifier.verifyServers(serversToVerify, Object.keys(options2).length > 0 ? options2 : undefined);
|
|
33114
33567
|
const successCount = results.filter((r) => r.status === "success").length;
|
|
33115
33568
|
const errorCount = results.filter((r) => r.status === "error").length;
|
|
33116
33569
|
const timeoutCount = results.filter((r) => r.status === "timeout").length;
|