claude-mpm 5.4.85__py3-none-any.whl → 5.6.76__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +8 -5
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +109 -706
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/autotodos.py +566 -0
- claude_mpm/cli/commands/commander.py +216 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init/core.py +2 -2
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +128 -16
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +84 -1
- claude_mpm/cli/parsers/commander_parser.py +116 -0
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +345 -40
- claude_mpm/cli/startup_display.py +76 -7
- claude_mpm/cli/startup_logging.py +2 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +78 -0
- claude_mpm/commander/adapters/__init__.py +60 -0
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +288 -0
- claude_mpm/commander/adapters/claude_code.py +392 -0
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +121 -0
- claude_mpm/commander/api/errors.py +133 -0
- claude_mpm/commander/api/routes/__init__.py +8 -0
- claude_mpm/commander/api/routes/events.py +184 -0
- claude_mpm/commander/api/routes/inbox.py +171 -0
- claude_mpm/commander/api/routes/messages.py +148 -0
- claude_mpm/commander/api/routes/projects.py +271 -0
- claude_mpm/commander/api/routes/sessions.py +226 -0
- claude_mpm/commander/api/routes/work.py +296 -0
- claude_mpm/commander/api/schemas.py +186 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +149 -0
- claude_mpm/commander/chat/commands.py +124 -0
- claude_mpm/commander/chat/repl.py +1957 -0
- claude_mpm/commander/config.py +51 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +603 -0
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +392 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +233 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +57 -0
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/inbox/__init__.py +16 -0
- claude_mpm/commander/inbox/dedup.py +128 -0
- claude_mpm/commander/inbox/inbox.py +224 -0
- claude_mpm/commander/inbox/models.py +70 -0
- claude_mpm/commander/instance_manager.py +868 -0
- claude_mpm/commander/llm/__init__.py +6 -0
- claude_mpm/commander/llm/openrouter_client.py +167 -0
- claude_mpm/commander/llm/summarizer.py +70 -0
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +127 -0
- claude_mpm/commander/models/project.py +162 -0
- claude_mpm/commander/models/work.py +214 -0
- claude_mpm/commander/parsing/__init__.py +20 -0
- claude_mpm/commander/parsing/extractor.py +132 -0
- claude_mpm/commander/parsing/output_parser.py +270 -0
- claude_mpm/commander/parsing/patterns.py +100 -0
- claude_mpm/commander/persistence/__init__.py +11 -0
- claude_mpm/commander/persistence/event_store.py +274 -0
- claude_mpm/commander/persistence/state_store.py +403 -0
- claude_mpm/commander/persistence/work_store.py +164 -0
- claude_mpm/commander/polling/__init__.py +13 -0
- claude_mpm/commander/polling/event_detector.py +104 -0
- claude_mpm/commander/polling/output_buffer.py +49 -0
- claude_mpm/commander/polling/output_poller.py +153 -0
- claude_mpm/commander/project_session.py +268 -0
- claude_mpm/commander/proxy/__init__.py +12 -0
- claude_mpm/commander/proxy/formatter.py +89 -0
- claude_mpm/commander/proxy/output_handler.py +191 -0
- claude_mpm/commander/proxy/relay.py +155 -0
- claude_mpm/commander/registry.py +410 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +346 -0
- claude_mpm/commander/session/__init__.py +6 -0
- claude_mpm/commander/session/context.py +81 -0
- claude_mpm/commander/session/manager.py +59 -0
- claude_mpm/commander/tmux_orchestrator.py +362 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +207 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +241 -0
- claude_mpm/commander/workflow/notifier.py +146 -0
- claude_mpm/commands/mpm-config.md +8 -0
- claude_mpm/commands/mpm-doctor.md +8 -0
- claude_mpm/commands/mpm-help.md +8 -0
- claude_mpm/commands/mpm-init.md +8 -0
- claude_mpm/commands/mpm-monitor.md +8 -0
- claude_mpm/commands/mpm-organize.md +8 -0
- claude_mpm/commands/mpm-postmortem.md +8 -0
- claude_mpm/commands/mpm-session-resume.md +9 -1
- claude_mpm/commands/mpm-status.md +8 -0
- claude_mpm/commands/mpm-ticket-view.md +8 -0
- claude_mpm/commands/mpm-version.md +8 -0
- claude_mpm/commands/mpm.md +8 -0
- claude_mpm/config/agent_presets.py +8 -7
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +35 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +53 -4
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +39 -13
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +52 -12
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → 1WZnGYqX.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 67pF3qNn.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → 6RxdMKe4.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → 8cZrfX0h.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → 9a6T2nm-.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → B443AUzu.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{RJiighC3.js → B8AwtY2H.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → BF15LAsF.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → BRcwIQNr.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BV6nKitt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → BViJ8lZt.js} +5 -5
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BcQ-Q0FE.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → Bpyvgze_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → C3rbW_a-.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C8WYN38h.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → C9I8FlXH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → CIQcWgO2.js} +3 -3
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CIctN7YN.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CKrS_JZW.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → CR6P9C4A.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → CRRR9MD_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → CSXtMOf0.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → CT-sbxSk.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CWm6DJsp.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → CpqQ1Kzn.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4B-KCzX.js → D2nGpDRe.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → D9iCMida.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BofRWZRR.js → D9ykgMoY.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → DL2Ldur1.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DPfltzjH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → DUliQN2b.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DXlhR01x.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → D_lyTybS.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → DngoTTgh.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → DqkmHtDC.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DsDh8EYs.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → DypDmXgd.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → IPYC-LnN.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → JpevfAFt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → Zxy7qc-l.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → qtd3IeO4.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → ulBFON_C.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → wQVh1CoA.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.Dr7t0z2J.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.RgBboRvH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.DG-KkbDf.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
- claude_mpm/dashboard/static/svelte-build/index.html +9 -9
- claude_mpm/experimental/cli_enhancements.py +2 -1
- claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
- claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +485 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +466 -136
- claude_mpm/hooks/claude_hooks/hook_handler.py +204 -104
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +291 -59
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +43 -60
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +41 -26
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +38 -105
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +75 -77
- claude_mpm/hooks/session_resume_hook.py +89 -1
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +22 -15
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +46 -19
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/cache_git_manager.py +1 -1
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
- claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/cli/__init__.py +3 -0
- claude_mpm/services/cli/incremental_pause_manager.py +561 -0
- claude_mpm/services/cli/session_resume_helper.py +10 -2
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/delegation_detector.py +175 -0
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
- claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
- claude_mpm/services/diagnostics/models.py +14 -1
- claude_mpm/services/event_log.py +325 -0
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/infrastructure/__init__.py +4 -0
- claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
- claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/daemon_manager.py +15 -4
- claude_mpm/services/monitor/management/lifecycle.py +8 -2
- claude_mpm/services/monitor/server.py +111 -16
- claude_mpm/services/pm_skills_deployer.py +261 -87
- claude_mpm/services/skills/git_skill_source_manager.py +130 -10
- claude_mpm/services/skills/selective_skill_deployer.py +142 -16
- claude_mpm/services/skills/skill_discovery_service.py +74 -4
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/services/socketio/handlers/hook.py +14 -7
- claude_mpm/services/socketio/server/main.py +12 -4
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
- claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
- claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
- claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
- claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
- claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
- claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
- claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
- claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
- claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
- claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
- claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
- claude_mpm/skills/bundled/pm/{pm-teaching-mode → mpm-teaching-mode}/SKILL.md +2 -2
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- claude_mpm/skills/registry.py +295 -90
- claude_mpm/skills/skill_manager.py +4 -4
- claude_mpm-5.6.76.dist-info/METADATA +416 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +312 -175
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +0 -24
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +0 -323
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +0 -1
- claude_mpm-5.4.85.dist-info/METADATA +0 -1023
- /claude_mpm/skills/bundled/pm/{pm-bug-reporting/pm-bug-reporting.md → mpm-bug-reporting/SKILL.md} +0 -0
- /claude_mpm/skills/bundled/pm/{pm-delegation-patterns → mpm-delegation-patterns}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-git-file-tracking → mpm-git-file-tracking}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-pr-workflow → mpm-pr-workflow}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-ticketing-integration → mpm-ticketing-integration}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-verification-protocols → mpm-verification-protocols}/SKILL.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""EventManager for MPM Commander inbox system.
|
|
2
|
+
|
|
3
|
+
Manages event lifecycle, inbox queries, and project event tracking.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import uuid
|
|
10
|
+
from asyncio import Queue
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from ..models.events import (
|
|
15
|
+
DEFAULT_PRIORITIES,
|
|
16
|
+
Event,
|
|
17
|
+
EventPriority,
|
|
18
|
+
EventStatus,
|
|
19
|
+
EventType,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EventManager:
|
|
26
|
+
"""Manages event lifecycle and inbox queries.
|
|
27
|
+
|
|
28
|
+
Thread-safe event storage with support for:
|
|
29
|
+
- Creating events with automatic priority assignment
|
|
30
|
+
- Querying pending events by project or globally
|
|
31
|
+
- Inbox sorting by priority then timestamp
|
|
32
|
+
- Responding to, acknowledging, or dismissing events
|
|
33
|
+
- Detecting blocking events
|
|
34
|
+
- Clearing project events
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
manager = EventManager()
|
|
38
|
+
event = manager.create(
|
|
39
|
+
project_id="proj_123",
|
|
40
|
+
event_type=EventType.DECISION_NEEDED,
|
|
41
|
+
title="Choose deployment target",
|
|
42
|
+
options=["staging", "production"]
|
|
43
|
+
)
|
|
44
|
+
inbox = manager.get_inbox()
|
|
45
|
+
manager.respond(event.id, "staging")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
"""Initialize empty event storage."""
|
|
50
|
+
self._events: Dict[str, Event] = {}
|
|
51
|
+
self._project_index: Dict[str, List[str]] = {} # project_id -> event_ids
|
|
52
|
+
self._lock = threading.RLock()
|
|
53
|
+
self._subscribers: Dict[EventType, List[Callable]] = {}
|
|
54
|
+
self._event_queue: Queue = Queue()
|
|
55
|
+
|
|
56
|
+
def create(
|
|
57
|
+
self,
|
|
58
|
+
project_id: str,
|
|
59
|
+
event_type: EventType,
|
|
60
|
+
title: str,
|
|
61
|
+
content: str = "",
|
|
62
|
+
session_id: Optional[str] = None,
|
|
63
|
+
priority: Optional[EventPriority] = None,
|
|
64
|
+
options: Optional[List[str]] = None,
|
|
65
|
+
context: Optional[Dict[str, Any]] = None,
|
|
66
|
+
) -> Event:
|
|
67
|
+
"""Create and queue a new event.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
project_id: ID of project raising this event
|
|
71
|
+
event_type: Type of event
|
|
72
|
+
title: Short summary
|
|
73
|
+
content: Detailed description
|
|
74
|
+
session_id: Optional session ID
|
|
75
|
+
priority: Optional priority (uses default if not specified)
|
|
76
|
+
options: For DECISION_NEEDED, list of choices
|
|
77
|
+
context: Additional structured data
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The created Event
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
event = manager.create(
|
|
84
|
+
project_id="proj_123",
|
|
85
|
+
event_type=EventType.ERROR,
|
|
86
|
+
title="Database connection failed",
|
|
87
|
+
content="Could not connect to postgres://..."
|
|
88
|
+
)
|
|
89
|
+
"""
|
|
90
|
+
with self._lock:
|
|
91
|
+
event_id = f"evt_{uuid.uuid4().hex[:12]}"
|
|
92
|
+
|
|
93
|
+
# Use default priority if not specified
|
|
94
|
+
if priority is None:
|
|
95
|
+
priority = DEFAULT_PRIORITIES.get(event_type, EventPriority.NORMAL)
|
|
96
|
+
|
|
97
|
+
event = Event(
|
|
98
|
+
id=event_id,
|
|
99
|
+
project_id=project_id,
|
|
100
|
+
session_id=session_id,
|
|
101
|
+
type=event_type,
|
|
102
|
+
priority=priority,
|
|
103
|
+
title=title,
|
|
104
|
+
content=content,
|
|
105
|
+
context=context or {},
|
|
106
|
+
options=options,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self._events[event_id] = event
|
|
110
|
+
|
|
111
|
+
# Index by project
|
|
112
|
+
if project_id not in self._project_index:
|
|
113
|
+
self._project_index[project_id] = []
|
|
114
|
+
self._project_index[project_id].append(event_id)
|
|
115
|
+
|
|
116
|
+
logger.info("Created event %s: [%s] %s", event_id, event_type.value, title)
|
|
117
|
+
return event
|
|
118
|
+
|
|
119
|
+
def add_event(self, event: Event) -> None:
|
|
120
|
+
"""Add existing event to manager (for loading from persistence).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
event: Event instance to add
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
# Load events from disk and add to manager
|
|
127
|
+
for event in loaded_events:
|
|
128
|
+
manager.add_event(event)
|
|
129
|
+
"""
|
|
130
|
+
with self._lock:
|
|
131
|
+
self._events[event.id] = event
|
|
132
|
+
|
|
133
|
+
# Index by project
|
|
134
|
+
if event.project_id not in self._project_index:
|
|
135
|
+
self._project_index[event.project_id] = []
|
|
136
|
+
if event.id not in self._project_index[event.project_id]:
|
|
137
|
+
self._project_index[event.project_id].append(event.id)
|
|
138
|
+
|
|
139
|
+
logger.debug("Added event %s to manager", event.id)
|
|
140
|
+
|
|
141
|
+
def get(self, event_id: str) -> Optional[Event]:
|
|
142
|
+
"""Get event by ID.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
event_id: Unique event identifier
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Event if found, None otherwise
|
|
149
|
+
"""
|
|
150
|
+
return self._events.get(event_id)
|
|
151
|
+
|
|
152
|
+
def get_pending(self, project_id: Optional[str] = None) -> List[Event]:
|
|
153
|
+
"""Get all pending events, optionally filtered by project.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
project_id: If provided, only return events for this project
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of pending events (unsorted)
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
# Get all pending events
|
|
163
|
+
all_pending = manager.get_pending()
|
|
164
|
+
|
|
165
|
+
# Get pending events for one project
|
|
166
|
+
project_pending = manager.get_pending("proj_123")
|
|
167
|
+
"""
|
|
168
|
+
with self._lock:
|
|
169
|
+
if project_id:
|
|
170
|
+
event_ids = self._project_index.get(project_id, [])
|
|
171
|
+
events = [self._events[eid] for eid in event_ids if eid in self._events]
|
|
172
|
+
else:
|
|
173
|
+
events = list(self._events.values())
|
|
174
|
+
|
|
175
|
+
return [e for e in events if e.status == EventStatus.PENDING]
|
|
176
|
+
|
|
177
|
+
def get_inbox(self, limit: int = 50) -> List[Event]:
|
|
178
|
+
"""Get events for inbox, sorted by priority then time.
|
|
179
|
+
|
|
180
|
+
Sorting order:
|
|
181
|
+
1. Priority: CRITICAL > HIGH > NORMAL > LOW > INFO
|
|
182
|
+
2. Within same priority: oldest first (created_at ascending)
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
limit: Maximum number of events to return
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Sorted list of pending events, limited to `limit` items
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
inbox = manager.get_inbox(limit=20)
|
|
192
|
+
for event in inbox:
|
|
193
|
+
print(f"{event.priority.value}: {event.title}")
|
|
194
|
+
"""
|
|
195
|
+
with self._lock:
|
|
196
|
+
pending = [
|
|
197
|
+
e for e in self._events.values() if e.status == EventStatus.PENDING
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
# Sort by priority (CRITICAL first) then by created_at (oldest first)
|
|
201
|
+
priority_order = [
|
|
202
|
+
EventPriority.CRITICAL,
|
|
203
|
+
EventPriority.HIGH,
|
|
204
|
+
EventPriority.NORMAL,
|
|
205
|
+
EventPriority.LOW,
|
|
206
|
+
EventPriority.INFO,
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
def sort_key(event: Event) -> tuple[int, datetime]:
|
|
210
|
+
pri_idx = (
|
|
211
|
+
priority_order.index(event.priority)
|
|
212
|
+
if event.priority in priority_order
|
|
213
|
+
else 99
|
|
214
|
+
)
|
|
215
|
+
return (pri_idx, event.created_at)
|
|
216
|
+
|
|
217
|
+
sorted_events = sorted(pending, key=sort_key)
|
|
218
|
+
return sorted_events[:limit]
|
|
219
|
+
|
|
220
|
+
def respond(self, event_id: str, response: str) -> Event:
|
|
221
|
+
"""Record response to event and mark as resolved.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
event_id: ID of event to respond to
|
|
225
|
+
response: User's response text
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The updated Event
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
KeyError: If event_id not found
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
event = manager.respond("evt_abc123", "Deploy to staging")
|
|
235
|
+
"""
|
|
236
|
+
with self._lock:
|
|
237
|
+
event = self._events.get(event_id)
|
|
238
|
+
if not event:
|
|
239
|
+
raise KeyError(f"Event not found: {event_id}")
|
|
240
|
+
|
|
241
|
+
event.response = response
|
|
242
|
+
event.responded_at = datetime.now(timezone.utc)
|
|
243
|
+
event.status = EventStatus.RESOLVED
|
|
244
|
+
|
|
245
|
+
logger.info("Responded to event %s: %s", event_id, response[:50])
|
|
246
|
+
return event
|
|
247
|
+
|
|
248
|
+
def dismiss(self, event_id: str) -> Event:
|
|
249
|
+
"""Mark event as dismissed without providing a response.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
event_id: ID of event to dismiss
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The updated Event
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
KeyError: If event_id not found
|
|
259
|
+
|
|
260
|
+
Example:
|
|
261
|
+
manager.dismiss("evt_abc123")
|
|
262
|
+
"""
|
|
263
|
+
with self._lock:
|
|
264
|
+
event = self._events.get(event_id)
|
|
265
|
+
if not event:
|
|
266
|
+
raise KeyError(f"Event not found: {event_id}")
|
|
267
|
+
|
|
268
|
+
event.status = EventStatus.DISMISSED
|
|
269
|
+
|
|
270
|
+
logger.info("Dismissed event %s", event_id)
|
|
271
|
+
return event
|
|
272
|
+
|
|
273
|
+
def acknowledge(self, event_id: str) -> Event:
|
|
274
|
+
"""Mark event as seen but not resolved yet.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
event_id: ID of event to acknowledge
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The updated Event
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
KeyError: If event_id not found
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
manager.acknowledge("evt_abc123")
|
|
287
|
+
"""
|
|
288
|
+
with self._lock:
|
|
289
|
+
event = self._events.get(event_id)
|
|
290
|
+
if not event:
|
|
291
|
+
raise KeyError(f"Event not found: {event_id}")
|
|
292
|
+
|
|
293
|
+
event.status = EventStatus.ACKNOWLEDGED
|
|
294
|
+
|
|
295
|
+
logger.info("Acknowledged event %s", event_id)
|
|
296
|
+
return event
|
|
297
|
+
|
|
298
|
+
def get_blocking_events(self, project_id: Optional[str] = None) -> List[Event]:
|
|
299
|
+
"""Get events that are blocking progress.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
project_id: If provided, include project-scoped blocking events
|
|
303
|
+
for this project. Always includes global blocking events.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
List of blocking events
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
# Get all blocking events (global scope only)
|
|
310
|
+
blockers = manager.get_blocking_events()
|
|
311
|
+
|
|
312
|
+
# Get blocking events for specific project (global + project scope)
|
|
313
|
+
blockers = manager.get_blocking_events("proj_123")
|
|
314
|
+
"""
|
|
315
|
+
with self._lock:
|
|
316
|
+
pending = self.get_pending(project_id)
|
|
317
|
+
return [e for e in pending if e.is_blocking]
|
|
318
|
+
|
|
319
|
+
def clear_project_events(self, project_id: str) -> int:
|
|
320
|
+
"""Clear all events for a project.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
project_id: ID of project whose events should be cleared
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Number of events removed
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
removed = manager.clear_project_events("proj_123")
|
|
330
|
+
print(f"Cleared {removed} events")
|
|
331
|
+
"""
|
|
332
|
+
with self._lock:
|
|
333
|
+
event_ids = self._project_index.pop(project_id, [])
|
|
334
|
+
for eid in event_ids:
|
|
335
|
+
self._events.pop(eid, None)
|
|
336
|
+
return len(event_ids)
|
|
337
|
+
|
|
338
|
+
def subscribe(self, event_type: EventType, callback: Callable) -> None:
|
|
339
|
+
"""Subscribe callback to event type.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
event_type: Type of event to subscribe to
|
|
343
|
+
callback: Function to call when event occurs (sync or async)
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
def on_error(event):
|
|
347
|
+
print(f"Error: {event.title}")
|
|
348
|
+
|
|
349
|
+
manager.subscribe(EventType.ERROR, on_error)
|
|
350
|
+
"""
|
|
351
|
+
if event_type not in self._subscribers:
|
|
352
|
+
self._subscribers[event_type] = []
|
|
353
|
+
self._subscribers[event_type].append(callback)
|
|
354
|
+
|
|
355
|
+
def unsubscribe(self, event_type: EventType, callback: Callable) -> None:
|
|
356
|
+
"""Unsubscribe callback from event type.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
event_type: Type of event to unsubscribe from
|
|
360
|
+
callback: Function to remove from subscribers
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
manager.unsubscribe(EventType.ERROR, on_error)
|
|
364
|
+
"""
|
|
365
|
+
if (
|
|
366
|
+
event_type in self._subscribers
|
|
367
|
+
and callback in self._subscribers[event_type]
|
|
368
|
+
):
|
|
369
|
+
self._subscribers[event_type].remove(callback)
|
|
370
|
+
|
|
371
|
+
async def emit(self, event: Event) -> None:
|
|
372
|
+
"""Emit event to all subscribers.
|
|
373
|
+
|
|
374
|
+
Queues the event and notifies all subscribed callbacks.
|
|
375
|
+
Supports both sync and async callbacks.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
event: Event to emit
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
await manager.emit(event)
|
|
382
|
+
"""
|
|
383
|
+
await self._event_queue.put(event)
|
|
384
|
+
if event.type in self._subscribers:
|
|
385
|
+
for callback in self._subscribers[event.type]:
|
|
386
|
+
try:
|
|
387
|
+
if asyncio.iscoroutinefunction(callback):
|
|
388
|
+
await callback(event)
|
|
389
|
+
else:
|
|
390
|
+
callback(event)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Subscriber callback error: {e}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Framework abstractions for AI coding assistants."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseFramework, InstanceInfo
|
|
4
|
+
from .claude_code import ClaudeCodeFramework
|
|
5
|
+
from .mpm import MPMFramework
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BaseFramework",
|
|
9
|
+
"ClaudeCodeFramework",
|
|
10
|
+
"InstanceInfo",
|
|
11
|
+
"MPMFramework",
|
|
12
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Base framework abstraction for different AI coding assistants."""
|
|
2
|
+
|
|
3
|
+
import subprocess # nosec B404 - Required for git operations
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class InstanceInfo:
|
|
12
|
+
"""Information about a running framework instance.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
name: Instance name (e.g., "myapp")
|
|
16
|
+
project_path: Path to project directory
|
|
17
|
+
framework: Framework identifier (e.g., "cc", "mpm")
|
|
18
|
+
tmux_session: Tmux session name
|
|
19
|
+
pane_target: Tmux pane target (e.g., "%1")
|
|
20
|
+
git_branch: Current git branch if project is a git repo
|
|
21
|
+
git_status: Git status summary if project is a git repo
|
|
22
|
+
connected: Whether instance has an active adapter connection
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> info = InstanceInfo(
|
|
26
|
+
... name="myapp",
|
|
27
|
+
... project_path=Path("/Users/user/myapp"),
|
|
28
|
+
... framework="cc",
|
|
29
|
+
... tmux_session="mpm-commander",
|
|
30
|
+
... pane_target="%1",
|
|
31
|
+
... git_branch="main",
|
|
32
|
+
... git_status="clean",
|
|
33
|
+
... connected=True
|
|
34
|
+
... )
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
project_path: Path
|
|
39
|
+
framework: str
|
|
40
|
+
tmux_session: str
|
|
41
|
+
pane_target: str
|
|
42
|
+
git_branch: Optional[str] = None
|
|
43
|
+
git_status: Optional[str] = None
|
|
44
|
+
connected: bool = False
|
|
45
|
+
ready: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class RegisteredInstance:
|
|
50
|
+
"""Persistent instance configuration (survives daemon restart).
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
name: Instance identifier
|
|
54
|
+
path: Original project directory path (stored as string for JSON)
|
|
55
|
+
framework: Framework identifier ("cc" or "mpm")
|
|
56
|
+
registered_at: ISO timestamp when instance was registered
|
|
57
|
+
worktree_path: Path to git worktree (if using worktree isolation)
|
|
58
|
+
worktree_branch: Branch name in the worktree
|
|
59
|
+
use_worktree: Whether worktree isolation is enabled
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> instance = RegisteredInstance(
|
|
63
|
+
... name="myapp",
|
|
64
|
+
... path="/Users/user/myapp",
|
|
65
|
+
... framework="cc",
|
|
66
|
+
... registered_at="2024-01-15T10:30:00"
|
|
67
|
+
... )
|
|
68
|
+
>>> instance.to_dict()
|
|
69
|
+
{'name': 'myapp', 'path': '/Users/user/myapp', 'framework': 'cc', ...}
|
|
70
|
+
>>> instance.working_path
|
|
71
|
+
'/Users/user/myapp'
|
|
72
|
+
|
|
73
|
+
>>> # With worktree enabled
|
|
74
|
+
>>> instance = RegisteredInstance(
|
|
75
|
+
... name="myapp",
|
|
76
|
+
... path="/Users/user/myapp",
|
|
77
|
+
... framework="cc",
|
|
78
|
+
... registered_at="2024-01-15T10:30:00",
|
|
79
|
+
... worktree_path="/Users/user/.mpm/worktrees/myapp",
|
|
80
|
+
... worktree_branch="feature/new-feature",
|
|
81
|
+
... use_worktree=True
|
|
82
|
+
... )
|
|
83
|
+
>>> instance.working_path
|
|
84
|
+
'/Users/user/.mpm/worktrees/myapp'
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
name: str
|
|
88
|
+
path: str # Original project path
|
|
89
|
+
framework: str
|
|
90
|
+
registered_at: str
|
|
91
|
+
# Worktree fields
|
|
92
|
+
worktree_path: Optional[str] = None # Path to worktree (if using)
|
|
93
|
+
worktree_branch: Optional[str] = None # Branch in worktree
|
|
94
|
+
use_worktree: bool = False # Whether worktree is enabled
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
"""Serialize for JSON storage."""
|
|
98
|
+
return {
|
|
99
|
+
"name": self.name,
|
|
100
|
+
"path": self.path,
|
|
101
|
+
"framework": self.framework,
|
|
102
|
+
"registered_at": self.registered_at,
|
|
103
|
+
"worktree_path": self.worktree_path,
|
|
104
|
+
"worktree_branch": self.worktree_branch,
|
|
105
|
+
"use_worktree": self.use_worktree,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, data: dict) -> "RegisteredInstance":
|
|
110
|
+
"""Deserialize from JSON."""
|
|
111
|
+
return cls(
|
|
112
|
+
name=data["name"],
|
|
113
|
+
path=data["path"],
|
|
114
|
+
framework=data["framework"],
|
|
115
|
+
registered_at=data.get("registered_at", ""),
|
|
116
|
+
worktree_path=data.get("worktree_path"),
|
|
117
|
+
worktree_branch=data.get("worktree_branch"),
|
|
118
|
+
use_worktree=data.get("use_worktree", False),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def working_path(self) -> str:
|
|
123
|
+
"""Get the actual working path (worktree or original).
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The worktree path if worktree is enabled and configured,
|
|
127
|
+
otherwise the original project path.
|
|
128
|
+
"""
|
|
129
|
+
if self.use_worktree and self.worktree_path:
|
|
130
|
+
return self.worktree_path
|
|
131
|
+
return self.path
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class BaseFramework(ABC):
|
|
135
|
+
"""Base class for AI coding assistant frameworks.
|
|
136
|
+
|
|
137
|
+
A framework represents a specific AI coding tool (Claude Code, Claude MPM, etc.)
|
|
138
|
+
that can be launched in a project directory via tmux.
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
name: Short identifier (e.g., "cc", "mpm")
|
|
142
|
+
display_name: Human-readable name (e.g., "Claude Code", "Claude MPM")
|
|
143
|
+
command: The command to run (e.g., "claude")
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> class MyFramework(BaseFramework):
|
|
147
|
+
... name = "my"
|
|
148
|
+
... display_name = "My Framework"
|
|
149
|
+
... command = "my-command"
|
|
150
|
+
...
|
|
151
|
+
... def get_startup_command(self, project_path: Path) -> str:
|
|
152
|
+
... return f"cd {project_path} && my-command"
|
|
153
|
+
...
|
|
154
|
+
... def is_available(self) -> bool:
|
|
155
|
+
... return True
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
name: str # "cc", "mpm", etc.
|
|
159
|
+
display_name: str # "Claude Code", "Claude MPM", etc.
|
|
160
|
+
command: str # The command to run
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def get_startup_command(self, project_path: Path) -> str:
|
|
164
|
+
"""Get the command to start this framework in a project.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
project_path: Path to the project directory
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Shell command string to start the framework
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> framework = ClaudeCodeFramework()
|
|
174
|
+
>>> framework.get_startup_command(Path("/Users/user/myapp"))
|
|
175
|
+
"cd /Users/user/myapp && claude"
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def is_available(self) -> bool:
|
|
180
|
+
"""Check if this framework is installed/available.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if the framework command is available on the system
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> framework = ClaudeCodeFramework()
|
|
187
|
+
>>> framework.is_available()
|
|
188
|
+
True
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def get_git_info(self, project_path: Path) -> tuple[Optional[str], Optional[str]]:
|
|
192
|
+
"""Get git branch and status for project.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
project_path: Path to the project directory
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Tuple of (branch, status) where:
|
|
199
|
+
- branch: Current git branch name or None if not a git repo
|
|
200
|
+
- status: "clean" if no changes, "dirty" if changes, or None if not a git repo
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> framework = ClaudeCodeFramework()
|
|
204
|
+
>>> branch, status = framework.get_git_info(Path("/Users/user/myapp"))
|
|
205
|
+
>>> print(branch, status)
|
|
206
|
+
main clean
|
|
207
|
+
"""
|
|
208
|
+
if not (project_path / ".git").exists():
|
|
209
|
+
return None, None
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
# Get current branch
|
|
213
|
+
result = subprocess.run( # nosec B603, B607 - Controlled git command
|
|
214
|
+
["git", "-C", str(project_path), "rev-parse", "--abbrev-ref", "HEAD"],
|
|
215
|
+
capture_output=True,
|
|
216
|
+
text=True,
|
|
217
|
+
check=True,
|
|
218
|
+
)
|
|
219
|
+
branch = result.stdout.strip()
|
|
220
|
+
|
|
221
|
+
# Check if working directory is clean
|
|
222
|
+
result = subprocess.run( # nosec B603, B607 - Controlled git command
|
|
223
|
+
["git", "-C", str(project_path), "status", "--porcelain"],
|
|
224
|
+
capture_output=True,
|
|
225
|
+
text=True,
|
|
226
|
+
check=True,
|
|
227
|
+
)
|
|
228
|
+
status = "clean" if not result.stdout.strip() else "dirty"
|
|
229
|
+
|
|
230
|
+
return branch, status
|
|
231
|
+
|
|
232
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
233
|
+
return None, None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Claude Code (cc) framework implementation."""
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .base import BaseFramework
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClaudeCodeFramework(BaseFramework):
|
|
11
|
+
"""Claude Code CLI framework.
|
|
12
|
+
|
|
13
|
+
This framework launches the standard Claude Code CLI in a project directory.
|
|
14
|
+
It uses the 'claude' command with appropriate flags for automated operation.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> framework = ClaudeCodeFramework()
|
|
18
|
+
>>> framework.name
|
|
19
|
+
'cc'
|
|
20
|
+
>>> framework.is_available()
|
|
21
|
+
True
|
|
22
|
+
>>> framework.get_startup_command(Path("/Users/user/myapp"))
|
|
23
|
+
"cd '/Users/user/myapp' && claude --dangerously-skip-permissions"
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name = "cc"
|
|
27
|
+
display_name = "Claude Code"
|
|
28
|
+
command = "claude"
|
|
29
|
+
|
|
30
|
+
def get_startup_command(self, project_path: Path) -> str:
|
|
31
|
+
"""Get the command to start Claude Code in a project.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project_path: Path to the project directory
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Shell command string to start Claude Code
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> framework = ClaudeCodeFramework()
|
|
41
|
+
>>> framework.get_startup_command(Path("/Users/user/myapp"))
|
|
42
|
+
"cd '/Users/user/myapp' && claude --dangerously-skip-permissions"
|
|
43
|
+
"""
|
|
44
|
+
quoted_path = shlex.quote(str(project_path))
|
|
45
|
+
return f"cd {quoted_path} && claude --dangerously-skip-permissions"
|
|
46
|
+
|
|
47
|
+
def is_available(self) -> bool:
|
|
48
|
+
"""Check if 'claude' command is available.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if 'claude' command exists in PATH
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> framework = ClaudeCodeFramework()
|
|
55
|
+
>>> framework.is_available()
|
|
56
|
+
True
|
|
57
|
+
"""
|
|
58
|
+
return shutil.which("claude") is not None
|