@cdxgen/cdxgen 13.0.1 → 13.2.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/README.md +30 -16
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +141 -7
- package/bin/evinse.js +3 -1
- package/data/component-tags.json +8 -1
- package/data/crypto-oid.json +88 -12
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1167 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +32 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +268 -49
- package/lib/cli/jsBom.js +91 -5
- package/lib/cli/jvmBom.js +1515 -1314
- package/lib/cli/managedBom.js +249 -28
- package/lib/cli/nativeBom.js +986 -10
- package/lib/core/activity.js +120 -2
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +108 -2
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +250 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +498 -17
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +188 -2
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +172 -36
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +944 -19
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +141 -13
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +17 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +508 -22
- package/lib/inventory/envcontext.js +215 -24
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +131 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +594 -0
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +250 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +284 -3
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +26 -26
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +10 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +43 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +53 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +21 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +34 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +12 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +43 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +43 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +35 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { build } from "@cdxgen/cdx-purl";
|
|
6
|
+
import { parse as tomlParse } from "smol-toml";
|
|
7
|
+
import { parse as loadYaml } from "yaml";
|
|
8
|
+
|
|
9
|
+
import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
|
|
10
|
+
import { safeExistsSync } from "../core/fs.js";
|
|
11
|
+
import { isMac, isWin } from "../core/paths.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The purl type used for installed agentic CLI tools. These tools ship as
|
|
15
|
+
* self-contained executables rather than as a resolvable source package, so a
|
|
16
|
+
* `pkg:generic/...` identifier is the honest representation.
|
|
17
|
+
*/
|
|
18
|
+
export const AGENTIC_TOOL_PURL_TYPE = "generic";
|
|
19
|
+
|
|
20
|
+
// A stable namespace keeps these generic purls from colliding with unrelated
|
|
21
|
+
// `pkg:generic/<name>` components when BOMs are merged.
|
|
22
|
+
const AGENTIC_TOOL_PURL_NAMESPACE = "cdxgen-agentic";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a namespaced `pkg:generic` purl for an agentic tool, returning
|
|
26
|
+
* undefined when the coordinates cannot form a valid purl rather than throwing.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} name Tool name
|
|
29
|
+
* @param {string} [version] Tool version
|
|
30
|
+
* @returns {string|undefined} Canonical purl string or undefined
|
|
31
|
+
*/
|
|
32
|
+
function buildGenericPurl(name, version) {
|
|
33
|
+
try {
|
|
34
|
+
return build({
|
|
35
|
+
type: AGENTIC_TOOL_PURL_TYPE,
|
|
36
|
+
namespace: AGENTIC_TOOL_PURL_NAMESPACE,
|
|
37
|
+
name,
|
|
38
|
+
version,
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err?.code?.startsWith("E_")) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Rewrite an absolute path under the user's home directory to a `~`-relative
|
|
50
|
+
* form so the emitted BOM does not carry the local username.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} filePath Absolute path
|
|
53
|
+
* @returns {string} Home-relative path when applicable, else the input
|
|
54
|
+
*/
|
|
55
|
+
export function redactHomePath(filePath) {
|
|
56
|
+
const home = homedir();
|
|
57
|
+
const value = String(filePath || "");
|
|
58
|
+
if (!home || !value.startsWith(home)) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
// Require a path boundary so a sibling home such as /Users/alice-backup is
|
|
62
|
+
// not rewritten as if it sat under /Users/alice.
|
|
63
|
+
const remainder = value.slice(home.length);
|
|
64
|
+
if (remainder && !/^[/\\]/u.test(remainder)) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
const rest = remainder.replace(/^[/\\]/u, "");
|
|
68
|
+
return rest ? `~/${rest.replaceAll("\\", "/")}` : "~";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Directory or file names inside a tool home that must never be read for their
|
|
72
|
+
// values. They hold credentials, transcripts, or telemetry. The scanner may
|
|
73
|
+
// count entries under some of these, but it does not open their contents.
|
|
74
|
+
const SENSITIVE_ENTRY_NAMES = new Set([
|
|
75
|
+
"credentials.json",
|
|
76
|
+
"credentials",
|
|
77
|
+
"token.json",
|
|
78
|
+
"tokens",
|
|
79
|
+
"history.jsonl",
|
|
80
|
+
"logs",
|
|
81
|
+
"log",
|
|
82
|
+
"sessions",
|
|
83
|
+
"session-index",
|
|
84
|
+
"cache",
|
|
85
|
+
"crash",
|
|
86
|
+
"telemetry",
|
|
87
|
+
"certs",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Describe where each supported agentic CLI tool stores its per-user state.
|
|
92
|
+
*
|
|
93
|
+
* Each entry lists the tool key, a display name, and the candidate home
|
|
94
|
+
* directories per platform (mirroring how `getIdeExtensionDirs` models IDE
|
|
95
|
+
* extension locations). Only the current platform's directories plus
|
|
96
|
+
* always-present dot-directories are included.
|
|
97
|
+
*
|
|
98
|
+
* @returns {Array<{tool: string, name: string, dirs: string[], supportDirs?: string[]}>}
|
|
99
|
+
* One entry per known agentic CLI tool.
|
|
100
|
+
*/
|
|
101
|
+
export function getAgenticToolDirs() {
|
|
102
|
+
const home = homedir();
|
|
103
|
+
const appData =
|
|
104
|
+
readEnvironmentVariable("APPDATA") || join(home, "AppData", "Roaming");
|
|
105
|
+
const xdgConfigHome =
|
|
106
|
+
readEnvironmentVariable("XDG_CONFIG_HOME") || join(home, ".config");
|
|
107
|
+
const xdgDataHome =
|
|
108
|
+
readEnvironmentVariable("XDG_DATA_HOME") || join(home, ".local", "share");
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
tool: "kiro-cli",
|
|
113
|
+
name: "Kiro CLI",
|
|
114
|
+
dirs: [join(home, ".kiro")],
|
|
115
|
+
supportDirs: isWin
|
|
116
|
+
? [join(appData, "kiro-cli")]
|
|
117
|
+
: isMac
|
|
118
|
+
? [join(home, "Library", "Application Support", "kiro-cli")]
|
|
119
|
+
: [join(xdgDataHome, "kiro-cli")],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
tool: "zcode",
|
|
123
|
+
name: "zcode",
|
|
124
|
+
dirs: [join(home, ".zcode")],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
tool: "opencode",
|
|
128
|
+
name: "opencode",
|
|
129
|
+
dirs: [
|
|
130
|
+
join(xdgConfigHome, "opencode"),
|
|
131
|
+
join(home, ".opencode"),
|
|
132
|
+
join(xdgDataHome, "opencode"),
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
tool: "claude-code",
|
|
137
|
+
name: "Claude Code",
|
|
138
|
+
dirs: [join(home, ".claude")],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
tool: "codex",
|
|
142
|
+
name: "OpenAI Codex CLI",
|
|
143
|
+
dirs: [join(home, ".codex")],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
tool: "gemini-cli",
|
|
147
|
+
name: "Gemini CLI",
|
|
148
|
+
dirs: [join(home, ".gemini")],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
tool: "amazon-q",
|
|
152
|
+
name: "Amazon Q Developer CLI",
|
|
153
|
+
dirs: [join(home, ".aws", "amazonq")],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
tool: "aider",
|
|
157
|
+
name: "Aider",
|
|
158
|
+
dirs: [join(home, ".aider")],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
tool: "cursor",
|
|
162
|
+
name: "Cursor",
|
|
163
|
+
dirs: [join(home, ".cursor")],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
tool: "windsurf",
|
|
167
|
+
name: "Windsurf",
|
|
168
|
+
dirs: [join(home, ".codeium", "windsurf")],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
tool: "goose",
|
|
172
|
+
name: "Goose",
|
|
173
|
+
dirs: [
|
|
174
|
+
join(xdgConfigHome, "goose"),
|
|
175
|
+
join(home, ".config", "goose"),
|
|
176
|
+
...(isWin ? [join(appData, "goose")] : []),
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
tool: "amp",
|
|
181
|
+
name: "Amp",
|
|
182
|
+
dirs: [
|
|
183
|
+
join(xdgConfigHome, "amp"),
|
|
184
|
+
join(home, ".config", "amp"),
|
|
185
|
+
...(isWin ? [join(appData, "amp")] : []),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
tool: "copilot-cli",
|
|
190
|
+
name: "GitHub Copilot CLI",
|
|
191
|
+
dirs: [join(home, ".copilot-cli"), join(home, ".copilot")],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
tool: "crush",
|
|
195
|
+
name: "Crush",
|
|
196
|
+
dirs: [
|
|
197
|
+
join(xdgConfigHome, "crush"),
|
|
198
|
+
join(home, ".config", "crush"),
|
|
199
|
+
...(isWin ? [join(appData, "crush")] : []),
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Discover which agentic CLI tools are installed for the current user by
|
|
207
|
+
* checking their known home directories.
|
|
208
|
+
*
|
|
209
|
+
* @returns {Array<{tool: string, name: string, dir: string, supportDir?: string}>}
|
|
210
|
+
* One entry per discovered tool, using the first existing directory.
|
|
211
|
+
*/
|
|
212
|
+
export function discoverAgenticTools() {
|
|
213
|
+
const found = [];
|
|
214
|
+
for (const entry of getAgenticToolDirs()) {
|
|
215
|
+
const dir = entry.dirs.find((candidate) => safeExistsSync(candidate));
|
|
216
|
+
if (!dir) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const supportDir = (entry.supportDirs || []).find((candidate) =>
|
|
220
|
+
safeExistsSync(candidate),
|
|
221
|
+
);
|
|
222
|
+
found.push({ tool: entry.tool, name: entry.name, dir, supportDir });
|
|
223
|
+
}
|
|
224
|
+
return found;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseJsonFile(filePath) {
|
|
228
|
+
try {
|
|
229
|
+
return parseJsonLikeText(readFileSync(filePath, "utf-8"));
|
|
230
|
+
} catch {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Strip `//` line comments, block comments, and trailing commas from JSONC
|
|
237
|
+
* text.
|
|
238
|
+
*
|
|
239
|
+
* The scan is character-wise and string-aware rather than regex-based: a
|
|
240
|
+
* regex cannot tell a comment from the same characters inside a string value,
|
|
241
|
+
* so `{"url": "https://x", "dir": "a//b"}` would be corrupted by one.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} raw Raw JSONC text
|
|
244
|
+
* @returns {string} Text that is valid JSON
|
|
245
|
+
*/
|
|
246
|
+
function stripJsonComments(raw) {
|
|
247
|
+
const text = String(raw);
|
|
248
|
+
let out = "";
|
|
249
|
+
let index = 0;
|
|
250
|
+
let inString = false;
|
|
251
|
+
while (index < text.length) {
|
|
252
|
+
const char = text[index];
|
|
253
|
+
const next = text[index + 1];
|
|
254
|
+
if (inString) {
|
|
255
|
+
out += char;
|
|
256
|
+
if (char === "\\") {
|
|
257
|
+
// Copy the escaped character verbatim so an escaped quote does not end
|
|
258
|
+
// the string.
|
|
259
|
+
out += next ?? "";
|
|
260
|
+
index += 2;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (char === '"') {
|
|
264
|
+
inString = false;
|
|
265
|
+
}
|
|
266
|
+
index += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (char === '"') {
|
|
270
|
+
inString = true;
|
|
271
|
+
out += char;
|
|
272
|
+
index += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (char === "/" && next === "/") {
|
|
276
|
+
while (index < text.length && text[index] !== "\n") {
|
|
277
|
+
index += 1;
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (char === "/" && next === "*") {
|
|
282
|
+
index += 2;
|
|
283
|
+
while (
|
|
284
|
+
index < text.length &&
|
|
285
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
286
|
+
) {
|
|
287
|
+
index += 1;
|
|
288
|
+
}
|
|
289
|
+
index += 2;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
out += char;
|
|
293
|
+
index += 1;
|
|
294
|
+
}
|
|
295
|
+
// Trailing commas are only ambiguous inside strings, which are gone by now.
|
|
296
|
+
return out.replace(/,(\s*[}\]])/gu, "$1");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Parse JSON or JSONC text, tolerating `//` and block comments and trailing
|
|
301
|
+
* commas so opencode.jsonc and similar configs are read rather than silently
|
|
302
|
+
* dropped. String contents are preserved.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} raw Raw file text
|
|
305
|
+
* @returns {*} Parsed value
|
|
306
|
+
*/
|
|
307
|
+
function parseJsonLikeText(raw) {
|
|
308
|
+
try {
|
|
309
|
+
return JSON.parse(raw);
|
|
310
|
+
} catch {
|
|
311
|
+
return JSON.parse(stripJsonComments(raw));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function countDirEntries(dirPath, filterFn) {
|
|
316
|
+
try {
|
|
317
|
+
return readdirSync(dirPath).filter((name) => {
|
|
318
|
+
if (SENSITIVE_ENTRY_NAMES.has(name.toLowerCase())) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return filterFn ? filterFn(name) : true;
|
|
322
|
+
}).length;
|
|
323
|
+
} catch {
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read a version string from a tool's own metadata files. No tool binary is
|
|
330
|
+
* executed; only declarative files are read, so scanning stays side-effect
|
|
331
|
+
* free. Returns undefined when no reliable version signal is available.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} tool Tool key
|
|
334
|
+
* @param {string} dir Discovered tool home directory
|
|
335
|
+
* @param {string} [supportDir] Optional platform support directory
|
|
336
|
+
* @returns {string|undefined} Version string when found
|
|
337
|
+
*/
|
|
338
|
+
function readToolVersion(tool, dir, supportDir) {
|
|
339
|
+
const candidates = [];
|
|
340
|
+
if (tool === "opencode") {
|
|
341
|
+
// ~/.opencode/package.json pins the installed opencode plugin runtime,
|
|
342
|
+
// which is a stable declarative proxy for the installed version.
|
|
343
|
+
const pkg = parseJsonFile(join(dir, "package.json"));
|
|
344
|
+
const pluginVersion = pkg?.dependencies?.["@opencode-ai/plugin"];
|
|
345
|
+
if (typeof pluginVersion === "string") {
|
|
346
|
+
const normalized = pluginVersion.replace(/^[^0-9]*/, "");
|
|
347
|
+
if (normalized) {
|
|
348
|
+
return normalized;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (typeof pkg?.version === "string" && pkg.version.trim()) {
|
|
352
|
+
return pkg.version.trim();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
candidates.push(
|
|
356
|
+
join(dir, "version.json"),
|
|
357
|
+
join(dir, "version"),
|
|
358
|
+
supportDir ? join(supportDir, "version.json") : undefined,
|
|
359
|
+
);
|
|
360
|
+
for (const candidate of candidates.filter(Boolean)) {
|
|
361
|
+
if (!safeExistsSync(candidate)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (candidate.endsWith(".json")) {
|
|
365
|
+
const parsed = parseJsonFile(candidate);
|
|
366
|
+
const version =
|
|
367
|
+
parsed?.version || parsed?.cliVersion || parsed?.appVersion;
|
|
368
|
+
if (typeof version === "string" && version.trim()) {
|
|
369
|
+
return version.trim();
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
try {
|
|
373
|
+
const raw = readFileSync(candidate, "utf-8").trim();
|
|
374
|
+
if (/^v?\d+\.\d+/.test(raw)) {
|
|
375
|
+
return raw.replace(/^v/, "");
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// ignore unreadable version file
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function countKiroSignals(dir, supportDir) {
|
|
386
|
+
const signals = {};
|
|
387
|
+
const agentsDir = join(dir, "agents");
|
|
388
|
+
if (safeExistsSync(agentsDir)) {
|
|
389
|
+
signals.agentCount = countDirEntries(
|
|
390
|
+
agentsDir,
|
|
391
|
+
(name) => name.endsWith(".json") && !name.endsWith(".example"),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (safeExistsSync(join(dir, "sessions"))) {
|
|
395
|
+
signals.hasSessions = true;
|
|
396
|
+
}
|
|
397
|
+
const knowledgeDir = supportDir
|
|
398
|
+
? join(supportDir, "knowledge_bases")
|
|
399
|
+
: undefined;
|
|
400
|
+
if (knowledgeDir && safeExistsSync(knowledgeDir)) {
|
|
401
|
+
signals.knowledgeBaseCount = countDirEntries(knowledgeDir);
|
|
402
|
+
}
|
|
403
|
+
const settings = parseJsonFile(join(dir, "settings", "cli.json"));
|
|
404
|
+
if (settings && typeof settings["chat.defaultAgent"] === "string") {
|
|
405
|
+
signals.defaultAgent = settings["chat.defaultAgent"];
|
|
406
|
+
}
|
|
407
|
+
return signals;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function countZcodeSignals(dir) {
|
|
411
|
+
const signals = {};
|
|
412
|
+
const cliConfig = parseJsonFile(join(dir, "cli", "config.json"));
|
|
413
|
+
const enabled = cliConfig?.plugins?.enabledPlugins;
|
|
414
|
+
if (enabled && typeof enabled === "object") {
|
|
415
|
+
const names = Object.keys(enabled);
|
|
416
|
+
signals.pluginCount = names.length;
|
|
417
|
+
signals.enabledPluginCount = names.filter(
|
|
418
|
+
(name) => enabled[name] === true,
|
|
419
|
+
).length;
|
|
420
|
+
}
|
|
421
|
+
const cacheDir = join(dir, "cli", "plugins", "cache");
|
|
422
|
+
if (safeExistsSync(cacheDir)) {
|
|
423
|
+
signals.cachedPluginRepoCount = countDirEntries(cacheDir);
|
|
424
|
+
}
|
|
425
|
+
return signals;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function countOpencodeSignals(dir) {
|
|
429
|
+
const signals = {};
|
|
430
|
+
const config =
|
|
431
|
+
parseJsonFile(join(dir, "opencode.json")) ||
|
|
432
|
+
parseJsonFile(join(dir, "opencode.jsonc"));
|
|
433
|
+
if (config?.provider && typeof config.provider === "object") {
|
|
434
|
+
signals.providerCount = Object.keys(config.provider).length;
|
|
435
|
+
}
|
|
436
|
+
if (typeof config?.model === "string") {
|
|
437
|
+
signals.hasDefaultModel = true;
|
|
438
|
+
}
|
|
439
|
+
const pluginsDir = join(dir, "plugins");
|
|
440
|
+
if (safeExistsSync(pluginsDir)) {
|
|
441
|
+
signals.pluginCount = countDirEntries(pluginsDir, (name) =>
|
|
442
|
+
/\.(ts|js|mjs|cjs)$/.test(name),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return signals;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function countClaudeSignals(dir) {
|
|
449
|
+
const signals = {};
|
|
450
|
+
const agentsDir = join(dir, "agents");
|
|
451
|
+
if (safeExistsSync(agentsDir)) {
|
|
452
|
+
signals.agentCount = countDirEntries(agentsDir, (name) =>
|
|
453
|
+
name.endsWith(".md"),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const commandsDir = join(dir, "commands");
|
|
457
|
+
if (safeExistsSync(commandsDir)) {
|
|
458
|
+
signals.commandCount = countDirEntries(commandsDir, (name) =>
|
|
459
|
+
name.endsWith(".md"),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (safeExistsSync(join(dir, "plugins"))) {
|
|
463
|
+
signals.pluginCount = countDirEntries(join(dir, "plugins"));
|
|
464
|
+
}
|
|
465
|
+
const skillsDir = join(dir, "skills");
|
|
466
|
+
if (safeExistsSync(skillsDir)) {
|
|
467
|
+
signals.skillCount = countDirEntries(skillsDir);
|
|
468
|
+
}
|
|
469
|
+
return signals;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function countCodexSignals(dir) {
|
|
473
|
+
const signals = {};
|
|
474
|
+
const configPath = join(dir, "config.toml");
|
|
475
|
+
if (!safeExistsSync(configPath)) {
|
|
476
|
+
return signals;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const config = tomlParse(readFileSync(configPath, "utf-8"));
|
|
480
|
+
if (config?.mcp_servers && typeof config.mcp_servers === "object") {
|
|
481
|
+
signals.mcpServerCount = Object.keys(config.mcp_servers).length;
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
// unreadable or invalid TOML
|
|
485
|
+
}
|
|
486
|
+
return signals;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function countCursorSignals(dir) {
|
|
490
|
+
const signals = {};
|
|
491
|
+
const mcpConfig = parseJsonFile(join(dir, "mcp.json"));
|
|
492
|
+
if (mcpConfig?.mcpServers && typeof mcpConfig.mcpServers === "object") {
|
|
493
|
+
signals.mcpServerCount = Object.keys(mcpConfig.mcpServers).length;
|
|
494
|
+
}
|
|
495
|
+
const agentsDir = join(dir, "agents");
|
|
496
|
+
if (safeExistsSync(agentsDir)) {
|
|
497
|
+
signals.agentCount = countDirEntries(agentsDir, (name) =>
|
|
498
|
+
name.endsWith(".md"),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return signals;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function countWindsurfSignals(dir) {
|
|
505
|
+
const signals = {};
|
|
506
|
+
const mcpConfig = parseJsonFile(join(dir, "mcp_config.json"));
|
|
507
|
+
if (mcpConfig?.mcpServers && typeof mcpConfig.mcpServers === "object") {
|
|
508
|
+
signals.mcpServerCount = Object.keys(mcpConfig.mcpServers).length;
|
|
509
|
+
}
|
|
510
|
+
return signals;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function countGooseSignals(dir) {
|
|
514
|
+
const signals = {};
|
|
515
|
+
// Goose keeps its resolved extension set in config.yaml.
|
|
516
|
+
try {
|
|
517
|
+
const config = loadYaml(readFileSync(join(dir, "config.yaml"), "utf-8"));
|
|
518
|
+
if (Array.isArray(config?.extensions)) {
|
|
519
|
+
signals.extensionCount = config.extensions.length;
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
// missing or unreadable config
|
|
523
|
+
}
|
|
524
|
+
return signals;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function countCrushSignals(dir) {
|
|
528
|
+
const signals = {};
|
|
529
|
+
const config = parseJsonFile(join(dir, "crush.json"));
|
|
530
|
+
if (config?.mcp && typeof config.mcp === "object") {
|
|
531
|
+
signals.mcpServerCount = Object.keys(config.mcp).length;
|
|
532
|
+
}
|
|
533
|
+
return signals;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function countGeminiSignals(dir) {
|
|
537
|
+
const signals = {};
|
|
538
|
+
const settings = parseJsonFile(join(dir, "settings.json"));
|
|
539
|
+
if (settings?.mcpServers && typeof settings.mcpServers === "object") {
|
|
540
|
+
signals.mcpServerCount = Object.keys(settings.mcpServers).length;
|
|
541
|
+
}
|
|
542
|
+
const commandsDir = join(dir, "commands");
|
|
543
|
+
if (safeExistsSync(commandsDir)) {
|
|
544
|
+
signals.commandCount = countDirEntries(commandsDir);
|
|
545
|
+
}
|
|
546
|
+
return signals;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Compute safe, redacted signal counts for a discovered tool. Only structural
|
|
551
|
+
* counts and small enumerated values are returned; no credential, transcript,
|
|
552
|
+
* or telemetry content is read.
|
|
553
|
+
*
|
|
554
|
+
* @param {string} tool Tool key
|
|
555
|
+
* @param {string} dir Discovered tool home directory
|
|
556
|
+
* @param {string} [supportDir] Optional platform support directory
|
|
557
|
+
* @returns {Object} Map of signal name to count/boolean/small-string value
|
|
558
|
+
*/
|
|
559
|
+
export function collectToolSignals(tool, dir, supportDir) {
|
|
560
|
+
switch (tool) {
|
|
561
|
+
case "kiro-cli":
|
|
562
|
+
return countKiroSignals(dir, supportDir);
|
|
563
|
+
case "zcode":
|
|
564
|
+
return countZcodeSignals(dir);
|
|
565
|
+
case "opencode":
|
|
566
|
+
return countOpencodeSignals(dir);
|
|
567
|
+
case "claude-code":
|
|
568
|
+
return countClaudeSignals(dir);
|
|
569
|
+
case "codex":
|
|
570
|
+
return countCodexSignals(dir);
|
|
571
|
+
case "gemini-cli":
|
|
572
|
+
return countGeminiSignals(dir);
|
|
573
|
+
case "cursor":
|
|
574
|
+
return countCursorSignals(dir);
|
|
575
|
+
case "windsurf":
|
|
576
|
+
return countWindsurfSignals(dir);
|
|
577
|
+
case "goose":
|
|
578
|
+
return countGooseSignals(dir);
|
|
579
|
+
case "crush":
|
|
580
|
+
return countCrushSignals(dir);
|
|
581
|
+
default: {
|
|
582
|
+
// Generic fallback: count top-level, non-sensitive config entries.
|
|
583
|
+
const signals = {};
|
|
584
|
+
const mcpConfig = parseJsonFile(join(dir, ".mcp.json"));
|
|
585
|
+
if (mcpConfig?.mcpServers) {
|
|
586
|
+
signals.mcpServerCount = Object.keys(mcpConfig.mcpServers).length;
|
|
587
|
+
}
|
|
588
|
+
return signals;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Build a CycloneDX application component for one discovered agentic tool.
|
|
595
|
+
*
|
|
596
|
+
* @param {{tool: string, name: string, dir: string, supportDir?: string}} entry
|
|
597
|
+
* Discovered tool entry
|
|
598
|
+
* @returns {Object} CycloneDX component
|
|
599
|
+
*/
|
|
600
|
+
export function componentForAgenticTool(entry) {
|
|
601
|
+
const { tool, name, dir, supportDir } = entry;
|
|
602
|
+
const version = readToolVersion(tool, dir, supportDir);
|
|
603
|
+
const purl = buildGenericPurl(tool, version);
|
|
604
|
+
const properties = [
|
|
605
|
+
{ name: "internal:SrcFile", value: redactHomePath(dir) },
|
|
606
|
+
{ name: "cdx:agentic:tool", value: tool },
|
|
607
|
+
{ name: "cdx:agentic:home", value: redactHomePath(dir) },
|
|
608
|
+
{ name: "cdx:agentic:inventorySource", value: "host-scan" },
|
|
609
|
+
];
|
|
610
|
+
if (supportDir) {
|
|
611
|
+
properties.push({
|
|
612
|
+
name: "cdx:agentic:supportDir",
|
|
613
|
+
value: redactHomePath(supportDir),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const signals = collectToolSignals(tool, dir, supportDir);
|
|
617
|
+
for (const [key, value] of Object.entries(signals)) {
|
|
618
|
+
if (value === undefined || value === null || value === "") {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
properties.push({
|
|
622
|
+
name: `cdx:agentic:${key}`,
|
|
623
|
+
value: String(value),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const component = {
|
|
627
|
+
"bom-ref": purl || `urn:agentic:${tool}`,
|
|
628
|
+
type: "application",
|
|
629
|
+
// Use the human-friendly display name and a stable group so BOM viewers
|
|
630
|
+
// read well; the machine key remains on cdx:agentic:tool.
|
|
631
|
+
group: AGENTIC_TOOL_PURL_NAMESPACE,
|
|
632
|
+
name,
|
|
633
|
+
properties,
|
|
634
|
+
tags: ["ai", "agentic-cli", tool],
|
|
635
|
+
};
|
|
636
|
+
if (version) {
|
|
637
|
+
component.version = version;
|
|
638
|
+
}
|
|
639
|
+
if (purl) {
|
|
640
|
+
component.purl = purl;
|
|
641
|
+
}
|
|
642
|
+
component.description = `${name} agentic CLI tool discovered on the host`;
|
|
643
|
+
return component;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function childBomRef(parentRef, kind, id) {
|
|
647
|
+
const safeId = String(id || "unknown").replaceAll(/[^a-zA-Z0-9._-]+/gu, "-");
|
|
648
|
+
return `${parentRef}:${kind}:${safeId}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Parse Kiro agent definitions (`~/.kiro/agents/*.json`) into components.
|
|
653
|
+
*
|
|
654
|
+
* Each agent contributes its name, model, and redacted counts of MCP servers,
|
|
655
|
+
* tools, and allowed tools. Prompts and resource values are never copied.
|
|
656
|
+
*
|
|
657
|
+
* @param {string} dir Kiro home directory
|
|
658
|
+
* @param {string} parentRef Parent tool bom-ref
|
|
659
|
+
* @returns {Object[]} Child components
|
|
660
|
+
*/
|
|
661
|
+
function parseKiroAgents(dir, parentRef) {
|
|
662
|
+
const agentsDir = join(dir, "agents");
|
|
663
|
+
if (!safeExistsSync(agentsDir)) {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
const components = [];
|
|
667
|
+
let files = [];
|
|
668
|
+
try {
|
|
669
|
+
files = readdirSync(agentsDir);
|
|
670
|
+
} catch {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
for (const file of files) {
|
|
674
|
+
if (!file.endsWith(".json") || file.endsWith(".example")) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const agent = parseJsonFile(join(agentsDir, file));
|
|
678
|
+
if (!agent?.name) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const properties = [
|
|
682
|
+
{
|
|
683
|
+
name: "internal:SrcFile",
|
|
684
|
+
value: redactHomePath(join(agentsDir, file)),
|
|
685
|
+
},
|
|
686
|
+
{ name: "cdx:agentic:tool", value: "kiro-cli" },
|
|
687
|
+
{ name: "cdx:agentic:kind", value: "agent-definition" },
|
|
688
|
+
];
|
|
689
|
+
if (typeof agent.model === "string") {
|
|
690
|
+
properties.push({ name: "cdx:agentic:model", value: agent.model });
|
|
691
|
+
}
|
|
692
|
+
if (agent.mcpServers && typeof agent.mcpServers === "object") {
|
|
693
|
+
properties.push({
|
|
694
|
+
name: "cdx:agentic:mcpServerCount",
|
|
695
|
+
value: String(Object.keys(agent.mcpServers).length),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
if (Array.isArray(agent.tools)) {
|
|
699
|
+
properties.push({
|
|
700
|
+
name: "cdx:agentic:toolCount",
|
|
701
|
+
value: String(agent.tools.length),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (Array.isArray(agent.allowedTools)) {
|
|
705
|
+
properties.push({
|
|
706
|
+
name: "cdx:agentic:allowedToolCount",
|
|
707
|
+
value: String(agent.allowedTools.length),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
components.push({
|
|
711
|
+
"bom-ref": childBomRef(parentRef, "agent", agent.name),
|
|
712
|
+
type: "application",
|
|
713
|
+
name: String(agent.name),
|
|
714
|
+
properties,
|
|
715
|
+
tags: ["ai", "agent-definition", "kiro-cli"],
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return components;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Parse cached zcode plugins
|
|
723
|
+
* (`~/.zcode/cli/plugins/cache/<repo>/<plugin>/<version>/.zcode-plugin/plugin.json`)
|
|
724
|
+
* into components.
|
|
725
|
+
*
|
|
726
|
+
* @param {string} dir zcode home directory
|
|
727
|
+
* @param {string} parentRef Parent tool bom-ref
|
|
728
|
+
* @returns {Object[]} Child components
|
|
729
|
+
*/
|
|
730
|
+
function parseZcodePlugins(dir, parentRef) {
|
|
731
|
+
const cacheDir = join(dir, "cli", "plugins", "cache");
|
|
732
|
+
if (!safeExistsSync(cacheDir)) {
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
const components = [];
|
|
736
|
+
const seen = new Set();
|
|
737
|
+
const walk = (current, depth) => {
|
|
738
|
+
if (depth > 5) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
let entries = [];
|
|
742
|
+
try {
|
|
743
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
744
|
+
} catch {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
for (const dirent of entries) {
|
|
748
|
+
const full = join(current, dirent.name);
|
|
749
|
+
if (dirent.isDirectory()) {
|
|
750
|
+
walk(full, depth + 1);
|
|
751
|
+
} else if (dirent.name === "plugin.json") {
|
|
752
|
+
const plugin = parseJsonFile(full);
|
|
753
|
+
if (!plugin?.name) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const key = `${plugin.name}@${plugin.version || "0"}`;
|
|
757
|
+
if (seen.has(key)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
seen.add(key);
|
|
761
|
+
const purl = buildGenericPurl(
|
|
762
|
+
`zcode-plugin-${plugin.name}`,
|
|
763
|
+
typeof plugin.version === "string" ? plugin.version : undefined,
|
|
764
|
+
);
|
|
765
|
+
const properties = [
|
|
766
|
+
{ name: "internal:SrcFile", value: redactHomePath(full) },
|
|
767
|
+
{ name: "cdx:agentic:tool", value: "zcode" },
|
|
768
|
+
{ name: "cdx:agentic:kind", value: "plugin" },
|
|
769
|
+
];
|
|
770
|
+
if (typeof plugin.license === "string") {
|
|
771
|
+
properties.push({
|
|
772
|
+
name: "cdx:agentic:license",
|
|
773
|
+
value: plugin.license,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (plugin.mcpServers && typeof plugin.mcpServers === "object") {
|
|
777
|
+
properties.push({
|
|
778
|
+
name: "cdx:agentic:mcpServerCount",
|
|
779
|
+
value: String(Object.keys(plugin.mcpServers).length),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (plugin.skills) {
|
|
783
|
+
properties.push({ name: "cdx:agentic:hasSkills", value: "true" });
|
|
784
|
+
}
|
|
785
|
+
const component = {
|
|
786
|
+
"bom-ref": childBomRef(
|
|
787
|
+
parentRef,
|
|
788
|
+
"plugin",
|
|
789
|
+
`${plugin.name}-${plugin.version || ""}`,
|
|
790
|
+
),
|
|
791
|
+
type: "application",
|
|
792
|
+
name: String(plugin.name),
|
|
793
|
+
properties,
|
|
794
|
+
tags: ["ai", "agentic-plugin", "zcode"],
|
|
795
|
+
};
|
|
796
|
+
if (typeof plugin.version === "string") {
|
|
797
|
+
component.version = plugin.version;
|
|
798
|
+
}
|
|
799
|
+
if (purl) {
|
|
800
|
+
component.purl = purl;
|
|
801
|
+
}
|
|
802
|
+
components.push(component);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
walk(cacheDir, 0);
|
|
807
|
+
return components;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Parse the opencode configuration (`opencode.json`) into provider service
|
|
812
|
+
* references. Only provider identifiers and counts are recorded.
|
|
813
|
+
*
|
|
814
|
+
* @param {string} dir opencode home directory
|
|
815
|
+
* @param {string} parentRef Parent tool bom-ref
|
|
816
|
+
* @returns {Object[]} Child components
|
|
817
|
+
*/
|
|
818
|
+
/**
|
|
819
|
+
* Parse the opencode configuration (`opencode.json` or `opencode.jsonc`) into
|
|
820
|
+
* provider service references. Providers are emitted as CycloneDX services to
|
|
821
|
+
* match how inference providers are modeled elsewhere in the AI inventory.
|
|
822
|
+
* Only provider identifiers are recorded.
|
|
823
|
+
*
|
|
824
|
+
* @param {string} dir opencode home directory
|
|
825
|
+
* @param {string} parentRef Parent tool bom-ref
|
|
826
|
+
* @returns {Object[]} Child provider services
|
|
827
|
+
*/
|
|
828
|
+
function parseOpencodeProviders(dir, parentRef) {
|
|
829
|
+
const jsonPath = join(dir, "opencode.json");
|
|
830
|
+
const jsoncPath = join(dir, "opencode.jsonc");
|
|
831
|
+
const usedPath = safeExistsSync(jsonPath) ? jsonPath : jsoncPath;
|
|
832
|
+
const config = parseJsonFile(jsonPath) || parseJsonFile(jsoncPath);
|
|
833
|
+
if (!config?.provider || typeof config.provider !== "object") {
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
const services = [];
|
|
837
|
+
for (const providerName of Object.keys(config.provider)) {
|
|
838
|
+
services.push({
|
|
839
|
+
"bom-ref": childBomRef(parentRef, "provider", providerName),
|
|
840
|
+
group: providerName,
|
|
841
|
+
name: providerName,
|
|
842
|
+
properties: [
|
|
843
|
+
{ name: "internal:SrcFile", value: redactHomePath(usedPath) },
|
|
844
|
+
{ name: "cdx:agentic:tool", value: "opencode" },
|
|
845
|
+
{ name: "cdx:agentic:kind", value: "provider" },
|
|
846
|
+
{ name: "cdx:ai:kind", value: "inference-service" },
|
|
847
|
+
],
|
|
848
|
+
tags: ["ai", "agentic-provider", "opencode"],
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
return services;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Parse the child components and services a tool defines.
|
|
856
|
+
*
|
|
857
|
+
* @param {string} tool Tool key
|
|
858
|
+
* @param {string} dir Tool home directory
|
|
859
|
+
* @param {string} parentRef Parent tool bom-ref
|
|
860
|
+
* @returns {{components: Object[], services: Object[]}} Child inventory
|
|
861
|
+
*/
|
|
862
|
+
function collectToolChildren(tool, dir, parentRef) {
|
|
863
|
+
switch (tool) {
|
|
864
|
+
case "kiro-cli":
|
|
865
|
+
return { components: parseKiroAgents(dir, parentRef), services: [] };
|
|
866
|
+
case "zcode":
|
|
867
|
+
return { components: parseZcodePlugins(dir, parentRef), services: [] };
|
|
868
|
+
case "opencode":
|
|
869
|
+
return {
|
|
870
|
+
components: [],
|
|
871
|
+
services: parseOpencodeProviders(dir, parentRef),
|
|
872
|
+
};
|
|
873
|
+
default:
|
|
874
|
+
return { components: [], services: [] };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Discover installed agentic CLI tools on the current host and return CycloneDX
|
|
880
|
+
* components describing each one, plus the child agents, plugins, and providers
|
|
881
|
+
* they define and the dependency edges linking children to their tool.
|
|
882
|
+
*
|
|
883
|
+
* This host scan is unconditional: it does not consult scan filters such as
|
|
884
|
+
* `--exclude` or profile/technique options, because it inventories fixed tool
|
|
885
|
+
* home directories rather than a project tree. The `options` argument is
|
|
886
|
+
* accepted for signature parity and is intentionally not used to alter results.
|
|
887
|
+
*
|
|
888
|
+
* @param {Object} [options={}] Collection options (accepted but not applied)
|
|
889
|
+
* @returns {{components: Object[], services: Object[], dependencies: Object[]}} Discovered inventory
|
|
890
|
+
*/
|
|
891
|
+
export function collectAgenticTools(_options = {}) {
|
|
892
|
+
const components = [];
|
|
893
|
+
const services = [];
|
|
894
|
+
const dependencies = [];
|
|
895
|
+
for (const entry of discoverAgenticTools()) {
|
|
896
|
+
try {
|
|
897
|
+
const toolComponent = componentForAgenticTool(entry);
|
|
898
|
+
components.push(toolComponent);
|
|
899
|
+
const parentRef = toolComponent["bom-ref"];
|
|
900
|
+
const children = collectToolChildren(entry.tool, entry.dir, parentRef);
|
|
901
|
+
const childRefs = [];
|
|
902
|
+
if (children.components.length) {
|
|
903
|
+
components.push(...children.components);
|
|
904
|
+
childRefs.push(...children.components.map((c) => c["bom-ref"]));
|
|
905
|
+
}
|
|
906
|
+
if (children.services.length) {
|
|
907
|
+
services.push(...children.services);
|
|
908
|
+
childRefs.push(...children.services.map((s) => s["bom-ref"]));
|
|
909
|
+
}
|
|
910
|
+
if (childRefs.length) {
|
|
911
|
+
dependencies.push({
|
|
912
|
+
ref: parentRef,
|
|
913
|
+
dependsOn: childRefs.sort(),
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
} catch (err) {
|
|
917
|
+
if (DEBUG_MODE) {
|
|
918
|
+
console.log(
|
|
919
|
+
`Unable to describe agentic tool ${entry.tool}: ${err.message}`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return { components, services, dependencies };
|
|
925
|
+
}
|