runlayer 0.22.0__tar.gz → 0.22.2__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 (203) hide show
  1. {runlayer-0.22.0 → runlayer-0.22.2}/PKG-INFO +7 -6
  2. {runlayer-0.22.0 → runlayer-0.22.2}/README.md +5 -5
  3. {runlayer-0.22.0 → runlayer-0.22.2}/pyproject.toml +2 -1
  4. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/setup.py +1 -1
  5. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/skills.py +58 -23
  6. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/installer.py +4 -8
  7. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/clients.py +34 -0
  8. runlayer-0.22.2/runlayer_cli/scan/codex_plugins.py +168 -0
  9. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/config_parser.py +110 -8
  10. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/cursor_plugins.py +2 -21
  11. runlayer-0.22.2/runlayer_cli/scan/opencode_plugins.py +198 -0
  12. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/plugin_scanner.py +297 -1
  13. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/project_scanner.py +39 -11
  14. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/service.py +48 -4
  15. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/skill_scanner.py +1 -0
  16. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/sync_engine.py +3 -3
  17. runlayer-0.22.2/tests/test_opencode_plugins.py +287 -0
  18. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugin_scanner.py +307 -0
  19. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_clients.py +51 -0
  20. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_parser.py +236 -2
  21. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_skills.py +17 -0
  22. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_commands.py +160 -3
  23. {runlayer-0.22.0 → runlayer-0.22.2}/.gitignore +0 -0
  24. {runlayer-0.22.0 → runlayer-0.22.2}/.python-version +0 -0
  25. {runlayer-0.22.0 → runlayer-0.22.2}/AGENTS.md +0 -0
  26. {runlayer-0.22.0 → runlayer-0.22.2}/CLAUDE.md +0 -0
  27. {runlayer-0.22.0 → runlayer-0.22.2}/LICENSE +0 -0
  28. {runlayer-0.22.0 → runlayer-0.22.2}/Makefile +0 -0
  29. {runlayer-0.22.0 → runlayer-0.22.2}/development.md +0 -0
  30. {runlayer-0.22.0 → runlayer-0.22.2}/hooks/README.md +0 -0
  31. {runlayer-0.22.0 → runlayer-0.22.2}/hooks/__init__.py +0 -0
  32. {runlayer-0.22.0 → runlayer-0.22.2}/hooks/runlayer-hook.sh +0 -0
  33. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/__init__.py +0 -0
  34. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/api.py +0 -0
  35. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/__init__.py +0 -0
  36. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/auth.py +0 -0
  37. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/cache.py +0 -0
  38. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/deploy.py +0 -0
  39. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/hooks.py +0 -0
  40. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/logs.py +0 -0
  41. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/org_api_key.py +0 -0
  42. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/plugins.py +0 -0
  43. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/scan.py +0 -0
  44. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/config.py +0 -0
  45. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/console.py +0 -0
  46. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/credential_store.py +0 -0
  47. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/__init__.py +0 -0
  48. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/config.py +0 -0
  49. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/docker_builder.py +0 -0
  50. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/service.py +0 -0
  51. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/env_substitution.py +0 -0
  52. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/index.html +0 -0
  53. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/logging.py +0 -0
  54. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/main.py +0 -0
  55. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/metrics.py +0 -0
  56. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/middleware.py +0 -0
  57. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/models.py +0 -0
  58. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/oauth.py +0 -0
  59. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/oauth_callback.py +0 -0
  60. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/paths.py +0 -0
  61. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/__init__.py +0 -0
  62. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/claude_manifest.py +0 -0
  63. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/discovery.py +0 -0
  64. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/models.py +0 -0
  65. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/sync_engine.py +0 -0
  66. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/__init__.py +0 -0
  67. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/claude_code_plugins.py +0 -0
  68. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/device.py +0 -0
  69. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/file_collector.py +0 -0
  70. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/openclaw_detector.py +0 -0
  71. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/AGENTS.md +0 -0
  72. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/CLAUDE.md +0 -0
  73. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/README.md +0 -0
  74. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/__init__.py +0 -0
  75. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/hashing.py +0 -0
  76. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/merkle.py +0 -0
  77. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/skill_id.py +0 -0
  78. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/types.py +0 -0
  79. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/__init__.py +0 -0
  80. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/discovery.py +0 -0
  81. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/installer.py +0 -0
  82. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/models.py +0 -0
  83. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/symbols.py +0 -0
  84. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/sync.py +0 -0
  85. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/README.md +0 -0
  86. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/__init__.py +0 -0
  87. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/config.py +0 -0
  88. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/exceptions.py +0 -0
  89. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/proxy.py +0 -0
  90. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/__init__.py +0 -0
  91. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/base.py +0 -0
  92. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/macos.py +0 -0
  93. {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/windows.py +0 -0
  94. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/__init__.py +0 -0
  95. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/cache/__init__.py +0 -0
  96. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/cache/test_cache.py +0 -0
  97. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/conftest.py +0 -0
  98. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/deploy/__init__.py +0 -0
  99. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/deploy/test_lifecycle.py +0 -0
  100. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/deploy/test_validate.py +0 -0
  101. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/claude_code.md +0 -0
  102. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/claude_desktop.md +0 -0
  103. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/codex.md +0 -0
  104. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/cursor.md +0 -0
  105. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/goose.md +0 -0
  106. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/opencode.md +0 -0
  107. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/vscode.md +0 -0
  108. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/windsurf.md +0 -0
  109. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/zed.md +0 -0
  110. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.claude-plugin/marketplace.json +0 -0
  111. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.claude-plugin/plugin.json +0 -0
  112. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.lsp.json +0 -0
  113. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.mcp.json +0 -0
  114. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/README.md +0 -0
  115. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/agents/README.md +0 -0
  116. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/agents/code-reviewer.md +0 -0
  117. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/commands/review.md +0 -0
  118. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/config.json +0 -0
  119. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/hooks/hooks.json +0 -0
  120. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/hooks/validate.sh +0 -0
  121. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/notes.txt +0 -0
  122. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/scripts/deploy.sh +0 -0
  123. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/settings.json +0 -0
  124. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/code-review/SKILL.md +0 -0
  125. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/code-review/prompts.md +0 -0
  126. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/SKILL.md +0 -0
  127. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/helper.py +0 -0
  128. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills-v2/notes.md +0 -0
  129. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skillsets/reference.md +0 -0
  130. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/tool.ts +0 -0
  131. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/org_api_key/__init__.py +0 -0
  132. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/org_api_key/test_org_api_key.py +0 -0
  133. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/__init__.py +0 -0
  134. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/test_add.py +0 -0
  135. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/test_mcp_fallback.py +0 -0
  136. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/test_push.py +0 -0
  137. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/run/__init__.py +0 -0
  138. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/run/test_run.py +0 -0
  139. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/scan/__init__.py +0 -0
  140. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/scan/test_scan.py +0 -0
  141. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/setup/test_install_opencode.py +0 -0
  142. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/skills/__init__.py +0 -0
  143. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/skills/test_lifecycle.py +0 -0
  144. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/test_help_matrix.py +0 -0
  145. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/version/__init__.py +0 -0
  146. {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/version/test_version.py +0 -0
  147. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/claude_code_config.json +0 -0
  148. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/claude_desktop_config.json +0 -0
  149. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/cursor_config.json +0 -0
  150. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/empty_config.json +0 -0
  151. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/goose_config.yaml +0 -0
  152. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/invalid_json.txt +0 -0
  153. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/sse_server_config.json +0 -0
  154. {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/vscode_config.json +0 -0
  155. {runlayer-0.22.0 → runlayer-0.22.2}/tests/skill_identifier/__init__.py +0 -0
  156. {runlayer-0.22.0 → runlayer-0.22.2}/tests/skill_identifier/test_merkle.py +0 -0
  157. {runlayer-0.22.0 → runlayer-0.22.2}/tests/skill_identifier/test_skill_id.py +0 -0
  158. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_api.py +0 -0
  159. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_auth.py +0 -0
  160. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_backwards_compatibility.py +0 -0
  161. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cache.py +0 -0
  162. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_claude_code_plugins.py +0 -0
  163. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_claude_json_integration.py +0 -0
  164. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cli.py +0 -0
  165. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cli_backwards_compat.py +0 -0
  166. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_config.py +0 -0
  167. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_credential_store.py +0 -0
  168. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cursor_plugins.py +0 -0
  169. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_deploy_service.py +0 -0
  170. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_docker_builder.py +0 -0
  171. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_env_substitution.py +0 -0
  172. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_hook_script.py +0 -0
  173. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_hooks_relay.py +0 -0
  174. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_logging.py +0 -0
  175. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_logs.py +0 -0
  176. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_metrics.py +0 -0
  177. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_middleware.py +0 -0
  178. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_oauth_browser_lock.py +0 -0
  179. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_oauth_token_storage.py +0 -0
  180. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_org_api_key_commands.py +0 -0
  181. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_commands.py +0 -0
  182. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_discovery.py +0 -0
  183. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_installer.py +0 -0
  184. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_sync.py +0 -0
  185. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_device.py +0 -0
  186. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_openclaw.py +0 -0
  187. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_project.py +0 -0
  188. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_service.py +0 -0
  189. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_hooks.py +0 -0
  190. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_install_claude_desktop.py +0 -0
  191. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_install_config_formats.py +0 -0
  192. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_install_local_servers.py +0 -0
  193. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_discovery.py +0 -0
  194. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_installer.py +0 -0
  195. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_sync.py +0 -0
  196. {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_symbols.py +0 -0
  197. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/__init__.py +0 -0
  198. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_config.py +0 -0
  199. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_proxy.py +0 -0
  200. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/__init__.py +0 -0
  201. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/test_base.py +0 -0
  202. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/test_macos.py +0 -0
  203. {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/test_windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runlayer
3
- Version: 0.22.0
3
+ Version: 0.22.2
4
4
  Summary: A command-line interface for running MCP servers via HTTP transport
5
5
  Project-URL: Homepage, https://runlayer.com
6
6
  Project-URL: Documentation, https://docs.runlayer.com/
@@ -229,6 +229,7 @@ Requires-Dist: pyyaml>=6.0.0
229
229
  Requires-Dist: questionary>=2.0.0
230
230
  Requires-Dist: rich>=13.0.0
231
231
  Requires-Dist: structlog>=25.4.0
232
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
232
233
  Requires-Dist: typer>=0.17.4
233
234
  Description-Content-Type: text/markdown
234
235
 
@@ -531,7 +532,7 @@ The `--secret` flag still accepts any raw API key (user or org) directly and tak
531
532
 
532
533
  ### `scan` - Scan MCP Client Configurations
533
534
 
534
- Scan for MCP server configurations across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf) and submit results to Runlayer for classification.
535
+ Scan for MCP server configurations, skills, and plugins across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf, Goose, Zed, OpenCode, Codex) and submit results to Runlayer for classification.
535
536
 
536
537
  #### Command Options
537
538
 
@@ -732,12 +733,12 @@ uvx runlayer skills push [PATH] --namespace <namespace> [OPTIONS]
732
733
  uvx runlayer skills scan <PATH> [OPTIONS]
733
734
  ```
734
735
 
735
- - `PATH`: Skill dir or `SKILL.md` path. Must resolve to exactly one skill
736
- - `--name`: Override skill name sent to the security scan API
736
+ - `PATH`: Skill dir, `SKILL.md` path, or a root containing many skills
737
+ - `--name`: Override skill name sent to the security scan API for single-skill scans only
737
738
  - `--fail-on`: Exit non-zero on `warn` or `block`
738
- - Output is always JSON
739
+ - Output is always JSON. Single-skill scans return one scan object; multi-skill scans return `{ "skills": [...] }`
739
740
 
740
- This calls the security scan API to score one local skill without pushing it to the
741
+ This calls the security scan API to score one or many local skills without pushing them to the
741
742
  Runlayer skills catalog or AI Watch shadow-skill discovery flow.
742
743
 
743
744
  #### `skills add`
@@ -297,7 +297,7 @@ The `--secret` flag still accepts any raw API key (user or org) directly and tak
297
297
 
298
298
  ### `scan` - Scan MCP Client Configurations
299
299
 
300
- Scan for MCP server configurations across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf) and submit results to Runlayer for classification.
300
+ Scan for MCP server configurations, skills, and plugins across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf, Goose, Zed, OpenCode, Codex) and submit results to Runlayer for classification.
301
301
 
302
302
  #### Command Options
303
303
 
@@ -498,12 +498,12 @@ uvx runlayer skills push [PATH] --namespace <namespace> [OPTIONS]
498
498
  uvx runlayer skills scan <PATH> [OPTIONS]
499
499
  ```
500
500
 
501
- - `PATH`: Skill dir or `SKILL.md` path. Must resolve to exactly one skill
502
- - `--name`: Override skill name sent to the security scan API
501
+ - `PATH`: Skill dir, `SKILL.md` path, or a root containing many skills
502
+ - `--name`: Override skill name sent to the security scan API for single-skill scans only
503
503
  - `--fail-on`: Exit non-zero on `warn` or `block`
504
- - Output is always JSON
504
+ - Output is always JSON. Single-skill scans return one scan object; multi-skill scans return `{ "skills": [...] }`
505
505
 
506
- This calls the security scan API to score one local skill without pushing it to the
506
+ This calls the security scan API to score one or many local skills without pushing them to the
507
507
  Runlayer skills catalog or AI Watch shadow-skill discovery flow.
508
508
 
509
509
  #### `skills add`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runlayer"
3
- version = "0.22.0"
3
+ version = "0.22.2"
4
4
  description = "A command-line interface for running MCP servers via HTTP transport"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -29,6 +29,7 @@ dependencies = [
29
29
  "rich>=13.0.0",
30
30
  "python-dotenv>=1.0.0",
31
31
  "json5>=0.12.1",
32
+ "tomli>=2.0.0; python_version < '3.11'",
32
33
  "questionary>=2.0.0",
33
34
  "keyring>=25.0.0,<26",
34
35
  ]
@@ -1175,7 +1175,7 @@ def _build_opencode_server_entry(spec: InstallServerSpec) -> dict[str, Any]:
1175
1175
  }
1176
1176
  return entry
1177
1177
 
1178
- entry = {"enabled": True, "type": "remote", "url": spec.proxy_url}
1178
+ entry: dict[str, Any] = {"enabled": True, "type": "remote", "url": spec.proxy_url}
1179
1179
  if spec.headers:
1180
1180
  entry["headers"] = spec.headers
1181
1181
  return entry
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from enum import Enum
2
3
  from pathlib import Path
3
4
 
@@ -5,7 +6,7 @@ import anyio
5
6
  import structlog
6
7
  import typer
7
8
 
8
- from runlayer_cli.api import RunlayerClient
9
+ from runlayer_cli.api import RunlayerClient, SkillScanResponse
9
10
  from runlayer_cli.console import print_error
10
11
  from runlayer_cli.config import resolve_credentials, set_credentials_in_context
11
12
  from runlayer_cli.logging import setup_logging
@@ -61,7 +62,7 @@ def _resolve_client(client_name: str | None) -> str:
61
62
  return "claude_code"
62
63
 
63
64
 
64
- def _load_single_skill(path: str) -> DiscoveredSkill:
65
+ def _load_skills(path: str) -> list[DiscoveredSkill]:
65
66
  target = Path(path).resolve()
66
67
  if not target.exists():
67
68
  raise ValueError(f"Path does not exist: {target}")
@@ -73,14 +74,51 @@ def _load_single_skill(path: str) -> DiscoveredSkill:
73
74
  else:
74
75
  root = target
75
76
 
76
- discovered = discover_skills(root)
77
- if not discovered:
77
+ skills = discover_skills(root)
78
+ if not skills:
78
79
  raise ValueError(f"No skill found under {root}")
79
- if len(discovered) > 1:
80
- raise ValueError(
81
- f"Found {len(discovered)} skills under {root}. Pass a single skill dir."
82
- )
83
- return discovered[0]
80
+ return skills
81
+
82
+
83
+ def _scan_skill(client: RunlayerClient, skill: DiscoveredSkill) -> SkillScanResponse:
84
+ return client.score_skill(
85
+ skill_name=skill.name,
86
+ files=[
87
+ {
88
+ "name": file.title,
89
+ "content": file.content,
90
+ }
91
+ for file in skill.files
92
+ ],
93
+ )
94
+
95
+
96
+ def _override_skill_name(skill: DiscoveredSkill, name: str) -> DiscoveredSkill:
97
+ return DiscoveredSkill(
98
+ path=skill.path,
99
+ name=name,
100
+ description=skill.description,
101
+ files=skill.files,
102
+ )
103
+
104
+
105
+ def _render_scan_results(
106
+ scanned_skills: list[tuple[DiscoveredSkill, SkillScanResponse]],
107
+ ) -> str:
108
+ if len(scanned_skills) == 1:
109
+ return scanned_skills[0][1].model_dump_json(indent=2)
110
+
111
+ payload = {
112
+ "skills": [
113
+ {
114
+ "path": skill.path,
115
+ "name": skill.name,
116
+ **result.model_dump(mode="json"),
117
+ }
118
+ for skill, result in scanned_skills
119
+ ]
120
+ }
121
+ return json.dumps(payload, indent=2)
84
122
 
85
123
 
86
124
  def _should_fail(risk_level: str, fail_on: FailOn | None) -> bool:
@@ -184,23 +222,20 @@ def scan(
184
222
  credentials = resolve_credentials(ctx, require_auth=True, allow_org_key=True)
185
223
 
186
224
  try:
187
- skill = _load_single_skill(path)
188
- skill_name = name or skill.name
225
+ skills = _load_skills(path)
226
+ if name is not None and len(skills) != 1:
227
+ raise ValueError("--name requires a single skill path")
228
+ if name is not None:
229
+ skills[0] = _override_skill_name(skills[0], name)
189
230
  client = RunlayerClient(
190
231
  hostname=credentials["host"], secret=credentials["secret"]
191
232
  )
192
- result = client.score_skill(
193
- skill_name=skill_name,
194
- files=[
195
- {
196
- "name": file.title,
197
- "content": file.content,
198
- }
199
- for file in skill.files
200
- ],
201
- )
202
- typer.echo(result.model_dump_json(indent=2))
203
- if _should_fail(result.skill_risk_level, fail_on):
233
+ scanned_skills = [(skill, _scan_skill(client, skill)) for skill in skills]
234
+ typer.echo(_render_scan_results(scanned_skills))
235
+ if any(
236
+ _should_fail(result.skill_risk_level, fail_on)
237
+ for _, result in scanned_skills
238
+ ):
204
239
  raise typer.Exit(2 if fail_on == FailOn.WARN else 3)
205
240
  except typer.Exit:
206
241
  raise
@@ -380,14 +380,10 @@ def _build_plugin_mcp_config(
380
380
  host: str,
381
381
  client_name: str,
382
382
  ) -> dict[str, dict[str, dict[str, str]]]:
383
- # VS Code plugin `.mcp.json` uses "mcpServers" (plugin schema),
384
- # while VS Code client config uses "servers".
385
- if client_name == "vscode":
386
- servers_key = "mcpServers"
387
- else:
388
- client_def = get_client_by_name(client_name)
389
- servers_key = client_def.servers_key if client_def else "mcpServers"
390
- return {servers_key: _build_plugin_proxy_servers(plugin, host)}
383
+ # Plugin `.mcp.json` always uses the standard "mcpServers" key regardless
384
+ # of what key the client's own config format uses (e.g. VS Code uses
385
+ # "servers", Codex uses "mcp_servers" in TOML).
386
+ return {"mcpServers": _build_plugin_proxy_servers(plugin, host)}
391
387
 
392
388
 
393
389
  def _write_plugin_mcp_json(
@@ -404,6 +404,40 @@ MCP_CLIENTS: list[MCPClientDefinition] = [
404
404
  ),
405
405
  notes="Servers live under top-level 'mcp' and use OpenCode format (type=local|remote, command[]=..., environment={...}).",
406
406
  ),
407
+ MCPClientDefinition(
408
+ name="codex",
409
+ display_name="Codex",
410
+ paths=[
411
+ ConfigPath("~/.codex/config.toml", platform="macos"),
412
+ ConfigPath("~/.codex/config.toml", platform="linux"),
413
+ ConfigPath("%USERPROFILE%/.codex/config.toml", platform="windows"),
414
+ ],
415
+ servers_key="mcp_servers",
416
+ config_format="toml",
417
+ project_config=ProjectConfigPattern(
418
+ relative_path=".codex/config.toml",
419
+ servers_key="mcp_servers",
420
+ ),
421
+ plugin_paths=[
422
+ PluginPath(
423
+ "~/.codex/plugins/cache",
424
+ platform="macos",
425
+ mcp_filenames=("mcp.json", ".mcp.json"),
426
+ ),
427
+ PluginPath(
428
+ "%USERPROFILE%/.codex/plugins/cache",
429
+ platform="windows",
430
+ mcp_filenames=("mcp.json", ".mcp.json"),
431
+ ),
432
+ PluginPath(
433
+ "~/.codex/plugins/cache",
434
+ platform="linux",
435
+ mcp_filenames=("mcp.json", ".mcp.json"),
436
+ ),
437
+ ],
438
+ notes="TOML format. MCP servers under [mcp_servers.<name>] tables. "
439
+ "Plugins in ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/.",
440
+ ),
407
441
  # ==========================================================================
408
442
  # DESCOPED FROM v0 - Add these in a future release
409
443
  # ==========================================================================
@@ -0,0 +1,168 @@
1
+ """Discover MCP servers bundled inside installed Codex plugins.
2
+
3
+ Plugins are cached at ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/.
4
+ Each plugin may contain mcp.json or .mcp.json with server definitions either
5
+ under the "mcpServers" key or at root level.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import platform
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ import structlog
17
+
18
+ from runlayer_cli.scan.config_parser import (
19
+ MCPClientConfig,
20
+ MCPServerConfig,
21
+ parse_plugin_mcp_file,
22
+ )
23
+ from runlayer_cli.scan.plugin_scanner import (
24
+ _version_sort_key,
25
+ compute_plugin_identifier,
26
+ )
27
+
28
+ logger = structlog.get_logger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class DiscoveredCodexPlugin:
33
+ """A Codex plugin found in the cache with MCP server definitions."""
34
+
35
+ name: str
36
+ config_path: Path
37
+ config_modified_at: str | None
38
+ servers: list[MCPServerConfig]
39
+ plugin_identifier: str | None = None
40
+
41
+
42
+ def _discover_codex_plugins(
43
+ plugin_cache_base: Path,
44
+ mcp_filenames: tuple[str, ...] = ("mcp.json", ".mcp.json"),
45
+ ) -> list[DiscoveredCodexPlugin]:
46
+ """Scan Codex plugin cache for plugins containing MCP configs.
47
+
48
+ Traverses <base>/<marketplace>/<plugin>/<version>/ looking for MCP files.
49
+ """
50
+ if not plugin_cache_base.is_dir():
51
+ return []
52
+
53
+ discovered: list[DiscoveredCodexPlugin] = []
54
+ seen: set[str] = set()
55
+
56
+ try:
57
+ for marketplace_dir in sorted(plugin_cache_base.iterdir()):
58
+ if not marketplace_dir.is_dir():
59
+ continue
60
+
61
+ for plugin_dir in sorted(marketplace_dir.iterdir()):
62
+ if not plugin_dir.is_dir():
63
+ continue
64
+ plugin_name = plugin_dir.name
65
+ seen_key = f"{marketplace_dir.name}/{plugin_name}"
66
+ if seen_key in seen:
67
+ continue
68
+
69
+ for version_dir in sorted(
70
+ plugin_dir.iterdir(), key=_version_sort_key, reverse=True
71
+ ):
72
+ if not version_dir.is_dir():
73
+ continue
74
+
75
+ for mcp_filename in mcp_filenames:
76
+ mcp_path = version_dir / mcp_filename
77
+ if not mcp_path.exists():
78
+ continue
79
+
80
+ servers = parse_plugin_mcp_file(mcp_path, plugin_name)
81
+ if not servers:
82
+ continue
83
+
84
+ try:
85
+ mtime = mcp_path.stat().st_mtime
86
+ modified_at = datetime.fromtimestamp(
87
+ mtime, tz=timezone.utc
88
+ ).isoformat()
89
+ except OSError:
90
+ modified_at = None
91
+
92
+ plugin_id = compute_plugin_identifier(version_dir)
93
+ discovered.append(
94
+ DiscoveredCodexPlugin(
95
+ name=plugin_name,
96
+ config_path=mcp_path,
97
+ config_modified_at=modified_at,
98
+ servers=servers,
99
+ plugin_identifier=plugin_id,
100
+ )
101
+ )
102
+ seen.add(seen_key)
103
+ logger.debug(
104
+ "Found Codex plugin MCP config",
105
+ plugin=plugin_name,
106
+ server_count=len(servers),
107
+ )
108
+ break # found mcp config in this version dir
109
+ else:
110
+ continue
111
+ break # use first version dir with an mcp config
112
+
113
+ except OSError as e:
114
+ logger.warning(
115
+ "Failed to scan Codex plugin cache",
116
+ path=str(plugin_cache_base),
117
+ error=str(e),
118
+ )
119
+
120
+ return discovered
121
+
122
+
123
+ def scan_codex_plugins(
124
+ plugin_cache_base: Path | None = None,
125
+ ) -> list[MCPClientConfig]:
126
+ """Scan Codex plugin cache for bundled MCP servers.
127
+
128
+ Args:
129
+ plugin_cache_base: Override path for testing (uses default if None)
130
+
131
+ Returns:
132
+ List of MCPClientConfig entries for plugins with MCP servers.
133
+ """
134
+ if plugin_cache_base is None:
135
+ if platform.system() == "Windows":
136
+ profile = os.environ.get("USERPROFILE")
137
+ if not profile:
138
+ return []
139
+ plugin_cache_base = Path(profile) / ".codex" / "plugins" / "cache"
140
+ else:
141
+ try:
142
+ plugin_cache_base = Path.home() / ".codex" / "plugins" / "cache"
143
+ except RuntimeError:
144
+ return []
145
+
146
+ plugins = _discover_codex_plugins(plugin_cache_base)
147
+ if not plugins:
148
+ return []
149
+
150
+ configurations: list[MCPClientConfig] = []
151
+ for plugin in plugins:
152
+ configurations.append(
153
+ MCPClientConfig(
154
+ client="codex",
155
+ config_path=str(plugin.config_path),
156
+ config_modified_at=plugin.config_modified_at,
157
+ servers=plugin.servers,
158
+ config_scope="plugin",
159
+ plugin_identifier=plugin.plugin_identifier,
160
+ )
161
+ )
162
+ logger.info(
163
+ "Found MCP servers in Codex plugin",
164
+ plugin=plugin.name,
165
+ server_count=len(plugin.servers),
166
+ )
167
+
168
+ return configurations
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import json
7
+ import sys
7
8
  from dataclasses import dataclass, field
8
9
  from datetime import datetime, timezone
9
10
  from pathlib import Path
@@ -13,6 +14,11 @@ import json5
13
14
  import structlog
14
15
  import yaml
15
16
 
17
+ if sys.version_info >= (3, 11):
18
+ import tomllib
19
+ else:
20
+ import tomli as tomllib
21
+
16
22
  if TYPE_CHECKING:
17
23
  from runlayer_cli.scan.clients import MCPClientDefinition
18
24
 
@@ -120,6 +126,25 @@ def parse_plugin_mcp_entries(
120
126
  return servers
121
127
 
122
128
 
129
+ def parse_plugin_mcp_file(mcp_path: Path, plugin_name: str) -> list[MCPServerConfig]:
130
+ """Read and parse an mcp.json / .mcp.json from a plugin directory."""
131
+ try:
132
+ raw = json5.loads(mcp_path.read_text(encoding="utf-8"))
133
+ except (ValueError, OSError) as e:
134
+ logger.warning(
135
+ "Failed to parse plugin MCP config",
136
+ plugin=plugin_name,
137
+ path=str(mcp_path),
138
+ error=str(e),
139
+ )
140
+ return []
141
+
142
+ if not isinstance(raw, dict):
143
+ return []
144
+
145
+ return parse_plugin_mcp_entries(raw, plugin_name)
146
+
147
+
123
148
  def _parse_goose_extension(name: str, config: dict[str, Any]) -> MCPServerConfig | None:
124
149
  """Parse a Goose extension entry from config file.
125
150
 
@@ -266,7 +291,8 @@ def _parse_opencode_mcp_server(
266
291
  cmd = command_list[0] if isinstance(command_list[0], str) else None
267
292
  if not cmd:
268
293
  return None
269
- args = [a for a in command_list[1:] if isinstance(a, str)] or None
294
+ filtered_args: list[str] = [a for a in command_list[1:] if isinstance(a, str)]
295
+ args: list[str] | None = filtered_args if filtered_args else None
270
296
  environment = config.get("environment")
271
297
  env = environment if isinstance(environment, dict) else None
272
298
  server = MCPServerConfig(
@@ -303,6 +329,69 @@ def _parse_opencode_mcp_server(
303
329
  return None
304
330
 
305
331
 
332
+ def _parse_codex_mcp_server(
333
+ name: str, config: dict[str, Any]
334
+ ) -> MCPServerConfig | None:
335
+ """Parse a Codex MCP server entry from config.toml.
336
+
337
+ Codex TOML format (docs: developers.openai.com/codex/mcp):
338
+ - enabled: bool (defaults to true)
339
+ - command + args + env for stdio
340
+ - url + http_headers / env_http_headers / bearer_token_env_var for streamable-http
341
+ """
342
+ if config.get("enabled") is False:
343
+ return None
344
+
345
+ if "url" in config:
346
+ url = config["url"]
347
+ if not isinstance(url, str) or not url:
348
+ return None
349
+ headers: dict[str, str] = {}
350
+ if isinstance(config.get("http_headers"), dict):
351
+ headers.update(config["http_headers"])
352
+ if isinstance(config.get("env_http_headers"), dict):
353
+ for header_name, env_var in config["env_http_headers"].items():
354
+ if isinstance(env_var, str):
355
+ headers[header_name] = f"${{{env_var}}}"
356
+ bearer_env = config.get("bearer_token_env_var")
357
+ if isinstance(bearer_env, str) and bearer_env:
358
+ headers["Authorization"] = f"Bearer ${{{bearer_env}}}"
359
+ server = MCPServerConfig(
360
+ name=name,
361
+ type="streamable-http",
362
+ command=None,
363
+ args=None,
364
+ url=url,
365
+ env=None,
366
+ headers=headers or None,
367
+ project_name=config.get("project_name"),
368
+ )
369
+ server.config_hash = compute_config_hash(server)
370
+ return server
371
+
372
+ command = config.get("command")
373
+ if not isinstance(command, str) or not command:
374
+ return None
375
+ args = config.get("args")
376
+ if args is not None and not isinstance(args, list):
377
+ args = None
378
+ env = config.get("env")
379
+ if env is not None and not isinstance(env, dict):
380
+ env = None
381
+ server = MCPServerConfig(
382
+ name=name,
383
+ type="stdio",
384
+ command=command,
385
+ args=args,
386
+ url=None,
387
+ env=env,
388
+ headers=None,
389
+ project_name=config.get("project_name"),
390
+ )
391
+ server.config_hash = compute_config_hash(server)
392
+ return server
393
+
394
+
306
395
  def _parse_server_entry(name: str, config: dict[str, Any]) -> MCPServerConfig:
307
396
  """Parse a single server entry from config file.
308
397
 
@@ -413,14 +502,24 @@ def parse_config_file(
413
502
  config_format = getattr(client_def, "config_format", "json")
414
503
 
415
504
  try:
416
- with open(config_path, encoding="utf-8") as f:
417
- if config_format == "yaml":
505
+ if config_format == "toml":
506
+ with open(config_path, "rb") as fb:
507
+ raw_config = tomllib.load(fb)
508
+ elif config_format == "yaml":
509
+ with open(config_path, encoding="utf-8") as f:
418
510
  raw_config = yaml.safe_load(f)
419
- else:
420
- # Use json5 for JSONC support (handles comments, trailing commas, etc.)
421
- # Many editors (VS Code, Zed, Cursor) use JSONC for config files
511
+ else:
512
+ with open(config_path, encoding="utf-8") as f:
422
513
  content = f.read()
423
- raw_config = json5.loads(content)
514
+ raw_config = json5.loads(content)
515
+ except tomllib.TOMLDecodeError as e:
516
+ logger.warning(
517
+ "Failed to parse config file - invalid TOML",
518
+ client=client_def.name,
519
+ path=str(config_path),
520
+ error=str(e),
521
+ )
522
+ return None
424
523
  except yaml.YAMLError as e:
425
524
  logger.warning(
426
525
  "Failed to parse config file - invalid YAML",
@@ -430,7 +529,6 @@ def parse_config_file(
430
529
  )
431
530
  return None
432
531
  except ValueError as e:
433
- # json5 raises ValueError for parse errors
434
532
  logger.warning(
435
533
  "Failed to parse config file - invalid JSON/JSONC",
436
534
  client=client_def.name,
@@ -484,6 +582,10 @@ def parse_config_file(
484
582
  server = _parse_opencode_mcp_server(name, server_config)
485
583
  if server is not None:
486
584
  servers.append(server)
585
+ elif client_def.name == "codex":
586
+ server = _parse_codex_mcp_server(name, server_config)
587
+ if server is not None:
588
+ servers.append(server)
487
589
  else:
488
590
  server = _parse_server_entry(name, server_config)
489
591
  servers.append(server)
@@ -20,7 +20,7 @@ from runlayer_cli.scan.clients import MCPClientDefinition
20
20
  from runlayer_cli.scan.config_parser import (
21
21
  MCPClientConfig,
22
22
  MCPServerConfig,
23
- parse_plugin_mcp_entries,
23
+ parse_plugin_mcp_file,
24
24
  )
25
25
  from runlayer_cli.scan.plugin_scanner import compute_plugin_identifier
26
26
 
@@ -38,25 +38,6 @@ class DiscoveredPlugin:
38
38
  plugin_identifier: str | None = None
39
39
 
40
40
 
41
- def _parse_plugin_mcp_file(mcp_path: Path, plugin_name: str) -> list[MCPServerConfig]:
42
- """Parse an mcp.json or .mcp.json from a plugin directory."""
43
- try:
44
- raw = json5.loads(mcp_path.read_text(encoding="utf-8"))
45
- except (ValueError, OSError) as e:
46
- logger.warning(
47
- "Failed to parse plugin MCP config",
48
- plugin=plugin_name,
49
- path=str(mcp_path),
50
- error=str(e),
51
- )
52
- return []
53
-
54
- if not isinstance(raw, dict):
55
- return []
56
-
57
- return parse_plugin_mcp_entries(raw, plugin_name)
58
-
59
-
60
41
  def discover_plugins(client_def: MCPClientDefinition) -> list[DiscoveredPlugin]:
61
42
  """Scan plugin cache directories for plugins containing MCP configs.
62
43
 
@@ -91,7 +72,7 @@ def discover_plugins(client_def: MCPClientDefinition) -> list[DiscoveredPlugin]:
91
72
  if not mcp_path.exists():
92
73
  continue
93
74
 
94
- servers = _parse_plugin_mcp_file(mcp_path, plugin_name)
75
+ servers = parse_plugin_mcp_file(mcp_path, plugin_name)
95
76
  if not servers:
96
77
  continue
97
78