claude-mpm 5.4.36__py3-none-any.whl → 5.4.59__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/PM_INSTRUCTIONS.md +489 -177
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/frontmatter_validator.py +2 -2
- claude_mpm/cli/commands/configure_agent_display.py +12 -0
- claude_mpm/cli/commands/mpm_init/core.py +72 -0
- claude_mpm/cli/commands/profile.py +276 -0
- claude_mpm/cli/commands/skills.py +14 -18
- claude_mpm/cli/executor.py +10 -0
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/profile_parser.py +147 -0
- claude_mpm/cli/parsers/skills_parser.py +0 -6
- claude_mpm/cli/startup.py +433 -147
- claude_mpm/commands/mpm-config.md +13 -250
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -206
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +61 -441
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/core/optimized_startup.py +61 -0
- claude_mpm/core/shared/config_loader.py +3 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CWc5urbQ.js → 4TdZjIqw.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uj46x2Wr.js → BSNlmTZj.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DjhvlsAc.js → NqQ1dWOy.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{N4qtv3Hx.js → Vzk33B_K.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.CAGBuiOw.js → 0.m1gL8KXf.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
- claude_mpm/dashboard/static/svelte-build/index.html +10 -10
- claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
- claude_mpm/hooks/kuzu_memory_hook.py +5 -5
- claude_mpm/init.py +276 -0
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/deployment/agent_deployment.py +22 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +3 -1
- claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
- claude_mpm/services/agents/deployment/agent_template_builder.py +29 -17
- claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
- claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +149 -4
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +47 -26
- claude_mpm/services/agents/git_source_manager.py +21 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +116 -5
- claude_mpm/services/monitor/management/lifecycle.py +7 -1
- claude_mpm/services/pm_skills_deployer.py +711 -0
- claude_mpm/services/profile_manager.py +337 -0
- claude_mpm/services/skills/git_skill_source_manager.py +148 -11
- claude_mpm/services/skills/selective_skill_deployer.py +97 -48
- claude_mpm/services/skills_deployer.py +161 -65
- claude_mpm/skills/bundled/security-scanning.md +112 -0
- claude_mpm/skills/skill_manager.py +98 -3
- claude_mpm/templates/.pre-commit-config.yaml +112 -0
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +3 -2
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +126 -67
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +0 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +0 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +0 -10
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/top_level.txt +0 -0
|
@@ -51,6 +51,59 @@ logger = get_logger(__name__)
|
|
|
51
51
|
# Deployment tracking index file
|
|
52
52
|
DEPLOYED_INDEX_FILE = ".mpm-deployed-skills.json"
|
|
53
53
|
|
|
54
|
+
# Core skills that are universally useful across all projects
|
|
55
|
+
# These are deployed when skill mapping returns too many skills (>60)
|
|
56
|
+
# Target: ~25-30 core skills for balanced functionality
|
|
57
|
+
CORE_SKILLS = {
|
|
58
|
+
# Universal debugging and verification (4 skills)
|
|
59
|
+
"universal-debugging-systematic-debugging",
|
|
60
|
+
"universal-debugging-verification-before-completion",
|
|
61
|
+
"universal-verification-pre-merge",
|
|
62
|
+
"universal-verification-screenshot",
|
|
63
|
+
|
|
64
|
+
# Universal testing patterns (2 skills)
|
|
65
|
+
"universal-testing-test-driven-development",
|
|
66
|
+
"universal-testing-testing-anti-patterns",
|
|
67
|
+
|
|
68
|
+
# Universal architecture and design (1 skill)
|
|
69
|
+
"universal-architecture-software-patterns",
|
|
70
|
+
|
|
71
|
+
# Universal infrastructure (3 skills)
|
|
72
|
+
"universal-infrastructure-env-manager",
|
|
73
|
+
"universal-infrastructure-docker",
|
|
74
|
+
"universal-infrastructure-github-actions",
|
|
75
|
+
|
|
76
|
+
# Universal collaboration (1 skill)
|
|
77
|
+
"universal-collaboration-stacked-prs",
|
|
78
|
+
|
|
79
|
+
# Universal emergency/operations (1 skill)
|
|
80
|
+
"toolchains-universal-emergency-release",
|
|
81
|
+
"toolchains-universal-dependency-audit",
|
|
82
|
+
|
|
83
|
+
# Common language toolchains (6 skills)
|
|
84
|
+
"toolchains-typescript-core",
|
|
85
|
+
"toolchains-python-core",
|
|
86
|
+
"toolchains-javascript-tooling-biome",
|
|
87
|
+
"toolchains-python-tooling-mypy",
|
|
88
|
+
"toolchains-typescript-testing-vitest",
|
|
89
|
+
"toolchains-python-frameworks-flask",
|
|
90
|
+
|
|
91
|
+
# Common web frameworks (4 skills)
|
|
92
|
+
"toolchains-javascript-frameworks-nextjs",
|
|
93
|
+
"toolchains-nextjs-core",
|
|
94
|
+
"toolchains-typescript-frameworks-nodejs-backend",
|
|
95
|
+
"toolchains-javascript-frameworks-react-state-machine",
|
|
96
|
+
|
|
97
|
+
# Common testing tools (2 skills)
|
|
98
|
+
"toolchains-javascript-testing-playwright",
|
|
99
|
+
"toolchains-typescript-testing-jest",
|
|
100
|
+
|
|
101
|
+
# Common data/UI tools (3 skills)
|
|
102
|
+
"universal-data-xlsx",
|
|
103
|
+
"toolchains-ui-styling-tailwind",
|
|
104
|
+
"toolchains-ui-components-headlessui",
|
|
105
|
+
}
|
|
106
|
+
|
|
54
107
|
|
|
55
108
|
def parse_agent_frontmatter(agent_file: Path) -> Dict[str, Any]:
|
|
56
109
|
"""Parse YAML frontmatter from agent markdown file.
|
|
@@ -140,50 +193,49 @@ def get_skills_from_agent(frontmatter: Dict[str, Any]) -> Set[str]:
|
|
|
140
193
|
def get_skills_from_mapping(agent_ids: List[str]) -> Set[str]:
|
|
141
194
|
"""Get skills for agents using SkillToAgentMapper inference.
|
|
142
195
|
|
|
143
|
-
|
|
144
|
-
|
|
196
|
+
DEPRECATED: This function is deprecated as of Phase 3 refactor.
|
|
197
|
+
Skills are now declared exclusively in agent frontmatter.
|
|
198
|
+
|
|
199
|
+
The static skill_to_agent_mapping.yaml is no longer used for skill deployment.
|
|
200
|
+
Each agent must declare its skills in frontmatter or it gets zero skills.
|
|
201
|
+
|
|
202
|
+
This function remains for backward compatibility but is NO LONGER CALLED
|
|
203
|
+
by get_required_skills_from_agents().
|
|
145
204
|
|
|
146
205
|
Args:
|
|
147
|
-
agent_ids: List of agent identifiers (e.g., ["python-engineer", "typescript-engineer"])
|
|
206
|
+
agent_ids: List of DEPLOYED agent identifiers (e.g., ["python-engineer", "typescript-engineer"])
|
|
207
|
+
These should be extracted from ~/.claude/agents/*.md files only.
|
|
148
208
|
|
|
149
209
|
Returns:
|
|
150
|
-
Set of unique skill names inferred from mapping configuration
|
|
210
|
+
Set of unique skill names inferred from mapping configuration for DEPLOYED agents only
|
|
211
|
+
NOTE: This is now an empty set as the function is deprecated.
|
|
151
212
|
|
|
152
213
|
Example:
|
|
153
|
-
>>>
|
|
154
|
-
>>>
|
|
155
|
-
>>>
|
|
214
|
+
>>> # DEPRECATED - use frontmatter instead
|
|
215
|
+
>>> deployed_agent_ids = ["python-engineer", "typescript-engineer", "qa"]
|
|
216
|
+
>>> skills = get_skills_from_mapping(deployed_agent_ids) # Returns empty set
|
|
156
217
|
"""
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
all_skills.update(agent_skills)
|
|
165
|
-
logger.debug(f"Mapped {len(agent_skills)} skills to {agent_id}")
|
|
166
|
-
|
|
167
|
-
logger.info(
|
|
168
|
-
f"Mapped {len(all_skills)} unique skills for {len(agent_ids)} agents"
|
|
169
|
-
)
|
|
170
|
-
return all_skills
|
|
171
|
-
|
|
172
|
-
except Exception as e:
|
|
173
|
-
logger.warning(f"Failed to load SkillToAgentMapper: {e}")
|
|
174
|
-
logger.info("Falling back to frontmatter-only skill discovery")
|
|
175
|
-
return set()
|
|
218
|
+
# DEPRECATED: Return empty set
|
|
219
|
+
logger.warning(
|
|
220
|
+
"get_skills_from_mapping() is DEPRECATED and returns empty set. "
|
|
221
|
+
"Skills are now declared in agent frontmatter only. "
|
|
222
|
+
"Update your agents with 'skills:' field in frontmatter."
|
|
223
|
+
)
|
|
224
|
+
return set()
|
|
176
225
|
|
|
177
226
|
|
|
178
227
|
def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
|
|
179
228
|
"""Extract all skills referenced by deployed agents.
|
|
180
229
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
MAJOR CHANGE (Phase 3): Now ONLY uses frontmatter-declared skills.
|
|
231
|
+
The static skill_to_agent_mapping.yaml is DEPRECATED. Each agent must
|
|
232
|
+
declare its skills in frontmatter or it gets zero skills deployed.
|
|
184
233
|
|
|
185
|
-
This
|
|
186
|
-
|
|
234
|
+
This change:
|
|
235
|
+
- Eliminates dual-source complexity (frontmatter + mapping)
|
|
236
|
+
- Makes skill requirements explicit per agent
|
|
237
|
+
- Enables per-agent customization via frontmatter
|
|
238
|
+
- Removes dependency on static YAML mapping
|
|
187
239
|
|
|
188
240
|
Args:
|
|
189
241
|
agents_dir: Path to deployed agents directory (e.g., .claude/agents/)
|
|
@@ -204,13 +256,11 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
|
|
|
204
256
|
agent_files = list(agents_dir.glob("*.md"))
|
|
205
257
|
logger.debug(f"Scanning {len(agent_files)} agent files in {agents_dir}")
|
|
206
258
|
|
|
207
|
-
#
|
|
259
|
+
# ONLY use frontmatter skills - no more mapping inference
|
|
208
260
|
frontmatter_skills = set()
|
|
209
|
-
agent_ids = []
|
|
210
261
|
|
|
211
262
|
for agent_file in agent_files:
|
|
212
263
|
agent_id = agent_file.stem
|
|
213
|
-
agent_ids.append(agent_id)
|
|
214
264
|
|
|
215
265
|
frontmatter = parse_agent_frontmatter(agent_file)
|
|
216
266
|
agent_skills = get_skills_from_agent(frontmatter)
|
|
@@ -220,24 +270,23 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
|
|
|
220
270
|
logger.debug(
|
|
221
271
|
f"Agent {agent_id}: {len(agent_skills)} skills from frontmatter"
|
|
222
272
|
)
|
|
273
|
+
else:
|
|
274
|
+
logger.debug(f"Agent {agent_id}: No skills declared in frontmatter")
|
|
223
275
|
|
|
224
|
-
logger.info(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# Combine both sources
|
|
230
|
-
required_skills = frontmatter_skills | mapped_skills
|
|
276
|
+
logger.info(
|
|
277
|
+
f"Found {len(frontmatter_skills)} unique skills from agent frontmatter "
|
|
278
|
+
f"(static mapping no longer used)"
|
|
279
|
+
)
|
|
231
280
|
|
|
232
281
|
# Normalize skill paths: convert slashes to dashes for compatibility with deployment
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
normalized_skills = {skill.replace("/", "-") for skill in required_skills}
|
|
282
|
+
# Some skills may use slash format, normalize to dashes
|
|
283
|
+
normalized_skills = {skill.replace("/", "-") for skill in frontmatter_skills}
|
|
236
284
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
285
|
+
if normalized_skills != frontmatter_skills:
|
|
286
|
+
logger.debug(
|
|
287
|
+
f"Normalized {len(frontmatter_skills)} skills to {len(normalized_skills)} "
|
|
288
|
+
"(converted slashes to dashes)"
|
|
289
|
+
)
|
|
241
290
|
|
|
242
291
|
return normalized_skills
|
|
243
292
|
|
|
@@ -174,44 +174,93 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
174
174
|
if selective:
|
|
175
175
|
# Auto-detect project root if not provided
|
|
176
176
|
if project_root is None:
|
|
177
|
-
# Try to find project root by looking for .claude directory
|
|
177
|
+
# Try to find project root by looking for .claude-mpm directory
|
|
178
178
|
# Start from current directory and walk up
|
|
179
179
|
current = Path.cwd()
|
|
180
180
|
while current != current.parent:
|
|
181
|
-
if (current / ".claude").exists():
|
|
181
|
+
if (current / ".claude-mpm").exists():
|
|
182
182
|
project_root = current
|
|
183
183
|
break
|
|
184
184
|
current = current.parent
|
|
185
185
|
|
|
186
|
+
# Read skills from configuration.yaml instead of agent frontmatter
|
|
186
187
|
if project_root:
|
|
187
|
-
|
|
188
|
+
config_path = Path(project_root) / ".claude-mpm" / "configuration.yaml"
|
|
188
189
|
else:
|
|
189
|
-
# Fallback to current directory's
|
|
190
|
-
|
|
190
|
+
# Fallback to current directory's configuration
|
|
191
|
+
config_path = Path.cwd() / ".claude-mpm" / "configuration.yaml"
|
|
191
192
|
|
|
192
193
|
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
193
194
|
get_required_skills_from_agents,
|
|
195
|
+
get_skills_to_deploy,
|
|
196
|
+
save_agent_skills_to_config,
|
|
194
197
|
)
|
|
195
198
|
|
|
196
|
-
|
|
199
|
+
# Check if agent_referenced is empty and needs to be populated
|
|
200
|
+
required_skill_names, source = get_skills_to_deploy(config_path)
|
|
201
|
+
|
|
202
|
+
if not required_skill_names and project_root:
|
|
203
|
+
# agent_referenced is empty, scan deployed agents to populate it
|
|
204
|
+
agents_dir = Path(project_root) / ".claude" / "agents"
|
|
205
|
+
if agents_dir.exists():
|
|
206
|
+
self.logger.info(
|
|
207
|
+
"agent_referenced is empty in configuration.yaml, scanning deployed agents..."
|
|
208
|
+
)
|
|
209
|
+
agent_skills = get_required_skills_from_agents(agents_dir)
|
|
210
|
+
if agent_skills:
|
|
211
|
+
save_agent_skills_to_config(list(agent_skills), config_path)
|
|
212
|
+
self.logger.info(
|
|
213
|
+
f"Populated agent_referenced with {len(agent_skills)} skills from deployed agents"
|
|
214
|
+
)
|
|
215
|
+
# Re-read configuration after update
|
|
216
|
+
required_skill_names, source = get_skills_to_deploy(config_path)
|
|
217
|
+
else:
|
|
218
|
+
self.logger.warning(
|
|
219
|
+
"No skills found in deployed agents - configuration.yaml remains empty"
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
self.logger.warning(
|
|
223
|
+
f"Agents directory not found at {agents_dir} - cannot scan for skills"
|
|
224
|
+
)
|
|
197
225
|
|
|
198
226
|
if required_skill_names:
|
|
227
|
+
# Convert required_skill_names to a set for O(1) lookup
|
|
228
|
+
required_set = set(required_skill_names)
|
|
229
|
+
|
|
199
230
|
# Filter to only required skills
|
|
200
|
-
# Match on
|
|
231
|
+
# Match on: 'name', 'skill_id', or normalized 'source_path'
|
|
232
|
+
# source_path example: "universal/web/api-design-patterns/SKILL.md"
|
|
233
|
+
# normalized: "universal-web-api-design-patterns"
|
|
234
|
+
def skill_matches_requirement(skill):
|
|
235
|
+
# Check basic name and skill_id
|
|
236
|
+
if skill.get("name") in required_set:
|
|
237
|
+
return True
|
|
238
|
+
if skill.get("skill_id") in required_set:
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Check normalized source_path
|
|
242
|
+
source_path = skill.get("source_path", "")
|
|
243
|
+
if source_path:
|
|
244
|
+
# Remove /SKILL.md suffix and replace / with -
|
|
245
|
+
normalized = source_path.replace("/SKILL.md", "").replace(
|
|
246
|
+
"/", "-"
|
|
247
|
+
)
|
|
248
|
+
if normalized in required_set:
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
return False
|
|
252
|
+
|
|
201
253
|
filtered_skills = [
|
|
202
|
-
s
|
|
203
|
-
for s in filtered_skills
|
|
204
|
-
if s.get("name") in required_skill_names
|
|
205
|
-
or s.get("skill_id") in required_skill_names
|
|
254
|
+
s for s in filtered_skills if skill_matches_requirement(s)
|
|
206
255
|
]
|
|
207
256
|
|
|
208
257
|
self.logger.info(
|
|
209
258
|
f"Selective deployment: {len(filtered_skills)}/{total_available} skills "
|
|
210
|
-
f"(
|
|
259
|
+
f"(source: {source})"
|
|
211
260
|
)
|
|
212
261
|
else:
|
|
213
262
|
self.logger.warning(
|
|
214
|
-
f"No skills found in
|
|
263
|
+
f"No skills found in configuration at {config_path}. "
|
|
215
264
|
f"Deploying all {total_available} skills."
|
|
216
265
|
)
|
|
217
266
|
else:
|
|
@@ -224,12 +273,19 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
224
273
|
skipped = []
|
|
225
274
|
errors = []
|
|
226
275
|
|
|
227
|
-
# Extract skill names for cleanup (needed regardless of deployment outcome)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if isinstance(skill, dict) and "name" in skill
|
|
232
|
-
|
|
276
|
+
# Extract normalized skill names for cleanup (needed regardless of deployment outcome)
|
|
277
|
+
# Must match the names used during deployment (normalized from source_path)
|
|
278
|
+
filtered_skills_names = []
|
|
279
|
+
for skill in filtered_skills:
|
|
280
|
+
if isinstance(skill, dict) and "name" in skill:
|
|
281
|
+
source_path = skill.get("source_path", "")
|
|
282
|
+
if source_path:
|
|
283
|
+
# Normalize: "universal/web/api-design-patterns/SKILL.md" -> "universal-web-api-design-patterns"
|
|
284
|
+
normalized = source_path.replace("/SKILL.md", "").replace("/", "-")
|
|
285
|
+
filtered_skills_names.append(normalized)
|
|
286
|
+
else:
|
|
287
|
+
# Fallback to skill name
|
|
288
|
+
filtered_skills_names.append(skill["name"])
|
|
233
289
|
|
|
234
290
|
for skill in filtered_skills:
|
|
235
291
|
try:
|
|
@@ -243,9 +299,25 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
243
299
|
skill, skills_data["temp_dir"], collection_name, force=force
|
|
244
300
|
)
|
|
245
301
|
if result["deployed"]:
|
|
246
|
-
|
|
302
|
+
# Use normalized name for reporting
|
|
303
|
+
source_path = skill.get("source_path", "")
|
|
304
|
+
if source_path:
|
|
305
|
+
normalized = source_path.replace("/SKILL.md", "").replace(
|
|
306
|
+
"/", "-"
|
|
307
|
+
)
|
|
308
|
+
deployed.append(normalized)
|
|
309
|
+
else:
|
|
310
|
+
deployed.append(skill["name"])
|
|
247
311
|
elif result["skipped"]:
|
|
248
|
-
|
|
312
|
+
# Use normalized name for reporting
|
|
313
|
+
source_path = skill.get("source_path", "")
|
|
314
|
+
if source_path:
|
|
315
|
+
normalized = source_path.replace("/SKILL.md", "").replace(
|
|
316
|
+
"/", "-"
|
|
317
|
+
)
|
|
318
|
+
skipped.append(normalized)
|
|
319
|
+
else:
|
|
320
|
+
skipped.append(skill["name"])
|
|
249
321
|
if result["error"]:
|
|
250
322
|
errors.append(result["error"])
|
|
251
323
|
except Exception as e:
|
|
@@ -257,9 +329,9 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
257
329
|
self.logger.error(f"Failed to deploy {skill_name}: {e}")
|
|
258
330
|
errors.append(f"{skill_name}: {e}")
|
|
259
331
|
|
|
260
|
-
# Step 5: Cleanup orphaned skills (
|
|
332
|
+
# Step 5: Cleanup orphaned skills (always run in selective mode)
|
|
261
333
|
cleanup_result = {"removed_count": 0, "removed_skills": []}
|
|
262
|
-
if selective
|
|
334
|
+
if selective:
|
|
263
335
|
# Get the set of skills that should remain deployed
|
|
264
336
|
# This is the union of what we just deployed and what was already there
|
|
265
337
|
try:
|
|
@@ -267,7 +339,8 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
267
339
|
cleanup_orphan_skills,
|
|
268
340
|
)
|
|
269
341
|
|
|
270
|
-
#
|
|
342
|
+
# Cleanup orphaned skills not referenced by agents
|
|
343
|
+
# This runs even if nothing new was deployed to remove stale skills
|
|
271
344
|
cleanup_result = cleanup_orphan_skills(
|
|
272
345
|
self.CLAUDE_SKILLS_DIR, set(filtered_skills_names)
|
|
273
346
|
)
|
|
@@ -804,55 +877,76 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
804
877
|
Dict with deployed, skipped, error flags
|
|
805
878
|
"""
|
|
806
879
|
skill_name = skill["name"]
|
|
807
|
-
|
|
880
|
+
|
|
881
|
+
# Use normalized source_path for both target directory and deployment tracking
|
|
882
|
+
# This ensures consistency with configuration.yaml skill names
|
|
883
|
+
source_path = skill.get("source_path", "")
|
|
884
|
+
if source_path:
|
|
885
|
+
# Normalize: "universal/web/api-design-patterns/SKILL.md" -> "universal-web-api-design-patterns"
|
|
886
|
+
normalized_name = source_path.replace("/SKILL.md", "").replace("/", "-")
|
|
887
|
+
target_dir = self.CLAUDE_SKILLS_DIR / normalized_name
|
|
888
|
+
else:
|
|
889
|
+
# Fallback to skill name if no source_path
|
|
890
|
+
target_dir = self.CLAUDE_SKILLS_DIR / skill_name
|
|
808
891
|
|
|
809
892
|
# Check if already deployed
|
|
810
893
|
if target_dir.exists() and not force:
|
|
811
894
|
self.logger.debug(f"Skipped {skill_name} (already deployed)")
|
|
812
895
|
return {"deployed": False, "skipped": True, "error": None}
|
|
813
896
|
|
|
814
|
-
# Find skill source
|
|
815
|
-
|
|
816
|
-
# OR: collection_dir / universal / skill-name
|
|
817
|
-
# OR: collection_dir / toolchains / toolchain-name / skill-name
|
|
897
|
+
# Find skill source using source_path from manifest
|
|
898
|
+
source_dir = None
|
|
818
899
|
|
|
819
|
-
|
|
820
|
-
|
|
900
|
+
if source_path:
|
|
901
|
+
# Direct lookup using source_path (most reliable)
|
|
902
|
+
# Example: "universal/web/api-design-patterns/SKILL.md" -> "universal/web/api-design-patterns"
|
|
903
|
+
skill_dir_path = source_path.replace("/SKILL.md", "")
|
|
904
|
+
potential_source = collection_dir / skill_dir_path
|
|
905
|
+
if potential_source.exists():
|
|
906
|
+
source_dir = potential_source
|
|
907
|
+
else:
|
|
908
|
+
self.logger.debug(
|
|
909
|
+
f"Source path {skill_dir_path} not found, trying fallback search"
|
|
910
|
+
)
|
|
821
911
|
|
|
822
|
-
#
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
search_paths
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
for cat_dir in skills_base.iterdir():
|
|
849
|
-
if not cat_dir.is_dir():
|
|
850
|
-
continue
|
|
851
|
-
potential = cat_dir / skill_name
|
|
852
|
-
if potential.exists():
|
|
853
|
-
source_dir = potential
|
|
912
|
+
# Fallback: search using old logic (for backward compatibility)
|
|
913
|
+
if not source_dir:
|
|
914
|
+
skills_base = collection_dir / "skills"
|
|
915
|
+
category = skill.get("category", "")
|
|
916
|
+
|
|
917
|
+
# Try multiple possible locations
|
|
918
|
+
search_paths = []
|
|
919
|
+
|
|
920
|
+
# Try category-based path
|
|
921
|
+
if category and skills_base.exists():
|
|
922
|
+
search_paths.append(skills_base / category / skill_name)
|
|
923
|
+
|
|
924
|
+
# Try universal/toolchains structure
|
|
925
|
+
if (collection_dir / "universal").exists():
|
|
926
|
+
search_paths.append(collection_dir / "universal" / skill_name)
|
|
927
|
+
|
|
928
|
+
if (collection_dir / "toolchains").exists():
|
|
929
|
+
toolchain_dir = collection_dir / "toolchains"
|
|
930
|
+
for tc in toolchain_dir.iterdir():
|
|
931
|
+
if tc.is_dir():
|
|
932
|
+
search_paths.append(tc / skill_name)
|
|
933
|
+
|
|
934
|
+
# Search in all possible locations
|
|
935
|
+
for path in search_paths:
|
|
936
|
+
if path.exists():
|
|
937
|
+
source_dir = path
|
|
854
938
|
break
|
|
855
939
|
|
|
940
|
+
# Final fallback: search recursively for skill in skills directory
|
|
941
|
+
if not source_dir and skills_base.exists():
|
|
942
|
+
for cat_dir in skills_base.iterdir():
|
|
943
|
+
if not cat_dir.is_dir():
|
|
944
|
+
continue
|
|
945
|
+
potential = cat_dir / skill_name
|
|
946
|
+
if potential.exists():
|
|
947
|
+
source_dir = potential
|
|
948
|
+
break
|
|
949
|
+
|
|
856
950
|
if not source_dir or not source_dir.exists():
|
|
857
951
|
return {
|
|
858
952
|
"deployed": False,
|
|
@@ -887,12 +981,14 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
887
981
|
# NOTE: We use copy instead of symlink to maintain Claude Code compatibility
|
|
888
982
|
shutil.copytree(source_dir, target_dir)
|
|
889
983
|
|
|
890
|
-
# Track deployment in index
|
|
984
|
+
# Track deployment in index using normalized name
|
|
891
985
|
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
892
986
|
track_deployed_skill,
|
|
893
987
|
)
|
|
894
988
|
|
|
895
|
-
|
|
989
|
+
# Use normalized name for tracking (matches configuration.yaml format)
|
|
990
|
+
track_name = normalized_name if source_path else skill_name
|
|
991
|
+
track_deployed_skill(self.CLAUDE_SKILLS_DIR, track_name, collection_name)
|
|
896
992
|
|
|
897
993
|
self.logger.debug(
|
|
898
994
|
f"Deployed {skill_name} from {source_dir} to {target_dir}"
|
|
@@ -81,6 +81,36 @@ API_KEY = "sk-1234567890abcdef" # In code! # pragma: allowlist secret
|
|
|
81
81
|
|
|
82
82
|
# ✅ Safe: Use environment variables
|
|
83
83
|
API_KEY = os.getenv("API_KEY")
|
|
84
|
+
|
|
85
|
+
# ❌ CRITICAL: MCP config files with API keys
|
|
86
|
+
# NEVER commit these files:
|
|
87
|
+
# - .mcp-vector-search/config.json (OpenRouter API keys)
|
|
88
|
+
# - .mcp/config.json (MCP server credentials)
|
|
89
|
+
# - openrouter.json, anthropic-config.json
|
|
90
|
+
# - credentials.json, secrets.json, api-keys.json
|
|
91
|
+
|
|
92
|
+
# ✅ Safe: Verify .gitignore before committing
|
|
93
|
+
# Check file is ignored: git check-ignore <file_path>
|
|
94
|
+
# Check file not tracked: git ls-files <file_path>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**MCP Secret File Patterns (High Risk):**
|
|
98
|
+
```bash
|
|
99
|
+
# Files that commonly contain API keys:
|
|
100
|
+
.mcp-vector-search/config.json # OpenRouter, OpenAI keys
|
|
101
|
+
.mcp/config.json # MCP server credentials
|
|
102
|
+
**/mcp-config.json
|
|
103
|
+
openrouter.json
|
|
104
|
+
anthropic-config.json
|
|
105
|
+
openai-config.json
|
|
106
|
+
credentials.json
|
|
107
|
+
secrets.json
|
|
108
|
+
api-keys.json
|
|
109
|
+
|
|
110
|
+
# ALWAYS add to .gitignore:
|
|
111
|
+
echo ".mcp-vector-search/" >> .gitignore
|
|
112
|
+
echo "credentials.json" >> .gitignore
|
|
113
|
+
echo "secrets.json" >> .gitignore
|
|
84
114
|
```
|
|
85
115
|
|
|
86
116
|
### 4. XML External Entities (XXE)
|
|
@@ -178,6 +208,88 @@ if failed_login_count > 5:
|
|
|
178
208
|
alert_security_team()
|
|
179
209
|
```
|
|
180
210
|
|
|
211
|
+
## Secret Detection and Prevention
|
|
212
|
+
|
|
213
|
+
### Pre-commit Hooks with detect-secrets
|
|
214
|
+
```bash
|
|
215
|
+
# Install detect-secrets
|
|
216
|
+
pip install detect-secrets
|
|
217
|
+
|
|
218
|
+
# Create baseline of existing secrets
|
|
219
|
+
detect-secrets scan > .secrets.baseline
|
|
220
|
+
|
|
221
|
+
# Install pre-commit hooks
|
|
222
|
+
pip install pre-commit
|
|
223
|
+
pre-commit install
|
|
224
|
+
|
|
225
|
+
# Add to .pre-commit-config.yaml:
|
|
226
|
+
# - repo: https://github.com/Yelp/detect-secrets
|
|
227
|
+
# rev: v1.5.0
|
|
228
|
+
# hooks:
|
|
229
|
+
# - id: detect-secrets
|
|
230
|
+
# args: ['--baseline', '.secrets.baseline']
|
|
231
|
+
|
|
232
|
+
# Scan for new secrets
|
|
233
|
+
detect-secrets scan --baseline .secrets.baseline
|
|
234
|
+
|
|
235
|
+
# Audit baseline (mark false positives)
|
|
236
|
+
detect-secrets audit .secrets.baseline
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Manual Secret Scanning
|
|
240
|
+
```bash
|
|
241
|
+
# Check if file is ignored by git
|
|
242
|
+
git check-ignore .mcp-vector-search/config.json
|
|
243
|
+
# Exit code 0 = ignored (safe)
|
|
244
|
+
# Exit code 1 = NOT ignored (DANGER!)
|
|
245
|
+
|
|
246
|
+
# Check if file is tracked by git
|
|
247
|
+
git ls-files .mcp-vector-search/config.json
|
|
248
|
+
# Output present = tracked (CRITICAL - remove immediately!)
|
|
249
|
+
# No output = not tracked (safe if also in .gitignore)
|
|
250
|
+
|
|
251
|
+
# Search git history for committed secrets
|
|
252
|
+
git log --all --full-history -- .mcp-vector-search/config.json
|
|
253
|
+
|
|
254
|
+
# Remove file from git history (if accidentally committed)
|
|
255
|
+
git filter-branch --force --index-filter \
|
|
256
|
+
'git rm --cached --ignore-unmatch .mcp-vector-search/config.json' \
|
|
257
|
+
--prune-empty --tag-name-filter cat -- --all
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Incident Response: Exposed API Key
|
|
261
|
+
If you've committed an API key to git:
|
|
262
|
+
|
|
263
|
+
1. **IMMEDIATELY rotate the exposed credential**
|
|
264
|
+
- OpenRouter: https://openrouter.ai/settings/keys
|
|
265
|
+
- Anthropic: https://console.anthropic.com/settings/keys
|
|
266
|
+
- OpenAI: https://platform.openai.com/api-keys
|
|
267
|
+
|
|
268
|
+
2. **Remove from git history**
|
|
269
|
+
```bash
|
|
270
|
+
# Using git-filter-repo (recommended)
|
|
271
|
+
git filter-repo --path .mcp-vector-search/config.json --invert-paths
|
|
272
|
+
|
|
273
|
+
# Force push to remote (WARNING: destructive)
|
|
274
|
+
git push origin --force --all
|
|
275
|
+
git push origin --force --tags
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
3. **Add to .gitignore** (if not already there)
|
|
279
|
+
```bash
|
|
280
|
+
echo ".mcp-vector-search/" >> .gitignore
|
|
281
|
+
git add .gitignore
|
|
282
|
+
git commit -m "chore: add MCP config to gitignore"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
4. **Verify cleanup**
|
|
286
|
+
```bash
|
|
287
|
+
git log --all --full-history -- .mcp-vector-search/config.json
|
|
288
|
+
# Should show no results
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
5. **Notify stakeholders** if the key had production access
|
|
292
|
+
|
|
181
293
|
## Security Scanning Tools
|
|
182
294
|
|
|
183
295
|
### Python
|