@cdxgen/cdxgen 13.6.0 → 13.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/bin/audit.js +7 -4
- package/bin/cdxgen.js +256 -37
- package/bin/convert.js +146 -22
- package/bin/evinse.js +25 -3
- package/bin/repl.js +47 -3
- package/bin/sign.js +211 -34
- package/bin/tracebom.js +18 -6
- package/bin/validate.js +18 -3
- package/bin/verify.js +113 -5
- package/data/behavior-taxonomy.schema.json +1491 -0
- package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
- package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
- package/data/pqc-migration.json +239 -0
- package/data/remediations.json +52 -0
- package/data/rules/cbom-security.yaml +4 -3
- package/data/rules/package-integrity.yaml +28 -0
- package/lib/audit/index.js +25 -3
- package/lib/cli/bomAssembly.js +80 -1
- package/lib/cli/cliOptions.js +121 -17
- package/lib/cli/index.js +609 -898
- package/lib/cli/jsBom.js +3 -0
- package/lib/cli/jvmBom.js +365 -40
- package/lib/cli/managedBom.js +19 -3
- package/lib/cli/nativeBom.js +10 -0
- package/lib/core/env.js +26 -0
- package/lib/core/fs.js +126 -3
- package/lib/core/globPatterns.js +218 -0
- package/lib/core/processTree.js +99 -12
- package/lib/core/projectConfig.js +332 -25
- package/lib/core/toolRegistry.js +15 -1
- package/lib/ecosystems/denoutils.js +2 -2
- package/lib/ecosystems/golem.js +3 -8
- package/lib/ecosystems/gradleutils.js +1082 -6
- package/lib/ecosystems/jsEvidence.js +90 -38
- package/lib/ecosystems/kosi.js +3 -5
- package/lib/ecosystems/npmVersionResolution.js +376 -0
- package/lib/ecosystems/parsers-python.js +89 -28
- package/lib/ecosystems/parsers-rust.js +117 -15
- package/lib/ecosystems/remote/huggingface.js +22 -0
- package/lib/ecosystems/rusi.js +2 -4
- package/lib/ecosystems/scalasem.js +2 -1
- package/lib/evinser/evinser.js +196 -34
- package/lib/evinser/scalasem.js +5 -1
- package/lib/helpers/agenticToolutils.js +112 -1
- package/lib/helpers/bomSigner.js +43 -9
- package/lib/helpers/jssSigner.js +1276 -0
- package/lib/inventory/agentBlueprint.js +193 -0
- package/lib/inventory/agentToolNames.js +145 -0
- package/lib/inventory/aiCollector.js +153 -7
- package/lib/inventory/aiInventory.js +44 -8
- package/lib/inventory/aiProviders.js +324 -0
- package/lib/inventory/bomUtils.js +8 -1
- package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
- package/lib/inventory/citations.js +31 -2
- package/lib/inventory/communityAiConfigParser.js +978 -80
- package/lib/inventory/cyclonedx20.js +152 -0
- package/lib/inventory/cyclonedx20Common.js +286 -0
- package/lib/inventory/cyclonedx20Derived.js +159 -0
- package/lib/inventory/cyclonedx20Parties.js +552 -0
- package/lib/inventory/cyclonedx20Read.js +768 -0
- package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
- package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
- package/lib/inventory/cyclonedx20ReadContext.js +380 -0
- package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
- package/lib/inventory/cyclonedx20ReadSections.js +599 -0
- package/lib/inventory/cyclonedx20Render.js +766 -0
- package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
- package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
- package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
- package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
- package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
- package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
- package/lib/inventory/cyclonedx20RenderSections.js +499 -0
- package/lib/inventory/cyclonedx20Tables.js +594 -0
- package/lib/inventory/deps.js +106 -13
- package/lib/inventory/dosai.js +236 -1
- package/lib/inventory/dosaiParsers.js +3 -6
- package/lib/inventory/dotnetRuntime.js +164 -14
- package/lib/inventory/ecosystemFormulationParsers.js +979 -0
- package/lib/inventory/envcontext.js +132 -38
- package/lib/inventory/evidenceUtils.js +2 -1
- package/lib/inventory/formulationParsers.js +95 -13
- package/lib/inventory/hbom.js +45 -6
- package/lib/inventory/hbomLoader.js +166 -0
- package/lib/inventory/mcpConfigParser.js +5 -0
- package/lib/inventory/occurrenceKey.js +90 -0
- package/lib/inventory/pqcMigration.js +426 -0
- package/lib/inventory/rustFormulationParser.js +21 -10
- package/lib/inventory/source.js +65 -6
- package/lib/parsers/huggingfaceManifest.js +9 -1
- package/lib/parsers/huggingfaceUtils.js +18 -0
- package/lib/parsers/kiroPermissions.js +166 -0
- package/lib/server/server.js +36 -34
- package/lib/stages/postgen/postgen.js +41 -1
- package/lib/stages/postgen/specVersionCompat.js +76 -0
- package/lib/validator/bomValidator.js +8 -1
- package/lib/validator/complianceEngine.js +30 -3
- package/lib/validator/complianceRules.js +1416 -4
- package/lib/validator/generated/validate-2.0.mjs +57554 -13851
- package/lib/validator/index.js +48 -7
- package/lib/validator/reporters/json.js +6 -0
- package/package.json +9 -9
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +6 -2
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +16 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +6 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +2 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +15 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +30 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +10 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +4 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/globPatterns.d.ts +70 -0
- package/types/lib/core/globPatterns.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +9 -0
- package/types/lib/core/paths.d.ts.map +1 -1
- package/types/lib/core/processTree.d.ts +16 -5
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts +47 -8
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +248 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
- package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts +58 -16
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +61 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/scalasem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +53 -19
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +20 -3
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/helpers/jssSigner.d.ts +288 -0
- package/types/lib/helpers/jssSigner.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +303 -19
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/inventory/agentBlueprint.d.ts +70 -0
- package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
- package/types/lib/inventory/agentToolNames.d.ts +51 -0
- package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/aiProviders.d.ts +85 -0
- package/types/lib/inventory/aiProviders.d.ts.map +1 -0
- package/types/lib/inventory/bomUtils.d.ts.map +1 -1
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +5 -0
- package/types/lib/inventory/citations.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/cyclonedx20.d.ts +99 -0
- package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +35 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +19 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +22 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts +11 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomLoader.d.ts +54 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/occurrenceKey.d.ts +52 -0
- package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
- package/types/lib/inventory/pqcMigration.d.ts +84 -0
- package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
- package/types/lib/inventory/scalaDedupe.d.ts +43 -0
- package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +15 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/kiroPermissions.d.ts +62 -0
- package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +7 -0
- 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/postgen/specVersionCompat.d.ts +25 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts +5 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceEngine.d.ts +4 -4
- package/types/lib/validator/complianceEngine.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts +14 -2
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, extname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { parse as loadYaml } from "yaml";
|
|
5
5
|
|
|
6
6
|
import { safeExistsSync } from "../core/fs.js";
|
|
7
7
|
import { sanitizeBomPropertyValue } from "../core/propertySanitizer.js";
|
|
8
|
+
import {
|
|
9
|
+
kiroPermissionSummaryEntries,
|
|
10
|
+
summarizeKiroPermissions,
|
|
11
|
+
} from "../parsers/kiroPermissions.js";
|
|
8
12
|
import { isAgentSkillSpecValid } from "./agentSkillSpec.js";
|
|
13
|
+
import { summarizeAgentToolList } from "./agentToolNames.js";
|
|
9
14
|
import { parseJsonLike } from "./jsonLike.js";
|
|
10
15
|
import {
|
|
11
16
|
credentialIndicatorsForText,
|
|
@@ -54,6 +59,33 @@ const COMMUNITY_AI_PATTERNS = [
|
|
|
54
59
|
"**/.nanocoder/commands/**/*.md",
|
|
55
60
|
".nanocoder/skills/**/*.md",
|
|
56
61
|
"**/.nanocoder/skills/**/*.md",
|
|
62
|
+
".kiro/steering/**/*.md",
|
|
63
|
+
"**/.kiro/steering/**/*.md",
|
|
64
|
+
".kiro/specs/**/*.md",
|
|
65
|
+
"**/.kiro/specs/**/*.md",
|
|
66
|
+
".kiro/skills/*/SKILL.md",
|
|
67
|
+
"**/.kiro/skills/*/SKILL.md",
|
|
68
|
+
".kiro/agents/*.json",
|
|
69
|
+
"**/.kiro/agents/*.json",
|
|
70
|
+
".kiro/agents/*.md",
|
|
71
|
+
"**/.kiro/agents/*.md",
|
|
72
|
+
".kiro/settings/permissions.yaml",
|
|
73
|
+
"**/.kiro/settings/permissions.yaml",
|
|
74
|
+
".kiro/settings/cli.json",
|
|
75
|
+
"**/.kiro/settings/cli.json",
|
|
76
|
+
".kiro/hooks/*.{json,sh,bash,zsh,ps1}",
|
|
77
|
+
"**/.kiro/hooks/*.{json,sh,bash,zsh,ps1}",
|
|
78
|
+
".kiro/workflows/*.{yaml,yml,json}",
|
|
79
|
+
"**/.kiro/workflows/*.{yaml,yml,json}",
|
|
80
|
+
// Kiro keeps the same content outside .kiro in repositories that install
|
|
81
|
+
// its CLI configuration at a project root (steering documents, settings,
|
|
82
|
+
// and hooks). Only the settings files are matched as candidates; the
|
|
83
|
+
// steering and hook files are picked up from the validated Kiro root in
|
|
84
|
+
// parse(), so a plain docs steering/ folder never matches anything.
|
|
85
|
+
"settings/permissions.yaml",
|
|
86
|
+
"**/settings/permissions.yaml",
|
|
87
|
+
"settings/cli.json",
|
|
88
|
+
"**/settings/cli.json",
|
|
57
89
|
"langgraph.json",
|
|
58
90
|
"**/langgraph.json",
|
|
59
91
|
"agents.py",
|
|
@@ -119,6 +151,119 @@ function parseFrontmatter(raw) {
|
|
|
119
151
|
return { body: match[2].trim(), metadata };
|
|
120
152
|
}
|
|
121
153
|
|
|
154
|
+
function parseYamlSafe(raw) {
|
|
155
|
+
try {
|
|
156
|
+
const doc = loadYaml(raw);
|
|
157
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : {};
|
|
158
|
+
} catch {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseJsonLikeSafe(raw) {
|
|
164
|
+
try {
|
|
165
|
+
const value = parseJsonLike(raw);
|
|
166
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
167
|
+
? value
|
|
168
|
+
: undefined;
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Whether a parsed permissions document holds at least one rule entry that
|
|
176
|
+
* names both a capability and an effect, the shape that sets a Kiro V3
|
|
177
|
+
* permission file apart from any other settings/permissions.yaml.
|
|
178
|
+
*
|
|
179
|
+
* @param {Object} doc Parsed YAML document
|
|
180
|
+
* @returns {boolean} True when the document looks like Kiro permission rules
|
|
181
|
+
*/
|
|
182
|
+
function hasKiroPermissionRulesShape(doc) {
|
|
183
|
+
return (
|
|
184
|
+
Array.isArray(doc?.rules) &&
|
|
185
|
+
doc.rules.some(
|
|
186
|
+
(rule) =>
|
|
187
|
+
rule !== null &&
|
|
188
|
+
typeof rule === "object" &&
|
|
189
|
+
Object.hasOwn(rule, "capability") &&
|
|
190
|
+
Object.hasOwn(rule, "effect"),
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Whether a parsed cli.json carries at least one `chat.`-prefixed key, the
|
|
197
|
+
* shape only the Kiro CLI writes to its settings file.
|
|
198
|
+
*
|
|
199
|
+
* @param {Object|undefined} settings Parsed cli.json content
|
|
200
|
+
* @returns {boolean} True when a chat-prefixed key is present
|
|
201
|
+
*/
|
|
202
|
+
function hasKiroChatKeys(settings) {
|
|
203
|
+
return (
|
|
204
|
+
Boolean(settings) &&
|
|
205
|
+
Object.keys(settings).some((key) => key.startsWith("chat."))
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Decide whether a directory is a Kiro configuration root of the layout that
|
|
211
|
+
* keeps steering, settings, and hooks at the project root instead of under
|
|
212
|
+
* .kiro. The check reads the two settings files directly, so a directory that
|
|
213
|
+
* merely contains a docs steering/ folder never qualifies.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} rootDir Candidate root directory
|
|
216
|
+
* @returns {boolean} True when the directory holds Kiro settings
|
|
217
|
+
*/
|
|
218
|
+
function isKiroConfigurationRoot(rootDir) {
|
|
219
|
+
const permissionsFile = join(rootDir, "settings", "permissions.yaml");
|
|
220
|
+
if (
|
|
221
|
+
safeExistsSync(permissionsFile) &&
|
|
222
|
+
hasKiroPermissionRulesShape(
|
|
223
|
+
parseYamlSafe(readFileSync(permissionsFile, "utf-8")),
|
|
224
|
+
)
|
|
225
|
+
) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
const cliFile = join(rootDir, "settings", "cli.json");
|
|
229
|
+
return (
|
|
230
|
+
safeExistsSync(cliFile) &&
|
|
231
|
+
hasKiroChatKeys(parseJsonLikeSafe(readFileSync(cliFile, "utf-8")))
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* List the files directly inside a directory, or an empty list when the
|
|
237
|
+
* directory is missing or unreadable.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} dirPath Directory to list
|
|
240
|
+
* @returns {string[]} File names
|
|
241
|
+
*/
|
|
242
|
+
function listFileNames(dirPath) {
|
|
243
|
+
try {
|
|
244
|
+
return readdirSync(dirPath, { withFileTypes: true })
|
|
245
|
+
.filter((entry) => entry.isFile())
|
|
246
|
+
.map((entry) => entry.name);
|
|
247
|
+
} catch {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether a lower-cased path sits in a named directory either at the start of
|
|
254
|
+
* a root-relative path or after a separator, so both `steering/x.md` and
|
|
255
|
+
* `<root>/steering/x.md` match while `my-steering/x.md` does not.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} lowerPath Normalized lower-cased path
|
|
258
|
+
* @param {string} dirName Directory name
|
|
259
|
+
* @returns {boolean} True when the path is inside the named directory
|
|
260
|
+
*/
|
|
261
|
+
function inNamedDir(lowerPath, dirName) {
|
|
262
|
+
return (
|
|
263
|
+
lowerPath.startsWith(`${dirName}/`) || lowerPath.includes(`/${dirName}/`)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
122
267
|
function frameworkForFile(filePath, raw = "") {
|
|
123
268
|
const lowerPath = normalizeFilePath(filePath).toLowerCase();
|
|
124
269
|
if (
|
|
@@ -131,6 +276,24 @@ function frameworkForFile(filePath, raw = "") {
|
|
|
131
276
|
if (lowerPath.includes("/.nanocoder/") || lowerPath.endsWith(".mcp.json")) {
|
|
132
277
|
return "nanocoder";
|
|
133
278
|
}
|
|
279
|
+
if (lowerPath.includes("/.kiro/")) {
|
|
280
|
+
return "kiro";
|
|
281
|
+
}
|
|
282
|
+
// Kiro keeps its configuration outside .kiro in some repositories. The
|
|
283
|
+
// settings files below only name the framework when their content matches
|
|
284
|
+
// the Kiro shapes, so a plain settings folder stays community-ai.
|
|
285
|
+
if (
|
|
286
|
+
lowerPath.endsWith("/settings/permissions.yaml") &&
|
|
287
|
+
hasKiroPermissionRulesShape(parseYamlSafe(raw))
|
|
288
|
+
) {
|
|
289
|
+
return "kiro";
|
|
290
|
+
}
|
|
291
|
+
if (
|
|
292
|
+
lowerPath.endsWith("/settings/cli.json") &&
|
|
293
|
+
hasKiroChatKeys(parseJsonLikeSafe(raw))
|
|
294
|
+
) {
|
|
295
|
+
return "kiro";
|
|
296
|
+
}
|
|
134
297
|
if (lowerPath.includes("/.cursor/rules/")) {
|
|
135
298
|
return "cursor";
|
|
136
299
|
}
|
|
@@ -431,15 +594,11 @@ function parseClaudeSubagent(filePath, raw) {
|
|
|
431
594
|
metadata?.description,
|
|
432
595
|
);
|
|
433
596
|
addUniqueProperty(component.properties, "cdx:agent:model", metadata?.model);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
);
|
|
440
|
-
} else if (typeof metadata?.tools === "string") {
|
|
441
|
-
addUniqueProperty(component.properties, "cdx:agent:tools", metadata.tools);
|
|
442
|
-
}
|
|
597
|
+
addUniqueProperty(
|
|
598
|
+
component.properties,
|
|
599
|
+
"cdx:agent:tools",
|
|
600
|
+
summarizeAgentToolList(metadata?.tools),
|
|
601
|
+
);
|
|
443
602
|
maybeAddFileSignals(component.properties, filePath, raw);
|
|
444
603
|
return [component];
|
|
445
604
|
}
|
|
@@ -472,20 +631,370 @@ function parseAgentMarkdown(filePath, raw) {
|
|
|
472
631
|
addUniqueProperty(
|
|
473
632
|
component.properties,
|
|
474
633
|
"cdx:agent:tools",
|
|
475
|
-
metadata.tools
|
|
634
|
+
summarizeAgentToolList(metadata.tools),
|
|
476
635
|
);
|
|
477
636
|
}
|
|
478
637
|
if (Array.isArray(metadata?.disallowedTools)) {
|
|
479
638
|
addUniqueProperty(
|
|
480
639
|
component.properties,
|
|
481
640
|
"cdx:agent:disallowedTools",
|
|
482
|
-
metadata.disallowedTools
|
|
641
|
+
summarizeAgentToolList(metadata.disallowedTools),
|
|
483
642
|
);
|
|
484
643
|
}
|
|
485
644
|
maybeAddFileSignals(component.properties, filePath, raw);
|
|
486
645
|
return [component];
|
|
487
646
|
}
|
|
488
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Add the summarized V3 permission posture of a Kiro agent or settings
|
|
650
|
+
* document to a component property list, using the `cdx:agent:` namespace of
|
|
651
|
+
* the project scan.
|
|
652
|
+
*
|
|
653
|
+
* @param {Object[]} properties Component property list to extend
|
|
654
|
+
* @param {Object} permissionDoc Parsed `permissions` object or YAML document
|
|
655
|
+
*/
|
|
656
|
+
function addKiroPermissionProperties(properties, permissionDoc) {
|
|
657
|
+
for (const [suffix, value] of Object.entries(
|
|
658
|
+
kiroPermissionSummaryEntries(summarizeKiroPermissions(permissionDoc)),
|
|
659
|
+
)) {
|
|
660
|
+
addUniqueProperty(properties, `cdx:agent:${suffix}`, value);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Parse a Kiro V3 agent definition (`.kiro/agents/*.json`) into an
|
|
666
|
+
* agent-definition component. Only the name, the model, redacted counts of
|
|
667
|
+
* MCP servers and tools, the permission posture, and the integer upgrade
|
|
668
|
+
* revision are recorded; the prompt, resource values, toolsSettings, and the
|
|
669
|
+
* upgrade fingerprint or source are never copied.
|
|
670
|
+
*
|
|
671
|
+
* @param {string} filePath Agent definition path
|
|
672
|
+
* @param {string} raw File contents
|
|
673
|
+
* @returns {Object[]} Component list (one agent component or empty)
|
|
674
|
+
*/
|
|
675
|
+
function parseKiroAgentJson(filePath, raw) {
|
|
676
|
+
if (basename(filePath).toLowerCase().endsWith(".example")) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
const agent = parseJsonLikeSafe(raw);
|
|
680
|
+
if (!agent || typeof agent.name !== "string" || !agent.name.trim()) {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
const component = createComponent(
|
|
684
|
+
filePath,
|
|
685
|
+
frameworkForFile(filePath, raw),
|
|
686
|
+
"agent-definition",
|
|
687
|
+
agent.name,
|
|
688
|
+
);
|
|
689
|
+
addUniqueProperty(component.properties, "cdx:agent:role", "kiro-agent");
|
|
690
|
+
if (typeof agent.model === "string") {
|
|
691
|
+
addUniqueProperty(component.properties, "cdx:agent:model", agent.model);
|
|
692
|
+
}
|
|
693
|
+
if (agent.mcpServers && typeof agent.mcpServers === "object") {
|
|
694
|
+
addUniqueProperty(
|
|
695
|
+
component.properties,
|
|
696
|
+
"cdx:agent:mcpServerCount",
|
|
697
|
+
Object.keys(agent.mcpServers).length,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
if (Array.isArray(agent.tools)) {
|
|
701
|
+
addUniqueProperty(
|
|
702
|
+
component.properties,
|
|
703
|
+
"cdx:agent:toolCount",
|
|
704
|
+
agent.tools.length,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (Array.isArray(agent.allowedTools)) {
|
|
708
|
+
addUniqueProperty(
|
|
709
|
+
component.properties,
|
|
710
|
+
"cdx:agent:allowedToolCount",
|
|
711
|
+
agent.allowedTools.length,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (agent.permissions && typeof agent.permissions === "object") {
|
|
715
|
+
addKiroPermissionProperties(component.properties, agent.permissions);
|
|
716
|
+
}
|
|
717
|
+
if (Number.isInteger(agent.kiroUpgrade?.revision)) {
|
|
718
|
+
addUniqueProperty(
|
|
719
|
+
component.properties,
|
|
720
|
+
"cdx:agent:upgradeRevision",
|
|
721
|
+
agent.kiroUpgrade.revision,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
maybeAddFileSignals(component.properties, filePath, raw);
|
|
725
|
+
return [component];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Parse a Kiro permission policy (`permissions.yaml`) into a file component
|
|
730
|
+
* carrying the summarized rule posture. Match patterns and other rule text
|
|
731
|
+
* are never copied.
|
|
732
|
+
*
|
|
733
|
+
* @param {string} filePath Permissions file path
|
|
734
|
+
* @param {string} raw File contents
|
|
735
|
+
* @returns {Object[]} Component list
|
|
736
|
+
*/
|
|
737
|
+
function parseKiroPermissionPolicy(filePath, raw, framework) {
|
|
738
|
+
const component = createComponent(
|
|
739
|
+
filePath,
|
|
740
|
+
framework || frameworkForFile(filePath, raw),
|
|
741
|
+
"permission-policy",
|
|
742
|
+
basename(filePath),
|
|
743
|
+
"file",
|
|
744
|
+
);
|
|
745
|
+
addKiroPermissionProperties(component.properties, parseYamlSafe(raw));
|
|
746
|
+
maybeAddFileSignals(component.properties, filePath, raw);
|
|
747
|
+
return [component];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Hook triggers and action types of the Kiro hook schema shared by the CLI
|
|
751
|
+
// and the IDE, kept as allowlisted enums so a manifest never contributes free
|
|
752
|
+
// text. Matcher values, command strings, and prompts are never emitted.
|
|
753
|
+
const KIRO_HOOK_EVENTS = new Map(
|
|
754
|
+
[
|
|
755
|
+
"PostFileCreate",
|
|
756
|
+
"PostFileDelete",
|
|
757
|
+
"PostFileSave",
|
|
758
|
+
"PostTaskExec",
|
|
759
|
+
"PostToolUse",
|
|
760
|
+
"PreTaskExec",
|
|
761
|
+
"PreToolUse",
|
|
762
|
+
"SessionStart",
|
|
763
|
+
"Stop",
|
|
764
|
+
"UserPromptSubmit",
|
|
765
|
+
].map((event) => [event.toLowerCase(), event]),
|
|
766
|
+
);
|
|
767
|
+
const KIRO_HOOK_ACTION_TYPES = new Map(
|
|
768
|
+
["agent", "command"].map((actionType) => [
|
|
769
|
+
actionType.toLowerCase(),
|
|
770
|
+
actionType,
|
|
771
|
+
]),
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Parse a Kiro hook file (`.kiro/hooks/*.json` and hook scripts) into a file
|
|
776
|
+
* component. JSON manifests contribute the allowlisted lifecycle events,
|
|
777
|
+
* allowlisted action types, and a hook count; scripts contribute the file
|
|
778
|
+
* signals only.
|
|
779
|
+
*
|
|
780
|
+
* @param {string} filePath Hook file path
|
|
781
|
+
* @param {string} raw File contents
|
|
782
|
+
* @returns {Object[]} Component list
|
|
783
|
+
*/
|
|
784
|
+
function parseKiroHookFile(filePath, raw, framework) {
|
|
785
|
+
const component = createComponent(
|
|
786
|
+
filePath,
|
|
787
|
+
framework || frameworkForFile(filePath, raw),
|
|
788
|
+
"hook-file",
|
|
789
|
+
basename(filePath),
|
|
790
|
+
"file",
|
|
791
|
+
);
|
|
792
|
+
if (basename(filePath).toLowerCase().endsWith(".json")) {
|
|
793
|
+
const manifest = parseJsonLikeSafe(raw);
|
|
794
|
+
const hooks = Array.isArray(manifest?.hooks)
|
|
795
|
+
? manifest.hooks.filter(
|
|
796
|
+
(hook) => hook !== null && typeof hook === "object",
|
|
797
|
+
)
|
|
798
|
+
: [];
|
|
799
|
+
addUniqueProperty(
|
|
800
|
+
component.properties,
|
|
801
|
+
"cdx:agent:hookCount",
|
|
802
|
+
hooks.length,
|
|
803
|
+
);
|
|
804
|
+
const events = new Set();
|
|
805
|
+
const actionTypes = new Set();
|
|
806
|
+
for (const hook of hooks) {
|
|
807
|
+
if (typeof hook.trigger === "string") {
|
|
808
|
+
const event = KIRO_HOOK_EVENTS.get(hook.trigger.toLowerCase());
|
|
809
|
+
if (event) {
|
|
810
|
+
events.add(event);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (typeof hook.action?.type === "string") {
|
|
814
|
+
const actionType = KIRO_HOOK_ACTION_TYPES.get(
|
|
815
|
+
hook.action.type.toLowerCase(),
|
|
816
|
+
);
|
|
817
|
+
if (actionType) {
|
|
818
|
+
actionTypes.add(actionType);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
addUniqueProperty(
|
|
823
|
+
component.properties,
|
|
824
|
+
"cdx:agent:hookEvents",
|
|
825
|
+
[...events].sort().join(","),
|
|
826
|
+
);
|
|
827
|
+
addUniqueProperty(
|
|
828
|
+
component.properties,
|
|
829
|
+
"cdx:agent:hookActionTypes",
|
|
830
|
+
[...actionTypes].sort().join(","),
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
maybeAddFileSignals(component.properties, filePath, raw);
|
|
834
|
+
return [component];
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Parse a Kiro workflow file (`.kiro/workflows/*`) into a file component.
|
|
839
|
+
* Workflow contents are never copied.
|
|
840
|
+
*
|
|
841
|
+
* @param {string} filePath Workflow file path
|
|
842
|
+
* @param {string} raw File contents
|
|
843
|
+
* @returns {Object[]} Component list
|
|
844
|
+
*/
|
|
845
|
+
function parseKiroWorkflowFile(filePath, raw, framework) {
|
|
846
|
+
const component = createComponent(
|
|
847
|
+
filePath,
|
|
848
|
+
framework || frameworkForFile(filePath, raw),
|
|
849
|
+
"workflow-file",
|
|
850
|
+
basename(filePath),
|
|
851
|
+
"file",
|
|
852
|
+
);
|
|
853
|
+
maybeAddFileSignals(component.properties, filePath, raw);
|
|
854
|
+
return [component];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Parse a Kiro CLI settings file (`settings/cli.json`) into a file component.
|
|
859
|
+
* Only the default agent name and a count of boolean feature flags are
|
|
860
|
+
* recorded; every other value stays out of the BOM.
|
|
861
|
+
*
|
|
862
|
+
* @param {string} filePath Settings file path
|
|
863
|
+
* @param {string} raw File contents
|
|
864
|
+
* @returns {Object[]} Component list
|
|
865
|
+
*/
|
|
866
|
+
function parseKiroSettings(filePath, raw, framework) {
|
|
867
|
+
const settings = parseJsonLikeSafe(raw);
|
|
868
|
+
if (!settings) {
|
|
869
|
+
return [];
|
|
870
|
+
}
|
|
871
|
+
const component = createComponent(
|
|
872
|
+
filePath,
|
|
873
|
+
framework || frameworkForFile(filePath, raw),
|
|
874
|
+
"settings",
|
|
875
|
+
basename(filePath),
|
|
876
|
+
"file",
|
|
877
|
+
);
|
|
878
|
+
if (typeof settings["chat.defaultAgent"] === "string") {
|
|
879
|
+
addUniqueProperty(
|
|
880
|
+
component.properties,
|
|
881
|
+
"cdx:agent:defaultAgent",
|
|
882
|
+
settings["chat.defaultAgent"],
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const booleanFlagCount = Object.keys(settings).filter(
|
|
886
|
+
(key) => typeof settings[key] === "boolean",
|
|
887
|
+
).length;
|
|
888
|
+
if (booleanFlagCount) {
|
|
889
|
+
addUniqueProperty(
|
|
890
|
+
component.properties,
|
|
891
|
+
"cdx:agent:booleanFeatureFlagCount",
|
|
892
|
+
booleanFlagCount,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
maybeAddFileSignals(component.properties, filePath, raw);
|
|
896
|
+
return [component];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Route one Kiro file (under `.kiro` or in a validated Kiro configuration
|
|
901
|
+
* root) to its parser. Files outside these shapes return no components. A
|
|
902
|
+
* validated configuration root names its framework explicitly, because the
|
|
903
|
+
* individual file content of a steering or hook document cannot identify it.
|
|
904
|
+
*
|
|
905
|
+
* @param {string} filePath File path
|
|
906
|
+
* @param {string} raw File contents
|
|
907
|
+
* @param {string} [framework] Framework override for validated root files
|
|
908
|
+
* @returns {Object[]} Component list
|
|
909
|
+
*/
|
|
910
|
+
function parseKiroLayoutFile(filePath, raw, framework) {
|
|
911
|
+
const resolvedFramework = framework || frameworkForFile(filePath, raw);
|
|
912
|
+
const lowerPath = normalizeFilePath(filePath).toLowerCase();
|
|
913
|
+
if (lowerPath.endsWith("/skill.md")) {
|
|
914
|
+
return parseSkillFile(filePath, raw);
|
|
915
|
+
}
|
|
916
|
+
if (lowerPath.endsWith(".json") && inNamedDir(lowerPath, "agents")) {
|
|
917
|
+
return parseKiroAgentJson(filePath, raw);
|
|
918
|
+
}
|
|
919
|
+
if (inNamedDir(lowerPath, "agents")) {
|
|
920
|
+
return parseAgentMarkdown(filePath, raw);
|
|
921
|
+
}
|
|
922
|
+
if (lowerPath.endsWith(".md") && inNamedDir(lowerPath, "specs")) {
|
|
923
|
+
return parseInstructionMarkdown(
|
|
924
|
+
filePath,
|
|
925
|
+
raw,
|
|
926
|
+
"agent-spec",
|
|
927
|
+
resolvedFramework,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (lowerPath.endsWith(".md") && inNamedDir(lowerPath, "steering")) {
|
|
931
|
+
return parseInstructionMarkdown(
|
|
932
|
+
filePath,
|
|
933
|
+
raw,
|
|
934
|
+
undefined,
|
|
935
|
+
resolvedFramework,
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (lowerPath.endsWith("/permissions.yaml")) {
|
|
939
|
+
return parseKiroPermissionPolicy(filePath, raw, resolvedFramework);
|
|
940
|
+
}
|
|
941
|
+
if (lowerPath.endsWith("/cli.json")) {
|
|
942
|
+
return parseKiroSettings(filePath, raw, resolvedFramework);
|
|
943
|
+
}
|
|
944
|
+
if (inNamedDir(lowerPath, "hooks")) {
|
|
945
|
+
return parseKiroHookFile(filePath, raw, resolvedFramework);
|
|
946
|
+
}
|
|
947
|
+
if (inNamedDir(lowerPath, "workflows")) {
|
|
948
|
+
return parseKiroWorkflowFile(filePath, raw, resolvedFramework);
|
|
949
|
+
}
|
|
950
|
+
return [];
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Pick up the steering, settings, and hook files of a validated Kiro
|
|
955
|
+
* configuration root. Files the parser already received are skipped, so a
|
|
956
|
+
* settings file claimed by its own glob is never parsed twice.
|
|
957
|
+
*
|
|
958
|
+
* @param {string} rootDir Validated Kiro configuration root
|
|
959
|
+
* @param {Set<string>} inputKeys Lower-cased paths already being parsed
|
|
960
|
+
* @param {Object[]} components Component list to append to
|
|
961
|
+
*/
|
|
962
|
+
function expandKiroConfigurationRoot(rootDir, inputKeys, components) {
|
|
963
|
+
const candidates = [
|
|
964
|
+
...listFileNames(join(rootDir, "settings")).map((name) =>
|
|
965
|
+
join(rootDir, "settings", name),
|
|
966
|
+
),
|
|
967
|
+
...listFileNames(join(rootDir, "steering")).map((name) =>
|
|
968
|
+
join(rootDir, "steering", name),
|
|
969
|
+
),
|
|
970
|
+
...listFileNames(join(rootDir, "hooks")).map((name) =>
|
|
971
|
+
join(rootDir, "hooks", name),
|
|
972
|
+
),
|
|
973
|
+
];
|
|
974
|
+
for (const candidate of candidates) {
|
|
975
|
+
const lowerCandidate = normalizeFilePath(candidate).toLowerCase();
|
|
976
|
+
if (inputKeys.has(lowerCandidate)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const isLayoutFile =
|
|
980
|
+
lowerCandidate.endsWith("/settings/permissions.yaml") ||
|
|
981
|
+
lowerCandidate.endsWith("/settings/cli.json") ||
|
|
982
|
+
(lowerCandidate.endsWith(".md") &&
|
|
983
|
+
inNamedDir(lowerCandidate, "steering")) ||
|
|
984
|
+
(lowerCandidate.endsWith(".json") && inNamedDir(lowerCandidate, "hooks"));
|
|
985
|
+
if (!isLayoutFile) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
let raw;
|
|
989
|
+
try {
|
|
990
|
+
raw = readFileSync(candidate, "utf-8");
|
|
991
|
+
} catch {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
components.push(...parseKiroLayoutFile(candidate, raw, "kiro"));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
489
998
|
function parseNanocoderCommand(filePath, raw) {
|
|
490
999
|
const { metadata } = parseFrontmatter(raw);
|
|
491
1000
|
const component = createComponent(
|
|
@@ -535,8 +1044,8 @@ function parseNanocoderCommand(filePath, raw) {
|
|
|
535
1044
|
return [component];
|
|
536
1045
|
}
|
|
537
1046
|
|
|
538
|
-
function parseInstructionMarkdown(filePath, raw) {
|
|
539
|
-
const
|
|
1047
|
+
function parseInstructionMarkdown(filePath, raw, kindOverride, framework) {
|
|
1048
|
+
const resolvedFramework = framework || frameworkForFile(filePath, raw);
|
|
540
1049
|
const { body, metadata } = parseFrontmatter(raw);
|
|
541
1050
|
const lowerPath = normalizeFilePath(filePath).toLowerCase();
|
|
542
1051
|
const headingMatch = body.match(/^#\s+(.+)$/mu);
|
|
@@ -547,12 +1056,20 @@ function parseInstructionMarkdown(filePath, raw) {
|
|
|
547
1056
|
if (!name || !body.trim()) {
|
|
548
1057
|
return [];
|
|
549
1058
|
}
|
|
550
|
-
const kind =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
? "
|
|
554
|
-
: "
|
|
555
|
-
|
|
1059
|
+
const kind =
|
|
1060
|
+
kindOverride ||
|
|
1061
|
+
(lowerPath.includes("/commands/")
|
|
1062
|
+
? "custom-command"
|
|
1063
|
+
: lowerPath.includes("/skills/")
|
|
1064
|
+
? "skill-file"
|
|
1065
|
+
: "agent-instructions");
|
|
1066
|
+
const component = createComponent(
|
|
1067
|
+
filePath,
|
|
1068
|
+
resolvedFramework,
|
|
1069
|
+
kind,
|
|
1070
|
+
name,
|
|
1071
|
+
"file",
|
|
1072
|
+
);
|
|
556
1073
|
addUniqueProperty(
|
|
557
1074
|
component.properties,
|
|
558
1075
|
"cdx:agent:description",
|
|
@@ -772,18 +1289,405 @@ function parseCrewAiYaml(filePath, raw) {
|
|
|
772
1289
|
return components;
|
|
773
1290
|
}
|
|
774
1291
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
]
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1292
|
+
// Keyword arguments of a CrewAI Agent(...) or Task(...) call that cdxgen
|
|
1293
|
+
// records, with the property each one becomes.
|
|
1294
|
+
const CREWAI_TEXT_ARGUMENTS = [
|
|
1295
|
+
["role", "cdx:crewai:role"],
|
|
1296
|
+
["goal", "cdx:crewai:goal"],
|
|
1297
|
+
["backstory", "cdx:crewai:backstory"],
|
|
1298
|
+
["description", "cdx:crewai:description"],
|
|
1299
|
+
["expected_output", "cdx:crewai:expectedOutput"],
|
|
1300
|
+
];
|
|
1301
|
+
|
|
1302
|
+
function isPythonBlank(character) {
|
|
1303
|
+
return (
|
|
1304
|
+
character === " " ||
|
|
1305
|
+
character === "\t" ||
|
|
1306
|
+
character === "\r" ||
|
|
1307
|
+
character === "\f" ||
|
|
1308
|
+
character === "\n"
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function isPythonNameCharacter(character) {
|
|
1313
|
+
return (
|
|
1314
|
+
character !== undefined &&
|
|
1315
|
+
((character >= "a" && character <= "z") ||
|
|
1316
|
+
(character >= "A" && character <= "Z") ||
|
|
1317
|
+
(character >= "0" && character <= "9") ||
|
|
1318
|
+
character === "_")
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Find the end of the Python string literal whose opening quote is at
|
|
1324
|
+
* `index`. A single-quoted literal also ends at a newline, where Python
|
|
1325
|
+
* would reject it.
|
|
1326
|
+
*
|
|
1327
|
+
* @param {string} text Python source
|
|
1328
|
+
* @param {number} index offset of the opening quote
|
|
1329
|
+
* @param {number} limit offset the literal may not reach past
|
|
1330
|
+
* @returns {{contentStart: number, contentEnd: number, end: number}} offsets
|
|
1331
|
+
* of the literal's content and of the first character after it
|
|
1332
|
+
*/
|
|
1333
|
+
function pythonStringSpan(text, index, limit) {
|
|
1334
|
+
const quote = text[index];
|
|
1335
|
+
const delimiter = text.startsWith(quote.repeat(3), index)
|
|
1336
|
+
? quote.repeat(3)
|
|
1337
|
+
: quote;
|
|
1338
|
+
const contentStart = index + delimiter.length;
|
|
1339
|
+
let cursor = contentStart;
|
|
1340
|
+
while (cursor < limit) {
|
|
1341
|
+
if (text[cursor] === "\\") {
|
|
1342
|
+
cursor += 2;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (delimiter.length === 1 && text[cursor] === "\n") {
|
|
1346
|
+
return { contentEnd: cursor, contentStart, end: cursor };
|
|
1347
|
+
}
|
|
1348
|
+
if (text.startsWith(delimiter, cursor)) {
|
|
1349
|
+
return {
|
|
1350
|
+
contentEnd: cursor,
|
|
1351
|
+
contentStart,
|
|
1352
|
+
end: cursor + delimiter.length,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
cursor += 1;
|
|
1356
|
+
}
|
|
1357
|
+
return { contentEnd: limit, contentStart, end: limit };
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Split Python source into logical lines: a statement together with the
|
|
1362
|
+
* lines it continues onto inside brackets, a triple-quoted string or after a
|
|
1363
|
+
* backslash. Blank and comment-only lines are left out. Each character is
|
|
1364
|
+
* read once, so the cost is linear in the size of the source.
|
|
1365
|
+
*
|
|
1366
|
+
* @param {string} raw Python source
|
|
1367
|
+
* @returns {{start: number, end: number, indent: number}[]} offsets of each
|
|
1368
|
+
* logical line's first non-blank character and of its end, and the width
|
|
1369
|
+
* of its indentation
|
|
1370
|
+
*/
|
|
1371
|
+
function pythonLogicalLines(raw) {
|
|
1372
|
+
const lines = [];
|
|
1373
|
+
let depth = 0;
|
|
1374
|
+
let current;
|
|
1375
|
+
let cursor = 0;
|
|
1376
|
+
while (cursor < raw.length) {
|
|
1377
|
+
if (!current) {
|
|
1378
|
+
let indent = 0;
|
|
1379
|
+
while (raw[cursor] === " " || raw[cursor] === "\t") {
|
|
1380
|
+
indent += 1;
|
|
1381
|
+
cursor += 1;
|
|
1382
|
+
}
|
|
1383
|
+
const first = raw[cursor];
|
|
1384
|
+
if (first === undefined) {
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
if (first === "\n" || first === "\r" || first === "#") {
|
|
1388
|
+
const newline = raw.indexOf("\n", cursor);
|
|
1389
|
+
cursor = newline === -1 ? raw.length : newline + 1;
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
current = { end: raw.length, indent, start: cursor };
|
|
1393
|
+
depth = 0;
|
|
1394
|
+
}
|
|
1395
|
+
const character = raw[cursor];
|
|
1396
|
+
if (character === '"' || character === "'") {
|
|
1397
|
+
cursor = pythonStringSpan(raw, cursor, raw.length).end;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (character === "#") {
|
|
1401
|
+
const newline = raw.indexOf("\n", cursor);
|
|
1402
|
+
cursor = newline === -1 ? raw.length : newline;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (character === "\\") {
|
|
1406
|
+
cursor += 2;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (character === "(" || character === "[" || character === "{") {
|
|
1410
|
+
depth += 1;
|
|
1411
|
+
} else if (character === ")" || character === "]" || character === "}") {
|
|
1412
|
+
depth = Math.max(0, depth - 1);
|
|
1413
|
+
} else if (character === "\n" && depth === 0) {
|
|
1414
|
+
current.end = cursor;
|
|
1415
|
+
lines.push(current);
|
|
1416
|
+
current = undefined;
|
|
1417
|
+
}
|
|
1418
|
+
cursor += 1;
|
|
1419
|
+
}
|
|
1420
|
+
if (current) {
|
|
1421
|
+
lines.push(current);
|
|
1422
|
+
}
|
|
1423
|
+
return lines;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function skipPythonSpaces(raw, cursor, end) {
|
|
1427
|
+
let position = cursor;
|
|
1428
|
+
while (position < end && (raw[position] === " " || raw[position] === "\t")) {
|
|
1429
|
+
position += 1;
|
|
1430
|
+
}
|
|
1431
|
+
return position;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Read the function name from a logical line that starts with `def name(`
|
|
1436
|
+
* or `async def name(`.
|
|
1437
|
+
*
|
|
1438
|
+
* @param {string} raw Python source
|
|
1439
|
+
* @param {{start: number, end: number}} line logical line
|
|
1440
|
+
* @returns {string|undefined} function name
|
|
1441
|
+
*/
|
|
1442
|
+
function pythonFunctionName(raw, line) {
|
|
1443
|
+
let cursor = line.start;
|
|
1444
|
+
if (raw.startsWith("async", cursor) && isPythonBlank(raw[cursor + 5])) {
|
|
1445
|
+
cursor = skipPythonSpaces(raw, cursor + 5, line.end);
|
|
1446
|
+
}
|
|
1447
|
+
if (!raw.startsWith("def", cursor) || !isPythonBlank(raw[cursor + 3])) {
|
|
1448
|
+
return undefined;
|
|
1449
|
+
}
|
|
1450
|
+
cursor = skipPythonSpaces(raw, cursor + 3, line.end);
|
|
1451
|
+
const nameStart = cursor;
|
|
1452
|
+
while (cursor < line.end && isPythonNameCharacter(raw[cursor])) {
|
|
1453
|
+
cursor += 1;
|
|
1454
|
+
}
|
|
1455
|
+
if (cursor === nameStart) {
|
|
1456
|
+
return undefined;
|
|
1457
|
+
}
|
|
1458
|
+
const name = raw.slice(nameStart, cursor);
|
|
1459
|
+
cursor = skipPythonSpaces(raw, cursor, line.end);
|
|
1460
|
+
return raw[cursor] === "(" ? name : undefined;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Find the opening parenthesis of `return <constructor>(` at the start of a
|
|
1465
|
+
* logical line.
|
|
1466
|
+
*
|
|
1467
|
+
* @param {string} raw Python source
|
|
1468
|
+
* @param {{start: number, end: number}} line logical line
|
|
1469
|
+
* @param {string} constructorName Agent or Task
|
|
1470
|
+
* @returns {number} offset of the parenthesis, or -1
|
|
1471
|
+
*/
|
|
1472
|
+
function returnedCallStart(raw, line, constructorName) {
|
|
1473
|
+
if (!raw.startsWith("return", line.start)) {
|
|
1474
|
+
return -1;
|
|
1475
|
+
}
|
|
1476
|
+
let cursor = line.start + 6;
|
|
1477
|
+
if (!isPythonBlank(raw[cursor])) {
|
|
1478
|
+
return -1;
|
|
1479
|
+
}
|
|
1480
|
+
cursor = skipPythonSpaces(raw, cursor, line.end);
|
|
1481
|
+
if (!raw.startsWith(constructorName, cursor)) {
|
|
1482
|
+
return -1;
|
|
1483
|
+
}
|
|
1484
|
+
cursor = skipPythonSpaces(raw, cursor + constructorName.length, line.end);
|
|
1485
|
+
return raw[cursor] === "(" ? cursor : -1;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Read the arguments of a call, from just after its opening parenthesis to
|
|
1490
|
+
* the matching closing one, or to `limit` when it never closes.
|
|
1491
|
+
*
|
|
1492
|
+
* @param {string} raw Python source
|
|
1493
|
+
* @param {number} openParen offset of the opening parenthesis
|
|
1494
|
+
* @param {number} limit end of the logical line holding the call
|
|
1495
|
+
* @returns {string} argument text
|
|
1496
|
+
*/
|
|
1497
|
+
function pythonCallArguments(raw, openParen, limit) {
|
|
1498
|
+
let depth = 1;
|
|
1499
|
+
let cursor = openParen + 1;
|
|
1500
|
+
while (cursor < limit) {
|
|
1501
|
+
const character = raw[cursor];
|
|
1502
|
+
if (character === '"' || character === "'") {
|
|
1503
|
+
cursor = pythonStringSpan(raw, cursor, limit).end;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (character === "(" || character === "[" || character === "{") {
|
|
1507
|
+
depth += 1;
|
|
1508
|
+
} else if (character === ")" || character === "]" || character === "}") {
|
|
1509
|
+
depth -= 1;
|
|
1510
|
+
if (depth === 0) {
|
|
1511
|
+
return raw.slice(openParen + 1, cursor);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
cursor += 1;
|
|
1515
|
+
}
|
|
1516
|
+
return raw.slice(openParen + 1, limit);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Split text on the commas that are outside brackets and strings.
|
|
1521
|
+
*
|
|
1522
|
+
* @param {string} text argument or list text
|
|
1523
|
+
* @returns {string[]} trimmed, non-empty parts
|
|
1524
|
+
*/
|
|
1525
|
+
function splitPythonTopLevel(text) {
|
|
1526
|
+
const parts = [];
|
|
1527
|
+
let depth = 0;
|
|
1528
|
+
let partStart = 0;
|
|
1529
|
+
let cursor = 0;
|
|
1530
|
+
while (cursor < text.length) {
|
|
1531
|
+
const character = text[cursor];
|
|
1532
|
+
if (character === '"' || character === "'") {
|
|
1533
|
+
cursor = pythonStringSpan(text, cursor, text.length).end;
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (character === "(" || character === "[" || character === "{") {
|
|
1537
|
+
depth += 1;
|
|
1538
|
+
} else if (character === ")" || character === "]" || character === "}") {
|
|
1539
|
+
depth = Math.max(0, depth - 1);
|
|
1540
|
+
} else if (character === "," && depth === 0) {
|
|
1541
|
+
parts.push(text.slice(partStart, cursor));
|
|
1542
|
+
partStart = cursor + 1;
|
|
1543
|
+
}
|
|
1544
|
+
cursor += 1;
|
|
1545
|
+
}
|
|
1546
|
+
parts.push(text.slice(partStart));
|
|
1547
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Map each `name=value` keyword argument of a call to its value text.
|
|
1552
|
+
*
|
|
1553
|
+
* @param {string} argumentText text between the call's parentheses
|
|
1554
|
+
* @returns {Map<string, string>} keyword argument values, first one wins
|
|
1555
|
+
*/
|
|
1556
|
+
function pythonKeywordArguments(argumentText) {
|
|
1557
|
+
const keywordArguments = new Map();
|
|
1558
|
+
for (const argument of splitPythonTopLevel(argumentText)) {
|
|
1559
|
+
const equals = argument.indexOf("=");
|
|
1560
|
+
if (equals <= 0 || argument[equals + 1] === "=") {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
const name = argument.slice(0, equals).trim();
|
|
1564
|
+
if (
|
|
1565
|
+
!name ||
|
|
1566
|
+
![...name].every(isPythonNameCharacter) ||
|
|
1567
|
+
keywordArguments.has(name)
|
|
1568
|
+
) {
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
keywordArguments.set(name, argument.slice(equals + 1).trim());
|
|
1572
|
+
}
|
|
1573
|
+
return keywordArguments;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function unwrapPythonCall(value, prefixes) {
|
|
1577
|
+
for (const prefix of prefixes) {
|
|
1578
|
+
if (value.startsWith(prefix) && value.endsWith(")")) {
|
|
1579
|
+
return value.slice(prefix.length, -1).trim();
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return value;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Read the text of a keyword argument whose value is a string literal, a run
|
|
1587
|
+
* of adjacent literals, or either of those inside parentheses or
|
|
1588
|
+
* `dedent(...)`. Other values, such as names or calls, give undefined.
|
|
1589
|
+
*
|
|
1590
|
+
* @param {string|undefined} value keyword argument value text
|
|
1591
|
+
* @returns {string|undefined} literal text with whitespace collapsed
|
|
1592
|
+
*/
|
|
1593
|
+
function pythonStringValue(value) {
|
|
1594
|
+
if (!value) {
|
|
1595
|
+
return undefined;
|
|
1596
|
+
}
|
|
1597
|
+
let text = unwrapPythonCall(value, ["textwrap.dedent(", "dedent("]);
|
|
1598
|
+
if (text.startsWith("(") && text.endsWith(")")) {
|
|
1599
|
+
text = text.slice(1, -1).trim();
|
|
1600
|
+
}
|
|
1601
|
+
const parts = [];
|
|
1602
|
+
let cursor = 0;
|
|
1603
|
+
while (cursor < text.length) {
|
|
1604
|
+
while (cursor < text.length && isPythonBlank(text[cursor])) {
|
|
1605
|
+
cursor += 1;
|
|
1606
|
+
}
|
|
1607
|
+
let quoteAt = cursor;
|
|
1608
|
+
while (quoteAt - cursor < 2 && "rRbBuUfF".includes(text[quoteAt] || "-")) {
|
|
1609
|
+
quoteAt += 1;
|
|
1610
|
+
}
|
|
1611
|
+
if (text[quoteAt] !== '"' && text[quoteAt] !== "'") {
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
const isRaw = text.slice(cursor, quoteAt).toLowerCase().includes("r");
|
|
1615
|
+
const span = pythonStringSpan(text, quoteAt, text.length);
|
|
1616
|
+
const content = text.slice(span.contentStart, span.contentEnd);
|
|
1617
|
+
parts.push(
|
|
1618
|
+
isRaw
|
|
1619
|
+
? content
|
|
1620
|
+
: content.replace(/\\(.)/gsu, (_match, escaped) =>
|
|
1621
|
+
escaped === "n" || escaped === "t" || escaped === "r"
|
|
1622
|
+
? " "
|
|
1623
|
+
: escaped,
|
|
1624
|
+
),
|
|
1625
|
+
);
|
|
1626
|
+
cursor = span.end;
|
|
1627
|
+
}
|
|
1628
|
+
const joined = parts.join("").replace(/\s+/gu, " ").trim();
|
|
1629
|
+
return joined || undefined;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Read a list literal of tools as a comma-separated string.
|
|
1634
|
+
*
|
|
1635
|
+
* @param {string|undefined} value keyword argument value text
|
|
1636
|
+
* @returns {string|undefined} tool expressions without whitespace
|
|
1637
|
+
*/
|
|
1638
|
+
function pythonListValue(value) {
|
|
1639
|
+
if (!value?.startsWith("[") || !value.endsWith("]")) {
|
|
1640
|
+
return undefined;
|
|
1641
|
+
}
|
|
1642
|
+
const items = splitPythonTopLevel(value.slice(1, -1)).map((item) =>
|
|
1643
|
+
item.replace(/\s+/gu, ""),
|
|
1644
|
+
);
|
|
1645
|
+
return items.length ? items.join(",") : undefined;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Find the functions in a CrewAI Python module that return an Agent or a
|
|
1650
|
+
* Task, with the arguments of that call. A function's body is the run of
|
|
1651
|
+
* logical lines indented deeper than its `def`, and the first matching
|
|
1652
|
+
* return statement in it belongs to the innermost function. The source is
|
|
1653
|
+
* read in one pass and each call's arguments only within their own logical
|
|
1654
|
+
* line, so crafted input cannot make the work grow faster than its size.
|
|
1655
|
+
*
|
|
1656
|
+
* @param {string} raw Python source
|
|
1657
|
+
* @param {string} constructorName Agent or Task
|
|
1658
|
+
* @returns {{name: string, argumentText: string}[]} returned calls in order
|
|
1659
|
+
*/
|
|
1660
|
+
function crewAiReturnedCalls(raw, constructorName) {
|
|
1661
|
+
const openFunctions = [];
|
|
1662
|
+
const calls = [];
|
|
1663
|
+
for (const line of pythonLogicalLines(raw)) {
|
|
1664
|
+
while (openFunctions.length && openFunctions.at(-1).indent >= line.indent) {
|
|
1665
|
+
openFunctions.pop();
|
|
784
1666
|
}
|
|
1667
|
+
const functionName = pythonFunctionName(raw, line);
|
|
1668
|
+
if (functionName) {
|
|
1669
|
+
openFunctions.push({
|
|
1670
|
+
indent: line.indent,
|
|
1671
|
+
name: functionName,
|
|
1672
|
+
returned: false,
|
|
1673
|
+
});
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
const innermost = openFunctions.at(-1);
|
|
1677
|
+
if (!innermost || innermost.returned) {
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
const openParen = returnedCallStart(raw, line, constructorName);
|
|
1681
|
+
if (openParen === -1) {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
innermost.returned = true;
|
|
1685
|
+
calls.push({
|
|
1686
|
+
argumentText: pythonCallArguments(raw, openParen, line.end),
|
|
1687
|
+
name: innermost.name,
|
|
1688
|
+
});
|
|
785
1689
|
}
|
|
786
|
-
return
|
|
1690
|
+
return calls;
|
|
787
1691
|
}
|
|
788
1692
|
|
|
789
1693
|
function parseCrewAiPython(filePath, raw) {
|
|
@@ -792,61 +1696,25 @@ function parseCrewAiPython(filePath, raw) {
|
|
|
792
1696
|
}
|
|
793
1697
|
const isTaskFile = /\bfrom\s+crewai\s+import\s+Task\b/u.test(raw);
|
|
794
1698
|
const constructorName = isTaskFile ? "Task" : "Agent";
|
|
795
|
-
const
|
|
796
|
-
...raw.matchAll(
|
|
797
|
-
new RegExp(
|
|
798
|
-
`def\\s+([A-Za-z0-9_]+)\\s*\\([^)]*\\):[\\s\\S]*?return\\s+${constructorName}\\(([\\s\\S]*?)\\n\\s*\\)`,
|
|
799
|
-
"gu",
|
|
800
|
-
),
|
|
801
|
-
),
|
|
802
|
-
];
|
|
1699
|
+
const kind = isTaskFile ? "crew-task" : "crew-agent";
|
|
803
1700
|
const components = [];
|
|
804
|
-
for (const
|
|
805
|
-
const component = createComponent(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
match[1],
|
|
810
|
-
);
|
|
811
|
-
addUniqueProperty(
|
|
812
|
-
component.properties,
|
|
813
|
-
"cdx:agent:role",
|
|
814
|
-
isTaskFile ? "crew-task" : "crew-agent",
|
|
815
|
-
);
|
|
816
|
-
addUniqueProperty(
|
|
817
|
-
component.properties,
|
|
818
|
-
"cdx:crewai:role",
|
|
819
|
-
captureQuotedValue(match[2], "role"),
|
|
820
|
-
);
|
|
821
|
-
addUniqueProperty(
|
|
822
|
-
component.properties,
|
|
823
|
-
"cdx:crewai:goal",
|
|
824
|
-
captureQuotedValue(match[2], "goal"),
|
|
825
|
-
);
|
|
826
|
-
addUniqueProperty(
|
|
827
|
-
component.properties,
|
|
828
|
-
"cdx:crewai:backstory",
|
|
829
|
-
captureQuotedValue(match[2], "backstory"),
|
|
830
|
-
);
|
|
831
|
-
addUniqueProperty(
|
|
832
|
-
component.properties,
|
|
833
|
-
"cdx:crewai:description",
|
|
834
|
-
captureQuotedValue(match[2], "description"),
|
|
835
|
-
);
|
|
836
|
-
addUniqueProperty(
|
|
837
|
-
component.properties,
|
|
838
|
-
"cdx:crewai:expectedOutput",
|
|
839
|
-
captureQuotedValue(match[2], "expected_output"),
|
|
840
|
-
);
|
|
841
|
-
const toolsMatch = match[2].match(/tools\s*=\s*\[([^\]]+)\]/u);
|
|
842
|
-
if (toolsMatch?.[1]) {
|
|
1701
|
+
for (const call of crewAiReturnedCalls(raw, constructorName)) {
|
|
1702
|
+
const component = createComponent(filePath, "crewai", kind, call.name);
|
|
1703
|
+
addUniqueProperty(component.properties, "cdx:agent:role", kind);
|
|
1704
|
+
const keywordArguments = pythonKeywordArguments(call.argumentText);
|
|
1705
|
+
for (const [argumentName, propertyName] of CREWAI_TEXT_ARGUMENTS) {
|
|
843
1706
|
addUniqueProperty(
|
|
844
1707
|
component.properties,
|
|
845
|
-
|
|
846
|
-
|
|
1708
|
+
propertyName,
|
|
1709
|
+
pythonStringValue(keywordArguments.get(argumentName)),
|
|
847
1710
|
);
|
|
848
1711
|
}
|
|
849
|
-
|
|
1712
|
+
addUniqueProperty(
|
|
1713
|
+
component.properties,
|
|
1714
|
+
"cdx:crewai:tools",
|
|
1715
|
+
pythonListValue(keywordArguments.get("tools")),
|
|
1716
|
+
);
|
|
1717
|
+
maybeAddFileSignals(component.properties, filePath, call.argumentText);
|
|
850
1718
|
components.push(component);
|
|
851
1719
|
}
|
|
852
1720
|
return components;
|
|
@@ -870,6 +1738,12 @@ export const communityAiConfigParser = {
|
|
|
870
1738
|
parse(files, _options = {}) {
|
|
871
1739
|
const components = [];
|
|
872
1740
|
const services = [];
|
|
1741
|
+
const inputKeys = new Set(
|
|
1742
|
+
(files || []).map((filePath) =>
|
|
1743
|
+
normalizeFilePath(filePath).toLowerCase(),
|
|
1744
|
+
),
|
|
1745
|
+
);
|
|
1746
|
+
const expandedKiroRoots = new Set();
|
|
873
1747
|
for (const filePath of [...new Set(files || [])]) {
|
|
874
1748
|
let raw;
|
|
875
1749
|
try {
|
|
@@ -878,6 +1752,30 @@ export const communityAiConfigParser = {
|
|
|
878
1752
|
continue;
|
|
879
1753
|
}
|
|
880
1754
|
const lowerPath = normalizeFilePath(filePath).toLowerCase();
|
|
1755
|
+
// Kiro files under .kiro are all handled by the Kiro parsers, so the
|
|
1756
|
+
// generic community branches below never see them.
|
|
1757
|
+
if (lowerPath.includes("/.kiro/")) {
|
|
1758
|
+
components.push(...parseKiroLayoutFile(filePath, raw));
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
// A settings file outside .kiro is a Kiro configuration root candidate.
|
|
1762
|
+
// The root is validated against the Kiro content shapes, and only a
|
|
1763
|
+
// validated root contributes its steering, settings, and hook files.
|
|
1764
|
+
if (
|
|
1765
|
+
lowerPath.endsWith("/settings/permissions.yaml") ||
|
|
1766
|
+
lowerPath.endsWith("/settings/cli.json")
|
|
1767
|
+
) {
|
|
1768
|
+
const rootDir = dirname(dirname(filePath));
|
|
1769
|
+
const rootKey = normalizeFilePath(rootDir).toLowerCase();
|
|
1770
|
+
if (isKiroConfigurationRoot(rootDir)) {
|
|
1771
|
+
components.push(...parseKiroLayoutFile(filePath, raw));
|
|
1772
|
+
if (!expandedKiroRoots.has(rootKey)) {
|
|
1773
|
+
expandedKiroRoots.add(rootKey);
|
|
1774
|
+
expandKiroConfigurationRoot(rootDir, inputKeys, components);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
881
1779
|
if (
|
|
882
1780
|
lowerPath.endsWith("opencode.json") ||
|
|
883
1781
|
lowerPath.endsWith("opencode.jsonc")
|