cycode 3.15.3.dev8__py3-none-any.whl → 3.15.4.dev2__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 (27) hide show
  1. cycode/__init__.py +1 -1
  2. cycode/cli/apps/ai_guardrails/command_utils.py +2 -45
  3. cycode/cli/apps/ai_guardrails/consts.py +3 -135
  4. cycode/cli/apps/ai_guardrails/hooks_manager.py +123 -152
  5. cycode/cli/apps/ai_guardrails/ides/__init__.py +45 -0
  6. cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +73 -0
  7. cycode/cli/apps/ai_guardrails/ides/base.py +176 -0
  8. cycode/cli/apps/ai_guardrails/ides/claude_code.py +369 -0
  9. cycode/cli/apps/ai_guardrails/ides/codex.py +310 -0
  10. cycode/cli/apps/ai_guardrails/ides/cursor.py +119 -0
  11. cycode/cli/apps/ai_guardrails/install_command.py +14 -23
  12. cycode/cli/apps/ai_guardrails/scan/handlers.py +102 -101
  13. cycode/cli/apps/ai_guardrails/scan/payload.py +14 -255
  14. cycode/cli/apps/ai_guardrails/scan/scan_command.py +60 -48
  15. cycode/cli/apps/ai_guardrails/scan/types.py +8 -30
  16. cycode/cli/apps/ai_guardrails/session_start_command.py +14 -78
  17. cycode/cli/apps/ai_guardrails/status_command.py +13 -16
  18. cycode/cli/apps/ai_guardrails/uninstall_command.py +12 -22
  19. cycode/cli/utils/jwt_utils.py +8 -0
  20. {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/METADATA +3 -1
  21. {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/RECORD +24 -21
  22. cycode/cli/apps/ai_guardrails/scan/claude_config.py +0 -159
  23. cycode/cli/apps/ai_guardrails/scan/cursor_config.py +0 -36
  24. cycode/cli/apps/ai_guardrails/scan/response_builders.py +0 -135
  25. {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/WHEEL +0 -0
  26. {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/entry_points.txt +0 -0
  27. {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/licenses/LICENCE +0 -0
@@ -7,9 +7,9 @@ from typing import Annotated, Optional
7
7
  import typer
8
8
  from rich.table import Table
9
9
 
10
- from cycode.cli.apps.ai_guardrails.command_utils import console, validate_and_parse_ide, validate_scope
11
- from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
10
+ from cycode.cli.apps.ai_guardrails.command_utils import console, validate_scope
12
11
  from cycode.cli.apps.ai_guardrails.hooks_manager import get_hooks_status
12
+ from cycode.cli.apps.ai_guardrails.ides import DEFAULT_IDE_NAME, IDES, resolve_ides
13
13
 
14
14
 
15
15
  def status_command(
@@ -26,9 +26,9 @@ def status_command(
26
26
  str,
27
27
  typer.Option(
28
28
  '--ide',
29
- help='IDE to check status for (e.g., "cursor", "claude-code", or "all" for all IDEs). Defaults to cursor.',
29
+ help=f'IDE to check status for ({", ".join(IDES)}, or "all").',
30
30
  ),
31
- ] = AIIDEType.CURSOR.value,
31
+ ] = DEFAULT_IDE_NAME,
32
32
  repo_path: Annotated[
33
33
  Optional[Path],
34
34
  typer.Option(
@@ -43,32 +43,30 @@ def status_command(
43
43
  ) -> None:
44
44
  """Show AI guardrails hook installation status.
45
45
 
46
- Displays the current status of Cycode AI guardrails hooks for the specified IDE.
47
-
48
46
  Examples:
49
47
  cycode ai-guardrails status # Show both user and repo status
50
48
  cycode ai-guardrails status --scope user # Show only user-level status
51
49
  cycode ai-guardrails status --scope repo # Show only repo-level status
52
- cycode ai-guardrails status --ide cursor # Check status for Cursor IDE
53
- cycode ai-guardrails status --ide all # Check status for all supported IDEs
50
+ cycode ai-guardrails status --ide claude-code
51
+ cycode ai-guardrails status --ide all # Check every supported IDE
54
52
  """
55
- # Validate inputs (status allows 'all' scope)
56
53
  validate_scope(scope, allowed_scopes=('user', 'repo', 'all'))
57
54
  if repo_path is None:
58
55
  repo_path = Path(os.getcwd())
59
- ide_type = validate_and_parse_ide(ide)
60
-
61
- ides_to_check: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
56
+ ides_to_check = resolve_ides(ide)
62
57
 
63
58
  scopes_to_check = ['user', 'repo'] if scope == 'all' else [scope]
64
59
 
65
60
  for current_ide in ides_to_check:
66
- ide_name = IDE_CONFIGS[current_ide].name
67
61
  console.print()
68
- console.print(f'[bold cyan]═══ {ide_name} ═══[/]')
62
+ console.print(f'[bold cyan]═══ {current_ide.display_name} ═══[/]')
69
63
 
70
64
  for check_scope in scopes_to_check:
71
- status = get_hooks_status(check_scope, repo_path if check_scope == 'repo' else None, ide=current_ide)
65
+ status = get_hooks_status(
66
+ current_ide,
67
+ check_scope,
68
+ repo_path if check_scope == 'repo' else None,
69
+ )
72
70
 
73
71
  console.print()
74
72
  console.print(f'[bold]{check_scope.upper()} SCOPE[/]')
@@ -83,7 +81,6 @@ def status_command(
83
81
  else:
84
82
  console.print('[yellow]○ Cycode AI guardrails: NOT INSTALLED[/]')
85
83
 
86
- # Show hook details
87
84
  table = Table(show_header=True, header_style='bold')
88
85
  table.add_column('Hook Event')
89
86
  table.add_column('Cycode Enabled')
@@ -5,14 +5,9 @@ from typing import Annotated, Optional
5
5
 
6
6
  import typer
7
7
 
8
- from cycode.cli.apps.ai_guardrails.command_utils import (
9
- console,
10
- resolve_repo_path,
11
- validate_and_parse_ide,
12
- validate_scope,
13
- )
14
- from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
8
+ from cycode.cli.apps.ai_guardrails.command_utils import console, resolve_repo_path, validate_scope
15
9
  from cycode.cli.apps.ai_guardrails.hooks_manager import uninstall_hooks
10
+ from cycode.cli.apps.ai_guardrails.ides import DEFAULT_IDE_NAME, IDES, resolve_ides
16
11
 
17
12
 
18
13
  def uninstall_command(
@@ -29,9 +24,9 @@ def uninstall_command(
29
24
  str,
30
25
  typer.Option(
31
26
  '--ide',
32
- help='IDE to uninstall hooks from (e.g., "cursor", "claude-code", "all"). Defaults to cursor.',
27
+ help=f'IDE to uninstall hooks from ({", ".join(IDES)}, or "all").',
33
28
  ),
34
- ] = AIIDEType.CURSOR.value,
29
+ ] = DEFAULT_IDE_NAME,
35
30
  repo_path: Annotated[
36
31
  Optional[Path],
37
32
  typer.Option(
@@ -46,32 +41,27 @@ def uninstall_command(
46
41
  ) -> None:
47
42
  """Remove AI guardrails hooks from supported IDEs.
48
43
 
49
- This command removes Cycode hooks from the IDE's hooks configuration.
50
- Other hooks (if any) will be preserved.
44
+ Removes Cycode hooks from the IDE's hooks configuration. Other hooks
45
+ (if any) are preserved.
51
46
 
52
47
  Examples:
53
48
  cycode ai-guardrails uninstall # Remove user-level hooks
54
49
  cycode ai-guardrails uninstall --scope repo # Remove repo-level hooks
55
- cycode ai-guardrails uninstall --ide cursor # Uninstall from Cursor IDE
56
- cycode ai-guardrails uninstall --ide all # Uninstall from all supported IDEs
50
+ cycode ai-guardrails uninstall --ide claude-code # Uninstall from a specific IDE
51
+ cycode ai-guardrails uninstall --ide all # Uninstall from every supported IDE
57
52
  """
58
- # Validate inputs
59
53
  validate_scope(scope)
60
54
  repo_path = resolve_repo_path(scope, repo_path)
61
- ide_type = validate_and_parse_ide(ide)
62
-
63
- ides_to_uninstall: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
55
+ ides_to_uninstall = resolve_ides(ide)
64
56
 
65
57
  results: list[tuple[str, bool, str]] = []
66
58
  for current_ide in ides_to_uninstall:
67
- ide_name = IDE_CONFIGS[current_ide].name
68
- success, message = uninstall_hooks(scope, repo_path, ide=current_ide)
69
- results.append((ide_name, success, message))
59
+ success, message = uninstall_hooks(current_ide, scope, repo_path)
60
+ results.append((current_ide.display_name, success, message))
70
61
 
71
- # Report results for each IDE
72
62
  any_success = False
73
63
  all_success = True
74
- for _ide_name, success, message in results:
64
+ for _name, success, message in results:
75
65
  if success:
76
66
  console.print(f'[green]✓[/] {message}')
77
67
  any_success = True
@@ -5,6 +5,14 @@ import jwt
5
5
  _JWT_PAYLOAD_POSSIBLE_USER_ID_FIELD_NAMES = ('userId', 'internalId', 'token-user-id')
6
6
 
7
7
 
8
+ def decode_jwt_unverified(token: str) -> Optional[dict]:
9
+ """Return JWT claims without signature verification, or None if the token is unreadable."""
10
+ try:
11
+ return jwt.decode(token, options={'verify_signature': False})
12
+ except jwt.PyJWTError:
13
+ return None
14
+
15
+
8
16
  def get_user_and_tenant_ids_from_access_token(access_token: str) -> tuple[Optional[str], Optional[str]]:
9
17
  payload = jwt.decode(access_token, options={'verify_signature': False})
10
18
 
@@ -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.dev2
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
@@ -34,6 +34,8 @@ Requires-Dist: pyyaml (>=6.0,<7.0)
34
34
  Requires-Dist: requests (>=2.32.4,<3.0)
35
35
  Requires-Dist: rich (>=13.9.4,<14)
36
36
  Requires-Dist: tenacity (>=9.0.0,<9.1.0)
37
+ Requires-Dist: tomli (>=2.0.0,<3.0.0) ; python_version < "3.11"
38
+ Requires-Dist: tomli-w (>=1.0.0,<2.0.0)
37
39
  Requires-Dist: typer (>=0.15.3,<0.16.0)
38
40
  Requires-Dist: urllib3 (>=2.4.0,<3.0.0)
39
41
  Project-URL: Repository, https://github.com/cycodehq/cycode-cli
@@ -1,28 +1,31 @@
1
- cycode/__init__.py,sha256=S0MXR32Gh81GzG2SydtJYW2spFlLLfAgOvuvHh2WXMA,115
1
+ cycode/__init__.py,sha256=QDmVY8Xl94_IN6IhGe3VfDW0t23u1d5S_iQ9f1k08Ww,115
2
2
  cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
3
3
  cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  cycode/cli/app.py,sha256=7ReEcVkRX9IaQ2I7jAj7Sl9smbtvxiuK8-9bitMEQik,7491
5
5
  cycode/cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cycode/cli/apps/activation_manager.py,sha256=Hz9PDJFB-ZmYi4HSG8iYC-fR8j5v25VuUU-l95Otsdk,1678
7
7
  cycode/cli/apps/ai_guardrails/__init__.py,sha256=NsqB1Ca83BIjJMcDSt6suec6Ed0iNnacC0gBqkuuTtI,1367
8
- cycode/cli/apps/ai_guardrails/command_utils.py,sha256=itWoARiiqC-kCJuppBxBwKDjCSnci2m0EG95GQPy3r4,1924
9
- cycode/cli/apps/ai_guardrails/consts.py,sha256=hJ_P6OKtylrhcOsiLmsyRaL0kQ8YsDgmREP_xY8JTkk,4449
10
- cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=37IcEMCK60pQ8lnuy8GThlq9oeNOfETVp_xYGeJ9EpM,9428
11
- cycode/cli/apps/ai_guardrails/install_command.py,sha256=qlklts1Uj6j3urK6jwAWJY-L_DgVaZWuk7vZcpoKPAQ,4571
8
+ cycode/cli/apps/ai_guardrails/command_utils.py,sha256=NVwd0-2RGRKIqhsQ-4LNDR1D0gVm_o7n-z5LxG2bqAo,800
9
+ cycode/cli/apps/ai_guardrails/consts.py,sha256=Js2QtSNYG9Kt0eo3vepRd5TFciCeJHEC9NN18zqKvlE,620
10
+ cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=c8okVel9KjeXWm5QTnvTraWsTwzrUTqqKd6C80C34Y4,9003
11
+ cycode/cli/apps/ai_guardrails/ides/__init__.py,sha256=JMXbQlq-7q1429w3nQq9Z4FZ8K0zdiUK6zCbilmD3JU,1689
12
+ cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py,sha256=_wJfdUHeCJGHQIOdoXQLuMJ4YGaOq_aLjY0ZUyzpsxU,2747
13
+ cycode/cli/apps/ai_guardrails/ides/base.py,sha256=tFWjkuTKBn-4IZUFXHThwKctB6CWOxnG9q9Yr9fscJA,6594
14
+ cycode/cli/apps/ai_guardrails/ides/claude_code.py,sha256=2Rpc22lKGdAmZOCIQtIOTh34g-J_AMUMCDUJocJHzag,13552
15
+ cycode/cli/apps/ai_guardrails/ides/codex.py,sha256=Ep2rNsULM4FzZnej0YXb9KyKBRGIj7qPs-eC78MZd3k,11939
16
+ cycode/cli/apps/ai_guardrails/ides/cursor.py,sha256=_u-DS4Pdvu_UiNh-W5i2ViHPV0IDlDvFrpRnisL3ks8,5235
17
+ cycode/cli/apps/ai_guardrails/install_command.py,sha256=vGZSIvHHVMS9_zhV_6lEhxqtmr5H6uykSq4AS5nxQYw,4278
12
18
  cycode/cli/apps/ai_guardrails/scan/__init__.py,sha256=qJc82XiQGiAuc1sYY8Ij_A-qXpxgLPuayQq8xWlouMA,48
13
- cycode/cli/apps/ai_guardrails/scan/claude_config.py,sha256=2hVuPHfT-9_kgf5yCNgN522IcporEZvJEyYTLaaae2c,5195
14
19
  cycode/cli/apps/ai_guardrails/scan/consts.py,sha256=drAslw6vW3kxmbCs2qPCUbUPR7PJouT2lsXtu5sD-lQ,1094
15
- cycode/cli/apps/ai_guardrails/scan/cursor_config.py,sha256=D4bQsTu6MFzJFhygk0QCyopy5gJMkJm0oRrWNt2PC0g,1087
16
- cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=7zxGQrePRJcs6kkpKnbYq59wM2ADKz70Ve_ZB5ZNRQE,15573
17
- cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=WGvniNa0PuqgGoeNdpXGAYK6aJqOiaB38xNUYaL2CWk,10458
20
+ cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=pf5PrUIVnGLEEE6QKPny9at5V6Ms5u2IEtFP72hKgqA,15523
21
+ cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=pvT3UUqNMvdK3EVzzPjy4JMlOrF-WgxZ3fHN2AtN5eA,1126
18
22
  cycode/cli/apps/ai_guardrails/scan/policy.py,sha256=39s8hnxgjny1l6XAO59wsRcAlpW-LG00GUnO0PfqvuY,2566
19
- cycode/cli/apps/ai_guardrails/scan/response_builders.py,sha256=tVFJCnGdqSmyileg-idypOihygct7F6T4KHXYlX8y_c,4653
20
- cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=_2fa6vz8ZmJlvCYrYNoWfX9fWrrpzcNCwL1UD-JxqLM,5618
21
- cycode/cli/apps/ai_guardrails/scan/types.py,sha256=H25MKJhAXmp7Mz1YeCIRmAY1Zg5GSpgBq8G1TEI9PFk,1868
23
+ cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=-Gl7cHELF1wlwLGno6ZVukFtK6NI6Z3-dTpIOsz8ors,6079
24
+ cycode/cli/apps/ai_guardrails/scan/types.py,sha256=lDttkYFBfOkdMEEaRbq1IT2QTK0R-7Ht3T8vZdyB3b4,1038
22
25
  cycode/cli/apps/ai_guardrails/scan/utils.py,sha256=KVfX-NrcM-QW4quLtoNqfmz4GF0FlDs-TkqUOu1hAWM,2057
23
- cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=fmRQLFFgE0IN9ATVA6dTvkfu-ZF40JRLVo2eAy7pfnk,5618
24
- cycode/cli/apps/ai_guardrails/status_command.py,sha256=UerHtjIGi6sY4RXGR06Es6jQFQAEWTx2Dvhk784WQIM,3539
25
- cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=0qhXNC4PQPqrtt5JmexcM4W6i-VyvObB3DQT_DINM1Q,2969
26
+ cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=z-yClXkV-SAxh5E8zKGIyo_qbhkpujoTgnx-lUDGS0I,3279
27
+ cycode/cli/apps/ai_guardrails/status_command.py,sha256=Uqss68TEPCYPXpLix6Bh-4J3g-khxWsAqlIGYH5x4bQ,3203
28
+ cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=dOmePfZmlHAPy2zEJM1yMtSuDqvzDwtqgmLKYK-T9PI,2698
26
29
  cycode/cli/apps/ai_remediation/__init__.py,sha256=8vYthY9RQeJqEni3AIF5sryz8n-XJQ6VNqG4aEFBAdY,553
27
30
  cycode/cli/apps/ai_remediation/ai_remediation_command.py,sha256=u1EdebaKCEmzv9fXmnIN0xDSLcCmGyjueYKvYfLOj_8,1549
28
31
  cycode/cli/apps/ai_remediation/apply_fix.py,sha256=9zgqiqF9HBQXi7Oz9ZIiANIAuKAMTji1PlNncCEOf5Q,817
@@ -170,7 +173,7 @@ cycode/cli/utils/enum_utils.py,sha256=h_VTCfJ-0hnhwDsEznmx56rJrCb5FQ8u6PrI6p8MP3
170
173
  cycode/cli/utils/get_api_client.py,sha256=wwHabfVCDbFjcIwOn5Raho8MEPiOAgkHlGUEfXKpl8U,3542
171
174
  cycode/cli/utils/git_proxy.py,sha256=FPHMBiyLFK9X9vKYpKySRKJH6Dc9Cb3nO241Q95dASE,2911
172
175
  cycode/cli/utils/ignore_utils.py,sha256=cODqhnOHA2kRo8rMY0YcmcKkmXNPOC9UTCmFu62RRqE,15567
173
- cycode/cli/utils/jwt_utils.py,sha256=TfTHCCCxKO6RvSKT2qspx4577Gax3n9YRj2UgigpGuQ,537
176
+ cycode/cli/utils/jwt_utils.py,sha256=EGI-0CKhCGY8hIcZ9b9diq9hqtOUf8Ha8ukeVJIf974,818
174
177
  cycode/cli/utils/path_utils.py,sha256=U5te1unzhs9pnU5d9BWExgFWElHQkgKvFxKiOF-lp-w,3245
175
178
  cycode/cli/utils/progress_bar.py,sha256=bKBWHHdZsVkdDdWMJLfgLGR0cBYeB44P_DpRM8pvWqU,9528
176
179
  cycode/cli/utils/scan_batch.py,sha256=5xKGVDVqoRxdKhuZkK11x4QrNqKmU20Q83E_fy8Nndk,5188
@@ -204,8 +207,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
204
207
  cycode/cyclient/scan_client.py,sha256=6TK5FQkfrvV7PHqRnUzEn1PBNd2oPYVamvIixcUfe3c,16755
205
208
  cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
206
209
  cycode/logger.py,sha256=EfZGRK6VC5rE_LAjIcRrHFiQCueylCDXoG6bvGkrIME,2111
207
- cycode-3.15.3.dev8.dist-info/METADATA,sha256=F223hOYh-Wc0xdC5zIczE-rsKzl3uYEBfQbYPJdHrzQ,89102
208
- cycode-3.15.3.dev8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
209
- cycode-3.15.3.dev8.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
210
- cycode-3.15.3.dev8.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
211
- cycode-3.15.3.dev8.dist-info/RECORD,,
210
+ cycode-3.15.4.dev2.dist-info/METADATA,sha256=xiTfy56jgKUKJCRHNswSmoi0vEizW1tCcM1FUQazfp0,89206
211
+ cycode-3.15.4.dev2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
212
+ cycode-3.15.4.dev2.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
213
+ cycode-3.15.4.dev2.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
214
+ cycode-3.15.4.dev2.dist-info/RECORD,,
@@ -1,159 +0,0 @@
1
- """Reader for ~/.claude.json configuration file.
2
-
3
- Extracts user email from the Claude Code global config file
4
- for use in AI guardrails scan enrichment.
5
- """
6
-
7
- import json
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- from cycode.logger import get_logger
12
-
13
- logger = get_logger('AI Guardrails Claude Config')
14
-
15
- _CLAUDE_CONFIG_PATH = Path.home() / '.claude.json'
16
- _CLAUDE_SETTINGS_PATH = Path.home() / '.claude' / 'settings.json'
17
-
18
-
19
- def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]:
20
- """Load and parse ~/.claude.json.
21
-
22
- Args:
23
- config_path: Override path for testing. Defaults to ~/.claude.json.
24
-
25
- Returns:
26
- Parsed dict or None if file is missing or invalid.
27
- """
28
- path = config_path or _CLAUDE_CONFIG_PATH
29
- if not path.exists():
30
- logger.debug('Claude config file not found', extra={'path': str(path)})
31
- return None
32
- try:
33
- content = path.read_text(encoding='utf-8')
34
- return json.loads(content)
35
- except Exception as e:
36
- logger.debug('Failed to load Claude config file', exc_info=e)
37
- return None
38
-
39
-
40
- def get_user_email(config: dict) -> Optional[str]:
41
- """Extract user email from Claude config.
42
-
43
- Reads oauthAccount.emailAddress from the config dict.
44
- """
45
- return config.get('oauthAccount', {}).get('emailAddress')
46
-
47
-
48
- def get_mcp_servers(config: dict) -> Optional[dict]:
49
- """Extract MCP servers from Claude config.
50
-
51
- Reads mcpServers from the config dict.
52
- """
53
- return config.get('mcpServers')
54
-
55
-
56
- def load_claude_settings(settings_path: Optional[Path] = None) -> Optional[dict]:
57
- """Load and parse ~/.claude/settings.json.
58
-
59
- Args:
60
- settings_path: Override path for testing. Defaults to ~/.claude/settings.json.
61
-
62
- Returns:
63
- Parsed dict or None if file is missing or invalid.
64
- """
65
- path = settings_path or _CLAUDE_SETTINGS_PATH
66
- if not path.exists():
67
- logger.debug('Claude settings file not found', extra={'path': str(path)})
68
- return None
69
- try:
70
- content = path.read_text(encoding='utf-8')
71
- return json.loads(content)
72
- except Exception as e:
73
- logger.debug('Failed to load Claude settings file', exc_info=e)
74
- return None
75
-
76
-
77
- def _resolve_marketplace_path(marketplace: dict) -> Optional[Path]:
78
- """
79
- Resolve filesystem path for a directory-type marketplace.
80
- """
81
- source = marketplace.get('source', {})
82
- if source.get('source') != 'directory':
83
- return None
84
- raw = source.get('path')
85
- if not raw:
86
- return None
87
- path = Path(raw)
88
- return path if path.is_dir() else None
89
-
90
-
91
- def _load_plugin_json_file(plugin_path: Path, relative_path: str) -> Optional[dict]:
92
- """Load and parse a JSON file inside a plugin directory.
93
-
94
- Returns None if the file is missing, unreadable, or has invalid JSON.
95
- """
96
- target = plugin_path / relative_path
97
- if not target.exists():
98
- return None
99
- try:
100
- return json.loads(target.read_text(encoding='utf-8'))
101
- except Exception as e:
102
- logger.debug('Failed to load plugin file', extra={'path': str(target)}, exc_info=e)
103
- return None
104
-
105
-
106
- def resolve_plugins(settings: dict) -> tuple[dict, dict]:
107
- """Resolve enabled plugins to their MCP servers and metadata.
108
-
109
- Walks enabledPlugins from claude settings, resolves each plugin's 'marketplace' directory
110
- via the 'extraKnownMarketplaces' field, and reads:
111
- - <path>/.mcp.json for MCP servers (merged into a flat dict)
112
- - <path>/.claude-plugin/plugin.json for metadata (name, version, description)
113
-
114
- Args:
115
- settings: Parsed ~/.claude/settings.json dict.
116
-
117
- Returns:
118
- Tuple of (merged_mcp_servers, enriched_plugins):
119
- - merged_mcp_servers: {server_name: server_config, ...}
120
- - enriched_plugins: {plugin_key: {"enabled": True, "name": ..., ...}, ...}
121
- """
122
- enabled = settings.get('enabledPlugins') or {}
123
- marketplaces = settings.get('extraKnownMarketplaces') or {}
124
- merged_mcp: dict = {}
125
- enriched: dict = {}
126
-
127
- for plugin_key, is_enabled in enabled.items():
128
- if not is_enabled:
129
- continue
130
-
131
- entry: dict = {'enabled': True}
132
- enriched[plugin_key] = entry
133
-
134
- if '@' not in plugin_key:
135
- continue
136
-
137
- _plugin_name, marketplace_name = plugin_key.split('@', 1)
138
- marketplace = marketplaces.get(marketplace_name)
139
- if not marketplace:
140
- continue
141
-
142
- plugin_path = _resolve_marketplace_path(marketplace)
143
- if plugin_path is None:
144
- continue
145
-
146
- metadata = _load_plugin_json_file(plugin_path, '.claude-plugin/plugin.json') or {}
147
- for field in ('name', 'version', 'description'):
148
- if field in metadata:
149
- entry[field] = metadata[field]
150
-
151
- mcp_config = _load_plugin_json_file(plugin_path, '.mcp.json') or {}
152
- plugin_server_names = []
153
- for server_name, server_cfg in (mcp_config.get('mcpServers') or {}).items():
154
- merged_mcp[server_name] = server_cfg
155
- plugin_server_names.append(server_name)
156
- if plugin_server_names:
157
- entry['mcp_server_names'] = plugin_server_names
158
-
159
- return merged_mcp, enriched
@@ -1,36 +0,0 @@
1
- """Reader for ~/.cursor/mcp.json configuration file.
2
-
3
- Extracts MCP server definitions from the Cursor global config file
4
- for use in AI guardrails session-context reporting.
5
- """
6
-
7
- import json
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- from cycode.logger import get_logger
12
-
13
- logger = get_logger('AI Guardrails Cursor Config')
14
-
15
- _CURSOR_MCP_CONFIG_PATH = Path.home() / '.cursor' / 'mcp.json'
16
-
17
-
18
- def load_cursor_config(config_path: Optional[Path] = None) -> Optional[dict]:
19
- """Load and parse ~/.cursor/mcp.json.
20
-
21
- Args:
22
- config_path: Override path for testing. Defaults to ~/.cursor/mcp.json.
23
-
24
- Returns:
25
- Parsed dict or None if file is missing or invalid.
26
- """
27
- path = config_path or _CURSOR_MCP_CONFIG_PATH
28
- if not path.exists():
29
- logger.debug('Cursor MCP config file not found', extra={'path': str(path)})
30
- return None
31
- try:
32
- content = path.read_text(encoding='utf-8')
33
- return json.loads(content)
34
- except Exception as e:
35
- logger.debug('Failed to load Cursor MCP config file', exc_info=e)
36
- return None
@@ -1,135 +0,0 @@
1
- """
2
- Response builders for different AI IDE hooks.
3
-
4
- Each IDE has its own response format for hooks. This module provides
5
- an abstract interface and concrete implementations for each supported IDE.
6
- """
7
-
8
- from abc import ABC, abstractmethod
9
-
10
- from cycode.cli.apps.ai_guardrails.consts import AIIDEType
11
-
12
-
13
- class IDEResponseBuilder(ABC):
14
- """Abstract base class for IDE-specific response builders."""
15
-
16
- @abstractmethod
17
- def allow_permission(self) -> dict:
18
- """Build response to allow file read or MCP execution."""
19
-
20
- @abstractmethod
21
- def deny_permission(self, user_message: str, agent_message: str) -> dict:
22
- """Build response to deny file read or MCP execution."""
23
-
24
- @abstractmethod
25
- def ask_permission(self, user_message: str, agent_message: str) -> dict:
26
- """Build response to ask user for permission (warn mode)."""
27
-
28
- @abstractmethod
29
- def allow_prompt(self) -> dict:
30
- """Build response to allow prompt submission."""
31
-
32
- @abstractmethod
33
- def deny_prompt(self, user_message: str) -> dict:
34
- """Build response to deny prompt submission."""
35
-
36
-
37
- class CursorResponseBuilder(IDEResponseBuilder):
38
- """Response builder for Cursor IDE hooks.
39
-
40
- Cursor hook response formats:
41
- - beforeSubmitPrompt: {"continue": bool, "user_message": str}
42
- - beforeReadFile: {"permission": str, "user_message": str, "agent_message": str}
43
- - beforeMCPExecution: {"permission": str, "user_message": str, "agent_message": str}
44
- """
45
-
46
- def allow_permission(self) -> dict:
47
- """Allow file read or MCP execution."""
48
- return {'permission': 'allow'}
49
-
50
- def deny_permission(self, user_message: str, agent_message: str) -> dict:
51
- """Deny file read or MCP execution."""
52
- return {'permission': 'deny', 'user_message': user_message, 'agent_message': agent_message}
53
-
54
- def ask_permission(self, user_message: str, agent_message: str) -> dict:
55
- """Ask user for permission (warn mode)."""
56
- return {'permission': 'ask', 'user_message': user_message, 'agent_message': agent_message}
57
-
58
- def allow_prompt(self) -> dict:
59
- """Allow prompt submission."""
60
- return {'continue': True}
61
-
62
- def deny_prompt(self, user_message: str) -> dict:
63
- """Deny prompt submission."""
64
- return {'continue': False, 'user_message': user_message}
65
-
66
-
67
- class ClaudeCodeResponseBuilder(IDEResponseBuilder):
68
- """Response builder for Claude Code IDE hooks.
69
-
70
- Claude Code hook response formats:
71
- - UserPromptSubmit: {} for allow, {"decision": "block", "reason": str} for deny
72
- - PreToolUse: hookSpecificOutput with permissionDecision (allow/deny/ask)
73
- """
74
-
75
- def allow_permission(self) -> dict:
76
- """Allow file read or MCP execution."""
77
- return {
78
- 'hookSpecificOutput': {
79
- 'hookEventName': 'PreToolUse',
80
- 'permissionDecision': 'allow',
81
- }
82
- }
83
-
84
- def deny_permission(self, user_message: str, agent_message: str) -> dict:
85
- """Deny file read or MCP execution."""
86
- return {
87
- 'hookSpecificOutput': {
88
- 'hookEventName': 'PreToolUse',
89
- 'permissionDecision': 'deny',
90
- 'permissionDecisionReason': user_message,
91
- }
92
- }
93
-
94
- def ask_permission(self, user_message: str, agent_message: str) -> dict:
95
- """Ask user for permission (warn mode)."""
96
- return {
97
- 'hookSpecificOutput': {
98
- 'hookEventName': 'PreToolUse',
99
- 'permissionDecision': 'ask',
100
- 'permissionDecisionReason': user_message,
101
- }
102
- }
103
-
104
- def allow_prompt(self) -> dict:
105
- """Allow prompt submission (empty response means allow)."""
106
- return {}
107
-
108
- def deny_prompt(self, user_message: str) -> dict:
109
- """Deny prompt submission."""
110
- return {'decision': 'block', 'reason': user_message}
111
-
112
-
113
- # Registry of response builders by IDE type
114
- _RESPONSE_BUILDERS: dict[str, IDEResponseBuilder] = {
115
- AIIDEType.CURSOR: CursorResponseBuilder(),
116
- AIIDEType.CLAUDE_CODE: ClaudeCodeResponseBuilder(),
117
- }
118
-
119
-
120
- def get_response_builder(ide: str = AIIDEType.CURSOR.value) -> IDEResponseBuilder:
121
- """Get the response builder for a specific IDE.
122
-
123
- Args:
124
- ide: The IDE name (e.g., 'cursor', 'claude-code') or AIIDEType enum
125
-
126
- Returns:
127
- IDEResponseBuilder instance for the specified IDE
128
-
129
- Raises:
130
- ValueError: If the IDE is not supported
131
- """
132
- builder = _RESPONSE_BUILDERS.get(ide.lower())
133
- if not builder:
134
- raise ValueError(f'Unsupported IDE: {ide}. Supported IDEs: {list(_RESPONSE_BUILDERS.keys())}')
135
- return builder