cycode 3.25.2.dev1__py3-none-any.whl → 3.25.2.dev3__py3-none-any.whl

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 (124) hide show
  1. cycode/__init__.py +1 -1
  2. cycode/cli/app.py +9 -10
  3. cycode/cli/apps/activation_manager.py +6 -6
  4. cycode/cli/apps/ai_guardrails/command_utils.py +1 -2
  5. cycode/cli/apps/ai_guardrails/hooks_manager.py +6 -7
  6. cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +6 -5
  7. cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +1 -2
  8. cycode/cli/apps/ai_guardrails/ides/base.py +9 -11
  9. cycode/cli/apps/ai_guardrails/ides/claude_code.py +14 -14
  10. cycode/cli/apps/ai_guardrails/ides/codex.py +11 -11
  11. cycode/cli/apps/ai_guardrails/ides/copilot.py +7 -7
  12. cycode/cli/apps/ai_guardrails/ides/cursor.py +5 -5
  13. cycode/cli/apps/ai_guardrails/install_command.py +3 -3
  14. cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +10 -11
  15. cycode/cli/apps/ai_guardrails/scan/handlers.py +15 -14
  16. cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +6 -6
  17. cycode/cli/apps/ai_guardrails/scan/payload.py +13 -14
  18. cycode/cli/apps/ai_guardrails/scan/policy.py +3 -3
  19. cycode/cli/apps/ai_guardrails/scan/scan_command.py +4 -4
  20. cycode/cli/apps/ai_guardrails/session_start_command.py +8 -8
  21. cycode/cli/apps/ai_guardrails/status_command.py +2 -2
  22. cycode/cli/apps/ai_guardrails/uninstall_command.py +2 -2
  23. cycode/cli/apps/api/__init__.py +3 -3
  24. cycode/cli/apps/api/api_command.py +6 -6
  25. cycode/cli/apps/api/openapi_spec.py +4 -5
  26. cycode/cli/apps/auth/auth_common.py +4 -4
  27. cycode/cli/apps/configure/configure_command.py +2 -4
  28. cycode/cli/apps/configure/messages.py +2 -4
  29. cycode/cli/apps/configure/prompts.py +5 -7
  30. cycode/cli/apps/ignore/ignore_command.py +7 -7
  31. cycode/cli/apps/mcp/mcp_command.py +11 -11
  32. cycode/cli/apps/report/sbom/common.py +5 -5
  33. cycode/cli/apps/report/sbom/sbom_command.py +2 -2
  34. cycode/cli/apps/report/sbom/sbom_report_file.py +1 -2
  35. cycode/cli/apps/report_import/sbom/sbom_command.py +3 -3
  36. cycode/cli/apps/sca_options.py +3 -3
  37. cycode/cli/apps/scan/aggregation_report.py +4 -4
  38. cycode/cli/apps/scan/code_scanner.py +8 -7
  39. cycode/cli/apps/scan/commit_range_scanner.py +10 -10
  40. cycode/cli/apps/scan/detection_excluder.py +2 -4
  41. cycode/cli/apps/scan/pre_commit/pre_commit_command.py +3 -3
  42. cycode/cli/apps/scan/pre_push/pre_push_command.py +3 -3
  43. cycode/cli/apps/scan/pre_receive/pre_receive_command.py +2 -2
  44. cycode/cli/apps/scan/remote_url_resolver.py +6 -8
  45. cycode/cli/apps/scan/repository/repository_command.py +2 -2
  46. cycode/cli/apps/scan/scan_command.py +2 -2
  47. cycode/cli/apps/scan/scan_parameters.py +1 -3
  48. cycode/cli/apps/scan/scan_result.py +5 -5
  49. cycode/cli/console.py +2 -2
  50. cycode/cli/consts.py +2 -0
  51. cycode/cli/exceptions/custom_exceptions.py +1 -1
  52. cycode/cli/exceptions/handle_errors.py +1 -3
  53. cycode/cli/exceptions/handle_scan_errors.py +1 -2
  54. cycode/cli/files_collector/commit_range_documents.py +14 -14
  55. cycode/cli/files_collector/models/in_memory_zip.py +3 -3
  56. cycode/cli/files_collector/repository_documents.py +3 -3
  57. cycode/cli/files_collector/sca/base_restore_dependencies.py +7 -8
  58. cycode/cli/files_collector/sca/go/restore_go_dependencies.py +1 -2
  59. cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +2 -3
  60. cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +3 -4
  61. cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +3 -4
  62. cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +1 -2
  63. cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +2 -3
  64. cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +2 -3
  65. cycode/cli/files_collector/sca/npm/workspace/coverage.py +5 -5
  66. cycode/cli/files_collector/sca/npm/workspace/files.py +2 -3
  67. cycode/cli/files_collector/sca/npm/workspace/globs.py +1 -2
  68. cycode/cli/files_collector/sca/npm/workspace/resolvers.py +14 -16
  69. cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +1 -2
  70. cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +2 -3
  71. cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +1 -2
  72. cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +2 -3
  73. cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +2 -3
  74. cycode/cli/files_collector/sca/sca_file_collector.py +5 -5
  75. cycode/cli/files_collector/zip_documents.py +1 -2
  76. cycode/cli/models.py +8 -8
  77. cycode/cli/printers/console_printer.py +9 -9
  78. cycode/cli/printers/json_printer.py +2 -2
  79. cycode/cli/printers/printer_base.py +3 -3
  80. cycode/cli/printers/rich_printer.py +2 -2
  81. cycode/cli/printers/tables/table.py +4 -4
  82. cycode/cli/printers/tables/table_models.py +2 -2
  83. cycode/cli/printers/tables/table_printer_base.py +2 -2
  84. cycode/cli/printers/text_printer.py +4 -4
  85. cycode/cli/printers/utils/code_snippet_syntax.py +1 -1
  86. cycode/cli/printers/utils/detection_data.py +3 -3
  87. cycode/cli/printers/utils/sca_ossf.py +4 -4
  88. cycode/cli/printers/utils/sca_policy_details.py +2 -1
  89. cycode/cli/user_settings/config_file_manager.py +10 -10
  90. cycode/cli/user_settings/configuration_manager.py +6 -6
  91. cycode/cli/user_settings/credentials_manager.py +5 -6
  92. cycode/cli/utils/get_api_client.py +5 -5
  93. cycode/cli/utils/git_proxy.py +8 -8
  94. cycode/cli/utils/host_info.py +21 -38
  95. cycode/cli/utils/ignore_utils.py +13 -15
  96. cycode/cli/utils/jwt_utils.py +2 -4
  97. cycode/cli/utils/path_utils.py +6 -6
  98. cycode/cli/utils/progress_bar.py +4 -4
  99. cycode/cli/utils/scan_batch.py +2 -1
  100. cycode/cli/utils/scan_utils.py +2 -2
  101. cycode/cli/utils/shell_executor.py +3 -4
  102. cycode/cli/utils/task_timer.py +4 -4
  103. cycode/cli/utils/trust_store.py +0 -17
  104. cycode/cli/utils/url_utils.py +1 -2
  105. cycode/cli/utils/version_checker.py +4 -5
  106. cycode/cli/utils/yaml_utils.py +1 -1
  107. cycode/config.py +1 -2
  108. cycode/cyclient/ai_security_manager_client.py +17 -17
  109. cycode/cyclient/auth_client.py +2 -4
  110. cycode/cyclient/base_token_auth_client.py +2 -2
  111. cycode/cyclient/client_creator.py +4 -6
  112. cycode/cyclient/cycode_client_base.py +10 -9
  113. cycode/cyclient/cycode_dev_based_client.py +1 -3
  114. cycode/cyclient/headers.py +1 -2
  115. cycode/cyclient/import_sbom_client.py +3 -4
  116. cycode/cyclient/models.py +33 -33
  117. cycode/cyclient/report_client.py +1 -2
  118. cycode/cyclient/scan_client.py +8 -7
  119. cycode/logger.py +4 -4
  120. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/METADATA +12 -19
  121. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/RECORD +124 -124
  122. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/WHEEL +1 -1
  123. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/entry_points.txt +0 -0
  124. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/licenses/LICENCE +0 -0
cycode/__init__.py CHANGED
@@ -5,4 +5,4 @@ import time as _time
5
5
  # end-to-end scan duration from the moment the user actually triggered it.
6
6
  _BOOT_WALL: float = _time.time()
7
7
 
8
- __version__ = '3.25.2.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
8
+ __version__ = '3.25.2.dev3' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
cycode/cli/app.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import importlib
2
2
  import logging
3
3
  import sys
4
- from typing import Annotated, Optional
4
+ from typing import Annotated
5
5
 
6
6
  import click
7
7
  import typer
@@ -51,13 +51,12 @@ _SUBAPP_MODULES: dict[str, str] = {
51
51
  'auth': 'cycode.cli.apps.auth',
52
52
  'configure': 'cycode.cli.apps.configure',
53
53
  'ignore': 'cycode.cli.apps.ignore',
54
+ 'mcp': 'cycode.cli.apps.mcp',
54
55
  'report': 'cycode.cli.apps.report',
55
56
  'import': 'cycode.cli.apps.report_import',
56
57
  'scan': 'cycode.cli.apps.scan',
57
58
  'status': 'cycode.cli.apps.status',
58
59
  }
59
- if sys.version_info >= (3, 10):
60
- _SUBAPP_MODULES['mcp'] = 'cycode.cli.apps.mcp'
61
60
 
62
61
  # Aliases: alternate spellings that resolve to a primary subcommand key.
63
62
  _SUBAPP_ALIASES: dict[str, str] = {
@@ -80,7 +79,7 @@ _ROOT_OPTS_WITH_VALUE = frozenset(
80
79
  )
81
80
 
82
81
 
83
- def _detect_invocation() -> tuple[Optional[str], Optional[str]]:
82
+ def _detect_invocation() -> tuple[str | None, str | None]:
84
83
  """Return (top-level-subapp, second-level-subcommand) parsed from sys.argv.
85
84
 
86
85
  Both values may be None: when no positional arg matches a known subapp,
@@ -113,7 +112,7 @@ def _detect_invocation() -> tuple[Optional[str], Optional[str]]:
113
112
  _INVOKED_SUBAPP, _INVOKED_SUBCOMMAND = _detect_invocation()
114
113
 
115
114
 
116
- def _register_subapps(only: Optional[str]) -> None:
115
+ def _register_subapps(only: str | None) -> None:
117
116
  if only is not None:
118
117
  app.add_typer(importlib.import_module(_SUBAPP_MODULES[only]).app)
119
118
  return
@@ -189,32 +188,32 @@ def app_callback(
189
188
  OutputTypeOption, typer.Option('--output', '-o', case_sensitive=False, help='Specify the output type.')
190
189
  ] = OutputTypeOption.RICH,
191
190
  user_agent: Annotated[
192
- Optional[str],
191
+ str | None,
193
192
  typer.Option(hidden=True, help='Characteristic JSON object that lets servers identify the application.'),
194
193
  ] = None,
195
194
  client_secret: Annotated[
196
- Optional[str],
195
+ str | None,
197
196
  typer.Option(
198
197
  help='Specify a Cycode client secret for this specific scan execution.',
199
198
  rich_help_panel=_AUTH_RICH_HELP_PANEL,
200
199
  ),
201
200
  ] = None,
202
201
  client_id: Annotated[
203
- Optional[str],
202
+ str | None,
204
203
  typer.Option(
205
204
  help='Specify a Cycode client ID for this specific scan execution.',
206
205
  rich_help_panel=_AUTH_RICH_HELP_PANEL,
207
206
  ),
208
207
  ] = None,
209
208
  id_token: Annotated[
210
- Optional[str],
209
+ str | None,
211
210
  typer.Option(
212
211
  help='Specify a Cycode OIDC ID token for this specific scan execution.',
213
212
  rich_help_panel=_AUTH_RICH_HELP_PANEL,
214
213
  ),
215
214
  ] = None,
216
215
  _: Annotated[
217
- Optional[bool],
216
+ bool | None,
218
217
  typer.Option(
219
218
  '--install-completion',
220
219
  callback=install_callback,
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING
2
2
 
3
3
  from cycode import __version__
4
4
  from cycode.cli.config import configuration_manager
@@ -13,13 +13,13 @@ logger = get_logger('Activation Manager')
13
13
  _CLI_CLIENT_NAME = 'cli'
14
14
 
15
15
 
16
- def _get_client_and_version(plugin_app_name: Optional[str], plugin_app_version: Optional[str]) -> tuple[str, str]:
16
+ def _get_client_and_version(plugin_app_name: str | None, plugin_app_version: str | None) -> tuple[str, str]:
17
17
  return plugin_app_name or _CLI_CLIENT_NAME, plugin_app_version or __version__
18
18
 
19
19
 
20
20
  def should_report_cli_activation(
21
- plugin_app_name: Optional[str] = None,
22
- plugin_app_version: Optional[str] = None,
21
+ plugin_app_name: str | None = None,
22
+ plugin_app_version: str | None = None,
23
23
  ) -> bool:
24
24
  client, version = _get_client_and_version(plugin_app_name, plugin_app_version)
25
25
  return configuration_manager.get_last_reported_activation_version(client) != version
@@ -27,8 +27,8 @@ def should_report_cli_activation(
27
27
 
28
28
  def report_cli_activation(
29
29
  cycode_client: 'CycodeClientBase',
30
- plugin_app_name: Optional[str] = None,
31
- plugin_app_version: Optional[str] = None,
30
+ plugin_app_name: str | None = None,
31
+ plugin_app_version: str | None = None,
32
32
  ) -> None:
33
33
  """Report CLI/IDE activation to the backend if the (client, version) pair is new.
34
34
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import Optional
6
5
 
7
6
  import typer
8
7
  from rich.console import Console
@@ -18,7 +17,7 @@ def validate_scope(scope: str, allowed_scopes: tuple[str, ...] = ('user', 'repo'
18
17
  raise typer.Exit(1)
19
18
 
20
19
 
21
- def resolve_repo_path(scope: str, repo_path: Optional[Path]) -> Optional[Path]:
20
+ def resolve_repo_path(scope: str, repo_path: Path | None) -> Path | None:
22
21
  """Default repo_path to cwd for 'repo' scope; leave None for 'user' scope."""
23
22
  if scope == 'repo' and repo_path is None:
24
23
  return Path(os.getcwd())
@@ -8,7 +8,6 @@ agent-agnostic.
8
8
  import copy
9
9
  import json
10
10
  from pathlib import Path
11
- from typing import Optional
12
11
 
13
12
  import yaml
14
13
 
@@ -47,7 +46,7 @@ def is_cycode_hook_entry(entry: dict) -> bool:
47
46
  return False
48
47
 
49
48
 
50
- def _strip_cycode_from_entry(entry: dict) -> Optional[dict]:
49
+ def _strip_cycode_from_entry(entry: dict) -> dict | None:
51
50
  """Remove Cycode hooks from ``entry`` and return the remainder.
52
51
 
53
52
  Returns ``None`` when nothing useful remains (Cursor-flat Cycode entry, or
@@ -72,7 +71,7 @@ def _strip_cycode_from_entry(entry: dict) -> Optional[dict]:
72
71
  return entry
73
72
 
74
73
 
75
- def _load_hooks_file(hooks_path: Path) -> Optional[dict]:
74
+ def _load_hooks_file(hooks_path: Path) -> dict | None:
76
75
  if not hooks_path.exists():
77
76
  return None
78
77
  try:
@@ -102,7 +101,7 @@ def _load_policy_dict(policy_path: Path) -> dict:
102
101
  return {**copy.deepcopy(DEFAULT_POLICY), **existing}
103
102
 
104
103
 
105
- def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
104
+ def create_policy_file(scope: str, repo_path: Path | None = None) -> tuple[bool, str]:
106
105
  """Create or update the ai-guardrails.yaml policy file (operational knobs only).
107
106
 
108
107
  Enforcement mode and sensitive-path globs are platform-managed; those keys are stripped
@@ -125,7 +124,7 @@ def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bo
125
124
  def install_hooks(
126
125
  ide: IDE,
127
126
  scope: str = 'user',
128
- repo_path: Optional[Path] = None,
127
+ repo_path: Path | None = None,
129
128
  ) -> tuple[bool, str]:
130
129
  """Install Cycode AI guardrails hooks for ``ide``."""
131
130
  hooks_path = ide.settings_path(scope, repo_path)
@@ -197,7 +196,7 @@ def _persist_uninstall(hooks_path: Path, existing: dict, modified: bool) -> tupl
197
196
  return True, f'Cycode hooks removed from: {hooks_path}'
198
197
 
199
198
 
200
- def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> tuple[bool, str]:
199
+ def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Path | None = None) -> tuple[bool, str]:
201
200
  """Remove Cycode AI guardrails hooks for ``ide``."""
202
201
  hooks_path = ide.settings_path(scope, repo_path)
203
202
 
@@ -218,7 +217,7 @@ def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = N
218
217
  return True, message
219
218
 
220
219
 
221
- def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> dict:
220
+ def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Path | None = None) -> dict:
222
221
  """Return installation status of Cycode hooks for ``ide``."""
223
222
  hooks_path = ide.settings_path(scope, repo_path)
224
223
 
@@ -7,15 +7,16 @@ supplies the two callables that vary (``locate_dir`` + ``read_plugin``).
7
7
  """
8
8
 
9
9
  import json
10
+ from collections.abc import Callable
10
11
  from pathlib import Path
11
- from typing import Any, Callable, Optional
12
+ from typing import Any
12
13
 
13
14
  from cycode.logger import get_logger
14
15
 
15
16
  logger = get_logger('AI Guardrails Plugins')
16
17
 
17
18
 
18
- def resolve_cached_plugin_dir(cache_root: Path, marketplace: str, plugin_name: str) -> Optional[Path]:
19
+ def resolve_cached_plugin_dir(cache_root: Path, marketplace: str, plugin_name: str) -> Path | None:
19
20
  """Find ``<cache_root>/<marketplace>/<plugin>/<version-or-hash>/``.
20
21
 
21
22
  Both Claude Code and Codex cache installed plugin content in this layout (the trailing
@@ -31,7 +32,7 @@ def resolve_cached_plugin_dir(cache_root: Path, marketplace: str, plugin_name: s
31
32
  return max(candidates, key=lambda d: (d.stat().st_mtime, d.name))
32
33
 
33
34
 
34
- def load_plugin_json(path: Path) -> Optional[dict]:
35
+ def load_plugin_json(path: Path) -> dict | None:
35
36
  """Load a JSON file inside a plugin directory; None if missing or invalid."""
36
37
  if not path.exists():
37
38
  return None
@@ -42,7 +43,7 @@ def load_plugin_json(path: Path) -> Optional[dict]:
42
43
  return None
43
44
 
44
45
 
45
- def build_global_config_files(path: Path, mcp_servers: Optional[dict]) -> list[dict]:
46
+ def build_global_config_files(path: Path, mcp_servers: dict | None) -> list[dict]:
46
47
  """Wrap a global (non-plugin) MCP config into the session-context file shape.
47
48
 
48
49
  Returns ``[{"path": <full path>, "content": <{"mcpServers": ...} JSON>}]`` when
@@ -58,7 +59,7 @@ def build_global_config_files(path: Path, mcp_servers: Optional[dict]) -> list[d
58
59
  def walk_enabled_plugins(
59
60
  plugin_entries: dict[str, Any],
60
61
  is_enabled: Callable[[Any], bool],
61
- locate_dir: Callable[[str, str], Optional[Path]],
62
+ locate_dir: Callable[[str, str], Path | None],
62
63
  read_plugin: Callable[[Path], tuple[dict, dict]],
63
64
  ) -> dict:
64
65
  """Iterate enabled plugins and build their inventory metadata.
@@ -11,7 +11,6 @@ the device its MCP inventory as well.
11
11
  """
12
12
 
13
13
  from pathlib import Path
14
- from typing import Optional
15
14
 
16
15
  from cycode.logger import get_logger
17
16
 
@@ -32,7 +31,7 @@ MAX_SKILL_FILE_BYTES = 256 * 1024
32
31
  MAX_SKILLS_PER_ROOT = 200
33
32
 
34
33
 
35
- def _read_skill_file(skill_file: Path) -> Optional[dict]:
34
+ def _read_skill_file(skill_file: Path) -> dict | None:
36
35
  """Read one ``SKILL.md`` into the session-context file shape, or None if unusable."""
37
36
  try:
38
37
  size = skill_file.stat().st_size
@@ -18,7 +18,7 @@ from abc import ABC, abstractmethod
18
18
  from dataclasses import dataclass
19
19
  from enum import Enum
20
20
  from pathlib import Path
21
- from typing import ClassVar, Optional
21
+ from typing import ClassVar
22
22
 
23
23
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
24
24
  from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
@@ -44,17 +44,15 @@ class HookDecision:
44
44
 
45
45
  action: DecisionAction
46
46
  event_type: AiHookEventType
47
- user_message: Optional[str] = None
48
- agent_message: Optional[str] = None
47
+ user_message: str | None = None
48
+ agent_message: str | None = None
49
49
 
50
50
  @classmethod
51
51
  def allow(cls, event_type: AiHookEventType) -> 'HookDecision':
52
52
  return cls(action=DecisionAction.ALLOW, event_type=event_type)
53
53
 
54
54
  @classmethod
55
- def deny(
56
- cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None
57
- ) -> 'HookDecision':
55
+ def deny(cls, event_type: AiHookEventType, user_message: str, agent_message: str | None = None) -> 'HookDecision':
58
56
  return cls(
59
57
  action=DecisionAction.DENY,
60
58
  event_type=event_type,
@@ -63,7 +61,7 @@ class HookDecision:
63
61
  )
64
62
 
65
63
  @classmethod
66
- def ask(cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None) -> 'HookDecision':
64
+ def ask(cls, event_type: AiHookEventType, user_message: str, agent_message: str | None = None) -> 'HookDecision':
67
65
  return cls(
68
66
  action=DecisionAction.ASK,
69
67
  event_type=event_type,
@@ -92,7 +90,7 @@ class IDE(ABC):
92
90
  # --- install / status ---
93
91
 
94
92
  @abstractmethod
95
- def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
93
+ def settings_path(self, scope: str, repo_path: Path | None = None) -> Path:
96
94
  """Return the hooks/settings file path for the given scope.
97
95
 
98
96
  `scope` is 'user' or 'repo'. `repo_path` is required when scope == 'repo'.
@@ -108,7 +106,7 @@ class IDE(ABC):
108
106
  ``hooks_manager`` can treat them uniformly.
109
107
  """
110
108
 
111
- def post_install(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
109
+ def post_install(self, scope: str, repo_path: Path | None = None) -> tuple[bool, str]:
112
110
  """Run IDE-specific actions after the hooks file is written.
113
111
 
114
112
  Default: no-op success. Override to perform extra setup that doesn't
@@ -120,7 +118,7 @@ class IDE(ABC):
120
118
  """
121
119
  return True, ''
122
120
 
123
- def post_uninstall(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
121
+ def post_uninstall(self, scope: str, repo_path: Path | None = None) -> tuple[bool, str]:
124
122
  """Run IDE-specific cleanup after the hooks file is removed.
125
123
 
126
124
  Default: no-op success. Override to undo whatever ``post_install``
@@ -169,7 +167,7 @@ class IDE(ABC):
169
167
  """
170
168
  return AIHookPayload(ide_provider=self.name)
171
169
 
172
- def get_user_email(self) -> Optional[str]:
170
+ def get_user_email(self) -> str | None:
173
171
  """Best-effort read of the user's email from IDE-specific config.
174
172
 
175
173
  Default: None. Override if the IDE stores a usable account locally.
@@ -5,7 +5,7 @@ import re
5
5
  from collections.abc import Iterator
6
6
  from copy import deepcopy
7
7
  from pathlib import Path
8
- from typing import ClassVar, Optional
8
+ from typing import ClassVar
9
9
 
10
10
  from cycode.cli.apps.ai_guardrails.consts import CYCODE_SCAN_PROMPT_COMMAND, CYCODE_SESSION_START_COMMAND
11
11
  from cycode.cli.apps.ai_guardrails.ides._plugin_utils import (
@@ -80,12 +80,12 @@ def _reverse_readline(path: Path, buf_size: int = 8192) -> Iterator[str]:
80
80
  yield buffer.decode('utf-8', errors='replace')
81
81
 
82
82
 
83
- def _extract_model(entry: dict) -> Optional[str]:
83
+ def _extract_model(entry: dict) -> str | None:
84
84
  """Extract model from a transcript entry (top level or nested in message)."""
85
85
  return entry.get('model') or (entry.get('message') or {}).get('model')
86
86
 
87
87
 
88
- def _extract_generation_id(entry: dict) -> Optional[str]:
88
+ def _extract_generation_id(entry: dict) -> str | None:
89
89
  """Extract generation ID from a user-type transcript entry."""
90
90
  if entry.get('type') == 'user':
91
91
  return entry.get('uuid')
@@ -94,7 +94,7 @@ def _extract_generation_id(entry: dict) -> Optional[str]:
94
94
 
95
95
  def extract_from_claude_transcript(
96
96
  transcript_path: str,
97
- ) -> tuple[Optional[str], Optional[str], Optional[str]]:
97
+ ) -> tuple[str | None, str | None, str | None]:
98
98
  """Extract ``(ide_version, model, generation_id)`` from a transcript.
99
99
 
100
100
  The transcript is a JSONL file scanned from end → start so the most recent
@@ -135,7 +135,7 @@ def extract_from_claude_transcript(
135
135
  # --- ~/.claude.json + ~/.claude/settings.json parsing -------------------------
136
136
 
137
137
 
138
- def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]:
138
+ def load_claude_config(config_path: Path | None = None) -> dict | None:
139
139
  """Load and parse `~/.claude.json`. Returns None if missing/invalid."""
140
140
  path = config_path or _CLAUDE_CONFIG_PATH
141
141
  if not path.exists():
@@ -148,17 +148,17 @@ def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]:
148
148
  return None
149
149
 
150
150
 
151
- def _email_from_config(config: dict) -> Optional[str]:
151
+ def _email_from_config(config: dict) -> str | None:
152
152
  """Read ``oauthAccount.emailAddress`` from a parsed Claude config."""
153
153
  return config.get('oauthAccount', {}).get('emailAddress')
154
154
 
155
155
 
156
- def get_mcp_servers(config: dict) -> Optional[dict]:
156
+ def get_mcp_servers(config: dict) -> dict | None:
157
157
  """Read ``mcpServers`` from a parsed Claude config."""
158
158
  return config.get('mcpServers')
159
159
 
160
160
 
161
- def load_claude_settings(settings_path: Optional[Path] = None) -> Optional[dict]:
161
+ def load_claude_settings(settings_path: Path | None = None) -> dict | None:
162
162
  """Load and parse `~/.claude/settings.json`. Returns None if missing/invalid."""
163
163
  path = settings_path or _CLAUDE_SETTINGS_PATH
164
164
  if not path.exists():
@@ -185,7 +185,7 @@ def _plugins_cache_dir() -> Path:
185
185
  return Path.home() / '.claude' / 'plugins' / 'cache'
186
186
 
187
187
 
188
- def _resolve_marketplace_path(marketplace: dict) -> Optional[Path]:
188
+ def _resolve_marketplace_path(marketplace: dict) -> Path | None:
189
189
  """Resolve filesystem path for a directory-type marketplace."""
190
190
  source = marketplace.get('source', {})
191
191
  if source.get('source') != 'directory':
@@ -236,7 +236,7 @@ def resolve_plugins(settings: dict) -> dict:
236
236
  enabled = settings.get('enabledPlugins') or {}
237
237
  marketplaces = settings.get('extraKnownMarketplaces') or {}
238
238
 
239
- def _locate(plugin_name: str, marketplace_name: str) -> Optional[Path]:
239
+ def _locate(plugin_name: str, marketplace_name: str) -> 'Path | None':
240
240
  # Directory-type marketplaces point straight at the plugin source; every other source
241
241
  # type (git, github, ...) is cloned into the local plugin cache.
242
242
  marketplace = marketplaces.get(marketplace_name)
@@ -259,7 +259,7 @@ def _sanitize_mcp_name(name: str) -> str:
259
259
  return re.sub(r'[^a-z0-9_-]', '_', name.lower())
260
260
 
261
261
 
262
- def _local_mcp_servers(config: Optional[dict], cwd: Optional[str]) -> Iterator[tuple[str, str]]:
262
+ def _local_mcp_servers(config: dict | None, cwd: str | None) -> Iterator[tuple[str, str]]:
263
263
  """Yield ``(name as Claude Code namespaces it, config key)`` for each locally configured MCP server."""
264
264
  sources = []
265
265
  if config:
@@ -280,7 +280,7 @@ def _local_mcp_servers(config: Optional[dict], cwd: Optional[str]) -> Iterator[t
280
280
  yield f'plugin_{plugin_name}_{server}', server
281
281
 
282
282
 
283
- def _resolve_mcp_server_name(server: str, config: Optional[dict], cwd: Optional[str]) -> str:
283
+ def _resolve_mcp_server_name(server: str, config: dict | None, cwd: str | None) -> str:
284
284
  """Map the server segment of ``mcp__<server>__<tool>`` back to its config key; unknown names pass through."""
285
285
  wanted = _sanitize_mcp_name(server)
286
286
  for namespaced, name in _local_mcp_servers(config, cwd):
@@ -297,7 +297,7 @@ class ClaudeCode(IDE):
297
297
  display_name: ClassVar[str] = 'Claude Code'
298
298
  hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS)
299
299
 
300
- def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
300
+ def settings_path(self, scope: str, repo_path: Path | None = None) -> Path:
301
301
  if scope == 'repo' and repo_path:
302
302
  return repo_path / _REPO_SUBDIR / _HOOKS_FILE_NAME
303
303
  return _USER_HOOKS_DIR / _HOOKS_FILE_NAME
@@ -430,7 +430,7 @@ class ClaudeCode(IDE):
430
430
  source=raw_payload.get('source'),
431
431
  )
432
432
 
433
- def get_user_email(self) -> Optional[str]:
433
+ def get_user_email(self) -> str | None:
434
434
  config = load_claude_config()
435
435
  return _email_from_config(config) if config else None
436
436
 
@@ -6,7 +6,7 @@ import re
6
6
  import sys
7
7
  from collections.abc import Iterator
8
8
  from pathlib import Path
9
- from typing import ClassVar, Optional
9
+ from typing import ClassVar
10
10
 
11
11
  import tomli_w
12
12
 
@@ -61,14 +61,14 @@ def _codex_skills_dir() -> Path:
61
61
  return _codex_home() / 'skills'
62
62
 
63
63
 
64
- def _codex_config_toml_path(scope: str, repo_path: Optional[Path] = None) -> Path:
64
+ def _codex_config_toml_path(scope: str, repo_path: Path | None = None) -> Path:
65
65
  """Return the Codex ``config.toml`` path for the given scope."""
66
66
  if scope == 'repo' and repo_path:
67
67
  return repo_path / _CONFIG_DIR_NAME / _CONFIG_TOML_NAME
68
68
  return _codex_home() / _CONFIG_TOML_NAME
69
69
 
70
70
 
71
- def _load_codex_config(config_path: Optional[Path] = None) -> Optional[dict]:
71
+ def _load_codex_config(config_path: Path | None = None) -> dict | None:
72
72
  """Load and parse Codex's ``config.toml``. Returns None on missing/invalid."""
73
73
  path = config_path or (_codex_home() / _CONFIG_TOML_NAME)
74
74
  if not path.exists():
@@ -82,7 +82,7 @@ def _load_codex_config(config_path: Optional[Path] = None) -> Optional[dict]:
82
82
  return None
83
83
 
84
84
 
85
- def _email_from_auth(auth_path: Optional[Path] = None) -> Optional[str]:
85
+ def _email_from_auth(auth_path: Path | None = None) -> str | None:
86
86
  """Best-effort extraction of the signed-in Codex user's email.
87
87
 
88
88
  Reads ``~/.codex/auth.json`` and decodes the JWT in ``tokens.id_token``
@@ -108,7 +108,7 @@ def _email_from_auth(auth_path: Optional[Path] = None) -> Optional[str]:
108
108
  return claims.get('email')
109
109
 
110
110
 
111
- def _resolve_codex_plugin_dir(plugin_name: str, marketplace: str) -> Optional[Path]:
111
+ def _resolve_codex_plugin_dir(plugin_name: str, marketplace: str) -> Path | None:
112
112
  """Find ``~/.codex/plugins/cache/<marketplace>/<plugin>/<hash>/``."""
113
113
  return resolve_cached_plugin_dir(_codex_home() / 'plugins' / 'cache', marketplace, plugin_name)
114
114
 
@@ -177,13 +177,13 @@ def _local_mcp_server_names() -> Iterator[str]:
177
177
  yield from entry.get('mcp_server_names') or []
178
178
 
179
179
 
180
- def _split_mcp_tool_name(tool_name: str) -> tuple[Optional[str], Optional[str]]:
180
+ def _split_mcp_tool_name(tool_name: str) -> tuple[str | None, str | None]:
181
181
  """Split ``mcp__<namespace>__<tool>`` into ``(server config key, tool)``; unknown namespaces split on ``__``."""
182
182
  rest = tool_name[len('mcp__') :]
183
183
  names = list(_local_mcp_server_names())
184
184
  # Codex appends `_<12 hex>` to a namespace on collision or length overflow.
185
185
  for suffix in ('__', r'_+[0-9a-f]{12}__'):
186
- best: Optional[tuple[int, str]] = None
186
+ best: tuple[int, str] | None = None
187
187
  for name in names:
188
188
  namespace = _sanitize_mcp_name(name).rstrip('_')
189
189
  match = namespace and re.match(re.escape(namespace) + suffix, rest)
@@ -196,7 +196,7 @@ def _split_mcp_tool_name(tool_name: str) -> tuple[Optional[str], Optional[str]]:
196
196
  return (parts[1] if len(parts) >= 2 else None), (parts[2] if len(parts) >= 3 else None)
197
197
 
198
198
 
199
- def _enable_codex_hooks_feature(scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
199
+ def _enable_codex_hooks_feature(scope: str, repo_path: Path | None = None) -> tuple[bool, str]:
200
200
  """Set ``[features] hooks = true`` in Codex's ``config.toml``.
201
201
 
202
202
  Codex's hook scripts are gated behind this feature flag. We preserve any
@@ -234,7 +234,7 @@ class Codex(IDE):
234
234
  display_name: ClassVar[str] = 'Codex'
235
235
  hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS)
236
236
 
237
- def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
237
+ def settings_path(self, scope: str, repo_path: Path | None = None) -> Path:
238
238
  if scope == 'repo' and repo_path:
239
239
  return repo_path / _CONFIG_DIR_NAME / _HOOKS_FILE_NAME
240
240
  return _codex_home() / _HOOKS_FILE_NAME
@@ -264,7 +264,7 @@ class Codex(IDE):
264
264
  },
265
265
  }
266
266
 
267
- def post_install(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
267
+ def post_install(self, scope: str, repo_path: Path | None = None) -> tuple[bool, str]:
268
268
  return _enable_codex_hooks_feature(scope, repo_path)
269
269
 
270
270
  def matches_payload(self, raw_payload: dict) -> bool:
@@ -333,7 +333,7 @@ class Codex(IDE):
333
333
  source=raw_payload.get('source'),
334
334
  )
335
335
 
336
- def get_user_email(self) -> Optional[str]:
336
+ def get_user_email(self) -> str | None:
337
337
  return _email_from_auth()
338
338
 
339
339
  def get_session_context(self) -> tuple[list[dict], dict]:
@@ -27,7 +27,7 @@ import platform
27
27
  import re
28
28
  from collections.abc import Iterable
29
29
  from pathlib import Path
30
- from typing import ClassVar, Optional, Union
30
+ from typing import ClassVar
31
31
  from urllib.parse import urlparse
32
32
  from urllib.request import url2pathname
33
33
 
@@ -131,7 +131,7 @@ def _vscode_mcp_config_path() -> Path:
131
131
  return _vscode_user_dir() / _MCP_CONFIG_FILENAME
132
132
 
133
133
 
134
- def _load_vscode_mcp_config(config_path: Optional[Path] = None) -> Optional[dict]:
134
+ def _load_vscode_mcp_config(config_path: Path | None = None) -> dict | None:
135
135
  """Load and parse VS Code's user-level ``mcp.json``. Returns None if missing/invalid."""
136
136
  path = config_path or _vscode_mcp_config_path()
137
137
  if not path.exists():
@@ -144,7 +144,7 @@ def _load_vscode_mcp_config(config_path: Optional[Path] = None) -> Optional[dict
144
144
  return None
145
145
 
146
146
 
147
- def _load_jsonc(path: Path) -> Optional[dict]:
147
+ def _load_jsonc(path: Path) -> dict | None:
148
148
  """Parse a JSON file tolerating //-comment lines (Copilot's config.json ships
149
149
  with a comment header; VS Code's settings.json is JSONC).
150
150
 
@@ -321,7 +321,7 @@ def _server_name_variants(server_name: str) -> set[str]:
321
321
  return {v for v in (server_name, underscored, collapsed) if v}
322
322
 
323
323
 
324
- def _read_file_path(tool_name: str, tool_input: object) -> Optional[str]:
324
+ def _read_file_path(tool_name: str, tool_input: object) -> str | None:
325
325
  """Path of a file-read tool call, or None when this isn't one.
326
326
 
327
327
  The agent runtime reuses its read tool for directory listings, with a payload
@@ -350,7 +350,7 @@ def is_mcp_tool_name(tool_name: str) -> bool:
350
350
  return tool_name.startswith(_MCP_TOOL_PREFIX) or _MCP_AGENT_SEPARATOR in tool_name
351
351
 
352
352
 
353
- def split_mcp_tool_name(tool_name: str, server_names: Iterable[str]) -> tuple[Optional[str], Optional[str]]:
353
+ def split_mcp_tool_name(tool_name: str, server_names: Iterable[str]) -> tuple[str | None, str | None]:
354
354
  """Split an MCP tool name into ``(server, tool)``.
355
355
 
356
356
  Handles both naming schemes: VS Code's ``mcp_<server>_<tool>`` and the agent
@@ -385,7 +385,7 @@ class Copilot(IDE):
385
385
  display_name: ClassVar[str] = 'GitHub Copilot'
386
386
  hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS)
387
387
 
388
- def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
388
+ def settings_path(self, scope: str, repo_path: Path | None = None) -> Path:
389
389
  # Dedicated Cycode-owned file (Copilot reads every *.json in the hooks
390
390
  # dir), unlike the shared settings files of other IDEs.
391
391
  if scope == 'repo' and repo_path:
@@ -419,7 +419,7 @@ class Copilot(IDE):
419
419
  read_path = _read_file_path(tool_name, tool_input)
420
420
 
421
421
  if hook_event_name == 'UserPromptSubmit':
422
- canonical_event: Union[AiHookEventType, str] = AiHookEventType.PROMPT
422
+ canonical_event: AiHookEventType | str = AiHookEventType.PROMPT
423
423
  elif hook_event_name == 'PreToolUse' and read_path is not None:
424
424
  canonical_event = AiHookEventType.FILE_READ
425
425
  elif hook_event_name == 'PreToolUse' and is_mcp_tool_name(tool_name):
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  import platform
5
5
  from pathlib import Path
6
- from typing import ClassVar, Optional
6
+ from typing import ClassVar
7
7
 
8
8
  from cycode.cli.apps.ai_guardrails.consts import CYCODE_SCAN_PROMPT_COMMAND, CYCODE_SESSION_START_COMMAND
9
9
  from cycode.cli.apps.ai_guardrails.ides._plugin_utils import build_global_config_files
@@ -51,7 +51,7 @@ def _cursor_skills_dir() -> Path:
51
51
  return Path.home() / '.cursor' / 'skills'
52
52
 
53
53
 
54
- def _load_cursor_mcp_config(config_path: Optional[Path] = None) -> Optional[dict]:
54
+ def _load_cursor_mcp_config(config_path: Path | None = None) -> dict | None:
55
55
  """Load and parse `~/.cursor/mcp.json`. Returns None if missing/invalid."""
56
56
  path = config_path or _cursor_mcp_config_path()
57
57
  if not path.exists():
@@ -71,7 +71,7 @@ def _cursor_mcp_config_paths(workspace_roots: object) -> list[Path]:
71
71
  return paths
72
72
 
73
73
 
74
- def _server_command_line(server: dict) -> Optional[str]:
74
+ def _server_command_line(server: dict) -> str | None:
75
75
  command = server.get('command')
76
76
  if not isinstance(command, str) or not command:
77
77
  return None
@@ -79,7 +79,7 @@ def _server_command_line(server: dict) -> Optional[str]:
79
79
  return ' '.join([command, *(str(arg) for arg in args)]) if isinstance(args, list) else command
80
80
 
81
81
 
82
- def _resolve_mcp_server_name(raw_payload: dict) -> Optional[str]:
82
+ def _resolve_mcp_server_name(raw_payload: dict) -> str | None:
83
83
  """Cursor sends the server's url or command, but the platform knows it by its mcp.json entry name."""
84
84
  url = raw_payload.get('url')
85
85
  command = raw_payload.get('command')
@@ -107,7 +107,7 @@ class Cursor(IDE):
107
107
  display_name: ClassVar[str] = 'Cursor'
108
108
  hook_events: ClassVar[list[str]] = list(_CURSOR_EVENT_MAPPING)
109
109
 
110
- def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
110
+ def settings_path(self, scope: str, repo_path: Path | None = None) -> Path:
111
111
  if scope == 'repo' and repo_path:
112
112
  return repo_path / _REPO_SUBDIR / _HOOKS_FILE_NAME
113
113
  return _user_hooks_dir() / _HOOKS_FILE_NAME