apm-cli 0.7.9__tar.gz → 0.8.1__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 (145) hide show
  1. {apm_cli-0.7.9/src/apm_cli.egg-info → apm_cli-0.8.1}/PKG-INFO +6 -5
  2. {apm_cli-0.7.9 → apm_cli-0.8.1}/README.md +5 -4
  3. {apm_cli-0.7.9 → apm_cli-0.8.1}/pyproject.toml +1 -1
  4. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/base.py +33 -0
  5. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/codex.py +18 -3
  6. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/copilot.py +5 -0
  7. apm_cli-0.8.1/src/apm_cli/adapters/client/cursor.py +138 -0
  8. apm_cli-0.8.1/src/apm_cli/adapters/client/opencode.py +157 -0
  9. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/vscode.py +37 -1
  10. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/packer.py +38 -3
  11. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/unpacker.py +50 -3
  12. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/cli.py +2 -0
  13. apm_cli-0.8.1/src/apm_cli/commands/audit.py +570 -0
  14. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/compile.py +28 -2
  15. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/install.py +379 -31
  16. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/pack.py +14 -2
  17. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/uninstall.py +29 -1
  18. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/agents_compiler.py +20 -2
  19. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/claude_formatter.py +17 -1
  20. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/target_detection.py +45 -17
  21. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/token_manager.py +64 -2
  22. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/github_downloader.py +125 -15
  23. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/drift.py +7 -1
  24. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/factory.py +4 -0
  25. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/__init__.py +12 -0
  26. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/agent_integrator.py +197 -3
  27. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/base_integrator.py +33 -8
  28. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/command_integrator.py +73 -0
  29. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/hook_integrator.py +168 -4
  30. apm_cli-0.8.1/src/apm_cli/integration/instruction_integrator.py +261 -0
  31. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/mcp_integrator.py +72 -4
  32. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/skill_integrator.py +83 -35
  33. apm_cli-0.8.1/src/apm_cli/integration/targets.py +180 -0
  34. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/dependency.py +7 -28
  35. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/client.py +28 -27
  36. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/operations.py +16 -2
  37. apm_cli-0.8.1/src/apm_cli/security/__init__.py +24 -0
  38. apm_cli-0.8.1/src/apm_cli/security/audit_report.py +251 -0
  39. apm_cli-0.8.1/src/apm_cli/security/content_scanner.py +303 -0
  40. apm_cli-0.8.1/src/apm_cli/security/gate.py +222 -0
  41. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/console.py +1 -1
  42. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/diagnostics.py +106 -2
  43. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/github_host.py +25 -0
  44. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/helpers.py +3 -1
  45. {apm_cli-0.7.9 → apm_cli-0.8.1/src/apm_cli.egg-info}/PKG-INFO +6 -5
  46. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/SOURCES.txt +9 -0
  47. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_apm_package_models.py +15 -35
  48. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_enhanced_discovery.py +2 -2
  49. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_github_downloader.py +444 -0
  50. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_github_downloader_token_precedence.py +5 -2
  51. apm_cli-0.8.1/tests/test_token_manager.py +196 -0
  52. apm_cli-0.7.9/src/apm_cli/integration/instruction_integrator.py +0 -114
  53. {apm_cli-0.7.9 → apm_cli-0.8.1}/AUTHORS +0 -0
  54. {apm_cli-0.7.9 → apm_cli-0.8.1}/LICENSE +0 -0
  55. {apm_cli-0.7.9 → apm_cli-0.8.1}/setup.cfg +0 -0
  56. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/__init__.py +0 -0
  57. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/__init__.py +0 -0
  58. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/__init__.py +0 -0
  59. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  60. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/package_manager/base.py +0 -0
  61. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  62. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/__init__.py +0 -0
  63. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
  64. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/__init__.py +0 -0
  65. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/_helpers.py +0 -0
  66. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/config.py +0 -0
  67. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/deps.py +0 -0
  68. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/init.py +0 -0
  69. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/list_cmd.py +0 -0
  70. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/mcp.py +0 -0
  71. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/prune.py +0 -0
  72. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/run.py +0 -0
  73. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/runtime.py +0 -0
  74. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/update.py +0 -0
  75. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/__init__.py +0 -0
  76. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/constants.py +0 -0
  77. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/constitution.py +0 -0
  78. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/constitution_block.py +0 -0
  79. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/context_optimizer.py +0 -0
  80. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/distributed_compiler.py +0 -0
  81. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/injector.py +0 -0
  82. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/link_resolver.py +0 -0
  83. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/template_builder.py +0 -0
  84. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/config.py +0 -0
  85. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/__init__.py +0 -0
  86. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/conflict_detector.py +0 -0
  87. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/docker_args.py +0 -0
  88. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/operations.py +0 -0
  89. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/safe_installer.py +0 -0
  90. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/script_runner.py +0 -0
  91. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/__init__.py +0 -0
  92. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/aggregator.py +0 -0
  93. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/apm_resolver.py +0 -0
  94. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/collection_parser.py +0 -0
  95. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/dependency_graph.py +0 -0
  96. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/lockfile.py +0 -0
  97. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/package_validator.py +0 -0
  98. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/plugin_parser.py +0 -0
  99. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/verifier.py +0 -0
  100. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/prompt_integrator.py +0 -0
  101. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/skill_transformer.py +0 -0
  102. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/utils.py +0 -0
  103. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/__init__.py +0 -0
  104. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/apm_package.py +0 -0
  105. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/plugin.py +0 -0
  106. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/validation.py +0 -0
  107. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/__init__.py +0 -0
  108. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/formatters.py +0 -0
  109. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/models.py +0 -0
  110. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/script_formatters.py +0 -0
  111. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/__init__.py +0 -0
  112. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/discovery.py +0 -0
  113. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/models.py +0 -0
  114. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/parser.py +0 -0
  115. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/__init__.py +0 -0
  116. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/integration.py +0 -0
  117. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/__init__.py +0 -0
  118. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/base.py +0 -0
  119. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/codex_runtime.py +0 -0
  120. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  121. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/factory.py +0 -0
  122. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/llm_runtime.py +0 -0
  123. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/manager.py +0 -0
  124. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/__init__.py +0 -0
  125. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/version_checker.py +0 -0
  126. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/version.py +0 -0
  127. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/__init__.py +0 -0
  128. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/discovery.py +0 -0
  129. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/parser.py +0 -0
  130. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/runner.py +0 -0
  131. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  132. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/entry_points.txt +0 -0
  133. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/requires.txt +0 -0
  134. {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/top_level.txt +0 -0
  135. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_apm_resolver.py +0 -0
  136. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_codex_docker_args_fix.py +0 -0
  137. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_codex_empty_string_and_defaults.py +0 -0
  138. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_collision_integration.py +0 -0
  139. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_console.py +0 -0
  140. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_distributed_compilation.py +0 -0
  141. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_empty_string_and_defaults.py +0 -0
  142. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_lockfile.py +0 -0
  143. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_runnable_prompts.py +0 -0
  144. {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_runtime_manager_token_precedence.py +0 -0
  145. {apm_cli-0.7.9 → apm_cli-0.8.1}/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.1
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
 
@@ -88,7 +88,7 @@ dependencies:
88
88
  # Specific agent primitives from any repository
89
89
  - github/awesome-copilot/agents/api-architect.agent.md
90
90
  # A full APM package with instructions, skills, prompts, hooks...
91
- - microsoft/apm-sample-package
91
+ - microsoft/apm-sample-package#v1.0.0
92
92
  ```
93
93
 
94
94
  ```bash
@@ -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
 
@@ -148,7 +149,7 @@ pip install apm-cli
148
149
  Then start adding packages:
149
150
 
150
151
  ```bash
151
- apm install microsoft/apm-sample-package
152
+ apm install microsoft/apm-sample-package#v1.0.0
152
153
  ```
153
154
 
154
155
  See the **[Getting Started guide](https://microsoft.github.io/apm/getting-started/quick-start/)** for the full walkthrough.
@@ -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
 
@@ -27,7 +27,7 @@ dependencies:
27
27
  # Specific agent primitives from any repository
28
28
  - github/awesome-copilot/agents/api-architect.agent.md
29
29
  # A full APM package with instructions, skills, prompts, hooks...
30
- - microsoft/apm-sample-package
30
+ - microsoft/apm-sample-package#v1.0.0
31
31
  ```
32
32
 
33
33
  ```bash
@@ -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
 
@@ -87,7 +88,7 @@ pip install apm-cli
87
88
  Then start adding packages:
88
89
 
89
90
  ```bash
90
- apm install microsoft/apm-sample-package
91
+ apm install microsoft/apm-sample-package#v1.0.0
91
92
  ```
92
93
 
93
94
  See the **[Getting Started guide](https://microsoft.github.io/apm/getting-started/quick-start/)** for the full walkthrough.
@@ -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.1"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,7 +1,10 @@
1
1
  """Base adapter interface for MCP clients."""
2
2
 
3
+ import re
3
4
  from abc import ABC, abstractmethod
4
5
 
6
+ _INPUT_VAR_RE = re.compile(r"\$\{input:([^}]+)\}")
7
+
5
8
 
6
9
  class MCPClientAdapter(ABC):
7
10
  """Base adapter for MCP clients."""
@@ -84,3 +87,33 @@ class MCPClientAdapter(ABC):
84
87
  return "nuget"
85
88
 
86
89
  return ""
90
+
91
+ @staticmethod
92
+ def _warn_input_variables(mapping, server_name, runtime_label):
93
+ """Emit a warning for each ``${input:...}`` reference found in *mapping*.
94
+
95
+ Runtimes that do not support VS Code-style input prompts (Copilot CLI,
96
+ Codex CLI, etc.) should call this so users know their placeholders
97
+ will not be resolved at runtime.
98
+
99
+ Args:
100
+ mapping (dict): Header or env dict to scan.
101
+ server_name (str): Server name for the warning message.
102
+ runtime_label (str): Human-readable runtime name (e.g. "Copilot CLI").
103
+ """
104
+ if not mapping:
105
+ return
106
+ seen: set = set()
107
+ for value in mapping.values():
108
+ if not isinstance(value, str):
109
+ continue
110
+ for match in _INPUT_VAR_RE.finditer(value):
111
+ var_id = match.group(1)
112
+ if var_id in seen:
113
+ continue
114
+ seen.add(var_id)
115
+ print(
116
+ f"[!] Warning: ${{input:{var_id}}} in server "
117
+ f"'{server_name}' will not be resolved \u2014 "
118
+ f"{runtime_label} does not support input variable prompts"
119
+ )
@@ -181,6 +181,7 @@ class CodexClientAdapter(MCPClientAdapter):
181
181
  config["args"] = raw["args"]
182
182
  if raw.get("env"):
183
183
  config["env"] = raw["env"]
184
+ self._warn_input_variables(raw["env"], server_info.get("name", ""), "Codex CLI")
184
185
  return config
185
186
 
186
187
  # Note: Remote servers (SSE type) are handled in configure_mcp_server and rejected early
@@ -218,10 +219,24 @@ class CodexClientAdapter(MCPClientAdapter):
218
219
  # Generate command and args based on package type
219
220
  if registry_name == "npm":
220
221
  config["command"] = runtime_hint or "npx"
221
- # Always include package name; filter duplicates from legacy runtime_arguments
222
222
  all_args = processed_runtime_args + processed_package_args
223
- extra_args = [a for a in all_args if a != package_name] if all_args else []
224
- config["args"] = ["-y", package_name] + extra_args
223
+ if all_args:
224
+ # If runtime_arguments already include the package (bare or
225
+ # versioned), use them as-is — they are authoritative from
226
+ # the registry and may carry a version pin.
227
+ has_pkg = any(
228
+ a == package_name or a.startswith(f"{package_name}@")
229
+ for a in all_args
230
+ )
231
+ if has_pkg:
232
+ config["args"] = all_args
233
+ else:
234
+ # Legacy: runtime_arguments don't mention the package,
235
+ # prepend -y + bare name ourselves.
236
+ extra_args = [a for a in all_args if a != "-y"]
237
+ config["args"] = ["-y", package_name] + extra_args
238
+ else:
239
+ config["args"] = ["-y", package_name]
225
240
  # For NPM packages, also use env block for environment variables
226
241
  if resolved_env:
227
242
  config["env"] = resolved_env
@@ -173,6 +173,7 @@ class CopilotClientAdapter(MCPClientAdapter):
173
173
  config["args"] = raw["args"]
174
174
  if raw.get("env"):
175
175
  config["env"] = raw["env"]
176
+ self._warn_input_variables(raw["env"], server_info.get("name", ""), "Copilot CLI")
176
177
  # Apply tools override if present
177
178
  tools_override = server_info.get("_apm_tools_override")
178
179
  if tools_override:
@@ -218,6 +219,10 @@ class CopilotClientAdapter(MCPClientAdapter):
218
219
  resolved_value = self._resolve_env_variable(header_name, header_value, env_overrides)
219
220
  config["headers"][header_name] = resolved_value
220
221
 
222
+ # Warn about unresolvable ${input:...} references in headers
223
+ if config.get("headers"):
224
+ self._warn_input_variables(config["headers"], server_info.get("name", ""), "Copilot CLI")
225
+
221
226
  # Apply tools override from MCP dependency overlay if present
222
227
  tools_override = server_info.get("_apm_tools_override")
223
228
  if tools_override:
@@ -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
@@ -8,7 +8,7 @@ https://code.visualstudio.com/docs/copilot/chat/mcp-servers
8
8
  import json
9
9
  import os
10
10
  from pathlib import Path
11
- from .base import MCPClientAdapter
11
+ from .base import MCPClientAdapter, _INPUT_VAR_RE
12
12
  from ...registry.client import SimpleRegistryClient
13
13
  from ...registry.integration import RegistryIntegration
14
14
 
@@ -197,6 +197,9 @@ class VSCodeClientAdapter(MCPClientAdapter):
197
197
  }
198
198
  if raw.get("env"):
199
199
  server_config["env"] = raw["env"]
200
+ input_vars.extend(
201
+ self._extract_input_variables(raw["env"], server_info.get("name", ""))
202
+ )
200
203
  return server_config, input_vars
201
204
 
202
205
  # Check for packages information
@@ -308,6 +311,9 @@ class VSCodeClientAdapter(MCPClientAdapter):
308
311
  "url": remote.get("url", ""),
309
312
  "headers": headers,
310
313
  }
314
+ input_vars.extend(
315
+ self._extract_input_variables(headers, server_info.get("name", ""))
316
+ )
311
317
  # If no packages AND no endpoints/remotes, fail with clear error
312
318
  else:
313
319
  packages = server_info.get("packages", [])
@@ -323,6 +329,36 @@ class VSCodeClientAdapter(MCPClientAdapter):
323
329
 
324
330
  return server_config, input_vars
325
331
 
332
+ def _extract_input_variables(self, mapping, server_name):
333
+ """Scan dict values for ${input:...} references and return input variable definitions.
334
+
335
+ Args:
336
+ mapping (dict): Header or env dict whose values may contain
337
+ ``${input:<id>}`` placeholders.
338
+ server_name (str): Server name used in the description field.
339
+
340
+ Returns:
341
+ list[dict]: Input variable definitions (``promptString``, ``password: true``).
342
+ Duplicates within *mapping* are already deduplicated.
343
+ """
344
+ seen: set = set()
345
+ result: list = []
346
+ for value in (mapping or {}).values():
347
+ if not isinstance(value, str):
348
+ continue
349
+ for match in _INPUT_VAR_RE.finditer(value):
350
+ var_id = match.group(1)
351
+ if var_id in seen:
352
+ continue
353
+ seen.add(var_id)
354
+ result.append({
355
+ "type": "promptString",
356
+ "id": var_id,
357
+ "description": f"{var_id} for MCP server {server_name}",
358
+ "password": True,
359
+ })
360
+ return result
361
+
326
362
  @staticmethod
327
363
  def _extract_package_args(package):
328
364
  """Extract positional arguments from a package 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,35 @@ def pack_bundle(
153
156
  lockfile_enriched=True,
154
157
  )
155
158
 
159
+ # 5b. Scan files for hidden characters before bundling.
160
+ # Intentionally non-blocking (warn only) — pack is an authoring tool.
161
+ # Critical findings here mean the author's own source files contain
162
+ # hidden characters. We surface them so the author can fix before
163
+ # publishing, but don't block the bundle. Consumers are protected by
164
+ # install/unpack which block on critical.
165
+ from ..security.gate import WARN_POLICY, SecurityGate
166
+ from ..utils.console import _rich_warning
167
+
168
+ _scan_findings_total = 0
169
+ for rel_path in unique_files:
170
+ src = project_root / rel_path
171
+ if src.is_symlink():
172
+ continue
173
+ if src.is_dir():
174
+ verdict = SecurityGate.scan_files(src, policy=WARN_POLICY)
175
+ _scan_findings_total += len(verdict.all_findings)
176
+ elif src.is_file():
177
+ verdict = SecurityGate.scan_text(
178
+ src.read_text(encoding="utf-8", errors="replace"),
179
+ str(src), policy=WARN_POLICY,
180
+ )
181
+ _scan_findings_total += len(verdict.all_findings)
182
+ if _scan_findings_total:
183
+ _rich_warning(
184
+ f"Bundle contains {_scan_findings_total} hidden character(s) across source files "
185
+ f"— run 'apm audit' to inspect before publishing"
186
+ )
187
+
156
188
  # 6. Build output directory
157
189
  bundle_dir = output_dir / f"{pkg_name}-{pkg_version}"
158
190
  bundle_dir.mkdir(parents=True, exist_ok=True)
@@ -160,12 +192,15 @@ def pack_bundle(
160
192
  # 7. Copy files preserving directory structure
161
193
  for rel_path in unique_files:
162
194
  src = project_root / rel_path
195
+ if src.is_symlink():
196
+ continue # Never bundle symlinks
163
197
  dest = bundle_dir / rel_path
164
198
  if src.is_dir():
165
- shutil.copytree(src, dest, dirs_exist_ok=True)
199
+ from ..security.gate import ignore_symlinks
200
+ shutil.copytree(src, dest, dirs_exist_ok=True, ignore=ignore_symlinks)
166
201
  else:
167
202
  dest.parent.mkdir(parents=True, exist_ok=True)
168
- shutil.copy2(src, dest)
203
+ shutil.copy2(src, dest, follow_symlinks=False)
169
204
 
170
205
  # 8. Enrich lockfile copy and write to bundle
171
206
  enriched_yaml = enrich_lockfile_for_pack(lockfile, fmt, effective_target)