cycode 3.15.3.dev8__tar.gz → 3.15.4.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/PKG-INFO +1 -1
  2. cycode-3.15.4.dev1/cycode/__init__.py +1 -0
  3. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/command_utils.py +25 -0
  4. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/consts.py +23 -0
  5. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/hooks_manager.py +51 -138
  6. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/ides/__init__.py +44 -0
  7. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/ides/base.py +156 -0
  8. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/ides/claude_code.py +389 -0
  9. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/ides/cursor.py +119 -0
  10. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/install_command.py +14 -23
  11. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/scan/handlers.py +46 -89
  12. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/scan/payload.py +34 -0
  13. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/scan/scan_command.py +154 -0
  14. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/scan/types.py +43 -0
  15. cycode-3.15.4.dev1/cycode/cli/apps/ai_guardrails/session_start_command.py +91 -0
  16. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/status_command.py +13 -16
  17. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/uninstall_command.py +12 -22
  18. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/pyproject.toml +1 -1
  19. cycode-3.15.3.dev8/cycode/__init__.py +0 -1
  20. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/command_utils.py +0 -68
  21. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/consts.py +0 -155
  22. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/claude_config.py +0 -159
  23. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/cursor_config.py +0 -36
  24. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/payload.py +0 -275
  25. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/response_builders.py +0 -135
  26. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/scan_command.py +0 -142
  27. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/types.py +0 -65
  28. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/session_start_command.py +0 -155
  29. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/LICENCE +0 -0
  30. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/README.md +0 -0
  31. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/__main__.py +0 -0
  32. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/__init__.py +0 -0
  33. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/app.py +0 -0
  34. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/__init__.py +0 -0
  35. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/activation_manager.py +0 -0
  36. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/__init__.py +0 -0
  37. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/scan/__init__.py +0 -0
  38. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/scan/consts.py +0 -0
  39. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/scan/policy.py +0 -0
  40. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_guardrails/scan/utils.py +0 -0
  41. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_remediation/__init__.py +0 -0
  42. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_remediation/ai_remediation_command.py +0 -0
  43. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_remediation/apply_fix.py +0 -0
  44. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ai_remediation/print_remediation.py +0 -0
  45. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/api/__init__.py +0 -0
  46. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/api/api_command.py +0 -0
  47. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/api/openapi_spec.py +0 -0
  48. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/auth/__init__.py +0 -0
  49. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/auth/auth_command.py +0 -0
  50. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/auth/auth_common.py +0 -0
  51. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/auth/auth_manager.py +0 -0
  52. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/auth/models.py +0 -0
  53. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/configure/__init__.py +0 -0
  54. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/configure/configure_command.py +0 -0
  55. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/configure/consts.py +0 -0
  56. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/configure/messages.py +0 -0
  57. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/configure/prompts.py +0 -0
  58. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ignore/__init__.py +0 -0
  59. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/ignore/ignore_command.py +0 -0
  60. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/mcp/__init__.py +0 -0
  61. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/mcp/mcp_command.py +0 -0
  62. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/__init__.py +0 -0
  63. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/report_command.py +0 -0
  64. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/__init__.py +0 -0
  65. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/common.py +0 -0
  66. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/path/__init__.py +0 -0
  67. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/path/path_command.py +0 -0
  68. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/repository_url/__init__.py +0 -0
  69. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/repository_url/repository_url_command.py +0 -0
  70. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/sbom_command.py +0 -0
  71. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report/sbom/sbom_report_file.py +0 -0
  72. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report_import/__init__.py +0 -0
  73. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report_import/report_import_command.py +0 -0
  74. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report_import/sbom/__init__.py +0 -0
  75. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/report_import/sbom/sbom_command.py +0 -0
  76. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/sca_options.py +0 -0
  77. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/__init__.py +0 -0
  78. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/aggregation_report.py +0 -0
  79. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/code_scanner.py +0 -0
  80. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/commit_history/__init__.py +0 -0
  81. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/commit_history/commit_history_command.py +0 -0
  82. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/commit_range_scanner.py +0 -0
  83. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/detection_excluder.py +0 -0
  84. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/path/__init__.py +0 -0
  85. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/path/path_command.py +0 -0
  86. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/pre_commit/__init__.py +0 -0
  87. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/pre_commit/pre_commit_command.py +0 -0
  88. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/pre_push/__init__.py +0 -0
  89. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/pre_push/pre_push_command.py +0 -0
  90. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/pre_receive/__init__.py +0 -0
  91. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/pre_receive/pre_receive_command.py +0 -0
  92. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/remote_url_resolver.py +0 -0
  93. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/repository/__init__.py +0 -0
  94. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/repository/repository_command.py +0 -0
  95. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/scan_ci/__init__.py +0 -0
  96. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/scan_ci/ci_integrations.py +0 -0
  97. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/scan_ci/scan_ci_command.py +0 -0
  98. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/scan_command.py +0 -0
  99. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/scan_parameters.py +0 -0
  100. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/scan/scan_result.py +0 -0
  101. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/status/__init__.py +0 -0
  102. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/status/get_cli_status.py +0 -0
  103. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/status/models.py +0 -0
  104. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/status/status_command.py +0 -0
  105. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/apps/status/version_command.py +0 -0
  106. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/cli_types.py +0 -0
  107. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/config.py +0 -0
  108. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/console.py +0 -0
  109. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/consts.py +0 -0
  110. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/__init__.py +0 -0
  111. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/custom_exceptions.py +0 -0
  112. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/handle_ai_remediation_errors.py +0 -0
  113. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/handle_auth_errors.py +0 -0
  114. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/handle_errors.py +0 -0
  115. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/handle_report_sbom_errors.py +0 -0
  116. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/exceptions/handle_scan_errors.py +0 -0
  117. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/__init__.py +0 -0
  118. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/commit_range_documents.py +0 -0
  119. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/documents_walk_ignore.py +0 -0
  120. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/file_excluder.py +0 -0
  121. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/iac/__init__.py +0 -0
  122. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/iac/tf_content_generator.py +0 -0
  123. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/models/__init__.py +0 -0
  124. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/models/in_memory_zip.py +0 -0
  125. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/path_documents.py +0 -0
  126. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/repository_documents.py +0 -0
  127. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/__init__.py +0 -0
  128. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/base_restore_dependencies.py +0 -0
  129. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/go/__init__.py +0 -0
  130. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +0 -0
  131. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/maven/__init__.py +0 -0
  132. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +0 -0
  133. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +0 -0
  134. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/npm/__init__.py +0 -0
  135. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +0 -0
  136. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +0 -0
  137. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +0 -0
  138. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +0 -0
  139. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/nuget/__init__.py +0 -0
  140. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py +0 -0
  141. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/php/__init__.py +0 -0
  142. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +0 -0
  143. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/python/__init__.py +0 -0
  144. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +0 -0
  145. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +0 -0
  146. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +0 -0
  147. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/ruby/__init__.py +0 -0
  148. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py +0 -0
  149. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/sbt/__init__.py +0 -0
  150. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py +0 -0
  151. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/sca/sca_file_collector.py +0 -0
  152. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/walk_ignore.py +0 -0
  153. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/files_collector/zip_documents.py +0 -0
  154. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/logger.py +0 -0
  155. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/main.py +0 -0
  156. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/models.py +0 -0
  157. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/__init__.py +0 -0
  158. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/console_printer.py +0 -0
  159. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/json_printer.py +0 -0
  160. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/printer_base.py +0 -0
  161. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/rich_printer.py +0 -0
  162. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/tables/__init__.py +0 -0
  163. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/tables/sca_table_printer.py +0 -0
  164. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/tables/table.py +0 -0
  165. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/tables/table_models.py +0 -0
  166. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/tables/table_printer.py +0 -0
  167. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/tables/table_printer_base.py +0 -0
  168. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/text_printer.py +0 -0
  169. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/__init__.py +0 -0
  170. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/code_snippet_syntax.py +0 -0
  171. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/detection_data.py +0 -0
  172. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/detection_ordering/__init__.py +0 -0
  173. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/detection_ordering/common_ordering.py +0 -0
  174. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/detection_ordering/sca_ordering.py +0 -0
  175. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/printers/utils/rich_helpers.py +0 -0
  176. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/user_settings/__init__.py +0 -0
  177. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/user_settings/base_file_manager.py +0 -0
  178. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/user_settings/config_file_manager.py +0 -0
  179. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/user_settings/configuration_manager.py +0 -0
  180. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/user_settings/credentials_manager.py +0 -0
  181. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/user_settings/jwt_creator.py +0 -0
  182. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/__init__.py +0 -0
  183. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/binary_utils.py +0 -0
  184. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/enum_utils.py +0 -0
  185. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/get_api_client.py +0 -0
  186. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/git_proxy.py +0 -0
  187. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/ignore_utils.py +0 -0
  188. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/jwt_utils.py +0 -0
  189. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/path_utils.py +0 -0
  190. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/progress_bar.py +0 -0
  191. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/scan_batch.py +0 -0
  192. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/scan_utils.py +0 -0
  193. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/shell_executor.py +0 -0
  194. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/string_utils.py +0 -0
  195. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/task_timer.py +0 -0
  196. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/url_utils.py +0 -0
  197. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/version_checker.py +0 -0
  198. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cli/utils/yaml_utils.py +0 -0
  199. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/config.py +0 -0
  200. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/__init__.py +0 -0
  201. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/ai_security_manager_client.py +0 -0
  202. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/ai_security_manager_service_config.py +0 -0
  203. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/auth_client.py +0 -0
  204. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/base_token_auth_client.py +0 -0
  205. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/cli_activation_client.py +0 -0
  206. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/client_creator.py +0 -0
  207. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/config.py +0 -0
  208. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/config_dev.py +0 -0
  209. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/cycode_client.py +0 -0
  210. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/cycode_client_base.py +0 -0
  211. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/cycode_dev_based_client.py +0 -0
  212. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/cycode_oidc_based_client.py +0 -0
  213. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/cycode_token_based_client.py +0 -0
  214. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/headers.py +0 -0
  215. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/import_sbom_client.py +0 -0
  216. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/logger.py +0 -0
  217. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/models.py +0 -0
  218. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/report_client.py +0 -0
  219. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/scan_client.py +0 -0
  220. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/cyclient/scan_config_base.py +0 -0
  221. {cycode-3.15.3.dev8 → cycode-3.15.4.dev1}/cycode/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.15.3.dev8
3
+ Version: 3.15.4.dev1
4
4
  Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
5
5
  License-Expression: MIT
6
6
  License-File: LICENCE
@@ -0,0 +1 @@
1
+ __version__ = '3.15.4.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
@@ -0,0 +1,25 @@
1
+ """Common utilities for AI guardrails commands."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def validate_scope(scope: str, allowed_scopes: tuple[str, ...] = ('user', 'repo')) -> None:
14
+ """Validate scope parameter."""
15
+ if scope not in allowed_scopes:
16
+ scopes_list = ', '.join(f'"{s}"' for s in allowed_scopes)
17
+ console.print(f'[red]Error:[/] Invalid scope. Use {scopes_list}.', style='bold red')
18
+ raise typer.Exit(1)
19
+
20
+
21
+ def resolve_repo_path(scope: str, repo_path: Optional[Path]) -> Optional[Path]:
22
+ """Default repo_path to cwd for 'repo' scope; leave None for 'user' scope."""
23
+ if scope == 'repo' and repo_path is None:
24
+ return Path(os.getcwd())
25
+ return repo_path
@@ -0,0 +1,23 @@
1
+ """Shared constants and policy/mode enums for AI guardrails."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class PolicyMode(str, Enum):
7
+ """Policy enforcement mode for global mode and per-feature actions."""
8
+
9
+ BLOCK = 'block'
10
+ WARN = 'warn'
11
+
12
+
13
+ class InstallMode(str, Enum):
14
+ """Installation mode for ai-guardrails install command."""
15
+
16
+ REPORT = 'report'
17
+ BLOCK = 'block'
18
+
19
+
20
+ # Base CLI commands invoked from installed hooks. IDE classes append --ide flags
21
+ # (and any other suffix) on top of these.
22
+ CYCODE_SCAN_PROMPT_COMMAND = 'cycode ai-guardrails scan'
23
+ CYCODE_SESSION_START_COMMAND = 'cycode ai-guardrails session-start'
@@ -1,8 +1,8 @@
1
- """
2
- Hooks manager for AI guardrails.
1
+ """Hooks manager for AI guardrails.
3
2
 
4
- Handles installation, removal, and status checking of AI IDE hooks.
5
- Supports multiple IDEs: Cursor, Claude Code (future).
3
+ Generic install/uninstall/status logic. All IDE-specific concerns (settings
4
+ paths, hooks template shape) live on the `IDE` instance; this module is
5
+ agent-agnostic.
6
6
  """
7
7
 
8
8
  import copy
@@ -12,47 +12,45 @@ from typing import Optional
12
12
 
13
13
  import yaml
14
14
 
15
- from cycode.cli.apps.ai_guardrails.consts import (
16
- DEFAULT_IDE,
17
- IDE_CONFIGS,
18
- AIIDEType,
19
- PolicyMode,
20
- get_hooks_config,
21
- )
15
+ from cycode.cli.apps.ai_guardrails.consts import PolicyMode
16
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE
22
17
  from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_POLICY, POLICY_FILE_NAME
23
18
  from cycode.logger import get_logger
24
19
 
25
20
  logger = get_logger('AI Guardrails Hooks')
26
21
 
27
22
 
28
- def get_hooks_path(scope: str, repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE) -> Path:
29
- """Get the hooks.json path for the given scope and IDE.
23
+ _CYCODE_COMMAND_MARKERS = ('cycode ai-guardrails',)
24
+
30
25
 
31
- Args:
32
- scope: 'user' for user-level hooks, 'repo' for repository-level hooks
33
- repo_path: Repository path (required if scope is 'repo')
34
- ide: The AI IDE type (default: Cursor)
35
- """
36
- config = IDE_CONFIGS[ide]
37
- if scope == 'repo' and repo_path:
38
- return repo_path / config.repo_hooks_subdir / config.hooks_file_name
39
- return config.hooks_dir / config.hooks_file_name
26
+ def _is_cycode_command(command: str) -> bool:
27
+ return any(marker in command for marker in _CYCODE_COMMAND_MARKERS)
28
+
29
+
30
+ def is_cycode_hook_entry(entry: dict) -> bool:
31
+ """Detect Cycode hook entries in both Cursor (flat) and Claude Code (nested) shapes."""
32
+ command = entry.get('command', '')
33
+ if _is_cycode_command(command):
34
+ return True
35
+
36
+ for hook in entry.get('hooks', []):
37
+ if isinstance(hook, dict) and _is_cycode_command(hook.get('command', '')):
38
+ return True
39
+
40
+ return False
40
41
 
41
42
 
42
- def load_hooks_file(hooks_path: Path) -> Optional[dict]:
43
- """Load existing hooks.json file."""
43
+ def _load_hooks_file(hooks_path: Path) -> Optional[dict]:
44
44
  if not hooks_path.exists():
45
45
  return None
46
46
  try:
47
- content = hooks_path.read_text(encoding='utf-8')
48
- return json.loads(content)
47
+ return json.loads(hooks_path.read_text(encoding='utf-8'))
49
48
  except Exception as e:
50
49
  logger.debug('Failed to load hooks file', exc_info=e)
51
50
  return None
52
51
 
53
52
 
54
- def save_hooks_file(hooks_path: Path, hooks_config: dict) -> bool:
55
- """Save hooks.json file."""
53
+ def _save_hooks_file(hooks_path: Path, hooks_config: dict) -> bool:
56
54
  try:
57
55
  hooks_path.parent.mkdir(parents=True, exist_ok=True)
58
56
  hooks_path.write_text(json.dumps(hooks_config, indent=2), encoding='utf-8')
@@ -62,39 +60,7 @@ def save_hooks_file(hooks_path: Path, hooks_config: dict) -> bool:
62
60
  return False
63
61
 
64
62
 
65
- _CYCODE_COMMAND_MARKERS = ('cycode ai-guardrails',)
66
-
67
-
68
- def _is_cycode_command(command: str) -> bool:
69
- return any(marker in command for marker in _CYCODE_COMMAND_MARKERS)
70
-
71
-
72
- def is_cycode_hook_entry(entry: dict) -> bool:
73
- """Check if a hook entry is from cycode-cli.
74
-
75
- Handles both Cursor format (flat) and Claude Code format (nested).
76
-
77
- Cursor format: {"command": "cycode ai-guardrails scan"}
78
- Claude Code format: {"hooks": [{"type": "command", "command": "cycode ai-guardrails scan --ide claude-code"}]}
79
- """
80
- # Check Cursor format (flat command)
81
- command = entry.get('command', '')
82
- if _is_cycode_command(command):
83
- return True
84
-
85
- # Check Claude Code format (nested hooks array)
86
- hooks = entry.get('hooks', [])
87
- for hook in hooks:
88
- if isinstance(hook, dict):
89
- hook_command = hook.get('command', '')
90
- if _is_cycode_command(hook_command):
91
- return True
92
-
93
- return False
94
-
95
-
96
- def _load_policy(policy_path: Path) -> dict:
97
- """Load existing policy file merged with defaults, or return defaults if not found."""
63
+ def _load_policy_dict(policy_path: Path) -> dict:
98
64
  if not policy_path.exists():
99
65
  return copy.deepcopy(DEFAULT_POLICY)
100
66
  try:
@@ -107,22 +73,13 @@ def _load_policy(policy_path: Path) -> dict:
107
73
  def create_policy_file(scope: str, mode: PolicyMode, repo_path: Optional[Path] = None) -> tuple[bool, str]:
108
74
  """Create or update the ai-guardrails.yaml policy file.
109
75
 
110
- If the file already exists, only the mode field is updated.
111
- If it doesn't exist, a new file is created from the default policy.
112
-
113
- Args:
114
- scope: 'user' for user-level, 'repo' for repository-level
115
- mode: The policy mode to set
116
- repo_path: Repository path (required if scope is 'repo')
117
-
118
- Returns:
119
- Tuple of (success, message)
76
+ If the file already exists, only the mode field is updated; otherwise a new
77
+ file is created from the default policy.
120
78
  """
121
79
  config_dir = repo_path / '.cycode' if scope == 'repo' and repo_path else Path.home() / '.cycode'
122
80
  policy_path = config_dir / POLICY_FILE_NAME
123
81
 
124
- policy = _load_policy(policy_path)
125
-
82
+ policy = _load_policy_dict(policy_path)
126
83
  policy['mode'] = mode.value
127
84
 
128
85
  try:
@@ -135,35 +92,21 @@ def create_policy_file(scope: str, mode: PolicyMode, repo_path: Optional[Path] =
135
92
 
136
93
 
137
94
  def install_hooks(
95
+ ide: IDE,
138
96
  scope: str = 'user',
139
97
  repo_path: Optional[Path] = None,
140
- ide: AIIDEType = DEFAULT_IDE,
141
98
  report_mode: bool = False,
142
99
  ) -> tuple[bool, str]:
143
- """
144
- Install Cycode AI guardrails hooks.
145
-
146
- Args:
147
- scope: 'user' for user-level hooks, 'repo' for repository-level hooks
148
- repo_path: Repository path (required if scope is 'repo')
149
- ide: The AI IDE type (default: Cursor)
150
- report_mode: If True, install hooks in async mode (non-blocking)
100
+ """Install Cycode AI guardrails hooks for ``ide``."""
101
+ hooks_path = ide.settings_path(scope, repo_path)
151
102
 
152
- Returns:
153
- Tuple of (success, message)
154
- """
155
- hooks_path = get_hooks_path(scope, repo_path, ide)
156
-
157
- # Load existing hooks or create new
158
- existing = load_hooks_file(hooks_path) or {'version': 1, 'hooks': {}}
103
+ existing = _load_hooks_file(hooks_path) or {'version': 1, 'hooks': {}}
159
104
  existing.setdefault('version', 1)
160
105
  existing.setdefault('hooks', {})
161
106
 
162
- # Get IDE-specific hooks configuration
163
- hooks_config = get_hooks_config(ide, async_mode=report_mode)
107
+ rendered = ide.render_hooks_config(async_mode=report_mode)
164
108
 
165
- # Add/update Cycode hooks
166
- for event, entries in hooks_config['hooks'].items():
109
+ for event, entries in rendered['hooks'].items():
167
110
  existing['hooks'].setdefault(event, [])
168
111
 
169
112
  # Remove any existing Cycode entries for this event
@@ -173,47 +116,31 @@ def install_hooks(
173
116
  for entry in entries:
174
117
  existing['hooks'][event].append(entry)
175
118
 
176
- # Save
177
- if save_hooks_file(hooks_path, existing):
119
+ if _save_hooks_file(hooks_path, existing):
178
120
  return True, f'AI guardrails hooks installed: {hooks_path}'
179
121
  return False, f'Failed to install hooks to {hooks_path}'
180
122
 
181
123
 
182
- def uninstall_hooks(
183
- scope: str = 'user', repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE
184
- ) -> tuple[bool, str]:
185
- """
186
- Remove Cycode AI guardrails hooks.
187
-
188
- Args:
189
- scope: 'user' for user-level hooks, 'repo' for repository-level hooks
190
- repo_path: Repository path (required if scope is 'repo')
191
- ide: The AI IDE type (default: Cursor)
192
-
193
- Returns:
194
- Tuple of (success, message)
195
- """
196
- hooks_path = get_hooks_path(scope, repo_path, ide)
124
+ def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> tuple[bool, str]:
125
+ """Remove Cycode AI guardrails hooks for ``ide``."""
126
+ hooks_path = ide.settings_path(scope, repo_path)
197
127
 
198
- existing = load_hooks_file(hooks_path)
128
+ existing = _load_hooks_file(hooks_path)
199
129
  if existing is None:
200
130
  return True, f'No hooks file found at {hooks_path}'
201
131
 
202
- # Remove Cycode entries from all events
203
132
  modified = False
204
133
  for event in list(existing.get('hooks', {}).keys()):
205
134
  original_count = len(existing['hooks'][event])
206
135
  existing['hooks'][event] = [e for e in existing['hooks'][event] if not is_cycode_hook_entry(e)]
207
136
  if len(existing['hooks'][event]) != original_count:
208
137
  modified = True
209
- # Remove empty event lists
210
138
  if not existing['hooks'][event]:
211
139
  del existing['hooks'][event]
212
140
 
213
141
  if not modified:
214
142
  return True, 'No Cycode hooks found to remove'
215
143
 
216
- # Save or delete if empty
217
144
  if not existing.get('hooks'):
218
145
  try:
219
146
  hooks_path.unlink()
@@ -222,48 +149,35 @@ def uninstall_hooks(
222
149
  logger.debug('Failed to delete hooks file', exc_info=e)
223
150
  return False, f'Failed to remove hooks file: {hooks_path}'
224
151
 
225
- if save_hooks_file(hooks_path, existing):
152
+ if _save_hooks_file(hooks_path, existing):
226
153
  return True, f'Cycode hooks removed from: {hooks_path}'
227
154
  return False, f'Failed to update hooks file: {hooks_path}'
228
155
 
229
156
 
230
- def get_hooks_status(scope: str = 'user', repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE) -> dict:
231
- """
232
- Get the status of AI guardrails hooks.
233
-
234
- Args:
235
- scope: 'user' for user-level hooks, 'repo' for repository-level hooks
236
- repo_path: Repository path (required if scope is 'repo')
237
- ide: The AI IDE type (default: Cursor)
238
-
239
- Returns:
240
- Dict with status information
241
- """
242
- hooks_path = get_hooks_path(scope, repo_path, ide)
157
+ def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> dict:
158
+ """Return installation status of Cycode hooks for ``ide``."""
159
+ hooks_path = ide.settings_path(scope, repo_path)
243
160
 
244
- status = {
161
+ status: dict = {
245
162
  'scope': scope,
246
- 'ide': ide.value,
247
- 'ide_name': IDE_CONFIGS[ide].name,
163
+ 'ide': ide.name,
164
+ 'ide_name': ide.display_name,
248
165
  'hooks_path': str(hooks_path),
249
166
  'file_exists': hooks_path.exists(),
250
167
  'cycode_installed': False,
251
168
  'hooks': {},
252
169
  }
253
170
 
254
- existing = load_hooks_file(hooks_path)
171
+ existing = _load_hooks_file(hooks_path)
255
172
  if existing is None:
256
173
  return status
257
174
 
258
- # Check each hook event for this IDE
259
- ide_config = IDE_CONFIGS[ide]
260
175
  has_cycode_hooks = False
261
- for event in ide_config.hook_events:
262
- # Handle event:matcher format
176
+ for event in ide.hook_events:
177
+ # '<event>:<matcher>' filters entries to a specific tool/matcher.
263
178
  if ':' in event:
264
179
  actual_event, matcher_prefix = event.split(':', 1)
265
180
  all_entries = existing.get('hooks', {}).get(actual_event, [])
266
- # Filter entries by matcher
267
181
  entries = [e for e in all_entries if e.get('matcher', '').startswith(matcher_prefix)]
268
182
  else:
269
183
  entries = existing.get('hooks', {}).get(event, [])
@@ -278,5 +192,4 @@ def get_hooks_status(scope: str = 'user', repo_path: Optional[Path] = None, ide:
278
192
  }
279
193
 
280
194
  status['cycode_installed'] = has_cycode_hooks
281
-
282
195
  return status
@@ -0,0 +1,44 @@
1
+ """Registry of supported AI guardrails IDE integrations.
2
+
3
+ Adding a new IDE: create `ides/<name>.py` with a subclass of `IDE`, import it
4
+ here, and include an instance in the `IDES` tuple. Nothing else in the package
5
+ needs to change.
6
+ """
7
+
8
+ import typer
9
+
10
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE
11
+ from cycode.cli.apps.ai_guardrails.ides.claude_code import ClaudeCode
12
+ from cycode.cli.apps.ai_guardrails.ides.cursor import Cursor
13
+
14
+ # Single source of truth: name → singleton instance.
15
+ # `--ide` choices and install/uninstall/status iteration both derive from this.
16
+ IDES: dict[str, IDE] = {ide.name: ide for ide in (Cursor(), ClaudeCode())}
17
+
18
+ # Default IDE used when `--ide` is omitted. Kept here so the value is colocated
19
+ # with the registry; no module outside `ides/` needs to know which IDE wins.
20
+ DEFAULT_IDE_NAME = 'cursor'
21
+
22
+
23
+ def get_ide(name: str) -> IDE:
24
+ """Look up the IDE integration registered under ``name``.
25
+
26
+ Raises ``typer.BadParameter`` when the name is unknown — surfaces as a
27
+ user-friendly CLI error rather than a KeyError stack trace.
28
+ """
29
+ ide = IDES.get(name.lower())
30
+ if ide is None:
31
+ valid = ', '.join(IDES.keys())
32
+ raise typer.BadParameter(f'Unknown IDE "{name}". Supported: {valid}.')
33
+ return ide
34
+
35
+
36
+ def resolve_ides(name: str) -> list[IDE]:
37
+ """Resolve an ``--ide`` argument to one or all IDE instances.
38
+
39
+ ``"all"`` returns every registered IDE; anything else returns a single
40
+ matching IDE (raising ``typer.BadParameter`` for unknown names).
41
+ """
42
+ if name.lower() == 'all':
43
+ return list(IDES.values())
44
+ return [get_ide(name)]
@@ -0,0 +1,156 @@
1
+ """Base abstractions for AI guardrails IDE integrations.
2
+
3
+ Each AI IDE (Cursor, Claude Code, …) is represented by a subclass of `IDE`
4
+ that consolidates every IDE-specific concern in a single module: settings file
5
+ paths, hooks template rendering, payload parsing, response building, and any
6
+ IDE-specific session-context lookup.
7
+
8
+ Adding a new IDE is a matter of:
9
+ 1. Subclassing `IDE` and implementing the abstract methods.
10
+ 2. Registering the instance in `cycode/cli/apps/ai_guardrails/ides/__init__.py`.
11
+
12
+ The `HookDecision` dataclass is the canonical, IDE-agnostic return type for
13
+ event handlers; `IDE.build_hook_response` translates it into the IDE-specific
14
+ JSON response shape that the IDE expects on stdout.
15
+ """
16
+
17
+ from abc import ABC, abstractmethod
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import ClassVar, Optional
22
+
23
+ from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
24
+ from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
25
+
26
+
27
+ class DecisionAction(str, Enum):
28
+ """Canonical decision action returned by event handlers."""
29
+
30
+ ALLOW = 'allow'
31
+ DENY = 'deny'
32
+ ASK = 'ask'
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class HookDecision:
37
+ """Canonical, IDE-agnostic decision returned by event handlers.
38
+
39
+ Carries the event type so `IDE.build_hook_response` can pick the right
40
+ IDE-specific response shape (Cursor's "permission" style for tool events
41
+ vs. "continue" style for prompts; Claude Code's "hookSpecificOutput"
42
+ vs. "decision: block").
43
+ """
44
+
45
+ action: DecisionAction
46
+ event_type: AiHookEventType
47
+ user_message: Optional[str] = None
48
+ agent_message: Optional[str] = None
49
+
50
+ @classmethod
51
+ def allow(cls, event_type: AiHookEventType) -> 'HookDecision':
52
+ return cls(action=DecisionAction.ALLOW, event_type=event_type)
53
+
54
+ @classmethod
55
+ def deny(
56
+ cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None
57
+ ) -> 'HookDecision':
58
+ return cls(
59
+ action=DecisionAction.DENY,
60
+ event_type=event_type,
61
+ user_message=user_message,
62
+ agent_message=agent_message,
63
+ )
64
+
65
+ @classmethod
66
+ def ask(cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None) -> 'HookDecision':
67
+ return cls(
68
+ action=DecisionAction.ASK,
69
+ event_type=event_type,
70
+ user_message=user_message,
71
+ agent_message=agent_message,
72
+ )
73
+
74
+
75
+ class IDE(ABC):
76
+ """Per-IDE integration. Owns every IDE-specific concern in a single module.
77
+
78
+ Subclasses declare identity via class attributes and implement the abstract
79
+ methods. Defaults are provided for `get_user_email` and `get_session_context`
80
+ so IDEs without those capabilities (e.g. no plugin system, no local
81
+ account file) can skip them.
82
+ """
83
+
84
+ # CLI value passed to --ide (e.g. 'cursor', 'claude-code').
85
+ name: ClassVar[str]
86
+ # Human-friendly name for output ('Cursor', 'Claude Code').
87
+ display_name: ClassVar[str]
88
+ # Event names for status display. Use '<event>:<matcher>' for IDEs that
89
+ # qualify a single hook by a sub-matcher (e.g. Claude Code's PreToolUse:Read).
90
+ hook_events: ClassVar[list[str]]
91
+
92
+ # --- install / status ---
93
+
94
+ @abstractmethod
95
+ def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
96
+ """Return the hooks/settings file path for the given scope.
97
+
98
+ `scope` is 'user' or 'repo'. `repo_path` is required when scope == 'repo'.
99
+ """
100
+
101
+ @abstractmethod
102
+ def render_hooks_config(self, async_mode: bool = False) -> dict:
103
+ """Return the settings blob to merge into the IDE's settings file.
104
+
105
+ Shape is IDE-specific (Cursor uses a flat ``{event: [{command}]}`` dict;
106
+ Claude Code uses a nested ``{event: [{hooks: [{type, command}]}]}``
107
+ dict). Both share the outer ``{"hooks": ...}`` wrapper so
108
+ ``hooks_manager`` can treat them uniformly.
109
+ """
110
+
111
+ # --- runtime scan ---
112
+
113
+ @abstractmethod
114
+ def matches_payload(self, raw_payload: dict) -> bool:
115
+ """Return True if ``raw_payload`` originated from this IDE.
116
+
117
+ Prevents double-processing when an IDE forwards another IDE's hook
118
+ event (e.g. Cursor reading Claude Code hooks from ~/.claude/settings.json).
119
+ """
120
+
121
+ @abstractmethod
122
+ def parse_hook_payload(self, raw_payload: dict) -> AIHookPayload:
123
+ """Normalize a raw stdin payload into the canonical ``AIHookPayload``."""
124
+
125
+ @abstractmethod
126
+ def build_hook_response(self, decision: HookDecision) -> dict:
127
+ """Translate a canonical ``HookDecision`` into the IDE-specific JSON.
128
+
129
+ The result is what ``scan_command`` writes to stdout for the IDE to
130
+ act on.
131
+ """
132
+
133
+ # --- session lifecycle (optional; sensible defaults) ---
134
+
135
+ def build_session_payload(self, raw_payload: dict) -> AIHookPayload:
136
+ """Build a session-start payload from the raw stdin payload.
137
+
138
+ Default: a minimal payload tagged with this IDE's ``name``. IDEs
139
+ that need to enrich with transcript/version info should override.
140
+ """
141
+ return AIHookPayload(ide_provider=self.name)
142
+
143
+ def get_user_email(self) -> Optional[str]:
144
+ """Best-effort read of the user's email from IDE-specific config.
145
+
146
+ Default: None. Override if the IDE stores a usable account locally.
147
+ """
148
+ return None
149
+
150
+ def get_session_context(self) -> tuple[dict, dict]:
151
+ """Return ``(mcp_servers, enabled_plugins)`` for session-context reporting.
152
+
153
+ Default: empty dicts (no plugin system, no discoverable MCP config).
154
+ Override to surface MCP/plugin inventory.
155
+ """
156
+ return {}, {}