@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.
Files changed (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. 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
+ }