android-midscene-automation 0.2.4 → 1.1.1
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.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- 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()
|