nexus-agents 2.29.0 → 2.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adaptive-memory-5VP5WWTE.js +15 -0
- package/dist/chunk-5COIDGQJ.js +1585 -0
- package/dist/chunk-5COIDGQJ.js.map +1 -0
- package/dist/{chunk-HWDBNDUX.js → chunk-63AJLNKU.js} +2 -2
- package/dist/chunk-66NNHMVB.js +195 -0
- package/dist/chunk-66NNHMVB.js.map +1 -0
- package/dist/chunk-AP2FD37C.js +127 -0
- package/dist/chunk-AP2FD37C.js.map +1 -0
- package/dist/chunk-BC3M4VLP.js +359 -0
- package/dist/chunk-BC3M4VLP.js.map +1 -0
- package/dist/chunk-BQ4YXGGQ.js +127 -0
- package/dist/chunk-BQ4YXGGQ.js.map +1 -0
- package/dist/{chunk-ZBZJHXRT.js → chunk-CW2Z773T.js} +19 -347
- package/dist/chunk-CW2Z773T.js.map +1 -0
- package/dist/chunk-DDQGAVQA.js +944 -0
- package/dist/chunk-DDQGAVQA.js.map +1 -0
- package/dist/chunk-ED6VQWNG.js +63 -0
- package/dist/chunk-ED6VQWNG.js.map +1 -0
- package/dist/{chunk-7F6HYUIY.js → chunk-EPMBGZQX.js} +16 -97
- package/dist/chunk-EPMBGZQX.js.map +1 -0
- package/dist/chunk-GX436VRU.js +931 -0
- package/dist/chunk-GX436VRU.js.map +1 -0
- package/dist/{chunk-IMWYKX4H.js → chunk-HSOPD265.js} +444 -399
- package/dist/chunk-HSOPD265.js.map +1 -0
- package/dist/{chunk-S3BKWNST.js → chunk-J245RJGW.js} +680 -1436
- package/dist/chunk-J245RJGW.js.map +1 -0
- package/dist/{chunk-I6YDS23R.js → chunk-KQIDTE52.js} +2 -2
- package/dist/{chunk-POBO4G2P.js → chunk-LDIN2PLV.js} +250 -110
- package/dist/chunk-LDIN2PLV.js.map +1 -0
- package/dist/{chunk-KGDG6PWZ.js → chunk-LKDHAJJB.js} +2 -2
- package/dist/{chunk-T7PU3NPQ.js → chunk-NKGTEJYU.js} +7 -5
- package/dist/{chunk-T7PU3NPQ.js.map → chunk-NKGTEJYU.js.map} +1 -1
- package/dist/chunk-QGODFK36.js +122 -0
- package/dist/chunk-QGODFK36.js.map +1 -0
- package/dist/{chunk-DAMRMAM2.js → chunk-QSNAFOE6.js} +12369 -14499
- package/dist/chunk-QSNAFOE6.js.map +1 -0
- package/dist/chunk-TL2GJMJ5.js +700 -0
- package/dist/chunk-TL2GJMJ5.js.map +1 -0
- package/dist/{chunk-WSK4VSXP.js → chunk-V6MSPUQF.js} +2 -2
- package/dist/chunk-VZ2YOQWU.js +90 -0
- package/dist/chunk-VZ2YOQWU.js.map +1 -0
- package/dist/{chunk-5VZLXMO7.js → chunk-WSYJN7BI.js} +7 -6
- package/dist/chunk-WSYJN7BI.js.map +1 -0
- package/dist/chunk-Y477EGI4.js +356 -0
- package/dist/chunk-Y477EGI4.js.map +1 -0
- package/dist/{chunk-HH5LVGEE.js → chunk-Z4OZ25VS.js} +4 -4
- package/dist/cli-circuit-breaker-6EJO3PPU.js +13 -0
- package/dist/cli.js +123 -68
- package/dist/cli.js.map +1 -1
- package/dist/codebase-search-CZUA37RU.js +9 -0
- package/dist/{composite-router-YPRWVTRB.js → composite-router-JD7URTC2.js} +2 -2
- package/dist/{consensus-vote-DBE6RNZG.js → consensus-vote-COW34Q2Y.js} +7 -5
- package/dist/{dist-7PQR2BQB.js → dist-CV74KUT7.js} +1302 -805
- package/dist/dist-CV74KUT7.js.map +1 -0
- package/dist/{doctor-deep-AWE7SRU6.js → doctor-deep-4A4X5X6U.js} +3 -3
- package/dist/expert-bridge-J36C7VES.js +10 -0
- package/dist/{expert-config-FHNBQRX2.js → expert-config-MQ5OJE3U.js} +2 -2
- package/dist/{factory-O5C7ZBZO.js → factory-4Z4RSUYE.js} +5 -4
- package/dist/{factory-PCHGQ3ZG.js → factory-NHORX63J.js} +4 -3
- package/dist/index.d.ts +507 -42
- package/dist/index.js +331 -78
- package/dist/index.js.map +1 -1
- package/dist/issue-triage-TIG3RKXF.js +15 -0
- package/dist/{mcp-config-AUZQPUBY.js → mcp-config-ETY7GFGW.js} +3 -3
- package/dist/mobimem-5PAAMVFR.js +13 -0
- package/dist/mobimem-5PAAMVFR.js.map +1 -0
- package/dist/repo-analyze-HWMXSK5C.js +24 -0
- package/dist/repo-analyze-HWMXSK5C.js.map +1 -0
- package/dist/repo-security-plan-KQB3ZJTE.js +17 -0
- package/dist/repo-security-plan-KQB3ZJTE.js.map +1 -0
- package/dist/research-helpers-synthesize-ZMERZZ5B.js +10 -0
- package/dist/research-helpers-synthesize-ZMERZZ5B.js.map +1 -0
- package/dist/{routing-memory-QY3XMU2R.js → routing-memory-3ES3OHLM.js} +2 -2
- package/dist/routing-memory-3ES3OHLM.js.map +1 -0
- package/dist/{session-memory-3MBCE5KS.js → session-memory-E2OE2CYR.js} +3 -3
- package/dist/session-memory-E2OE2CYR.js.map +1 -0
- package/dist/{setup-command-IQ4MD3FT.js → setup-command-CMCQRBJF.js} +7 -6
- package/dist/setup-command-CMCQRBJF.js.map +1 -0
- package/dist/{setup-config-5YUPLDXF.js → setup-config-KITOPV7V.js} +3 -3
- package/dist/setup-config-KITOPV7V.js.map +1 -0
- package/dist/shared-memory-AEO2HJLC.js +8 -0
- package/dist/shared-memory-AEO2HJLC.js.map +1 -0
- package/dist/symbol-extractor-UEBANFSN.js +10 -0
- package/dist/symbol-extractor-UEBANFSN.js.map +1 -0
- package/dist/{weather-report-CC2C4KAX.js → weather-report-KUSVNXDZ.js} +2 -2
- package/dist/weather-report-KUSVNXDZ.js.map +1 -0
- package/package.json +14 -13
- package/dist/chunk-5VZLXMO7.js.map +0 -1
- package/dist/chunk-7F6HYUIY.js.map +0 -1
- package/dist/chunk-DAMRMAM2.js.map +0 -1
- package/dist/chunk-IMWYKX4H.js.map +0 -1
- package/dist/chunk-POBO4G2P.js.map +0 -1
- package/dist/chunk-S3BKWNST.js.map +0 -1
- package/dist/chunk-ZBZJHXRT.js.map +0 -1
- package/dist/dist-7PQR2BQB.js.map +0 -1
- /package/dist/{composite-router-YPRWVTRB.js.map → adaptive-memory-5VP5WWTE.js.map} +0 -0
- /package/dist/{chunk-HWDBNDUX.js.map → chunk-63AJLNKU.js.map} +0 -0
- /package/dist/{chunk-I6YDS23R.js.map → chunk-KQIDTE52.js.map} +0 -0
- /package/dist/{chunk-KGDG6PWZ.js.map → chunk-LKDHAJJB.js.map} +0 -0
- /package/dist/{chunk-WSK4VSXP.js.map → chunk-V6MSPUQF.js.map} +0 -0
- /package/dist/{chunk-HH5LVGEE.js.map → chunk-Z4OZ25VS.js.map} +0 -0
- /package/dist/{consensus-vote-DBE6RNZG.js.map → cli-circuit-breaker-6EJO3PPU.js.map} +0 -0
- /package/dist/{doctor-deep-AWE7SRU6.js.map → codebase-search-CZUA37RU.js.map} +0 -0
- /package/dist/{expert-config-FHNBQRX2.js.map → composite-router-JD7URTC2.js.map} +0 -0
- /package/dist/{factory-O5C7ZBZO.js.map → consensus-vote-COW34Q2Y.js.map} +0 -0
- /package/dist/{factory-PCHGQ3ZG.js.map → doctor-deep-4A4X5X6U.js.map} +0 -0
- /package/dist/{mcp-config-AUZQPUBY.js.map → expert-bridge-J36C7VES.js.map} +0 -0
- /package/dist/{routing-memory-QY3XMU2R.js.map → expert-config-MQ5OJE3U.js.map} +0 -0
- /package/dist/{session-memory-3MBCE5KS.js.map → factory-4Z4RSUYE.js.map} +0 -0
- /package/dist/{setup-command-IQ4MD3FT.js.map → factory-NHORX63J.js.map} +0 -0
- /package/dist/{setup-config-5YUPLDXF.js.map → issue-triage-TIG3RKXF.js.map} +0 -0
- /package/dist/{weather-report-CC2C4KAX.js.map → mcp-config-ETY7GFGW.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli-adapters/fallback-chains.ts","../src/cli-adapters/cli-circuit-breaker.ts"],"sourcesContent":["/**\n * nexus-agents/cli-adapters - Fallback Chain Registry\n *\n * Configurable fallback chains per task type with metrics tracking.\n * Provides ordered CLI preferences based on task classification.\n *\n * @module cli-adapters/fallback-chains\n * (Source: Issue #362 - Task-type-aware fallback chains)\n */\n\nimport { z } from 'zod';\nimport type { CliName } from './types-core.js';\nimport type { FallbackTaskType } from './task-classifier.js';\nimport type { TaskCategory } from '../config/task-specialization-types.js';\n\n/**\n * Fallback chain: ordered list of CLIs to try for a task type.\n */\nexport type FallbackChain = readonly CliName[];\n\n/**\n * Registry mapping task types to fallback chains.\n */\nexport type FallbackChainRegistry = Readonly<Record<FallbackTaskType, FallbackChain>>;\n\n/**\n * Metrics for tracking fallback chain success rates.\n */\nexport interface FallbackChainMetrics {\n /** Total attempts for this task type */\n readonly totalAttempts: number;\n /** Successes by position in chain (0-indexed) */\n readonly successByPosition: readonly number[];\n /** Failures (exhausted all positions) */\n readonly exhaustedCount: number;\n /** Average position of successful attempt (lower is better) */\n readonly avgSuccessPosition: number;\n}\n\n/**\n * Metrics for all task types.\n */\nexport type FallbackMetricsRegistry = Readonly<Record<FallbackTaskType, FallbackChainMetrics>>;\n\n/**\n * Outcome of a fallback chain execution.\n */\nexport interface FallbackOutcome {\n /** Task type that was classified */\n readonly taskType: FallbackTaskType;\n /** Position in chain that succeeded (0-indexed), or -1 if exhausted */\n readonly successPosition: number;\n /** CLI that succeeded, or undefined if exhausted */\n readonly successfulCli?: CliName | undefined;\n /** Whether the chain was exhausted without success */\n readonly exhausted: boolean;\n}\n\n/**\n * Zod schema for fallback chain validation.\n */\nexport const FallbackChainSchema = z\n .array(z.enum(['claude', 'gemini', 'codex', 'opencode']))\n .min(1)\n .readonly();\n\n/**\n * Zod schema for fallback chain registry.\n */\nexport const FallbackChainRegistrySchema = z.object({\n code: FallbackChainSchema,\n research: FallbackChainSchema,\n documentation: FallbackChainSchema,\n analysis: FallbackChainSchema,\n general: FallbackChainSchema,\n});\n\n/**\n * Default fallback chains derived from the task specialization matrix.\n * Each chain orders CLIs by: primary → secondary → others.\n *\n * - code: Codex primary (92.4% code_gen), Claude secondary\n * - research: Gemini primary (86.8% research), Claude secondary\n * - documentation: Gemini primary (71.4% docs), Claude secondary\n * - analysis: Claude primary (architecture/security/planning), Codex secondary\n * - general: Balanced fallback order\n */\nexport const DEFAULT_FALLBACK_CHAINS: FallbackChainRegistry = {\n // code_generation/code_review/testing: codex primary, claude secondary\n code: ['codex', 'claude', 'gemini', 'opencode'],\n // research/exploration: gemini primary, claude secondary\n research: ['gemini', 'claude', 'codex', 'opencode'],\n // documentation: gemini primary, claude secondary\n documentation: ['gemini', 'claude', 'codex', 'opencode'],\n // architecture/security/planning: claude primary, gemini secondary\n // Weather data: gemini arch 66.7% (n=24) > codex 33.3% (n=3)\n analysis: ['claude', 'gemini', 'codex', 'opencode'],\n // general: balanced order\n general: ['claude', 'gemini', 'codex', 'opencode'],\n} as const;\n\n/**\n * Per-category fallback chain overrides.\n *\n * Categories in the same FallbackTaskType bucket may need different CLI ordering.\n * Entries here override the bucket-level chain from DEFAULT_FALLBACK_CHAINS.\n *\n * Weather data 2026-03-09:\n * - architecture: gemini 69.6% (n=23) > claude 40.9% (n=235) → gemini first\n * - planning: claude 92.2% → keep claude first (bucket default)\n * - security_review: codex 60% (n=5) > gemini 53.8% (n=13) > claude 54.2% (n=382) → codex first\n * - exploration: gemini 100% (n=307) > claude 83.9% (n=380) → gemini first, codex secondary (#1526)\n * - devops: claude 80% (n=5), gemini 100% (n=1) → keep claude first, gemini secondary (#1526)\n * - research: gemini 86.8% (n=38) > claude 84.1% (n=44) > codex 15% (n=20) → gemini first, codex last (#1536)\n * - documentation: gemini 71.4% (n=35) > claude 64.7% (n=17) > codex 33.3% (n=6) → gemini first, codex last (#1536)\n * - code_review: claude 91% (n=200) > codex 89.2% (n=93) > gemini 37.5% (n=8) → claude first (#1401)\n */\nexport const CATEGORY_CHAIN_OVERRIDES: Partial<Record<TaskCategory, FallbackChain>> = {\n architecture: ['gemini', 'claude', 'codex', 'opencode'],\n security_review: ['codex', 'gemini', 'claude', 'opencode'],\n code_review: ['claude', 'codex', 'gemini', 'opencode'],\n exploration: ['gemini', 'codex', 'claude', 'opencode'],\n devops: ['claude', 'gemini', 'codex', 'opencode'],\n // codex has 15% research success (n=20) — push to last position (#1536)\n research: ['gemini', 'claude', 'opencode', 'codex'],\n // codex has 33.3% docs success (n=6) — push to last position (#1536)\n documentation: ['gemini', 'claude', 'opencode', 'codex'],\n} as const;\n\n/**\n * Gets the fallback chain for a specific TaskCategory.\n * Returns a category-specific override if available, otherwise falls through\n * to the bucket-level chain via CATEGORY_TO_FALLBACK mapping.\n */\nexport function getFallbackChainForCategory(\n category: TaskCategory,\n bucketType: FallbackTaskType,\n registry: FallbackChainRegistry = DEFAULT_FALLBACK_CHAINS\n): FallbackChain {\n const override = CATEGORY_CHAIN_OVERRIDES[category];\n if (override !== undefined) return override;\n return registry[bucketType];\n}\n\n/**\n * Creates initial empty metrics for a task type.\n */\nfunction createEmptyMetrics(): FallbackChainMetrics {\n return {\n totalAttempts: 0,\n successByPosition: [0, 0, 0, 0],\n avgSuccessPosition: 0,\n exhaustedCount: 0,\n };\n}\n\n/**\n * Creates initial metrics registry with empty metrics for all types.\n */\nfunction createEmptyMetricsRegistry(): Record<FallbackTaskType, FallbackChainMetrics> {\n return {\n code: createEmptyMetrics(),\n research: createEmptyMetrics(),\n documentation: createEmptyMetrics(),\n analysis: createEmptyMetrics(),\n general: createEmptyMetrics(),\n };\n}\n\n/**\n * Gets the fallback chain for a task type.\n *\n * @param taskType - Task type to get chain for\n * @param registry - Optional custom registry (uses default if not provided)\n * @returns Ordered list of CLIs to try\n *\n * @example\n * ```typescript\n * const chain = getFallbackChain('code');\n * // chain === ['claude', 'codex', 'gemini']\n * ```\n */\nexport function getFallbackChain(\n taskType: FallbackTaskType,\n registry: FallbackChainRegistry = DEFAULT_FALLBACK_CHAINS\n): FallbackChain {\n return registry[taskType];\n}\n\n/**\n * Filters a fallback chain to only include available CLIs.\n *\n * @param chain - Original fallback chain\n * @param availableClis - Set of currently available CLI names\n * @returns Filtered chain with only available CLIs\n *\n * @example\n * ```typescript\n * const filtered = filterAvailableClis(['claude', 'codex', 'gemini'], new Set(['claude', 'gemini']));\n * // filtered === ['claude', 'gemini']\n * ```\n */\nexport function filterAvailableClis(\n chain: FallbackChain,\n availableClis: ReadonlySet<CliName>\n): CliName[] {\n return chain.filter((cli) => availableClis.has(cli));\n}\n\n/**\n * Gets the next CLI to try in a fallback chain.\n *\n * @param chain - Fallback chain\n * @param currentPosition - Current position (0-indexed)\n * @returns Next CLI to try, or undefined if chain is exhausted\n */\nexport function getNextCli(chain: FallbackChain, currentPosition: number): CliName | undefined {\n const nextPosition = currentPosition + 1;\n return chain[nextPosition];\n}\n\n/**\n * Checks if a fallback chain is exhausted at the given position.\n *\n * @param chain - Fallback chain\n * @param currentPosition - Current position (0-indexed)\n * @returns True if no more CLIs to try\n */\nexport function isChainExhausted(chain: FallbackChain, currentPosition: number): boolean {\n return currentPosition >= chain.length - 1;\n}\n\n/**\n * Fallback chain manager with metrics tracking.\n */\nexport class FallbackChainManager {\n private readonly registry: FallbackChainRegistry;\n private readonly metrics: Record<FallbackTaskType, FallbackChainMetrics>;\n\n constructor(registry: FallbackChainRegistry = DEFAULT_FALLBACK_CHAINS) {\n this.registry = registry;\n this.metrics = createEmptyMetricsRegistry();\n }\n\n /**\n * Gets the fallback chain for a task type.\n */\n getChain(taskType: FallbackTaskType): FallbackChain {\n return getFallbackChain(taskType, this.registry);\n }\n\n /**\n * Gets the chain filtered by available CLIs.\n */\n getAvailableChain(taskType: FallbackTaskType, availableClis: ReadonlySet<CliName>): CliName[] {\n const chain = this.getChain(taskType);\n return filterAvailableClis(chain, availableClis);\n }\n\n /**\n * Records an outcome and updates metrics.\n *\n * @param outcome - The outcome to record\n */\n recordOutcome(outcome: FallbackOutcome): void {\n const currentMetrics = this.metrics[outcome.taskType];\n const newSuccessByPosition = [...currentMetrics.successByPosition];\n\n // Update success counts\n if (!outcome.exhausted && outcome.successPosition >= 0) {\n const position = outcome.successPosition;\n if (position < newSuccessByPosition.length) {\n const currentCount = newSuccessByPosition[position] ?? 0;\n newSuccessByPosition[position] = currentCount + 1;\n } else {\n // Extend array if needed\n while (newSuccessByPosition.length <= position) {\n newSuccessByPosition.push(0);\n }\n newSuccessByPosition[position] = 1;\n }\n }\n\n // Calculate new average success position\n const totalSuccesses = newSuccessByPosition.reduce((sum, count) => sum + count, 0);\n const weightedSum = newSuccessByPosition.reduce((sum, count, pos) => sum + count * pos, 0);\n const avgSuccessPosition = totalSuccesses > 0 ? weightedSum / totalSuccesses : 0;\n\n // Update metrics\n this.metrics[outcome.taskType] = {\n totalAttempts: currentMetrics.totalAttempts + 1,\n successByPosition: newSuccessByPosition,\n exhaustedCount: currentMetrics.exhaustedCount + (outcome.exhausted ? 1 : 0),\n avgSuccessPosition,\n };\n }\n\n /**\n * Gets metrics for a specific task type.\n */\n getMetrics(taskType: FallbackTaskType): FallbackChainMetrics {\n return this.metrics[taskType];\n }\n\n /**\n * Gets metrics for all task types.\n */\n getAllMetrics(): FallbackMetricsRegistry {\n return { ...this.metrics };\n }\n\n /**\n * Gets the success rate at each position for a task type.\n *\n * @param taskType - Task type to get rates for\n * @returns Array of success rates (0-1) by position\n */\n getSuccessRatesByPosition(taskType: FallbackTaskType): readonly number[] {\n const metrics = this.metrics[taskType];\n if (metrics.totalAttempts === 0) {\n return [];\n }\n return metrics.successByPosition.map((count) => count / metrics.totalAttempts);\n }\n\n /**\n * Gets overall success rate for a task type.\n */\n getOverallSuccessRate(taskType: FallbackTaskType): number {\n const metrics = this.metrics[taskType];\n if (metrics.totalAttempts === 0) {\n return 0;\n }\n const totalSuccesses = metrics.successByPosition.reduce((sum, count) => sum + count, 0);\n return totalSuccesses / metrics.totalAttempts;\n }\n\n /**\n * Resets metrics for all task types.\n */\n resetMetrics(): void {\n const types: FallbackTaskType[] = ['code', 'research', 'documentation', 'analysis', 'general'];\n for (const type of types) {\n this.metrics[type] = createEmptyMetrics();\n }\n }\n}\n\n/**\n * Creates a new FallbackChainManager instance.\n *\n * @param registry - Optional custom fallback chain registry\n * @returns New FallbackChainManager instance\n */\nexport function createFallbackChainManager(registry?: FallbackChainRegistry): FallbackChainManager {\n return new FallbackChainManager(registry);\n}\n\n/**\n * Creates a custom fallback chain registry by merging with defaults.\n *\n * @param overrides - Partial registry with overrides\n * @returns Complete registry with defaults for unspecified types\n */\nexport function createFallbackChainRegistry(\n overrides: Partial<FallbackChainRegistry>\n): FallbackChainRegistry {\n return {\n ...DEFAULT_FALLBACK_CHAINS,\n ...overrides,\n };\n}\n","/**\n * nexus-agents/cli-adapters - CLI Circuit Breaker Integration\n *\n * Wraps CLI adapter calls with circuit breaker pattern for resilient\n * multi-CLI execution with automatic fallback on failures.\n *\n * (Source: Issue #359 - Integrate circuit breaker with CLI adapters)\n */\n\nimport type { Result, ILogger } from '../core/index.js';\nimport { ok, err, createLogger, getTimeProvider } from '../core/index.js';\nimport type { TaskCategory } from '../config/task-specialization-types.js';\nimport type { FallbackTaskType } from './task-classifier.js';\nimport { getFallbackChainForCategory } from './fallback-chains.js';\nimport type { ICliAdapter, CliName, CliTask, CliResponse, CliError } from './types.js';\nimport {\n CircuitBreakerRegistry,\n CircuitError,\n mapCliErrorToCategory,\n type CircuitBreakerConfig,\n type CircuitBreakerSnapshot,\n type CircuitStateChangeListener,\n} from './circuit-breaker.js';\n\n/** Maps canonical TaskCategory (10 types) to FallbackTaskType (5 types). */\nconst CATEGORY_TO_FALLBACK: Record<TaskCategory, FallbackTaskType> = {\n code_generation: 'code',\n code_review: 'code',\n testing: 'code',\n research: 'research',\n exploration: 'research',\n documentation: 'documentation',\n architecture: 'analysis',\n security_review: 'analysis',\n planning: 'analysis',\n devops: 'general',\n};\n\n/** Configuration for CLI circuit breaker integration. */\nexport interface CliCircuitBreakerConfig {\n readonly perCliConfig?: Partial<Record<CliName, Partial<CircuitBreakerConfig>>>;\n readonly fallbackChain?: ReadonlyArray<CliName>;\n readonly enableFallback?: boolean;\n readonly maxFallbackAttempts?: number;\n}\n\n/** Result of a circuit-protected execution with fallback info. */\nexport interface CircuitProtectedResult {\n readonly response: CliResponse;\n readonly executedBy: CliName;\n readonly usedFallback: boolean;\n readonly fallbackAttempts?: ReadonlyArray<CliName>;\n}\n\n/** Health status for all CLIs with circuit state. */\nexport interface CliCircuitHealthStatus {\n readonly clis: ReadonlyArray<{\n readonly name: CliName;\n readonly healthy: boolean;\n readonly circuitState: 'closed' | 'open' | 'half-open';\n readonly failureCount: number;\n readonly lastFailureTime: number | null;\n }>;\n readonly systemHealthy: boolean;\n readonly healthyCount: number;\n readonly timestamp: number;\n}\n\n/** Interface for CLI circuit breaker integration. */\nexport interface ICliCircuitBreakerIntegration {\n execute(\n adapter: ICliAdapter,\n task: CliTask,\n taskCategory?: TaskCategory\n ): Promise<Result<CircuitProtectedResult, CircuitError | CliError>>;\n getHealthStatus(): CliCircuitHealthStatus;\n getCircuitSnapshots(): Map<CliName, CircuitBreakerSnapshot>;\n resetCircuit(cliName: CliName): void;\n resetAllCircuits(): void;\n addStateChangeListener(listener: CircuitStateChangeListener): void;\n}\n\nconst DEFAULT_FALLBACK_CHAIN: ReadonlyArray<CliName> = ['claude', 'gemini', 'codex', 'opencode'];\nconst DEFAULT_CONFIG: Required<CliCircuitBreakerConfig> = {\n perCliConfig: {},\n fallbackChain: DEFAULT_FALLBACK_CHAIN,\n enableFallback: true,\n maxFallbackAttempts: 2,\n};\n\n/**\n * Integrates circuit breaker pattern with CLI adapters.\n * Provides automatic fallback when a CLI's circuit opens.\n */\nexport class CliCircuitBreakerIntegration implements ICliCircuitBreakerIntegration {\n private readonly registry: CircuitBreakerRegistry;\n private readonly adapters: Map<CliName, ICliAdapter> = new Map();\n private readonly config: Required<CliCircuitBreakerConfig>;\n private readonly logger: ILogger;\n\n constructor(\n adapters: ReadonlyArray<ICliAdapter>,\n config?: CliCircuitBreakerConfig,\n logger?: ILogger\n ) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.logger = logger ?? createLogger({ component: 'cli-circuit-breaker-integration' });\n this.registry = new CircuitBreakerRegistry();\n for (const adapter of adapters) {\n this.adapters.set(adapter.name, adapter);\n this.registry.getBreaker(adapter.name, this.config.perCliConfig[adapter.name]);\n }\n }\n\n async execute(\n adapter: ICliAdapter,\n task: CliTask,\n taskCategory?: TaskCategory\n ): Promise<Result<CircuitProtectedResult, CircuitError | CliError>> {\n const primaryCli = adapter.name;\n const fallbackAttempts: CliName[] = [];\n let lastError: CircuitError | CliError | undefined;\n\n const primaryResult = await this.executeWithBreaker(adapter, task);\n if (primaryResult.ok) {\n return ok({ response: primaryResult.value, executedBy: primaryCli, usedFallback: false });\n }\n lastError = primaryResult.error;\n\n if (!this.config.enableFallback || !(lastError instanceof CircuitError)) {\n return err(lastError);\n }\n\n for (const cli of this.getFallbackClis(primaryCli, taskCategory).slice(\n 0,\n this.config.maxFallbackAttempts\n )) {\n const fallbackAdapter = this.adapters.get(cli);\n if (!fallbackAdapter) continue;\n fallbackAttempts.push(cli);\n this.logger.info('Attempting fallback', { from: primaryCli, to: cli });\n const result = await this.executeWithBreaker(fallbackAdapter, task);\n if (result.ok) {\n return ok({\n response: result.value,\n executedBy: cli,\n usedFallback: true,\n fallbackAttempts,\n });\n }\n lastError = result.error;\n }\n\n this.logger.warn('All fallback attempts failed', { primaryCli, fallbackAttempts });\n return err(lastError);\n }\n\n getHealthStatus(): CliCircuitHealthStatus {\n const snapshots = this.registry.getAllSnapshots();\n const clis: CliCircuitHealthStatus['clis'][number][] = [];\n let healthyCount = 0;\n for (const name of this.adapters.keys()) {\n const snapshot = snapshots.get(name);\n if (!snapshot) continue;\n const healthy = snapshot.state === 'closed';\n if (healthy) healthyCount++;\n clis.push({\n name,\n healthy,\n circuitState: snapshot.state,\n failureCount: snapshot.failureCount,\n lastFailureTime: snapshot.lastFailureTime,\n });\n }\n return {\n clis,\n systemHealthy: healthyCount > 0,\n healthyCount,\n timestamp: getTimeProvider().now(),\n };\n }\n\n getCircuitSnapshots(): Map<CliName, CircuitBreakerSnapshot> {\n return this.registry.getAllSnapshots();\n }\n\n resetCircuit(cliName: CliName): void {\n this.registry.reset(cliName);\n this.logger.info('Circuit reset', { cliName });\n }\n\n resetAllCircuits(): void {\n this.registry.resetAll();\n this.logger.info('All circuits reset');\n }\n\n addStateChangeListener(listener: CircuitStateChangeListener): void {\n this.registry.addGlobalStateChangeListener(listener);\n }\n\n private async executeWithBreaker(\n adapter: ICliAdapter,\n task: CliTask\n ): Promise<Result<CliResponse, CircuitError | CliError>> {\n const breaker = this.registry.getBreaker(adapter.name);\n const result = await breaker.execute(async () => {\n const execResult = await adapter.execute(task);\n if (!execResult.ok) {\n breaker.recordFailure(mapCliErrorToCategory(execResult.error.code));\n const wrappedError = new Error(execResult.error.message);\n (wrappedError as Error & { cliError: CliError }).cliError = execResult.error;\n throw wrappedError;\n }\n return execResult.value;\n });\n\n if (!result.ok) {\n if (result.error.circuitErrorCode === 'CIRCUIT_OPEN') return err(result.error);\n const wrapped = result.error.cause as (Error & { cliError?: CliError }) | undefined;\n if (wrapped?.cliError) return err(wrapped.cliError);\n return err(result.error);\n }\n return ok(result.value);\n }\n\n private getFallbackClis(excludeCli: CliName, taskCategory?: TaskCategory): CliName[] {\n const chain =\n taskCategory !== undefined\n ? getFallbackChainForCategory(taskCategory, CATEGORY_TO_FALLBACK[taskCategory])\n : this.config.fallbackChain;\n return [...chain].filter(\n (cli) => cli !== excludeCli && !this.registry.isOpen(cli) && this.adapters.has(cli)\n );\n }\n}\n\n/** Creates a CLI circuit breaker integration with the specified adapters. */\nexport function createCliCircuitBreakerIntegration(\n adapters: ReadonlyArray<ICliAdapter>,\n config?: CliCircuitBreakerConfig,\n logger?: ILogger\n): CliCircuitBreakerIntegration {\n return new CliCircuitBreakerIntegration(adapters, config, logger);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractSymbols
|
|
3
|
+
} from "./chunk-BQ4YXGGQ.js";
|
|
4
|
+
|
|
5
|
+
// src/indexer/codebase-search.ts
|
|
6
|
+
import { readdir } from "fs/promises";
|
|
7
|
+
import { resolve, extname, relative } from "path";
|
|
8
|
+
var SCORE_EXACT = 20;
|
|
9
|
+
var SCORE_PREFIX = 10;
|
|
10
|
+
var SCORE_WORD = 5;
|
|
11
|
+
var SCORE_SUBSTRING = 2;
|
|
12
|
+
var SCORE_EXPORTED_BONUS = 3;
|
|
13
|
+
function isSourceFile(name) {
|
|
14
|
+
const ext = extname(name).toLowerCase();
|
|
15
|
+
return [".ts", ".tsx", ".js", ".jsx"].includes(ext) && !name.endsWith(".test.ts") && !name.endsWith(".test.tsx") && !name.endsWith(".d.ts");
|
|
16
|
+
}
|
|
17
|
+
async function findSourceFiles(dir, maxDepth) {
|
|
18
|
+
if (maxDepth <= 0) return [];
|
|
19
|
+
const files = [];
|
|
20
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = resolve(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") {
|
|
24
|
+
files.push(...await findSourceFiles(fullPath, maxDepth - 1));
|
|
25
|
+
}
|
|
26
|
+
if (entry.isFile() && isSourceFile(entry.name)) {
|
|
27
|
+
files.push(fullPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
function scoreMatch(symbolName, query) {
|
|
33
|
+
const nameLower = symbolName.toLowerCase();
|
|
34
|
+
const queryLower = query.toLowerCase();
|
|
35
|
+
if (nameLower === queryLower) return SCORE_EXACT;
|
|
36
|
+
if (nameLower.startsWith(queryLower)) return SCORE_PREFIX;
|
|
37
|
+
const words = symbolName.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/);
|
|
38
|
+
if (words.some((w) => w === queryLower)) return SCORE_WORD;
|
|
39
|
+
if (nameLower.includes(queryLower)) return SCORE_SUBSTRING;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function getMatchType(score) {
|
|
43
|
+
if (score >= SCORE_EXACT) return "exact";
|
|
44
|
+
if (score >= SCORE_PREFIX) return "prefix";
|
|
45
|
+
if (score >= SCORE_WORD) return "word";
|
|
46
|
+
return "substring";
|
|
47
|
+
}
|
|
48
|
+
var CodebaseIndex = class {
|
|
49
|
+
symbols = [];
|
|
50
|
+
fileResults = /* @__PURE__ */ new Map();
|
|
51
|
+
rootDir;
|
|
52
|
+
constructor(rootDir) {
|
|
53
|
+
this.rootDir = rootDir;
|
|
54
|
+
}
|
|
55
|
+
/** Index all TS/JS source files in the directory. */
|
|
56
|
+
async index(maxDepth = 4) {
|
|
57
|
+
const files = await findSourceFiles(this.rootDir, maxDepth);
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const result = await extractSymbols(file);
|
|
60
|
+
const relPath = relative(this.rootDir, file);
|
|
61
|
+
this.fileResults.set(relPath, result);
|
|
62
|
+
for (const symbol of result.symbols) {
|
|
63
|
+
this.symbols.push({ ...symbol, filePath: relPath });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
totalFiles: files.length,
|
|
68
|
+
totalSymbols: this.symbols.length,
|
|
69
|
+
indexedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Search symbols by keyword. Returns top N results sorted by relevance. */
|
|
73
|
+
search(query, limit = 20) {
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const symbol of this.symbols) {
|
|
76
|
+
const baseScore = scoreMatch(symbol.name, query);
|
|
77
|
+
if (baseScore === null) continue;
|
|
78
|
+
const bonus = symbol.exported ? SCORE_EXPORTED_BONUS : 0;
|
|
79
|
+
results.push({
|
|
80
|
+
symbol,
|
|
81
|
+
score: baseScore + bonus,
|
|
82
|
+
matchType: getMatchType(baseScore)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
86
|
+
}
|
|
87
|
+
/** Get a compact summary of a file's symbols. */
|
|
88
|
+
getFileSummary(filePath) {
|
|
89
|
+
const result = this.fileResults.get(filePath);
|
|
90
|
+
if (result === void 0) return void 0;
|
|
91
|
+
const kinds = {};
|
|
92
|
+
let exported = 0;
|
|
93
|
+
let priv = 0;
|
|
94
|
+
for (const s of result.symbols) {
|
|
95
|
+
kinds[s.kind] = (kinds[s.kind] ?? 0) + 1;
|
|
96
|
+
if (s.exported) exported++;
|
|
97
|
+
else priv++;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
filePath,
|
|
101
|
+
totalLines: result.totalLines,
|
|
102
|
+
exportedSymbols: exported,
|
|
103
|
+
privateSymbols: priv,
|
|
104
|
+
kinds
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** List all indexed files with symbol counts. */
|
|
108
|
+
listFiles() {
|
|
109
|
+
return [...this.fileResults.entries()].map(([path, result]) => ({
|
|
110
|
+
path,
|
|
111
|
+
symbols: result.symbols.length,
|
|
112
|
+
lines: result.totalLines
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
/** Get index statistics. */
|
|
116
|
+
get stats() {
|
|
117
|
+
return {
|
|
118
|
+
files: this.fileResults.size,
|
|
119
|
+
symbols: this.symbols.length
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
CodebaseIndex
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=chunk-AP2FD37C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/indexer/codebase-search.ts"],"sourcesContent":["/**\n * Codebase search — keyword search across symbol indices.\n *\n * Builds an in-memory symbol index for a directory of TS/JS files,\n * then supports keyword search, file summaries, and symbol lookup.\n *\n * Inspired by Augment Code's Context Engine. Uses the existing\n * extractSymbols() function for AST parsing.\n *\n * @module indexer/codebase-search\n */\n\nimport { readdir } from 'node:fs/promises';\nimport { resolve, extname, relative } from 'node:path';\nimport {\n extractSymbols,\n type CodeSymbol,\n type SymbolExtractionResult,\n} from './symbol-extractor.js';\n\n/** A symbol with its source file path. */\nexport interface IndexedSymbol extends CodeSymbol {\n /** Relative file path from the indexed root. */\n filePath: string;\n}\n\n/** Search result with relevance scoring. */\nexport interface SearchResult {\n symbol: IndexedSymbol;\n /** Relevance score (higher = better match). */\n score: number;\n /** How the query matched (exact, prefix, substring, word). */\n matchType: 'exact' | 'prefix' | 'substring' | 'word';\n}\n\n/** File summary — compact overview of a source file. */\nexport interface FileSummary {\n filePath: string;\n totalLines: number;\n exportedSymbols: number;\n privateSymbols: number;\n kinds: Record<string, number>;\n}\n\n/** Index statistics. */\nexport interface IndexStats {\n totalFiles: number;\n totalSymbols: number;\n indexedAt: string;\n}\n\n// Score weights for different match types\nconst SCORE_EXACT = 20;\nconst SCORE_PREFIX = 10;\nconst SCORE_WORD = 5;\nconst SCORE_SUBSTRING = 2;\nconst SCORE_EXPORTED_BONUS = 3;\n\nfunction isSourceFile(name: string): boolean {\n const ext = extname(name).toLowerCase();\n return (\n ['.ts', '.tsx', '.js', '.jsx'].includes(ext) &&\n !name.endsWith('.test.ts') &&\n !name.endsWith('.test.tsx') &&\n !name.endsWith('.d.ts')\n );\n}\n\nasync function findSourceFiles(dir: string, maxDepth: number): Promise<string[]> {\n if (maxDepth <= 0) return [];\n const files: string[] = [];\n const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);\n for (const entry of entries) {\n const fullPath = resolve(dir, entry.name);\n if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {\n files.push(...(await findSourceFiles(fullPath, maxDepth - 1)));\n }\n if (entry.isFile() && isSourceFile(entry.name)) {\n files.push(fullPath);\n }\n }\n return files;\n}\n\nfunction scoreMatch(symbolName: string, query: string): SearchResult['score'] | null {\n const nameLower = symbolName.toLowerCase();\n const queryLower = query.toLowerCase();\n\n if (nameLower === queryLower) return SCORE_EXACT;\n if (nameLower.startsWith(queryLower)) return SCORE_PREFIX;\n\n // Word boundary match (camelCase splitting)\n const words = symbolName\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .split(/[\\s_-]+/);\n if (words.some((w) => w === queryLower)) return SCORE_WORD;\n\n if (nameLower.includes(queryLower)) return SCORE_SUBSTRING;\n\n return null;\n}\n\nfunction getMatchType(score: number): SearchResult['matchType'] {\n if (score >= SCORE_EXACT) return 'exact';\n if (score >= SCORE_PREFIX) return 'prefix';\n if (score >= SCORE_WORD) return 'word';\n return 'substring';\n}\n\n/** In-memory codebase symbol index. */\nexport class CodebaseIndex {\n private readonly symbols: IndexedSymbol[] = [];\n private readonly fileResults = new Map<string, SymbolExtractionResult>();\n private readonly rootDir: string;\n\n constructor(rootDir: string) {\n this.rootDir = rootDir;\n }\n\n /** Index all TS/JS source files in the directory. */\n async index(maxDepth = 4): Promise<IndexStats> {\n const files = await findSourceFiles(this.rootDir, maxDepth);\n\n for (const file of files) {\n const result = await extractSymbols(file);\n const relPath = relative(this.rootDir, file);\n this.fileResults.set(relPath, result);\n\n for (const symbol of result.symbols) {\n this.symbols.push({ ...symbol, filePath: relPath });\n }\n }\n\n return {\n totalFiles: files.length,\n totalSymbols: this.symbols.length,\n indexedAt: new Date().toISOString(),\n };\n }\n\n /** Search symbols by keyword. Returns top N results sorted by relevance. */\n search(query: string, limit = 20): SearchResult[] {\n const results: SearchResult[] = [];\n\n for (const symbol of this.symbols) {\n const baseScore = scoreMatch(symbol.name, query);\n if (baseScore === null) continue;\n\n const bonus = symbol.exported ? SCORE_EXPORTED_BONUS : 0;\n results.push({\n symbol,\n score: baseScore + bonus,\n matchType: getMatchType(baseScore),\n });\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n }\n\n /** Get a compact summary of a file's symbols. */\n getFileSummary(filePath: string): FileSummary | undefined {\n const result = this.fileResults.get(filePath);\n if (result === undefined) return undefined;\n\n const kinds: Record<string, number> = {};\n let exported = 0;\n let priv = 0;\n\n for (const s of result.symbols) {\n kinds[s.kind] = (kinds[s.kind] ?? 0) + 1;\n if (s.exported) exported++;\n else priv++;\n }\n\n return {\n filePath,\n totalLines: result.totalLines,\n exportedSymbols: exported,\n privateSymbols: priv,\n kinds,\n };\n }\n\n /** List all indexed files with symbol counts. */\n listFiles(): Array<{ path: string; symbols: number; lines: number }> {\n return [...this.fileResults.entries()].map(([path, result]) => ({\n path,\n symbols: result.symbols.length,\n lines: result.totalLines,\n }));\n }\n\n /** Get index statistics. */\n get stats(): { files: number; symbols: number } {\n return {\n files: this.fileResults.size,\n symbols: this.symbols.length,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// src/mcp/tools/repo-analyze.ts
|
|
2
|
+
function normalizeRepoId(input) {
|
|
3
|
+
const urlMatch = /github\.com\/([^/]+\/[^/]+?)(?:\.git)?(?:\/|$)/.exec(input);
|
|
4
|
+
const matched = urlMatch?.[1] ?? "";
|
|
5
|
+
if (matched.length > 0) return matched;
|
|
6
|
+
if (/^[^/]+\/[^/]+$/.test(input)) return input;
|
|
7
|
+
throw new Error(`Invalid repo format: "${input}". Use "owner/name" or a GitHub URL.`);
|
|
8
|
+
}
|
|
9
|
+
var PACKAGE_MANAGER_RULES = [
|
|
10
|
+
[["pnpm-lock.yaml"], "pnpm"],
|
|
11
|
+
[["yarn.lock"], "yarn"],
|
|
12
|
+
[["package-lock.json", "package.json"], "npm"],
|
|
13
|
+
[["Cargo.toml"], "cargo"],
|
|
14
|
+
[["go.mod"], "go"],
|
|
15
|
+
[["requirements.txt", "pyproject.toml"], "pip"],
|
|
16
|
+
[["Gemfile"], "bundler"],
|
|
17
|
+
[["pom.xml"], "maven"],
|
|
18
|
+
[["build.gradle", "build.gradle.kts"], "gradle"]
|
|
19
|
+
];
|
|
20
|
+
function detectPackageManager(entries) {
|
|
21
|
+
for (const [files, manager] of PACKAGE_MANAGER_RULES) {
|
|
22
|
+
if (files.some((f) => entries.includes(f))) return manager;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function detectCiProvider(entries) {
|
|
27
|
+
if (entries.includes(".github")) return "github-actions";
|
|
28
|
+
if (entries.includes(".gitlab-ci.yml")) return "gitlab-ci";
|
|
29
|
+
if (entries.includes("Jenkinsfile")) return "jenkins";
|
|
30
|
+
if (entries.includes(".circleci")) return "circleci";
|
|
31
|
+
if (entries.includes(".travis.yml")) return "travis";
|
|
32
|
+
if (entries.includes("azure-pipelines.yml")) return "azure-devops";
|
|
33
|
+
if (entries.includes("concourse")) return "concourse";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
var WORKFLOW_SECURITY_PATTERNS = [
|
|
37
|
+
["semgrep", "semgrep"],
|
|
38
|
+
["codeql", "codeql"],
|
|
39
|
+
["grype", "grype"],
|
|
40
|
+
["snyk", "snyk"]
|
|
41
|
+
];
|
|
42
|
+
function detectWorkflowSecurity(workflowEntries, existing) {
|
|
43
|
+
const wfLower = workflowEntries.map((w) => w.toLowerCase());
|
|
44
|
+
const found = [];
|
|
45
|
+
for (const [pattern, tool] of WORKFLOW_SECURITY_PATTERNS) {
|
|
46
|
+
if (!existing.includes(tool) && wfLower.some((w) => w.includes(pattern))) {
|
|
47
|
+
found.push(tool);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
function detectSecurityTooling(entries, workflowEntries) {
|
|
53
|
+
const tools = [];
|
|
54
|
+
if (entries.includes(".semgrep.yml") || entries.includes(".semgrep")) tools.push("semgrep");
|
|
55
|
+
if (entries.includes(".snyk")) tools.push("snyk");
|
|
56
|
+
if (entries.includes("SECURITY.md")) tools.push("security-policy");
|
|
57
|
+
if (entries.includes(".grype.yaml")) tools.push("grype");
|
|
58
|
+
if (entries.includes("CODEOWNERS")) tools.push("codeowners");
|
|
59
|
+
if (workflowEntries !== void 0) {
|
|
60
|
+
tools.push(...detectWorkflowSecurity(workflowEntries, tools));
|
|
61
|
+
}
|
|
62
|
+
return tools;
|
|
63
|
+
}
|
|
64
|
+
function detectFramework(entries) {
|
|
65
|
+
if (entries.includes("helmfile.yaml") || entries.includes("helmfile.yaml.gotmpl"))
|
|
66
|
+
return "helmfile";
|
|
67
|
+
if (entries.includes("next.config.js") || entries.includes("next.config.ts")) return "nextjs";
|
|
68
|
+
if (entries.includes("angular.json")) return "angular";
|
|
69
|
+
if (entries.includes("vite.config.ts") || entries.includes("vite.config.js")) return "vite";
|
|
70
|
+
if (entries.includes("tsconfig.json") && entries.includes("package.json")) return "typescript";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
var GAP_RULES = [
|
|
74
|
+
[["SECURITY.md"], "No SECURITY.md policy"],
|
|
75
|
+
[["CODEOWNERS"], "No CODEOWNERS file"],
|
|
76
|
+
[["LICENSE", "LICENSE.md"], "No LICENSE file"],
|
|
77
|
+
[
|
|
78
|
+
[".semgrep.yml", ".semgrep", ".grype.yaml", ".snyk"],
|
|
79
|
+
"No SAST/SCA security scanning configured"
|
|
80
|
+
],
|
|
81
|
+
// Test detection handled separately via detectTestInfra (supports monorepo + co-located patterns)
|
|
82
|
+
[[".gitignore"], "No .gitignore file"]
|
|
83
|
+
];
|
|
84
|
+
var LANGUAGE_SCANNER_MATRIX = {
|
|
85
|
+
TypeScript: {
|
|
86
|
+
sast: ["semgrep (p/typescript, p/nodejs)", "eslint-plugin-security"],
|
|
87
|
+
sca: ["osv-scanner", "npm audit"]
|
|
88
|
+
},
|
|
89
|
+
JavaScript: {
|
|
90
|
+
sast: ["semgrep (p/javascript, p/nodejs)", "eslint-plugin-security"],
|
|
91
|
+
sca: ["osv-scanner", "npm audit"]
|
|
92
|
+
},
|
|
93
|
+
Python: {
|
|
94
|
+
sast: ["semgrep (p/python)", "bandit"],
|
|
95
|
+
sca: ["osv-scanner", "pip-audit"]
|
|
96
|
+
},
|
|
97
|
+
Java: {
|
|
98
|
+
sast: ["semgrep (p/java)", "spotbugs + find-sec-bugs"],
|
|
99
|
+
sca: ["osv-scanner", "OWASP dependency-check"]
|
|
100
|
+
},
|
|
101
|
+
Go: {
|
|
102
|
+
sast: ["semgrep (p/golang)", "gosec"],
|
|
103
|
+
sca: ["osv-scanner", "govulncheck"]
|
|
104
|
+
},
|
|
105
|
+
Rust: {
|
|
106
|
+
sast: ["semgrep (p/rust)"],
|
|
107
|
+
sca: ["osv-scanner", "cargo-audit"]
|
|
108
|
+
},
|
|
109
|
+
"C++": {
|
|
110
|
+
sast: ["semgrep (p/c)", "cppcheck"],
|
|
111
|
+
sca: ["osv-scanner"]
|
|
112
|
+
},
|
|
113
|
+
C: {
|
|
114
|
+
sast: ["semgrep (p/c)", "cppcheck"],
|
|
115
|
+
sca: ["osv-scanner"]
|
|
116
|
+
},
|
|
117
|
+
Kotlin: {
|
|
118
|
+
sast: ["semgrep (p/kotlin)", "detekt"],
|
|
119
|
+
sca: ["osv-scanner", "OWASP dependency-check"]
|
|
120
|
+
},
|
|
121
|
+
Swift: {
|
|
122
|
+
sast: ["semgrep (p/swift)"],
|
|
123
|
+
sca: ["osv-scanner"]
|
|
124
|
+
},
|
|
125
|
+
Ruby: {
|
|
126
|
+
sast: ["semgrep (p/ruby)", "brakeman"],
|
|
127
|
+
sca: ["osv-scanner", "bundler-audit"]
|
|
128
|
+
},
|
|
129
|
+
PHP: {
|
|
130
|
+
sast: ["semgrep (p/php)", "phpstan"],
|
|
131
|
+
sca: ["osv-scanner", "composer audit"]
|
|
132
|
+
},
|
|
133
|
+
Shell: {
|
|
134
|
+
sast: ["semgrep (p/bash)", "shellcheck"],
|
|
135
|
+
sca: []
|
|
136
|
+
},
|
|
137
|
+
HCL: {
|
|
138
|
+
sast: ["semgrep (p/terraform)", "tfsec"],
|
|
139
|
+
sca: ["osv-scanner"]
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function getLanguageRecommendations(language, securityTooling) {
|
|
143
|
+
if (language === null) return [];
|
|
144
|
+
const scanners = LANGUAGE_SCANNER_MATRIX[language];
|
|
145
|
+
if (scanners === void 0) return [];
|
|
146
|
+
const hasSast = securityTooling.includes("semgrep") || securityTooling.includes("snyk");
|
|
147
|
+
const hasSca = securityTooling.includes("osv-scanner") || securityTooling.includes("grype") || securityTooling.includes("snyk");
|
|
148
|
+
const recs = [];
|
|
149
|
+
if (!hasSast && scanners.sast.length > 0) {
|
|
150
|
+
const tools = scanners.sast.join(", ");
|
|
151
|
+
recs.push(`${language} project missing SAST: ${tools}`);
|
|
152
|
+
}
|
|
153
|
+
if (!hasSca && scanners.sca.length > 0) {
|
|
154
|
+
const tools = scanners.sca.join(", ");
|
|
155
|
+
recs.push(`${language} project missing SCA: ${tools}`);
|
|
156
|
+
}
|
|
157
|
+
return recs;
|
|
158
|
+
}
|
|
159
|
+
var SAST_TOOLS = /* @__PURE__ */ new Set(["semgrep", "codeql", "snyk"]);
|
|
160
|
+
var SAST_GAP_MSG = "No SAST/SCA security scanning configured";
|
|
161
|
+
function removeSastGapIfToolDetected(gaps, secTools) {
|
|
162
|
+
if (secTools.some((t) => SAST_TOOLS.has(t))) {
|
|
163
|
+
const idx = gaps.indexOf(SAST_GAP_MSG);
|
|
164
|
+
if (idx !== -1) gaps.splice(idx, 1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function identifyGaps(entries, ciProvider, language, securityTooling) {
|
|
168
|
+
const gaps = [];
|
|
169
|
+
if (ciProvider === null) gaps.push("No CI/CD configuration detected");
|
|
170
|
+
for (const [files, message] of GAP_RULES) {
|
|
171
|
+
if (!files.some((f) => entries.includes(f))) gaps.push(message);
|
|
172
|
+
}
|
|
173
|
+
if (!detectTestInfra(entries)) gaps.push("No test directory detected");
|
|
174
|
+
removeSastGapIfToolDetected(gaps, securityTooling ?? []);
|
|
175
|
+
const hasGenericSecGap = gaps.includes("No SAST/SCA security scanning configured");
|
|
176
|
+
if (hasGenericSecGap && language !== null && language !== void 0 && securityTooling !== void 0) {
|
|
177
|
+
const langRecs = getLanguageRecommendations(language, securityTooling);
|
|
178
|
+
gaps.push(...langRecs);
|
|
179
|
+
}
|
|
180
|
+
return gaps;
|
|
181
|
+
}
|
|
182
|
+
function analyzeRepo(metadata, topLevelEntries, workflowEntries) {
|
|
183
|
+
const ciProvider = detectCiProvider(topLevelEntries);
|
|
184
|
+
const secTooling = detectSecurityTooling(topLevelEntries, workflowEntries);
|
|
185
|
+
const hasTests = detectTestInfra(topLevelEntries);
|
|
186
|
+
return {
|
|
187
|
+
name: metadata.full_name,
|
|
188
|
+
language: metadata.language,
|
|
189
|
+
framework: detectFramework(topLevelEntries),
|
|
190
|
+
packageManager: detectPackageManager(topLevelEntries),
|
|
191
|
+
ciProvider,
|
|
192
|
+
securityTooling: secTooling,
|
|
193
|
+
hasDockerfile: topLevelEntries.includes("Dockerfile") || topLevelEntries.includes("docker-compose.yml") || topLevelEntries.includes("docker-compose.yaml"),
|
|
194
|
+
hasHelmCharts: topLevelEntries.includes("Chart.yaml") || topLevelEntries.includes("charts") || topLevelEntries.includes("helm"),
|
|
195
|
+
hasMakefile: topLevelEntries.includes("Makefile"),
|
|
196
|
+
hasTests,
|
|
197
|
+
license: metadata.license?.spdx_id ?? null,
|
|
198
|
+
description: metadata.description,
|
|
199
|
+
defaultBranch: metadata.default_branch,
|
|
200
|
+
stars: metadata.stargazers_count,
|
|
201
|
+
topLevelEntries: [...topLevelEntries],
|
|
202
|
+
gaps: identifyGaps(topLevelEntries, ciProvider, metadata.language, secTooling)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
var MARKUP_LANGUAGES = /* @__PURE__ */ new Set([
|
|
206
|
+
"HTML",
|
|
207
|
+
"CSS",
|
|
208
|
+
"SCSS",
|
|
209
|
+
"Less",
|
|
210
|
+
"Markdown",
|
|
211
|
+
"Roff",
|
|
212
|
+
"SVG",
|
|
213
|
+
"XML",
|
|
214
|
+
"XSLT",
|
|
215
|
+
"Mustache",
|
|
216
|
+
"Handlebars",
|
|
217
|
+
"EJS"
|
|
218
|
+
]);
|
|
219
|
+
function detectPrimaryLanguage(languages, fallback) {
|
|
220
|
+
const sorted = Object.entries(languages).filter(([lang]) => !MARKUP_LANGUAGES.has(lang)).sort((a, b) => b[1] - a[1]);
|
|
221
|
+
const top = sorted[0];
|
|
222
|
+
return top !== void 0 ? top[0] : fallback;
|
|
223
|
+
}
|
|
224
|
+
function detectTestInfra(entries) {
|
|
225
|
+
const testDirs = ["tests", "test", "__tests__", "spec"];
|
|
226
|
+
if (testDirs.some((d) => entries.includes(d))) return true;
|
|
227
|
+
const testConfigs = [
|
|
228
|
+
"vitest.config.ts",
|
|
229
|
+
"vitest.config.js",
|
|
230
|
+
"vitest.config.mts",
|
|
231
|
+
"vitest.workspace.ts",
|
|
232
|
+
"vitest.workspace.js",
|
|
233
|
+
"jest.config.ts",
|
|
234
|
+
"jest.config.js",
|
|
235
|
+
"jest.config.mjs",
|
|
236
|
+
"cypress.config.ts",
|
|
237
|
+
"cypress.config.js",
|
|
238
|
+
"playwright.config.ts",
|
|
239
|
+
".mocharc.yml",
|
|
240
|
+
".mocharc.json"
|
|
241
|
+
];
|
|
242
|
+
if (testConfigs.some((c) => entries.includes(c))) return true;
|
|
243
|
+
return entries.includes("packages") && entries.includes("package.json");
|
|
244
|
+
}
|
|
245
|
+
function inferLanguageFromEntries(entries, fallback) {
|
|
246
|
+
if (entries.includes("tsconfig.json")) return "TypeScript";
|
|
247
|
+
if (entries.includes("Cargo.toml")) return "Rust";
|
|
248
|
+
if (entries.includes("go.mod")) return "Go";
|
|
249
|
+
if (entries.includes("pyproject.toml") || entries.includes("setup.py")) return "Python";
|
|
250
|
+
if (entries.includes("pom.xml") || entries.includes("build.gradle")) return "Java";
|
|
251
|
+
if (entries.includes("Gemfile")) return "Ruby";
|
|
252
|
+
if (entries.includes("package.json")) return "JavaScript";
|
|
253
|
+
return fallback;
|
|
254
|
+
}
|
|
255
|
+
async function getExecFile() {
|
|
256
|
+
const { execFile } = await import("child_process");
|
|
257
|
+
const { promisify } = await import("util");
|
|
258
|
+
return promisify(execFile);
|
|
259
|
+
}
|
|
260
|
+
async function fetchRepoData(repoId, exec) {
|
|
261
|
+
const { stdout: metaJson } = await exec(
|
|
262
|
+
"gh",
|
|
263
|
+
[
|
|
264
|
+
"api",
|
|
265
|
+
`repos/${repoId}`,
|
|
266
|
+
"--jq",
|
|
267
|
+
"{name: .name, full_name: .full_name, description: .description, language: .language, default_branch: .default_branch, stargazers_count: .stargazers_count, license: .license}"
|
|
268
|
+
],
|
|
269
|
+
{ timeout: 3e4 }
|
|
270
|
+
);
|
|
271
|
+
let metadata;
|
|
272
|
+
try {
|
|
273
|
+
metadata = JSON.parse(metaJson.trim());
|
|
274
|
+
} catch {
|
|
275
|
+
throw new Error(`Failed to parse repo metadata for ${repoId}: ${metaJson.slice(0, 200)}`);
|
|
276
|
+
}
|
|
277
|
+
const { stdout: contentsJson } = await exec(
|
|
278
|
+
"gh",
|
|
279
|
+
["api", `repos/${repoId}/contents`, "--jq", "[.[].name]"],
|
|
280
|
+
{ timeout: 3e4 }
|
|
281
|
+
);
|
|
282
|
+
let entries;
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(contentsJson.trim());
|
|
285
|
+
entries = Array.isArray(parsed) ? parsed.filter((e) => typeof e === "string") : [];
|
|
286
|
+
} catch {
|
|
287
|
+
throw new Error(`Failed to parse repo contents for ${repoId}: ${contentsJson.slice(0, 200)}`);
|
|
288
|
+
}
|
|
289
|
+
return { metadata, entries };
|
|
290
|
+
}
|
|
291
|
+
async function resolveLicense(repoId, exec) {
|
|
292
|
+
try {
|
|
293
|
+
const { stdout } = await exec(
|
|
294
|
+
"gh",
|
|
295
|
+
["api", `repos/${repoId}/license`, "--jq", ".license.spdx_id"],
|
|
296
|
+
{ timeout: 15e3 }
|
|
297
|
+
);
|
|
298
|
+
const spdxId = stdout.trim();
|
|
299
|
+
if (spdxId !== "" && spdxId !== "null" && spdxId !== "NOASSERTION") {
|
|
300
|
+
return spdxId;
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
async function resolveLanguage(repoId, entries, metadata, exec) {
|
|
307
|
+
let languages = {};
|
|
308
|
+
try {
|
|
309
|
+
const { stdout } = await exec("gh", ["api", `repos/${repoId}/languages`], { timeout: 15e3 });
|
|
310
|
+
languages = JSON.parse(stdout.trim());
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
const primary = detectPrimaryLanguage(languages, metadata.language);
|
|
314
|
+
if (primary === null || MARKUP_LANGUAGES.has(primary)) {
|
|
315
|
+
return inferLanguageFromEntries(entries, primary);
|
|
316
|
+
}
|
|
317
|
+
return primary;
|
|
318
|
+
}
|
|
319
|
+
async function fetchWorkflowEntries(repoId, exec) {
|
|
320
|
+
try {
|
|
321
|
+
const { stdout } = await exec(
|
|
322
|
+
"gh",
|
|
323
|
+
["api", `repos/${repoId}/contents/.github/workflows`, "--jq", "[.[].name]"],
|
|
324
|
+
{ timeout: 15e3 }
|
|
325
|
+
);
|
|
326
|
+
const parsed = JSON.parse(stdout.trim());
|
|
327
|
+
return Array.isArray(parsed) ? parsed.filter((e) => typeof e === "string") : [];
|
|
328
|
+
} catch {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function analyzeGitHubRepo(input) {
|
|
333
|
+
const repoId = normalizeRepoId(input.repo);
|
|
334
|
+
const exec = await getExecFile();
|
|
335
|
+
const { metadata, entries } = await fetchRepoData(repoId, exec);
|
|
336
|
+
const primaryLang = await resolveLanguage(repoId, entries, metadata, exec);
|
|
337
|
+
const enhanced = { ...metadata, language: primaryLang };
|
|
338
|
+
const hasLicenseFile = entries.includes("LICENSE") || entries.includes("LICENSE.md");
|
|
339
|
+
const licenseUnresolved = enhanced.license === null || enhanced.license.spdx_id === "NOASSERTION";
|
|
340
|
+
if (licenseUnresolved && hasLicenseFile) {
|
|
341
|
+
const resolved = await resolveLicense(repoId, exec);
|
|
342
|
+
if (resolved !== null) enhanced.license = { spdx_id: resolved };
|
|
343
|
+
}
|
|
344
|
+
const workflowEntries = entries.includes(".github") ? await fetchWorkflowEntries(repoId, exec) : [];
|
|
345
|
+
return analyzeRepo(enhanced, entries, workflowEntries);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export {
|
|
349
|
+
normalizeRepoId,
|
|
350
|
+
detectPackageManager,
|
|
351
|
+
detectCiProvider,
|
|
352
|
+
detectSecurityTooling,
|
|
353
|
+
detectFramework,
|
|
354
|
+
getLanguageRecommendations,
|
|
355
|
+
identifyGaps,
|
|
356
|
+
analyzeRepo,
|
|
357
|
+
analyzeGitHubRepo
|
|
358
|
+
};
|
|
359
|
+
//# sourceMappingURL=chunk-BC3M4VLP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp/tools/repo-analyze.ts"],"sourcesContent":["/* eslint-disable max-lines -- cohesive module, governance allows 400-600 */\n/**\n * nexus-agents/mcp - Repository Analyze Logic\n *\n * Inspects a GitHub repository and returns structured analysis\n * including language, tooling, CI, security, and gap identification.\n *\n * @module mcp/tools/repo-analyze\n * (Source: Issue #1074)\n */\n\nimport type { RepoAnalyzeInput, RepoAnalysis } from './repo-analyze-types.js';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Normalize \"owner/repo\" from either \"owner/repo\" or full GitHub URL. */\nexport function normalizeRepoId(input: string): string {\n const urlMatch = /github\\.com\\/([^/]+\\/[^/]+?)(?:\\.git)?(?:\\/|$)/.exec(input);\n const matched = urlMatch?.[1] ?? '';\n if (matched.length > 0) return matched;\n if (/^[^/]+\\/[^/]+$/.test(input)) return input;\n throw new Error(`Invalid repo format: \"${input}\". Use \"owner/name\" or a GitHub URL.`);\n}\n\n/** Package manager detection rules: [files, manager]. First match wins. */\nconst PACKAGE_MANAGER_RULES: ReadonlyArray<readonly [readonly string[], string]> = [\n [['pnpm-lock.yaml'], 'pnpm'],\n [['yarn.lock'], 'yarn'],\n [['package-lock.json', 'package.json'], 'npm'],\n [['Cargo.toml'], 'cargo'],\n [['go.mod'], 'go'],\n [['requirements.txt', 'pyproject.toml'], 'pip'],\n [['Gemfile'], 'bundler'],\n [['pom.xml'], 'maven'],\n [['build.gradle', 'build.gradle.kts'], 'gradle'],\n];\n\n/** Detect package manager from top-level files. */\nexport function detectPackageManager(entries: readonly string[]): string | null {\n for (const [files, manager] of PACKAGE_MANAGER_RULES) {\n if (files.some((f) => entries.includes(f))) return manager;\n }\n return null;\n}\n\n/** Detect CI provider from directory structure. */\nexport function detectCiProvider(entries: readonly string[]): string | null {\n if (entries.includes('.github')) return 'github-actions';\n if (entries.includes('.gitlab-ci.yml')) return 'gitlab-ci';\n if (entries.includes('Jenkinsfile')) return 'jenkins';\n if (entries.includes('.circleci')) return 'circleci';\n if (entries.includes('.travis.yml')) return 'travis';\n if (entries.includes('azure-pipelines.yml')) return 'azure-devops';\n if (entries.includes('concourse')) return 'concourse';\n return null;\n}\n\n/** Tool names detectable from CI workflow filenames (#1674). */\nconst WORKFLOW_SECURITY_PATTERNS: ReadonlyArray<readonly [string, string]> = [\n ['semgrep', 'semgrep'],\n ['codeql', 'codeql'],\n ['grype', 'grype'],\n ['snyk', 'snyk'],\n];\n\n/** Detect security tools from .github/workflows/ filenames. */\nfunction detectWorkflowSecurity(\n workflowEntries: readonly string[],\n existing: readonly string[]\n): readonly string[] {\n const wfLower = workflowEntries.map((w) => w.toLowerCase());\n const found: string[] = [];\n for (const [pattern, tool] of WORKFLOW_SECURITY_PATTERNS) {\n if (!existing.includes(tool) && wfLower.some((w) => w.includes(pattern))) {\n found.push(tool);\n }\n }\n return found;\n}\n\n/** Detect security tooling from root files and CI workflow filenames (#1674). */\nexport function detectSecurityTooling(\n entries: readonly string[],\n workflowEntries?: readonly string[]\n): readonly string[] {\n const tools: string[] = [];\n if (entries.includes('.semgrep.yml') || entries.includes('.semgrep')) tools.push('semgrep');\n if (entries.includes('.snyk')) tools.push('snyk');\n if (entries.includes('SECURITY.md')) tools.push('security-policy');\n if (entries.includes('.grype.yaml')) tools.push('grype');\n if (entries.includes('CODEOWNERS')) tools.push('codeowners');\n if (workflowEntries !== undefined) {\n tools.push(...detectWorkflowSecurity(workflowEntries, tools));\n }\n return tools;\n}\n\n/** Detect framework from package manager config. */\nexport function detectFramework(entries: readonly string[]): string | null {\n if (entries.includes('helmfile.yaml') || entries.includes('helmfile.yaml.gotmpl'))\n return 'helmfile';\n if (entries.includes('next.config.js') || entries.includes('next.config.ts')) return 'nextjs';\n if (entries.includes('angular.json')) return 'angular';\n if (entries.includes('vite.config.ts') || entries.includes('vite.config.js')) return 'vite';\n if (entries.includes('tsconfig.json') && entries.includes('package.json')) return 'typescript';\n return null;\n}\n\n/** Gap detection rules: [files-any-present, gap message]. */\nconst GAP_RULES: ReadonlyArray<readonly [readonly string[], string]> = [\n [['SECURITY.md'], 'No SECURITY.md policy'],\n [['CODEOWNERS'], 'No CODEOWNERS file'],\n [['LICENSE', 'LICENSE.md'], 'No LICENSE file'],\n [\n ['.semgrep.yml', '.semgrep', '.grype.yaml', '.snyk'],\n 'No SAST/SCA security scanning configured',\n ],\n // Test detection handled separately via detectTestInfra (supports monorepo + co-located patterns)\n [['.gitignore'], 'No .gitignore file'],\n];\n\n/** Scanner recommendation per language. Canonical source: secure-language-stacks. */\ninterface LanguageScanners {\n readonly sast: readonly string[];\n readonly sca: readonly string[];\n}\n\nconst LANGUAGE_SCANNER_MATRIX: Readonly<Record<string, LanguageScanners>> = {\n TypeScript: {\n sast: ['semgrep (p/typescript, p/nodejs)', 'eslint-plugin-security'],\n sca: ['osv-scanner', 'npm audit'],\n },\n JavaScript: {\n sast: ['semgrep (p/javascript, p/nodejs)', 'eslint-plugin-security'],\n sca: ['osv-scanner', 'npm audit'],\n },\n Python: {\n sast: ['semgrep (p/python)', 'bandit'],\n sca: ['osv-scanner', 'pip-audit'],\n },\n Java: {\n sast: ['semgrep (p/java)', 'spotbugs + find-sec-bugs'],\n sca: ['osv-scanner', 'OWASP dependency-check'],\n },\n Go: {\n sast: ['semgrep (p/golang)', 'gosec'],\n sca: ['osv-scanner', 'govulncheck'],\n },\n Rust: {\n sast: ['semgrep (p/rust)'],\n sca: ['osv-scanner', 'cargo-audit'],\n },\n 'C++': {\n sast: ['semgrep (p/c)', 'cppcheck'],\n sca: ['osv-scanner'],\n },\n C: {\n sast: ['semgrep (p/c)', 'cppcheck'],\n sca: ['osv-scanner'],\n },\n Kotlin: {\n sast: ['semgrep (p/kotlin)', 'detekt'],\n sca: ['osv-scanner', 'OWASP dependency-check'],\n },\n Swift: {\n sast: ['semgrep (p/swift)'],\n sca: ['osv-scanner'],\n },\n Ruby: {\n sast: ['semgrep (p/ruby)', 'brakeman'],\n sca: ['osv-scanner', 'bundler-audit'],\n },\n PHP: {\n sast: ['semgrep (p/php)', 'phpstan'],\n sca: ['osv-scanner', 'composer audit'],\n },\n Shell: {\n sast: ['semgrep (p/bash)', 'shellcheck'],\n sca: [],\n },\n HCL: {\n sast: ['semgrep (p/terraform)', 'tfsec'],\n sca: ['osv-scanner'],\n },\n};\n\n/** Generate language-specific scanner recommendations when SAST/SCA is missing. */\nexport function getLanguageRecommendations(\n language: string | null,\n securityTooling: readonly string[]\n): readonly string[] {\n if (language === null) return [];\n const scanners = LANGUAGE_SCANNER_MATRIX[language];\n if (scanners === undefined) return [];\n\n const hasSast = securityTooling.includes('semgrep') || securityTooling.includes('snyk');\n const hasSca =\n securityTooling.includes('osv-scanner') ||\n securityTooling.includes('grype') ||\n securityTooling.includes('snyk');\n\n const recs: string[] = [];\n if (!hasSast && scanners.sast.length > 0) {\n const tools = scanners.sast.join(', ');\n recs.push(`${language} project missing SAST: ${tools}`);\n }\n if (!hasSca && scanners.sca.length > 0) {\n const tools = scanners.sca.join(', ');\n recs.push(`${language} project missing SCA: ${tools}`);\n }\n return recs;\n}\n\n/** SAST tool names that suppress the generic gap message. */\nconst SAST_TOOLS = new Set(['semgrep', 'codeql', 'snyk']);\nconst SAST_GAP_MSG = 'No SAST/SCA security scanning configured';\n\n/** Remove the SAST/SCA gap if any SAST tool was detected (#1674). */\nfunction removeSastGapIfToolDetected(gaps: string[], secTools: readonly string[]): void {\n if (secTools.some((t) => SAST_TOOLS.has(t))) {\n const idx = gaps.indexOf(SAST_GAP_MSG);\n if (idx !== -1) gaps.splice(idx, 1);\n }\n}\n\n/** Identify gaps in repository best practices. */\nexport function identifyGaps(\n entries: readonly string[],\n ciProvider: string | null,\n language?: string | null,\n securityTooling?: readonly string[]\n): readonly string[] {\n const gaps: string[] = [];\n if (ciProvider === null) gaps.push('No CI/CD configuration detected');\n for (const [files, message] of GAP_RULES) {\n if (!files.some((f) => entries.includes(f))) gaps.push(message);\n }\n // Test detection: uses detectTestInfra for monorepo + co-located pattern support (#1130)\n if (!detectTestInfra(entries)) gaps.push('No test directory detected');\n\n // Remove SAST/SCA gap if workflow-level security was detected (#1674)\n removeSastGapIfToolDetected(gaps, securityTooling ?? []);\n\n // Language-specific recommendations when generic SAST/SCA gap detected\n const hasGenericSecGap = gaps.includes('No SAST/SCA security scanning configured');\n if (\n hasGenericSecGap &&\n language !== null &&\n language !== undefined &&\n securityTooling !== undefined\n ) {\n const langRecs = getLanguageRecommendations(language, securityTooling);\n gaps.push(...langRecs);\n }\n\n return gaps;\n}\n\n// ============================================================================\n// Core\n// ============================================================================\n\n/** GitHub repo metadata from the API. */\nexport interface GhRepoMetadata {\n readonly name: string;\n readonly full_name: string;\n readonly description: string | null;\n readonly language: string | null;\n readonly default_branch: string;\n readonly stargazers_count: number;\n readonly license: { readonly spdx_id: string } | null;\n}\n\n/** Analyze a GitHub repository given its metadata and file tree. */\nexport function analyzeRepo(\n metadata: GhRepoMetadata,\n topLevelEntries: readonly string[],\n workflowEntries?: readonly string[]\n): RepoAnalysis {\n const ciProvider = detectCiProvider(topLevelEntries);\n const secTooling = detectSecurityTooling(topLevelEntries, workflowEntries);\n const hasTests = detectTestInfra(topLevelEntries);\n\n return {\n name: metadata.full_name,\n language: metadata.language,\n framework: detectFramework(topLevelEntries),\n packageManager: detectPackageManager(topLevelEntries),\n ciProvider,\n securityTooling: secTooling,\n hasDockerfile:\n topLevelEntries.includes('Dockerfile') ||\n topLevelEntries.includes('docker-compose.yml') ||\n topLevelEntries.includes('docker-compose.yaml'),\n hasHelmCharts:\n topLevelEntries.includes('Chart.yaml') ||\n topLevelEntries.includes('charts') ||\n topLevelEntries.includes('helm'),\n hasMakefile: topLevelEntries.includes('Makefile'),\n hasTests,\n license: metadata.license?.spdx_id ?? null,\n description: metadata.description,\n defaultBranch: metadata.default_branch,\n stars: metadata.stargazers_count,\n topLevelEntries: [...topLevelEntries],\n gaps: identifyGaps(topLevelEntries, ciProvider, metadata.language, secTooling),\n };\n}\n\n/** Non-code languages to exclude when detecting primary language. */\nconst MARKUP_LANGUAGES = new Set([\n 'HTML',\n 'CSS',\n 'SCSS',\n 'Less',\n 'Markdown',\n 'Roff',\n 'SVG',\n 'XML',\n 'XSLT',\n 'Mustache',\n 'Handlebars',\n 'EJS',\n]);\n\n/** Detect primary language from GitHub languages API (byte counts). */\nfunction detectPrimaryLanguage(\n languages: Record<string, number>,\n fallback: string | null\n): string | null {\n const sorted = Object.entries(languages)\n .filter(([lang]) => !MARKUP_LANGUAGES.has(lang))\n .sort((a, b) => b[1] - a[1]);\n const top = sorted[0];\n return top !== undefined ? top[0] : fallback;\n}\n\n/** Check for test infrastructure beyond top-level directories. */\nfunction detectTestInfra(entries: readonly string[]): boolean {\n const testDirs = ['tests', 'test', '__tests__', 'spec'];\n if (testDirs.some((d) => entries.includes(d))) return true;\n // Check for test config files (co-located test pattern, monorepos)\n const testConfigs = [\n 'vitest.config.ts',\n 'vitest.config.js',\n 'vitest.config.mts',\n 'vitest.workspace.ts',\n 'vitest.workspace.js',\n 'jest.config.ts',\n 'jest.config.js',\n 'jest.config.mjs',\n 'cypress.config.ts',\n 'cypress.config.js',\n 'playwright.config.ts',\n '.mocharc.yml',\n '.mocharc.json',\n ];\n if (testConfigs.some((c) => entries.includes(c))) return true;\n // Monorepo: packages/ dir + package.json implies co-located tests\n return entries.includes('packages') && entries.includes('package.json');\n}\n\n/** Infer code language from project files when GitHub reports markup. */\nfunction inferLanguageFromEntries(\n entries: readonly string[],\n fallback: string | null\n): string | null {\n if (entries.includes('tsconfig.json')) return 'TypeScript';\n if (entries.includes('Cargo.toml')) return 'Rust';\n if (entries.includes('go.mod')) return 'Go';\n if (entries.includes('pyproject.toml') || entries.includes('setup.py')) return 'Python';\n if (entries.includes('pom.xml') || entries.includes('build.gradle')) return 'Java';\n if (entries.includes('Gemfile')) return 'Ruby';\n if (entries.includes('package.json')) return 'JavaScript';\n return fallback;\n}\n\ntype ExecFileFn = (\n cmd: string,\n args: string[],\n options?: { timeout?: number }\n) => Promise<{ stdout: string }>;\n\n/** Lazy-load promisified execFile. */\nasync function getExecFile(): Promise<ExecFileFn> {\n const { execFile } = await import('node:child_process');\n const { promisify } = await import('node:util');\n return promisify(execFile);\n}\n\n/** Fetch repo metadata and languages via GitHub API. */\nasync function fetchRepoData(\n repoId: string,\n exec: ExecFileFn\n): Promise<{ metadata: GhRepoMetadata; entries: string[] }> {\n const { stdout: metaJson } = await exec(\n 'gh',\n [\n 'api',\n `repos/${repoId}`,\n '--jq',\n '{name: .name, full_name: .full_name, description: .description, language: .language, default_branch: .default_branch, stargazers_count: .stargazers_count, license: .license}',\n ],\n { timeout: 30_000 }\n );\n let metadata: GhRepoMetadata;\n try {\n metadata = JSON.parse(metaJson.trim()) as GhRepoMetadata;\n } catch {\n throw new Error(`Failed to parse repo metadata for ${repoId}: ${metaJson.slice(0, 200)}`);\n }\n const { stdout: contentsJson } = await exec(\n 'gh',\n ['api', `repos/${repoId}/contents`, '--jq', '[.[].name]'],\n { timeout: 30_000 }\n );\n let entries: string[];\n try {\n const parsed: unknown = JSON.parse(contentsJson.trim());\n entries = Array.isArray(parsed) ? parsed.filter((e): e is string => typeof e === 'string') : [];\n } catch {\n throw new Error(`Failed to parse repo contents for ${repoId}: ${contentsJson.slice(0, 200)}`);\n }\n return { metadata, entries };\n}\n\n/** Resolve NOASSERTION license via the GitHub license API. */\nasync function resolveLicense(repoId: string, exec: ExecFileFn): Promise<string | null> {\n try {\n const { stdout } = await exec(\n 'gh',\n ['api', `repos/${repoId}/license`, '--jq', '.license.spdx_id'],\n { timeout: 15_000 }\n );\n const spdxId = stdout.trim();\n if (spdxId !== '' && spdxId !== 'null' && spdxId !== 'NOASSERTION') {\n return spdxId;\n }\n } catch {\n // Keep NOASSERTION if license API also fails\n }\n return null;\n}\n\n/** Resolve primary language, falling back to entry inference for markup repos. */\nasync function resolveLanguage(\n repoId: string,\n entries: readonly string[],\n metadata: GhRepoMetadata,\n exec: ExecFileFn\n): Promise<string | null> {\n let languages: Record<string, number> = {};\n try {\n const { stdout } = await exec('gh', ['api', `repos/${repoId}/languages`], { timeout: 15_000 });\n languages = JSON.parse(stdout.trim()) as Record<string, number>;\n } catch {\n /* fall back to metadata.language */\n }\n const primary = detectPrimaryLanguage(languages, metadata.language);\n if (primary === null || MARKUP_LANGUAGES.has(primary)) {\n return inferLanguageFromEntries(entries, primary);\n }\n return primary;\n}\n\n/** Fetch workflow filenames from .github/workflows/ (#1674). Best-effort. */\nasync function fetchWorkflowEntries(repoId: string, exec: ExecFileFn): Promise<readonly string[]> {\n try {\n const { stdout } = await exec(\n 'gh',\n ['api', `repos/${repoId}/contents/.github/workflows`, '--jq', '[.[].name]'],\n { timeout: 15_000 }\n );\n const parsed: unknown = JSON.parse(stdout.trim());\n return Array.isArray(parsed) ? parsed.filter((e): e is string => typeof e === 'string') : [];\n } catch {\n return []; // No workflows directory or API error — graceful fallback\n }\n}\n\n/** Fetch repo data from GitHub and produce analysis. */\nexport async function analyzeGitHubRepo(input: RepoAnalyzeInput): Promise<RepoAnalysis> {\n const repoId = normalizeRepoId(input.repo);\n const exec = await getExecFile();\n const { metadata, entries } = await fetchRepoData(repoId, exec);\n\n const primaryLang = await resolveLanguage(repoId, entries, metadata, exec);\n const enhanced = { ...metadata, language: primaryLang };\n\n // Resolve null or NOASSERTION license when LICENSE file exists\n const hasLicenseFile = entries.includes('LICENSE') || entries.includes('LICENSE.md');\n const licenseUnresolved = enhanced.license === null || enhanced.license.spdx_id === 'NOASSERTION';\n if (licenseUnresolved && hasLicenseFile) {\n const resolved = await resolveLicense(repoId, exec);\n if (resolved !== null) enhanced.license = { spdx_id: resolved };\n }\n\n // Fetch workflow filenames for CI-level security detection (#1674)\n const workflowEntries = entries.includes('.github')\n ? await fetchWorkflowEntries(repoId, exec)\n : [];\n\n return analyzeRepo(enhanced, entries, workflowEntries);\n}\n"],"mappings":"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","names":[]}
|