android-midscene-automation 0.2.3 → 1.1.0

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 (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,862 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Model Context Protocol (MCP) server command (artemis mcp)."""
16
+
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ import re
21
+ import sys
22
+ import threading
23
+ import tomllib
24
+ from typing import Annotated
25
+
26
+ from mcp_server.base import mcp as agent_mcp
27
+ import mcp_server.tools # noqa: F401
28
+ from mcp_server.utils import env_utils
29
+ from artemis.mcp.adb_server import mcp as adb_mcp
30
+ from artemis.runtime import shutdown_awake_service, start_awake_service
31
+ from third_party.mobile_use.utils.logger import get_logger
32
+ from rich.console import Console
33
+ from rich.syntax import Syntax
34
+ import typer
35
+
36
+ logger = get_logger(__name__)
37
+ console = Console()
38
+
39
+ ARTEMIS_MCP_TOOLS = (
40
+ "mobile_run_task",
41
+ "mobile_manage_task",
42
+ "mobile_get_device_state",
43
+ "mobile_inspect_trace",
44
+ "mobile_diagnose",
45
+ )
46
+
47
+
48
+ def _eager_tools_config() -> dict[str, dict[str, bool]]:
49
+ """Return the eager-loading extension used by clients that support it."""
50
+ return {tool_name: {"eager": True} for tool_name in ARTEMIS_MCP_TOOLS}
51
+
52
+
53
+ def _get_vscode_user_dir() -> Path:
54
+ """Returns the platform-specific VS Code user configuration directory."""
55
+ if sys.platform == "darwin":
56
+ return Path.home() / "Library" / "Application Support" / "Code" / "User"
57
+ elif sys.platform == "win32":
58
+ appdata = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
59
+ return Path(appdata) / "Code" / "User"
60
+ else:
61
+ return Path.home() / ".config" / "Code" / "User"
62
+
63
+
64
+ def _get_config_snippet(client: str, python_exe: str, project_root: str) -> dict:
65
+ """Generates MCP configuration dictionary for the specified client."""
66
+ config_body = {
67
+ "command": python_exe,
68
+ "args": ["-m", "mcp_server"],
69
+ "env": {
70
+ "PYTHONUNBUFFERED": "1",
71
+ "PYTHONPATH": project_root,
72
+ "ARTEMIS_DESKTOP_NOTIFY": "true",
73
+ },
74
+ }
75
+ config_with_cwd = {**config_body, "cwd": project_root}
76
+
77
+ if client == "antigravity":
78
+ return {
79
+ "mcpServers": {
80
+ "artemis": {
81
+ **config_with_cwd,
82
+ # This is the documented way to expose every tool. Avoid
83
+ # proprietary eager fields in the current config so a
84
+ # strict parser can always load the server.
85
+ "disabledTools": [],
86
+ }
87
+ }
88
+ }
89
+ elif client == "jetski":
90
+ return {
91
+ "mcpServers": {
92
+ "artemis": {
93
+ **config_with_cwd,
94
+ "disabledTools": [],
95
+ # Retain the legacy Jetski eager extension only in its
96
+ # legacy config file. Current Antigravity gets the strict,
97
+ # documented configuration above as a safe fallback.
98
+ "tools": _eager_tools_config(),
99
+ }
100
+ }
101
+ }
102
+ elif client == "openclaw":
103
+ return {
104
+ "mcp": {
105
+ "servers": {
106
+ "artemis": {
107
+ **config_with_cwd,
108
+ "enabled": True,
109
+ "connectionTimeoutMs": 120000,
110
+ }
111
+ }
112
+ }
113
+ }
114
+ elif client == "codex":
115
+ return {
116
+ "mcp_servers": {
117
+ "artemis": {
118
+ **config_with_cwd,
119
+ # Codex has no eager/deferred MCP setting. Requiring the
120
+ # server makes it initialize during host startup, while an
121
+ # explicit allow-list guarantees all Artemis tools are
122
+ # registered. The host may still present schemas through
123
+ # tool search to control context size.
124
+ "enabled": True,
125
+ "required": True,
126
+ "startup_timeout_sec": 120,
127
+ "enabled_tools": list(ARTEMIS_MCP_TOOLS),
128
+ }
129
+ }
130
+ }
131
+ elif client == "vscode":
132
+ return {"servers": {"artemis": {**config_with_cwd, "type": "stdio"}}}
133
+ elif client in ("claude", "claude_code", "claude_desktop"):
134
+ return {"mcpServers": {"artemis": {**config_body, "type": "stdio"}}}
135
+ elif client == "cline":
136
+ return {
137
+ "mcpServers": {
138
+ "artemis": {
139
+ **config_body,
140
+ "disabled": False,
141
+ "autoApprove": [],
142
+ "timeout": 120,
143
+ }
144
+ }
145
+ }
146
+ elif client in ("roo", "roo_code"):
147
+ return {
148
+ "mcpServers": {
149
+ "artemis": {
150
+ **config_body,
151
+ "disabled": False,
152
+ "alwaysAllow": [],
153
+ "timeout": 120,
154
+ }
155
+ }
156
+ }
157
+ else: # cursor, windsurf, generic
158
+ return {"mcpServers": {"artemis": config_body}}
159
+
160
+
161
+ def _parse_json_lenient(text: str) -> dict | None:
162
+ """Parses JSON or JSONC (JSON with comments / trailing commas) safely.
163
+
164
+ Returns ``None`` when the text cannot be parsed as a JSON object, so callers
165
+ can tell a genuinely empty ``{}`` file apart from a corrupt one.
166
+ """
167
+ try:
168
+ data = json.loads(text)
169
+ return data if isinstance(data, dict) else None
170
+ except ValueError:
171
+ # Not strict JSON; fall through to the lenient JSONC cleanup below.
172
+ pass
173
+ # Remove // single-line comments
174
+ cleaned = re.sub(r"//.*", "", text)
175
+ # Remove /* ... */ multi-line comments
176
+ cleaned = re.sub(r"/\*.*?\*/", "", cleaned, flags=re.DOTALL)
177
+ # Remove trailing commas before } or ]
178
+ cleaned = re.sub(r",\s*([}\]])", r"\1", cleaned)
179
+ try:
180
+ data = json.loads(cleaned)
181
+ return data if isinstance(data, dict) else None
182
+ except Exception:
183
+ return None
184
+
185
+
186
+ def _merge_json_file(
187
+ file_path: Path,
188
+ server_name: str,
189
+ server_config: dict,
190
+ key_name: str | tuple[str, ...] = "mcpServers",
191
+ remove_paths: tuple[tuple[str, ...], ...] = (),
192
+ strict_parse: bool = False,
193
+ ) -> bool:
194
+ """Merges server configuration into a target JSON file without overwriting existing servers.
195
+
196
+ With ``strict_parse=True``, an existing non-empty file that cannot be parsed
197
+ aborts the merge instead of being replaced. Use this for files that hold
198
+ state beyond MCP config (e.g. ~/.claude.json), where a degraded rewrite
199
+ would destroy unrelated data.
200
+ """
201
+ try:
202
+ file_path.parent.mkdir(parents=True, exist_ok=True)
203
+ data: dict = {}
204
+ if file_path.exists() and file_path.stat().st_size > 0:
205
+ raw_text = file_path.read_text(encoding="utf-8")
206
+ parsed = _parse_json_lenient(raw_text)
207
+ data = parsed or {}
208
+ if parsed is None and raw_text.strip():
209
+ if strict_parse:
210
+ logger.warning(
211
+ f"Existing JSON in {file_path} could not be parsed; refusing to rewrite it. "
212
+ "Fix or remove the file, then re-run the install."
213
+ )
214
+ return False
215
+ # Backup unparseable existing file to prevent accidental loss
216
+ backup_path = file_path.with_suffix(file_path.suffix + ".bak")
217
+ try:
218
+ backup_path.write_text(raw_text, encoding="utf-8")
219
+ logger.warning(
220
+ f"Existing JSON in {file_path} could not be parsed; backup created at {backup_path}"
221
+ )
222
+ except OSError as exc:
223
+ logger.warning(
224
+ f"Existing JSON in {file_path} could not be parsed AND the backup to"
225
+ f" {backup_path} failed ({exc}); refusing to rewrite it."
226
+ " Fix or remove the file, then re-run the install."
227
+ )
228
+ return False
229
+
230
+ for remove_path in remove_paths:
231
+ parent = data
232
+ for part in remove_path[:-1]:
233
+ child = parent.get(part)
234
+ if not isinstance(child, dict):
235
+ parent = {}
236
+ break
237
+ parent = child
238
+ if parent:
239
+ parent.pop(remove_path[-1], None)
240
+
241
+ key_path = (key_name,) if isinstance(key_name, str) else key_name
242
+ target = data
243
+ for part in key_path:
244
+ if not isinstance(target.get(part), dict):
245
+ target[part] = {}
246
+ target = target[part]
247
+
248
+ target[server_name] = server_config
249
+ file_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
250
+ return True
251
+ except Exception as e:
252
+ logger.warning(f"Could not update MCP config file {file_path}: {e}")
253
+ return False
254
+
255
+
256
+ def _codex_toml_block(server_config: dict) -> str:
257
+ """Renders a managed Codex MCP server block using TOML-compatible JSON strings."""
258
+ command = json.dumps(str(server_config["command"]), ensure_ascii=False)
259
+ args = json.dumps(server_config.get("args", []), ensure_ascii=False)
260
+ cwd = json.dumps(str(server_config["cwd"]), ensure_ascii=False)
261
+ env = server_config.get("env", {})
262
+ lines = [
263
+ "# BEGIN ARTEMIS MCP CONFIG",
264
+ "[mcp_servers.artemis]",
265
+ f"command = {command}",
266
+ f"args = {args}",
267
+ f"cwd = {cwd}",
268
+ ]
269
+ for key in ("enabled", "required"):
270
+ if key in server_config:
271
+ lines.append(f"{key} = {'true' if server_config[key] else 'false'}")
272
+ if "startup_timeout_sec" in server_config:
273
+ lines.append(f"startup_timeout_sec = {int(server_config['startup_timeout_sec'])}")
274
+ if "enabled_tools" in server_config:
275
+ lines.append(
276
+ f"enabled_tools = {json.dumps(server_config['enabled_tools'], ensure_ascii=False)}"
277
+ )
278
+ if env:
279
+ lines.extend(["", "[mcp_servers.artemis.env]"])
280
+ for key, value in env.items():
281
+ lines.append(f"{key} = {json.dumps(str(value), ensure_ascii=False)}")
282
+ lines.append("# END ARTEMIS MCP CONFIG")
283
+ return "\n".join(lines) + "\n"
284
+
285
+
286
+ def _merge_codex_toml(file_path: Path, server_config: dict) -> bool:
287
+ """Adds or updates Artemis in Codex config.toml while preserving unrelated settings."""
288
+ try:
289
+ file_path.parent.mkdir(parents=True, exist_ok=True)
290
+ existing = file_path.read_text(encoding="utf-8") if file_path.exists() else ""
291
+ managed_block = _codex_toml_block(server_config)
292
+ begin_marker = "# BEGIN ARTEMIS MCP CONFIG"
293
+ end_marker = "# END ARTEMIS MCP CONFIG"
294
+
295
+ if begin_marker in existing and end_marker in existing:
296
+ managed_pattern = re.compile(
297
+ rf"(?ms)^{re.escape(begin_marker)}\n.*?^{re.escape(end_marker)}\n?"
298
+ )
299
+ updated = managed_pattern.sub(managed_block, existing, count=1)
300
+ else:
301
+ # Remove an older, unmanaged Artemis table before adding the managed block.
302
+ kept_lines: list[str] = []
303
+ skipping_artemis = False
304
+ header_pattern = re.compile(r"^\s*\[{1,2}\s*(.+?)\s*\]{1,2}\s*(?:#.*)?$")
305
+ for line in existing.splitlines(keepends=True):
306
+ header_match = header_pattern.match(line.rstrip("\r\n"))
307
+ if header_match:
308
+ normalized = re.sub(r"[\s\"']", "", header_match.group(1)).lower()
309
+ skipping_artemis = normalized == "mcp_servers.artemis" or normalized.startswith(
310
+ "mcp_servers.artemis."
311
+ )
312
+ if not skipping_artemis:
313
+ kept_lines.append(line)
314
+ preserved = "".join(kept_lines).rstrip()
315
+ updated = f"{preserved}\n\n{managed_block}" if preserved else managed_block
316
+
317
+ # Never replace a user's config with invalid TOML.
318
+ tomllib.loads(updated)
319
+ if updated != existing:
320
+ file_path.write_text(updated, encoding="utf-8")
321
+ return True
322
+ except Exception as e:
323
+ if file_path.exists():
324
+ try:
325
+ backup_path = file_path.with_suffix(file_path.suffix + ".bak")
326
+ backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8")
327
+ logger.warning(
328
+ f"Could not update {file_path}; backup created at {backup_path}: {e}"
329
+ )
330
+ except (OSError, UnicodeDecodeError) as backup_exc:
331
+ logger.warning(
332
+ f"Could not update {file_path} ({e}); backup also failed: {backup_exc}"
333
+ )
334
+ else:
335
+ logger.warning(f"Could not create Codex MCP config file {file_path}: {e}")
336
+ return False
337
+
338
+
339
+ def _remove_rules_block(file_path: Path) -> bool:
340
+ """Removes a previously injected managed ARTEMIS block from a rules file.
341
+
342
+ Used to migrate clients that load both their main rules file and a
343
+ standalone rule file (e.g. Claude Code loads ~/.claude/CLAUDE.md AND
344
+ ~/.claude/rules/*.md), where keeping the injected block would duplicate
345
+ the rules in the model's context. If the file contains nothing but the
346
+ managed block, the file is deleted entirely.
347
+ """
348
+ try:
349
+ if not file_path.exists():
350
+ return True
351
+ begin_marker = "<!-- BEGIN ARTEMIS MOBILE TESTING RULES -->"
352
+ end_marker = "<!-- END ARTEMIS MOBILE TESTING RULES -->"
353
+ existing = file_path.read_text(encoding="utf-8")
354
+ if begin_marker not in existing or end_marker not in existing:
355
+ return True
356
+ prefix = existing.split(begin_marker)[0]
357
+ suffix = existing.split(end_marker)[1]
358
+ remainder = f"{prefix.rstrip()}\n\n{suffix.lstrip()}".strip()
359
+ if remainder:
360
+ file_path.write_text(remainder + "\n", encoding="utf-8")
361
+ else:
362
+ file_path.unlink()
363
+ return True
364
+ except Exception as e:
365
+ logger.warning(f"Could not remove rules block from {file_path}: {e}")
366
+ return False
367
+
368
+
369
+ def _inject_rules_block(file_path: Path, content: str) -> bool:
370
+ """Injects or updates a managed ARTEMIS block in a global rules file without overwriting user content."""
371
+ try:
372
+ file_path.parent.mkdir(parents=True, exist_ok=True)
373
+ begin_marker = "<!-- BEGIN ARTEMIS MOBILE TESTING RULES -->"
374
+ end_marker = "<!-- END ARTEMIS MOBILE TESTING RULES -->"
375
+ new_block = f"{begin_marker}\n{content.strip()}\n{end_marker}\n"
376
+
377
+ existing = ""
378
+ if file_path.exists():
379
+ try:
380
+ existing = file_path.read_text(encoding="utf-8")
381
+ except Exception:
382
+ existing = ""
383
+
384
+ if begin_marker in existing and end_marker in existing:
385
+ prefix = existing.split(begin_marker)[0]
386
+ suffix = existing.split(end_marker)[1]
387
+ updated = f"{prefix.rstrip()}\n\n{new_block}\n{suffix.lstrip()}".strip() + "\n"
388
+ else:
389
+ updated = f"{existing.rstrip()}\n\n{new_block}".strip() + "\n"
390
+
391
+ if existing == updated:
392
+ return True
393
+
394
+ file_path.write_text(updated, encoding="utf-8")
395
+ return True
396
+ except Exception as e:
397
+ logger.warning(f"Could not inject rules block into {file_path}: {e}")
398
+ return False
399
+
400
+
401
+ def _write_rule_file(file_path: Path, content: str) -> bool:
402
+ """Writes or overwrites a standalone rule file."""
403
+ try:
404
+ file_path.parent.mkdir(parents=True, exist_ok=True)
405
+ new_text = content.strip() + "\n"
406
+ if file_path.exists():
407
+ try:
408
+ if file_path.read_text(encoding="utf-8") == new_text:
409
+ return True
410
+ except (OSError, UnicodeDecodeError):
411
+ # Unreadable existing file: skip the no-op check and rewrite it.
412
+ pass
413
+ file_path.write_text(new_text, encoding="utf-8")
414
+ return True
415
+ except Exception as e:
416
+ logger.warning(f"Could not write rule file {file_path}: {e}")
417
+ return False
418
+
419
+
420
+ def _write_cursor_mdc(file_path: Path, content: str) -> bool:
421
+ """Writes a Cursor .mdc rule file with required YAML frontmatter."""
422
+ try:
423
+ file_path.parent.mkdir(parents=True, exist_ok=True)
424
+ mdc_header = (
425
+ "---\n"
426
+ "description: Artemis Mobile Testing Mindset & Rules\n"
427
+ "globs: **/*\n"
428
+ "alwaysApply: true\n"
429
+ "---\n\n"
430
+ )
431
+ new_text = mdc_header + content.strip() + "\n"
432
+ if file_path.exists():
433
+ try:
434
+ if file_path.read_text(encoding="utf-8") == new_text:
435
+ return True
436
+ except (OSError, UnicodeDecodeError):
437
+ # Unreadable existing file: skip the no-op check and rewrite it.
438
+ pass
439
+ file_path.write_text(new_text, encoding="utf-8")
440
+ return True
441
+ except Exception as e:
442
+ logger.warning(f"Could not write Cursor rule file {file_path}: {e}")
443
+ return False
444
+
445
+
446
+ def install_rules(client: str, project_root: str) -> list[str]:
447
+ """Auto-installs ARTEMIS testing rules globally into user IDE rule directories across any OS."""
448
+ installed_paths: list[str] = []
449
+ rules_src = Path(project_root) / "mcp_server" / "rules.md"
450
+ if not rules_src.exists():
451
+ alt_src = Path(__file__).resolve().parents[4] / "mcp_server" / "rules.md"
452
+ if alt_src.exists():
453
+ rules_src = alt_src
454
+ else:
455
+ logger.warning(f"Could not locate rules.md at {rules_src}")
456
+ return []
457
+
458
+ try:
459
+ raw_rules = rules_src.read_text(encoding="utf-8").strip()
460
+ except Exception as e:
461
+ logger.warning(f"Could not read rules file {rules_src}: {e}")
462
+ return []
463
+
464
+ targets = (
465
+ [
466
+ "antigravity",
467
+ "cursor",
468
+ "claude",
469
+ "windsurf",
470
+ "vscode",
471
+ "cline",
472
+ "roo",
473
+ "openclaw",
474
+ "codex",
475
+ ]
476
+ if client == "all"
477
+ else [client]
478
+ )
479
+
480
+ for target in targets:
481
+ if target in ("antigravity", "jetski"):
482
+ gemini_md = Path.home() / ".gemini" / "GEMINI.md"
483
+ global_rule = Path.home() / ".gemini" / "rules" / "artemis.md"
484
+ if _inject_rules_block(gemini_md, raw_rules):
485
+ installed_paths.append(str(gemini_md))
486
+ if _write_rule_file(global_rule, raw_rules):
487
+ installed_paths.append(str(global_rule))
488
+ elif target == "cursor":
489
+ cursor_rules_file = Path.home() / ".cursorrules"
490
+ global_mdc = Path.home() / ".cursor" / "rules" / "artemis.mdc"
491
+ if _inject_rules_block(cursor_rules_file, raw_rules):
492
+ installed_paths.append(str(cursor_rules_file))
493
+ if _write_cursor_mdc(global_mdc, raw_rules):
494
+ installed_paths.append(str(global_mdc))
495
+ elif target in ("claude", "claude_code"):
496
+ # Claude Code loads BOTH ~/.claude/CLAUDE.md and ~/.claude/rules/*.md
497
+ # into context every session, so install the rules in exactly one
498
+ # place (the standalone rule file) and migrate away any block a
499
+ # previous version injected into CLAUDE.md.
500
+ claude_md = Path.home() / ".claude" / "CLAUDE.md"
501
+ global_rule = Path.home() / ".claude" / "rules" / "artemis.md"
502
+ _remove_rules_block(claude_md)
503
+ if _write_rule_file(global_rule, raw_rules):
504
+ installed_paths.append(str(global_rule))
505
+ elif target == "claude_desktop":
506
+ # Claude Desktop only reads ~/.claude/CLAUDE.md (not rules/*.md), so
507
+ # the rules block stays injected there for this target.
508
+ claude_md = Path.home() / ".claude" / "CLAUDE.md"
509
+ if _inject_rules_block(claude_md, raw_rules):
510
+ installed_paths.append(str(claude_md))
511
+ elif target == "windsurf":
512
+ global_rule = Path.home() / ".codeium" / "windsurf" / "rules" / "artemis.md"
513
+ global_mem = Path.home() / ".codeium" / "windsurf" / "memories" / "global_rules.md"
514
+ if _inject_rules_block(global_mem, raw_rules):
515
+ installed_paths.append(str(global_mem))
516
+ if _write_rule_file(global_rule, raw_rules):
517
+ installed_paths.append(str(global_rule))
518
+ elif target == "vscode":
519
+ global_rule = Path.home() / ".vscode" / "rules" / "artemis.md"
520
+ user_rule = _get_vscode_user_dir() / "rules" / "artemis.md"
521
+ if _write_rule_file(global_rule, raw_rules):
522
+ installed_paths.append(str(global_rule))
523
+ if _write_rule_file(user_rule, raw_rules):
524
+ installed_paths.append(str(user_rule))
525
+ elif target == "cline":
526
+ clinerules = Path.home() / ".clinerules"
527
+ global_rule = Path.home() / ".cline" / "rules" / "artemis.md"
528
+ if _inject_rules_block(clinerules, raw_rules):
529
+ installed_paths.append(str(clinerules))
530
+ if _write_rule_file(global_rule, raw_rules):
531
+ installed_paths.append(str(global_rule))
532
+ elif target in ("roo", "roo_code"):
533
+ roorules = Path.home() / ".roorules"
534
+ global_rule = Path.home() / ".roo" / "rules" / "artemis.md"
535
+ if _inject_rules_block(roorules, raw_rules):
536
+ installed_paths.append(str(roorules))
537
+ if _write_rule_file(global_rule, raw_rules):
538
+ installed_paths.append(str(global_rule))
539
+ elif target == "openclaw":
540
+ openclaw_md = Path.home() / ".openclaw" / "OPENCLAW.md"
541
+ global_rule = Path.home() / ".openclaw" / "rules" / "artemis.md"
542
+ if _inject_rules_block(openclaw_md, raw_rules):
543
+ installed_paths.append(str(openclaw_md))
544
+ if _write_rule_file(global_rule, raw_rules):
545
+ installed_paths.append(str(global_rule))
546
+ elif target == "codex":
547
+ codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
548
+ override_file = codex_home / "AGENTS.override.md"
549
+ agents_file = (
550
+ override_file
551
+ if override_file.exists() and override_file.read_text(encoding="utf-8").strip()
552
+ else codex_home / "AGENTS.md"
553
+ )
554
+ if _inject_rules_block(agents_file, raw_rules):
555
+ installed_paths.append(str(agents_file))
556
+
557
+ return installed_paths
558
+
559
+
560
+ def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[str]:
561
+ """Auto-installs/merges ARTEMIS MCP configuration and testing rules into IDE config files across any OS."""
562
+ installed_paths: list[str] = []
563
+ targets = (
564
+ [
565
+ "antigravity",
566
+ "cursor",
567
+ "claude",
568
+ "windsurf",
569
+ "vscode",
570
+ "cline",
571
+ "roo",
572
+ "openclaw",
573
+ "codex",
574
+ ]
575
+ if client == "all"
576
+ else [client]
577
+ )
578
+
579
+ for target in targets:
580
+ snippet = _get_config_snippet(target, python_exe, project_root)
581
+ if target in ("antigravity", "jetski"):
582
+ current_server_cfg = _get_config_snippet("antigravity", python_exe, project_root)[
583
+ "mcpServers"
584
+ ]["artemis"]
585
+ legacy_server_cfg = _get_config_snippet("jetski", python_exe, project_root)[
586
+ "mcpServers"
587
+ ]["artemis"]
588
+ jetski_path = Path.home() / ".gemini" / "jetski" / "mcp_config.json"
589
+ antigravity_legacy_path = Path.home() / ".gemini" / "antigravity" / "mcp_config.json"
590
+ config_path = Path.home() / ".gemini" / "config" / "mcp_config.json"
591
+ if _merge_json_file(jetski_path, "artemis", legacy_server_cfg):
592
+ installed_paths.append(str(jetski_path))
593
+ if _merge_json_file(antigravity_legacy_path, "artemis", current_server_cfg):
594
+ installed_paths.append(str(antigravity_legacy_path))
595
+ if _merge_json_file(config_path, "artemis", current_server_cfg):
596
+ installed_paths.append(str(config_path))
597
+ elif target in ("claude", "claude_code", "claude_desktop"):
598
+ server_cfg = snippet["mcpServers"]["artemis"]
599
+ if sys.platform == "darwin":
600
+ claude_path = (
601
+ Path.home()
602
+ / "Library"
603
+ / "Application Support"
604
+ / "Claude"
605
+ / "claude_desktop_config.json"
606
+ )
607
+ elif sys.platform == "win32":
608
+ appdata = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
609
+ claude_path = Path(appdata) / "Claude" / "claude_desktop_config.json"
610
+ else:
611
+ claude_path = Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
612
+ if _merge_json_file(claude_path, "artemis", server_cfg):
613
+ installed_paths.append(str(claude_path))
614
+
615
+ # Also install to Claude Code CLI global config (~/.claude.json).
616
+ # This file holds Claude Code state well beyond MCP config, so a
617
+ # parse failure must abort the merge rather than rewrite the file.
618
+ claude_code_path = Path.home() / ".claude.json"
619
+ if _merge_json_file(claude_code_path, "artemis", server_cfg, strict_parse=True):
620
+ installed_paths.append(str(claude_code_path))
621
+ elif target == "cursor":
622
+ server_cfg = snippet["mcpServers"]["artemis"]
623
+ cursor_path = Path.home() / ".cursor" / "mcp.json"
624
+ if _merge_json_file(cursor_path, "artemis", server_cfg):
625
+ installed_paths.append(str(cursor_path))
626
+ elif target == "windsurf":
627
+ server_cfg = snippet["mcpServers"]["artemis"]
628
+ windsurf_path = Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
629
+ if _merge_json_file(windsurf_path, "artemis", server_cfg):
630
+ installed_paths.append(str(windsurf_path))
631
+ elif target == "vscode":
632
+ server_cfg = snippet["servers"]["artemis"]
633
+ vscode_path = _get_vscode_user_dir() / "mcp.json"
634
+ copilot_path = Path.home() / ".copilot" / "mcp-config.json"
635
+ if _merge_json_file(vscode_path, "artemis", server_cfg, key_name="servers"):
636
+ installed_paths.append(str(vscode_path))
637
+ if _merge_json_file(copilot_path, "artemis", server_cfg, key_name="servers"):
638
+ installed_paths.append(str(copilot_path))
639
+ elif target == "cline":
640
+ server_cfg = snippet["mcpServers"]["artemis"]
641
+ cline_paths = (
642
+ Path.home() / ".cline" / "data" / "settings" / "cline_mcp_settings.json",
643
+ Path.home() / ".cline" / "mcp.json",
644
+ _get_vscode_user_dir()
645
+ / "globalStorage"
646
+ / "saoudrizwan.claude-dev"
647
+ / "settings"
648
+ / "cline_mcp_settings.json",
649
+ )
650
+ for cline_path in cline_paths:
651
+ if _merge_json_file(cline_path, "artemis", server_cfg):
652
+ installed_paths.append(str(cline_path))
653
+ elif target in ("roo", "roo_code"):
654
+ server_cfg = snippet["mcpServers"]["artemis"]
655
+ roo_settings_dir = (
656
+ _get_vscode_user_dir() / "globalStorage" / "rooveterinaryinc.roo-cline" / "settings"
657
+ )
658
+ roo_paths = (
659
+ roo_settings_dir / "mcp_settings.json",
660
+ roo_settings_dir / "cline_mcp_settings.json",
661
+ Path(project_root) / ".roo" / "mcp.json",
662
+ )
663
+ for roo_path in roo_paths:
664
+ if _merge_json_file(roo_path, "artemis", server_cfg):
665
+ installed_paths.append(str(roo_path))
666
+ elif target == "openclaw":
667
+ server_cfg = snippet["mcp"]["servers"]["artemis"]
668
+ openclaw_path = Path.home() / ".openclaw" / "openclaw.json"
669
+ if _merge_json_file(
670
+ openclaw_path,
671
+ "artemis",
672
+ server_cfg,
673
+ key_name=("mcp", "servers"),
674
+ remove_paths=(("plugins", "artemis_mcp"),),
675
+ ):
676
+ installed_paths.append(str(openclaw_path))
677
+ elif target == "codex":
678
+ server_cfg = snippet["mcp_servers"]["artemis"]
679
+ codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
680
+ codex_path = codex_home / "config.toml"
681
+ if _merge_codex_toml(codex_path, server_cfg):
682
+ installed_paths.append(str(codex_path))
683
+
684
+ rules_paths = install_rules(client, project_root)
685
+ installed_paths.extend(rules_paths)
686
+
687
+ unique_paths: list[str] = []
688
+ for p in installed_paths:
689
+ if p not in unique_paths:
690
+ unique_paths.append(p)
691
+ return unique_paths
692
+
693
+
694
+ def mcp_command(
695
+ server_type: Annotated[
696
+ str,
697
+ typer.Option(
698
+ "--type",
699
+ "-t",
700
+ help="Type of MCP server to start: 'agent' (default, universal IDE mobile agent), 'adb' (raw adb), 'xml' (xml fuzzy search).",
701
+ ),
702
+ ] = "agent",
703
+ transport: Annotated[
704
+ str,
705
+ typer.Option(
706
+ "--transport",
707
+ help="MCP transport protocol ('stdio' or 'sse').",
708
+ ),
709
+ ] = "stdio",
710
+ host: Annotated[
711
+ str,
712
+ typer.Option(
713
+ "--host",
714
+ help="Host address when running with SSE transport.",
715
+ ),
716
+ ] = "127.0.0.1",
717
+ port: Annotated[
718
+ int,
719
+ typer.Option(
720
+ "--port",
721
+ "-p",
722
+ help="Port number when running with SSE transport.",
723
+ ),
724
+ ] = 8001,
725
+ install_config: Annotated[
726
+ str | None,
727
+ typer.Option(
728
+ "--install",
729
+ "-i",
730
+ help="Auto-install and merge ARTEMIS MCP configuration and testing rules into 'antigravity', 'claude', 'cursor', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', 'codex', or 'all'.",
731
+ ),
732
+ ] = None,
733
+ generate_config: Annotated[
734
+ str | None,
735
+ typer.Option(
736
+ "--generate-config",
737
+ "-g",
738
+ help="Output ready-to-use MCP configuration for 'antigravity', 'cursor', 'claude', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', 'codex', or 'all'.",
739
+ ),
740
+ ] = None,
741
+ ) -> None:
742
+ """Launch or configure the ARTEMIS Model Context Protocol (MCP) server."""
743
+ project_root = env_utils.get_project_root()
744
+ python_exe = env_utils.resolve_python_executable(project_root)
745
+
746
+ if install_config:
747
+ client = install_config.lower()
748
+ if client not in (
749
+ "antigravity",
750
+ "jetski",
751
+ "claude",
752
+ "claude_code",
753
+ "claude_desktop",
754
+ "cursor",
755
+ "windsurf",
756
+ "vscode",
757
+ "cline",
758
+ "roo",
759
+ "roo_code",
760
+ "openclaw",
761
+ "codex",
762
+ "all",
763
+ ):
764
+ console.print(
765
+ f"[bold red]Unsupported install target: '{install_config}'. Use 'antigravity', 'claude', 'cursor', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', 'codex', or 'all'.[/bold red]"
766
+ )
767
+ raise typer.Exit(1)
768
+ installed_paths = install_mcp_config(client, python_exe, project_root)
769
+ console.print(
770
+ "[bold green]✔ Successfully installed ARTEMIS MCP server configuration & rules to:[/bold green]"
771
+ )
772
+ for path in installed_paths:
773
+ console.print(f" • [cyan]{path}[/cyan]")
774
+ console.print(
775
+ "\n[dim]Please restart or reload your IDE window to activate the Artemis MCP tools.[/dim]"
776
+ )
777
+ raise typer.Exit(0)
778
+
779
+ if generate_config:
780
+ client = generate_config.lower()
781
+ if client == "all":
782
+ all_configs = {
783
+ "antigravity (~/.gemini/jetski/mcp_config.json)": _get_config_snippet(
784
+ "antigravity", python_exe, project_root
785
+ ),
786
+ "cursor (.cursor/mcp.json)": _get_config_snippet(
787
+ "cursor", python_exe, project_root
788
+ ),
789
+ "claude (claude_desktop_config.json & ~/.claude.json)": _get_config_snippet(
790
+ "claude", python_exe, project_root
791
+ ),
792
+ "windsurf (~/.codeium/windsurf/mcp_config.json)": _get_config_snippet(
793
+ "windsurf", python_exe, project_root
794
+ ),
795
+ "vscode (mcp.json)": _get_config_snippet("vscode", python_exe, project_root),
796
+ "cline (cline_mcp_settings.json)": _get_config_snippet(
797
+ "cline", python_exe, project_root
798
+ ),
799
+ "roo (cline_mcp_settings.json)": _get_config_snippet(
800
+ "roo", python_exe, project_root
801
+ ),
802
+ "openclaw (openclaw.json)": _get_config_snippet(
803
+ "openclaw", python_exe, project_root
804
+ ),
805
+ "codex (~/.codex/config.toml)": _get_config_snippet(
806
+ "codex", python_exe, project_root
807
+ ),
808
+ }
809
+ json_str = json.dumps(all_configs, indent=2)
810
+ syntax_language = "json"
811
+ elif client == "codex":
812
+ server_cfg = _get_config_snippet("codex", python_exe, project_root)["mcp_servers"][
813
+ "artemis"
814
+ ]
815
+ json_str = _codex_toml_block(server_cfg)
816
+ syntax_language = "toml"
817
+ else:
818
+ snippet = _get_config_snippet(client, python_exe, project_root)
819
+ json_str = json.dumps(snippet, indent=2)
820
+ syntax_language = "json"
821
+
822
+ syntax = Syntax(json_str, syntax_language, theme="monokai", line_numbers=False)
823
+ console.print(f"[bold cyan]MCP Configuration for {client.upper()}:[/bold cyan]")
824
+ console.print(syntax)
825
+ raise typer.Exit(0)
826
+
827
+ threading.Thread(target=start_awake_service, daemon=True, name="artemis-awake-init").start()
828
+ try:
829
+ st = server_type.lower()
830
+ if st in ("agent", "mobile", "artemis", "default"):
831
+ logger.info(f"Starting Artemis Mobile Agent MCP Server over {transport}...")
832
+ if transport.lower() == "sse":
833
+ agent_mcp.run(transport="sse", host=host, port=port)
834
+ else:
835
+ agent_mcp.run(transport="stdio")
836
+ elif st == "adb":
837
+ if transport.lower() == "sse":
838
+ logger.info(f"Starting Artemis ADB MCP Server over {transport}...")
839
+ adb_mcp.run(transport="sse", host=host, port=port)
840
+ else:
841
+ # stdio carries the MCP JSON-RPC stream: redirect logging and detach
842
+ # from the parent DataEngine before any output can corrupt it.
843
+ from artemis.mcp.adb_server import configure_stdio_mode
844
+
845
+ configure_stdio_mode()
846
+ logger.info("Starting Artemis ADB MCP Server over stdio...")
847
+ adb_mcp.run(transport="stdio")
848
+ elif st == "xml":
849
+ from artemis.mcp.xml_search_server import mcp as xml_mcp
850
+
851
+ logger.info(f"Starting Artemis XML Fuzzy Search MCP Server over {transport}...")
852
+ if transport.lower() == "sse":
853
+ xml_mcp.run(transport="sse", host=host, port=port)
854
+ else:
855
+ xml_mcp.run(transport="stdio")
856
+ else:
857
+ logger.error(
858
+ f"Unsupported MCP server type: {server_type}. Use 'agent', 'adb', or 'xml'."
859
+ )
860
+ raise typer.Exit(1)
861
+ finally:
862
+ shutdown_awake_service()