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.
Files changed (142) hide show
  1. {apm_cli-0.7.9/src/apm_cli.egg-info → apm_cli-0.8.0}/PKG-INFO +4 -3
  2. {apm_cli-0.7.9 → apm_cli-0.8.0}/README.md +3 -2
  3. {apm_cli-0.7.9 → apm_cli-0.8.0}/pyproject.toml +1 -1
  4. apm_cli-0.8.0/src/apm_cli/adapters/client/cursor.py +138 -0
  5. apm_cli-0.8.0/src/apm_cli/adapters/client/opencode.py +157 -0
  6. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/packer.py +32 -1
  7. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/cli.py +2 -0
  8. apm_cli-0.8.0/src/apm_cli/commands/audit.py +504 -0
  9. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/compile.py +15 -2
  10. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/install.py +387 -26
  11. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/pack.py +1 -1
  12. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/uninstall.py +29 -1
  13. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/agents_compiler.py +12 -0
  14. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/claude_formatter.py +11 -0
  15. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/target_detection.py +45 -17
  16. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/github_downloader.py +2 -1
  17. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/factory.py +4 -0
  18. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/__init__.py +12 -0
  19. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/agent_integrator.py +197 -3
  20. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/base_integrator.py +29 -7
  21. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/command_integrator.py +73 -0
  22. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/hook_integrator.py +168 -4
  23. apm_cli-0.8.0/src/apm_cli/integration/instruction_integrator.py +261 -0
  24. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/mcp_integrator.py +72 -4
  25. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/skill_integrator.py +83 -35
  26. apm_cli-0.8.0/src/apm_cli/integration/targets.py +180 -0
  27. apm_cli-0.8.0/src/apm_cli/security/__init__.py +5 -0
  28. apm_cli-0.8.0/src/apm_cli/security/content_scanner.py +303 -0
  29. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/console.py +1 -1
  30. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/diagnostics.py +84 -2
  31. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/helpers.py +3 -1
  32. {apm_cli-0.7.9 → apm_cli-0.8.0/src/apm_cli.egg-info}/PKG-INFO +4 -3
  33. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/SOURCES.txt +6 -0
  34. apm_cli-0.7.9/src/apm_cli/integration/instruction_integrator.py +0 -114
  35. {apm_cli-0.7.9 → apm_cli-0.8.0}/AUTHORS +0 -0
  36. {apm_cli-0.7.9 → apm_cli-0.8.0}/LICENSE +0 -0
  37. {apm_cli-0.7.9 → apm_cli-0.8.0}/setup.cfg +0 -0
  38. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/__init__.py +0 -0
  39. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/__init__.py +0 -0
  40. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/__init__.py +0 -0
  41. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/base.py +0 -0
  42. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/codex.py +0 -0
  43. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/copilot.py +0 -0
  44. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/client/vscode.py +0 -0
  45. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  46. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/base.py +0 -0
  47. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  48. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/__init__.py +0 -0
  49. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
  50. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/bundle/unpacker.py +0 -0
  51. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/__init__.py +0 -0
  52. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/_helpers.py +0 -0
  53. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/config.py +0 -0
  54. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/deps.py +0 -0
  55. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/init.py +0 -0
  56. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/list_cmd.py +0 -0
  57. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/mcp.py +0 -0
  58. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/prune.py +0 -0
  59. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/run.py +0 -0
  60. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/runtime.py +0 -0
  61. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/commands/update.py +0 -0
  62. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/__init__.py +0 -0
  63. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/constants.py +0 -0
  64. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution.py +0 -0
  65. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution_block.py +0 -0
  66. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/context_optimizer.py +0 -0
  67. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/distributed_compiler.py +0 -0
  68. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/injector.py +0 -0
  69. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/link_resolver.py +0 -0
  70. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/compilation/template_builder.py +0 -0
  71. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/config.py +0 -0
  72. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/__init__.py +0 -0
  73. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/conflict_detector.py +0 -0
  74. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/docker_args.py +0 -0
  75. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/operations.py +0 -0
  76. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/safe_installer.py +0 -0
  77. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/script_runner.py +0 -0
  78. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/core/token_manager.py +0 -0
  79. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/__init__.py +0 -0
  80. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/aggregator.py +0 -0
  81. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/apm_resolver.py +0 -0
  82. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/collection_parser.py +0 -0
  83. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/dependency_graph.py +0 -0
  84. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/lockfile.py +0 -0
  85. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/package_validator.py +0 -0
  86. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/plugin_parser.py +0 -0
  87. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/deps/verifier.py +0 -0
  88. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/drift.py +0 -0
  89. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/prompt_integrator.py +0 -0
  90. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/skill_transformer.py +0 -0
  91. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/integration/utils.py +0 -0
  92. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/__init__.py +0 -0
  93. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/apm_package.py +0 -0
  94. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/dependency.py +0 -0
  95. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/plugin.py +0 -0
  96. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/models/validation.py +0 -0
  97. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/__init__.py +0 -0
  98. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/formatters.py +0 -0
  99. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/models.py +0 -0
  100. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/output/script_formatters.py +0 -0
  101. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/__init__.py +0 -0
  102. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/discovery.py +0 -0
  103. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/models.py +0 -0
  104. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/primitives/parser.py +0 -0
  105. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/__init__.py +0 -0
  106. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/client.py +0 -0
  107. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/integration.py +0 -0
  108. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/registry/operations.py +0 -0
  109. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/__init__.py +0 -0
  110. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/base.py +0 -0
  111. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/codex_runtime.py +0 -0
  112. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  113. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/factory.py +0 -0
  114. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/llm_runtime.py +0 -0
  115. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/runtime/manager.py +0 -0
  116. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/__init__.py +0 -0
  117. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/github_host.py +0 -0
  118. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/utils/version_checker.py +0 -0
  119. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/version.py +0 -0
  120. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/__init__.py +0 -0
  121. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/discovery.py +0 -0
  122. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/parser.py +0 -0
  123. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli/workflow/runner.py +0 -0
  124. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  125. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/entry_points.txt +0 -0
  126. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/requires.txt +0 -0
  127. {apm_cli-0.7.9 → apm_cli-0.8.0}/src/apm_cli.egg-info/top_level.txt +0 -0
  128. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_apm_package_models.py +0 -0
  129. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_apm_resolver.py +0 -0
  130. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_codex_docker_args_fix.py +0 -0
  131. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_codex_empty_string_and_defaults.py +0 -0
  132. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_collision_integration.py +0 -0
  133. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_console.py +0 -0
  134. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_distributed_compilation.py +0 -0
  135. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_empty_string_and_defaults.py +0 -0
  136. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_enhanced_discovery.py +0 -0
  137. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_github_downloader.py +0 -0
  138. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_github_downloader_token_precedence.py +0 -0
  139. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_lockfile.py +0 -0
  140. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_runnable_prompts.py +0 -0
  141. {apm_cli-0.7.9 → apm_cli-0.8.0}/tests/test_runtime_manager_token_precedence.py +0 -0
  142. {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.7.9
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) and `CLAUDE.md` (Claude Code)
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) and `CLAUDE.md` (Claude Code)
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.7.9"
7
+ version = "0.8.0"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- "all": [".github/", ".claude/"],
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")