/**
 * AgentScanner -- Discovery engine for AI agent definitions across platforms.
 *
 * Scans project directories to detect agent configurations from Claude Code,
 * OpenAI, Cursor, and Gemini platforms. Determines governance status and
 * capabilities for each discovered agent.
 *
 * GATE 9: T-146
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../util/log.js';
const log = createLogger('agent-scanner');
/**
 * Governance markers that indicate an agent has governance controls applied.
 * Presence of these in agent content or project structure raises governance status.
 */
const GOVERNANCE_MARKERS = [
    'PERMISSIONS.md',
    'governance-runtime',
    'deny-by-default',
    'tamper-detection',
    'SHA-256',
    'capability-bundle',
    'cycle-lock',
    'approval-queue',
    'audit-log',
    '@connexum/ai-governance',
];
/**
 * Patterns that indicate agent capabilities from file content.
 */
const CAPABILITY_PATTERNS = [
    { pattern: /\bBash\b/i, capability: 'bash' },
    { pattern: /\bWebSearch\b/i, capability: 'web-search' },
    { pattern: /\bWebFetch\b/i, capability: 'web-fetch' },
    { pattern: /\bPlaywright\b|mcp__playwright/i, capability: 'browser' },
    { pattern: /\bRead\b.*file|file.*\bRead\b/i, capability: 'file-read' },
    { pattern: /\bEdit\b.*file|file.*\bEdit\b|Write.*file/i, capability: 'file-write' },
    { pattern: /\bgit\b/i, capability: 'git' },
    { pattern: /\bAPI\b.*key|api[_-]?key/i, capability: 'api-keys' },
    { pattern: /\bdeploy\b/i, capability: 'deploy' },
    { pattern: /\bexecute\b.*code|code.*\bexecute\b/i, capability: 'code-execution' },
];
export class AgentScanner {
    /**
     * Scan a project directory for agent definitions across all supported platforms.
     */
    scan(projectDir) {
        const resolvedDir = path.resolve(projectDir);
        log.info(`Scanning project directory: ${resolvedDir}`);
        const agents = [];
        // Check for governance infrastructure in the project
        const hasGovernanceInfra = this.detectGovernanceInfrastructure(resolvedDir);
        // Scan each platform
        agents.push(...this.scanClaudeCode(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanOpenAI(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanCursor(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanGemini(resolvedDir, hasGovernanceInfra));
        const stats = this.computeStats(agents);
        log.info(`Scan complete: ${stats.total} agents found`, {
            governed: stats.governed,
            partial: stats.partial,
            ungoverned: stats.ungoverned,
        });
        return {
            agents,
            projectDir: resolvedDir,
            scannedAt: new Date().toISOString(),
            stats,
        };
    }
    /**
     * Detect whether the project has governance infrastructure installed.
     */
    detectGovernanceInfrastructure(projectDir) {
        const markers = new Set();
        // Check for .claude/permissions/ directory
        const permDir = path.join(projectDir, '.claude', 'permissions');
        if (fs.existsSync(permDir)) {
            markers.add('permissions-dir');
        }
        // Check for governance config files
        const governanceFiles = [
            path.join(projectDir, '.claude', 'governance.json'),
            path.join(projectDir, '.governance', 'config.json'),
            path.join(projectDir, 'governance.config.json'),
        ];
        for (const f of governanceFiles) {
            if (fs.existsSync(f)) {
                markers.add('governance-config');
                break;
            }
        }
        // Check for hooks directory
        const hooksDir = path.join(projectDir, '.claude', 'hooks');
        if (fs.existsSync(hooksDir)) {
            markers.add('hooks');
        }
        // Check CLAUDE.md for governance references
        const claudeMd = path.join(projectDir, 'CLAUDE.md');
        if (fs.existsSync(claudeMd)) {
            try {
                const content = fs.readFileSync(claudeMd, 'utf-8');
                for (const marker of GOVERNANCE_MARKERS) {
                    if (content.includes(marker)) {
                        markers.add(`claude-md:${marker}`);
                    }
                }
            }
            catch {
                // Skip unreadable files
            }
        }
        return markers;
    }
    /**
     * Scan for Claude Code agent definitions.
     */
    scanClaudeCode(projectDir, infraMarkers) {
        const agents = [];
        // 1. Scan .claude/agents/*.md
        const agentsDir = path.join(projectDir, '.claude', 'agents');
        if (fs.existsSync(agentsDir)) {
            try {
                const files = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
                for (const file of files) {
                    const filePath = path.join(agentsDir, file);
                    const name = path.basename(file, '.md');
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    const governanceStatus = this.assessGovernanceStatus(content, infraMarkers, name, projectDir);
                    agents.push({
                        name,
                        platform: 'claude',
                        filePath,
                        governanceStatus,
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read agents directory: ${agentsDir}`);
            }
        }
        // 2. Check CLAUDE.md for inline agent definitions
        const claudeMd = path.join(projectDir, 'CLAUDE.md');
        if (fs.existsSync(claudeMd)) {
            const content = this.safeReadFile(claudeMd);
            if (content && this.containsAgentDefinitions(content)) {
                const capabilities = this.extractCapabilities(content);
                const governanceStatus = this.assessGovernanceStatus(content, infraMarkers, 'claude-md', projectDir);
                agents.push({
                    name: 'claude-md-config',
                    platform: 'claude',
                    filePath: claudeMd,
                    governanceStatus,
                    capabilities,
                });
            }
        }
        // 3. Check .claude/settings.json
        const settingsJson = path.join(projectDir, '.claude', 'settings.json');
        if (fs.existsSync(settingsJson)) {
            const content = this.safeReadFile(settingsJson);
            if (content) {
                try {
                    const settings = JSON.parse(content);
                    if (settings.agents || settings.permissions || settings.tools) {
                        const capabilities = this.extractSettingsCapabilities(settings);
                        agents.push({
                            name: 'claude-settings',
                            platform: 'claude',
                            filePath: settingsJson,
                            governanceStatus: infraMarkers.size > 2 ? 'partial' : 'ungoverned',
                            capabilities,
                        });
                    }
                }
                catch {
                    // Invalid JSON, skip
                }
            }
        }
        return agents;
    }
    /**
     * Scan for OpenAI agent definitions.
     */
    scanOpenAI(projectDir, infraMarkers) {
        const agents = [];
        // Check .openai/ directory
        const openaiDir = path.join(projectDir, '.openai');
        if (fs.existsSync(openaiDir)) {
            try {
                const files = fs.readdirSync(openaiDir).filter(f => f.endsWith('.json') || f.endsWith('.yaml') || f.endsWith('.yml'));
                for (const file of files) {
                    const filePath = path.join(openaiDir, file);
                    const name = path.basename(file, path.extname(file));
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    agents.push({
                        name,
                        platform: 'openai',
                        filePath,
                        governanceStatus: 'ungoverned',
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read OpenAI directory: ${openaiDir}`);
            }
        }
        // Check openai.json in project root
        const openaiJson = path.join(projectDir, 'openai.json');
        if (fs.existsSync(openaiJson)) {
            const content = this.safeReadFile(openaiJson);
            const capabilities = this.extractCapabilities(content);
            agents.push({
                name: 'openai-config',
                platform: 'openai',
                filePath: openaiJson,
                governanceStatus: 'ungoverned',
                capabilities,
            });
        }
        return agents;
    }
    /**
     * Scan for Cursor agent definitions.
     */
    scanCursor(projectDir, infraMarkers) {
        const agents = [];
        // Check .cursor/ directory
        const cursorDir = path.join(projectDir, '.cursor');
        if (fs.existsSync(cursorDir)) {
            try {
                const files = fs.readdirSync(cursorDir).filter(f => f.endsWith('.json') || f.endsWith('.md') || f === 'rules');
                for (const file of files) {
                    const filePath = path.join(cursorDir, file);
                    // Skip if it's a directory (like rules/)
                    if (fs.statSync(filePath).isDirectory())
                        continue;
                    const name = path.basename(file, path.extname(file));
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    agents.push({
                        name: `cursor-${name}`,
                        platform: 'cursor',
                        filePath,
                        governanceStatus: 'ungoverned',
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read Cursor directory: ${cursorDir}`);
            }
        }
        // Check .cursorrules in project root
        const cursorRules = path.join(projectDir, '.cursorrules');
        if (fs.existsSync(cursorRules)) {
            const content = this.safeReadFile(cursorRules);
            const capabilities = this.extractCapabilities(content);
            agents.push({
                name: 'cursorrules',
                platform: 'cursor',
                filePath: cursorRules,
                governanceStatus: 'ungoverned',
                capabilities,
            });
        }
        return agents;
    }
    /**
     * Scan for Gemini agent definitions.
     */
    scanGemini(projectDir, infraMarkers) {
        const agents = [];
        // Check .gemini/ directory
        const geminiDir = path.join(projectDir, '.gemini');
        if (fs.existsSync(geminiDir)) {
            try {
                const files = fs.readdirSync(geminiDir).filter(f => f.endsWith('.json') || f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md'));
                for (const file of files) {
                    const filePath = path.join(geminiDir, file);
                    if (fs.statSync(filePath).isDirectory())
                        continue;
                    const name = path.basename(file, path.extname(file));
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    agents.push({
                        name: `gemini-${name}`,
                        platform: 'gemini',
                        filePath,
                        governanceStatus: 'ungoverned',
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read Gemini directory: ${geminiDir}`);
            }
        }
        return agents;
    }
    /**
     * Determine governance status for a Claude Code agent.
     */
    assessGovernanceStatus(content, infraMarkers, agentName, projectDir) {
        let agentScore = 0; // Score from agent-specific evidence
        let infraScore = 0; // Score from project-level infrastructure
        // Check for agent-specific permissions file (strong signal)
        const permFile = path.join(projectDir, '.claude', 'permissions', `${agentName}.md`);
        if (fs.existsSync(permFile)) {
            agentScore += 4;
        }
        // Check content for governance markers
        for (const marker of GOVERNANCE_MARKERS) {
            if (content.toLowerCase().includes(marker.toLowerCase())) {
                agentScore += 1;
            }
        }
        // Infrastructure markers provide partial credit only
        if (infraMarkers.has('permissions-dir'))
            infraScore += 1;
        if (infraMarkers.has('governance-config'))
            infraScore += 1;
        if (infraMarkers.has('hooks'))
            infraScore += 1;
        // Agent-specific evidence is required for elevated governance status.
        // Infrastructure alone cannot classify an agent as governed or partial.
        const totalScore = agentScore + infraScore;
        if (agentScore >= 3 && totalScore >= 5)
            return 'governed';
        if (agentScore >= 1 && totalScore >= 3)
            return 'partial';
        return 'ungoverned';
    }
    /**
     * Extract capabilities from agent file content.
     */
    extractCapabilities(content) {
        const capabilities = [];
        for (const { pattern, capability } of CAPABILITY_PATTERNS) {
            if (pattern.test(content)) {
                capabilities.push(capability);
            }
        }
        return [...new Set(capabilities)];
    }
    /**
     * Extract capabilities from a parsed settings JSON object.
     */
    extractSettingsCapabilities(settings) {
        const capabilities = [];
        if (settings.tools && Array.isArray(settings.tools)) {
            capabilities.push(...settings.tools.map((t) => typeof t === 'string' ? t : String(t)));
        }
        if (settings.allowedTools && Array.isArray(settings.allowedTools)) {
            capabilities.push(...settings.allowedTools.map((t) => typeof t === 'string' ? t : String(t)));
        }
        return [...new Set(capabilities)];
    }
    /**
     * Check if CLAUDE.md content contains agent-like definitions.
     */
    containsAgentDefinitions(content) {
        const agentPatterns = [
            /agent[s]?\s*:/i,
            /## Agent/i,
            /role\s*:/i,
            /capabilities\s*:/i,
            /tools\s*:/i,
            /permissions\s*:/i,
        ];
        let matchCount = 0;
        for (const pattern of agentPatterns) {
            if (pattern.test(content))
                matchCount++;
        }
        return matchCount >= 2;
    }
    /**
     * Safely read a file, returning empty string on failure.
     */
    safeReadFile(filePath) {
        try {
            return fs.readFileSync(filePath, 'utf-8');
        }
        catch {
            return '';
        }
    }
    /**
     * Compute aggregate statistics from discovered agents.
     */
    computeStats(agents) {
        return {
            total: agents.length,
            governed: agents.filter(a => a.governanceStatus === 'governed').length,
            partial: agents.filter(a => a.governanceStatus === 'partial').length,
            ungoverned: agents.filter(a => a.governanceStatus === 'ungoverned').length,
        };
    }
}
// DA-MED-NEW-1 (P1H-01b): re-export the manifest-integrity rule class so
// consumers of `@connexum/ai-governance/scanner` can run the MANI rules
// without reaching into an internal path.
export { evaluateManifest, evaluateManifests, } from './manifest-integrity.js';
//# sourceMappingURL=index.js.map