claude-mpm 5.4.14__py3-none-any.whl → 5.4.36__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/BASE_ENGINEER.md +658 -0
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +363 -817
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +1 -1
- claude_mpm/agents/base_agent.json +31 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +10 -10
- claude_mpm/cli/commands/agents.py +9 -40
- claude_mpm/cli/commands/auto_configure.py +4 -4
- claude_mpm/cli/commands/configure.py +1 -1
- claude_mpm/cli/commands/postmortem.py +1 -1
- claude_mpm/cli/commands/skills.py +193 -187
- claude_mpm/cli/interactive/agent_wizard.py +2 -2
- claude_mpm/cli/parsers/agents_parser.py +0 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
- claude_mpm/cli/startup.py +330 -78
- claude_mpm/commands/mpm-config.md +1 -2
- claude_mpm/commands/mpm-help.md +14 -95
- claude_mpm/commands/mpm-organize.md +350 -153
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/loaders/agent_loader.py +1 -1
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +5 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
- claude_mpm/models/git_repository.py +3 -3
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +7 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -2
- claude_mpm/services/agents/deployment/agent_template_builder.py +2 -2
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +20 -22
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +55 -53
- claude_mpm/services/agents/git_source_manager.py +2 -2
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +5 -5
- claude_mpm/services/agents/startup_sync.py +22 -2
- claude_mpm/services/command_deployment_service.py +10 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/monitor/server.py +473 -3
- claude_mpm/services/skills/selective_skill_deployer.py +475 -1
- claude_mpm/services/skills_deployer.py +62 -6
- claude_mpm/services/socketio/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +37 -6
- claude_mpm/services/socketio/server/core.py +262 -123
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +1 -1
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/METADATA +5 -3
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/RECORD +96 -66
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,8 @@ DESIGN DECISIONS:
|
|
|
12
12
|
- Parse YAML frontmatter from agent markdown files
|
|
13
13
|
- Combine explicit + inferred skills for comprehensive coverage
|
|
14
14
|
- Return set of unique skill names for filtering
|
|
15
|
+
- Track deployed skills in .mpm-deployed-skills.json index
|
|
16
|
+
- Remove orphaned skills (deployed by mpm but no longer referenced)
|
|
15
17
|
|
|
16
18
|
FORMATS SUPPORTED:
|
|
17
19
|
1. Legacy: skills: [skill-a, skill-b, ...]
|
|
@@ -23,14 +25,21 @@ SKILL DISCOVERY FLOW:
|
|
|
23
25
|
3. Query SkillToAgentMapper for pattern-based skills
|
|
24
26
|
4. Combine both sources into unified set
|
|
25
27
|
|
|
28
|
+
DEPLOYMENT TRACKING:
|
|
29
|
+
1. Track which skills were deployed by claude-mpm in index file
|
|
30
|
+
2. Update index after each deployment operation
|
|
31
|
+
3. Clean up orphaned skills no longer referenced by agents
|
|
32
|
+
|
|
26
33
|
References:
|
|
27
34
|
- Feature: Progressive skills discovery (#117)
|
|
28
35
|
- Service: SkillToAgentMapper (skill_to_agent_mapper.py)
|
|
29
36
|
"""
|
|
30
37
|
|
|
38
|
+
import json
|
|
31
39
|
import re
|
|
40
|
+
from datetime import datetime, timezone
|
|
32
41
|
from pathlib import Path
|
|
33
|
-
from typing import Any, Dict, List, Set
|
|
42
|
+
from typing import Any, Dict, List, Set, Tuple
|
|
34
43
|
|
|
35
44
|
import yaml
|
|
36
45
|
|
|
@@ -39,6 +48,9 @@ from claude_mpm.services.skills.skill_to_agent_mapper import SkillToAgentMapper
|
|
|
39
48
|
|
|
40
49
|
logger = get_logger(__name__)
|
|
41
50
|
|
|
51
|
+
# Deployment tracking index file
|
|
52
|
+
DEPLOYED_INDEX_FILE = ".mpm-deployed-skills.json"
|
|
53
|
+
|
|
42
54
|
|
|
43
55
|
def parse_agent_frontmatter(agent_file: Path) -> Dict[str, Any]:
|
|
44
56
|
"""Parse YAML frontmatter from agent markdown file.
|
|
@@ -228,3 +240,465 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
|
|
|
228
240
|
)
|
|
229
241
|
|
|
230
242
|
return normalized_skills
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# === Deployment Tracking Functions ===
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_deployment_index(claude_skills_dir: Path) -> Dict[str, Any]:
|
|
249
|
+
"""Load deployment tracking index from ~/.claude/skills/.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Dict containing:
|
|
256
|
+
- deployed_skills: Dict mapping skill name to deployment metadata
|
|
257
|
+
- user_requested_skills: List of skill names manually requested by user
|
|
258
|
+
- last_sync: ISO timestamp of last sync operation
|
|
259
|
+
|
|
260
|
+
Example:
|
|
261
|
+
>>> index = load_deployment_index(Path.home() / ".claude" / "skills")
|
|
262
|
+
>>> print(f"Tracked skills: {len(index['deployed_skills'])}")
|
|
263
|
+
"""
|
|
264
|
+
index_path = claude_skills_dir / DEPLOYED_INDEX_FILE
|
|
265
|
+
|
|
266
|
+
if not index_path.exists():
|
|
267
|
+
logger.debug(f"No deployment index found at {index_path}, creating new")
|
|
268
|
+
return {"deployed_skills": {}, "user_requested_skills": [], "last_sync": None}
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
with open(index_path, encoding="utf-8") as f:
|
|
272
|
+
index = json.load(f)
|
|
273
|
+
|
|
274
|
+
# Ensure required keys exist
|
|
275
|
+
if "deployed_skills" not in index:
|
|
276
|
+
index["deployed_skills"] = {}
|
|
277
|
+
if "user_requested_skills" not in index:
|
|
278
|
+
index["user_requested_skills"] = []
|
|
279
|
+
if "last_sync" not in index:
|
|
280
|
+
index["last_sync"] = None
|
|
281
|
+
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"Loaded deployment index: {len(index['deployed_skills'])} tracked skills, "
|
|
284
|
+
f"{len(index['user_requested_skills'])} user-requested"
|
|
285
|
+
)
|
|
286
|
+
return index
|
|
287
|
+
|
|
288
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
289
|
+
logger.warning(f"Failed to load deployment index: {e}, creating new")
|
|
290
|
+
return {"deployed_skills": {}, "user_requested_skills": [], "last_sync": None}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def save_deployment_index(claude_skills_dir: Path, index: Dict[str, Any]) -> None:
|
|
294
|
+
"""Save deployment tracking index to ~/.claude/skills/.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
298
|
+
index: Index data to save
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
>>> index = {"deployed_skills": {...}, "last_sync": "2025-12-22T10:30:00Z"}
|
|
302
|
+
>>> save_deployment_index(Path.home() / ".claude" / "skills", index)
|
|
303
|
+
"""
|
|
304
|
+
index_path = claude_skills_dir / DEPLOYED_INDEX_FILE
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
# Ensure directory exists
|
|
308
|
+
claude_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
309
|
+
|
|
310
|
+
with open(index_path, "w", encoding="utf-8") as f:
|
|
311
|
+
json.dump(index, f, indent=2, ensure_ascii=False)
|
|
312
|
+
|
|
313
|
+
logger.debug(f"Saved deployment index: {len(index['deployed_skills'])} skills")
|
|
314
|
+
|
|
315
|
+
except OSError as e:
|
|
316
|
+
logger.error(f"Failed to save deployment index: {e}")
|
|
317
|
+
raise
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def track_deployed_skill(
|
|
321
|
+
claude_skills_dir: Path, skill_name: str, collection: str
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Track a newly deployed skill in the deployment index.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
327
|
+
skill_name: Name of deployed skill
|
|
328
|
+
collection: Collection name skill was deployed from
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
>>> track_deployed_skill(
|
|
332
|
+
... Path.home() / ".claude" / "skills",
|
|
333
|
+
... "systematic-debugging",
|
|
334
|
+
... "claude-mpm-skills"
|
|
335
|
+
... )
|
|
336
|
+
"""
|
|
337
|
+
index = load_deployment_index(claude_skills_dir)
|
|
338
|
+
|
|
339
|
+
# Add skill to deployed_skills
|
|
340
|
+
index["deployed_skills"][skill_name] = {
|
|
341
|
+
"collection": collection,
|
|
342
|
+
"deployed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Update last_sync timestamp
|
|
346
|
+
index["last_sync"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
347
|
+
|
|
348
|
+
save_deployment_index(claude_skills_dir, index)
|
|
349
|
+
logger.debug(f"Tracked deployment: {skill_name} from {collection}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def untrack_skill(claude_skills_dir: Path, skill_name: str) -> None:
|
|
353
|
+
"""Remove skill from deployment tracking index.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
357
|
+
skill_name: Name of skill to untrack
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
>>> untrack_skill(
|
|
361
|
+
... Path.home() / ".claude" / "skills",
|
|
362
|
+
... "old-skill"
|
|
363
|
+
... )
|
|
364
|
+
"""
|
|
365
|
+
index = load_deployment_index(claude_skills_dir)
|
|
366
|
+
|
|
367
|
+
if skill_name in index["deployed_skills"]:
|
|
368
|
+
del index["deployed_skills"][skill_name]
|
|
369
|
+
index["last_sync"] = (
|
|
370
|
+
datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
371
|
+
)
|
|
372
|
+
save_deployment_index(claude_skills_dir, index)
|
|
373
|
+
logger.debug(f"Untracked skill: {skill_name}")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def cleanup_orphan_skills(
|
|
377
|
+
claude_skills_dir: Path, required_skills: Set[str]
|
|
378
|
+
) -> Dict[str, Any]:
|
|
379
|
+
"""Remove skills deployed by claude-mpm but no longer referenced by agents.
|
|
380
|
+
|
|
381
|
+
This function:
|
|
382
|
+
1. Loads deployment tracking index
|
|
383
|
+
2. Identifies orphaned skills (tracked but not in required_skills AND not user-requested)
|
|
384
|
+
3. Removes orphaned skill directories from ~/.claude/skills/
|
|
385
|
+
4. Updates deployment index
|
|
386
|
+
|
|
387
|
+
User-requested skills are NEVER cleaned up as orphans - they are treated as required.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
391
|
+
required_skills: Set of skill names currently required by agents
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Dict containing:
|
|
395
|
+
- removed_count: Number of skills removed
|
|
396
|
+
- removed_skills: List of removed skill names
|
|
397
|
+
- kept_count: Number of skills kept
|
|
398
|
+
- errors: List of error messages
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> required = {"skill-a", "skill-b"}
|
|
402
|
+
>>> result = cleanup_orphan_skills(
|
|
403
|
+
... Path.home() / ".claude" / "skills",
|
|
404
|
+
... required
|
|
405
|
+
... )
|
|
406
|
+
>>> print(f"Removed {result['removed_count']} orphaned skills")
|
|
407
|
+
"""
|
|
408
|
+
import shutil
|
|
409
|
+
|
|
410
|
+
index = load_deployment_index(claude_skills_dir)
|
|
411
|
+
tracked_skills = set(index["deployed_skills"].keys())
|
|
412
|
+
user_requested = set(index.get("user_requested_skills", []))
|
|
413
|
+
|
|
414
|
+
# Find orphaned skills: tracked by mpm but not in required_skills AND not user-requested
|
|
415
|
+
# User-requested skills are treated as required and NEVER cleaned up
|
|
416
|
+
all_required = required_skills | user_requested
|
|
417
|
+
orphaned = tracked_skills - all_required
|
|
418
|
+
|
|
419
|
+
if not orphaned:
|
|
420
|
+
logger.info("No orphaned skills to remove")
|
|
421
|
+
return {
|
|
422
|
+
"removed_count": 0,
|
|
423
|
+
"removed_skills": [],
|
|
424
|
+
"kept_count": len(tracked_skills),
|
|
425
|
+
"errors": [],
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
logger.info(
|
|
429
|
+
f"Found {len(orphaned)} orphaned skills (tracked but not required by agents)"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
removed = []
|
|
433
|
+
errors = []
|
|
434
|
+
|
|
435
|
+
for skill_name in orphaned:
|
|
436
|
+
skill_dir = claude_skills_dir / skill_name
|
|
437
|
+
|
|
438
|
+
# Remove skill directory if it exists
|
|
439
|
+
if skill_dir.exists():
|
|
440
|
+
try:
|
|
441
|
+
# Validate path is within claude_skills_dir (security)
|
|
442
|
+
skill_dir.resolve().relative_to(claude_skills_dir.resolve())
|
|
443
|
+
|
|
444
|
+
# Remove directory
|
|
445
|
+
if skill_dir.is_symlink():
|
|
446
|
+
logger.debug(f"Removing symlink: {skill_dir}")
|
|
447
|
+
skill_dir.unlink()
|
|
448
|
+
else:
|
|
449
|
+
shutil.rmtree(skill_dir)
|
|
450
|
+
|
|
451
|
+
removed.append(skill_name)
|
|
452
|
+
logger.info(f"Removed orphaned skill: {skill_name}")
|
|
453
|
+
|
|
454
|
+
except ValueError:
|
|
455
|
+
error_msg = f"Path traversal attempt detected: {skill_dir}"
|
|
456
|
+
logger.error(error_msg)
|
|
457
|
+
errors.append(error_msg)
|
|
458
|
+
continue
|
|
459
|
+
except Exception as e:
|
|
460
|
+
error_msg = f"Failed to remove {skill_name}: {e}"
|
|
461
|
+
logger.error(error_msg)
|
|
462
|
+
errors.append(error_msg)
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
# Remove from tracking index
|
|
466
|
+
untrack_skill(claude_skills_dir, skill_name)
|
|
467
|
+
|
|
468
|
+
kept_count = len(tracked_skills) - len(removed)
|
|
469
|
+
|
|
470
|
+
logger.info(
|
|
471
|
+
f"Cleanup complete: removed {len(removed)} skills, kept {kept_count} skills"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
"removed_count": len(removed),
|
|
476
|
+
"removed_skills": removed,
|
|
477
|
+
"kept_count": kept_count,
|
|
478
|
+
"errors": errors,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# === Configuration Management Functions ===
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def save_agent_skills_to_config(skills: List[str], config_path: Path) -> None:
|
|
486
|
+
"""Save agent-scanned skills to configuration.yaml under skills.agent_referenced.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
skills: List of skill names scanned from deployed agents
|
|
490
|
+
config_path: Path to configuration.yaml file
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> skills = ["systematic-debugging", "typescript-core"]
|
|
494
|
+
>>> save_agent_skills_to_config(skills, Path(".claude-mpm/configuration.yaml"))
|
|
495
|
+
"""
|
|
496
|
+
import yaml
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
# Load existing configuration (or create empty dict)
|
|
500
|
+
if config_path.exists():
|
|
501
|
+
with open(config_path, encoding="utf-8") as f:
|
|
502
|
+
config = yaml.safe_load(f) or {}
|
|
503
|
+
else:
|
|
504
|
+
config = {}
|
|
505
|
+
|
|
506
|
+
# Ensure skills section exists
|
|
507
|
+
if "skills" not in config:
|
|
508
|
+
config["skills"] = {}
|
|
509
|
+
|
|
510
|
+
# Update agent_referenced skills (sorted for consistency)
|
|
511
|
+
config["skills"]["agent_referenced"] = sorted(skills)
|
|
512
|
+
|
|
513
|
+
# Ensure user_defined exists (but don't overwrite if set)
|
|
514
|
+
if "user_defined" not in config["skills"]:
|
|
515
|
+
config["skills"]["user_defined"] = []
|
|
516
|
+
|
|
517
|
+
# Save configuration
|
|
518
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
519
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
520
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
521
|
+
|
|
522
|
+
logger.info(
|
|
523
|
+
f"Saved {len(skills)} agent-referenced skills to configuration.yaml"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.error(f"Failed to save agent skills to config: {e}")
|
|
528
|
+
raise
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def get_skills_to_deploy(config_path: Path) -> Tuple[List[str], str]:
|
|
532
|
+
"""Resolve which skills to deploy based on configuration priority.
|
|
533
|
+
|
|
534
|
+
Returns (skills_list, source) where source is 'user_defined' or 'agent_referenced'.
|
|
535
|
+
|
|
536
|
+
Logic:
|
|
537
|
+
- If config.skills.user_defined is non-empty → return (user_defined, 'user_defined')
|
|
538
|
+
- Otherwise → return (agent_referenced, 'agent_referenced')
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
config_path: Path to configuration.yaml file
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Tuple of (skills list, source string)
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
>>> skills, source = get_skills_to_deploy(Path(".claude-mpm/configuration.yaml"))
|
|
548
|
+
>>> print(f"Deploy {len(skills)} skills from {source}")
|
|
549
|
+
"""
|
|
550
|
+
import yaml
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
# Load configuration
|
|
554
|
+
if not config_path.exists():
|
|
555
|
+
logger.warning(f"Configuration file not found: {config_path}")
|
|
556
|
+
return ([], "agent_referenced")
|
|
557
|
+
|
|
558
|
+
with open(config_path, encoding="utf-8") as f:
|
|
559
|
+
config = yaml.safe_load(f) or {}
|
|
560
|
+
|
|
561
|
+
skills_config = config.get("skills", {})
|
|
562
|
+
user_defined = skills_config.get("user_defined", [])
|
|
563
|
+
agent_referenced = skills_config.get("agent_referenced", [])
|
|
564
|
+
|
|
565
|
+
# Priority: user_defined if non-empty, otherwise agent_referenced
|
|
566
|
+
if user_defined:
|
|
567
|
+
logger.info(
|
|
568
|
+
f"Using {len(user_defined)} user-defined skills from configuration"
|
|
569
|
+
)
|
|
570
|
+
return (user_defined, "user_defined")
|
|
571
|
+
logger.info(
|
|
572
|
+
f"Using {len(agent_referenced)} agent-referenced skills from configuration"
|
|
573
|
+
)
|
|
574
|
+
return (agent_referenced, "agent_referenced")
|
|
575
|
+
|
|
576
|
+
except Exception as e:
|
|
577
|
+
logger.error(f"Failed to load skills from config: {e}")
|
|
578
|
+
return ([], "agent_referenced")
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# === User-Requested Skills Management ===
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_user_requested_skills(claude_skills_dir: Path) -> List[str]:
|
|
585
|
+
"""Get list of user-requested skills.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
List of skill names manually requested by user
|
|
592
|
+
|
|
593
|
+
Example:
|
|
594
|
+
>>> skills = get_user_requested_skills(Path.home() / ".claude" / "skills")
|
|
595
|
+
>>> print(f"User requested {len(skills)} skills")
|
|
596
|
+
"""
|
|
597
|
+
index = load_deployment_index(claude_skills_dir)
|
|
598
|
+
return index.get("user_requested_skills", [])
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def add_user_requested_skill(skill_name: str, claude_skills_dir: Path) -> bool:
|
|
602
|
+
"""Add a skill to user_requested_skills list.
|
|
603
|
+
|
|
604
|
+
This function:
|
|
605
|
+
1. Loads deployment index
|
|
606
|
+
2. Adds skill name to user_requested_skills (if not already present)
|
|
607
|
+
3. Saves updated index
|
|
608
|
+
4. Returns success status
|
|
609
|
+
|
|
610
|
+
Note: This function does NOT deploy the skill, it only marks it as user-requested.
|
|
611
|
+
Use this in conjunction with skill deployment functions.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
skill_name: Name of skill to mark as user-requested
|
|
615
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
True if skill was added, False if already present
|
|
619
|
+
|
|
620
|
+
Example:
|
|
621
|
+
>>> added = add_user_requested_skill(
|
|
622
|
+
... "django-framework",
|
|
623
|
+
... Path.home() / ".claude" / "skills"
|
|
624
|
+
... )
|
|
625
|
+
>>> print(f"Skill added: {added}")
|
|
626
|
+
"""
|
|
627
|
+
index = load_deployment_index(claude_skills_dir)
|
|
628
|
+
user_requested = index.get("user_requested_skills", [])
|
|
629
|
+
|
|
630
|
+
if skill_name in user_requested:
|
|
631
|
+
logger.debug(f"Skill {skill_name} already in user_requested_skills")
|
|
632
|
+
return False
|
|
633
|
+
|
|
634
|
+
user_requested.append(skill_name)
|
|
635
|
+
index["user_requested_skills"] = user_requested
|
|
636
|
+
index["last_sync"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
637
|
+
|
|
638
|
+
save_deployment_index(claude_skills_dir, index)
|
|
639
|
+
logger.info(f"Added {skill_name} to user_requested_skills")
|
|
640
|
+
return True
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def remove_user_requested_skill(skill_name: str, claude_skills_dir: Path) -> bool:
|
|
644
|
+
"""Remove a skill from user_requested_skills list.
|
|
645
|
+
|
|
646
|
+
This function:
|
|
647
|
+
1. Loads deployment index
|
|
648
|
+
2. Removes skill name from user_requested_skills
|
|
649
|
+
3. Saves updated index
|
|
650
|
+
4. Returns success status
|
|
651
|
+
|
|
652
|
+
Note: This function does NOT remove the deployed skill directory.
|
|
653
|
+
It only removes the skill from user_requested_skills, making it eligible
|
|
654
|
+
for cleanup during orphan removal.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
skill_name: Name of skill to remove from user_requested_skills
|
|
658
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
True if skill was removed, False if not present
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
>>> removed = remove_user_requested_skill(
|
|
665
|
+
... "django-framework",
|
|
666
|
+
... Path.home() / ".claude" / "skills"
|
|
667
|
+
... )
|
|
668
|
+
>>> print(f"Skill removed: {removed}")
|
|
669
|
+
"""
|
|
670
|
+
index = load_deployment_index(claude_skills_dir)
|
|
671
|
+
user_requested = index.get("user_requested_skills", [])
|
|
672
|
+
|
|
673
|
+
if skill_name not in user_requested:
|
|
674
|
+
logger.debug(f"Skill {skill_name} not in user_requested_skills")
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
user_requested.remove(skill_name)
|
|
678
|
+
index["user_requested_skills"] = user_requested
|
|
679
|
+
index["last_sync"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
680
|
+
|
|
681
|
+
save_deployment_index(claude_skills_dir, index)
|
|
682
|
+
logger.info(f"Removed {skill_name} from user_requested_skills")
|
|
683
|
+
return True
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def is_user_requested_skill(skill_name: str, claude_skills_dir: Path) -> bool:
|
|
687
|
+
"""Check if a skill is in the user_requested_skills list.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
skill_name: Name of skill to check
|
|
691
|
+
claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
True if skill is user-requested, False otherwise
|
|
695
|
+
|
|
696
|
+
Example:
|
|
697
|
+
>>> is_requested = is_user_requested_skill(
|
|
698
|
+
... "django-framework",
|
|
699
|
+
... Path.home() / ".claude" / "skills"
|
|
700
|
+
... )
|
|
701
|
+
>>> print(f"User requested: {is_requested}")
|
|
702
|
+
"""
|
|
703
|
+
user_requested = get_user_requested_skills(claude_skills_dir)
|
|
704
|
+
return skill_name in user_requested
|
|
@@ -224,6 +224,13 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
224
224
|
skipped = []
|
|
225
225
|
errors = []
|
|
226
226
|
|
|
227
|
+
# Extract skill names for cleanup (needed regardless of deployment outcome)
|
|
228
|
+
filtered_skills_names = [
|
|
229
|
+
skill["name"]
|
|
230
|
+
for skill in filtered_skills
|
|
231
|
+
if isinstance(skill, dict) and "name" in skill
|
|
232
|
+
]
|
|
233
|
+
|
|
227
234
|
for skill in filtered_skills:
|
|
228
235
|
try:
|
|
229
236
|
# Validate skill is a dictionary
|
|
@@ -232,7 +239,9 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
232
239
|
errors.append(f"Invalid skill format: {skill}")
|
|
233
240
|
continue
|
|
234
241
|
|
|
235
|
-
result = self._deploy_skill(
|
|
242
|
+
result = self._deploy_skill(
|
|
243
|
+
skill, skills_data["temp_dir"], collection_name, force=force
|
|
244
|
+
)
|
|
236
245
|
if result["deployed"]:
|
|
237
246
|
deployed.append(skill["name"])
|
|
238
247
|
elif result["skipped"]:
|
|
@@ -248,10 +257,33 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
248
257
|
self.logger.error(f"Failed to deploy {skill_name}: {e}")
|
|
249
258
|
errors.append(f"{skill_name}: {e}")
|
|
250
259
|
|
|
251
|
-
# Step 5: Cleanup
|
|
260
|
+
# Step 5: Cleanup orphaned skills (if selective mode enabled)
|
|
261
|
+
cleanup_result = {"removed_count": 0, "removed_skills": []}
|
|
262
|
+
if selective and len(deployed) > 0:
|
|
263
|
+
# Get the set of skills that should remain deployed
|
|
264
|
+
# This is the union of what we just deployed and what was already there
|
|
265
|
+
try:
|
|
266
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
267
|
+
cleanup_orphan_skills,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Only cleanup if we're in selective mode
|
|
271
|
+
cleanup_result = cleanup_orphan_skills(
|
|
272
|
+
self.CLAUDE_SKILLS_DIR, set(filtered_skills_names)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if cleanup_result["removed_count"] > 0:
|
|
276
|
+
self.logger.info(
|
|
277
|
+
f"Removed {cleanup_result['removed_count']} orphaned skills: "
|
|
278
|
+
f"{', '.join(cleanup_result['removed_skills'])}"
|
|
279
|
+
)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.logger.warning(f"Failed to cleanup orphaned skills: {e}")
|
|
282
|
+
|
|
283
|
+
# Step 6: Cleanup temp directory
|
|
252
284
|
self._cleanup(skills_data["temp_dir"])
|
|
253
285
|
|
|
254
|
-
# Step
|
|
286
|
+
# Step 7: Check if Claude Code restart needed
|
|
255
287
|
restart_required = len(deployed) > 0
|
|
256
288
|
restart_instructions = ""
|
|
257
289
|
|
|
@@ -275,7 +307,8 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
275
307
|
|
|
276
308
|
self.logger.info(
|
|
277
309
|
f"Deployment complete: {len(deployed)} deployed, "
|
|
278
|
-
f"{len(skipped)} skipped, {len(errors)} errors"
|
|
310
|
+
f"{len(skipped)} skipped, {len(errors)} errors, "
|
|
311
|
+
f"{cleanup_result['removed_count']} orphaned skills removed"
|
|
279
312
|
)
|
|
280
313
|
|
|
281
314
|
return {
|
|
@@ -289,6 +322,7 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
289
322
|
"collection": collection_name,
|
|
290
323
|
"selective_mode": selective,
|
|
291
324
|
"total_available": total_available,
|
|
325
|
+
"cleanup": cleanup_result,
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
def list_available_skills(self, collection: Optional[str] = None) -> Dict:
|
|
@@ -473,6 +507,13 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
473
507
|
removed.append(skill_name)
|
|
474
508
|
self.logger.info(f"Removed skill: {skill_name}")
|
|
475
509
|
|
|
510
|
+
# Untrack skill from deployment index
|
|
511
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
512
|
+
untrack_skill,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
untrack_skill(self.CLAUDE_SKILLS_DIR, skill_name)
|
|
516
|
+
|
|
476
517
|
except Exception as e:
|
|
477
518
|
self.logger.error(f"Failed to remove {skill_name}: {e}")
|
|
478
519
|
errors.append(f"{skill_name}: {e}")
|
|
@@ -738,17 +779,25 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
738
779
|
return filtered
|
|
739
780
|
|
|
740
781
|
def _deploy_skill(
|
|
741
|
-
self,
|
|
782
|
+
self,
|
|
783
|
+
skill: Dict,
|
|
784
|
+
collection_dir: Path,
|
|
785
|
+
collection_name: str,
|
|
786
|
+
force: bool = False,
|
|
742
787
|
) -> Dict:
|
|
743
|
-
"""Deploy a single skill to ~/.claude/skills
|
|
788
|
+
"""Deploy a single skill to ~/.claude/skills/ and track deployment.
|
|
744
789
|
|
|
745
790
|
NOTE: With multi-collection support, skills are now stored in collection
|
|
746
791
|
subdirectories. This method creates symlinks or copies to maintain the
|
|
747
792
|
flat structure that Claude Code expects in ~/.claude/skills/.
|
|
748
793
|
|
|
794
|
+
Additionally tracks deployed skills in .mpm-deployed-skills.json index
|
|
795
|
+
for orphan cleanup functionality.
|
|
796
|
+
|
|
749
797
|
Args:
|
|
750
798
|
skill: Skill metadata dict
|
|
751
799
|
collection_dir: Collection directory containing skills
|
|
800
|
+
collection_name: Name of collection (for tracking)
|
|
752
801
|
force: Overwrite if already exists
|
|
753
802
|
|
|
754
803
|
Returns:
|
|
@@ -838,6 +887,13 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
838
887
|
# NOTE: We use copy instead of symlink to maintain Claude Code compatibility
|
|
839
888
|
shutil.copytree(source_dir, target_dir)
|
|
840
889
|
|
|
890
|
+
# Track deployment in index
|
|
891
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
892
|
+
track_deployed_skill,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
track_deployed_skill(self.CLAUDE_SKILLS_DIR, skill_name, collection_name)
|
|
896
|
+
|
|
841
897
|
self.logger.debug(
|
|
842
898
|
f"Deployed {skill_name} from {source_dir} to {target_dir}"
|
|
843
899
|
)
|
|
@@ -152,6 +152,7 @@ class DashboardServer(SocketIOServiceInterface):
|
|
|
152
152
|
|
|
153
153
|
# Register handlers for all events we want to relay from monitor to dashboard
|
|
154
154
|
relay_events = [
|
|
155
|
+
"claude_event", # Tool events from Claude Code hooks
|
|
155
156
|
"session_started",
|
|
156
157
|
"session_ended",
|
|
157
158
|
"claude_status",
|