claude-mpm 4.20.3__py3-none-any.whl → 4.25.10__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 (454) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +23 -6
  3. claude_mpm/agents/OUTPUT_STYLE.md +3 -48
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +1783 -34
  5. claude_mpm/agents/WORKFLOW.md +75 -2
  6. claude_mpm/agents/base_agent.json +6 -3
  7. claude_mpm/agents/frontmatter_validator.py +1 -1
  8. claude_mpm/agents/templates/api_qa.json +5 -2
  9. claude_mpm/agents/templates/circuit_breakers.md +108 -2
  10. claude_mpm/agents/templates/documentation.json +33 -6
  11. claude_mpm/agents/templates/javascript_engineer_agent.json +380 -0
  12. claude_mpm/agents/templates/php-engineer.json +10 -4
  13. claude_mpm/agents/templates/pm_red_flags.md +89 -19
  14. claude_mpm/agents/templates/project_organizer.json +7 -3
  15. claude_mpm/agents/templates/qa.json +2 -1
  16. claude_mpm/agents/templates/react_engineer.json +1 -0
  17. claude_mpm/agents/templates/research.json +82 -12
  18. claude_mpm/agents/templates/security.json +4 -4
  19. claude_mpm/agents/templates/tauri_engineer.json +274 -0
  20. claude_mpm/agents/templates/ticketing.json +10 -6
  21. claude_mpm/agents/templates/version_control.json +4 -2
  22. claude_mpm/agents/templates/web_qa.json +2 -1
  23. claude_mpm/cli/README.md +253 -0
  24. claude_mpm/cli/__init__.py +11 -1
  25. claude_mpm/cli/commands/aggregate.py +1 -1
  26. claude_mpm/cli/commands/analyze.py +3 -3
  27. claude_mpm/cli/commands/cleanup.py +1 -1
  28. claude_mpm/cli/commands/configure_agent_display.py +4 -4
  29. claude_mpm/cli/commands/debug.py +12 -12
  30. claude_mpm/cli/commands/hook_errors.py +277 -0
  31. claude_mpm/cli/commands/mcp_install_commands.py +1 -1
  32. claude_mpm/cli/commands/mcp_install_commands.py.backup +284 -0
  33. claude_mpm/cli/commands/mpm_init/README.md +365 -0
  34. claude_mpm/cli/commands/mpm_init/__init__.py +73 -0
  35. claude_mpm/cli/commands/mpm_init/core.py +573 -0
  36. claude_mpm/cli/commands/mpm_init/display.py +341 -0
  37. claude_mpm/cli/commands/mpm_init/git_activity.py +427 -0
  38. claude_mpm/cli/commands/mpm_init/modes.py +397 -0
  39. claude_mpm/cli/commands/mpm_init/prompts.py +442 -0
  40. claude_mpm/cli/commands/mpm_init_cli.py +396 -0
  41. claude_mpm/cli/commands/mpm_init_handler.py +67 -1
  42. claude_mpm/cli/commands/run.py +124 -128
  43. claude_mpm/cli/commands/skills.py +522 -34
  44. claude_mpm/cli/executor.py +56 -0
  45. claude_mpm/cli/interactive/agent_wizard.py +5 -5
  46. claude_mpm/cli/parsers/base_parser.py +28 -0
  47. claude_mpm/cli/parsers/mpm_init_parser.py +42 -0
  48. claude_mpm/cli/parsers/skills_parser.py +138 -0
  49. claude_mpm/cli/startup.py +111 -8
  50. claude_mpm/cli/startup_display.py +480 -0
  51. claude_mpm/cli/utils.py +1 -1
  52. claude_mpm/cli_module/commands.py +1 -1
  53. claude_mpm/cli_module/refactoring_guide.md +253 -0
  54. claude_mpm/commands/mpm-help.md +3 -0
  55. claude_mpm/commands/mpm-init.md +19 -3
  56. claude_mpm/commands/mpm-resume.md +372 -0
  57. claude_mpm/commands/mpm-tickets.md +56 -7
  58. claude_mpm/commands/mpm.md +1 -0
  59. claude_mpm/config/agent_capabilities.yaml +658 -0
  60. claude_mpm/config/async_logging_config.yaml +145 -0
  61. claude_mpm/constants.py +12 -0
  62. claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +34 -0
  63. claude_mpm/core/api_validator.py +1 -1
  64. claude_mpm/core/claude_runner.py +14 -1
  65. claude_mpm/core/config.py +8 -0
  66. claude_mpm/core/constants.py +1 -1
  67. claude_mpm/core/framework/processors/metadata_processor.py +1 -1
  68. claude_mpm/core/hook_error_memory.py +381 -0
  69. claude_mpm/core/hook_manager.py +41 -2
  70. claude_mpm/core/interactive_session.py +48 -3
  71. claude_mpm/core/interfaces.py +56 -1
  72. claude_mpm/core/logger.py +3 -1
  73. claude_mpm/core/oneshot_session.py +39 -0
  74. claude_mpm/d2/.gitignore +22 -0
  75. claude_mpm/d2/ARCHITECTURE_COMPARISON.md +273 -0
  76. claude_mpm/d2/FLASK_INTEGRATION.md +156 -0
  77. claude_mpm/d2/IMPLEMENTATION_SUMMARY.md +452 -0
  78. claude_mpm/d2/QUICKSTART.md +186 -0
  79. claude_mpm/d2/README.md +232 -0
  80. claude_mpm/d2/STORE_FIX_SUMMARY.md +167 -0
  81. claude_mpm/d2/SVELTE5_STORES_GUIDE.md +180 -0
  82. claude_mpm/d2/TESTING.md +288 -0
  83. claude_mpm/d2/index.html +118 -0
  84. claude_mpm/d2/package.json +19 -0
  85. claude_mpm/d2/src/App.svelte +110 -0
  86. claude_mpm/d2/src/components/Header.svelte +153 -0
  87. claude_mpm/d2/src/components/MainContent.svelte +74 -0
  88. claude_mpm/d2/src/components/Sidebar.svelte +85 -0
  89. claude_mpm/d2/src/components/tabs/EventsTab.svelte +326 -0
  90. claude_mpm/d2/src/lib/socketio.js +144 -0
  91. claude_mpm/d2/src/main.js +7 -0
  92. claude_mpm/d2/src/stores/events.js +114 -0
  93. claude_mpm/d2/src/stores/socket.js +108 -0
  94. claude_mpm/d2/src/stores/theme.js +65 -0
  95. claude_mpm/d2/svelte.config.js +12 -0
  96. claude_mpm/d2/vite.config.js +15 -0
  97. claude_mpm/dashboard/.claude-mpm/memories/README.md +36 -0
  98. claude_mpm/dashboard/BUILD_NUMBER +1 -0
  99. claude_mpm/dashboard/README.md +121 -0
  100. claude_mpm/dashboard/VERSION +1 -0
  101. claude_mpm/dashboard/react/components/DataInspector/DataInspector.tsx +273 -0
  102. claude_mpm/dashboard/react/components/ErrorBoundary.tsx +75 -0
  103. claude_mpm/dashboard/react/components/EventViewer/EventViewer.tsx +141 -0
  104. claude_mpm/dashboard/react/components/shared/ConnectionStatus.tsx +36 -0
  105. claude_mpm/dashboard/react/components/shared/FilterBar.tsx +89 -0
  106. claude_mpm/dashboard/react/contexts/DashboardContext.tsx +215 -0
  107. claude_mpm/dashboard/react/entries/events.tsx +165 -0
  108. claude_mpm/dashboard/react/hooks/useEvents.ts +191 -0
  109. claude_mpm/dashboard/react/hooks/useSocket.ts +225 -0
  110. claude_mpm/dashboard/static/built/REFACTORING_SUMMARY.md +170 -0
  111. claude_mpm/dashboard/static/built/components/activity-tree.js.map +1 -0
  112. claude_mpm/dashboard/static/built/components/agent-hierarchy.js +101 -101
  113. claude_mpm/dashboard/static/built/components/agent-inference.js.map +1 -0
  114. claude_mpm/dashboard/static/built/components/build-tracker.js +59 -59
  115. claude_mpm/dashboard/static/built/components/code-simple.js +107 -107
  116. claude_mpm/dashboard/static/built/components/code-tree/tree-breadcrumb.js +29 -29
  117. claude_mpm/dashboard/static/built/components/code-tree/tree-constants.js +24 -24
  118. claude_mpm/dashboard/static/built/components/code-tree/tree-search.js +27 -27
  119. claude_mpm/dashboard/static/built/components/code-tree/tree-utils.js +25 -25
  120. claude_mpm/dashboard/static/built/components/code-tree.js.map +1 -0
  121. claude_mpm/dashboard/static/built/components/code-viewer.js.map +1 -0
  122. claude_mpm/dashboard/static/built/components/connection-debug.js +101 -101
  123. claude_mpm/dashboard/static/built/components/diff-viewer.js +113 -113
  124. claude_mpm/dashboard/static/built/components/event-processor.js.map +1 -0
  125. claude_mpm/dashboard/static/built/components/event-viewer.js.map +1 -0
  126. claude_mpm/dashboard/static/built/components/export-manager.js.map +1 -0
  127. claude_mpm/dashboard/static/built/components/file-change-tracker.js +57 -57
  128. claude_mpm/dashboard/static/built/components/file-change-viewer.js +74 -74
  129. claude_mpm/dashboard/static/built/components/file-tool-tracker.js.map +1 -0
  130. claude_mpm/dashboard/static/built/components/file-viewer.js.map +1 -0
  131. claude_mpm/dashboard/static/built/components/hud-library-loader.js.map +1 -0
  132. claude_mpm/dashboard/static/built/components/hud-manager.js.map +1 -0
  133. claude_mpm/dashboard/static/built/components/hud-visualizer.js.map +1 -0
  134. claude_mpm/dashboard/static/built/components/module-viewer.js.map +1 -0
  135. claude_mpm/dashboard/static/built/components/session-manager.js.map +1 -0
  136. claude_mpm/dashboard/static/built/components/socket-manager.js.map +1 -0
  137. claude_mpm/dashboard/static/built/components/ui-state-manager.js.map +1 -0
  138. claude_mpm/dashboard/static/built/components/unified-data-viewer.js.map +1 -0
  139. claude_mpm/dashboard/static/built/components/working-directory.js.map +1 -0
  140. claude_mpm/dashboard/static/built/connection-manager.js +76 -76
  141. claude_mpm/dashboard/static/built/dashboard.js.map +1 -0
  142. claude_mpm/dashboard/static/built/extension-error-handler.js +22 -22
  143. claude_mpm/dashboard/static/built/react/events.js.map +1 -0
  144. claude_mpm/dashboard/static/built/shared/dom-helpers.js +9 -9
  145. claude_mpm/dashboard/static/built/shared/event-bus.js +5 -5
  146. claude_mpm/dashboard/static/built/shared/logger.js +16 -16
  147. claude_mpm/dashboard/static/built/shared/tooltip-service.js +6 -6
  148. claude_mpm/dashboard/static/built/socket-client.js.map +1 -0
  149. claude_mpm/dashboard/static/css/activity.css +69 -69
  150. claude_mpm/dashboard/static/css/connection-status.css +10 -10
  151. claude_mpm/dashboard/static/css/dashboard.css +15 -15
  152. claude_mpm/dashboard/static/index.html +22 -22
  153. claude_mpm/dashboard/static/js/REFACTORING_SUMMARY.md +170 -0
  154. claude_mpm/dashboard/static/js/components/activity-tree.js +178 -178
  155. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +101 -101
  156. claude_mpm/dashboard/static/js/components/agent-inference.js +31 -31
  157. claude_mpm/dashboard/static/js/components/build-tracker.js +59 -59
  158. claude_mpm/dashboard/static/js/components/code-simple.js +107 -107
  159. claude_mpm/dashboard/static/js/components/connection-debug.js +101 -101
  160. claude_mpm/dashboard/static/js/components/diff-viewer.js +113 -113
  161. claude_mpm/dashboard/static/js/components/event-viewer.js +12 -12
  162. claude_mpm/dashboard/static/js/components/file-change-tracker.js +57 -57
  163. claude_mpm/dashboard/static/js/components/file-change-viewer.js +74 -74
  164. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +6 -6
  165. claude_mpm/dashboard/static/js/components/file-viewer.js +42 -42
  166. claude_mpm/dashboard/static/js/components/module-viewer.js +27 -27
  167. claude_mpm/dashboard/static/js/components/session-manager.js +14 -14
  168. claude_mpm/dashboard/static/js/components/socket-manager.js +1 -1
  169. claude_mpm/dashboard/static/js/components/ui-state-manager.js +14 -14
  170. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +110 -110
  171. claude_mpm/dashboard/static/js/components/working-directory.js +8 -8
  172. claude_mpm/dashboard/static/js/connection-manager.js +76 -76
  173. claude_mpm/dashboard/static/js/dashboard.js +76 -58
  174. claude_mpm/dashboard/static/js/extension-error-handler.js +22 -22
  175. claude_mpm/dashboard/static/js/shared/dom-helpers.js +9 -9
  176. claude_mpm/dashboard/static/js/shared/event-bus.js +5 -5
  177. claude_mpm/dashboard/static/js/shared/logger.js +16 -16
  178. claude_mpm/dashboard/static/js/shared/tooltip-service.js +6 -6
  179. claude_mpm/dashboard/static/js/socket-client.js +138 -121
  180. claude_mpm/dashboard/static/navigation-test-results.md +118 -0
  181. claude_mpm/dashboard/static/production/main.html +21 -21
  182. claude_mpm/dashboard/static/test-archive/dashboard.html +22 -22
  183. claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +36 -0
  184. claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +39 -0
  185. claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +38 -0
  186. claude_mpm/dashboard/templates/code_simple.html +23 -23
  187. claude_mpm/dashboard/templates/index.html +18 -18
  188. claude_mpm/hooks/README.md +143 -0
  189. claude_mpm/hooks/claude_hooks/event_handlers.py +3 -1
  190. claude_mpm/hooks/claude_hooks/hook_handler.py +24 -7
  191. claude_mpm/hooks/claude_hooks/installer.py +45 -0
  192. claude_mpm/hooks/templates/README.md +180 -0
  193. claude_mpm/hooks/templates/pre_tool_use_simple.py +78 -0
  194. claude_mpm/hooks/templates/pre_tool_use_template.py +323 -0
  195. claude_mpm/hooks/templates/settings.json.example +147 -0
  196. claude_mpm/schemas/agent_schema.json +596 -0
  197. claude_mpm/schemas/frontmatter_schema.json +165 -0
  198. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  199. claude_mpm/scripts/start_activity_logging.py +3 -1
  200. claude_mpm/services/agents/deployment/agent_format_converter.py +1 -1
  201. claude_mpm/services/agents/deployment/agent_metrics_collector.py +3 -3
  202. claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -3
  203. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +2 -2
  204. claude_mpm/services/agents/loading/framework_agent_loader.py +8 -8
  205. claude_mpm/services/agents/local_template_manager.py +3 -1
  206. claude_mpm/services/cli/session_pause_manager.py +504 -0
  207. claude_mpm/services/cli/session_resume_helper.py +36 -16
  208. claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
  209. claude_mpm/services/core/base.py +26 -11
  210. claude_mpm/services/core/interfaces.py +56 -1
  211. claude_mpm/services/core/models/agent_config.py +3 -0
  212. claude_mpm/services/core/models/process.py +4 -0
  213. claude_mpm/services/diagnostics/checks/agent_check.py +0 -2
  214. claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
  215. claude_mpm/services/diagnostics/checks/mcp_check.py +0 -1
  216. claude_mpm/services/diagnostics/checks/monitor_check.py +0 -1
  217. claude_mpm/services/diagnostics/doctor_reporter.py +6 -4
  218. claude_mpm/services/diagnostics/models.py +21 -0
  219. claude_mpm/services/event_bus/README.md +244 -0
  220. claude_mpm/services/event_bus/direct_relay.py +3 -3
  221. claude_mpm/services/event_bus/event_bus.py +36 -3
  222. claude_mpm/services/event_bus/relay.py +23 -7
  223. claude_mpm/services/events/README.md +303 -0
  224. claude_mpm/services/events/consumers/logging.py +1 -2
  225. claude_mpm/services/framework_claude_md_generator/README.md +119 -0
  226. claude_mpm/services/infrastructure/monitoring/resources.py +1 -1
  227. claude_mpm/services/local_ops/__init__.py +2 -0
  228. claude_mpm/services/local_ops/process_manager.py +1 -1
  229. claude_mpm/services/local_ops/resource_monitor.py +2 -2
  230. claude_mpm/services/mcp_gateway/README.md +185 -0
  231. claude_mpm/services/mcp_gateway/auto_configure.py +31 -25
  232. claude_mpm/services/mcp_gateway/config/configuration.py +1 -1
  233. claude_mpm/services/mcp_gateway/core/process_pool.py +19 -10
  234. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -2
  235. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +1 -1
  236. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +26 -21
  237. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +6 -2
  238. claude_mpm/services/memory/failure_tracker.py +19 -4
  239. claude_mpm/services/memory/optimizer.py +1 -1
  240. claude_mpm/services/model/model_router.py +8 -9
  241. claude_mpm/services/monitor/daemon.py +1 -1
  242. claude_mpm/services/monitor/server.py +2 -2
  243. claude_mpm/services/native_agent_converter.py +356 -0
  244. claude_mpm/services/port_manager.py +1 -1
  245. claude_mpm/services/project/documentation_manager.py +2 -1
  246. claude_mpm/services/project/toolchain_analyzer.py +3 -1
  247. claude_mpm/services/runner_configuration_service.py +1 -0
  248. claude_mpm/services/self_upgrade_service.py +165 -7
  249. claude_mpm/services/skills_config.py +547 -0
  250. claude_mpm/services/skills_deployer.py +955 -0
  251. claude_mpm/services/socketio/handlers/connection.py +1 -1
  252. claude_mpm/services/socketio/handlers/connection.py.backup +217 -0
  253. claude_mpm/services/socketio/handlers/git.py +2 -2
  254. claude_mpm/services/socketio/handlers/hook.py.backup +154 -0
  255. claude_mpm/services/static/.gitkeep +2 -0
  256. claude_mpm/services/system_instructions_service.py +1 -3
  257. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +0 -3
  258. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +0 -1
  259. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +1 -1
  260. claude_mpm/services/version_control/VERSION +1 -0
  261. claude_mpm/services/version_control/conflict_resolution.py +6 -4
  262. claude_mpm/services/visualization/mermaid_generator.py +2 -3
  263. claude_mpm/skills/__init__.py +3 -3
  264. claude_mpm/skills/agent_skills_injector.py +42 -49
  265. claude_mpm/skills/bundled/.gitkeep +2 -0
  266. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +4 -0
  267. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +108 -114
  268. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
  269. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
  270. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
  271. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
  272. claude_mpm/skills/bundled/collaboration/git-worktrees.md +317 -0
  273. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +46 -41
  274. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
  275. claude_mpm/skills/bundled/collaboration/stacked-prs.md +251 -0
  276. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +36 -73
  277. claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
  278. claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
  279. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +100 -125
  280. claude_mpm/skills/bundled/debugging/root-cause-tracing/find-polluter.sh +63 -0
  281. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
  282. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
  283. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
  284. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
  285. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +28 -72
  286. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +11 -0
  287. claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
  288. claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
  289. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +272 -0
  290. claude_mpm/skills/bundled/infrastructure/env-manager/INTEGRATION.md +611 -0
  291. claude_mpm/skills/bundled/infrastructure/env-manager/README.md +596 -0
  292. claude_mpm/skills/bundled/infrastructure/env-manager/SKILL.md +260 -0
  293. claude_mpm/skills/bundled/infrastructure/env-manager/examples/nextjs-env-structure.md +315 -0
  294. claude_mpm/skills/bundled/infrastructure/env-manager/references/frameworks.md +436 -0
  295. claude_mpm/skills/bundled/infrastructure/env-manager/references/security.md +433 -0
  296. claude_mpm/skills/bundled/infrastructure/env-manager/references/synchronization.md +452 -0
  297. claude_mpm/skills/bundled/infrastructure/env-manager/references/troubleshooting.md +404 -0
  298. claude_mpm/skills/bundled/infrastructure/env-manager/references/validation.md +420 -0
  299. claude_mpm/skills/bundled/infrastructure/env-manager/scripts/validate_env.py +576 -0
  300. claude_mpm/skills/bundled/main/artifacts-builder/LICENSE.txt +202 -0
  301. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +13 -1
  302. claude_mpm/skills/bundled/main/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  303. claude_mpm/skills/bundled/main/artifacts-builder/scripts/init-artifact.sh +322 -0
  304. claude_mpm/skills/bundled/main/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  305. claude_mpm/skills/bundled/main/internal-comms/LICENSE.txt +202 -0
  306. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +11 -0
  307. claude_mpm/skills/bundled/main/mcp-builder/LICENSE.txt +202 -0
  308. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +109 -277
  309. claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
  310. claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
  311. claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +17 -10
  312. claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +92 -39
  313. claude_mpm/skills/bundled/main/mcp-builder/scripts/example_evaluation.xml +22 -0
  314. claude_mpm/skills/bundled/main/mcp-builder/scripts/requirements.txt +2 -0
  315. claude_mpm/skills/bundled/main/skill-creator/LICENSE.txt +202 -0
  316. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +135 -155
  317. claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
  318. claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
  319. claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
  320. claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
  321. claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
  322. claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +13 -12
  323. claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +5 -3
  324. claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +19 -12
  325. claude_mpm/skills/bundled/performance-profiling.md +6 -0
  326. claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
  327. claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
  328. claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
  329. claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
  330. claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
  331. claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
  332. claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
  333. claude_mpm/skills/bundled/react/flexlayout-react.md +742 -0
  334. claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
  335. claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
  336. claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
  337. claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
  338. claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
  339. claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
  340. claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
  341. claude_mpm/skills/bundled/tauri/tauri-async-patterns.md +495 -0
  342. claude_mpm/skills/bundled/tauri/tauri-build-deploy.md +599 -0
  343. claude_mpm/skills/bundled/tauri/tauri-command-patterns.md +535 -0
  344. claude_mpm/skills/bundled/tauri/tauri-error-handling.md +613 -0
  345. claude_mpm/skills/bundled/tauri/tauri-event-system.md +648 -0
  346. claude_mpm/skills/bundled/tauri/tauri-file-system.md +673 -0
  347. claude_mpm/skills/bundled/tauri/tauri-frontend-integration.md +767 -0
  348. claude_mpm/skills/bundled/tauri/tauri-performance.md +669 -0
  349. claude_mpm/skills/bundled/tauri/tauri-state-management.md +573 -0
  350. claude_mpm/skills/bundled/tauri/tauri-testing.md +384 -0
  351. claude_mpm/skills/bundled/tauri/tauri-window-management.md +628 -0
  352. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +21 -25
  353. claude_mpm/skills/bundled/testing/condition-based-waiting/example.ts +158 -0
  354. claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
  355. claude_mpm/skills/bundled/testing/test-quality-inspector/SKILL.md +458 -0
  356. claude_mpm/skills/bundled/testing/test-quality-inspector/examples/example-inspection-report.md +411 -0
  357. claude_mpm/skills/bundled/testing/test-quality-inspector/references/assertion-quality.md +317 -0
  358. claude_mpm/skills/bundled/testing/test-quality-inspector/references/inspection-checklist.md +270 -0
  359. claude_mpm/skills/bundled/testing/test-quality-inspector/references/red-flags.md +436 -0
  360. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +86 -250
  361. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
  362. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
  363. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
  364. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
  365. claude_mpm/skills/bundled/testing/webapp-testing/LICENSE.txt +202 -0
  366. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +145 -57
  367. claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
  368. claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +6 -6
  369. claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +13 -9
  370. claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +8 -8
  371. claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
  372. claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
  373. claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +37 -15
  374. claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
  375. claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
  376. claude_mpm/skills/skills_registry.py +44 -48
  377. claude_mpm/skills/skills_service.py +117 -108
  378. claude_mpm/templates/questions/EXAMPLES.md +501 -0
  379. claude_mpm/templates/questions/__init__.py +43 -0
  380. claude_mpm/templates/questions/base.py +193 -0
  381. claude_mpm/templates/questions/pr_strategy.py +314 -0
  382. claude_mpm/templates/questions/project_init.py +388 -0
  383. claude_mpm/templates/questions/ticket_mgmt.py +397 -0
  384. claude_mpm/tools/README_SOCKETIO_DEBUG.md +224 -0
  385. claude_mpm/tools/__main__.py +8 -8
  386. claude_mpm/tools/code_tree_analyzer/README.md +64 -0
  387. claude_mpm/tools/code_tree_analyzer/__init__.py +45 -0
  388. claude_mpm/tools/code_tree_analyzer/analysis.py +299 -0
  389. claude_mpm/tools/code_tree_analyzer/cache.py +131 -0
  390. claude_mpm/tools/code_tree_analyzer/core.py +380 -0
  391. claude_mpm/tools/code_tree_analyzer/discovery.py +403 -0
  392. claude_mpm/tools/code_tree_analyzer/events.py +168 -0
  393. claude_mpm/tools/code_tree_analyzer/gitignore.py +308 -0
  394. claude_mpm/tools/code_tree_analyzer/models.py +39 -0
  395. claude_mpm/tools/code_tree_analyzer/multilang_analyzer.py +224 -0
  396. claude_mpm/tools/code_tree_analyzer/python_analyzer.py +284 -0
  397. claude_mpm/utils/agent_dependency_loader.py +3 -3
  398. claude_mpm/utils/dependency_cache.py +3 -1
  399. claude_mpm/utils/gitignore.py +241 -0
  400. claude_mpm/utils/log_cleanup.py +3 -3
  401. claude_mpm/utils/robust_installer.py +3 -5
  402. claude_mpm/utils/structured_questions.py +619 -0
  403. {claude_mpm-4.20.3.dist-info → claude_mpm-4.25.10.dist-info}/METADATA +218 -31
  404. {claude_mpm-4.20.3.dist-info → claude_mpm-4.25.10.dist-info}/RECORD +409 -246
  405. claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -17
  406. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +0 -3
  407. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +0 -39
  408. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250901_010124_142.md +0 -400
  409. claude_mpm/cli/commands/mpm_init.py +0 -2093
  410. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +0 -1
  411. claude_mpm/dashboard/static/archive/activity_dashboard_test.html +0 -61
  412. claude_mpm/dashboard/static/archive/test_activity_connection.html +0 -179
  413. claude_mpm/dashboard/static/archive/test_claude_tree_tab.html +0 -68
  414. claude_mpm/dashboard/static/archive/test_dashboard.html +0 -409
  415. claude_mpm/dashboard/static/archive/test_dashboard_fixed.html +0 -519
  416. claude_mpm/dashboard/static/archive/test_dashboard_verification.html +0 -181
  417. claude_mpm/dashboard/static/archive/test_file_data.html +0 -315
  418. claude_mpm/dashboard/static/archive/test_file_tree_empty_state.html +0 -243
  419. claude_mpm/dashboard/static/archive/test_file_tree_fix.html +0 -234
  420. claude_mpm/dashboard/static/archive/test_file_tree_rename.html +0 -117
  421. claude_mpm/dashboard/static/archive/test_file_tree_tab.html +0 -115
  422. claude_mpm/dashboard/static/archive/test_file_viewer.html +0 -224
  423. claude_mpm/dashboard/static/archive/test_final_activity.html +0 -220
  424. claude_mpm/dashboard/static/archive/test_tab_fix.html +0 -139
  425. claude_mpm/dashboard/static/dist/assets/events.DjpNxWNo.css +0 -1
  426. claude_mpm/dashboard/static/dist/components/activity-tree.js +0 -2
  427. claude_mpm/dashboard/static/dist/components/agent-inference.js +0 -2
  428. claude_mpm/dashboard/static/dist/components/code-tree.js +0 -2
  429. claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
  430. claude_mpm/dashboard/static/dist/components/event-processor.js +0 -2
  431. claude_mpm/dashboard/static/dist/components/event-viewer.js +0 -2
  432. claude_mpm/dashboard/static/dist/components/export-manager.js +0 -2
  433. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +0 -2
  434. claude_mpm/dashboard/static/dist/components/file-viewer.js +0 -2
  435. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +0 -2
  436. claude_mpm/dashboard/static/dist/components/hud-manager.js +0 -2
  437. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +0 -2
  438. claude_mpm/dashboard/static/dist/components/module-viewer.js +0 -2
  439. claude_mpm/dashboard/static/dist/components/session-manager.js +0 -2
  440. claude_mpm/dashboard/static/dist/components/socket-manager.js +0 -2
  441. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +0 -2
  442. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +0 -2
  443. claude_mpm/dashboard/static/dist/components/working-directory.js +0 -2
  444. claude_mpm/dashboard/static/dist/dashboard.js +0 -2
  445. claude_mpm/dashboard/static/dist/react/events.js +0 -30
  446. claude_mpm/dashboard/static/dist/socket-client.js +0 -2
  447. claude_mpm/dashboard/static/test-archive/test_debug.html +0 -25
  448. claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +0 -213
  449. claude_mpm/tools/code_tree_analyzer.py +0 -1825
  450. /claude_mpm/skills/bundled/collaboration/requesting-code-review/{code-reviewer.md → references/code-reviewer-template.md} +0 -0
  451. {claude_mpm-4.20.3.dist-info → claude_mpm-4.25.10.dist-info}/WHEEL +0 -0
  452. {claude_mpm-4.20.3.dist-info → claude_mpm-4.25.10.dist-info}/entry_points.txt +0 -0
  453. {claude_mpm-4.20.3.dist-info → claude_mpm-4.25.10.dist-info}/licenses/LICENSE +0 -0
  454. {claude_mpm-4.20.3.dist-info → claude_mpm-4.25.10.dist-info}/top_level.txt +0 -0
@@ -1,1825 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Code Tree Analyzer
4
- ==================
5
-
6
- WHY: Analyzes source code using AST to extract structure and metrics,
7
- supporting multiple languages and emitting incremental events for visualization.
8
-
9
- DESIGN DECISIONS:
10
- - Use Python's ast module for Python files
11
- - Use tree-sitter for multi-language support
12
- - Extract comprehensive metadata (complexity, docstrings, etc.)
13
- - Cache parsed results to avoid re-processing
14
- - Support incremental processing with checkpoints
15
- """
16
-
17
- import ast
18
- import hashlib
19
- import json
20
- import time
21
- from dataclasses import dataclass
22
- from datetime import timezone
23
- from pathlib import Path
24
- from typing import Any, ClassVar, Dict, List, Optional
25
-
26
- try:
27
- import pathspec
28
-
29
- PATHSPEC_AVAILABLE = True
30
- except ImportError:
31
- PATHSPEC_AVAILABLE = False
32
- pathspec = None
33
-
34
- import importlib.util
35
-
36
- if importlib.util.find_spec("tree_sitter"):
37
- import tree_sitter
38
-
39
- TREE_SITTER_AVAILABLE = True
40
- else:
41
- TREE_SITTER_AVAILABLE = False
42
- tree_sitter = None
43
-
44
- from ..core.logging_config import get_logger
45
- from .code_tree_events import CodeNodeEvent, CodeTreeEventEmitter
46
-
47
-
48
- class GitignoreManager:
49
- """Manages .gitignore pattern matching for file filtering.
50
-
51
- WHY: Properly respecting .gitignore patterns ensures we don't analyze
52
- or display files that should be ignored in the repository.
53
- """
54
-
55
- # Default patterns that should always be ignored
56
- DEFAULT_PATTERNS: ClassVar[list] = [
57
- ".git/",
58
- "__pycache__/",
59
- "*.pyc",
60
- "*.pyo",
61
- ".DS_Store",
62
- ".pytest_cache/",
63
- ".mypy_cache/",
64
- "dist/",
65
- "build/",
66
- "*.egg-info/",
67
- ".coverage",
68
- ".tox/",
69
- "htmlcov/",
70
- ".idea/",
71
- ".vscode/",
72
- "*.swp",
73
- "*.swo",
74
- "*~",
75
- "Thumbs.db",
76
- "node_modules/",
77
- ".venv/",
78
- "venv/",
79
- "env/",
80
- ".env",
81
- "*.log",
82
- ".ipynb_checkpoints/",
83
- "__MACOSX/",
84
- ".Spotlight-V100/",
85
- ".Trashes/",
86
- "desktop.ini",
87
- ]
88
-
89
- # Additional patterns to hide dotfiles (when enabled)
90
- DOTFILE_PATTERNS: ClassVar[list] = [
91
- ".*", # All dotfiles
92
- ".*/", # All dot directories
93
- ]
94
-
95
- # Important files/directories to always show
96
- DOTFILE_EXCEPTIONS: ClassVar[set] = {
97
- # Removed .gitignore from exceptions - it should be hidden by default
98
- ".env.example",
99
- ".env.sample",
100
- ".gitlab-ci.yml",
101
- ".travis.yml",
102
- ".dockerignore",
103
- ".editorconfig",
104
- ".eslintrc",
105
- ".prettierrc",
106
- # Removed .github from exceptions - it should be hidden by default
107
- }
108
-
109
- def __init__(self):
110
- """Initialize the GitignoreManager."""
111
- self.logger = get_logger(__name__)
112
- self._pathspec_cache: Dict[str, Any] = {}
113
- self._gitignore_cache: Dict[str, List[str]] = {}
114
- self._use_pathspec = PATHSPEC_AVAILABLE
115
-
116
- if not self._use_pathspec:
117
- self.logger.warning(
118
- "pathspec library not available - using basic pattern matching"
119
- )
120
-
121
- def get_ignore_patterns(self, working_dir: Path) -> List[str]:
122
- """Get all ignore patterns for a directory.
123
-
124
- Args:
125
- working_dir: The working directory to search for .gitignore files
126
-
127
- Returns:
128
- Combined list of ignore patterns from all sources
129
- """
130
- # Always include default patterns
131
- patterns = self.DEFAULT_PATTERNS.copy()
132
-
133
- # Don't add dotfile patterns here - handle them separately in should_ignore
134
- # This prevents exceptions from being overridden by the .* pattern
135
-
136
- # Find and parse .gitignore files
137
- gitignore_files = self._find_gitignore_files(working_dir)
138
- for gitignore_file in gitignore_files:
139
- patterns.extend(self._parse_gitignore(gitignore_file))
140
-
141
- return patterns
142
-
143
- def should_ignore(self, path: Path, working_dir: Path) -> bool:
144
- """Check if a path should be ignored based on patterns.
145
-
146
- Args:
147
- path: The path to check
148
- working_dir: The working directory (for relative path calculation)
149
-
150
- Returns:
151
- True if the path should be ignored
152
- """
153
- # Get the filename
154
- filename = path.name
155
-
156
- # 1. ALWAYS hide system files regardless of settings
157
- ALWAYS_HIDE = {".DS_Store", "Thumbs.db", ".pyc", ".pyo", ".pyd"}
158
- if filename in ALWAYS_HIDE or filename.endswith((".pyc", ".pyo", ".pyd")):
159
- return True
160
-
161
- # 2. Check dotfiles - ALWAYS filter them out (except exceptions)
162
- if filename.startswith("."):
163
- # Hide all dotfiles except those in the exceptions list
164
- # This means: return True (ignore) if NOT in exceptions
165
- return filename not in self.DOTFILE_EXCEPTIONS
166
-
167
- # Get or create PathSpec for this working directory
168
- pathspec_obj = self._get_pathspec(working_dir)
169
-
170
- if pathspec_obj:
171
- # Use pathspec for accurate matching
172
- try:
173
- rel_path = path.relative_to(working_dir)
174
- rel_path_str = str(rel_path)
175
-
176
- # For directories, also check with trailing slash
177
- if path.is_dir():
178
- return pathspec_obj.match_file(
179
- rel_path_str
180
- ) or pathspec_obj.match_file(rel_path_str + "/")
181
- return pathspec_obj.match_file(rel_path_str)
182
- except ValueError:
183
- # Path is outside working directory
184
- return False
185
- else:
186
- # Fallback to basic pattern matching
187
- return self._basic_should_ignore(path, working_dir)
188
-
189
- def _get_pathspec(self, working_dir: Path) -> Optional[Any]:
190
- """Get or create a PathSpec object for the working directory.
191
-
192
- Args:
193
- working_dir: The working directory
194
-
195
- Returns:
196
- PathSpec object or None if not available
197
- """
198
- if not self._use_pathspec:
199
- return None
200
-
201
- cache_key = str(working_dir)
202
- if cache_key not in self._pathspec_cache:
203
- patterns = self.get_ignore_patterns(working_dir)
204
- try:
205
- self._pathspec_cache[cache_key] = pathspec.PathSpec.from_lines(
206
- "gitwildmatch", patterns
207
- )
208
- except Exception as e:
209
- self.logger.warning(f"Failed to create PathSpec: {e}")
210
- return None
211
-
212
- return self._pathspec_cache[cache_key]
213
-
214
- def _find_gitignore_files(self, working_dir: Path) -> List[Path]:
215
- """Find all .gitignore files in the directory tree.
216
-
217
- Args:
218
- working_dir: The directory to search
219
-
220
- Returns:
221
- List of .gitignore file paths
222
- """
223
- gitignore_files = []
224
-
225
- # Check for .gitignore in working directory
226
- main_gitignore = working_dir / ".gitignore"
227
- if main_gitignore.exists():
228
- gitignore_files.append(main_gitignore)
229
-
230
- # Also check parent directories up to repository root
231
- current = working_dir
232
- while current != current.parent:
233
- parent_gitignore = current.parent / ".gitignore"
234
- if parent_gitignore.exists():
235
- gitignore_files.append(parent_gitignore)
236
-
237
- # Stop if we find a .git directory (repository root)
238
- if (current / ".git").exists():
239
- break
240
-
241
- current = current.parent
242
-
243
- return gitignore_files
244
-
245
- def _parse_gitignore(self, gitignore_path: Path) -> List[str]:
246
- """Parse a .gitignore file and return patterns.
247
-
248
- Args:
249
- gitignore_path: Path to .gitignore file
250
-
251
- Returns:
252
- List of patterns from the file
253
- """
254
- cache_key = str(gitignore_path)
255
-
256
- # Check cache
257
- if cache_key in self._gitignore_cache:
258
- return self._gitignore_cache[cache_key]
259
-
260
- patterns = []
261
- try:
262
- with Path(gitignore_path).open(
263
- encoding="utf-8",
264
- ) as f:
265
- for line in f:
266
- line = line.strip()
267
- # Skip empty lines and comments
268
- if line and not line.startswith("#"):
269
- patterns.append(line)
270
-
271
- self._gitignore_cache[cache_key] = patterns
272
- except Exception as e:
273
- self.logger.warning(f"Failed to parse {gitignore_path}: {e}")
274
-
275
- return patterns
276
-
277
- def _basic_should_ignore(self, path: Path, working_dir: Path) -> bool:
278
- """Basic pattern matching fallback when pathspec is not available.
279
-
280
- Args:
281
- path: The path to check
282
- working_dir: The working directory
283
-
284
- Returns:
285
- True if the path should be ignored
286
- """
287
- path_str = str(path)
288
- path_name = path.name
289
-
290
- # 1. ALWAYS hide system files regardless of settings
291
- ALWAYS_HIDE = {".DS_Store", "Thumbs.db", ".pyc", ".pyo", ".pyd"}
292
- if path_name in ALWAYS_HIDE or path_name.endswith((".pyc", ".pyo", ".pyd")):
293
- return True
294
-
295
- # 2. Check dotfiles - ALWAYS filter them out (except exceptions)
296
- if path_name.startswith("."):
297
- # Only show if in exceptions list
298
- return path_name not in self.DOTFILE_EXCEPTIONS
299
-
300
- patterns = self.get_ignore_patterns(working_dir)
301
-
302
- for pattern in patterns:
303
- # Skip dotfile patterns since we already handled them above
304
- if pattern in [".*", ".*/"]:
305
- continue
306
-
307
- # Simple pattern matching
308
- if pattern.endswith("/"):
309
- # Directory pattern
310
- if path.is_dir() and path_name == pattern[:-1]:
311
- return True
312
- elif pattern.startswith("*."):
313
- # Extension pattern
314
- if path_name.endswith(pattern[1:]):
315
- return True
316
- elif "*" in pattern:
317
- # Wildcard pattern (simplified)
318
- import fnmatch
319
-
320
- if fnmatch.fnmatch(path_name, pattern):
321
- return True
322
- elif pattern in path_str:
323
- # Substring match
324
- return True
325
- elif path_name == pattern:
326
- # Exact match
327
- return True
328
-
329
- return False
330
-
331
- def clear_cache(self):
332
- """Clear all caches."""
333
- self._pathspec_cache.clear()
334
- self._gitignore_cache.clear()
335
-
336
-
337
- @dataclass
338
- class CodeNode:
339
- """Represents a node in the code tree."""
340
-
341
- file_path: str
342
- node_type: str
343
- name: str
344
- line_start: int
345
- line_end: int
346
- complexity: int = 0
347
- has_docstring: bool = False
348
- decorators: List[str] = None
349
- parent: Optional[str] = None
350
- children: List["CodeNode"] = None
351
- language: str = "python"
352
- signature: str = ""
353
- metrics: Dict[str, Any] = None
354
-
355
- def __post_init__(self):
356
- if self.decorators is None:
357
- self.decorators = []
358
- if self.children is None:
359
- self.children = []
360
- if self.metrics is None:
361
- self.metrics = {}
362
-
363
-
364
- class PythonAnalyzer:
365
- """Analyzes Python source code using AST.
366
-
367
- WHY: Python's built-in AST module provides rich structural information
368
- that we can leverage for detailed analysis.
369
- """
370
-
371
- def __init__(self, emitter: Optional[CodeTreeEventEmitter] = None):
372
- self.logger = get_logger(__name__)
373
- self.emitter = emitter
374
-
375
- def analyze_file(self, file_path: Path) -> List[CodeNode]:
376
- """Analyze a Python file and extract code structure.
377
-
378
- Args:
379
- file_path: Path to Python file
380
-
381
- Returns:
382
- List of code nodes found in the file
383
- """
384
- nodes = []
385
-
386
- try:
387
- with Path(file_path).open(
388
- encoding="utf-8",
389
- ) as f:
390
- source = f.read()
391
-
392
- tree = ast.parse(source, filename=str(file_path))
393
- nodes = self._extract_nodes(tree, file_path, source)
394
-
395
- except SyntaxError as e:
396
- self.logger.warning(f"Syntax error in {file_path}: {e}")
397
- if self.emitter:
398
- self.emitter.emit_error(str(file_path), f"Syntax error: {e}")
399
- except Exception as e:
400
- self.logger.error(f"Error analyzing {file_path}: {e}")
401
- if self.emitter:
402
- self.emitter.emit_error(str(file_path), str(e))
403
-
404
- return nodes
405
-
406
- def _extract_nodes(
407
- self, tree: ast.AST, file_path: Path, source: str
408
- ) -> List[CodeNode]:
409
- """Extract code nodes from AST tree.
410
-
411
- Args:
412
- tree: AST tree
413
- file_path: Source file path
414
- source: Source code text
415
-
416
- Returns:
417
- List of extracted code nodes
418
- """
419
- nodes = []
420
- source.splitlines()
421
-
422
- class NodeVisitor(ast.NodeVisitor):
423
- def __init__(self, parent_name: Optional[str] = None):
424
- self.parent_name = parent_name
425
- self.current_class = None
426
-
427
- def visit_ClassDef(self, node):
428
- # Extract class information
429
- class_node = CodeNode(
430
- file_path=str(file_path),
431
- node_type="class",
432
- name=node.name,
433
- line_start=node.lineno,
434
- line_end=node.end_lineno or node.lineno,
435
- has_docstring=bool(ast.get_docstring(node)),
436
- decorators=[self._decorator_name(d) for d in node.decorator_list],
437
- parent=self.parent_name,
438
- complexity=self._calculate_complexity(node),
439
- signature=self._get_class_signature(node),
440
- )
441
-
442
- nodes.append(class_node)
443
-
444
- # Emit event if emitter is available
445
- if self.emitter:
446
- self.emitter.emit_node(
447
- CodeNodeEvent(
448
- file_path=str(file_path),
449
- node_type="class",
450
- name=node.name,
451
- line_start=node.lineno,
452
- line_end=node.end_lineno or node.lineno,
453
- complexity=class_node.complexity,
454
- has_docstring=class_node.has_docstring,
455
- decorators=class_node.decorators,
456
- parent=self.parent_name,
457
- children_count=len(node.body),
458
- )
459
- )
460
-
461
- # Visit class members
462
- old_class = self.current_class
463
- self.current_class = node.name
464
- for child in node.body:
465
- if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
466
- self.visit_FunctionDef(child, is_method=True)
467
- self.current_class = old_class
468
-
469
- def visit_FunctionDef(self, node, is_method=False):
470
- # Determine node type
471
- node_type = "method" if is_method else "function"
472
- parent = self.current_class if is_method else self.parent_name
473
-
474
- # Extract function information
475
- func_node = CodeNode(
476
- file_path=str(file_path),
477
- node_type=node_type,
478
- name=node.name,
479
- line_start=node.lineno,
480
- line_end=node.end_lineno or node.lineno,
481
- has_docstring=bool(ast.get_docstring(node)),
482
- decorators=[self._decorator_name(d) for d in node.decorator_list],
483
- parent=parent,
484
- complexity=self._calculate_complexity(node),
485
- signature=self._get_function_signature(node),
486
- )
487
-
488
- nodes.append(func_node)
489
-
490
- # Emit event if emitter is available
491
- if self.emitter:
492
- self.emitter.emit_node(
493
- CodeNodeEvent(
494
- file_path=str(file_path),
495
- node_type=node_type,
496
- name=node.name,
497
- line_start=node.lineno,
498
- line_end=node.end_lineno or node.lineno,
499
- complexity=func_node.complexity,
500
- has_docstring=func_node.has_docstring,
501
- decorators=func_node.decorators,
502
- parent=parent,
503
- children_count=0,
504
- )
505
- )
506
-
507
- def visit_Assign(self, node):
508
- # Handle module-level variable assignments
509
- if self.current_class is None: # Only module-level assignments
510
- for target in node.targets:
511
- if isinstance(target, ast.Name):
512
- var_node = CodeNode(
513
- file_path=str(file_path),
514
- node_type="variable",
515
- name=target.id,
516
- line_start=node.lineno,
517
- line_end=node.end_lineno or node.lineno,
518
- parent=self.parent_name,
519
- complexity=0,
520
- signature=f"{target.id} = ...",
521
- )
522
- nodes.append(var_node)
523
-
524
- # Emit event if emitter is available
525
- if self.emitter:
526
- self.emitter.emit_node(
527
- CodeNodeEvent(
528
- file_path=str(file_path),
529
- node_type="variable",
530
- name=target.id,
531
- line_start=node.lineno,
532
- line_end=node.end_lineno or node.lineno,
533
- parent=self.parent_name,
534
- )
535
- )
536
-
537
- def visit_AsyncFunctionDef(self, node):
538
- self.visit_FunctionDef(node)
539
-
540
- def _decorator_name(self, decorator):
541
- """Extract decorator name from AST node."""
542
- if isinstance(decorator, ast.Name):
543
- return decorator.id
544
- if isinstance(decorator, ast.Call):
545
- if isinstance(decorator.func, ast.Name):
546
- return decorator.func.id
547
- if isinstance(decorator.func, ast.Attribute):
548
- return decorator.func.attr
549
- return "unknown"
550
-
551
- def _calculate_complexity(self, node):
552
- """Calculate cyclomatic complexity of a node."""
553
- complexity = 1 # Base complexity
554
-
555
- for child in ast.walk(node):
556
- if isinstance(
557
- child, (ast.If, ast.While, ast.For, ast.ExceptHandler)
558
- ):
559
- complexity += 1
560
- elif isinstance(child, ast.BoolOp):
561
- complexity += len(child.values) - 1
562
-
563
- return complexity
564
-
565
- def _get_function_signature(self, node):
566
- """Extract function signature."""
567
- args = []
568
- for arg in node.args.args:
569
- args.append(arg.arg)
570
- return f"{node.name}({', '.join(args)})"
571
-
572
- def _get_class_signature(self, node):
573
- """Extract class signature."""
574
- bases = []
575
- for base in node.bases:
576
- if isinstance(base, ast.Name):
577
- bases.append(base.id)
578
- base_str = f"({', '.join(bases)})" if bases else ""
579
- return f"class {node.name}{base_str}"
580
-
581
- # Extract imports
582
- for node in ast.walk(tree):
583
- if isinstance(node, ast.Import):
584
- for alias in node.names:
585
- import_node = CodeNode(
586
- file_path=str(file_path),
587
- node_type="import",
588
- name=alias.name,
589
- line_start=node.lineno,
590
- line_end=node.end_lineno or node.lineno,
591
- signature=f"import {alias.name}",
592
- )
593
- nodes.append(import_node)
594
-
595
- elif isinstance(node, ast.ImportFrom):
596
- module = node.module or ""
597
- for alias in node.names:
598
- import_node = CodeNode(
599
- file_path=str(file_path),
600
- node_type="import",
601
- name=f"{module}.{alias.name}",
602
- line_start=node.lineno,
603
- line_end=node.end_lineno or node.lineno,
604
- signature=f"from {module} import {alias.name}",
605
- )
606
- nodes.append(import_node)
607
-
608
- # Visit all nodes
609
- visitor = NodeVisitor()
610
- visitor.emitter = self.emitter
611
- visitor.visit(tree)
612
-
613
- return nodes
614
-
615
- def _get_assignment_signature(self, node: ast.Assign, var_name: str) -> str:
616
- """Get assignment signature string."""
617
- try:
618
- # Try to get a simple representation of the value
619
- if isinstance(node.value, ast.Constant):
620
- if isinstance(node.value.value, str):
621
- return f'{var_name} = "{node.value.value}"'
622
- return f"{var_name} = {node.value.value}"
623
- if isinstance(node.value, ast.Name):
624
- return f"{var_name} = {node.value.id}"
625
- if isinstance(node.value, ast.List):
626
- return f"{var_name} = [...]"
627
- if isinstance(node.value, ast.Dict):
628
- return f"{var_name} = {{...}}"
629
- return f"{var_name} = ..."
630
- except Exception:
631
- return f"{var_name} = ..."
632
-
633
-
634
- class MultiLanguageAnalyzer:
635
- """Analyzes multiple programming languages using tree-sitter.
636
-
637
- WHY: Tree-sitter provides consistent parsing across multiple languages,
638
- allowing us to support JavaScript, TypeScript, and other languages.
639
- """
640
-
641
- LANGUAGE_PARSERS: ClassVar[dict] = {
642
- "python": "tree_sitter_python",
643
- "javascript": "tree_sitter_javascript",
644
- "typescript": "tree_sitter_typescript",
645
- }
646
-
647
- def __init__(self, emitter: Optional[CodeTreeEventEmitter] = None):
648
- self.logger = get_logger(__name__)
649
- self.emitter = emitter
650
- self.parsers = {}
651
- self._init_parsers()
652
-
653
- def _init_parsers(self):
654
- """Initialize tree-sitter parsers for supported languages."""
655
- if not TREE_SITTER_AVAILABLE:
656
- self.logger.warning(
657
- "tree-sitter not available - multi-language support disabled"
658
- )
659
- return
660
-
661
- for lang, module_name in self.LANGUAGE_PARSERS.items():
662
- try:
663
- # Dynamic import of language module
664
- module = __import__(module_name)
665
- parser = tree_sitter.Parser()
666
- # Different tree-sitter versions have different APIs
667
- if hasattr(parser, "set_language"):
668
- parser.set_language(tree_sitter.Language(module.language()))
669
- else:
670
- # Newer API
671
- lang_obj = tree_sitter.Language(module.language())
672
- parser = tree_sitter.Parser(lang_obj)
673
- self.parsers[lang] = parser
674
- except (ImportError, AttributeError) as e:
675
- # Silently skip unavailable parsers - will fall back to basic file discovery
676
- self.logger.debug(f"Language parser not available for {lang}: {e}")
677
-
678
- def analyze_file(self, file_path: Path, language: str) -> List[CodeNode]:
679
- """Analyze a file using tree-sitter.
680
-
681
- Args:
682
- file_path: Path to source file
683
- language: Programming language
684
-
685
- Returns:
686
- List of code nodes found in the file
687
- """
688
- if language not in self.parsers:
689
- # No parser available - return empty list to fall back to basic discovery
690
- self.logger.debug(
691
- f"No parser available for language: {language}, using basic file discovery"
692
- )
693
- return []
694
-
695
- nodes = []
696
-
697
- try:
698
- with file_path.open("rb") as f:
699
- source = f.read()
700
-
701
- parser = self.parsers[language]
702
- tree = parser.parse(source)
703
-
704
- # Extract nodes based on language
705
- if language in {"javascript", "typescript"}:
706
- nodes = self._extract_js_nodes(tree, file_path, source)
707
- else:
708
- nodes = self._extract_generic_nodes(tree, file_path, source, language)
709
-
710
- except Exception as e:
711
- self.logger.error(f"Error analyzing {file_path}: {e}")
712
- if self.emitter:
713
- self.emitter.emit_error(str(file_path), str(e))
714
-
715
- return nodes
716
-
717
- def _extract_js_nodes(self, tree, file_path: Path, source: bytes) -> List[CodeNode]:
718
- """Extract nodes from JavaScript/TypeScript files."""
719
- nodes = []
720
-
721
- def walk_tree(node, parent_name=None):
722
- if node.type == "class_declaration":
723
- # Extract class
724
- name_node = node.child_by_field_name("name")
725
- if name_node:
726
- class_node = CodeNode(
727
- file_path=str(file_path),
728
- node_type="class",
729
- name=source[name_node.start_byte : name_node.end_byte].decode(
730
- "utf-8"
731
- ),
732
- line_start=node.start_point[0] + 1,
733
- line_end=node.end_point[0] + 1,
734
- parent=parent_name,
735
- language="javascript",
736
- )
737
- nodes.append(class_node)
738
-
739
- if self.emitter:
740
- self.emitter.emit_node(
741
- CodeNodeEvent(
742
- file_path=str(file_path),
743
- node_type="class",
744
- name=class_node.name,
745
- line_start=class_node.line_start,
746
- line_end=class_node.line_end,
747
- parent=parent_name,
748
- language="javascript",
749
- )
750
- )
751
-
752
- elif node.type in (
753
- "function_declaration",
754
- "arrow_function",
755
- "method_definition",
756
- ):
757
- # Extract function
758
- name_node = node.child_by_field_name("name")
759
- if name_node:
760
- func_name = source[
761
- name_node.start_byte : name_node.end_byte
762
- ].decode("utf-8")
763
- func_node = CodeNode(
764
- file_path=str(file_path),
765
- node_type=(
766
- "function" if node.type != "method_definition" else "method"
767
- ),
768
- name=func_name,
769
- line_start=node.start_point[0] + 1,
770
- line_end=node.end_point[0] + 1,
771
- parent=parent_name,
772
- language="javascript",
773
- )
774
- nodes.append(func_node)
775
-
776
- if self.emitter:
777
- self.emitter.emit_node(
778
- CodeNodeEvent(
779
- file_path=str(file_path),
780
- node_type=func_node.node_type,
781
- name=func_name,
782
- line_start=func_node.line_start,
783
- line_end=func_node.line_end,
784
- parent=parent_name,
785
- language="javascript",
786
- )
787
- )
788
-
789
- # Recursively walk children
790
- for child in node.children:
791
- walk_tree(child, parent_name)
792
-
793
- walk_tree(tree.root_node)
794
- return nodes
795
-
796
- def _extract_generic_nodes(
797
- self, tree, file_path: Path, source: bytes, language: str
798
- ) -> List[CodeNode]:
799
- """Generic node extraction for other languages."""
800
- # Simple generic extraction - can be enhanced per language
801
- nodes = []
802
-
803
- def walk_tree(node):
804
- # Look for common patterns
805
- if "class" in node.type or "struct" in node.type:
806
- nodes.append(
807
- CodeNode(
808
- file_path=str(file_path),
809
- node_type="class",
810
- name=f"{node.type}_{node.start_point[0]}",
811
- line_start=node.start_point[0] + 1,
812
- line_end=node.end_point[0] + 1,
813
- language=language,
814
- )
815
- )
816
- elif "function" in node.type or "method" in node.type:
817
- nodes.append(
818
- CodeNode(
819
- file_path=str(file_path),
820
- node_type="function",
821
- name=f"{node.type}_{node.start_point[0]}",
822
- line_start=node.start_point[0] + 1,
823
- line_end=node.end_point[0] + 1,
824
- language=language,
825
- )
826
- )
827
-
828
- for child in node.children:
829
- walk_tree(child)
830
-
831
- walk_tree(tree.root_node)
832
- return nodes
833
-
834
-
835
- class CodeTreeAnalyzer:
836
- """Main analyzer that coordinates language-specific analyzers.
837
-
838
- WHY: Provides a unified interface for analyzing codebases with multiple
839
- languages, handling caching and incremental processing.
840
- """
841
-
842
- # Define code file extensions at class level for directory filtering
843
- CODE_EXTENSIONS: ClassVar[set] = {
844
- ".py",
845
- ".js",
846
- ".jsx",
847
- ".ts",
848
- ".tsx",
849
- ".mjs", # Added missing extension
850
- ".cjs", # Added missing extension
851
- ".java",
852
- ".cpp",
853
- ".c",
854
- ".h",
855
- ".hpp",
856
- ".cs",
857
- ".go",
858
- ".rs",
859
- ".rb",
860
- ".php",
861
- ".swift",
862
- ".kt",
863
- ".scala",
864
- ".r",
865
- ".m",
866
- ".mm",
867
- ".sh",
868
- ".bash",
869
- ".zsh",
870
- ".fish",
871
- ".ps1",
872
- ".bat",
873
- ".cmd",
874
- ".sql",
875
- ".html",
876
- ".css",
877
- ".scss",
878
- ".sass",
879
- ".less",
880
- ".xml",
881
- ".json",
882
- ".yaml",
883
- ".yml",
884
- ".toml",
885
- ".ini",
886
- ".cfg",
887
- ".conf",
888
- ".md",
889
- ".rst",
890
- ".txt",
891
- }
892
-
893
- # File extensions to language mapping
894
- LANGUAGE_MAP: ClassVar[dict] = {
895
- ".py": "python",
896
- ".js": "javascript",
897
- ".jsx": "javascript",
898
- ".ts": "typescript",
899
- ".tsx": "typescript",
900
- ".mjs": "javascript",
901
- ".cjs": "javascript",
902
- }
903
-
904
- def __init__(
905
- self,
906
- emit_events: bool = True,
907
- cache_dir: Optional[Path] = None,
908
- emitter: Optional[CodeTreeEventEmitter] = None,
909
- ):
910
- """Initialize the code tree analyzer.
911
-
912
- Args:
913
- emit_events: Whether to emit Socket.IO events
914
- cache_dir: Directory for caching analysis results
915
- emitter: Optional event emitter to use (creates one if not provided)
916
- """
917
- self.logger = get_logger(__name__)
918
- self.emit_events = emit_events
919
- self.cache_dir = cache_dir or Path.home() / ".claude-mpm" / "code-cache"
920
-
921
- # Initialize gitignore manager (always filters dotfiles)
922
- self.gitignore_manager = GitignoreManager()
923
- self._last_working_dir = None
924
-
925
- # Use provided emitter or create one
926
- if emitter:
927
- self.emitter = emitter
928
- elif emit_events:
929
- self.emitter = CodeTreeEventEmitter(use_stdout=True)
930
- else:
931
- self.emitter = None
932
-
933
- # Initialize language analyzers
934
- self.python_analyzer = PythonAnalyzer(self.emitter)
935
- self.multi_lang_analyzer = MultiLanguageAnalyzer(self.emitter)
936
-
937
- # For JavaScript/TypeScript
938
- self.javascript_analyzer = self.multi_lang_analyzer
939
- self.generic_analyzer = self.multi_lang_analyzer
940
-
941
- # Cache for processed files
942
- self.cache = {}
943
- self._load_cache()
944
-
945
- def analyze_directory(
946
- self,
947
- directory: Path,
948
- languages: Optional[List[str]] = None,
949
- ignore_patterns: Optional[List[str]] = None,
950
- max_depth: Optional[int] = None,
951
- ) -> Dict[str, Any]:
952
- """Analyze a directory and build code tree.
953
-
954
- Args:
955
- directory: Directory to analyze
956
- languages: Languages to include (None for all)
957
- ignore_patterns: Patterns to ignore
958
- max_depth: Maximum directory depth
959
-
960
- Returns:
961
- Dictionary containing the code tree and statistics
962
- """
963
- if self.emitter:
964
- self.emitter.start()
965
-
966
- start_time = time.time()
967
- all_nodes = []
968
- files_processed = 0
969
- total_files = 0
970
-
971
- # Collect files to process
972
- files_to_process = []
973
- for ext, lang in self.LANGUAGE_MAP.items():
974
- if languages and lang not in languages:
975
- continue
976
-
977
- for file_path in directory.rglob(f"*{ext}"):
978
- # Use gitignore manager for filtering with directory as working dir
979
- if self.gitignore_manager.should_ignore(file_path, directory):
980
- continue
981
-
982
- # Also check additional patterns
983
- if ignore_patterns and any(
984
- p in str(file_path) for p in ignore_patterns
985
- ):
986
- continue
987
-
988
- # Check max depth
989
- if max_depth:
990
- depth = len(file_path.relative_to(directory).parts) - 1
991
- if depth > max_depth:
992
- continue
993
-
994
- files_to_process.append((file_path, lang))
995
-
996
- total_files = len(files_to_process)
997
-
998
- # Process files
999
- for file_path, language in files_to_process:
1000
- # Check cache
1001
- file_hash = self._get_file_hash(file_path)
1002
- cache_key = f"{file_path}:{file_hash}"
1003
-
1004
- if cache_key in self.cache:
1005
- nodes = self.cache[cache_key]
1006
- self.logger.debug(f"Using cached results for {file_path}")
1007
- else:
1008
- # Emit file start event
1009
- if self.emitter:
1010
- self.emitter.emit_file_start(str(file_path), language)
1011
-
1012
- file_start = time.time()
1013
-
1014
- # Analyze based on language
1015
- if language == "python":
1016
- nodes = self.python_analyzer.analyze_file(file_path)
1017
- else:
1018
- nodes = self.multi_lang_analyzer.analyze_file(file_path, language)
1019
-
1020
- # If no nodes found and we have a valid language, emit basic file info
1021
- if not nodes and language != "unknown":
1022
- self.logger.debug(
1023
- f"No AST nodes found for {file_path}, using basic discovery"
1024
- )
1025
-
1026
- # Cache results
1027
- self.cache[cache_key] = nodes
1028
-
1029
- # Emit file complete event
1030
- if self.emitter:
1031
- self.emitter.emit_file_complete(
1032
- str(file_path), len(nodes), time.time() - file_start
1033
- )
1034
-
1035
- all_nodes.extend(nodes)
1036
- files_processed += 1
1037
-
1038
- # Emit progress
1039
- if self.emitter and files_processed % 10 == 0:
1040
- self.emitter.emit_progress(
1041
- files_processed, total_files, f"Processing {file_path.name}"
1042
- )
1043
-
1044
- # Build tree structure
1045
- tree = self._build_tree(all_nodes, directory)
1046
-
1047
- # Calculate statistics
1048
- duration = time.time() - start_time
1049
- stats = {
1050
- "files_processed": files_processed,
1051
- "total_nodes": len(all_nodes),
1052
- "duration": duration,
1053
- "classes": sum(1 for n in all_nodes if n.node_type == "class"),
1054
- "functions": sum(
1055
- 1 for n in all_nodes if n.node_type in ("function", "method")
1056
- ),
1057
- "imports": sum(1 for n in all_nodes if n.node_type == "import"),
1058
- "languages": list(
1059
- {n.language for n in all_nodes if hasattr(n, "language")}
1060
- ),
1061
- "avg_complexity": (
1062
- sum(n.complexity for n in all_nodes) / len(all_nodes)
1063
- if all_nodes
1064
- else 0
1065
- ),
1066
- }
1067
-
1068
- # Save cache
1069
- self._save_cache()
1070
-
1071
- # Stop emitter
1072
- if self.emitter:
1073
- self.emitter.stop()
1074
-
1075
- return {"tree": tree, "nodes": all_nodes, "stats": stats}
1076
-
1077
- def _should_ignore(self, file_path: Path, patterns: Optional[List[str]]) -> bool:
1078
- """Check if file should be ignored.
1079
-
1080
- Uses GitignoreManager for proper pattern matching.
1081
- """
1082
- # Get the working directory (use parent for files, self for directories)
1083
- if file_path.is_file():
1084
- working_dir = file_path.parent
1085
- else:
1086
- # For directories during discovery, use the parent
1087
- working_dir = (
1088
- file_path.parent if file_path.parent != file_path else Path.cwd()
1089
- )
1090
-
1091
- # Use gitignore manager for checking
1092
- if self.gitignore_manager.should_ignore(file_path, working_dir):
1093
- return True
1094
-
1095
- # Also check any additional patterns provided
1096
- if patterns:
1097
- path_str = str(file_path)
1098
- return any(pattern in path_str for pattern in patterns)
1099
-
1100
- return False
1101
-
1102
- def _get_file_hash(self, file_path: Path) -> str:
1103
- """Get hash of file contents for caching."""
1104
- hasher = hashlib.md5()
1105
- with file_path.open("rb") as f:
1106
- hasher.update(f.read())
1107
- return hasher.hexdigest()
1108
-
1109
- def _build_tree(self, nodes: List[CodeNode], root_dir: Path) -> Dict[str, Any]:
1110
- """Build hierarchical tree structure from flat nodes list."""
1111
- tree = {
1112
- "name": root_dir.name,
1113
- "type": "directory",
1114
- "path": str(root_dir),
1115
- "children": [],
1116
- }
1117
-
1118
- # Group nodes by file
1119
- files_map = {}
1120
- for node in nodes:
1121
- if node.file_path not in files_map:
1122
- files_map[node.file_path] = {
1123
- "name": Path(node.file_path).name,
1124
- "type": "file",
1125
- "path": node.file_path,
1126
- "children": [],
1127
- }
1128
-
1129
- # Add node to file
1130
- node_dict = {
1131
- "name": node.name,
1132
- "type": node.node_type,
1133
- "line_start": node.line_start,
1134
- "line_end": node.line_end,
1135
- "complexity": node.complexity,
1136
- "has_docstring": node.has_docstring,
1137
- "decorators": node.decorators,
1138
- "signature": node.signature,
1139
- }
1140
- files_map[node.file_path]["children"].append(node_dict)
1141
-
1142
- # Build directory structure
1143
- for file_path, file_node in files_map.items():
1144
- rel_path = Path(file_path).relative_to(root_dir)
1145
- parts = rel_path.parts
1146
-
1147
- current = tree
1148
- for part in parts[:-1]:
1149
- # Find or create directory
1150
- dir_node = None
1151
- for child in current["children"]:
1152
- if child["type"] == "directory" and child["name"] == part:
1153
- dir_node = child
1154
- break
1155
-
1156
- if not dir_node:
1157
- dir_node = {"name": part, "type": "directory", "children": []}
1158
- current["children"].append(dir_node)
1159
-
1160
- current = dir_node
1161
-
1162
- # Add file to current directory
1163
- current["children"].append(file_node)
1164
-
1165
- return tree
1166
-
1167
- def _load_cache(self):
1168
- """Load cache from disk."""
1169
- cache_file = self.cache_dir / "code_tree_cache.json"
1170
- if cache_file.exists():
1171
- try:
1172
- with cache_file.open() as f:
1173
- cache_data = json.load(f)
1174
- # Reconstruct CodeNode objects
1175
- for key, nodes_data in cache_data.items():
1176
- self.cache[key] = [
1177
- CodeNode(**node_data) for node_data in nodes_data
1178
- ]
1179
- self.logger.info(f"Loaded cache with {len(self.cache)} entries")
1180
- except Exception as e:
1181
- self.logger.warning(f"Failed to load cache: {e}")
1182
-
1183
- def _save_cache(self):
1184
- """Save cache to disk."""
1185
- self.cache_dir.mkdir(parents=True, exist_ok=True)
1186
- cache_file = self.cache_dir / "code_tree_cache.json"
1187
-
1188
- try:
1189
- # Convert CodeNode objects to dictionaries
1190
- cache_data = {}
1191
- for key, nodes in self.cache.items():
1192
- cache_data[key] = [
1193
- {
1194
- "file_path": n.file_path,
1195
- "node_type": n.node_type,
1196
- "name": n.name,
1197
- "line_start": n.line_start,
1198
- "line_end": n.line_end,
1199
- "complexity": n.complexity,
1200
- "has_docstring": n.has_docstring,
1201
- "decorators": n.decorators,
1202
- "parent": n.parent,
1203
- "language": n.language,
1204
- "signature": n.signature,
1205
- }
1206
- for n in nodes
1207
- ]
1208
-
1209
- with cache_file.open("w") as f:
1210
- json.dump(cache_data, f, indent=2)
1211
-
1212
- self.logger.info(f"Saved cache with {len(self.cache)} entries")
1213
- except Exception as e:
1214
- self.logger.warning(f"Failed to save cache: {e}")
1215
-
1216
- def has_code_files(
1217
- self, directory: Path, depth: int = 5, current_depth: int = 0
1218
- ) -> bool:
1219
- """Check if directory contains code files up to 5 levels deep.
1220
-
1221
- Args:
1222
- directory: Directory to check
1223
- depth: Maximum depth to search
1224
- current_depth: Current recursion depth
1225
-
1226
- Returns:
1227
- True if directory contains code files within depth levels
1228
- """
1229
- if current_depth >= depth:
1230
- return False
1231
-
1232
- # Skip checking these directories entirely
1233
- SKIP_DIRS = {
1234
- "node_modules",
1235
- "__pycache__",
1236
- ".git",
1237
- ".venv",
1238
- "venv",
1239
- "dist",
1240
- "build",
1241
- ".tox",
1242
- "htmlcov",
1243
- ".pytest_cache",
1244
- ".mypy_cache",
1245
- "coverage",
1246
- ".idea",
1247
- ".vscode",
1248
- "env",
1249
- ".coverage",
1250
- "__MACOSX",
1251
- ".ipynb_checkpoints",
1252
- }
1253
- # Skip directories in the skip list or egg-info directories
1254
- if directory.name in SKIP_DIRS or directory.name.endswith(".egg-info"):
1255
- return False
1256
-
1257
- try:
1258
- for item in directory.iterdir():
1259
- # Skip hidden items in scan
1260
- if item.name.startswith("."):
1261
- continue
1262
-
1263
- if item.is_file():
1264
- # Check if it's a code file
1265
- ext = item.suffix.lower()
1266
- if ext in self.CODE_EXTENSIONS:
1267
- return True
1268
- elif item.is_dir() and current_depth < depth - 1:
1269
- # Skip egg-info directories in the recursive check too
1270
- if item.name.endswith(".egg-info"):
1271
- continue
1272
- if self.has_code_files(item, depth, current_depth + 1):
1273
- return True
1274
-
1275
- except (PermissionError, OSError):
1276
- pass
1277
-
1278
- return False
1279
-
1280
- def discover_top_level(
1281
- self, directory: Path, ignore_patterns: Optional[List[str]] = None
1282
- ) -> Dict[str, Any]:
1283
- """Discover only top-level directories and files for lazy loading.
1284
-
1285
- Args:
1286
- directory: Root directory to discover
1287
- ignore_patterns: Patterns to ignore
1288
-
1289
- Returns:
1290
- Dictionary with top-level structure
1291
- """
1292
- # CRITICAL FIX: Use the directory parameter as the base for relative paths
1293
- # NOT the current working directory. This ensures we only show items
1294
- # within the requested directory, not parent directories.
1295
- Path(directory).absolute()
1296
-
1297
- # Emit discovery start event
1298
- if self.emitter:
1299
- from datetime import datetime, timezone
1300
-
1301
- self.emitter.emit(
1302
- "info",
1303
- {
1304
- "type": "discovery.start",
1305
- "action": "scanning_directory",
1306
- "path": str(directory),
1307
- "message": f"Starting discovery of {directory.name}",
1308
- "timestamp": datetime.now(timezone.utc).isoformat(),
1309
- },
1310
- )
1311
-
1312
- result = {
1313
- "path": str(directory),
1314
- "name": directory.name,
1315
- "type": "directory",
1316
- "children": [],
1317
- }
1318
-
1319
- try:
1320
- # Clear cache if working directory changed
1321
- if self._last_working_dir != directory:
1322
- self.gitignore_manager.clear_cache()
1323
- self._last_working_dir = directory
1324
-
1325
- # Get immediate children only (no recursion)
1326
- files_count = 0
1327
- dirs_count = 0
1328
- ignored_count = 0
1329
-
1330
- for item in directory.iterdir():
1331
- # Use gitignore manager for filtering with the directory as working dir
1332
- if self.gitignore_manager.should_ignore(item, directory):
1333
- if self.emitter:
1334
- from datetime import datetime
1335
-
1336
- self.emitter.emit(
1337
- "info",
1338
- {
1339
- "type": "filter.gitignore",
1340
- "path": str(item),
1341
- "reason": "gitignore pattern",
1342
- "message": f"Ignored by gitignore: {item.name}",
1343
- "timestamp": datetime.now(timezone.utc).isoformat(),
1344
- },
1345
- )
1346
- ignored_count += 1
1347
- continue
1348
-
1349
- # Also check additional patterns if provided
1350
- if ignore_patterns and any(p in str(item) for p in ignore_patterns):
1351
- if self.emitter:
1352
- from datetime import datetime
1353
-
1354
- self.emitter.emit(
1355
- "info",
1356
- {
1357
- "type": "filter.pattern",
1358
- "path": str(item),
1359
- "reason": "custom pattern",
1360
- "message": f"Ignored by pattern: {item.name}",
1361
- "timestamp": datetime.now(timezone.utc).isoformat(),
1362
- },
1363
- )
1364
- ignored_count += 1
1365
- continue
1366
-
1367
- if item.is_dir():
1368
- # Check if directory contains code files (recursively checking subdirectories)
1369
- # Important: We want to include directories even if they only have code
1370
- # in subdirectories (like src/claude_mpm/*.py)
1371
- if not self.has_code_files(item, depth=5):
1372
- if self.emitter:
1373
- from datetime import datetime
1374
-
1375
- self.emitter.emit(
1376
- "info",
1377
- {
1378
- "type": "filter.no_code",
1379
- "path": str(item.name),
1380
- "reason": "no code files",
1381
- "message": f"Skipped directory without code: {item.name}",
1382
- "timestamp": datetime.now(timezone.utc).isoformat(),
1383
- },
1384
- )
1385
- ignored_count += 1
1386
- continue
1387
-
1388
- # Directory - return just the item name
1389
- # The frontend will construct the full path by combining parent path with child name
1390
- path_str = item.name
1391
-
1392
- # Emit directory found event
1393
- if self.emitter:
1394
- from datetime import datetime
1395
-
1396
- self.emitter.emit(
1397
- "info",
1398
- {
1399
- "type": "discovery.directory",
1400
- "path": str(item),
1401
- "message": f"Found directory: {item.name}",
1402
- "timestamp": datetime.now(timezone.utc).isoformat(),
1403
- },
1404
- )
1405
- dirs_count += 1
1406
-
1407
- child = {
1408
- "path": path_str,
1409
- "name": item.name,
1410
- "type": "directory",
1411
- "discovered": False,
1412
- "children": [],
1413
- }
1414
- result["children"].append(child)
1415
-
1416
- # Don't emit directory discovered event here with empty children
1417
- # The actual discovery will happen when the directory is clicked
1418
- # This prevents confusing the frontend with empty directory events
1419
- # if self.emitter:
1420
- # self.emitter.emit_directory_discovered(path_str, [])
1421
-
1422
- elif item.is_file():
1423
- # Check if it's a supported code file or a special file we want to show
1424
- if item.suffix in self.supported_extensions or item.name in [
1425
- ".gitignore",
1426
- ".env.example",
1427
- ".env.sample",
1428
- ]:
1429
- # File - mark for lazy analysis
1430
- language = self._get_language(item)
1431
-
1432
- # File path should be just the item name
1433
- # The frontend will construct the full path by combining parent path with child name
1434
- path_str = item.name
1435
-
1436
- # Emit file found event
1437
- if self.emitter:
1438
- from datetime import datetime
1439
-
1440
- self.emitter.emit(
1441
- "info",
1442
- {
1443
- "type": "discovery.file",
1444
- "path": str(item),
1445
- "language": language,
1446
- "size": item.stat().st_size,
1447
- "message": f"Found file: {item.name} ({language})",
1448
- "timestamp": datetime.now(timezone.utc).isoformat(),
1449
- },
1450
- )
1451
- files_count += 1
1452
-
1453
- child = {
1454
- "path": path_str,
1455
- "name": item.name,
1456
- "type": "file",
1457
- "language": language,
1458
- "size": item.stat().st_size,
1459
- "analyzed": False,
1460
- }
1461
- result["children"].append(child)
1462
-
1463
- if self.emitter:
1464
- self.emitter.emit_file_discovered(
1465
- path_str, language, item.stat().st_size
1466
- )
1467
-
1468
- except PermissionError as e:
1469
- self.logger.warning(f"Permission denied accessing {directory}: {e}")
1470
- if self.emitter:
1471
- self.emitter.emit_error(str(directory), f"Permission denied: {e}")
1472
-
1473
- # Emit discovery complete event with stats
1474
- if self.emitter:
1475
- from datetime import datetime
1476
-
1477
- self.emitter.emit(
1478
- "info",
1479
- {
1480
- "type": "discovery.complete",
1481
- "path": str(directory),
1482
- "stats": {
1483
- "files": files_count,
1484
- "directories": dirs_count,
1485
- "ignored": ignored_count,
1486
- },
1487
- "message": f"Discovery complete: {files_count} files, {dirs_count} directories, {ignored_count} ignored",
1488
- "timestamp": datetime.now(timezone.utc).isoformat(),
1489
- },
1490
- )
1491
-
1492
- return result
1493
-
1494
- def discover_directory(
1495
- self, dir_path: str, ignore_patterns: Optional[List[str]] = None
1496
- ) -> Dict[str, Any]:
1497
- """Discover contents of a specific directory for lazy loading.
1498
-
1499
- Args:
1500
- dir_path: Directory path to discover
1501
- ignore_patterns: Patterns to ignore
1502
-
1503
- Returns:
1504
- Dictionary with directory contents
1505
- """
1506
- directory = Path(dir_path)
1507
- if not directory.exists() or not directory.is_dir():
1508
- return {"error": f"Invalid directory: {dir_path}"}
1509
-
1510
- # Clear cache if working directory changed
1511
- if self._last_working_dir != directory.parent:
1512
- self.gitignore_manager.clear_cache()
1513
- self._last_working_dir = directory.parent
1514
-
1515
- # The discover_top_level method will emit all the INFO events
1516
- return self.discover_top_level(directory, ignore_patterns)
1517
-
1518
- def analyze_file(self, file_path: str) -> Dict[str, Any]:
1519
- """Analyze a specific file and return its AST structure.
1520
-
1521
- Args:
1522
- file_path: Path to file to analyze
1523
-
1524
- Returns:
1525
- Dictionary with file analysis results
1526
- """
1527
- path = Path(file_path)
1528
- if not path.exists() or not path.is_file():
1529
- return {"error": f"Invalid file: {file_path}"}
1530
-
1531
- language = self._get_language(path)
1532
- self._emit_analysis_start(path, language)
1533
-
1534
- # Check cache
1535
- file_hash = self._get_file_hash(path)
1536
- cache_key = f"{file_path}:{file_hash}"
1537
-
1538
- if cache_key in self.cache:
1539
- nodes = self.cache[cache_key]
1540
- self._emit_cache_hit(path)
1541
- filtered_nodes = self._filter_nodes(nodes)
1542
- else:
1543
- nodes, filtered_nodes, duration = self._analyze_and_cache_file(
1544
- path, language, cache_key
1545
- )
1546
- self._emit_analysis_complete(path, filtered_nodes, duration)
1547
-
1548
- # Prepare final data structures
1549
- final_nodes = self._prepare_final_nodes(nodes, filtered_nodes)
1550
- elements = self._convert_nodes_to_elements(final_nodes)
1551
-
1552
- return self._build_result(file_path, language, final_nodes, elements)
1553
-
1554
- def _emit_analysis_start(self, path: Path, language: str) -> None:
1555
- """Emit analysis start event."""
1556
- if self.emitter:
1557
- from datetime import datetime
1558
-
1559
- self.emitter.emit(
1560
- "info",
1561
- {
1562
- "type": "analysis.start",
1563
- "file": str(path),
1564
- "language": language,
1565
- "message": f"Analyzing: {path.name}",
1566
- "timestamp": datetime.now(timezone.utc).isoformat(),
1567
- },
1568
- )
1569
-
1570
- def _emit_cache_hit(self, path: Path) -> None:
1571
- """Emit cache hit event."""
1572
- if self.emitter:
1573
- from datetime import datetime
1574
-
1575
- self.emitter.emit(
1576
- "info",
1577
- {
1578
- "type": "cache.hit",
1579
- "file": str(path),
1580
- "message": f"Using cached analysis for {path.name}",
1581
- "timestamp": datetime.now(timezone.utc).isoformat(),
1582
- },
1583
- )
1584
-
1585
- def _emit_cache_miss(self, path: Path) -> None:
1586
- """Emit cache miss event."""
1587
- if self.emitter:
1588
- from datetime import datetime
1589
-
1590
- self.emitter.emit(
1591
- "info",
1592
- {
1593
- "type": "cache.miss",
1594
- "file": str(path),
1595
- "message": f"Cache miss, analyzing fresh: {path.name}",
1596
- "timestamp": datetime.now(timezone.utc).isoformat(),
1597
- },
1598
- )
1599
-
1600
- def _emit_parsing_start(self, path: Path) -> None:
1601
- """Emit parsing start event."""
1602
- if self.emitter:
1603
- from datetime import datetime
1604
-
1605
- self.emitter.emit(
1606
- "info",
1607
- {
1608
- "type": "analysis.parse",
1609
- "file": str(path),
1610
- "message": f"Parsing file content: {path.name}",
1611
- "timestamp": datetime.now(timezone.utc).isoformat(),
1612
- },
1613
- )
1614
-
1615
- def _emit_node_found(self, node: CodeNode, path: Path) -> None:
1616
- """Emit node found event."""
1617
- if self.emitter:
1618
- from datetime import datetime
1619
-
1620
- self.emitter.emit(
1621
- "info",
1622
- {
1623
- "type": f"analysis.{node.node_type}",
1624
- "name": node.name,
1625
- "file": str(path),
1626
- "line_start": node.line_start,
1627
- "complexity": node.complexity,
1628
- "message": f"Found {node.node_type}: {node.name}",
1629
- "timestamp": datetime.now(timezone.utc).isoformat(),
1630
- },
1631
- )
1632
-
1633
- def _emit_analysis_complete(
1634
- self, path: Path, filtered_nodes: list, duration: float
1635
- ) -> None:
1636
- """Emit analysis complete event."""
1637
- if not self.emitter:
1638
- return
1639
-
1640
- from datetime import datetime
1641
-
1642
- stats = self._calculate_node_stats(filtered_nodes)
1643
- self.emitter.emit(
1644
- "info",
1645
- {
1646
- "type": "analysis.complete",
1647
- "file": str(path),
1648
- "stats": stats,
1649
- "duration": duration,
1650
- "message": f"Analysis complete: {stats['classes']} classes, {stats['functions']} functions, {stats['methods']} methods",
1651
- "timestamp": datetime.now(timezone.utc).isoformat(),
1652
- },
1653
- )
1654
- self.emitter.emit_file_analyzed(str(path), filtered_nodes, duration)
1655
-
1656
- def _analyze_and_cache_file(
1657
- self, path: Path, language: str, cache_key: str
1658
- ) -> tuple:
1659
- """Analyze file content and cache results."""
1660
- self._emit_cache_miss(path)
1661
- self._emit_parsing_start(path)
1662
-
1663
- # Select analyzer based on language
1664
- analyzer = self._select_analyzer(language)
1665
-
1666
- # Perform analysis
1667
- start_time = time.time()
1668
- nodes = analyzer.analyze_file(path) if analyzer else []
1669
- duration = time.time() - start_time
1670
-
1671
- # Cache results
1672
- self.cache[cache_key] = nodes
1673
-
1674
- # Filter and process nodes
1675
- filtered_nodes = self._filter_and_emit_nodes(nodes, path)
1676
-
1677
- return nodes, filtered_nodes, duration
1678
-
1679
- def _select_analyzer(self, language: str):
1680
- """Select appropriate analyzer for language."""
1681
- if language == "python":
1682
- return self.python_analyzer
1683
- if language in {"javascript", "typescript"}:
1684
- return self.javascript_analyzer
1685
- return self.generic_analyzer
1686
-
1687
- def _filter_nodes(self, nodes: list) -> list:
1688
- """Filter nodes without emitting events."""
1689
- return [self._node_to_dict(n) for n in nodes if not self._is_internal_node(n)]
1690
-
1691
- def _filter_and_emit_nodes(self, nodes: list, path: Path) -> list:
1692
- """Filter nodes and emit events for each."""
1693
- filtered_nodes = []
1694
- for node in nodes:
1695
- if not self._is_internal_node(node):
1696
- self._emit_node_found(node, path)
1697
- filtered_nodes.append(self._node_to_dict(node))
1698
- return filtered_nodes
1699
-
1700
- def _node_to_dict(self, node: CodeNode) -> dict:
1701
- """Convert CodeNode to dictionary."""
1702
- return {
1703
- "name": node.name,
1704
- "type": node.node_type,
1705
- "line_start": node.line_start,
1706
- "line_end": node.line_end,
1707
- "complexity": node.complexity,
1708
- "has_docstring": node.has_docstring,
1709
- "signature": node.signature,
1710
- }
1711
-
1712
- def _calculate_node_stats(self, filtered_nodes: list) -> dict:
1713
- """Calculate statistics from filtered nodes."""
1714
- classes_count = sum(1 for n in filtered_nodes if n["type"] == "class")
1715
- functions_count = sum(1 for n in filtered_nodes if n["type"] == "function")
1716
- methods_count = sum(1 for n in filtered_nodes if n["type"] == "method")
1717
- return {
1718
- "classes": classes_count,
1719
- "functions": functions_count,
1720
- "methods": methods_count,
1721
- "total_nodes": len(filtered_nodes),
1722
- }
1723
-
1724
- def _prepare_final_nodes(self, nodes: list, filtered_nodes: list) -> list:
1725
- """Prepare final nodes data structure."""
1726
- if filtered_nodes:
1727
- return filtered_nodes
1728
- return [self._node_to_dict(n) for n in nodes if not self._is_internal_node(n)]
1729
-
1730
- def _convert_nodes_to_elements(self, final_nodes: list) -> list:
1731
- """Convert nodes to elements format for dashboard."""
1732
- elements = []
1733
- for node in final_nodes:
1734
- element = {
1735
- "name": node["name"],
1736
- "type": node["type"],
1737
- "line": node["line_start"],
1738
- "complexity": node["complexity"],
1739
- "signature": node.get("signature", ""),
1740
- "has_docstring": node.get("has_docstring", False),
1741
- }
1742
- if node["type"] == "class":
1743
- element["methods"] = []
1744
- elements.append(element)
1745
- return elements
1746
-
1747
- def _build_result(
1748
- self, file_path: str, language: str, final_nodes: list, elements: list
1749
- ) -> dict:
1750
- """Build final result dictionary."""
1751
- return {
1752
- "path": file_path,
1753
- "language": language,
1754
- "nodes": final_nodes,
1755
- "elements": elements,
1756
- "complexity": sum(e["complexity"] for e in elements),
1757
- "lines": len(elements),
1758
- "stats": {
1759
- "classes": len([e for e in elements if e["type"] == "class"]),
1760
- "functions": len([e for e in elements if e["type"] == "function"]),
1761
- "methods": len([e for e in elements if e["type"] == "method"]),
1762
- "variables": len([e for e in elements if e["type"] == "variable"]),
1763
- "imports": len([e for e in elements if e["type"] == "import"]),
1764
- "total": len(elements),
1765
- },
1766
- }
1767
-
1768
- def _is_internal_node(self, node: CodeNode) -> bool:
1769
- """Check if node is an internal function that should be filtered."""
1770
- # Don't filter classes - always show them
1771
- if node.node_type == "class":
1772
- return False
1773
-
1774
- # Don't filter variables or imports - they're useful for tree view
1775
- if node.node_type in ["variable", "import"]:
1776
- return False
1777
-
1778
- name_lower = node.name.lower()
1779
-
1780
- # Filter only very specific internal patterns
1781
- # Be more conservative - only filter obvious internal handlers
1782
- if name_lower.startswith(("handle_", "on_")):
1783
- return True
1784
-
1785
- # Filter Python magic methods except important ones
1786
- if name_lower.startswith("__") and name_lower.endswith("__"):
1787
- # Keep important magic methods
1788
- important_magic = [
1789
- "__init__",
1790
- "__call__",
1791
- "__enter__",
1792
- "__exit__",
1793
- "__str__",
1794
- "__repr__",
1795
- ]
1796
- return node.name not in important_magic
1797
-
1798
- # Filter very generic getters/setters only if they're trivial
1799
- if (name_lower.startswith(("get_", "set_"))) and len(node.name) <= 8:
1800
- return True
1801
-
1802
- # Don't filter single underscore functions - they're often important
1803
- # (like _setup_logging, _validate_input, etc.)
1804
- return False
1805
-
1806
- return False
1807
-
1808
- @property
1809
- def supported_extensions(self):
1810
- """Get list of supported file extensions."""
1811
- return {".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
1812
-
1813
- def _get_language(self, file_path: Path) -> str:
1814
- """Determine language from file extension."""
1815
- ext = file_path.suffix.lower()
1816
- language_map = {
1817
- ".py": "python",
1818
- ".js": "javascript",
1819
- ".jsx": "javascript",
1820
- ".ts": "typescript",
1821
- ".tsx": "typescript",
1822
- ".mjs": "javascript",
1823
- ".cjs": "javascript",
1824
- }
1825
- return language_map.get(ext, "unknown")