apm-cli 0.7.9__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apm_cli-0.7.9/src/apm_cli.egg-info → apm_cli-0.8.0}/PKG-INFO +4 -3
- {apm_cli-0.7.9 → apm_cli-0.8.0}/README.md +3 -2
- {apm_cli-0.7.9 → apm_cli-0.8.0}/pyproject.toml +1 -1
- apm_cli-0.8.0/src/apm_cli/adapters/client/cursor.py +138 -0
- apm_cli-0.8.0/src/apm_cli/adapters/client/opencode.py +157 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/packer.py +32 -1
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/cli.py +2 -0
- apm_cli-0.8.0/src/apm_cli/commands/audit.py +504 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/compile.py +15 -2
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/install.py +387 -26
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/pack.py +1 -1
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/uninstall.py +29 -1
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/agents_compiler.py +12 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/claude_formatter.py +11 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/target_detection.py +45 -17
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/github_downloader.py +2 -1
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/factory.py +4 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/__init__.py +12 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/agent_integrator.py +197 -3
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/base_integrator.py +29 -7
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/command_integrator.py +73 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/hook_integrator.py +168 -4
- apm_cli-0.8.0/src/apm_cli/integration/instruction_integrator.py +261 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/mcp_integrator.py +72 -4
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/skill_integrator.py +83 -35
- apm_cli-0.8.0/src/apm_cli/integration/targets.py +180 -0
- apm_cli-0.8.0/src/apm_cli/security/__init__.py +5 -0
- apm_cli-0.8.0/src/apm_cli/security/content_scanner.py +303 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/console.py +1 -1
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/diagnostics.py +84 -2
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/helpers.py +3 -1
- {apm_cli-0.7.9 → apm_cli-0.8.0/src/apm_cli.egg-info}/PKG-INFO +4 -3
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/SOURCES.txt +6 -0
- apm_cli-0.7.9/src/apm_cli/integration/instruction_integrator.py +0 -114
- {apm_cli-0.7.9 → apm_cli-0.8.0}/AUTHORS +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/LICENSE +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/setup.cfg +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/base.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/codex.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/copilot.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/vscode.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/base.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/unpacker.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/_helpers.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/config.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/deps.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/init.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/list_cmd.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/mcp.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/prune.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/run.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/update.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/constants.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution_block.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/context_optimizer.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/distributed_compiler.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/injector.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/link_resolver.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/template_builder.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/config.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/conflict_detector.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/docker_args.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/operations.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/safe_installer.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/script_runner.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/token_manager.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/aggregator.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/apm_resolver.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/collection_parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/dependency_graph.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/lockfile.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/package_validator.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/plugin_parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/verifier.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/drift.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/prompt_integrator.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/skill_transformer.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/utils.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/apm_package.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/dependency.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/plugin.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/validation.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/formatters.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/models.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/script_formatters.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/discovery.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/models.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/client.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/integration.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/operations.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/base.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/codex_runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/copilot_runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/factory.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/llm_runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/manager.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/github_host.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/version_checker.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/version.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/discovery.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/runner.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/dependency_links.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/entry_points.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/requires.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/top_level.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_apm_package_models.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_apm_resolver.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_codex_docker_args_fix.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_codex_empty_string_and_defaults.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_collision_integration.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_console.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_distributed_compilation.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_empty_string_and_defaults.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_enhanced_discovery.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_github_downloader.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_github_downloader_token_precedence.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_lockfile.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_runnable_prompts.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_runtime_manager_token_precedence.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_virtual_package_multi_install.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apm-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: MCP configuration tool
|
|
5
5
|
Author-email: Daniel Meppiel <user@example.com>
|
|
6
6
|
License: MIT License
|
|
@@ -65,7 +65,7 @@ Dynamic: license-file
|
|
|
65
65
|
|
|
66
66
|
Think `package.json`, `requirements.txt`, or `Cargo.toml` — but for AI agent configuration.
|
|
67
67
|
|
|
68
|
-
GitHub Copilot · Claude Code
|
|
68
|
+
GitHub Copilot · Claude Code · Cursor · OpenCode
|
|
69
69
|
|
|
70
70
|
**[Documentation](https://microsoft.github.io/apm/)** · **[Quick Start](https://microsoft.github.io/apm/getting-started/quick-start/)** · **[CLI Reference](https://microsoft.github.io/apm/reference/cli-commands/)**
|
|
71
71
|
|
|
@@ -101,7 +101,8 @@ apm install # every agent is configured
|
|
|
101
101
|
- **One manifest for everything** — instructions, skills, prompts, agents, hooks, plugins, MCP servers
|
|
102
102
|
- **Install from anywhere** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, any git host
|
|
103
103
|
- **Transitive dependencies** — packages can depend on packages; APM resolves the full tree
|
|
104
|
-
- **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot)
|
|
104
|
+
- **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot, OpenCode), `CLAUDE.md` (Claude Code), and `.cursor/rules/` (Cursor)
|
|
105
|
+
- **Content security** — `apm audit` scans for hidden Unicode characters; `apm install` blocks compromised packages before agents can read them
|
|
105
106
|
- **Create & share** — `apm pack` bundles your current configuration as a zipped package
|
|
106
107
|
- **CI/CD ready** — [GitHub Action](https://github.com/microsoft/apm-action) for automated workflows
|
|
107
108
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Think `package.json`, `requirements.txt`, or `Cargo.toml` — but for AI agent configuration.
|
|
6
6
|
|
|
7
|
-
GitHub Copilot · Claude Code
|
|
7
|
+
GitHub Copilot · Claude Code · Cursor · OpenCode
|
|
8
8
|
|
|
9
9
|
**[Documentation](https://microsoft.github.io/apm/)** · **[Quick Start](https://microsoft.github.io/apm/getting-started/quick-start/)** · **[CLI Reference](https://microsoft.github.io/apm/reference/cli-commands/)**
|
|
10
10
|
|
|
@@ -40,7 +40,8 @@ apm install # every agent is configured
|
|
|
40
40
|
- **One manifest for everything** — instructions, skills, prompts, agents, hooks, plugins, MCP servers
|
|
41
41
|
- **Install from anywhere** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, any git host
|
|
42
42
|
- **Transitive dependencies** — packages can depend on packages; APM resolves the full tree
|
|
43
|
-
- **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot)
|
|
43
|
+
- **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot, OpenCode), `CLAUDE.md` (Claude Code), and `.cursor/rules/` (Cursor)
|
|
44
|
+
- **Content security** — `apm audit` scans for hidden Unicode characters; `apm install` blocks compromised packages before agents can read them
|
|
44
45
|
- **Create & share** — `apm pack` bundles your current configuration as a zipped package
|
|
45
46
|
- **CI/CD ready** — [GitHub Action](https://github.com/microsoft/apm-action) for automated workflows
|
|
46
47
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Cursor IDE implementation of MCP client adapter.
|
|
2
|
+
|
|
3
|
+
Cursor uses the standard ``mcpServers`` JSON format at ``.cursor/mcp.json``
|
|
4
|
+
(repo-local). The config schema is identical to GitHub Copilot CLI, so this
|
|
5
|
+
adapter subclasses :class:`CopilotClientAdapter` and only overrides the
|
|
6
|
+
config-path logic and the user-facing labels.
|
|
7
|
+
|
|
8
|
+
APM only writes to ``.cursor/mcp.json`` when the ``.cursor/`` directory
|
|
9
|
+
already exists — Cursor support is opt-in.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .copilot import CopilotClientAdapter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CursorClientAdapter(CopilotClientAdapter):
|
|
20
|
+
"""Cursor IDE MCP client adapter.
|
|
21
|
+
|
|
22
|
+
Inherits all config formatting from :class:`CopilotClientAdapter`
|
|
23
|
+
(``mcpServers`` JSON with ``command``/``args``/``env``). Only the
|
|
24
|
+
config-file location differs: repo-local ``.cursor/mcp.json`` instead
|
|
25
|
+
of global ``~/.copilot/mcp-config.json``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# ------------------------------------------------------------------ #
|
|
29
|
+
# Config path
|
|
30
|
+
# ------------------------------------------------------------------ #
|
|
31
|
+
|
|
32
|
+
def get_config_path(self):
|
|
33
|
+
"""Return the path to ``.cursor/mcp.json`` in the repository root.
|
|
34
|
+
|
|
35
|
+
Unlike the Copilot adapter this is a **repo-local** path. The
|
|
36
|
+
``.cursor/`` directory is *not* created automatically — APM only
|
|
37
|
+
writes here when the directory already exists.
|
|
38
|
+
"""
|
|
39
|
+
cursor_dir = Path(os.getcwd()) / ".cursor"
|
|
40
|
+
return str(cursor_dir / "mcp.json")
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------ #
|
|
43
|
+
# Config read / write — override to avoid auto-creating the directory
|
|
44
|
+
# ------------------------------------------------------------------ #
|
|
45
|
+
|
|
46
|
+
def update_config(self, config_updates):
|
|
47
|
+
"""Merge *config_updates* into the ``mcpServers`` section.
|
|
48
|
+
|
|
49
|
+
The ``.cursor/`` directory must already exist; if it does not, this
|
|
50
|
+
method returns silently (opt-in behaviour).
|
|
51
|
+
"""
|
|
52
|
+
config_path = Path(self.get_config_path())
|
|
53
|
+
|
|
54
|
+
# Opt-in: only write when .cursor/ already exists
|
|
55
|
+
if not config_path.parent.exists():
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
current_config = self.get_current_config()
|
|
59
|
+
if "mcpServers" not in current_config:
|
|
60
|
+
current_config["mcpServers"] = {}
|
|
61
|
+
|
|
62
|
+
current_config["mcpServers"].update(config_updates)
|
|
63
|
+
|
|
64
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
65
|
+
json.dump(current_config, f, indent=2)
|
|
66
|
+
|
|
67
|
+
def get_current_config(self):
|
|
68
|
+
"""Read the current ``.cursor/mcp.json`` contents."""
|
|
69
|
+
config_path = self.get_config_path()
|
|
70
|
+
|
|
71
|
+
if not os.path.exists(config_path):
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
76
|
+
return json.load(f)
|
|
77
|
+
except (json.JSONDecodeError, IOError):
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------ #
|
|
81
|
+
# configure_mcp_server — thin override for the print label
|
|
82
|
+
# ------------------------------------------------------------------ #
|
|
83
|
+
|
|
84
|
+
def configure_mcp_server(
|
|
85
|
+
self,
|
|
86
|
+
server_url,
|
|
87
|
+
server_name=None,
|
|
88
|
+
enabled=True,
|
|
89
|
+
env_overrides=None,
|
|
90
|
+
server_info_cache=None,
|
|
91
|
+
runtime_vars=None,
|
|
92
|
+
):
|
|
93
|
+
"""Configure an MCP server in Cursor's ``.cursor/mcp.json``.
|
|
94
|
+
|
|
95
|
+
Delegates entirely to the parent implementation but prints a
|
|
96
|
+
Cursor-specific success message.
|
|
97
|
+
"""
|
|
98
|
+
if not server_url:
|
|
99
|
+
print("Error: server_url cannot be empty")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Opt-in: skip silently when .cursor/ does not exist
|
|
103
|
+
cursor_dir = Path(os.getcwd()) / ".cursor"
|
|
104
|
+
if not cursor_dir.exists():
|
|
105
|
+
return True # nothing to do, not an error
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# Use cached server info if available, otherwise fetch from registry
|
|
109
|
+
if server_info_cache and server_url in server_info_cache:
|
|
110
|
+
server_info = server_info_cache[server_url]
|
|
111
|
+
else:
|
|
112
|
+
server_info = self.registry_client.find_server_by_reference(server_url)
|
|
113
|
+
|
|
114
|
+
if not server_info:
|
|
115
|
+
print(f"Error: MCP server '{server_url}' not found in registry")
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
# Determine config key
|
|
119
|
+
if server_name:
|
|
120
|
+
config_key = server_name
|
|
121
|
+
elif "/" in server_url:
|
|
122
|
+
config_key = server_url.split("/")[-1]
|
|
123
|
+
else:
|
|
124
|
+
config_key = server_url
|
|
125
|
+
|
|
126
|
+
server_config = self._format_server_config(
|
|
127
|
+
server_info, env_overrides, runtime_vars
|
|
128
|
+
)
|
|
129
|
+
self.update_config({config_key: server_config})
|
|
130
|
+
|
|
131
|
+
print(
|
|
132
|
+
f"Successfully configured MCP server '{config_key}' for Cursor"
|
|
133
|
+
)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"Error configuring MCP server: {e}")
|
|
138
|
+
return False
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""OpenCode implementation of MCP client adapter.
|
|
2
|
+
|
|
3
|
+
OpenCode uses ``opencode.json`` at the project root with an ``mcp`` key.
|
|
4
|
+
The schema differs from VSCode/Cursor:
|
|
5
|
+
|
|
6
|
+
.. code-block:: json
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
"mcp": {
|
|
10
|
+
"server-name": {
|
|
11
|
+
"type": "local",
|
|
12
|
+
"command": ["npx", "-y", "@modelcontextprotocol/server-foo"],
|
|
13
|
+
"environment": { "KEY": "value" },
|
|
14
|
+
"enabled": true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Key differences from Copilot/Cursor:
|
|
20
|
+
- Config file: ``opencode.json`` (not ``mcp.json``)
|
|
21
|
+
- Wrapper key: ``mcp`` (not ``mcpServers``)
|
|
22
|
+
- Command format: single array ``command`` (not ``command`` + ``args``)
|
|
23
|
+
- Env key: ``environment`` (not ``env``)
|
|
24
|
+
|
|
25
|
+
APM only writes to ``opencode.json`` when the ``.opencode/`` directory
|
|
26
|
+
already exists — OpenCode support is opt-in.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from .copilot import CopilotClientAdapter
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OpenCodeClientAdapter(CopilotClientAdapter):
|
|
37
|
+
"""OpenCode MCP client adapter.
|
|
38
|
+
|
|
39
|
+
Converts the standard Copilot config format into OpenCode's schema
|
|
40
|
+
and writes to ``opencode.json`` in the project root.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def get_config_path(self):
|
|
44
|
+
"""Return the path to ``opencode.json`` in the repository root."""
|
|
45
|
+
return str(Path(os.getcwd()) / "opencode.json")
|
|
46
|
+
|
|
47
|
+
def update_config(self, config_updates, enabled=True):
|
|
48
|
+
"""Merge *config_updates* into the ``mcp`` section of ``opencode.json``.
|
|
49
|
+
|
|
50
|
+
The ``.opencode/`` directory must already exist; if it does not, this
|
|
51
|
+
method returns silently (opt-in behaviour).
|
|
52
|
+
|
|
53
|
+
Translates Copilot-format entries (``command``/``args``/``env``) into
|
|
54
|
+
OpenCode format (``command`` array / ``environment``).
|
|
55
|
+
"""
|
|
56
|
+
opencode_dir = Path(os.getcwd()) / ".opencode"
|
|
57
|
+
if not opencode_dir.is_dir():
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
config_path = Path(self.get_config_path())
|
|
61
|
+
current_config = self.get_current_config()
|
|
62
|
+
if "mcp" not in current_config:
|
|
63
|
+
current_config["mcp"] = {}
|
|
64
|
+
|
|
65
|
+
for name, copilot_entry in config_updates.items():
|
|
66
|
+
current_config["mcp"][name] = self._to_opencode_format(copilot_entry, enabled=enabled)
|
|
67
|
+
|
|
68
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
69
|
+
json.dump(current_config, f, indent=2)
|
|
70
|
+
|
|
71
|
+
def get_current_config(self):
|
|
72
|
+
"""Read the current ``opencode.json`` contents."""
|
|
73
|
+
config_path = self.get_config_path()
|
|
74
|
+
if not os.path.exists(config_path):
|
|
75
|
+
return {}
|
|
76
|
+
try:
|
|
77
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
78
|
+
return json.load(f)
|
|
79
|
+
except (json.JSONDecodeError, IOError):
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
def configure_mcp_server(
|
|
83
|
+
self,
|
|
84
|
+
server_url,
|
|
85
|
+
server_name=None,
|
|
86
|
+
enabled=True,
|
|
87
|
+
env_overrides=None,
|
|
88
|
+
server_info_cache=None,
|
|
89
|
+
runtime_vars=None,
|
|
90
|
+
):
|
|
91
|
+
"""Configure an MCP server in ``opencode.json``.
|
|
92
|
+
|
|
93
|
+
Delegates to the parent for config formatting, then converts to
|
|
94
|
+
OpenCode schema before writing.
|
|
95
|
+
"""
|
|
96
|
+
if not server_url:
|
|
97
|
+
print("Error: server_url cannot be empty")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
opencode_dir = Path(os.getcwd()) / ".opencode"
|
|
101
|
+
if not opencode_dir.is_dir():
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if server_info_cache and server_url in server_info_cache:
|
|
106
|
+
server_info = server_info_cache[server_url]
|
|
107
|
+
else:
|
|
108
|
+
server_info = self.registry_client.find_server_by_reference(server_url)
|
|
109
|
+
|
|
110
|
+
if not server_info:
|
|
111
|
+
print(f"Error: MCP server '{server_url}' not found in registry")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
if server_name:
|
|
115
|
+
config_key = server_name
|
|
116
|
+
elif "/" in server_url:
|
|
117
|
+
config_key = server_url.split("/")[-1]
|
|
118
|
+
else:
|
|
119
|
+
config_key = server_url
|
|
120
|
+
|
|
121
|
+
server_config = self._format_server_config(
|
|
122
|
+
server_info, env_overrides, runtime_vars
|
|
123
|
+
)
|
|
124
|
+
self.update_config({config_key: server_config}, enabled=enabled)
|
|
125
|
+
|
|
126
|
+
print(
|
|
127
|
+
f"Successfully configured MCP server '{config_key}' for OpenCode"
|
|
128
|
+
)
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Error configuring MCP server: {e}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _to_opencode_format(copilot_entry: dict, enabled: bool = True) -> dict:
|
|
137
|
+
"""Convert a Copilot-format server config to OpenCode format.
|
|
138
|
+
|
|
139
|
+
Copilot: ``{"command": "npx", "args": ["-y", "pkg"], "env": {...}}``
|
|
140
|
+
OpenCode: ``{"type": "local", "command": ["npx", "-y", "pkg"],
|
|
141
|
+
"environment": {...}, "enabled": true}``
|
|
142
|
+
"""
|
|
143
|
+
entry: dict = {"type": "local", "enabled": enabled}
|
|
144
|
+
|
|
145
|
+
cmd = copilot_entry.get("command", "")
|
|
146
|
+
args = copilot_entry.get("args", [])
|
|
147
|
+
if cmd:
|
|
148
|
+
entry["command"] = [cmd] + list(args)
|
|
149
|
+
elif "url" in copilot_entry:
|
|
150
|
+
entry["type"] = "remote"
|
|
151
|
+
entry["url"] = copilot_entry["url"]
|
|
152
|
+
|
|
153
|
+
env = copilot_entry.get("env") or {}
|
|
154
|
+
if env:
|
|
155
|
+
entry["environment"] = env
|
|
156
|
+
|
|
157
|
+
return entry
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Bundle packer -- creates self-contained APM bundles from the resolved dependency tree."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import tarfile
|
|
5
6
|
from dataclasses import dataclass, field
|
|
@@ -17,7 +18,9 @@ _TARGET_PREFIXES = {
|
|
|
17
18
|
"copilot": [".github/"],
|
|
18
19
|
"vscode": [".github/"],
|
|
19
20
|
"claude": [".claude/"],
|
|
20
|
-
"
|
|
21
|
+
"cursor": [".cursor/"],
|
|
22
|
+
"opencode": [".opencode/"],
|
|
23
|
+
"all": [".github/", ".claude/", ".cursor/", ".opencode/"],
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
|
|
@@ -153,6 +156,34 @@ def pack_bundle(
|
|
|
153
156
|
lockfile_enriched=True,
|
|
154
157
|
)
|
|
155
158
|
|
|
159
|
+
# 5b. Scan files for hidden characters before bundling
|
|
160
|
+
from ..security.content_scanner import ContentScanner
|
|
161
|
+
from ..utils.console import _rich_warning
|
|
162
|
+
|
|
163
|
+
_scan_findings_total = 0
|
|
164
|
+
for rel_path in unique_files:
|
|
165
|
+
src = project_root / rel_path
|
|
166
|
+
if src.is_symlink():
|
|
167
|
+
continue
|
|
168
|
+
if src.is_file():
|
|
169
|
+
findings = ContentScanner.scan_file(src)
|
|
170
|
+
if findings:
|
|
171
|
+
_scan_findings_total += len(findings)
|
|
172
|
+
elif src.is_dir():
|
|
173
|
+
for dirpath, _dirnames, filenames in os.walk(src, followlinks=False):
|
|
174
|
+
for fname in filenames:
|
|
175
|
+
fpath = Path(dirpath) / fname
|
|
176
|
+
if fpath.is_symlink():
|
|
177
|
+
continue
|
|
178
|
+
findings = ContentScanner.scan_file(fpath)
|
|
179
|
+
if findings:
|
|
180
|
+
_scan_findings_total += len(findings)
|
|
181
|
+
if _scan_findings_total:
|
|
182
|
+
_rich_warning(
|
|
183
|
+
f"Bundle contains {_scan_findings_total} hidden character(s) across source files "
|
|
184
|
+
f"— run 'apm audit' to inspect before publishing"
|
|
185
|
+
)
|
|
186
|
+
|
|
156
187
|
# 6. Build output directory
|
|
157
188
|
bundle_dir = output_dir / f"{pkg_name}-{pkg_version}"
|
|
158
189
|
bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -15,6 +15,7 @@ from apm_cli.commands._helpers import (
|
|
|
15
15
|
_check_and_notify_updates,
|
|
16
16
|
print_version,
|
|
17
17
|
)
|
|
18
|
+
from apm_cli.commands.audit import audit
|
|
18
19
|
from apm_cli.commands.compile import compile as compile_cmd
|
|
19
20
|
from apm_cli.commands.config import config
|
|
20
21
|
from apm_cli.commands.deps import deps
|
|
@@ -52,6 +53,7 @@ def cli(ctx):
|
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
# Register command groups
|
|
56
|
+
cli.add_command(audit)
|
|
55
57
|
cli.add_command(deps)
|
|
56
58
|
cli.add_command(pack_cmd, name="pack")
|
|
57
59
|
cli.add_command(unpack_cmd, name="unpack")
|