diff --git a/CHANGELOG.md b/CHANGELOG.md index 3130820b..cdcbc533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security + +- Removed the unused production `js-yaml` dependency, eliminating exposure to GHSA-52cp-r559-cp3m (quadratic CPU consumption through YAML merge-key chains). + +### Fixed + +- Restored Cursor and Kiro installation after a core sync removed adapter discovery and transformation functions still called by `agentsys --tool cursor` and `agentsys --tool kiro` (#380). +- Scoped Jest worktree ignore patterns to the repository root so tests run when AgentSys itself is checked out under a `worktrees` directory. + +### Tests + +- Added isolated-home installer regression coverage for Cursor commands/skills and Kiro prompts/skills/agents. + ## [6.0.0] - 2026-05-29 ### Removed diff --git a/__tests__/platform-adapter-install.test.js b/__tests__/platform-adapter-install.test.js new file mode 100644 index 00000000..28f11b19 --- /dev/null +++ b/__tests__/platform-adapter-install.test.js @@ -0,0 +1,117 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const discovery = require('../lib/discovery'); +const transforms = require('../lib/adapter-transforms'); +const { installForCursor, installForKiro } = require('../bin/cli'); + +describe('Cursor and Kiro adapter installers', () => { + let tempDir; + let installDir; + let originalHome; + let logSpy; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsys-platform-install-')); + installDir = path.join(tempDir, 'install'); + originalHome = process.env.HOME; + process.env.HOME = tempDir; + logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + const pluginDir = path.join(installDir, 'plugins', 'test-plugin'); + fs.mkdirSync(path.join(pluginDir, '.claude-plugin'), { recursive: true }); + fs.mkdirSync(path.join(pluginDir, 'commands'), { recursive: true }); + fs.mkdirSync(path.join(pluginDir, 'skills', 'test-skill'), { recursive: true }); + fs.mkdirSync(path.join(pluginDir, 'agents'), { recursive: true }); + + fs.writeFileSync( + path.join(pluginDir, '.claude-plugin', 'plugin.json'), + JSON.stringify({ name: 'test-plugin', version: '1.0.0' }) + ); + fs.writeFileSync( + path.join(pluginDir, 'commands', 'test-command.md'), + '---\ndescription: Test command\n---\nRun ${CLAUDE_PLUGIN_ROOT}/scripts/test.js\n' + ); + fs.writeFileSync( + path.join(pluginDir, 'skills', 'test-skill', 'SKILL.md'), + '---\nname: test-skill\ndescription: Test skill\n---\nUse ${CLAUDE_PLUGIN_ROOT}/lib/test.js\n' + ); + fs.writeFileSync( + path.join(pluginDir, 'agents', 'test-agent.md'), + '---\nname: test-agent\ndescription: Test agent\ntools: Read, Write\n---\nReview the repository.\n' + ); + + discovery.invalidateCache(); + }); + + afterEach(() => { + discovery.invalidateCache(); + logSpy.mockRestore(); + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test('exports every adapter API used by the CLI', () => { + for (const name of ['getCursorRuleMappings', 'getKiroSteeringMappings']) { + expect(discovery[name]).toEqual(expect.any(Function)); + } + + for (const name of [ + 'transformRuleForCursor', + 'transformSkillForCursor', + 'transformCommandForCursor', + 'transformSkillForKiro', + 'transformCommandForKiro', + 'transformAgentForKiro', + 'generateCombinedReviewerAgent' + ]) { + expect(transforms[name]).toEqual(expect.any(Function)); + } + }); + + test('installs Cursor commands and skills into an isolated home', () => { + expect(() => installForCursor(installDir)).not.toThrow(); + + const command = fs.readFileSync( + path.join(tempDir, '.cursor', 'commands', 'test-command.md'), + 'utf8' + ); + const skill = fs.readFileSync( + path.join(tempDir, '.cursor', 'skills', 'test-skill', 'SKILL.md'), + 'utf8' + ); + + expect(command).not.toContain('${CLAUDE_PLUGIN_ROOT}'); + expect(skill).not.toContain('${CLAUDE_PLUGIN_ROOT}'); + }); + + test('installs Kiro prompts, skills, and agents into an isolated home', () => { + expect(() => installForKiro(installDir)).not.toThrow(); + + const prompt = fs.readFileSync( + path.join(tempDir, '.kiro', 'prompts', 'test-command.md'), + 'utf8' + ); + const skill = fs.readFileSync( + path.join(tempDir, '.kiro', 'skills', 'test-skill', 'SKILL.md'), + 'utf8' + ); + const agent = JSON.parse( + fs.readFileSync(path.join(tempDir, '.kiro', 'agents', 'test-agent.json'), 'utf8') + ); + + expect(prompt).toContain('inclusion: manual'); + expect(prompt).not.toContain('${CLAUDE_PLUGIN_ROOT}'); + expect(skill).not.toContain('${CLAUDE_PLUGIN_ROOT}'); + expect(agent).toMatchObject({ + name: 'test-agent', + description: 'Test agent', + tools: ['read', 'write'] + }); + }); +}); diff --git a/jest.config.js b/jest.config.js index 7af92cbe..b88de469 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,8 +8,8 @@ module.exports = { testPathIgnorePatterns: [ '/node_modules/', '/plugins/.*/lib/', - '/.claude/worktrees/', - '/worktrees/', + '/.claude/worktrees/', + '/worktrees/', // lib/binary/index.test.js is a node:test suite (co-located with the // module in agent-core convention). Jest's discovery picks it up and // fails with "must contain at least one test" because it has no @@ -17,8 +17,8 @@ module.exports = { '/lib/binary/.*\\.test\\.js$' ], modulePathIgnorePatterns: [ - '/.claude/worktrees/', - '/worktrees/' + '/.claude/worktrees/', + '/worktrees/' ], collectCoverageFrom: [ 'bin/**/*.js', diff --git a/lib/adapter-transforms.js b/lib/adapter-transforms.js index e62e573a..895e074b 100644 --- a/lib/adapter-transforms.js +++ b/lib/adapter-transforms.js @@ -297,10 +297,375 @@ function transformForCodex(content, options) { return content; } +/** + * Transform content for Cursor (.mdc rule files). + * + * MDC format uses YAML frontmatter with `description`, `globs`, and + * `alwaysApply` fields followed by a markdown body. + * + * @param {string} content - Source markdown content (may have frontmatter) + * @param {Object} options + * @param {string} options.description - Rule description + * @param {string} options.pluginInstallPath - Absolute path to plugin install dir + * @param {string} [options.globs] - Optional glob pattern for file matching + * @param {boolean} [options.alwaysApply] - Whether rule always applies (default true) + * @returns {string} Transformed MDC content + */ +function transformRuleForCursor(content, options) { + const { description = '', pluginInstallPath, globs = '', alwaysApply = true } = options; + + // Strip control characters and escape description for YAML + const cleanDescription = description.replace(/[\x00-\x1f\x7f]/g, ' '); + const escapedDescription = cleanDescription.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const yamlDescription = `"${escapedDescription}"`; + + // Build MDC frontmatter + let frontmatter = `---\ndescription: ${yamlDescription}\n`; + if (globs) { + frontmatter += `globs: ${JSON.stringify(globs)}\n`; + } + frontmatter += `alwaysApply: ${alwaysApply}\n---\n`; + + // Strip existing frontmatter if present + if (content.startsWith('---')) { + content = content.replace(/^---\n[\s\S]*?\n---\n?/, ''); + } + + content = frontmatter + content; + + // Replace PLUGIN_ROOT paths with actual install path + // Use function replacement to avoid $ pattern interpretation in replacement string + content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath); + content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath); + + // Strip Claude-specific syntax: Task tool calls (handles one level of nested braces) + content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => { + const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/); + if (agentMatch) { + return `Invoke the ${agentMatch[1]} agent`; + } + return ''; + }); + + // Strip require() statements + content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, ''); + content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, ''); + + // Strip plugin namespacing (e.g. next-task:agent-name -> agent-name) + content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1'); + + return content; +} + +/** + * Transform skill content for Cursor. + * + * Minimal transform - Cursor reads SKILL.md frontmatter natively so we + * preserve it. Only replaces PLUGIN_ROOT paths and strips namespace prefixes. + * + * @param {string} content - Source SKILL.md content + * @param {Object} options + * @param {string} options.pluginInstallPath - Absolute path to plugin install dir + * @returns {string} Transformed skill content + */ +function transformSkillForCursor(content, options) { + const { pluginInstallPath } = options; + + // Replace PLUGIN_ROOT paths with actual install path + content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath); + content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath); + + // Strip plugin namespacing (e.g. next-task:agent-name -> agent-name) + content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1'); + + return content; +} + +/** + * Transform command content for Cursor. + * + * Light transform - strips frontmatter, replaces PLUGIN_ROOT paths, + * removes require() statements and Task() calls, strips namespace prefixes. + * + * @param {string} content - Source command markdown content + * @param {Object} options + * @param {string} options.pluginInstallPath - Absolute path to plugin install dir + * @returns {string} Transformed command content + */ +function transformCommandForCursor(content, options) { + const { pluginInstallPath } = options; + + // Strip existing frontmatter if present + if (content.startsWith('---')) { + content = content.replace(/^---\n[\s\S]*?\n---\n?/, ''); + } + + // Replace PLUGIN_ROOT paths with actual install path + content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath); + content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath); + + // Strip require() statements + content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, ''); + content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, ''); + + // Strip Claude-specific syntax: Task tool calls (handles one level of nested braces) + content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => { + const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/); + if (agentMatch) { + return `Invoke the ${agentMatch[1]} agent`; + } + return ''; + }); + + // Strip plugin namespacing (e.g. next-task:agent-name -> agent-name) + content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1'); + + return content; +} + +/** + * Transform skill content for Kiro. + * + * Minimal transform - Kiro reads standard SKILL.md format natively so we + * preserve it. Only replaces PLUGIN_ROOT paths and strips namespace prefixes. + * + * @param {string} content - Source SKILL.md content + * @param {Object} options + * @param {string} options.pluginInstallPath - Absolute path to plugin install dir + * @returns {string} Transformed skill content + */ +function transformSkillForKiro(content, options) { + const { pluginInstallPath } = options; + + content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath); + content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath); + + content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1'); + + return content; +} + +/** + * Transform command content for Kiro prompt files. + * + * Strips existing frontmatter, prepends inclusion: manual frontmatter, + * replaces PLUGIN_ROOT paths, removes require()/Task() calls, strips namespaces. + */ +function transformCommandForKiro(content, options) { + const { pluginInstallPath, name = '', description = '' } = options; + + if (content.startsWith('---')) { + content = content.replace(/^---\n[\s\S]*?\n---\n?/, ''); + } + + const cleanDescription = description.replace(/[\x00-\x1f\x7f]/g, ' '); + const escapedDescription = cleanDescription.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + let frontmatter = '---\n'; + frontmatter += 'inclusion: manual\n'; + if (name) frontmatter += `name: "${name}"\n`; + if (description) frontmatter += `description: "${escapedDescription}"\n`; + frontmatter += '---\n'; + + content = frontmatter + content; + + content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath); + content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath); + content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath); + + content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, ''); + content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, ''); + + // Transform code blocks containing Promise.all + Task() (parallel reviewer spawns). + // The bare Task() regex below can't reach inside fenced code blocks. + // Fence boundaries must be at line start (^```) to avoid matching backtick + // template literals inside the code as false fence endings. + content = content.replace(/^```(?:javascript|js)?\n([\s\S]*?)^```$/gm, (fullBlock, codeContent) => { + if (!codeContent.includes('Promise.all') || !codeContent.includes('Task(')) return fullBlock; + // Use lazy [\s\S]*? for the template literal body: it stops at the first backtick and + // naturally matches any character including standalone $ (e.g. $100, $BUDGET). + const taskMatches = [...codeContent.matchAll(/Task\s*\(\s*\{[\s\S]*?subagent_type:\s*['"](?:[^"':]+:)?([^'"]+)['"][\s\S]*?prompt:\s*`([\s\S]*?)`/gs)]; + if (taskMatches.length < 2) return fullBlock; + + const delegations = taskMatches.map(m => { + const agent = m[1]; + const promptFirstLine = m[2].split('\n').find(l => l.trim()) || ''; + return `Delegate to the \`${agent}\` subagent:\n> ${promptFirstLine.trim()}`; + }); + + let result = delegations.join('\n\n'); + + const hasReviewKeyword = delegations.some(d => + /review|quality|security|performance|test|coverage/i.test(d) + ); + if (delegations.length >= 4 && hasReviewKeyword) { + result = `**Review phase (Kiro - max 4 agents, fallback to 2 sequential):**\n\nTry delegating to these subagents (experimental parallel spawning):\n\n${result}\n\nIf parallel spawning is unavailable, run 2 combined reviewers sequentially:\n1. Delegate to the \`reviewer-quality-security\` subagent (code quality + security)\n2. Then delegate to the \`reviewer-perf-test\` subagent (performance + test coverage)\n\nAggregate all findings from whichever execution path succeeded.`; + } + + return result; + }); + + // Transform bare Task() calls outside code blocks + content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => { + const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/); + const promptMatch = match.match(/prompt:\s*[`"']([\s\S]*?)[`"']/); + if (agentMatch) { + const agentName = agentMatch[1]; + const prompt = promptMatch ? promptMatch[1].replace(/\\n/g, '\n').trim() : ''; + if (prompt) { + return `Delegate to the \`${agentName}\` subagent:\n> ${prompt.split('\n')[0]}`; + } + return `Delegate to the \`${agentName}\` subagent.`; + } + return ''; + }); + + // Transform AskUserQuestion to markdown prompt for Kiro chat + content = content.replace(/(?:await\s+)?AskUserQuestion\s*\(\s*\{[\s\S]*?\}\s*\);?/g, (match) => { + const questionMatch = match.match(/question:\s*["'`]([\s\S]*?)["'`]/); + const question = questionMatch ? questionMatch[1] : 'Please choose:'; + const optionMatches = [...match.matchAll(/label:\s*["'`]([^"'`]+)["'`][\s\S]*?description:\s*["'`]([^"'`]+)["'`]/g)]; + if (optionMatches.length > 0) { + const options = optionMatches.map((m, i) => `${i + 1}. **${m[1]}** - ${m[2]}`).join('\n'); + return `**${question}**\n\n${options}\n\nReply with the number or name of your choice.`; + } + return `**${question}**\n\nReply in chat with your choice.`; + }); + + content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1'); + + // Batch parallel reviewer delegations for Kiro's agent limit. + // Detects 4+ consecutive "Delegate to" lines with review-related names + // and rewrites as try-4-then-fallback-to-2 pattern. + const reviewerBatchPattern = /((?:Delegate to the `[^`]*` subagent[^\n]*\n){4,})/g; + content = content.replace(reviewerBatchPattern, (block) => { + const delegations = block.match(/Delegate to the `([^`]+)` subagent/g) || []; + if (delegations.length < 4) return block; + + const hasReviewKeyword = delegations.some(d => + /review|quality|security|performance|test|coverage/i.test(d) + ); + if (!hasReviewKeyword) return block; + + return `**Review phase (Kiro - max 4 agents, fallback to 2 sequential):** + +Try delegating to these subagents (experimental parallel spawning): +${block} +If parallel spawning is unavailable, run 2 combined reviewers sequentially: +1. Delegate to the \`reviewer-quality-security\` subagent (code quality + security) +2. Then delegate to the \`reviewer-perf-test\` subagent (performance + test coverage) + +Aggregate all findings from whichever execution path succeeded.\n`; + }); + + return content; +} + +/** + * Transform agent markdown+frontmatter to Kiro JSON format. + * + * Parses frontmatter for name/description/model/tools, uses body as prompt. + * Returns a JSON string matching Kiro's agent schema. + */ +function transformAgentForKiro(content, options) { + const { pluginInstallPath } = options || {}; + + const frontmatter = discovery.parseFrontmatter(content); + let body = content; + if (content.startsWith('---')) { + const endIdx = content.indexOf('\n---', 3); + if (endIdx !== -1) { + body = content.substring(endIdx + 4).replace(/^\n/, ''); + } + } + + if (pluginInstallPath) { + body = body.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath); + body = body.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath); + body = body.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath); + body = body.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath); + } + + body = body.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1'); + + const agent = { + name: frontmatter.name || '', + description: frontmatter.description || '', + prompt: body.trim() + }; + + if (frontmatter.tools) { + // parseFrontmatter returns arrays for YAML list syntax, string for inline + const toolItems = Array.isArray(frontmatter.tools) + ? frontmatter.tools.map(t => t.toLowerCase()) + : [frontmatter.tools.toLowerCase()]; + const toolStr = toolItems.join(' '); + const tools = []; + if (toolStr.includes('read')) tools.push('read'); + if (toolStr.includes('edit') || toolStr.includes('write')) tools.push('write'); + if (toolStr.includes('bash') || toolStr.includes('shell')) tools.push('shell'); + if (toolStr.includes('glob')) tools.push('read'); + if (toolStr.includes('grep')) tools.push('read'); + if (toolStr.includes('task') || toolStr.includes('agent')) tools.push('shell'); + if (toolStr.includes('web') || toolStr.includes('fetch')) tools.push('shell'); + if (toolStr.includes('notebook')) tools.push('write'); + if (toolStr.includes('lsp')) tools.push('read'); + const deduped = [...new Set(tools)]; + agent.tools = deduped.length > 0 ? deduped : ['read']; + } else { + agent.tools = ['read']; + } + + agent.resources = ['file://.kiro/prompts/**/*.md']; + + return JSON.stringify(agent, null, 2); +} + +/** + * Generate a combined reviewer agent JSON for Kiro's 4-agent limit. + * Merges multiple review responsibilities into a single agent. + * + * @param {Array<{name: string, focus: string}>} roles - Review passes to combine + * @param {string} name - Agent name + * @param {string} description - Agent description + * @returns {string} JSON string for .kiro/agents/*.json + */ +function generateCombinedReviewerAgent(roles, name, description) { + const sections = roles.map(r => + `## ${r.name} Review\n\nFocus: ${r.focus}` + ).join('\n\n---\n\n'); + + const agent = { + name, + description, + prompt: `You are a combined code reviewer covering multiple review passes in a single session.\n\n${sections}\n\nFor each file you review, check ALL of the above review dimensions. Return findings as a JSON array with objects containing: pass (which review), file, line, severity (critical/high/medium/low), description, suggestion.`, + tools: ['read'], + resources: ['file://.kiro/prompts/**/*.md'], + }; + + return JSON.stringify(agent, null, 2); +} + module.exports = { transformBodyForOpenCode, transformCommandFrontmatterForOpenCode, transformAgentFrontmatterForOpenCode, transformSkillBodyForOpenCode, - transformForCodex + transformForCodex, + transformRuleForCursor, + transformSkillForCursor, + transformCommandForCursor, + transformForCursor: transformRuleForCursor, + transformSkillForKiro, + transformCommandForKiro, + transformAgentForKiro, + generateCombinedReviewerAgent }; diff --git a/lib/discovery/index.js b/lib/discovery/index.js index 4dd98b0b..ce36dfaa 100644 --- a/lib/discovery/index.js +++ b/lib/discovery/index.js @@ -337,6 +337,48 @@ function invalidateCache() { _cacheRoot = null; } +/** + * Build Cursor rule mappings from discovered commands. + * Returns [ruleName, pluginName, sourceFile, description, type, globs] tuples. + * Uses cursor-description frontmatter field, falls back to codex-description, then description. + * + * @param {string} [repoRoot] - Repository root path + * @returns {Array<[string, string, string, string, string, string]>} + */ +function getCursorRuleMappings(repoRoot) { + const commands = discoverCommands(repoRoot); + return commands.map(cmd => { + const description = cmd.frontmatter['cursor-description'] || + cmd.frontmatter['codex-description'] || + cmd.frontmatter.description || + ''; + const type = cmd.frontmatter.type || 'command'; + const globs = cmd.frontmatter.globs || ''; + return [`agentsys-${cmd.plugin}-${cmd.name}`, cmd.plugin, cmd.file, description, type, globs]; + }); +} + +/** + * Build Kiro steering mappings from discovered commands. + * Returns [steeringName, pluginName, sourceFile, description] tuples. + * Uses kiro-description frontmatter field, falls back to cursor-description, + * codex-description, then description. + * + * @param {string} [repoRoot] - Repository root path + * @returns {Array<[string, string, string, string]>} + */ +function getKiroSteeringMappings(repoRoot) { + const commands = discoverCommands(repoRoot); + return commands.map(cmd => { + const description = cmd.frontmatter['kiro-description'] || + cmd.frontmatter['cursor-description'] || + cmd.frontmatter['codex-description'] || + cmd.frontmatter.description || + ''; + return [cmd.name, cmd.plugin, cmd.file, description]; + }); +} + module.exports = { parseFrontmatter, isValidPluginName, @@ -347,6 +389,8 @@ module.exports = { discoverAll, getCommandMappings, getCodexSkillMappings, + getCursorRuleMappings, + getKiroSteeringMappings, getPluginPrefixRegex, invalidateCache }; diff --git a/package-lock.json b/package-lock.json index dc4dc841..146a3d15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "workspaces": [ "lib" ], - "dependencies": { - "js-yaml": "~4.2.0" - }, "bin": { "agentsys": "bin/cli.js", "agentsys-dev": "bin/dev-cli.js" @@ -1125,12 +1122,6 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2718,28 +2709,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", diff --git a/package.json b/package.json index 3f6f55b8..e95b57b4 100644 --- a/package.json +++ b/package.json @@ -80,9 +80,6 @@ "engines": { "node": ">=18.0.0" }, - "dependencies": { - "js-yaml": "~4.2.0" - }, "devDependencies": { "jest": "^29.7.0" }, diff --git a/tests/lib-package.test.js b/tests/lib-package.test.js index 9ec228e0..1ccc3674 100644 --- a/tests/lib-package.test.js +++ b/tests/lib-package.test.js @@ -10,6 +10,13 @@ const fs = require('fs'); const ROOT_DIR = path.resolve(__dirname, '..'); +describe('agentsys package security', () => { + it('does not ship the unused js-yaml parser', () => { + const rootPkg = require(path.join(ROOT_DIR, 'package.json')); + expect(rootPkg.dependencies?.['js-yaml']).toBeUndefined(); + }); +}); + describe('@agentsys/lib workspace', () => { let libPkg;