cycode 3.15.3.dev7__py3-none-any.whl → 3.15.4.dev1__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.
@@ -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