claude-mpm 5.4.41__py3-none-any.whl → 5.6.72__py3-none-any.whl

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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (490) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +66 -241
  3. claude_mpm/agents/CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md +413 -0
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +109 -1925
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +161 -298
  6. claude_mpm/agents/WORKFLOW.md +2 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  8. claude_mpm/auth/__init__.py +35 -0
  9. claude_mpm/auth/callback_server.py +328 -0
  10. claude_mpm/auth/models.py +104 -0
  11. claude_mpm/auth/oauth_manager.py +266 -0
  12. claude_mpm/auth/providers/__init__.py +12 -0
  13. claude_mpm/auth/providers/base.py +165 -0
  14. claude_mpm/auth/providers/google.py +261 -0
  15. claude_mpm/auth/token_storage.py +252 -0
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/agents.py +2 -4
  18. claude_mpm/cli/commands/agents_reconcile.py +197 -0
  19. claude_mpm/cli/commands/autotodos.py +566 -0
  20. claude_mpm/cli/commands/commander.py +216 -0
  21. claude_mpm/cli/commands/configure.py +620 -21
  22. claude_mpm/cli/commands/configure_agent_display.py +3 -1
  23. claude_mpm/cli/commands/hook_errors.py +60 -60
  24. claude_mpm/cli/commands/mcp.py +29 -17
  25. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  26. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  27. claude_mpm/cli/commands/monitor.py +2 -2
  28. claude_mpm/cli/commands/mpm_init/core.py +15 -8
  29. claude_mpm/cli/commands/oauth.py +481 -0
  30. claude_mpm/cli/commands/profile.py +9 -10
  31. claude_mpm/cli/commands/run.py +35 -3
  32. claude_mpm/cli/commands/skill_source.py +51 -2
  33. claude_mpm/cli/commands/skills.py +182 -32
  34. claude_mpm/cli/executor.py +129 -16
  35. claude_mpm/cli/helpers.py +1 -1
  36. claude_mpm/cli/interactive/__init__.py +10 -0
  37. claude_mpm/cli/interactive/agent_wizard.py +30 -50
  38. claude_mpm/cli/interactive/questionary_styles.py +65 -0
  39. claude_mpm/cli/interactive/skill_selector.py +481 -0
  40. claude_mpm/cli/parsers/base_parser.py +89 -1
  41. claude_mpm/cli/parsers/commander_parser.py +116 -0
  42. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  43. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  44. claude_mpm/cli/parsers/profile_parser.py +0 -1
  45. claude_mpm/cli/parsers/run_parser.py +10 -0
  46. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  47. claude_mpm/cli/parsers/skills_parser.py +2 -3
  48. claude_mpm/cli/startup.py +662 -524
  49. claude_mpm/cli/startup_display.py +76 -7
  50. claude_mpm/cli/startup_logging.py +2 -2
  51. claude_mpm/cli/utils.py +7 -3
  52. claude_mpm/commander/__init__.py +78 -0
  53. claude_mpm/commander/adapters/__init__.py +60 -0
  54. claude_mpm/commander/adapters/auggie.py +260 -0
  55. claude_mpm/commander/adapters/base.py +288 -0
  56. claude_mpm/commander/adapters/claude_code.py +392 -0
  57. claude_mpm/commander/adapters/codex.py +237 -0
  58. claude_mpm/commander/adapters/communication.py +366 -0
  59. claude_mpm/commander/adapters/example_usage.py +310 -0
  60. claude_mpm/commander/adapters/mpm.py +389 -0
  61. claude_mpm/commander/adapters/registry.py +204 -0
  62. claude_mpm/commander/api/__init__.py +16 -0
  63. claude_mpm/commander/api/app.py +121 -0
  64. claude_mpm/commander/api/errors.py +133 -0
  65. claude_mpm/commander/api/routes/__init__.py +8 -0
  66. claude_mpm/commander/api/routes/events.py +184 -0
  67. claude_mpm/commander/api/routes/inbox.py +171 -0
  68. claude_mpm/commander/api/routes/messages.py +148 -0
  69. claude_mpm/commander/api/routes/projects.py +271 -0
  70. claude_mpm/commander/api/routes/sessions.py +226 -0
  71. claude_mpm/commander/api/routes/work.py +296 -0
  72. claude_mpm/commander/api/schemas.py +186 -0
  73. claude_mpm/commander/chat/__init__.py +7 -0
  74. claude_mpm/commander/chat/cli.py +149 -0
  75. claude_mpm/commander/chat/commands.py +122 -0
  76. claude_mpm/commander/chat/repl.py +1821 -0
  77. claude_mpm/commander/config.py +51 -0
  78. claude_mpm/commander/config_loader.py +115 -0
  79. claude_mpm/commander/core/__init__.py +10 -0
  80. claude_mpm/commander/core/block_manager.py +325 -0
  81. claude_mpm/commander/core/response_manager.py +323 -0
  82. claude_mpm/commander/daemon.py +603 -0
  83. claude_mpm/commander/env_loader.py +59 -0
  84. claude_mpm/commander/events/__init__.py +26 -0
  85. claude_mpm/commander/events/manager.py +392 -0
  86. claude_mpm/commander/frameworks/__init__.py +12 -0
  87. claude_mpm/commander/frameworks/base.py +233 -0
  88. claude_mpm/commander/frameworks/claude_code.py +58 -0
  89. claude_mpm/commander/frameworks/mpm.py +57 -0
  90. claude_mpm/commander/git/__init__.py +5 -0
  91. claude_mpm/commander/git/worktree_manager.py +212 -0
  92. claude_mpm/commander/inbox/__init__.py +16 -0
  93. claude_mpm/commander/inbox/dedup.py +128 -0
  94. claude_mpm/commander/inbox/inbox.py +224 -0
  95. claude_mpm/commander/inbox/models.py +70 -0
  96. claude_mpm/commander/instance_manager.py +865 -0
  97. claude_mpm/commander/llm/__init__.py +6 -0
  98. claude_mpm/commander/llm/openrouter_client.py +167 -0
  99. claude_mpm/commander/llm/summarizer.py +70 -0
  100. claude_mpm/commander/memory/__init__.py +45 -0
  101. claude_mpm/commander/memory/compression.py +347 -0
  102. claude_mpm/commander/memory/embeddings.py +230 -0
  103. claude_mpm/commander/memory/entities.py +310 -0
  104. claude_mpm/commander/memory/example_usage.py +290 -0
  105. claude_mpm/commander/memory/integration.py +325 -0
  106. claude_mpm/commander/memory/search.py +381 -0
  107. claude_mpm/commander/memory/store.py +657 -0
  108. claude_mpm/commander/models/__init__.py +18 -0
  109. claude_mpm/commander/models/events.py +127 -0
  110. claude_mpm/commander/models/project.py +162 -0
  111. claude_mpm/commander/models/work.py +214 -0
  112. claude_mpm/commander/parsing/__init__.py +20 -0
  113. claude_mpm/commander/parsing/extractor.py +132 -0
  114. claude_mpm/commander/parsing/output_parser.py +270 -0
  115. claude_mpm/commander/parsing/patterns.py +100 -0
  116. claude_mpm/commander/persistence/__init__.py +11 -0
  117. claude_mpm/commander/persistence/event_store.py +274 -0
  118. claude_mpm/commander/persistence/state_store.py +403 -0
  119. claude_mpm/commander/persistence/work_store.py +164 -0
  120. claude_mpm/commander/polling/__init__.py +13 -0
  121. claude_mpm/commander/polling/event_detector.py +104 -0
  122. claude_mpm/commander/polling/output_buffer.py +49 -0
  123. claude_mpm/commander/polling/output_poller.py +153 -0
  124. claude_mpm/commander/project_session.py +268 -0
  125. claude_mpm/commander/proxy/__init__.py +12 -0
  126. claude_mpm/commander/proxy/formatter.py +89 -0
  127. claude_mpm/commander/proxy/output_handler.py +191 -0
  128. claude_mpm/commander/proxy/relay.py +155 -0
  129. claude_mpm/commander/registry.py +410 -0
  130. claude_mpm/commander/runtime/__init__.py +10 -0
  131. claude_mpm/commander/runtime/executor.py +191 -0
  132. claude_mpm/commander/runtime/monitor.py +346 -0
  133. claude_mpm/commander/session/__init__.py +6 -0
  134. claude_mpm/commander/session/context.py +81 -0
  135. claude_mpm/commander/session/manager.py +59 -0
  136. claude_mpm/commander/tmux_orchestrator.py +362 -0
  137. claude_mpm/commander/web/__init__.py +1 -0
  138. claude_mpm/commander/work/__init__.py +30 -0
  139. claude_mpm/commander/work/executor.py +207 -0
  140. claude_mpm/commander/work/queue.py +405 -0
  141. claude_mpm/commander/workflow/__init__.py +27 -0
  142. claude_mpm/commander/workflow/event_handler.py +241 -0
  143. claude_mpm/commander/workflow/notifier.py +146 -0
  144. claude_mpm/commands/mpm-config.md +8 -0
  145. claude_mpm/commands/mpm-doctor.md +8 -0
  146. claude_mpm/commands/mpm-help.md +8 -0
  147. claude_mpm/commands/mpm-init.md +8 -0
  148. claude_mpm/commands/mpm-monitor.md +8 -0
  149. claude_mpm/commands/mpm-organize.md +8 -0
  150. claude_mpm/commands/mpm-postmortem.md +8 -0
  151. claude_mpm/commands/mpm-session-resume.md +9 -1
  152. claude_mpm/commands/mpm-status.md +8 -0
  153. claude_mpm/commands/mpm-ticket-view.md +8 -0
  154. claude_mpm/commands/mpm-version.md +8 -0
  155. claude_mpm/commands/mpm.md +8 -0
  156. claude_mpm/config/agent_presets.py +8 -7
  157. claude_mpm/config/skill_sources.py +16 -0
  158. claude_mpm/constants.py +6 -0
  159. claude_mpm/core/claude_runner.py +154 -2
  160. claude_mpm/core/config.py +35 -22
  161. claude_mpm/core/config_constants.py +74 -9
  162. claude_mpm/core/constants.py +56 -12
  163. claude_mpm/core/hook_manager.py +53 -4
  164. claude_mpm/core/interactive_session.py +12 -11
  165. claude_mpm/core/logger.py +26 -9
  166. claude_mpm/core/logging_utils.py +39 -13
  167. claude_mpm/core/network_config.py +148 -0
  168. claude_mpm/core/oneshot_session.py +7 -6
  169. claude_mpm/core/optimized_startup.py +3 -1
  170. claude_mpm/core/output_style_manager.py +66 -18
  171. claude_mpm/core/shared/config_loader.py +3 -1
  172. claude_mpm/core/socketio_pool.py +47 -15
  173. claude_mpm/core/unified_config.py +54 -8
  174. claude_mpm/core/unified_paths.py +95 -90
  175. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  176. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  177. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/1WZnGYqX.js +24 -0
  178. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/67pF3qNn.js +1 -0
  179. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/6RxdMKe4.js +1 -0
  180. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/8cZrfX0h.js +60 -0
  181. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/9a6T2nm-.js +7 -0
  182. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B443AUzu.js +1 -0
  183. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B8AwtY2H.js +1 -0
  184. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BF15LAsF.js +1 -0
  185. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  186. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BRcwIQNr.js +4 -0
  187. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uj46x2Wr.js → BSNlmTZj.js} +1 -1
  188. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BV6nKitt.js +43 -0
  189. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BViJ8lZt.js +128 -0
  190. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BcQ-Q0FE.js +1 -0
  191. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bpyvgze_.js +30 -0
  192. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  193. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  194. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C3rbW_a-.js +1 -0
  195. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C8WYN38h.js +1 -0
  196. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C9I8FlXH.js +61 -0
  197. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIQcWgO2.js +36 -0
  198. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIctN7YN.js +7 -0
  199. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CKrS_JZW.js +145 -0
  200. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CR6P9C4A.js +89 -0
  201. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRRR9MD_.js +2 -0
  202. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  203. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CSXtMOf0.js +1 -0
  204. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CT-sbxSk.js +1 -0
  205. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWm6DJsp.js +1 -0
  206. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  207. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CpqQ1Kzn.js +1 -0
  208. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  209. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D2nGpDRe.js +1 -0
  210. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9iCMida.js +267 -0
  211. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9ykgMoY.js +10 -0
  212. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DL2Ldur1.js +1 -0
  213. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DPfltzjH.js +165 -0
  214. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{N4qtv3Hx.js → DR8nis88.js} +2 -2
  215. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUliQN2b.js +1 -0
  216. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  217. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DXlhR01x.js +122 -0
  218. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D_lyTybS.js +1 -0
  219. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DngoTTgh.js +1 -0
  220. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DqkmHtDC.js +220 -0
  221. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DsDh8EYs.js +1 -0
  222. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DypDmXgd.js +139 -0
  223. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  224. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/IPYC-LnN.js +162 -0
  225. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
  226. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JpevfAFt.js +68 -0
  227. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DjhvlsAc.js → NqQ1dWOy.js} +1 -1
  228. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/R8CEIRAd.js +2 -0
  229. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Zxy7qc-l.js +64 -0
  230. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  231. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/qtd3IeO4.js +15 -0
  232. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ulBFON_C.js +65 -0
  233. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/wQVh1CoA.js +10 -0
  234. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.Dr7t0z2J.js +2 -0
  235. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  236. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.CAGBuiOw.js → 0.RgBboRvH.js} +1 -1
  237. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DG-KkbDf.js +1 -0
  238. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  239. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  240. claude_mpm/dashboard/static/svelte-build/index.html +11 -11
  241. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  242. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  243. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  244. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  245. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  246. claude_mpm/experimental/cli_enhancements.py +2 -1
  247. claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
  248. claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
  249. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  250. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  251. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  252. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  253. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  254. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +485 -0
  255. claude_mpm/hooks/claude_hooks/event_handlers.py +466 -136
  256. claude_mpm/hooks/claude_hooks/hook_handler.py +204 -104
  257. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  258. claude_mpm/hooks/claude_hooks/installer.py +291 -59
  259. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  260. claude_mpm/hooks/claude_hooks/response_tracking.py +43 -60
  261. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  262. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  263. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  264. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  265. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  266. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  267. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  268. claude_mpm/hooks/claude_hooks/services/connection_manager.py +41 -26
  269. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +38 -105
  270. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  271. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  272. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  273. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +75 -77
  274. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  275. claude_mpm/hooks/session_resume_hook.py +89 -1
  276. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  277. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  278. claude_mpm/init.py +224 -4
  279. claude_mpm/mcp/__init__.py +9 -0
  280. claude_mpm/mcp/google_workspace_server.py +610 -0
  281. claude_mpm/scripts/claude-hook-handler.sh +46 -19
  282. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  283. claude_mpm/services/agents/agent_selection_service.py +2 -2
  284. claude_mpm/services/agents/cache_git_manager.py +1 -1
  285. claude_mpm/services/agents/deployment/agent_discovery_service.py +3 -1
  286. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  287. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -17
  288. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  289. claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
  290. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  291. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +36 -8
  292. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +50 -26
  293. claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
  294. claude_mpm/services/agents/git_source_manager.py +21 -2
  295. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  296. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  297. claude_mpm/services/agents/sources/git_source_sync_service.py +116 -5
  298. claude_mpm/services/agents/startup_sync.py +5 -2
  299. claude_mpm/services/cli/__init__.py +3 -0
  300. claude_mpm/services/cli/incremental_pause_manager.py +561 -0
  301. claude_mpm/services/cli/session_resume_helper.py +10 -2
  302. claude_mpm/services/command_deployment_service.py +44 -26
  303. claude_mpm/services/delegation_detector.py +175 -0
  304. claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
  305. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  306. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  307. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  308. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  309. claude_mpm/services/diagnostics/models.py +14 -1
  310. claude_mpm/services/event_log.py +325 -0
  311. claude_mpm/services/hook_installer_service.py +77 -8
  312. claude_mpm/services/infrastructure/__init__.py +4 -0
  313. claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
  314. claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
  315. claude_mpm/services/mcp_config_manager.py +99 -19
  316. claude_mpm/services/mcp_service_registry.py +294 -0
  317. claude_mpm/services/monitor/daemon_manager.py +15 -4
  318. claude_mpm/services/monitor/management/lifecycle.py +8 -3
  319. claude_mpm/services/monitor/server.py +111 -16
  320. claude_mpm/services/pm_skills_deployer.py +302 -94
  321. claude_mpm/services/profile_manager.py +10 -4
  322. claude_mpm/services/skills/git_skill_source_manager.py +192 -29
  323. claude_mpm/services/skills/selective_skill_deployer.py +211 -46
  324. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  325. claude_mpm/services/skills_deployer.py +192 -70
  326. claude_mpm/services/socketio/handlers/hook.py +14 -7
  327. claude_mpm/services/socketio/server/main.py +12 -4
  328. claude_mpm/skills/__init__.py +2 -1
  329. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +79 -0
  330. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +178 -0
  331. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
  332. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
  333. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
  334. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
  335. claude_mpm/skills/bundled/collaboration/git-worktrees.md +317 -0
  336. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +112 -0
  337. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +146 -0
  338. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
  339. claude_mpm/skills/bundled/collaboration/stacked-prs.md +251 -0
  340. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +81 -0
  341. claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
  342. claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
  343. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +152 -0
  344. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
  345. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
  346. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
  347. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
  348. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
  349. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
  350. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
  351. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
  352. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
  353. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
  354. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
  355. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
  356. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
  357. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
  358. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +131 -0
  359. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +325 -0
  360. claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
  361. claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
  362. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +499 -0
  363. claude_mpm/skills/bundled/infrastructure/env-manager/INTEGRATION.md +611 -0
  364. claude_mpm/skills/bundled/infrastructure/env-manager/README.md +596 -0
  365. claude_mpm/skills/bundled/infrastructure/env-manager/SKILL.md +260 -0
  366. claude_mpm/skills/bundled/infrastructure/env-manager/examples/nextjs-env-structure.md +315 -0
  367. claude_mpm/skills/bundled/infrastructure/env-manager/references/frameworks.md +436 -0
  368. claude_mpm/skills/bundled/infrastructure/env-manager/references/security.md +433 -0
  369. claude_mpm/skills/bundled/infrastructure/env-manager/references/synchronization.md +452 -0
  370. claude_mpm/skills/bundled/infrastructure/env-manager/references/troubleshooting.md +404 -0
  371. claude_mpm/skills/bundled/infrastructure/env-manager/references/validation.md +420 -0
  372. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +86 -0
  373. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +43 -0
  374. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
  375. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
  376. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
  377. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
  378. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +160 -0
  379. claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
  380. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
  381. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
  382. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
  383. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
  384. claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
  385. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +189 -0
  386. claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
  387. claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
  388. claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
  389. claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
  390. claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
  391. claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
  392. claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
  393. claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
  394. claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
  395. claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
  396. claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
  397. claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
  398. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  399. claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
  400. claude_mpm/skills/bundled/pm/mpm-bug-reporting/SKILL.md +248 -0
  401. claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
  402. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  403. claude_mpm/skills/bundled/pm/mpm-delegation-patterns/SKILL.md +167 -0
  404. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  405. claude_mpm/skills/bundled/pm/mpm-git-file-tracking/SKILL.md +113 -0
  406. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  407. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  408. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  409. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  410. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  411. claude_mpm/skills/bundled/pm/mpm-pr-workflow/SKILL.md +124 -0
  412. claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
  413. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  414. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  415. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  416. claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
  417. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  418. claude_mpm/skills/bundled/pm/mpm-ticketing-integration/SKILL.md +154 -0
  419. claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
  420. claude_mpm/skills/bundled/pm/mpm-verification-protocols/SKILL.md +198 -0
  421. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  422. claude_mpm/skills/bundled/react/flexlayout-react.md +742 -0
  423. claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
  424. claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
  425. claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
  426. claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
  427. claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
  428. claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
  429. claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
  430. claude_mpm/skills/bundled/security-scanning.md +112 -0
  431. claude_mpm/skills/bundled/tauri/tauri-async-patterns.md +495 -0
  432. claude_mpm/skills/bundled/tauri/tauri-build-deploy.md +599 -0
  433. claude_mpm/skills/bundled/tauri/tauri-command-patterns.md +535 -0
  434. claude_mpm/skills/bundled/tauri/tauri-error-handling.md +613 -0
  435. claude_mpm/skills/bundled/tauri/tauri-event-system.md +648 -0
  436. claude_mpm/skills/bundled/tauri/tauri-file-system.md +673 -0
  437. claude_mpm/skills/bundled/tauri/tauri-frontend-integration.md +767 -0
  438. claude_mpm/skills/bundled/tauri/tauri-performance.md +669 -0
  439. claude_mpm/skills/bundled/tauri/tauri-state-management.md +573 -0
  440. claude_mpm/skills/bundled/tauri/tauri-testing.md +384 -0
  441. claude_mpm/skills/bundled/tauri/tauri-window-management.md +628 -0
  442. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +119 -0
  443. claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
  444. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
  445. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
  446. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
  447. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
  448. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
  449. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
  450. claude_mpm/skills/bundled/testing/test-quality-inspector/SKILL.md +458 -0
  451. claude_mpm/skills/bundled/testing/test-quality-inspector/examples/example-inspection-report.md +411 -0
  452. claude_mpm/skills/bundled/testing/test-quality-inspector/references/assertion-quality.md +317 -0
  453. claude_mpm/skills/bundled/testing/test-quality-inspector/references/inspection-checklist.md +270 -0
  454. claude_mpm/skills/bundled/testing/test-quality-inspector/references/red-flags.md +436 -0
  455. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +140 -0
  456. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
  457. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
  458. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
  459. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
  460. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +184 -0
  461. claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
  462. claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
  463. claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
  464. claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
  465. claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
  466. claude_mpm/skills/registry.py +295 -90
  467. claude_mpm/skills/skill_manager.py +29 -23
  468. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  469. claude_mpm/utils/agent_dependency_loader.py +103 -4
  470. claude_mpm/utils/robust_installer.py +45 -24
  471. claude_mpm-5.6.72.dist-info/METADATA +416 -0
  472. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/RECORD +477 -159
  473. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/WHEEL +1 -1
  474. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/entry_points.txt +2 -0
  475. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +0 -1
  476. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +0 -1
  477. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +0 -1
  478. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +0 -1
  479. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +0 -1
  480. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +0 -2
  481. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +0 -2
  482. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +0 -1
  483. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +0 -1
  484. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +0 -10
  485. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  486. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  487. claude_mpm-5.4.41.dist-info/METADATA +0 -998
  488. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE +0 -0
  489. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  490. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1821 @@
1
+ """Commander chat REPL interface."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import uuid
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from enum import Enum
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Optional
14
+
15
+ from prompt_toolkit import PromptSession, prompt as pt_prompt
16
+ from prompt_toolkit.completion import Completer, Completion
17
+ from prompt_toolkit.history import FileHistory
18
+ from prompt_toolkit.patch_stdout import patch_stdout
19
+
20
+
21
+ class RequestStatus(Enum):
22
+ """Status of a pending request."""
23
+
24
+ QUEUED = "queued"
25
+ SENDING = "sending"
26
+ WAITING = "waiting"
27
+ STARTING = "starting" # Instance starting up
28
+ COMPLETED = "completed"
29
+ ERROR = "error"
30
+
31
+
32
+ class RequestType(Enum):
33
+ """Type of pending request."""
34
+
35
+ MESSAGE = "message" # Message to instance
36
+ STARTUP = "startup" # Instance startup/ready wait
37
+
38
+
39
+ @dataclass
40
+ class PendingRequest:
41
+ """Tracks an in-flight request to an instance."""
42
+
43
+ id: str
44
+ target: str # Instance name
45
+ message: str
46
+ request_type: RequestType = RequestType.MESSAGE
47
+ status: RequestStatus = RequestStatus.QUEUED
48
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
49
+ response: Optional[str] = None
50
+ error: Optional[str] = None
51
+
52
+ def elapsed_seconds(self) -> int:
53
+ """Get elapsed time since request was created."""
54
+ return int((datetime.now(timezone.utc) - self.created_at).total_seconds())
55
+
56
+ def display_message(self, max_len: int = 40) -> str:
57
+ """Get truncated message for display."""
58
+ msg = self.message.replace("\n", " ")
59
+ if len(msg) > max_len:
60
+ return msg[: max_len - 3] + "..."
61
+ return msg
62
+
63
+
64
+ @dataclass
65
+ class SavedRegistration:
66
+ """A saved instance registration for persistence."""
67
+
68
+ name: str
69
+ path: str
70
+ framework: str # "cc" or "mpm"
71
+ registered_at: str # ISO timestamp
72
+
73
+ def to_dict(self) -> dict:
74
+ """Convert to dictionary for JSON serialization."""
75
+ return {
76
+ "name": self.name,
77
+ "path": self.path,
78
+ "framework": self.framework,
79
+ "registered_at": self.registered_at,
80
+ }
81
+
82
+ @classmethod
83
+ def from_dict(cls, data: dict) -> "SavedRegistration":
84
+ """Create from dictionary."""
85
+ return cls(
86
+ name=data["name"],
87
+ path=data["path"],
88
+ framework=data["framework"],
89
+ registered_at=data.get(
90
+ "registered_at", datetime.now(timezone.utc).isoformat()
91
+ ),
92
+ )
93
+
94
+
95
+ from claude_mpm.commander.instance_manager import InstanceManager
96
+ from claude_mpm.commander.llm.openrouter_client import OpenRouterClient
97
+ from claude_mpm.commander.models.events import EventType
98
+ from claude_mpm.commander.proxy.relay import OutputRelay
99
+ from claude_mpm.commander.session.manager import SessionManager
100
+
101
+ from .commands import Command, CommandParser, CommandType
102
+
103
+ if TYPE_CHECKING:
104
+ from claude_mpm.commander.events.manager import EventManager
105
+ from claude_mpm.commander.models.events import Event
106
+
107
+
108
+ class CommandCompleter(Completer):
109
+ """Autocomplete for slash commands and instance names."""
110
+
111
+ COMMANDS = [
112
+ ("register", "Register and start a new instance"),
113
+ ("start", "Start a registered instance"),
114
+ ("stop", "Stop a running instance"),
115
+ ("close", "Close instance and merge worktree"),
116
+ ("connect", "Connect to instance (starts from saved if needed)"),
117
+ ("disconnect", "Disconnect from current instance"),
118
+ ("switch", "Switch to another instance"),
119
+ ("list", "List all instances"),
120
+ ("ls", "List all instances (alias)"),
121
+ ("saved", "List saved registrations"),
122
+ ("forget", "Remove a saved registration"),
123
+ ("status", "Show connection status"),
124
+ ("cleanup", "Clean up orphan tmux panes"),
125
+ ("help", "Show help"),
126
+ ("exit", "Exit commander"),
127
+ ("quit", "Exit commander (alias)"),
128
+ ("q", "Exit commander (alias)"),
129
+ ]
130
+
131
+ def __init__(self, get_instances_func):
132
+ """Initialize with function to get instance names.
133
+
134
+ Args:
135
+ get_instances_func: Callable that returns list of instance names.
136
+ """
137
+ self.get_instances = get_instances_func
138
+
139
+ def get_completions(self, document, complete_event):
140
+ """Generate completions for the current input.
141
+
142
+ Args:
143
+ document: The document being edited.
144
+ complete_event: The completion event.
145
+
146
+ Yields:
147
+ Completion objects for matching commands or instance names.
148
+ """
149
+ text = document.text_before_cursor
150
+
151
+ # Complete slash commands
152
+ if text.startswith("/"):
153
+ cmd_text = text[1:].lower()
154
+ # Check if we're completing command args (has space after command)
155
+ if " " in cmd_text:
156
+ # Complete instance names after certain commands
157
+ parts = cmd_text.split()
158
+ cmd = parts[0]
159
+ partial = parts[-1] if len(parts) > 1 else ""
160
+ if cmd in ("start", "stop", "close", "connect", "switch"):
161
+ yield from self._complete_instance_names(partial)
162
+ else:
163
+ # Complete command names
164
+ for cmd, desc in self.COMMANDS:
165
+ if cmd.startswith(cmd_text):
166
+ yield Completion(
167
+ cmd,
168
+ start_position=-len(cmd_text),
169
+ display_meta=desc,
170
+ )
171
+
172
+ # Complete instance names after @ prefix
173
+ elif text.startswith("@"):
174
+ partial = text[1:]
175
+ yield from self._complete_instance_names(partial)
176
+
177
+ # Complete instance names inside parentheses
178
+ elif text.startswith("("):
179
+ # Extract partial name, stripping ) and : if present
180
+ partial = text[1:].rstrip("):")
181
+ yield from self._complete_instance_names(partial)
182
+
183
+ def _complete_instance_names(self, partial: str):
184
+ """Generate completions for instance names.
185
+
186
+ Args:
187
+ partial: Partial instance name typed so far.
188
+
189
+ Yields:
190
+ Completion objects for matching instance names.
191
+ """
192
+ try:
193
+ instances = self.get_instances()
194
+ for name in instances:
195
+ if name.lower().startswith(partial.lower()):
196
+ yield Completion(
197
+ name,
198
+ start_position=-len(partial),
199
+ display_meta="instance",
200
+ )
201
+ except Exception: # nosec B110 - Graceful fallback if instance lookup fails
202
+ pass
203
+
204
+
205
+ class CommanderREPL:
206
+ """Interactive REPL for Commander mode."""
207
+
208
+ CAPABILITIES_CONTEXT = """
209
+ MPM Commander Capabilities:
210
+
211
+ INSTANCE MANAGEMENT (use / prefix):
212
+ - /list, /ls: Show all running Claude Code instances with their status
213
+ - /register <path> <framework> <name>: Register, start, and auto-connect (creates worktree)
214
+ - /start <name>: Start a registered instance by name
215
+ - /start <path> [--framework cc|mpm] [--name name]: Start new instance (creates worktree)
216
+ - /stop <name>: Stop a running instance (keeps worktree)
217
+ - /close <name> [--no-merge]: Close instance, merge worktree to main, and cleanup
218
+ - /connect <name>: Connect to a specific instance for interactive chat
219
+ - /switch <name>: Alias for /connect
220
+ - /disconnect: Disconnect from current instance
221
+ - /status: Show current connection status
222
+
223
+ DIRECT MESSAGING (both syntaxes work the same):
224
+ - @<name> <message>: Send message directly to any instance
225
+ - (<name>) <message>: Same as @name (parentheses syntax)
226
+ - Instance names appear in responses: @myapp: response summary...
227
+
228
+ WHEN CONNECTED:
229
+ - Send natural language messages to Claude (no / prefix)
230
+ - Receive streaming responses
231
+ - Access instance memory and context
232
+ - Execute multi-turn conversations
233
+
234
+ BUILT-IN COMMANDS:
235
+ - /help: Show available commands
236
+ - /exit, /quit, /q: Exit Commander
237
+
238
+ FEATURES:
239
+ - Real-time streaming responses
240
+ - Direct @mention messaging to any instance
241
+ - Worktree isolation and merge workflow
242
+ - Instance discovery via daemon
243
+ - Automatic reconnection handling
244
+ - Session context preservation
245
+ """
246
+
247
+ def __init__(
248
+ self,
249
+ instance_manager: InstanceManager,
250
+ session_manager: SessionManager,
251
+ output_relay: Optional[OutputRelay] = None,
252
+ llm_client: Optional[OpenRouterClient] = None,
253
+ event_manager: Optional["EventManager"] = None,
254
+ ):
255
+ """Initialize REPL.
256
+
257
+ Args:
258
+ instance_manager: Manages Claude instances.
259
+ session_manager: Manages chat session state.
260
+ output_relay: Optional relay for instance output.
261
+ llm_client: Optional OpenRouter client for chat.
262
+ event_manager: Optional event manager for notifications.
263
+ """
264
+ self.instances = instance_manager
265
+ self.session = session_manager
266
+ self.relay = output_relay
267
+ self.llm = llm_client
268
+ self.event_manager = event_manager
269
+ self.parser = CommandParser()
270
+ self._running = False
271
+ self._instance_ready: dict[str, bool] = {}
272
+
273
+ # Async request tracking
274
+ self._pending_requests: dict[str, PendingRequest] = {}
275
+ self._request_queue: asyncio.Queue[PendingRequest] = asyncio.Queue()
276
+ self._response_task: Optional[asyncio.Task] = None
277
+ self._startup_tasks: dict[str, asyncio.Task] = {} # Background startup tasks
278
+ self._stdout_context = None # For patch_stdout
279
+
280
+ # Bottom toolbar status for spinners
281
+ self._toolbar_status = ""
282
+ self.prompt_session: Optional[PromptSession] = None
283
+
284
+ # Persistent registration config
285
+ self._config_dir = Path.cwd() / ".claude-mpm" / "commander"
286
+ self._config_file = self._config_dir / "registrations.json"
287
+ self._saved_registrations: dict[str, SavedRegistration] = {}
288
+ self._load_registrations()
289
+
290
+ def _get_bottom_toolbar(self) -> str:
291
+ """Get bottom toolbar status for prompt_toolkit.
292
+
293
+ Returns:
294
+ Status string for display in toolbar, or empty string if no status.
295
+ """
296
+ return self._toolbar_status
297
+
298
+ async def run(self) -> None:
299
+ """Start the REPL loop."""
300
+ self._running = True
301
+ self._print_welcome()
302
+
303
+ # Wire up EventManager to InstanceManager
304
+ if self.event_manager and self.instances:
305
+ self.instances.set_event_manager(self.event_manager)
306
+
307
+ # Subscribe to instance lifecycle events
308
+ if self.event_manager:
309
+ self.event_manager.subscribe(
310
+ EventType.INSTANCE_STARTING, self._on_instance_event
311
+ )
312
+ self.event_manager.subscribe(
313
+ EventType.INSTANCE_READY, self._on_instance_event
314
+ )
315
+ self.event_manager.subscribe(
316
+ EventType.INSTANCE_ERROR, self._on_instance_event
317
+ )
318
+
319
+ # Setup history file
320
+ history_path = Path.home() / ".claude-mpm" / "commander_history"
321
+ history_path.parent.mkdir(parents=True, exist_ok=True)
322
+
323
+ # Create completer for slash commands and instance names
324
+ completer = CommandCompleter(self._get_instance_names)
325
+
326
+ self.prompt_session = PromptSession(
327
+ history=FileHistory(str(history_path)),
328
+ completer=completer,
329
+ complete_while_typing=False, # Only complete on Tab
330
+ bottom_toolbar=self._get_bottom_toolbar,
331
+ )
332
+
333
+ # Start background response processor
334
+ self._response_task = asyncio.create_task(self._process_responses())
335
+
336
+ # Use patch_stdout to allow printing above prompt
337
+ with patch_stdout():
338
+ while self._running:
339
+ try:
340
+ # Show pending requests status above prompt
341
+ self._render_pending_status()
342
+ user_input = await self.prompt_session.prompt_async(
343
+ self._get_prompt
344
+ )
345
+ await self._handle_input(user_input.strip())
346
+ except KeyboardInterrupt:
347
+ continue
348
+ except EOFError:
349
+ break
350
+
351
+ # Cleanup
352
+ if self._response_task:
353
+ self._response_task.cancel()
354
+ try:
355
+ await self._response_task
356
+ except asyncio.CancelledError:
357
+ pass
358
+
359
+ # Stop all running instances before exiting
360
+ instances_to_stop = self.instances.list_instances()
361
+ for instance in instances_to_stop:
362
+ try:
363
+ await self.instances.stop_instance(instance.name)
364
+ except Exception as e:
365
+ self._print(f"Warning: Failed to stop '{instance.name}': {e}")
366
+
367
+ self._print("\nGoodbye!")
368
+
369
+ def _load_registrations(self) -> None:
370
+ """Load saved registrations from config file."""
371
+ if not self._config_file.exists():
372
+ return
373
+ try:
374
+ with self._config_file.open() as f:
375
+ data = json.load(f)
376
+ for reg_data in data.get("registrations", []):
377
+ reg = SavedRegistration.from_dict(reg_data)
378
+ self._saved_registrations[reg.name] = reg
379
+ except (json.JSONDecodeError, KeyError, OSError):
380
+ # Ignore corrupt/unreadable config
381
+ pass
382
+
383
+ def _save_registrations(self) -> None:
384
+ """Save registrations to config file."""
385
+ self._config_dir.mkdir(parents=True, exist_ok=True)
386
+ data = {
387
+ "registrations": [
388
+ reg.to_dict() for reg in self._saved_registrations.values()
389
+ ]
390
+ }
391
+ with self._config_file.open("w") as f:
392
+ json.dump(data, f, indent=2)
393
+
394
+ def _save_registration(self, name: str, path: str, framework: str) -> None:
395
+ """Save a single registration."""
396
+ reg = SavedRegistration(
397
+ name=name,
398
+ path=path,
399
+ framework=framework,
400
+ registered_at=datetime.now(timezone.utc).isoformat(),
401
+ )
402
+ self._saved_registrations[name] = reg
403
+ self._save_registrations()
404
+
405
+ def _forget_registration(self, name: str) -> bool:
406
+ """Remove a saved registration. Returns True if removed."""
407
+ if name in self._saved_registrations:
408
+ del self._saved_registrations[name]
409
+ self._save_registrations()
410
+ return True
411
+ return False
412
+
413
+ async def _handle_input(self, input_text: str) -> None:
414
+ """Handle user input - command or natural language.
415
+
416
+ Args:
417
+ input_text: User input string.
418
+ """
419
+ if not input_text:
420
+ return
421
+
422
+ # Check for direct @mention first (before any other parsing)
423
+ mention = self._parse_mention(input_text)
424
+ if mention:
425
+ target, message = mention
426
+ await self._cmd_message_instance(target, message)
427
+ return
428
+
429
+ # Check if it's a built-in slash command first
430
+ command = self.parser.parse(input_text)
431
+ if command:
432
+ await self._execute_command(command)
433
+ return
434
+
435
+ # Use LLM to classify natural language input
436
+ intent_result = await self._classify_intent_llm(input_text)
437
+ intent = intent_result.get("intent", "chat")
438
+ args = intent_result.get("args", {})
439
+
440
+ # Handle command intents detected by LLM
441
+ if intent == "register":
442
+ await self._cmd_register_from_args(args)
443
+ elif intent == "start":
444
+ await self._cmd_start_from_args(args)
445
+ elif intent == "stop":
446
+ await self._cmd_stop_from_args(args)
447
+ elif intent in {"connect", "switch"}:
448
+ await self._cmd_connect_from_args(args)
449
+ elif intent == "disconnect":
450
+ await self._cmd_disconnect([])
451
+ elif intent == "list":
452
+ await self._cmd_list([])
453
+ elif intent == "status":
454
+ await self._cmd_status([])
455
+ elif intent == "help":
456
+ await self._cmd_help([])
457
+ elif intent == "exit":
458
+ await self._cmd_exit([])
459
+ elif intent == "capabilities":
460
+ await self._handle_capabilities(input_text)
461
+ elif intent == "greeting":
462
+ self._handle_greeting()
463
+ elif intent == "message":
464
+ # Handle @mention detected by LLM
465
+ target = args.get("target")
466
+ message = args.get("message")
467
+ if target and message:
468
+ await self._cmd_message_instance(target, message)
469
+ else:
470
+ await self._send_to_instance(input_text)
471
+ else:
472
+ # Default to chat - send to connected instance
473
+ await self._send_to_instance(input_text)
474
+
475
+ async def _execute_command(self, cmd: Command) -> None:
476
+ """Execute a built-in command.
477
+
478
+ Args:
479
+ cmd: Parsed command.
480
+ """
481
+ handlers = {
482
+ CommandType.LIST: self._cmd_list,
483
+ CommandType.START: self._cmd_start,
484
+ CommandType.STOP: self._cmd_stop,
485
+ CommandType.CLOSE: self._cmd_close,
486
+ CommandType.REGISTER: self._cmd_register,
487
+ CommandType.CONNECT: self._cmd_connect,
488
+ CommandType.DISCONNECT: self._cmd_disconnect,
489
+ CommandType.SAVED: self._cmd_saved,
490
+ CommandType.FORGET: self._cmd_forget,
491
+ CommandType.STATUS: self._cmd_status,
492
+ CommandType.HELP: self._cmd_help,
493
+ CommandType.EXIT: self._cmd_exit,
494
+ CommandType.MPM_OAUTH: self._cmd_oauth,
495
+ CommandType.CLEANUP: self._cmd_cleanup,
496
+ }
497
+ handler = handlers.get(cmd.type)
498
+ if handler:
499
+ await handler(cmd.args)
500
+
501
+ def _classify_intent(self, text: str) -> str:
502
+ """Classify user input intent.
503
+
504
+ Args:
505
+ text: User input text.
506
+
507
+ Returns:
508
+ Intent type: 'greeting', 'capabilities', or 'chat'.
509
+ """
510
+ t = text.lower().strip()
511
+ if any(t.startswith(g) for g in ["hello", "hi", "hey", "howdy"]):
512
+ return "greeting"
513
+ if any(p in t for p in ["what can you", "can you", "help me", "how do i"]):
514
+ return "capabilities"
515
+ return "chat"
516
+
517
+ def _parse_mention(self, text: str) -> tuple[str, str] | None:
518
+ """Parse @name or (name) message patterns - both work the same.
519
+
520
+ Both syntaxes are equivalent:
521
+ @name message
522
+ (name) message
523
+ (name): message
524
+
525
+ Args:
526
+ text: User input text.
527
+
528
+ Returns:
529
+ Tuple of (target_name, message) if pattern matches, None otherwise.
530
+ """
531
+ # @name message
532
+ match = re.match(r"^@(\w+)\s+(.+)$", text.strip())
533
+ if match:
534
+ return match.group(1), match.group(2)
535
+
536
+ # (name): message or (name) message - same behavior as @name
537
+ match = re.match(r"^\((\w+)\):?\s*(.+)$", text.strip())
538
+ if match:
539
+ return match.group(1), match.group(2)
540
+
541
+ return None
542
+
543
+ async def _classify_intent_llm(self, text: str) -> dict:
544
+ """Use LLM to classify user intent.
545
+
546
+ Args:
547
+ text: User input text.
548
+
549
+ Returns:
550
+ Dict with 'intent' and 'args' keys.
551
+ """
552
+ if not self.llm:
553
+ return {"intent": "chat", "args": {}}
554
+
555
+ system_prompt = """Classify user intent. Return JSON only.
556
+
557
+ Commands available:
558
+ - register: Register new instance (needs: path, framework, name)
559
+ - start: Start registered instance (needs: name)
560
+ - stop: Stop instance (needs: name)
561
+ - connect: Connect to instance (needs: name)
562
+ - disconnect: Disconnect from current instance
563
+ - switch: Switch to different instance (needs: name)
564
+ - list: List instances
565
+ - status: Show status
566
+ - help: Show help
567
+ - exit: Exit commander
568
+
569
+ If user wants a command, extract arguments.
570
+ If user is chatting/asking questions, intent is "chat".
571
+
572
+ Examples:
573
+ "register my project at ~/foo as myapp using mpm" -> {"intent":"register","args":{"path":"~/foo","framework":"mpm","name":"myapp"}}
574
+ "start myapp" -> {"intent":"start","args":{"name":"myapp"}}
575
+ "stop the server" -> {"intent":"stop","args":{"name":null}}
576
+ "list instances" -> {"intent":"list","args":{}}
577
+ "hello how are you" -> {"intent":"chat","args":{}}
578
+ "what can you do" -> {"intent":"capabilities","args":{}}
579
+ "@izzie show me the code" -> {"intent":"message","args":{"target":"izzie","message":"show me the code"}}
580
+ "(myapp): what's the status" -> {"intent":"message","args":{"target":"myapp","message":"what's the status"}}
581
+
582
+ Return ONLY valid JSON."""
583
+
584
+ try:
585
+ messages = [{"role": "user", "content": f"Classify: {text}"}]
586
+ response = await self.llm.chat(messages, system=system_prompt)
587
+ return json.loads(response.strip())
588
+ except (json.JSONDecodeError, Exception): # nosec B110 - Graceful fallback
589
+ return {"intent": "chat", "args": {}}
590
+
591
+ def _handle_greeting(self) -> None:
592
+ """Handle greeting intent."""
593
+ self._print(
594
+ "Hello! I'm MPM Commander. Type '/help' for commands, or '/list' to see instances."
595
+ )
596
+
597
+ async def _handle_capabilities(self, query: str = "") -> None:
598
+ """Answer questions about capabilities, using LLM if available.
599
+
600
+ Args:
601
+ query: Optional user query about capabilities.
602
+ """
603
+ if query and self.llm:
604
+ try:
605
+ messages = [
606
+ {
607
+ "role": "user",
608
+ "content": f"Based on these capabilities:\n{self.CAPABILITIES_CONTEXT}\n\nUser asks: {query}",
609
+ }
610
+ ]
611
+ system = (
612
+ "Answer concisely about MPM Commander capabilities. "
613
+ "If asked about something not in the capabilities, say so."
614
+ )
615
+ response = await self.llm.chat(messages, system=system)
616
+ self._print(response)
617
+ return
618
+ except Exception: # nosec B110 - Graceful fallback to static output
619
+ pass
620
+ # Fallback to static output
621
+ self._print(self.CAPABILITIES_CONTEXT)
622
+
623
+ async def _cmd_list(self, args: list[str]) -> None:
624
+ """List instances: both running and saved registrations.
625
+
626
+ Shows:
627
+ - Running instances with status (connected, ready, or connecting)
628
+ - Saved registrations that are not currently running
629
+ """
630
+ running_instances = self.instances.list_instances()
631
+ running_names = {inst.name for inst in running_instances}
632
+ saved_registrations = self._saved_registrations
633
+
634
+ # Collect all unique names
635
+ all_names = set(running_names) | set(saved_registrations.keys())
636
+
637
+ if not all_names:
638
+ self._print("No instances (running or saved).")
639
+ self._print("Use '/register <path> <framework> <name>' to create one.")
640
+ return
641
+
642
+ # Build output
643
+ self._print("Sessions:")
644
+
645
+ # Display in order: running first, then saved
646
+ for name in sorted(all_names):
647
+ inst = next((i for i in running_instances if i.name == name), None)
648
+ is_connected = inst and name == self.session.context.connected_instance
649
+
650
+ if inst:
651
+ # Running instance
652
+ git_info = f" [{inst.git_branch}]" if inst.git_branch else ""
653
+
654
+ # Determine status
655
+ if is_connected:
656
+ instance_status = "connected"
657
+ elif inst.ready:
658
+ instance_status = "ready"
659
+ else:
660
+ instance_status = "starting"
661
+
662
+ # Format with right-aligned path
663
+ line = f" {name} (running, {instance_status})"
664
+ path_display = f"{inst.project_path}{git_info}"
665
+ # Pad to align paths
666
+ padding = max(1, 40 - len(line))
667
+ self._print(f"{line}{' ' * padding}{path_display}")
668
+ else:
669
+ # Saved registration (not running)
670
+ reg = saved_registrations[name]
671
+ line = f" {name} (saved)"
672
+ # Pad to align paths
673
+ padding = max(1, 40 - len(line))
674
+ self._print(f"{line}{' ' * padding}{reg.path}")
675
+
676
+ async def _cmd_start(self, args: list[str]) -> None:
677
+ """Start instance: start <name> OR start <path> [--framework cc|mpm] [--name name]."""
678
+ if not args:
679
+ self._print("Usage: start <name> (for registered instances)")
680
+ self._print(" start <path> [--framework cc|mpm] [--name name]")
681
+ return
682
+
683
+ # Check if first arg is a registered instance name (no path separators)
684
+ if len(args) == 1 and "/" not in args[0] and not args[0].startswith("~"):
685
+ name = args[0]
686
+ try:
687
+ instance = await self.instances.start_by_name(name)
688
+ if instance:
689
+ self._print(f"Started registered instance '{name}'")
690
+ self._print(
691
+ f" Tmux: {instance.tmux_session}:{instance.pane_target}"
692
+ )
693
+ else:
694
+ self._print(f"No registered instance named '{name}'")
695
+ self._print(
696
+ "Use 'register <path> <framework> <name>' to register first"
697
+ )
698
+ except Exception as e:
699
+ self._print(f"Error starting instance: {e}")
700
+ return
701
+
702
+ # Path-based start logic
703
+ project_path = Path(args[0]).expanduser().resolve()
704
+ framework = "cc" # default
705
+ name = project_path.name # default
706
+
707
+ # Parse optional flags
708
+ i = 1
709
+ while i < len(args):
710
+ if args[i] == "--framework" and i + 1 < len(args):
711
+ framework = args[i + 1]
712
+ i += 2
713
+ elif args[i] == "--name" and i + 1 < len(args):
714
+ name = args[i + 1]
715
+ i += 2
716
+ else:
717
+ i += 1
718
+
719
+ # Validate path
720
+ if not project_path.exists():
721
+ self._print(f"Error: Path does not exist: {project_path}")
722
+ return
723
+
724
+ if not project_path.is_dir():
725
+ self._print(f"Error: Path is not a directory: {project_path}")
726
+ return
727
+
728
+ # Register and start instance (creates worktree for git repos)
729
+ try:
730
+ instance = await self.instances.register_instance(
731
+ str(project_path), framework, name
732
+ )
733
+ self._print(f"Started instance '{name}' ({framework}) at {project_path}")
734
+ self._print(f" Tmux: {instance.tmux_session}:{instance.pane_target}")
735
+
736
+ # Check if worktree was created
737
+ if self.instances._state_store:
738
+ registered = self.instances._state_store.get_instance(name)
739
+ if registered and registered.use_worktree and registered.worktree_path:
740
+ self._print(f" Worktree: {registered.worktree_path}")
741
+ self._print(f" Branch: {registered.worktree_branch}")
742
+
743
+ # Spawn background task to wait for ready (non-blocking with spinner)
744
+ self._spawn_startup_task(name, auto_connect=True, timeout=30)
745
+ except Exception as e:
746
+ self._print(f"Error starting instance: {e}")
747
+
748
+ async def _cmd_stop(self, args: list[str]) -> None:
749
+ """Stop an instance: stop <name>."""
750
+ if not args:
751
+ self._print("Usage: stop <instance-name>")
752
+ return
753
+
754
+ name = args[0]
755
+
756
+ try:
757
+ await self.instances.stop_instance(name)
758
+ self._print(f"Stopped instance '{name}'")
759
+
760
+ # Disconnect if we were connected
761
+ if self.session.context.connected_instance == name:
762
+ self.session.disconnect()
763
+ except Exception as e:
764
+ self._print(f"Error stopping instance: {e}")
765
+
766
+ async def _cmd_close(self, args: list[str]) -> None:
767
+ """Close instance: merge worktree to main and end session.
768
+
769
+ Usage: /close <name> [--no-merge]
770
+ """
771
+ if not args:
772
+ self._print("Usage: /close <name> [--no-merge]")
773
+ return
774
+
775
+ name = args[0]
776
+ merge = "--no-merge" not in args
777
+
778
+ # Disconnect if we were connected
779
+ if self.session.context.connected_instance == name:
780
+ self.session.disconnect()
781
+
782
+ success, msg = await self.instances.close_instance(name, merge=merge)
783
+ if success:
784
+ self._print(f"Closed '{name}'")
785
+ if merge:
786
+ self._print(" Worktree merged to main")
787
+ else:
788
+ self._print(f"Error: {msg}")
789
+
790
+ async def _cmd_register(self, args: list[str]) -> None:
791
+ """Register and start an instance: register <path> <framework> <name>."""
792
+ if len(args) < 3:
793
+ self._print("Usage: register <path> <framework> <name>")
794
+ self._print(" framework: cc (Claude Code) or mpm")
795
+ return
796
+
797
+ path, framework, name = args[0], args[1], args[2]
798
+ path = Path(path).expanduser().resolve()
799
+
800
+ if framework not in ("cc", "mpm"):
801
+ self._print(f"Unknown framework: {framework}. Use 'cc' or 'mpm'")
802
+ return
803
+
804
+ # Validate path
805
+ if not path.exists():
806
+ self._print(f"Error: Path does not exist: {path}")
807
+ return
808
+
809
+ if not path.is_dir():
810
+ self._print(f"Error: Path is not a directory: {path}")
811
+ return
812
+
813
+ try:
814
+ instance = await self.instances.register_instance(
815
+ str(path), framework, name
816
+ )
817
+ self._print(f"Registered and started '{name}' ({framework}) at {path}")
818
+ self._print(f" Tmux: {instance.tmux_session}:{instance.pane_target}")
819
+
820
+ # Save registration for persistence
821
+ self._save_registration(name, str(path), framework)
822
+
823
+ # Spawn background task to wait for ready (non-blocking with spinner)
824
+ self._spawn_startup_task(name, auto_connect=True, timeout=30)
825
+ except Exception as e:
826
+ self._print(f"Failed to register: {e}")
827
+
828
+ async def _cmd_connect(self, args: list[str]) -> None:
829
+ """Connect to an instance: connect <name>.
830
+
831
+ If instance is not running but has saved registration, start it first.
832
+ """
833
+ if not args:
834
+ self._print("Usage: connect <instance-name>")
835
+ return
836
+
837
+ name = args[0]
838
+ inst = self.instances.get_instance(name)
839
+
840
+ if not inst:
841
+ # Check if we have a saved registration
842
+ saved = self._saved_registrations.get(name)
843
+ if saved:
844
+ self._print(f"Starting '{name}' from saved config...")
845
+ try:
846
+ instance = await self.instances.register_instance(
847
+ saved.path, saved.framework, name
848
+ )
849
+ self._print(f"Started '{name}' ({saved.framework}) at {saved.path}")
850
+ self._print(
851
+ f" Tmux: {instance.tmux_session}:{instance.pane_target}"
852
+ )
853
+ # Spawn background task to wait for ready (non-blocking with spinner)
854
+ self._spawn_startup_task(name, auto_connect=True, timeout=30)
855
+ return
856
+ except Exception as e:
857
+ self._print(f"Failed to start from saved config: {e}")
858
+ return
859
+ else:
860
+ self._print(f"Instance '{name}' not found")
861
+ self._print(" Use /saved to see saved registrations")
862
+ return
863
+
864
+ self.session.connect_to(name)
865
+ self._print(f"Connected to {name}")
866
+
867
+ async def _cmd_disconnect(self, args: list[str]) -> None:
868
+ """Disconnect from current instance."""
869
+ if not self.session.context.is_connected:
870
+ self._print("Not connected to any instance")
871
+ return
872
+
873
+ name = self.session.context.connected_instance
874
+ self.session.disconnect()
875
+ self._print(f"Disconnected from {name}")
876
+
877
+ async def _cmd_status(self, args: list[str]) -> None:
878
+ """Show status of current session."""
879
+ if self.session.context.is_connected:
880
+ name = self.session.context.connected_instance
881
+ inst = self.instances.get_instance(name)
882
+ if inst:
883
+ self._print(f"Connected to: {name}")
884
+ self._print(f" Framework: {inst.framework}")
885
+ self._print(f" Project: {inst.project_path}")
886
+ if inst.git_branch:
887
+ self._print(f" Git: {inst.git_branch} ({inst.git_status})")
888
+ self._print(f" Tmux: {inst.tmux_session}:{inst.pane_target}")
889
+ else:
890
+ self._print(f"Connected to: {name} (instance no longer exists)")
891
+ else:
892
+ self._print("Not connected to any instance")
893
+
894
+ self._print(f"Messages in history: {len(self.session.context.messages)}")
895
+
896
+ async def _cmd_saved(self, args: list[str]) -> None:
897
+ """List saved registrations."""
898
+ if not self._saved_registrations:
899
+ self._print("No saved registrations")
900
+ self._print(" Use /register to create one")
901
+ return
902
+
903
+ self._print("Saved registrations:")
904
+ for reg in self._saved_registrations.values():
905
+ running = self.instances.get_instance(reg.name) is not None
906
+ status = " (running)" if running else ""
907
+ self._print(f" {reg.name}: {reg.path} [{reg.framework}]{status}")
908
+
909
+ async def _cmd_forget(self, args: list[str]) -> None:
910
+ """Remove a saved registration: forget <name>."""
911
+ if not args:
912
+ self._print("Usage: forget <name>")
913
+ return
914
+
915
+ name = args[0]
916
+ if self._forget_registration(name):
917
+ self._print(f"Removed saved registration '{name}'")
918
+ else:
919
+ self._print(f"No saved registration named '{name}'")
920
+
921
+ async def _cmd_help(self, args: list[str]) -> None:
922
+ """Show help message."""
923
+ help_text = """
924
+ Commander Commands (use / prefix):
925
+ /register <path> <framework> <name>
926
+ Register, start, and auto-connect (creates worktree)
927
+ /connect <name> Connect to instance (starts from saved config if needed)
928
+ /switch <name> Alias for /connect
929
+ /disconnect Disconnect from current instance
930
+ /start <name> Start a registered instance by name
931
+ /start <path> Start new instance (creates worktree for git repos)
932
+ /stop <name> Stop an instance (keeps worktree)
933
+ /close <name> [--no-merge]
934
+ Close instance: merge worktree to main and cleanup
935
+ /list, /ls List active instances
936
+ /saved List saved registrations
937
+ /forget <name> Remove a saved registration
938
+ /status Show current session status
939
+ /cleanup [--force] Clean up orphan tmux panes (--force to kill them)
940
+ /help Show this help message
941
+ /exit, /quit, /q Exit Commander
942
+
943
+ Direct Messaging (both syntaxes work the same):
944
+ @<name> <message> Send message to specific instance
945
+ (<name>) <message> Same as @name (parentheses syntax)
946
+
947
+ Natural Language:
948
+ Any input without / prefix is sent to the connected instance.
949
+
950
+ Git Worktree Isolation:
951
+ When starting instances in git repos, a worktree is created on a
952
+ session-specific branch. Use /close to merge changes back to main.
953
+
954
+ Examples:
955
+ /register ~/myproject cc myapp # Register, start, and connect
956
+ /start ~/myproject # Start with auto-detected name
957
+ /start myapp # Start registered instance
958
+ /close myapp # Merge worktree to main and cleanup
959
+ /close myapp --no-merge # Cleanup without merging
960
+ /cleanup # Show orphan panes
961
+ /cleanup --force # Kill orphan panes
962
+ @myapp show me the code # Direct message to myapp
963
+ (izzie) what's the status # Same as @izzie
964
+ Fix the authentication bug # Send to connected instance
965
+ /exit
966
+ """
967
+ self._print(help_text)
968
+
969
+ async def _cmd_exit(self, args: list[str]) -> None:
970
+ """Exit the REPL and stop all running instances."""
971
+ # Stop all running instances before exiting
972
+ instances_to_stop = self.instances.list_instances()
973
+ for instance in instances_to_stop:
974
+ try:
975
+ await self.instances.stop_instance(instance.name)
976
+ except Exception as e:
977
+ self._print(f"Warning: Failed to stop '{instance.name}': {e}")
978
+
979
+ self._running = False
980
+
981
+ async def _cmd_oauth(self, args: list[str]) -> None:
982
+ """Handle OAuth command with subcommands.
983
+
984
+ Usage:
985
+ /mpm-oauth - Show help
986
+ /mpm-oauth list - List OAuth-capable services
987
+ /mpm-oauth setup <service> - Set up OAuth for a service
988
+ /mpm-oauth status <service> - Show token status
989
+ /mpm-oauth revoke <service> - Revoke OAuth tokens
990
+ /mpm-oauth refresh <service> - Refresh OAuth tokens
991
+ """
992
+ if not args:
993
+ await self._cmd_oauth_help()
994
+ return
995
+
996
+ subcommand = args[0].lower()
997
+ subargs = args[1:] if len(args) > 1 else []
998
+
999
+ if subcommand == "help":
1000
+ await self._cmd_oauth_help()
1001
+ elif subcommand == "list":
1002
+ await self._cmd_oauth_list()
1003
+ elif subcommand == "setup":
1004
+ if not subargs:
1005
+ self._print("Usage: /mpm-oauth setup <service>")
1006
+ return
1007
+ await self._cmd_oauth_setup(subargs[0])
1008
+ elif subcommand == "status":
1009
+ if not subargs:
1010
+ self._print("Usage: /mpm-oauth status <service>")
1011
+ return
1012
+ await self._cmd_oauth_status(subargs[0])
1013
+ elif subcommand == "revoke":
1014
+ if not subargs:
1015
+ self._print("Usage: /mpm-oauth revoke <service>")
1016
+ return
1017
+ await self._cmd_oauth_revoke(subargs[0])
1018
+ elif subcommand == "refresh":
1019
+ if not subargs:
1020
+ self._print("Usage: /mpm-oauth refresh <service>")
1021
+ return
1022
+ await self._cmd_oauth_refresh(subargs[0])
1023
+ else:
1024
+ self._print(f"Unknown subcommand: {subcommand}")
1025
+ await self._cmd_oauth_help()
1026
+
1027
+ async def _cmd_oauth_help(self) -> None:
1028
+ """Print OAuth command help."""
1029
+ help_text = """
1030
+ OAuth Commands:
1031
+ /mpm-oauth list List OAuth-capable MCP services
1032
+ /mpm-oauth setup <service> Set up OAuth authentication for a service
1033
+ /mpm-oauth status <service> Show token status for a service
1034
+ /mpm-oauth revoke <service> Revoke OAuth tokens for a service
1035
+ /mpm-oauth refresh <service> Refresh OAuth tokens for a service
1036
+ /mpm-oauth help Show this help message
1037
+
1038
+ Examples:
1039
+ /mpm-oauth list
1040
+ /mpm-oauth setup google-drive
1041
+ /mpm-oauth status google-drive
1042
+ """
1043
+ self._print(help_text)
1044
+
1045
+ async def _cmd_oauth_list(self) -> None:
1046
+ """List OAuth-capable services from MCP registry."""
1047
+ try:
1048
+ from claude_mpm.services.mcp_service_registry import MCPServiceRegistry
1049
+
1050
+ registry = MCPServiceRegistry()
1051
+ services = registry.list_oauth_services()
1052
+
1053
+ if not services:
1054
+ self._print("No OAuth-capable services found.")
1055
+ return
1056
+
1057
+ self._print("OAuth-capable services:")
1058
+ for service in services:
1059
+ self._print(f" - {service}")
1060
+ except ImportError:
1061
+ self._print("MCP Service Registry not available.")
1062
+ except Exception as e:
1063
+ self._print(f"Error listing services: {e}")
1064
+
1065
+ def _load_oauth_credentials_from_env_files(self) -> tuple[str | None, str | None]:
1066
+ """Load OAuth credentials from .env files.
1067
+
1068
+ Checks .env.local first (user overrides), then .env.
1069
+ Returns tuple of (client_id, client_secret), either may be None.
1070
+ """
1071
+ client_id = None
1072
+ client_secret = None
1073
+
1074
+ # Priority order: .env.local first (user overrides), then .env
1075
+ env_files = [".env.local", ".env"]
1076
+
1077
+ for env_file in env_files:
1078
+ env_path = Path.cwd() / env_file
1079
+ if env_path.exists():
1080
+ try:
1081
+ with open(env_path) as f:
1082
+ for line in f:
1083
+ line = line.strip()
1084
+ # Skip empty lines and comments
1085
+ if not line or line.startswith("#"):
1086
+ continue
1087
+ if "=" in line:
1088
+ key, _, value = line.partition("=")
1089
+ key = key.strip()
1090
+ value = value.strip().strip('"').strip("'")
1091
+
1092
+ if key == "GOOGLE_OAUTH_CLIENT_ID" and not client_id:
1093
+ client_id = value
1094
+ elif (
1095
+ key == "GOOGLE_OAUTH_CLIENT_SECRET"
1096
+ and not client_secret
1097
+ ):
1098
+ client_secret = value
1099
+
1100
+ # If we found both, no need to check more files
1101
+ if client_id and client_secret:
1102
+ break
1103
+ except Exception: # nosec B110 - intentionally ignore .env file read errors
1104
+ # Silently ignore read errors
1105
+ pass
1106
+
1107
+ return client_id, client_secret
1108
+
1109
+ async def _cmd_oauth_setup(self, service_name: str) -> None:
1110
+ """Set up OAuth for a service.
1111
+
1112
+ Args:
1113
+ service_name: Name of the service to authenticate.
1114
+ """
1115
+ # Priority: 1) .env files, 2) environment variables, 3) interactive prompt
1116
+ # Check .env files first
1117
+ client_id, client_secret = self._load_oauth_credentials_from_env_files()
1118
+
1119
+ # Fall back to environment variables if not found in .env files
1120
+ if not client_id:
1121
+ client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
1122
+ if not client_secret:
1123
+ client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
1124
+
1125
+ # If credentials missing, prompt for them interactively
1126
+ if not client_id or not client_secret:
1127
+ self._console.print(
1128
+ "\n[yellow]Google OAuth credentials not found.[/yellow]"
1129
+ )
1130
+ self._console.print(
1131
+ "Checked: .env.local, .env, and environment variables.\n"
1132
+ )
1133
+ self._console.print(
1134
+ "Get credentials from: https://console.cloud.google.com/apis/credentials\n"
1135
+ )
1136
+ self._console.print(
1137
+ "[dim]Tip: Add to .env.local for automatic loading:[/dim]"
1138
+ )
1139
+ self._console.print('[dim] GOOGLE_OAUTH_CLIENT_ID="your-client-id"[/dim]')
1140
+ self._console.print(
1141
+ '[dim] GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"[/dim]\n' # pragma: allowlist secret
1142
+ )
1143
+
1144
+ try:
1145
+ client_id = pt_prompt("Enter GOOGLE_OAUTH_CLIENT_ID: ")
1146
+ if not client_id.strip():
1147
+ self._print("Error: Client ID is required")
1148
+ return
1149
+
1150
+ client_secret = pt_prompt(
1151
+ "Enter GOOGLE_OAUTH_CLIENT_SECRET: ", is_password=True
1152
+ )
1153
+ if not client_secret.strip():
1154
+ self._print("Error: Client Secret is required")
1155
+ return
1156
+
1157
+ # Set in environment for this session
1158
+ os.environ["GOOGLE_OAUTH_CLIENT_ID"] = client_id.strip()
1159
+ os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = client_secret.strip()
1160
+ self._console.print(
1161
+ "\n[green]Credentials set for this session.[/green]"
1162
+ )
1163
+
1164
+ # Ask if user wants to save credentials
1165
+ save_response = pt_prompt(
1166
+ "\nSave credentials to shell profile? (y/n): "
1167
+ )
1168
+ if save_response.strip().lower() in ("y", "yes"):
1169
+ self._console.print("\nAdd these lines to your shell profile:")
1170
+ self._console.print(
1171
+ f' export GOOGLE_OAUTH_CLIENT_ID="{client_id.strip()}"'
1172
+ )
1173
+ self._console.print(
1174
+ f' export GOOGLE_OAUTH_CLIENT_SECRET="{client_secret.strip()}"'
1175
+ )
1176
+ self._console.print("")
1177
+
1178
+ except (EOFError, KeyboardInterrupt):
1179
+ self._print("\nCredential entry cancelled.")
1180
+ return
1181
+
1182
+ try:
1183
+ from claude_mpm.auth import OAuthManager
1184
+
1185
+ manager = OAuthManager()
1186
+
1187
+ self._print(f"Setting up OAuth for '{service_name}'...")
1188
+ self._print("Opening browser for authentication...")
1189
+ self._print("Callback server listening on http://localhost:8085/callback")
1190
+
1191
+ result = await manager.authenticate(service_name)
1192
+
1193
+ if result.success:
1194
+ self._print(f"OAuth setup complete for '{service_name}'")
1195
+ self._print(f" Token expires: {result.expires_at}")
1196
+ else:
1197
+ self._print(f"OAuth setup failed: {result.error}")
1198
+ except ImportError:
1199
+ self._print("OAuth module not available.")
1200
+ except Exception as e:
1201
+ self._print(f"Error during OAuth setup: {e}")
1202
+
1203
+ async def _cmd_oauth_status(self, service_name: str) -> None:
1204
+ """Show OAuth token status for a service.
1205
+
1206
+ Args:
1207
+ service_name: Name of the service to check.
1208
+ """
1209
+ try:
1210
+ from claude_mpm.auth import OAuthManager
1211
+
1212
+ manager = OAuthManager()
1213
+ status = await manager.get_status(service_name)
1214
+
1215
+ if status is None:
1216
+ self._print(f"No OAuth tokens found for '{service_name}'")
1217
+ return
1218
+
1219
+ self._print_token_status(service_name, status, stored=True)
1220
+ except ImportError:
1221
+ self._print("OAuth module not available.")
1222
+ except Exception as e:
1223
+ self._print(f"Error checking status: {e}")
1224
+
1225
+ async def _cmd_oauth_revoke(self, service_name: str) -> None:
1226
+ """Revoke OAuth tokens for a service.
1227
+
1228
+ Args:
1229
+ service_name: Name of the service to revoke.
1230
+ """
1231
+ try:
1232
+ from claude_mpm.auth import OAuthManager
1233
+
1234
+ manager = OAuthManager()
1235
+
1236
+ self._print(f"Revoking OAuth tokens for '{service_name}'...")
1237
+ result = await manager.revoke(service_name)
1238
+
1239
+ if result.success:
1240
+ self._print(f"OAuth tokens revoked for '{service_name}'")
1241
+ else:
1242
+ self._print(f"Failed to revoke: {result.error}")
1243
+ except ImportError:
1244
+ self._print("OAuth module not available.")
1245
+ except Exception as e:
1246
+ self._print(f"Error revoking tokens: {e}")
1247
+
1248
+ async def _cmd_oauth_refresh(self, service_name: str) -> None:
1249
+ """Refresh OAuth tokens for a service.
1250
+
1251
+ Args:
1252
+ service_name: Name of the service to refresh.
1253
+ """
1254
+ try:
1255
+ from claude_mpm.auth import OAuthManager
1256
+
1257
+ manager = OAuthManager()
1258
+
1259
+ self._print(f"Refreshing OAuth tokens for '{service_name}'...")
1260
+ result = await manager.refresh(service_name)
1261
+
1262
+ if result.success:
1263
+ self._print(f"OAuth tokens refreshed for '{service_name}'")
1264
+ self._print(f" New expiry: {result.expires_at}")
1265
+ else:
1266
+ self._print(f"Failed to refresh: {result.error}")
1267
+ except ImportError:
1268
+ self._print("OAuth module not available.")
1269
+ except Exception as e:
1270
+ self._print(f"Error refreshing tokens: {e}")
1271
+
1272
+ async def _cmd_cleanup(self, args: list[str]) -> None:
1273
+ """Clean up orphan tmux panes not in tracked instances.
1274
+
1275
+ Identifies all tmux panes in the commander session and removes those
1276
+ that are not associated with any tracked instance.
1277
+
1278
+ Usage:
1279
+ /cleanup - Show orphan panes without killing
1280
+ /cleanup --force - Kill orphan panes
1281
+ """
1282
+ force_kill = "--force" in args
1283
+
1284
+ # Get all panes in the commander session
1285
+ try:
1286
+ all_panes = self.instances.orchestrator.list_panes()
1287
+ except Exception as e:
1288
+ self._print(f"Error listing panes: {e}")
1289
+ return
1290
+
1291
+ # Get tracked instance pane targets
1292
+ tracked_instances = self.instances.list_instances()
1293
+ tracked_panes = {inst.pane_target for inst in tracked_instances}
1294
+
1295
+ # Find orphan panes (panes not in any tracked instance)
1296
+ orphan_panes = []
1297
+ for pane in all_panes:
1298
+ pane_id = pane["id"]
1299
+ session_pane_target = (
1300
+ f"{self.instances.orchestrator.session_name}:{pane_id}"
1301
+ )
1302
+
1303
+ # Skip if this pane is tracked
1304
+ if session_pane_target in tracked_panes:
1305
+ continue
1306
+
1307
+ orphan_panes.append((session_pane_target, pane["path"]))
1308
+
1309
+ if not orphan_panes:
1310
+ self._print("No orphan panes found.")
1311
+ return
1312
+
1313
+ # Display orphan panes
1314
+ self._print(f"Found {len(orphan_panes)} orphan pane(s):")
1315
+ for target, path in orphan_panes:
1316
+ self._print(f" - {target} ({path})")
1317
+
1318
+ if force_kill:
1319
+ # Kill orphan panes
1320
+ killed_count = 0
1321
+ for target, path in orphan_panes:
1322
+ try:
1323
+ self.instances.orchestrator.kill_pane(target)
1324
+ killed_count += 1
1325
+ self._print(f" Killed: {target}")
1326
+ except Exception as e:
1327
+ self._print(f" Error killing {target}: {e}")
1328
+
1329
+ self._print(f"\nCleaned up {killed_count} orphan pane(s).")
1330
+ else:
1331
+ self._print("\nUse '/cleanup --force' to remove these panes.")
1332
+
1333
+ def _print_token_status(
1334
+ self, name: str, status: dict, stored: bool = False
1335
+ ) -> None:
1336
+ """Print token status information.
1337
+
1338
+ Args:
1339
+ name: Service name.
1340
+ status: Status dict with token info.
1341
+ stored: Whether tokens are stored.
1342
+ """
1343
+ self._print(f"OAuth Status for '{name}':")
1344
+ self._print(f" Stored: {'Yes' if stored else 'No'}")
1345
+
1346
+ if status.get("valid"):
1347
+ self._print(" Status: Valid")
1348
+ else:
1349
+ self._print(" Status: Invalid/Expired")
1350
+
1351
+ if status.get("expires_at"):
1352
+ self._print(f" Expires: {status['expires_at']}")
1353
+
1354
+ if status.get("scopes"):
1355
+ self._print(f" Scopes: {', '.join(status['scopes'])}")
1356
+
1357
+ # Helper methods for LLM-extracted arguments
1358
+
1359
+ async def _cmd_register_from_args(self, args: dict) -> None:
1360
+ """Handle register command from LLM-extracted args.
1361
+
1362
+ Args:
1363
+ args: Dict with optional 'path', 'framework', 'name' keys.
1364
+ """
1365
+ path = args.get("path")
1366
+ framework = args.get("framework")
1367
+ name = args.get("name")
1368
+
1369
+ if not all([path, framework, name]):
1370
+ self._print("I need the path, framework, and name to register an instance.")
1371
+ self._print("Example: 'register ~/myproject as myapp using mpm'")
1372
+ return
1373
+
1374
+ await self._cmd_register([path, framework, name])
1375
+
1376
+ async def _cmd_start_from_args(self, args: dict) -> None:
1377
+ """Handle start command from LLM-extracted args.
1378
+
1379
+ Args:
1380
+ args: Dict with optional 'name' key.
1381
+ """
1382
+ name = args.get("name")
1383
+ if not name:
1384
+ # Try to infer from connected instance or list available
1385
+ instances = self.instances.list_instances()
1386
+ if len(instances) == 1:
1387
+ name = instances[0].name
1388
+ else:
1389
+ self._print("Which instance should I start?")
1390
+ await self._cmd_list([])
1391
+ return
1392
+
1393
+ await self._cmd_start([name])
1394
+
1395
+ async def _cmd_stop_from_args(self, args: dict) -> None:
1396
+ """Handle stop command from LLM-extracted args.
1397
+
1398
+ Args:
1399
+ args: Dict with optional 'name' key.
1400
+ """
1401
+ name = args.get("name")
1402
+ if not name:
1403
+ # Try to use connected instance
1404
+ if self.session.context.is_connected:
1405
+ name = self.session.context.connected_instance
1406
+ else:
1407
+ self._print("Which instance should I stop?")
1408
+ await self._cmd_list([])
1409
+ return
1410
+
1411
+ await self._cmd_stop([name])
1412
+
1413
+ async def _cmd_connect_from_args(self, args: dict) -> None:
1414
+ """Handle connect command from LLM-extracted args.
1415
+
1416
+ Args:
1417
+ args: Dict with optional 'name' key.
1418
+ """
1419
+ name = args.get("name")
1420
+ if not name:
1421
+ instances = self.instances.list_instances()
1422
+ if len(instances) == 1:
1423
+ name = instances[0].name
1424
+ else:
1425
+ self._print("Which instance should I connect to?")
1426
+ await self._cmd_list([])
1427
+ return
1428
+
1429
+ await self._cmd_connect([name])
1430
+
1431
+ async def _cmd_message_instance(self, target: str, message: str) -> None:
1432
+ """Send message to specific instance without connecting (non-blocking).
1433
+
1434
+ Enqueues the request and returns immediately. Response will appear
1435
+ above the prompt when it arrives.
1436
+
1437
+ Args:
1438
+ target: Instance name to message.
1439
+ message: Message to send.
1440
+ """
1441
+ # Check if instance exists
1442
+ inst = self.instances.get_instance(target)
1443
+ if not inst:
1444
+ # Try to start if registered
1445
+ try:
1446
+ inst = await self.instances.start_by_name(target)
1447
+ if inst:
1448
+ # Spawn background startup task (non-blocking)
1449
+ self._spawn_startup_task(target, auto_connect=False, timeout=30)
1450
+ self._print(
1451
+ f"Starting '{target}'... message will be sent when ready"
1452
+ )
1453
+ except Exception:
1454
+ inst = None
1455
+
1456
+ if not inst:
1457
+ self._print(
1458
+ f"Instance '{target}' not found. Use /list to see instances."
1459
+ )
1460
+ return
1461
+
1462
+ # Create and enqueue request (non-blocking)
1463
+ request = PendingRequest(
1464
+ id=str(uuid.uuid4())[:8],
1465
+ target=target,
1466
+ message=message,
1467
+ )
1468
+ self._pending_requests[request.id] = request
1469
+ await self._request_queue.put(request)
1470
+
1471
+ # Return immediately - response will be handled by _process_responses
1472
+
1473
+ def _display_response(self, instance_name: str, response: str) -> None:
1474
+ """Display response from instance above prompt.
1475
+
1476
+ Args:
1477
+ instance_name: Name of the instance that responded.
1478
+ response: Response content.
1479
+ """
1480
+ # Summarize if too long
1481
+ summary = response[:100] + "..." if len(response) > 100 else response
1482
+ summary = summary.replace("\n", " ")
1483
+ print(f"\n@{instance_name}: {summary}")
1484
+
1485
+ async def _send_to_instance(self, message: str) -> None:
1486
+ """Send natural language to connected instance (non-blocking).
1487
+
1488
+ Enqueues the request and returns immediately. Response will appear
1489
+ above the prompt when it arrives.
1490
+
1491
+ Args:
1492
+ message: User message to send.
1493
+ """
1494
+ # Check if instance is connected and ready
1495
+ if not self.session.context.is_connected:
1496
+ self._print("Not connected to any instance. Use 'connect <name>' first.")
1497
+ return
1498
+
1499
+ name = self.session.context.connected_instance
1500
+ inst = self.instances.get_instance(name)
1501
+ if not inst:
1502
+ self._print(f"Instance '{name}' no longer exists")
1503
+ self.session.disconnect()
1504
+ return
1505
+
1506
+ # Create and enqueue request (non-blocking)
1507
+ request = PendingRequest(
1508
+ id=str(uuid.uuid4())[:8],
1509
+ target=name,
1510
+ message=message,
1511
+ )
1512
+ self._pending_requests[request.id] = request
1513
+ await self._request_queue.put(request)
1514
+ self.session.add_user_message(message)
1515
+
1516
+ # Return immediately - response will be handled by _process_responses
1517
+
1518
+ async def _process_responses(self) -> None:
1519
+ """Background task that processes queued requests and waits for responses."""
1520
+ while self._running:
1521
+ try:
1522
+ # Get next request from queue (with timeout to allow checking _running)
1523
+ try:
1524
+ request = await asyncio.wait_for(
1525
+ self._request_queue.get(), timeout=0.5
1526
+ )
1527
+ except asyncio.TimeoutError:
1528
+ continue
1529
+
1530
+ # Update status and send to instance
1531
+ request.status = RequestStatus.SENDING
1532
+ self._render_pending_status()
1533
+
1534
+ inst = self.instances.get_instance(request.target)
1535
+ if not inst:
1536
+ request.status = RequestStatus.ERROR
1537
+ request.error = f"Instance '{request.target}' no longer exists"
1538
+ print(f"\n[{request.target}] {request.error}")
1539
+ continue
1540
+
1541
+ # Send to instance
1542
+ await self.instances.send_to_instance(request.target, request.message)
1543
+ request.status = RequestStatus.WAITING
1544
+ self._render_pending_status()
1545
+
1546
+ # Wait for response
1547
+ if self.relay:
1548
+ try:
1549
+ output = await self.relay.get_latest_output(
1550
+ request.target, inst.pane_target, context=request.message
1551
+ )
1552
+ request.status = RequestStatus.COMPLETED
1553
+ request.response = output
1554
+
1555
+ # Display response above prompt
1556
+ self._display_response(request.target, output)
1557
+ self.session.add_assistant_message(output)
1558
+ except Exception as e:
1559
+ request.status = RequestStatus.ERROR
1560
+ request.error = str(e)
1561
+ print(f"\n[{request.target}] Error: {e}")
1562
+
1563
+ # Remove from pending after a short delay
1564
+ await asyncio.sleep(0.5)
1565
+ self._pending_requests.pop(request.id, None)
1566
+
1567
+ except asyncio.CancelledError:
1568
+ break
1569
+ except Exception as e:
1570
+ print(f"\nResponse processor error: {e}")
1571
+
1572
+ def _render_pending_status(self) -> None:
1573
+ """Render pending request status above the prompt."""
1574
+ pending = [
1575
+ r
1576
+ for r in self._pending_requests.values()
1577
+ if r.status not in (RequestStatus.COMPLETED, RequestStatus.ERROR)
1578
+ ]
1579
+ if not pending:
1580
+ return
1581
+
1582
+ # Build status line
1583
+ status_parts = []
1584
+ for req in pending:
1585
+ elapsed = req.elapsed_seconds()
1586
+ status_indicator = {
1587
+ RequestStatus.QUEUED: "...",
1588
+ RequestStatus.SENDING: ">>>",
1589
+ RequestStatus.WAITING: "...",
1590
+ RequestStatus.STARTING: "...",
1591
+ }.get(req.status, "?")
1592
+ status_parts.append(
1593
+ f"{status_indicator} [{req.target}] {req.display_message(30)} ({elapsed}s)"
1594
+ )
1595
+
1596
+ # Print above prompt (patch_stdout handles cursor positioning)
1597
+ for part in status_parts:
1598
+ print(f"\r\033[K{part}")
1599
+
1600
+ def _on_instance_event(self, event: "Event") -> None:
1601
+ """Handle instance lifecycle events with interrupt display.
1602
+
1603
+ Args:
1604
+ event: The event to handle.
1605
+ """
1606
+ if event.type == EventType.INSTANCE_STARTING:
1607
+ print(f"\n[Starting] {event.title}")
1608
+ elif event.type == EventType.INSTANCE_READY:
1609
+ metadata = event.context or {}
1610
+ instance_name = metadata.get("instance_name", "")
1611
+
1612
+ # Mark instance as ready
1613
+ if instance_name:
1614
+ self._instance_ready[instance_name] = True
1615
+
1616
+ if metadata.get("timeout"):
1617
+ print(f"\n[Warning] {event.title} (startup timeout, may still work)")
1618
+ else:
1619
+ print(f"\n[Ready] {event.title}")
1620
+
1621
+ # Show ready notification based on whether this is the connected instance
1622
+ if (
1623
+ instance_name
1624
+ and instance_name == self.session.context.connected_instance
1625
+ ):
1626
+ print(f"\n({instance_name}) ready")
1627
+ elif instance_name:
1628
+ print(f" Use @{instance_name} or /connect {instance_name}")
1629
+ elif event.type == EventType.INSTANCE_ERROR:
1630
+ print(f"\n[Error] {event.title}: {event.content}")
1631
+
1632
+ def _get_prompt(self) -> str:
1633
+ """Get prompt string.
1634
+
1635
+ Returns:
1636
+ Prompt string for input, showing instance name when connected.
1637
+ """
1638
+ connected = self.session.context.connected_instance
1639
+ if connected:
1640
+ return f"Commander ({connected})> "
1641
+ return "Commander> "
1642
+
1643
+ def _print(self, msg: str) -> None:
1644
+ """Print message to console.
1645
+
1646
+ Args:
1647
+ msg: Message to print.
1648
+ """
1649
+ print(msg)
1650
+
1651
+ def _spawn_startup_task(
1652
+ self, name: str, auto_connect: bool = True, timeout: int = 30
1653
+ ) -> None:
1654
+ """Spawn a background task to wait for instance ready.
1655
+
1656
+ This returns immediately - the wait happens in the background.
1657
+ Prints status when starting and when complete.
1658
+
1659
+ Args:
1660
+ name: Instance name to wait for
1661
+ auto_connect: Whether to auto-connect when ready
1662
+ timeout: Maximum seconds to wait
1663
+ """
1664
+ # Print starting message (once)
1665
+ print(f"Waiting for '{name}' to be ready...")
1666
+
1667
+ # Spawn background task
1668
+ task = asyncio.create_task(
1669
+ self._wait_for_ready_background(name, auto_connect, timeout)
1670
+ )
1671
+ self._startup_tasks[name] = task
1672
+
1673
+ async def _wait_for_ready_background(
1674
+ self, name: str, auto_connect: bool, timeout: int
1675
+ ) -> None:
1676
+ """Background task that waits for instance ready.
1677
+
1678
+ Updates bottom toolbar with spinner animation, then prints result when done.
1679
+
1680
+ Args:
1681
+ name: Instance name to wait for
1682
+ auto_connect: Whether to auto-connect when ready
1683
+ timeout: Maximum seconds to wait
1684
+ """
1685
+ elapsed = 0.0
1686
+ interval = 0.1 # Update spinner every 100ms
1687
+ spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
1688
+ frame_idx = 0
1689
+
1690
+ try:
1691
+ while elapsed < timeout:
1692
+ inst = self.instances.get_instance(name)
1693
+ if inst and inst.ready:
1694
+ # Clear toolbar and print success
1695
+ self._toolbar_status = ""
1696
+ if self.prompt_session:
1697
+ self.prompt_session.app.invalidate()
1698
+ print(f"'{name}' ready ({int(elapsed)}s)")
1699
+
1700
+ if auto_connect:
1701
+ self.session.connect_to(name)
1702
+ print(f" Connected to '{name}'")
1703
+
1704
+ # Cleanup
1705
+ self._startup_tasks.pop(name, None)
1706
+ return
1707
+
1708
+ # Update toolbar with spinner frame
1709
+ frame = spinner_frames[frame_idx % len(spinner_frames)]
1710
+ self._toolbar_status = (
1711
+ f"{frame} Waiting for '{name}'... ({int(elapsed)}s)"
1712
+ )
1713
+ if self.prompt_session:
1714
+ self.prompt_session.app.invalidate()
1715
+ frame_idx += 1
1716
+
1717
+ await asyncio.sleep(interval)
1718
+ elapsed += interval
1719
+
1720
+ # Timeout - clear toolbar and show warning
1721
+ self._toolbar_status = ""
1722
+ if self.prompt_session:
1723
+ self.prompt_session.app.invalidate()
1724
+ print(f"'{name}' startup timeout ({timeout}s) - may still work")
1725
+
1726
+ # Still auto-connect on timeout (instance may become ready later)
1727
+ if auto_connect:
1728
+ self.session.connect_to(name)
1729
+ print(f" Connected to '{name}' (may not be fully ready)")
1730
+
1731
+ # Cleanup
1732
+ self._startup_tasks.pop(name, None)
1733
+
1734
+ except asyncio.CancelledError:
1735
+ self._toolbar_status = ""
1736
+ self._startup_tasks.pop(name, None)
1737
+ except Exception as e:
1738
+ self._toolbar_status = ""
1739
+ print(f"'{name}' startup error: {e}")
1740
+ self._startup_tasks.pop(name, None)
1741
+
1742
+ async def _wait_for_ready_with_spinner(self, name: str, timeout: int = 30) -> bool:
1743
+ """Wait for instance to be ready with animated spinner (BLOCKING).
1744
+
1745
+ NOTE: This method blocks. For non-blocking, use _spawn_startup_task().
1746
+
1747
+ Shows an animated waiting indicator that updates in place.
1748
+
1749
+ Args:
1750
+ name: Instance name to wait for
1751
+ timeout: Maximum seconds to wait
1752
+
1753
+ Returns:
1754
+ True if instance became ready, False on timeout
1755
+ """
1756
+ spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
1757
+ frame_idx = 0
1758
+ elapsed = 0.0
1759
+ interval = 0.1 # Update spinner every 100ms
1760
+
1761
+ while elapsed < timeout:
1762
+ inst = self.instances.get_instance(name)
1763
+ if inst and inst.ready:
1764
+ # Clear spinner line and show success
1765
+ sys.stdout.write(f"\r\033[K'{name}' ready\n")
1766
+ sys.stdout.flush()
1767
+ return True
1768
+
1769
+ # Show spinner with elapsed time
1770
+ frame = spinner_frames[frame_idx % len(spinner_frames)]
1771
+ sys.stdout.write(
1772
+ f"\r{frame} Waiting for '{name}' to be ready... ({int(elapsed)}s)"
1773
+ )
1774
+ sys.stdout.flush()
1775
+
1776
+ await asyncio.sleep(interval)
1777
+ elapsed += interval
1778
+ frame_idx += 1
1779
+
1780
+ # Timeout - clear spinner and show warning
1781
+ sys.stdout.write(f"\r\033[K'{name}' startup timeout (may still work)\n")
1782
+ sys.stdout.flush()
1783
+ return False
1784
+
1785
+ def _print_welcome(self) -> None:
1786
+ """Print welcome message."""
1787
+ print("╔══════════════════════════════════════════╗")
1788
+ print("║ MPM Commander - Interactive Mode ║")
1789
+ print("╚══════════════════════════════════════════╝")
1790
+ print("Type '/help' for commands, or natural language to chat.")
1791
+ print()
1792
+
1793
+ def _get_instance_names(self) -> list[str]:
1794
+ """Get list of instance names for autocomplete.
1795
+
1796
+ Returns:
1797
+ List of instance names (running and registered).
1798
+ """
1799
+ names: list[str] = []
1800
+
1801
+ # Running instances
1802
+ if self.instances:
1803
+ try:
1804
+ for inst in self.instances.list_instances():
1805
+ if inst.name not in names:
1806
+ names.append(inst.name)
1807
+ except Exception: # nosec B110 - Graceful fallback
1808
+ pass
1809
+
1810
+ # Registered instances from state store
1811
+ if self.instances and hasattr(self.instances, "_state_store"):
1812
+ try:
1813
+ state_store = self.instances._state_store
1814
+ if state_store:
1815
+ for name in state_store.load_instances():
1816
+ if name not in names:
1817
+ names.append(name)
1818
+ except Exception: # nosec B110 - Graceful fallback
1819
+ pass
1820
+
1821
+ return names