@cyclonedx/cdxgen 12.3.0 → 12.3.2

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 (121) hide show
  1. package/README.md +15 -5
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +241 -81
  4. package/bin/repl.js +138 -0
  5. package/data/rules/ai-agent-governance.yaml +249 -0
  6. package/data/rules/dependency-sources.yaml +41 -0
  7. package/data/rules/mcp-servers.yaml +304 -0
  8. package/data/rules/package-integrity.yaml +123 -0
  9. package/lib/audit/index.js +353 -29
  10. package/lib/audit/index.poku.js +247 -7
  11. package/lib/audit/reporters.js +26 -0
  12. package/lib/audit/scoring.js +262 -13
  13. package/lib/audit/scoring.poku.js +179 -0
  14. package/lib/audit/targets.js +391 -2
  15. package/lib/audit/targets.poku.js +416 -3
  16. package/lib/cli/index.js +588 -45
  17. package/lib/cli/index.poku.js +735 -1
  18. package/lib/evinser/evinser.js +8 -5
  19. package/lib/helpers/agentFormulationParser.js +318 -0
  20. package/lib/helpers/aiInventory.js +262 -0
  21. package/lib/helpers/aiInventory.poku.js +111 -0
  22. package/lib/helpers/analyzer.js +1769 -0
  23. package/lib/helpers/analyzer.poku.js +284 -3
  24. package/lib/helpers/auditCategories.js +76 -0
  25. package/lib/helpers/ciParsers/githubActions.js +140 -16
  26. package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
  27. package/lib/helpers/communityAiConfigParser.js +672 -0
  28. package/lib/helpers/communityAiConfigParser.poku.js +63 -0
  29. package/lib/helpers/depsUtils.js +108 -0
  30. package/lib/helpers/depsUtils.poku.js +72 -1
  31. package/lib/helpers/display.js +325 -3
  32. package/lib/helpers/display.poku.js +301 -0
  33. package/lib/helpers/formulationParsers.js +28 -0
  34. package/lib/helpers/formulationParsers.poku.js +504 -1
  35. package/lib/helpers/jsonLike.js +102 -0
  36. package/lib/helpers/jsonLike.poku.js +34 -0
  37. package/lib/helpers/mcp.js +248 -0
  38. package/lib/helpers/mcp.poku.js +101 -0
  39. package/lib/helpers/mcpConfigParser.js +656 -0
  40. package/lib/helpers/mcpConfigParser.poku.js +126 -0
  41. package/lib/helpers/mcpDiscovery.js +84 -0
  42. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  43. package/lib/helpers/protobom.js +3 -3
  44. package/lib/helpers/provenanceUtils.js +29 -4
  45. package/lib/helpers/provenanceUtils.poku.js +29 -3
  46. package/lib/helpers/registryProvenance.js +210 -0
  47. package/lib/helpers/registryProvenance.poku.js +144 -0
  48. package/lib/helpers/rustFormulationParser.js +330 -0
  49. package/lib/helpers/source.js +21 -2
  50. package/lib/helpers/source.poku.js +38 -0
  51. package/lib/helpers/utils.js +1331 -83
  52. package/lib/helpers/utils.poku.js +599 -188
  53. package/lib/helpers/vsixutils.js +12 -4
  54. package/lib/helpers/vsixutils.poku.js +34 -0
  55. package/lib/managers/binary.js +36 -12
  56. package/lib/managers/binary.poku.js +68 -0
  57. package/lib/managers/docker.js +59 -9
  58. package/lib/managers/docker.poku.js +61 -0
  59. package/lib/managers/piptree.js +12 -7
  60. package/lib/managers/piptree.poku.js +44 -0
  61. package/lib/stages/postgen/annotator.js +2 -1
  62. package/lib/stages/postgen/annotator.poku.js +15 -0
  63. package/lib/stages/postgen/auditBom.js +20 -6
  64. package/lib/stages/postgen/auditBom.poku.js +694 -1
  65. package/lib/stages/postgen/postgen.js +262 -11
  66. package/lib/stages/postgen/postgen.poku.js +306 -2
  67. package/lib/stages/postgen/ruleEngine.js +49 -1
  68. package/lib/stages/postgen/spdxConverter.poku.js +70 -0
  69. package/lib/stages/pregen/pregen.js +6 -4
  70. package/package.json +1 -1
  71. package/types/bin/repl.d.ts.map +1 -1
  72. package/types/lib/audit/index.d.ts.map +1 -1
  73. package/types/lib/audit/reporters.d.ts.map +1 -1
  74. package/types/lib/audit/scoring.d.ts.map +1 -1
  75. package/types/lib/audit/targets.d.ts +12 -0
  76. package/types/lib/audit/targets.d.ts.map +1 -1
  77. package/types/lib/cli/index.d.ts +2 -8
  78. package/types/lib/cli/index.d.ts.map +1 -1
  79. package/types/lib/evinser/evinser.d.ts.map +1 -1
  80. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  81. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  82. package/types/lib/helpers/aiInventory.d.ts +23 -0
  83. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  84. package/types/lib/helpers/analyzer.d.ts +10 -0
  85. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  86. package/types/lib/helpers/auditCategories.d.ts +12 -0
  87. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  88. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  89. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  90. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  91. package/types/lib/helpers/depsUtils.d.ts +8 -0
  92. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  93. package/types/lib/helpers/display.d.ts +17 -1
  94. package/types/lib/helpers/display.d.ts.map +1 -1
  95. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  96. package/types/lib/helpers/jsonLike.d.ts +4 -0
  97. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  98. package/types/lib/helpers/mcp.d.ts +29 -0
  99. package/types/lib/helpers/mcp.d.ts.map +1 -0
  100. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  101. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  102. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  103. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  104. package/types/lib/helpers/provenanceUtils.d.ts +5 -3
  105. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
  106. package/types/lib/helpers/registryProvenance.d.ts +9 -0
  107. package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
  108. package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
  109. package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
  110. package/types/lib/helpers/source.d.ts.map +1 -1
  111. package/types/lib/helpers/utils.d.ts +31 -1
  112. package/types/lib/helpers/utils.d.ts.map +1 -1
  113. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  114. package/types/lib/managers/binary.d.ts.map +1 -1
  115. package/types/lib/managers/docker.d.ts.map +1 -1
  116. package/types/lib/managers/piptree.d.ts.map +1 -1
  117. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  118. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  119. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  120. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  121. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -17,6 +17,9 @@ import {
17
17
  getTmpDir,
18
18
  PROJECT_TYPE_ALIASES,
19
19
  safeExistsSync,
20
+ safeMkdtempSync,
21
+ safeRmSync,
22
+ safeWriteSync,
20
23
  } from "../helpers/utils.js";
21
24
  import { postProcess } from "../stages/postgen/postgen.js";
22
25
  import { createOrLoad } from "./db.js";
@@ -192,7 +195,7 @@ export async function createAndStoreSlice(
192
195
  });
193
196
  }
194
197
  if (retMap?.tempDir?.startsWith(getTmpDir())) {
195
- fs.rmSync(retMap.tempDir, { recursive: true, force: true });
198
+ safeRmSync(retMap.tempDir, { recursive: true, force: true });
196
199
  }
197
200
  return sliceData;
198
201
  }
@@ -234,7 +237,7 @@ export async function createSlice(
234
237
  return {};
235
238
  }
236
239
 
237
- let sliceOutputDir = fs.mkdtempSync(join(getTmpDir(), `atom-${sliceType}-`));
240
+ let sliceOutputDir = safeMkdtempSync(join(getTmpDir(), `atom-${sliceType}-`));
238
241
  if (options?.output) {
239
242
  sliceOutputDir =
240
243
  safeExistsSync(options.output) &&
@@ -256,7 +259,7 @@ export async function createSlice(
256
259
  const slicesData = createSemanticsSlices(resolve(filePath), options);
257
260
  // Write the semantics slices data
258
261
  if (slicesData) {
259
- fs.writeFileSync(
262
+ safeWriteSync(
260
263
  slicesFile,
261
264
  JSON.stringify(slicesData, null, options.jsonPretty ? 2 : null),
262
265
  );
@@ -1523,7 +1526,7 @@ export function createEvinseFile(sliceArtefacts, options) {
1523
1526
  delete bomJson.signature;
1524
1527
  // Redo post-processing with evinse data
1525
1528
  const bomNSData = postProcess({ bomJson }, options);
1526
- fs.writeFileSync(
1529
+ safeWriteSync(
1527
1530
  evinseOutFile,
1528
1531
  JSON.stringify(bomNSData.bomJson, null, options.jsonPretty ? 2 : null),
1529
1532
  );
@@ -1555,7 +1558,7 @@ export function createEvinseFile(sliceArtefacts, options) {
1555
1558
  }
1556
1559
  }
1557
1560
  if (tempDir?.startsWith(getTmpDir())) {
1558
- fs.rmSync(tempDir, { recursive: true, force: true });
1561
+ safeRmSync(tempDir, { recursive: true, force: true });
1559
1562
  }
1560
1563
  return bomNSData?.bomJson;
1561
1564
  }
@@ -0,0 +1,318 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, extname } from "node:path";
3
+
4
+ import {
5
+ credentialIndicatorsForText,
6
+ isLocalHost,
7
+ providerNamesForText,
8
+ sanitizeMcpRefToken,
9
+ } from "./mcpDiscovery.js";
10
+ import { scanTextForHiddenUnicode } from "./unicodeScan.js";
11
+
12
+ const AGENT_FILE_PATTERNS = [
13
+ "AGENTS.md",
14
+ "agents.md",
15
+ "CLAUDE.md",
16
+ "Cursor.md",
17
+ ".github/copilot-instructions.md",
18
+ ".github/instructions/**/*.{md,mdx,markdown,txt}",
19
+ ".github/workflows/copilot-setup-steps.yml",
20
+ "**/*.{prompt,mdc}",
21
+ ];
22
+
23
+ const MCP_PACKAGE_REF_PATTERN =
24
+ /@modelcontextprotocol\/[a-z0-9._/-]+|@[a-z0-9._-]+\/mcp[a-z0-9._/-]*/giu;
25
+ const URL_PATTERN = /https?:\/\/[^\s<>"')\]}]+/giu;
26
+ const AUTH_HINT_PATTERNS = [
27
+ ["bearer", /\bbearer\b/i],
28
+ ["oauth", /\boauth\b|authorization_endpoint|token_endpoint|issuer/i],
29
+ ["api-key", /\bapi[_ -]?key\b/i],
30
+ ["token", /\btoken\b|authorization:/i],
31
+ ];
32
+ const TUNNEL_HOST_PATTERNS = [
33
+ /\.ngrok(?:-free)?\.app$/i,
34
+ /\.ngrok\.io$/i,
35
+ /\.trycloudflare\.com$/i,
36
+ /\.localhost\.run$/i,
37
+ /\.serveo\.net$/i,
38
+ ];
39
+
40
+ function syntaxForFile(filePath) {
41
+ const extension = extname(filePath).toLowerCase();
42
+ if ([".md", ".mdx", ".markdown"].includes(extension)) {
43
+ return "markdown";
44
+ }
45
+ if ([".yaml", ".yml"].includes(extension)) {
46
+ return "yaml";
47
+ }
48
+ return "text";
49
+ }
50
+
51
+ function kindForFile(filePath) {
52
+ const lowerPath = filePath.toLowerCase();
53
+ if (lowerPath.endsWith("copilot-setup-steps.yml")) {
54
+ return "copilot-setup-workflow";
55
+ }
56
+ if (lowerPath.endsWith("copilot-instructions.md")) {
57
+ return "copilot-instructions";
58
+ }
59
+ if (lowerPath.endsWith("agents.md") || lowerPath.endsWith("claude.md")) {
60
+ return "agent-instructions";
61
+ }
62
+ if (lowerPath.endsWith(".prompt") || lowerPath.endsWith(".mdc")) {
63
+ return "skill-file";
64
+ }
65
+ return "ai-agent-file";
66
+ }
67
+
68
+ function authHintsForText(text) {
69
+ return AUTH_HINT_PATTERNS.flatMap(([name, pattern]) =>
70
+ pattern.test(text) ? [name] : [],
71
+ );
72
+ }
73
+
74
+ function packageRefsForText(text) {
75
+ return [...new Set(text.match(MCP_PACKAGE_REF_PATTERN) || [])].sort();
76
+ }
77
+
78
+ function mcpUrlsForText(text) {
79
+ const urls = [];
80
+ for (const match of text.match(URL_PATTERN) || []) {
81
+ try {
82
+ const parsed = new URL(match);
83
+ if (
84
+ parsed.pathname.toLowerCase().includes("/mcp") ||
85
+ parsed.hostname.toLowerCase().includes("modelcontextprotocol")
86
+ ) {
87
+ urls.push(parsed.toString());
88
+ }
89
+ } catch {
90
+ // Ignore malformed URLs in untrusted agent instructions.
91
+ }
92
+ }
93
+ return [...new Set(urls)].sort();
94
+ }
95
+
96
+ function buildInferredMcpServices(filePath, mcpUrls, authHints, providerNames) {
97
+ return mcpUrls.map((urlValue, index) => {
98
+ const parsed = new URL(urlValue);
99
+ const hostname = parsed.hostname.toLowerCase();
100
+ const exposureType = isLocalHost(hostname)
101
+ ? "local-only"
102
+ : "networked-public";
103
+ const properties = [
104
+ { name: "SrcFile", value: filePath },
105
+ { name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
106
+ { name: "cdx:mcp:inventorySource", value: "agent-file" },
107
+ { name: "cdx:mcp:usageConfidence", value: "medium" },
108
+ { name: "cdx:mcp:reviewNeeded", value: "true" },
109
+ { name: "cdx:mcp:exposureType", value: exposureType },
110
+ { name: "cdx:mcp:agentReference", value: "true" },
111
+ ];
112
+ if (providerNames.length) {
113
+ properties.push({
114
+ name: "cdx:mcp:providerNames",
115
+ value: providerNames.join(","),
116
+ });
117
+ }
118
+ if (authHints.length) {
119
+ properties.push({
120
+ name: "cdx:mcp:authMode",
121
+ value: authHints.join(","),
122
+ });
123
+ }
124
+ return {
125
+ "bom-ref": `urn:service:agent-mcp:${sanitizeMcpRefToken(hostname || basename(filePath))}:${index + 1}`,
126
+ group: "mcp",
127
+ name: hostname || `${basename(filePath)}-mcp-endpoint`,
128
+ endpoints: [urlValue],
129
+ properties,
130
+ version: "inferred",
131
+ };
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Discover AI agent instruction and skill files that can hide MCP/runtime
137
+ * surfaces from package-only inventory.
138
+ */
139
+ export const agentFormulationParser = {
140
+ id: "agent-formulation",
141
+ patterns: AGENT_FILE_PATTERNS,
142
+ parse(files, _options = {}) {
143
+ const components = [];
144
+ const services = [];
145
+ for (const filePath of files || []) {
146
+ let raw;
147
+ try {
148
+ raw = readFileSync(filePath, "utf-8");
149
+ } catch {
150
+ continue;
151
+ }
152
+ const hiddenUnicodeScan = scanTextForHiddenUnicode(raw, {
153
+ syntax: syntaxForFile(filePath),
154
+ });
155
+ const packageRefs = packageRefsForText(raw);
156
+ const providerNames = providerNamesForText(raw);
157
+ const mcpUrls = mcpUrlsForText(raw);
158
+ const authHints = authHintsForText(raw);
159
+ const credentialIndicators = credentialIndicatorsForText(raw);
160
+ const mcpHosts = mcpUrls.map((urlValue) => new URL(urlValue).hostname);
161
+ const hasPublicMcpEndpoint = mcpHosts.some((host) => !isLocalHost(host));
162
+ const hasTunnelReference = mcpHosts.some((host) =>
163
+ TUNNEL_HOST_PATTERNS.some((pattern) => pattern.test(host)),
164
+ );
165
+ const hasMcpReferences =
166
+ mcpUrls.length > 0 ||
167
+ packageRefs.length > 0 ||
168
+ /\bmcp\b/i.test(raw) ||
169
+ /modelcontextprotocol/i.test(raw);
170
+ // Inventory all matched agent/instruction files, even when they do not
171
+ // yet contain hidden Unicode or explicit MCP references, so shipped files
172
+ // such as CLAUDE.md and AGENTS.md still surface in build/post-build BOMs.
173
+ if (!raw.trim().length) {
174
+ continue;
175
+ }
176
+ const hiddenComponentKinds = [];
177
+ if (mcpUrls.length) {
178
+ hiddenComponentKinds.push("mcp-endpoint");
179
+ }
180
+ if (providerNames.length) {
181
+ hiddenComponentKinds.push("provider");
182
+ }
183
+ if (packageRefs.length) {
184
+ hiddenComponentKinds.push("mcp-package-reference");
185
+ }
186
+ const properties = [
187
+ { name: "SrcFile", value: filePath },
188
+ { name: "cdx:file:kind", value: kindForFile(filePath) },
189
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
190
+ { name: "cdx:agent:hasMcpReferences", value: String(hasMcpReferences) },
191
+ {
192
+ name: "cdx:agent:hiddenEndpointCount",
193
+ value: String(mcpUrls.length),
194
+ },
195
+ ];
196
+ if (hiddenUnicodeScan.hasHiddenUnicode) {
197
+ properties.push(
198
+ { name: "cdx:file:hasHiddenUnicode", value: "true" },
199
+ {
200
+ name: "cdx:file:hiddenUnicodeCodePoints",
201
+ value: hiddenUnicodeScan.codePoints.join(","),
202
+ },
203
+ {
204
+ name: "cdx:file:hiddenUnicodeLineNumbers",
205
+ value: hiddenUnicodeScan.lineNumbers.join(","),
206
+ },
207
+ );
208
+ if (hiddenUnicodeScan.inComments) {
209
+ properties.push(
210
+ {
211
+ name: "cdx:file:hiddenUnicodeInComments",
212
+ value: "true",
213
+ },
214
+ {
215
+ name: "cdx:file:hiddenUnicodeCommentCodePoints",
216
+ value: hiddenUnicodeScan.commentCodePoints.join(","),
217
+ },
218
+ );
219
+ }
220
+ }
221
+ if (packageRefs.length) {
222
+ properties.push({
223
+ name: "cdx:agent:mcpPackageRefs",
224
+ value: packageRefs.join(","),
225
+ });
226
+ if (
227
+ packageRefs.some((ref) => !ref.startsWith("@modelcontextprotocol/"))
228
+ ) {
229
+ properties.push({
230
+ name: "cdx:agent:hasNonOfficialMcpReference",
231
+ value: "true",
232
+ });
233
+ }
234
+ }
235
+ if (mcpUrls.length) {
236
+ properties.push(
237
+ {
238
+ name: "cdx:agent:hiddenMcpUrls",
239
+ value: mcpUrls.join(","),
240
+ },
241
+ {
242
+ name: "cdx:agent:hiddenMcpHosts",
243
+ value: [...new Set(mcpHosts)].sort().join(","),
244
+ },
245
+ );
246
+ }
247
+ if (providerNames.length) {
248
+ properties.push({
249
+ name: "cdx:agent:providerNames",
250
+ value: providerNames.join(","),
251
+ });
252
+ }
253
+ if (authHints.length) {
254
+ properties.push({
255
+ name: "cdx:agent:authHints",
256
+ value: authHints.join(","),
257
+ });
258
+ }
259
+ if (credentialIndicators.length) {
260
+ properties.push(
261
+ {
262
+ name: "cdx:agent:credentialExposure",
263
+ value: "true",
264
+ },
265
+ {
266
+ name: "cdx:agent:credentialRiskIndicators",
267
+ value: credentialIndicators.join(","),
268
+ },
269
+ );
270
+ }
271
+ if (hasPublicMcpEndpoint) {
272
+ properties.push({
273
+ name: "cdx:agent:hasPublicMcpEndpoint",
274
+ value: "true",
275
+ });
276
+ }
277
+ if (hasTunnelReference) {
278
+ properties.push({
279
+ name: "cdx:agent:hasTunnelReference",
280
+ value: "true",
281
+ });
282
+ }
283
+ if (hiddenComponentKinds.length) {
284
+ properties.push({
285
+ name: "cdx:agent:hiddenComponentKinds",
286
+ value: hiddenComponentKinds.join(","),
287
+ });
288
+ }
289
+ if (
290
+ hiddenUnicodeScan.hasHiddenUnicode ||
291
+ hasPublicMcpEndpoint ||
292
+ hasTunnelReference ||
293
+ packageRefs.length > 0 ||
294
+ credentialIndicators.length > 0
295
+ ) {
296
+ properties.push({
297
+ name: "cdx:agent:reviewNeeded",
298
+ value: "true",
299
+ });
300
+ }
301
+ components.push({
302
+ "bom-ref": `file:${filePath}`,
303
+ name: basename(filePath),
304
+ properties,
305
+ type: "file",
306
+ });
307
+ services.push(
308
+ ...buildInferredMcpServices(
309
+ filePath,
310
+ mcpUrls,
311
+ authHints,
312
+ providerNames,
313
+ ),
314
+ );
315
+ }
316
+ return { components, services };
317
+ },
318
+ };
@@ -0,0 +1,262 @@
1
+ import { agentFormulationParser } from "./agentFormulationParser.js";
2
+ import { communityAiConfigParser } from "./communityAiConfigParser.js";
3
+ import { mergeServices, trimComponents } from "./depsUtils.js";
4
+ import { classifyMcpReference } from "./mcp.js";
5
+ import { mcpConfigParser } from "./mcpConfigParser.js";
6
+ import { getAllFiles } from "./utils.js";
7
+
8
+ export const AI_INVENTORY_PROJECT_TYPES = ["mcp", "ai-skill"];
9
+ export const AI_INSTRUCTION_FILE_KINDS = new Set([
10
+ "agent-config",
11
+ "agent-definition",
12
+ "agent-instructions",
13
+ "ai-agent-file",
14
+ "copilot-instructions",
15
+ "copilot-setup-workflow",
16
+ "crew-agent",
17
+ "crew-task",
18
+ "crew-tool",
19
+ "custom-command",
20
+ "custom-tool",
21
+ "graph-definition",
22
+ ]);
23
+ export const AI_SKILL_FILE_KIND = "skill-file";
24
+ export const MCP_CONFIG_FILE_KIND = "mcp-config";
25
+
26
+ const AI_INVENTORY_FILE_KINDS = new Set([
27
+ "agent-config",
28
+ "agent-definition",
29
+ "agent-instructions",
30
+ "ai-agent-file",
31
+ "copilot-instructions",
32
+ "copilot-setup-workflow",
33
+ "crew-agent",
34
+ "crew-task",
35
+ "crew-tool",
36
+ "custom-command",
37
+ "custom-tool",
38
+ "graph-definition",
39
+ AI_SKILL_FILE_KIND,
40
+ ]);
41
+
42
+ const AI_INVENTORY_PARSERS = [
43
+ {
44
+ id: agentFormulationParser.id,
45
+ parser: agentFormulationParser,
46
+ types: ["mcp", "ai-skill"],
47
+ },
48
+ {
49
+ id: mcpConfigParser.id,
50
+ parser: mcpConfigParser,
51
+ types: ["mcp"],
52
+ },
53
+ {
54
+ id: communityAiConfigParser.id,
55
+ parser: communityAiConfigParser,
56
+ types: ["ai-skill"],
57
+ },
58
+ ];
59
+
60
+ export function inventoryPropertyValue(subject, name) {
61
+ return subject?.properties?.find((property) => property.name === name)?.value;
62
+ }
63
+
64
+ function hasPropertyPrefix(subject, prefix) {
65
+ return (subject?.properties || []).some((property) =>
66
+ property?.name?.startsWith(prefix),
67
+ );
68
+ }
69
+
70
+ function uniqueNonEmptyTypes(types) {
71
+ return [...new Set((types || []).filter(Boolean))];
72
+ }
73
+
74
+ export function optionIncludesAiInventoryProjectType(optionValue, type) {
75
+ const values = Array.isArray(optionValue)
76
+ ? optionValue
77
+ : optionValue
78
+ ? [optionValue]
79
+ : [];
80
+ return values.some((value) => {
81
+ const normalizedValue = String(value).toLowerCase();
82
+ if (type === "ai-skill") {
83
+ return ["ai-skill", "skill", "skills"].includes(normalizedValue);
84
+ }
85
+ return normalizedValue === type;
86
+ });
87
+ }
88
+
89
+ export function inventoryTypesForSubject(subject) {
90
+ const types = new Set();
91
+ const fileKind = inventoryPropertyValue(subject, "cdx:file:kind");
92
+ if (
93
+ subject?.group === "mcp" ||
94
+ classifyMcpReference(subject).isMcp ||
95
+ hasPropertyPrefix(subject, "cdx:mcp:") ||
96
+ (subject?.tags || []).some((tag) => String(tag || "").startsWith("mcp"))
97
+ ) {
98
+ types.add("mcp");
99
+ }
100
+ if (
101
+ AI_INVENTORY_FILE_KINDS.has(fileKind) ||
102
+ hasPropertyPrefix(subject, "cdx:agent:") ||
103
+ hasPropertyPrefix(subject, "cdx:skill:") ||
104
+ hasPropertyPrefix(subject, "cdx:tool:") ||
105
+ hasPropertyPrefix(subject, "cdx:langgraph:") ||
106
+ hasPropertyPrefix(subject, "cdx:crewai:")
107
+ ) {
108
+ types.add("ai-skill");
109
+ }
110
+ if (
111
+ inventoryPropertyValue(subject, "cdx:mcp:inventorySource") === "agent-file"
112
+ ) {
113
+ types.add("ai-skill");
114
+ }
115
+ return Array.from(types);
116
+ }
117
+
118
+ export function matchesAiInventoryType(subject, type) {
119
+ return inventoryTypesForSubject(subject).includes(type);
120
+ }
121
+
122
+ export function matchesAiInventoryExcludeType(subject, type) {
123
+ if (type === "mcp") {
124
+ const fileKind = inventoryPropertyValue(subject, "cdx:file:kind");
125
+ return (
126
+ fileKind === MCP_CONFIG_FILE_KIND ||
127
+ subject?.group === "mcp" ||
128
+ inventoryPropertyValue(subject, "cdx:mcp:inventorySource") !==
129
+ undefined ||
130
+ inventoryPropertyValue(subject, "cdx:mcp:role") !== undefined
131
+ );
132
+ }
133
+ return matchesAiInventoryType(subject, type);
134
+ }
135
+
136
+ export function filterInventorySubjectsByTypes(subjects, types) {
137
+ const allowedTypes = uniqueNonEmptyTypes(types);
138
+ if (!allowedTypes.length) {
139
+ return [];
140
+ }
141
+ return (subjects || []).filter((subject) =>
142
+ inventoryTypesForSubject(subject).some((type) =>
143
+ allowedTypes.includes(type),
144
+ ),
145
+ );
146
+ }
147
+
148
+ export function filterInventoryDependencies(
149
+ dependencies,
150
+ components,
151
+ services,
152
+ ) {
153
+ const allowedRefs = new Set(
154
+ []
155
+ .concat(components || [])
156
+ .concat(services || [])
157
+ .map((subject) => subject?.["bom-ref"])
158
+ .filter(Boolean),
159
+ );
160
+ return (dependencies || [])
161
+ .filter((dependency) => allowedRefs.has(dependency.ref))
162
+ .map((dependency) => {
163
+ const filteredDependency = {
164
+ ref: dependency.ref,
165
+ };
166
+ if (dependency.dependsOn?.length) {
167
+ filteredDependency.dependsOn = dependency.dependsOn.filter((ref) =>
168
+ allowedRefs.has(ref),
169
+ );
170
+ }
171
+ if (dependency.provides?.length) {
172
+ filteredDependency.provides = dependency.provides.filter((ref) =>
173
+ allowedRefs.has(ref),
174
+ );
175
+ }
176
+ return filteredDependency;
177
+ });
178
+ }
179
+
180
+ export function collectAiInventory(discoveryPath, options, types) {
181
+ const requestedTypes = uniqueNonEmptyTypes(types);
182
+ if (!requestedTypes.length) {
183
+ return { components: [], dependencies: [], services: [] };
184
+ }
185
+ let components = [];
186
+ const dependencies = [];
187
+ let services = [];
188
+ for (const parserEntry of AI_INVENTORY_PARSERS) {
189
+ if (!parserEntry.types.some((type) => requestedTypes.includes(type))) {
190
+ continue;
191
+ }
192
+ const matchedFiles = [];
193
+ for (const pattern of parserEntry.parser.patterns) {
194
+ const found = getAllFiles(discoveryPath, pattern, options);
195
+ if (found?.length) {
196
+ matchedFiles.push(...found);
197
+ }
198
+ }
199
+ const uniqueMatchedFiles = [...new Set(matchedFiles)];
200
+ if (!uniqueMatchedFiles.length) {
201
+ continue;
202
+ }
203
+ let result;
204
+ try {
205
+ result = parserEntry.parser.parse(uniqueMatchedFiles, options);
206
+ } catch (err) {
207
+ console.warn(
208
+ `[aiInventory] Parser "${parserEntry.id}" threw an error:`,
209
+ err.message,
210
+ );
211
+ continue;
212
+ }
213
+ if (result?.components?.length) {
214
+ components = components.concat(result.components);
215
+ }
216
+ if (result?.services?.length) {
217
+ services = mergeServices(services, result.services);
218
+ }
219
+ if (result?.dependencies?.length) {
220
+ dependencies.push(...result.dependencies);
221
+ }
222
+ }
223
+ components = trimComponents(
224
+ filterInventorySubjectsByTypes(components, requestedTypes),
225
+ );
226
+ services = mergeServices(
227
+ [],
228
+ filterInventorySubjectsByTypes(services, requestedTypes),
229
+ );
230
+ return {
231
+ components,
232
+ dependencies: filterInventoryDependencies(
233
+ dependencies,
234
+ components,
235
+ services,
236
+ ),
237
+ services,
238
+ };
239
+ }
240
+
241
+ export function summarizeAiInventory(inventory) {
242
+ const components = inventory?.components || [];
243
+ const services = inventory?.services || [];
244
+ return {
245
+ instructionCount: components.filter((component) =>
246
+ AI_INSTRUCTION_FILE_KINDS.has(
247
+ inventoryPropertyValue(component, "cdx:file:kind"),
248
+ ),
249
+ ).length,
250
+ mcpConfigCount: components.filter(
251
+ (component) =>
252
+ inventoryPropertyValue(component, "cdx:file:kind") ===
253
+ MCP_CONFIG_FILE_KIND,
254
+ ).length,
255
+ mcpServiceCount: services.length,
256
+ skillCount: components.filter(
257
+ (component) =>
258
+ inventoryPropertyValue(component, "cdx:file:kind") ===
259
+ AI_SKILL_FILE_KIND,
260
+ ).length,
261
+ };
262
+ }