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.
- cycode/__init__.py +1 -1
- cycode/cli/app.py +9 -10
- cycode/cli/apps/activation_manager.py +6 -6
- cycode/cli/apps/ai_guardrails/command_utils.py +1 -2
- cycode/cli/apps/ai_guardrails/hooks_manager.py +6 -7
- cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +6 -5
- cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +1 -2
- cycode/cli/apps/ai_guardrails/ides/base.py +9 -11
- cycode/cli/apps/ai_guardrails/ides/claude_code.py +14 -14
- cycode/cli/apps/ai_guardrails/ides/codex.py +11 -11
- cycode/cli/apps/ai_guardrails/ides/copilot.py +7 -7
- cycode/cli/apps/ai_guardrails/ides/cursor.py +5 -5
- cycode/cli/apps/ai_guardrails/install_command.py +3 -3
- cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +10 -11
- cycode/cli/apps/ai_guardrails/scan/handlers.py +15 -14
- cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +6 -6
- cycode/cli/apps/ai_guardrails/scan/payload.py +13 -14
- cycode/cli/apps/ai_guardrails/scan/policy.py +3 -3
- cycode/cli/apps/ai_guardrails/scan/scan_command.py +4 -4
- cycode/cli/apps/ai_guardrails/session_start_command.py +8 -8
- cycode/cli/apps/ai_guardrails/status_command.py +2 -2
- cycode/cli/apps/ai_guardrails/uninstall_command.py +2 -2
- cycode/cli/apps/api/__init__.py +3 -3
- cycode/cli/apps/api/api_command.py +6 -6
- cycode/cli/apps/api/openapi_spec.py +4 -5
- cycode/cli/apps/auth/auth_common.py +4 -4
- cycode/cli/apps/configure/configure_command.py +2 -4
- cycode/cli/apps/configure/messages.py +2 -4
- cycode/cli/apps/configure/prompts.py +5 -7
- cycode/cli/apps/ignore/ignore_command.py +7 -7
- cycode/cli/apps/mcp/mcp_command.py +11 -11
- cycode/cli/apps/report/sbom/common.py +5 -5
- cycode/cli/apps/report/sbom/sbom_command.py +2 -2
- cycode/cli/apps/report/sbom/sbom_report_file.py +1 -2
- cycode/cli/apps/report_import/sbom/sbom_command.py +3 -3
- cycode/cli/apps/sca_options.py +3 -3
- cycode/cli/apps/scan/aggregation_report.py +4 -4
- cycode/cli/apps/scan/code_scanner.py +8 -7
- cycode/cli/apps/scan/commit_range_scanner.py +10 -10
- cycode/cli/apps/scan/detection_excluder.py +2 -4
- cycode/cli/apps/scan/pre_commit/pre_commit_command.py +3 -3
- cycode/cli/apps/scan/pre_push/pre_push_command.py +3 -3
- cycode/cli/apps/scan/pre_receive/pre_receive_command.py +2 -2
- cycode/cli/apps/scan/remote_url_resolver.py +6 -8
- cycode/cli/apps/scan/repository/repository_command.py +2 -2
- cycode/cli/apps/scan/scan_command.py +2 -2
- cycode/cli/apps/scan/scan_parameters.py +1 -3
- cycode/cli/apps/scan/scan_result.py +5 -5
- cycode/cli/console.py +2 -2
- cycode/cli/consts.py +2 -0
- cycode/cli/exceptions/custom_exceptions.py +1 -1
- cycode/cli/exceptions/handle_errors.py +1 -3
- cycode/cli/exceptions/handle_scan_errors.py +1 -2
- cycode/cli/files_collector/commit_range_documents.py +14 -14
- cycode/cli/files_collector/models/in_memory_zip.py +3 -3
- cycode/cli/files_collector/repository_documents.py +3 -3
- cycode/cli/files_collector/sca/base_restore_dependencies.py +7 -8
- cycode/cli/files_collector/sca/go/restore_go_dependencies.py +1 -2
- cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +2 -3
- cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +3 -4
- cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +3 -4
- cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +1 -2
- cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +2 -3
- cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +2 -3
- cycode/cli/files_collector/sca/npm/workspace/coverage.py +5 -5
- cycode/cli/files_collector/sca/npm/workspace/files.py +2 -3
- cycode/cli/files_collector/sca/npm/workspace/globs.py +1 -2
- cycode/cli/files_collector/sca/npm/workspace/resolvers.py +14 -16
- cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +1 -2
- cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +2 -3
- cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +1 -2
- cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +2 -3
- cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +2 -3
- cycode/cli/files_collector/sca/sca_file_collector.py +5 -5
- cycode/cli/files_collector/zip_documents.py +1 -2
- cycode/cli/models.py +8 -8
- cycode/cli/printers/console_printer.py +9 -9
- cycode/cli/printers/json_printer.py +2 -2
- cycode/cli/printers/printer_base.py +3 -3
- cycode/cli/printers/rich_printer.py +2 -2
- cycode/cli/printers/tables/table.py +4 -4
- cycode/cli/printers/tables/table_models.py +2 -2
- cycode/cli/printers/tables/table_printer_base.py +2 -2
- cycode/cli/printers/text_printer.py +4 -4
- cycode/cli/printers/utils/code_snippet_syntax.py +1 -1
- cycode/cli/printers/utils/detection_data.py +3 -3
- cycode/cli/printers/utils/sca_ossf.py +4 -4
- cycode/cli/printers/utils/sca_policy_details.py +2 -1
- cycode/cli/user_settings/config_file_manager.py +10 -10
- cycode/cli/user_settings/configuration_manager.py +6 -6
- cycode/cli/user_settings/credentials_manager.py +5 -6
- cycode/cli/utils/get_api_client.py +5 -5
- cycode/cli/utils/git_proxy.py +8 -8
- cycode/cli/utils/host_info.py +21 -38
- cycode/cli/utils/ignore_utils.py +13 -15
- cycode/cli/utils/jwt_utils.py +2 -4
- cycode/cli/utils/path_utils.py +6 -6
- cycode/cli/utils/progress_bar.py +4 -4
- cycode/cli/utils/scan_batch.py +2 -1
- cycode/cli/utils/scan_utils.py +2 -2
- cycode/cli/utils/shell_executor.py +3 -4
- cycode/cli/utils/task_timer.py +4 -4
- cycode/cli/utils/trust_store.py +0 -17
- cycode/cli/utils/url_utils.py +1 -2
- cycode/cli/utils/version_checker.py +4 -5
- cycode/cli/utils/yaml_utils.py +1 -1
- cycode/config.py +1 -2
- cycode/cyclient/ai_security_manager_client.py +17 -17
- cycode/cyclient/auth_client.py +2 -4
- cycode/cyclient/base_token_auth_client.py +2 -2
- cycode/cyclient/client_creator.py +4 -6
- cycode/cyclient/cycode_client_base.py +10 -9
- cycode/cyclient/cycode_dev_based_client.py +1 -3
- cycode/cyclient/headers.py +1 -2
- cycode/cyclient/import_sbom_client.py +3 -4
- cycode/cyclient/models.py +33 -33
- cycode/cyclient/report_client.py +1 -2
- cycode/cyclient/scan_client.py +8 -7
- cycode/logger.py +4 -4
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/METADATA +12 -19
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/RECORD +124 -124
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/WHEEL +1 -1
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
|
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[
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
22
|
-
plugin_app_version:
|
|
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:
|
|
31
|
-
plugin_app_version:
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
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],
|
|
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) ->
|
|
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
|
|
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:
|
|
48
|
-
agent_message:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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[
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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[
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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[
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
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
|