@softspark/ai-toolkit 1.0.0
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.
- package/AGENTS.md +412 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +632 -0
- package/action.yml +53 -0
- package/app/.claude-plugin/plugin.json +44 -0
- package/app/ARCHITECTURE.md +306 -0
- package/app/CLAUDE.md.template +23 -0
- package/app/agents/ai-engineer.md +128 -0
- package/app/agents/backend-specialist.md +193 -0
- package/app/agents/business-intelligence.md +54 -0
- package/app/agents/chaos-monkey.md +67 -0
- package/app/agents/chief-of-staff.md +51 -0
- package/app/agents/code-archaeologist.md +127 -0
- package/app/agents/code-reviewer.md +184 -0
- package/app/agents/command-expert.md +131 -0
- package/app/agents/data-analyst.md +205 -0
- package/app/agents/data-scientist.md +151 -0
- package/app/agents/database-architect.md +317 -0
- package/app/agents/debugger.md +238 -0
- package/app/agents/devops-implementer.md +194 -0
- package/app/agents/documenter.md +364 -0
- package/app/agents/explorer-agent.md +145 -0
- package/app/agents/fact-checker.md +172 -0
- package/app/agents/frontend-specialist.md +209 -0
- package/app/agents/game-developer.md +216 -0
- package/app/agents/incident-responder.md +226 -0
- package/app/agents/infrastructure-architect.md +127 -0
- package/app/agents/infrastructure-validator.md +247 -0
- package/app/agents/llm-ops-engineer.md +237 -0
- package/app/agents/mcp-expert.md +228 -0
- package/app/agents/mcp-server-architect.md +195 -0
- package/app/agents/mcp-testing-engineer.md +292 -0
- package/app/agents/meta-architect.md +58 -0
- package/app/agents/ml-engineer.md +136 -0
- package/app/agents/mobile-developer.md +190 -0
- package/app/agents/night-watchman.md +55 -0
- package/app/agents/nlp-engineer.md +154 -0
- package/app/agents/orchestrator.md +437 -0
- package/app/agents/performance-optimizer.md +254 -0
- package/app/agents/predictive-analyst.md +57 -0
- package/app/agents/product-manager.md +194 -0
- package/app/agents/project-planner.md +287 -0
- package/app/agents/prompt-engineer.md +103 -0
- package/app/agents/qa-automation-engineer.md +182 -0
- package/app/agents/rag-engineer.md +201 -0
- package/app/agents/research-synthesizer.md +138 -0
- package/app/agents/search-specialist.md +101 -0
- package/app/agents/security-architect.md +62 -0
- package/app/agents/security-auditor.md +293 -0
- package/app/agents/seo-specialist.md +111 -0
- package/app/agents/system-governor.md +57 -0
- package/app/agents/tech-lead.md +62 -0
- package/app/agents/technical-researcher.md +103 -0
- package/app/agents/test-engineer.md +264 -0
- package/app/constitution.md +38 -0
- package/app/hooks/_profile-check.sh +11 -0
- package/app/hooks/guard-destructive.sh +74 -0
- package/app/hooks/guard-path.sh +73 -0
- package/app/hooks/post-tool-use.sh +35 -0
- package/app/hooks/pre-compact.sh +31 -0
- package/app/hooks/quality-check.sh +22 -0
- package/app/hooks/quality-gate.sh +49 -0
- package/app/hooks/save-session.sh +24 -0
- package/app/hooks/session-end.sh +37 -0
- package/app/hooks/session-start.sh +29 -0
- package/app/hooks/subagent-start.sh +16 -0
- package/app/hooks/subagent-stop.sh +16 -0
- package/app/hooks/track-usage.sh +50 -0
- package/app/hooks/user-prompt-submit.sh +25 -0
- package/app/hooks.json +178 -0
- package/app/mcp-defaults.json +23 -0
- package/app/output-styles/golden-rules.md +43 -0
- package/app/plugins/README.md +19 -0
- package/app/plugins/csharp-pack/README.md +11 -0
- package/app/plugins/csharp-pack/plugin.json +18 -0
- package/app/plugins/enterprise-pack/README.md +16 -0
- package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
- package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
- package/app/plugins/enterprise-pack/plugin.json +24 -0
- package/app/plugins/frontend-pack/README.md +14 -0
- package/app/plugins/frontend-pack/plugin.json +22 -0
- package/app/plugins/java-pack/README.md +11 -0
- package/app/plugins/java-pack/plugin.json +18 -0
- package/app/plugins/kotlin-pack/README.md +11 -0
- package/app/plugins/kotlin-pack/plugin.json +18 -0
- package/app/plugins/memory-pack/README.md +24 -0
- package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
- package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
- package/app/plugins/memory-pack/plugin.json +22 -0
- package/app/plugins/memory-pack/scripts/init_db.py +81 -0
- package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
- package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
- package/app/plugins/research-pack/README.md +14 -0
- package/app/plugins/research-pack/plugin.json +22 -0
- package/app/plugins/ruby-pack/README.md +11 -0
- package/app/plugins/ruby-pack/plugin.json +18 -0
- package/app/plugins/rust-pack/README.md +11 -0
- package/app/plugins/rust-pack/plugin.json +18 -0
- package/app/plugins/security-pack/README.md +15 -0
- package/app/plugins/security-pack/plugin.json +23 -0
- package/app/plugins/swift-pack/README.md +11 -0
- package/app/plugins/swift-pack/plugin.json +18 -0
- package/app/rules/claude-toolkit-rules.md +21 -0
- package/app/rules/git-conventions.md +5 -0
- package/app/rules/quality-gates.md +10 -0
- package/app/skills/_lib/__init__.py +1 -0
- package/app/skills/_lib/detect_utils.py +150 -0
- package/app/skills/agent-creator/SKILL.md +82 -0
- package/app/skills/analyze/SKILL.md +92 -0
- package/app/skills/analyze/scripts/complexity.py +165 -0
- package/app/skills/api-patterns/SKILL.md +305 -0
- package/app/skills/app-builder/SKILL.md +187 -0
- package/app/skills/architecture-audit/SKILL.md +141 -0
- package/app/skills/architecture-decision/SKILL.md +55 -0
- package/app/skills/architecture-decision/templates/adr-template.md +36 -0
- package/app/skills/biz-scan/SKILL.md +30 -0
- package/app/skills/briefing/SKILL.md +27 -0
- package/app/skills/build/SKILL.md +97 -0
- package/app/skills/build/scripts/detect-build.py +151 -0
- package/app/skills/chaos/SKILL.md +32 -0
- package/app/skills/ci/SKILL.md +77 -0
- package/app/skills/ci/scripts/ci-detect.py +135 -0
- package/app/skills/ci/templates/github-actions-node.yml +38 -0
- package/app/skills/ci/templates/github-actions-python.yml +42 -0
- package/app/skills/ci-cd-patterns/SKILL.md +299 -0
- package/app/skills/clean-code/SKILL.md +110 -0
- package/app/skills/clean-code/reference/dart.md +18 -0
- package/app/skills/clean-code/reference/go.md +23 -0
- package/app/skills/clean-code/reference/php.md +32 -0
- package/app/skills/clean-code/reference/python.md +180 -0
- package/app/skills/clean-code/reference/typescript.md +26 -0
- package/app/skills/command-creator/SKILL.md +83 -0
- package/app/skills/commit/SKILL.md +98 -0
- package/app/skills/commit/scripts/pre-commit-check.py +87 -0
- package/app/skills/commit/templates/conventional-commit.md +52 -0
- package/app/skills/csharp-patterns/SKILL.md +450 -0
- package/app/skills/database-patterns/SKILL.md +297 -0
- package/app/skills/debug/SKILL.md +154 -0
- package/app/skills/debug/scripts/error-parser.py +187 -0
- package/app/skills/debugging-tactics/SKILL.md +136 -0
- package/app/skills/deploy/SKILL.md +130 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
- package/app/skills/deploy/templates/deployment-checklist.md +31 -0
- package/app/skills/design-an-interface/SKILL.md +105 -0
- package/app/skills/design-engineering/SKILL.md +260 -0
- package/app/skills/docker-devops/SKILL.md +303 -0
- package/app/skills/docs/SKILL.md +145 -0
- package/app/skills/docs/scripts/doc-inventory.py +176 -0
- package/app/skills/docs/templates/adr-template.md +36 -0
- package/app/skills/docs/templates/readme-template.md +67 -0
- package/app/skills/documentation-standards/SKILL.md +191 -0
- package/app/skills/ecommerce-patterns/SKILL.md +209 -0
- package/app/skills/evaluate/SKILL.md +132 -0
- package/app/skills/evolve/SKILL.md +27 -0
- package/app/skills/explain/SKILL.md +54 -0
- package/app/skills/explain/scripts/dependency-graph.py +215 -0
- package/app/skills/explore/SKILL.md +112 -0
- package/app/skills/explore/scripts/visualize.py +117 -0
- package/app/skills/fix/SKILL.md +78 -0
- package/app/skills/fix/scripts/error-classifier.py +191 -0
- package/app/skills/flutter-patterns/SKILL.md +254 -0
- package/app/skills/git-mastery/SKILL.md +70 -0
- package/app/skills/grill-me/SKILL.md +38 -0
- package/app/skills/health/SKILL.md +91 -0
- package/app/skills/health/scripts/health_check.py +162 -0
- package/app/skills/hive-mind/SKILL.md +56 -0
- package/app/skills/hook-creator/SKILL.md +107 -0
- package/app/skills/index/SKILL.md +74 -0
- package/app/skills/instinct-review/SKILL.md +77 -0
- package/app/skills/java-patterns/SKILL.md +442 -0
- package/app/skills/kotlin-patterns/SKILL.md +446 -0
- package/app/skills/lint/SKILL.md +103 -0
- package/app/skills/lint/scripts/detect-linters.py +112 -0
- package/app/skills/mcp-patterns/SKILL.md +270 -0
- package/app/skills/mem-search/SKILL.md +70 -0
- package/app/skills/migrate/SKILL.md +90 -0
- package/app/skills/migrate/scripts/migration-status.py +195 -0
- package/app/skills/migration-patterns/SKILL.md +260 -0
- package/app/skills/night-watch/SKILL.md +28 -0
- package/app/skills/observability-patterns/SKILL.md +203 -0
- package/app/skills/onboard/SKILL.md +76 -0
- package/app/skills/orchestrate/SKILL.md +86 -0
- package/app/skills/panic/SKILL.md +30 -0
- package/app/skills/performance-profiling/SKILL.md +59 -0
- package/app/skills/plan/SKILL.md +110 -0
- package/app/skills/plan/templates/plan-template.md +40 -0
- package/app/skills/plan-writing/SKILL.md +201 -0
- package/app/skills/plugin-creator/SKILL.md +78 -0
- package/app/skills/pr/SKILL.md +129 -0
- package/app/skills/pr/scripts/pr-summary.py +175 -0
- package/app/skills/prd-to-issues/SKILL.md +108 -0
- package/app/skills/prd-to-plan/SKILL.md +120 -0
- package/app/skills/predict/SKILL.md +30 -0
- package/app/skills/qa-session/SKILL.md +110 -0
- package/app/skills/rag-patterns/SKILL.md +203 -0
- package/app/skills/refactor/SKILL.md +124 -0
- package/app/skills/refactor/scripts/refactor-scan.py +210 -0
- package/app/skills/refactor-plan/SKILL.md +112 -0
- package/app/skills/repeat/SKILL.md +149 -0
- package/app/skills/research-mastery/SKILL.md +56 -0
- package/app/skills/review/SKILL.md +141 -0
- package/app/skills/review/scripts/diff-analyzer.py +170 -0
- package/app/skills/rollback/SKILL.md +87 -0
- package/app/skills/rollback/scripts/rollback_info.py +149 -0
- package/app/skills/ruby-patterns/SKILL.md +454 -0
- package/app/skills/rust-patterns/SKILL.md +446 -0
- package/app/skills/search/SKILL.md +64 -0
- package/app/skills/security-patterns/SKILL.md +91 -0
- package/app/skills/security-patterns/reference/authentication.md +37 -0
- package/app/skills/security-patterns/reference/authorization.md +22 -0
- package/app/skills/security-patterns/reference/input-validation.md +30 -0
- package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
- package/app/skills/skill-creator/SKILL.md +154 -0
- package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
- package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
- package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
- package/app/skills/subagent-development/SKILL.md +225 -0
- package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
- package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
- package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
- package/app/skills/swarm/SKILL.md +81 -0
- package/app/skills/swift-patterns/SKILL.md +500 -0
- package/app/skills/tdd/SKILL.md +174 -0
- package/app/skills/tdd/reference/deep-modules.md +32 -0
- package/app/skills/tdd/reference/interface-design.md +32 -0
- package/app/skills/tdd/reference/mocking.md +52 -0
- package/app/skills/tdd/reference/refactoring.md +10 -0
- package/app/skills/tdd/reference/tests.md +59 -0
- package/app/skills/teams/SKILL.md +101 -0
- package/app/skills/test/SKILL.md +107 -0
- package/app/skills/test/scripts/detect-runner.py +113 -0
- package/app/skills/testing-patterns/SKILL.md +73 -0
- package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
- package/app/skills/testing-patterns/reference/go-testing.md +52 -0
- package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
- package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
- package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
- package/app/skills/triage-issue/SKILL.md +120 -0
- package/app/skills/typescript-patterns/SKILL.md +256 -0
- package/app/skills/ubiquitous-language/SKILL.md +74 -0
- package/app/skills/verification-before-completion/SKILL.md +108 -0
- package/app/skills/workflow/SKILL.md +250 -0
- package/app/skills/write-a-prd/SKILL.md +129 -0
- package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
- package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
- package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
- package/app/templates/skill/generator/SKILL.md.template +40 -0
- package/app/templates/skill/knowledge/SKILL.md.template +52 -0
- package/app/templates/skill/linter/SKILL.md.template +34 -0
- package/app/templates/skill/reviewer/SKILL.md.template +51 -0
- package/app/templates/skill/workflow/SKILL.md.template +49 -0
- package/benchmarks/README.md +111 -0
- package/benchmarks/ecosystem-dashboard.json +148 -0
- package/benchmarks/ecosystem-harvest.json +148 -0
- package/benchmarks/results.json +38 -0
- package/benchmarks/run.py +351 -0
- package/bin/ai-toolkit.js +345 -0
- package/kb/best-practices/README.md +11 -0
- package/kb/howto/README.md +11 -0
- package/kb/procedures/maintenance-sop.md +306 -0
- package/kb/reference/agents-catalog.md +124 -0
- package/kb/reference/anti-pattern-registry-format.md +221 -0
- package/kb/reference/architecture-overview.md +232 -0
- package/kb/reference/benchmark-config.md +62 -0
- package/kb/reference/ci-integration.md +66 -0
- package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
- package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
- package/kb/reference/commands-catalog.md +21 -0
- package/kb/reference/distribution-model.md +63 -0
- package/kb/reference/global-install-model.md +56 -0
- package/kb/reference/hierarchical-override-pattern.md +200 -0
- package/kb/reference/hooks-catalog.md +306 -0
- package/kb/reference/integrations.md +88 -0
- package/kb/reference/language-packs.md +52 -0
- package/kb/reference/merge-friendly-install-model.md +58 -0
- package/kb/reference/plugin-pack-conventions.md +151 -0
- package/kb/reference/quick-wins-implementation-summary.md +70 -0
- package/kb/reference/skill-templates.md +50 -0
- package/kb/reference/skills-catalog.md +215 -0
- package/kb/reference/skills-unification.md +57 -0
- package/kb/reference/stats.md +69 -0
- package/kb/reference/sync.md +76 -0
- package/kb/troubleshooting/README.md +11 -0
- package/llms-full.txt +3068 -0
- package/llms.txt +39 -0
- package/package.json +75 -0
- package/scripts/_common.py +160 -0
- package/scripts/add_rule.py +50 -0
- package/scripts/benchmark_config.py +127 -0
- package/scripts/benchmark_ecosystem.py +288 -0
- package/scripts/check_deps.py +260 -0
- package/scripts/create_skill.py +118 -0
- package/scripts/doctor.py +504 -0
- package/scripts/eject.py +113 -0
- package/scripts/emission.py +256 -0
- package/scripts/evaluate_skills.py +260 -0
- package/scripts/frontmatter.py +58 -0
- package/scripts/generate_agents_md.py +91 -0
- package/scripts/generate_aider_conf.py +51 -0
- package/scripts/generate_cline.py +35 -0
- package/scripts/generate_copilot.py +30 -0
- package/scripts/generate_cursor_rules.py +35 -0
- package/scripts/generate_gemini.py +28 -0
- package/scripts/generate_llms_txt.py +164 -0
- package/scripts/generate_roo_modes.py +80 -0
- package/scripts/generate_windsurf.py +35 -0
- package/scripts/generator_base.py +140 -0
- package/scripts/harvest_ecosystem.py +50 -0
- package/scripts/inject_rule_cli.py +101 -0
- package/scripts/inject_section_cli.py +47 -0
- package/scripts/injection.py +180 -0
- package/scripts/install.py +236 -0
- package/scripts/install_git_hooks.py +71 -0
- package/scripts/install_steps/__init__.py +5 -0
- package/scripts/install_steps/ai_tools.py +261 -0
- package/scripts/install_steps/hooks.py +90 -0
- package/scripts/install_steps/markers.py +79 -0
- package/scripts/install_steps/symlinks.py +87 -0
- package/scripts/merge-hooks.py +192 -0
- package/scripts/plugin.py +642 -0
- package/scripts/plugin_schema.py +138 -0
- package/scripts/remove_rule.py +58 -0
- package/scripts/stats.py +81 -0
- package/scripts/sync.py +215 -0
- package/scripts/uninstall.py +292 -0
- package/scripts/validate.py +700 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Plugin manifest schema validation.
|
|
2
|
+
|
|
3
|
+
Single source of truth for validating plugin.json manifests.
|
|
4
|
+
Used by both validate.py and plugin.py.
|
|
5
|
+
|
|
6
|
+
Stdlib-only.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Required top-level fields
|
|
14
|
+
REQUIRED_FIELDS = ("name", "description", "version", "domain", "type", "status")
|
|
15
|
+
|
|
16
|
+
# Valid status values
|
|
17
|
+
VALID_STATUSES = frozenset({"stable", "experimental", "deprecated"})
|
|
18
|
+
|
|
19
|
+
# Valid plugin types
|
|
20
|
+
VALID_TYPES = frozenset({"behavioral", "language", "domain", "integration"})
|
|
21
|
+
|
|
22
|
+
# Valid hook event names (must match validate.py VALID_HOOK_EVENTS)
|
|
23
|
+
VALID_HOOK_EVENTS = frozenset({
|
|
24
|
+
"SessionStart", "Notification", "PreToolUse", "PostToolUse", "Stop",
|
|
25
|
+
"PreCompact", "SubagentStop", "UserPromptSubmit", "TaskCompleted",
|
|
26
|
+
"TeammateIdle", "SubagentStart", "SessionEnd", "PermissionRequest", "Setup",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_manifest(data: dict, pack_dir: Path | None = None) -> list[str]:
|
|
31
|
+
"""Validate a plugin manifest dict.
|
|
32
|
+
|
|
33
|
+
Returns a list of error messages (empty = valid).
|
|
34
|
+
"""
|
|
35
|
+
errors: list[str] = []
|
|
36
|
+
|
|
37
|
+
# Check required fields
|
|
38
|
+
for field in REQUIRED_FIELDS:
|
|
39
|
+
if field not in data or not data[field]:
|
|
40
|
+
errors.append(f"Missing required field: {field}")
|
|
41
|
+
|
|
42
|
+
# Validate status
|
|
43
|
+
status = data.get("status", "")
|
|
44
|
+
if status and status not in VALID_STATUSES:
|
|
45
|
+
errors.append(f"Invalid status '{status}' (valid: {', '.join(sorted(VALID_STATUSES))})")
|
|
46
|
+
|
|
47
|
+
# Validate includes structure
|
|
48
|
+
includes = data.get("includes")
|
|
49
|
+
if includes is None:
|
|
50
|
+
errors.append("Missing 'includes' field")
|
|
51
|
+
elif not isinstance(includes, dict):
|
|
52
|
+
errors.append("'includes' must be a dictionary")
|
|
53
|
+
else:
|
|
54
|
+
for key in ("agents", "skills", "rules", "hooks"):
|
|
55
|
+
val = includes.get(key, [])
|
|
56
|
+
if not isinstance(val, list):
|
|
57
|
+
errors.append(f"includes.{key} must be a list")
|
|
58
|
+
|
|
59
|
+
# Validate hook_events if present
|
|
60
|
+
hook_events = data.get("hook_events", {})
|
|
61
|
+
if hook_events:
|
|
62
|
+
if not isinstance(hook_events, dict):
|
|
63
|
+
errors.append("'hook_events' must be a dictionary mapping hook filenames to event names")
|
|
64
|
+
else:
|
|
65
|
+
for hook_file, event in hook_events.items():
|
|
66
|
+
if event not in VALID_HOOK_EVENTS:
|
|
67
|
+
errors.append(f"hook_events['{hook_file}']: invalid event '{event}'")
|
|
68
|
+
|
|
69
|
+
# Validate hook files exist (if pack_dir provided)
|
|
70
|
+
# Hooks can come from the plugin's own hooks/ dir OR from core app/hooks/
|
|
71
|
+
if pack_dir and includes and isinstance(includes, dict):
|
|
72
|
+
hooks_dir = pack_dir / "hooks"
|
|
73
|
+
# Resolve toolkit root to check core hooks
|
|
74
|
+
toolkit_hooks_dir = pack_dir.parent.parent / "hooks"
|
|
75
|
+
for hook_file in includes.get("hooks", []):
|
|
76
|
+
base_name = Path(hook_file).name
|
|
77
|
+
candidates = [
|
|
78
|
+
hooks_dir / hook_file,
|
|
79
|
+
hooks_dir / base_name,
|
|
80
|
+
pack_dir / hook_file,
|
|
81
|
+
toolkit_hooks_dir / base_name, # core hooks
|
|
82
|
+
]
|
|
83
|
+
if not any(c.is_file() for c in candidates):
|
|
84
|
+
errors.append(f"Hook file not found: {hook_file}")
|
|
85
|
+
|
|
86
|
+
return errors
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_references(
|
|
90
|
+
data: dict,
|
|
91
|
+
agents_dir: Path,
|
|
92
|
+
skills_dir: Path,
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
"""Validate that referenced agents and skills exist.
|
|
95
|
+
|
|
96
|
+
Returns a list of error messages.
|
|
97
|
+
"""
|
|
98
|
+
errors: list[str] = []
|
|
99
|
+
includes = data.get("includes", {})
|
|
100
|
+
|
|
101
|
+
for agent in includes.get("agents", []):
|
|
102
|
+
if not (agents_dir / f"{agent}.md").is_file():
|
|
103
|
+
errors.append(f"References missing agent: {agent}")
|
|
104
|
+
|
|
105
|
+
for skill in includes.get("skills", []):
|
|
106
|
+
if not (skills_dir / skill / "SKILL.md").is_file():
|
|
107
|
+
errors.append(f"References missing skill: {skill}")
|
|
108
|
+
|
|
109
|
+
return errors
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_hook_event(hook_filename: str, manifest: dict) -> str:
|
|
113
|
+
"""Resolve the Claude Code event for a hook file.
|
|
114
|
+
|
|
115
|
+
Checks hook_events in manifest first, then falls back to
|
|
116
|
+
filename-based guessing.
|
|
117
|
+
"""
|
|
118
|
+
# Check explicit mapping first
|
|
119
|
+
hook_events = manifest.get("hook_events", {})
|
|
120
|
+
base_name = Path(hook_filename).name
|
|
121
|
+
if base_name in hook_events:
|
|
122
|
+
return hook_events[base_name]
|
|
123
|
+
if hook_filename in hook_events:
|
|
124
|
+
return hook_events[hook_filename]
|
|
125
|
+
|
|
126
|
+
# Fallback: filename-based guessing
|
|
127
|
+
mapping = {
|
|
128
|
+
"observation-capture.sh": "PostToolUse",
|
|
129
|
+
"session-summary.sh": "Stop",
|
|
130
|
+
"status-line.sh": "Stop",
|
|
131
|
+
"output-style.sh": "Stop",
|
|
132
|
+
"session-end.sh": "SessionEnd",
|
|
133
|
+
"guard-destructive.sh": "PreToolUse",
|
|
134
|
+
"quality-gate.sh": "TaskCompleted",
|
|
135
|
+
"user-prompt-submit.sh": "UserPromptSubmit",
|
|
136
|
+
"post-tool-use.sh": "PostToolUse",
|
|
137
|
+
}
|
|
138
|
+
return mapping.get(base_name, "")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""remove-rule -- Unregister a rule (opposite of add-rule).
|
|
3
|
+
|
|
4
|
+
Removes the rule file from ~/.ai-toolkit/rules/ (so it is no longer
|
|
5
|
+
re-applied on future 'ai-toolkit install' runs) AND strips its injected
|
|
6
|
+
block from the target CLAUDE.md.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
remove_rule.py <rule-name> [target-dir]
|
|
10
|
+
|
|
11
|
+
Arguments:
|
|
12
|
+
rule-name Name of the rule (filename without .md)
|
|
13
|
+
target-dir Directory containing .claude/CLAUDE.md (default: $HOME)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
+
from _common import remove_rule_section
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
"""Unregister a rule and strip its injected block."""
|
|
26
|
+
if len(sys.argv) < 2:
|
|
27
|
+
print("Usage: remove_rule.py <rule-name> [target-dir]", file=sys.stderr)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
rule_name = sys.argv[1]
|
|
31
|
+
target_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path.home()
|
|
32
|
+
rules_dir = Path.home() / ".ai-toolkit" / "rules"
|
|
33
|
+
|
|
34
|
+
removed = 0
|
|
35
|
+
|
|
36
|
+
# 1. Unregister from ~/.ai-toolkit/rules/
|
|
37
|
+
rule_file = rules_dir / f"{rule_name}.md"
|
|
38
|
+
if rule_file.is_file():
|
|
39
|
+
rule_file.unlink()
|
|
40
|
+
print(f"Unregistered: '{rule_name}' (removed from {rules_dir})")
|
|
41
|
+
removed += 1
|
|
42
|
+
else:
|
|
43
|
+
print(f"Not registered: '{rule_name}' not found in {rules_dir}")
|
|
44
|
+
|
|
45
|
+
# 2. Strip injected block from .claude/CLAUDE.md
|
|
46
|
+
found = remove_rule_section(rule_name, target_dir)
|
|
47
|
+
if found:
|
|
48
|
+
print(f"Removed rule '{rule_name}' from {target_dir / '.claude' / 'CLAUDE.md'}")
|
|
49
|
+
removed += 1
|
|
50
|
+
|
|
51
|
+
if removed == 0:
|
|
52
|
+
print()
|
|
53
|
+
print("Nothing to unregister. To list registered rules:")
|
|
54
|
+
print(" ls ~/.ai-toolkit/rules/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
package/scripts/stats.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ai-toolkit stats -- Show skill usage statistics.
|
|
3
|
+
|
|
4
|
+
Reads ~/.ai-toolkit/stats.json (populated by track-usage.sh hook)
|
|
5
|
+
and displays a sorted table of skill invocations.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--reset Clear all stats
|
|
9
|
+
--json Output raw JSON
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
18
|
+
|
|
19
|
+
STATS_FILE = Path.home() / ".ai-toolkit" / "stats.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""Display, export, or reset usage statistics."""
|
|
24
|
+
flag = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
25
|
+
|
|
26
|
+
# --reset
|
|
27
|
+
if flag == "--reset":
|
|
28
|
+
if STATS_FILE.is_file():
|
|
29
|
+
STATS_FILE.unlink()
|
|
30
|
+
print("Stats reset.")
|
|
31
|
+
else:
|
|
32
|
+
print("No stats file found.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# --json
|
|
36
|
+
if flag == "--json":
|
|
37
|
+
if STATS_FILE.is_file():
|
|
38
|
+
print(STATS_FILE.read_text(encoding="utf-8"), end="")
|
|
39
|
+
else:
|
|
40
|
+
print("{}")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Default: pretty-print table
|
|
44
|
+
if not STATS_FILE.is_file():
|
|
45
|
+
print("No usage stats recorded yet.")
|
|
46
|
+
print("Stats are collected when skills are invoked via slash commands.")
|
|
47
|
+
print()
|
|
48
|
+
print(f"File: {STATS_FILE}")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
print("AI Toolkit Usage Stats")
|
|
52
|
+
print("========================")
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
with open(STATS_FILE, encoding="utf-8") as f:
|
|
56
|
+
data: dict = json.load(f)
|
|
57
|
+
|
|
58
|
+
if not data:
|
|
59
|
+
print("No invocations recorded.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
rows = sorted(data.items(), key=lambda x: x[1].get("count", 0), reverse=True)
|
|
63
|
+
|
|
64
|
+
print(f"{'Skill':<30} {'Count':>6} {'Last Used':<20}")
|
|
65
|
+
print("-" * 60)
|
|
66
|
+
for name, info in rows:
|
|
67
|
+
count = info.get("count", 0)
|
|
68
|
+
last = info.get("last_used", "unknown")
|
|
69
|
+
print(f"{name:<30} {count:>6} {last:<20}")
|
|
70
|
+
|
|
71
|
+
total = sum(v.get("count", 0) for v in data.values())
|
|
72
|
+
print()
|
|
73
|
+
print(f"Total invocations: {total}")
|
|
74
|
+
print(f"Unique skills: {len(data)}")
|
|
75
|
+
print()
|
|
76
|
+
print(f"File: {STATS_FILE}")
|
|
77
|
+
print("Reset: ai-toolkit stats --reset")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
package/scripts/sync.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ai-toolkit sync — Sync config to/from GitHub Gist.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
sync.py --export Export config snapshot as JSON to stdout
|
|
6
|
+
sync.py --push Push config to GitHub Gist (requires gh CLI)
|
|
7
|
+
sync.py --pull [gist-id] Pull config from Gist and apply
|
|
8
|
+
sync.py --import <file|url> Import config from local file or URL
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import urllib.request
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
|
+
from _common import toolkit_dir
|
|
24
|
+
|
|
25
|
+
CONFIG_DIR = Path.home() / ".ai-toolkit"
|
|
26
|
+
RULES_DIR = CONFIG_DIR / "rules"
|
|
27
|
+
GIST_ID_FILE = CONFIG_DIR / ".gist-id"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def do_export() -> str:
|
|
31
|
+
"""Export config snapshot as JSON."""
|
|
32
|
+
# Read toolkit version
|
|
33
|
+
version = "unknown"
|
|
34
|
+
pkg = toolkit_dir / "package.json"
|
|
35
|
+
if pkg.is_file():
|
|
36
|
+
with open(pkg) as f:
|
|
37
|
+
version = json.load(f).get("version", "unknown")
|
|
38
|
+
|
|
39
|
+
# Collect rules
|
|
40
|
+
rules: dict[str, str] = {}
|
|
41
|
+
if RULES_DIR.is_dir():
|
|
42
|
+
for f in sorted(RULES_DIR.glob("*.md")):
|
|
43
|
+
rules[f.stem] = f.read_text(encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
# Collect stats
|
|
46
|
+
stats: dict = {}
|
|
47
|
+
stats_file = CONFIG_DIR / "stats.json"
|
|
48
|
+
if stats_file.is_file():
|
|
49
|
+
try:
|
|
50
|
+
with open(stats_file) as f:
|
|
51
|
+
stats = json.load(f)
|
|
52
|
+
except (json.JSONDecodeError, OSError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
snapshot = {
|
|
56
|
+
"schema_version": 1,
|
|
57
|
+
"exported_at": datetime.now(timezone.utc).isoformat(),
|
|
58
|
+
"toolkit_version": version,
|
|
59
|
+
"rules": rules,
|
|
60
|
+
"stats": stats,
|
|
61
|
+
}
|
|
62
|
+
return json.dumps(snapshot, indent=2)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def do_import(source: str) -> None:
|
|
66
|
+
"""Import config from file or URL."""
|
|
67
|
+
tmpfile: str | None = None
|
|
68
|
+
|
|
69
|
+
if source.startswith("http"):
|
|
70
|
+
tmpfile = tempfile.mktemp(suffix=".json")
|
|
71
|
+
urllib.request.urlretrieve(source, tmpfile)
|
|
72
|
+
source = tmpfile
|
|
73
|
+
|
|
74
|
+
source_path = Path(source)
|
|
75
|
+
if not source_path.is_file():
|
|
76
|
+
print(f"Error: file not found: {source}", file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
with open(source_path) as f:
|
|
80
|
+
data = json.load(f)
|
|
81
|
+
|
|
82
|
+
if data.get("schema_version") != 1:
|
|
83
|
+
print("Error: unsupported schema version", file=sys.stderr)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
rules = data.get("rules", {})
|
|
87
|
+
if rules:
|
|
88
|
+
RULES_DIR.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
for name, content in rules.items():
|
|
90
|
+
if "/" in name or "\\" in name or ".." in name:
|
|
91
|
+
print(f" SKIPPED: '{name}' — invalid rule name (path traversal)", file=sys.stderr)
|
|
92
|
+
continue
|
|
93
|
+
path = RULES_DIR / f"{name}.md"
|
|
94
|
+
path.write_text(content, encoding="utf-8")
|
|
95
|
+
print(f" Applied rule: {name}")
|
|
96
|
+
|
|
97
|
+
print(f"\nImported {len(rules)} rules from {source_path}")
|
|
98
|
+
print(f"Toolkit version at export: {data.get('toolkit_version', 'unknown')}")
|
|
99
|
+
|
|
100
|
+
if tmpfile:
|
|
101
|
+
os.unlink(tmpfile)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def do_push() -> None:
|
|
105
|
+
"""Push config to GitHub Gist."""
|
|
106
|
+
if not shutil.which("gh"):
|
|
107
|
+
print("Error: gh CLI not found. Install: https://cli.github.com")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
result = subprocess.run(["gh", "auth", "status"], capture_output=True)
|
|
111
|
+
if result.returncode != 0:
|
|
112
|
+
print("Error: gh not authenticated. Run: gh auth login")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
tmpfile = tempfile.mktemp(suffix=".json")
|
|
116
|
+
Path(tmpfile).write_text(do_export(), encoding="utf-8")
|
|
117
|
+
|
|
118
|
+
if GIST_ID_FILE.is_file():
|
|
119
|
+
gist_id = GIST_ID_FILE.read_text().strip()
|
|
120
|
+
subprocess.run(
|
|
121
|
+
["gh", "gist", "edit", gist_id, "-f", "ai-toolkit-config.json", tmpfile],
|
|
122
|
+
check=True,
|
|
123
|
+
)
|
|
124
|
+
print(f"Updated gist: {gist_id}")
|
|
125
|
+
else:
|
|
126
|
+
result = subprocess.run(
|
|
127
|
+
["gh", "gist", "create", "--filename", "ai-toolkit-config.json",
|
|
128
|
+
"--desc", "ai-toolkit config sync", tmpfile],
|
|
129
|
+
capture_output=True, text=True,
|
|
130
|
+
)
|
|
131
|
+
gist_url = result.stdout.strip()
|
|
132
|
+
import re
|
|
133
|
+
match = re.search(r"[a-f0-9]{20,}", gist_url)
|
|
134
|
+
gist_id = match.group(0) if match else gist_url
|
|
135
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
GIST_ID_FILE.write_text(gist_id, encoding="utf-8")
|
|
137
|
+
print(f"Created gist: {gist_url}")
|
|
138
|
+
print(f"Gist ID saved to: {GIST_ID_FILE}")
|
|
139
|
+
|
|
140
|
+
os.unlink(tmpfile)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def do_pull(gist_id: str = "") -> None:
|
|
144
|
+
"""Pull config from GitHub Gist."""
|
|
145
|
+
if not shutil.which("gh"):
|
|
146
|
+
print("Error: gh CLI not found. Install: https://cli.github.com")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
if not gist_id and GIST_ID_FILE.is_file():
|
|
150
|
+
gist_id = GIST_ID_FILE.read_text().strip()
|
|
151
|
+
if not gist_id:
|
|
152
|
+
print("Error: no gist ID provided and no saved gist ID found")
|
|
153
|
+
print("Usage: ai-toolkit sync --pull <gist-id>")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
tmpfile = tempfile.mktemp(suffix=".json")
|
|
157
|
+
subprocess.run(
|
|
158
|
+
["gh", "gist", "view", gist_id, "-f", "ai-toolkit-config.json"],
|
|
159
|
+
stdout=open(tmpfile, "w"),
|
|
160
|
+
check=True,
|
|
161
|
+
)
|
|
162
|
+
do_import(tmpfile)
|
|
163
|
+
os.unlink(tmpfile)
|
|
164
|
+
|
|
165
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
GIST_ID_FILE.write_text(gist_id, encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main() -> None:
|
|
170
|
+
action = ""
|
|
171
|
+
arg = ""
|
|
172
|
+
args = sys.argv[1:]
|
|
173
|
+
i = 0
|
|
174
|
+
while i < len(args):
|
|
175
|
+
a = args[i]
|
|
176
|
+
if a == "--export":
|
|
177
|
+
action = "export"
|
|
178
|
+
elif a == "--push":
|
|
179
|
+
action = "push"
|
|
180
|
+
elif a == "--pull":
|
|
181
|
+
action = "pull"
|
|
182
|
+
if i + 1 < len(args) and not args[i + 1].startswith("--"):
|
|
183
|
+
i += 1
|
|
184
|
+
arg = args[i]
|
|
185
|
+
elif a == "--import":
|
|
186
|
+
action = "import"
|
|
187
|
+
if i + 1 < len(args):
|
|
188
|
+
i += 1
|
|
189
|
+
arg = args[i]
|
|
190
|
+
elif a.startswith("-"):
|
|
191
|
+
print(f"Unknown option: {a}")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
else:
|
|
194
|
+
arg = a
|
|
195
|
+
i += 1
|
|
196
|
+
|
|
197
|
+
if not action:
|
|
198
|
+
print("Usage: ai-toolkit sync [--export|--push|--pull <gist-id>|--import <file>]")
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
|
|
201
|
+
if action == "export":
|
|
202
|
+
print(do_export())
|
|
203
|
+
elif action == "import":
|
|
204
|
+
if not arg:
|
|
205
|
+
print("Error: --import requires a file path or URL")
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
do_import(arg)
|
|
208
|
+
elif action == "push":
|
|
209
|
+
do_push()
|
|
210
|
+
elif action == "pull":
|
|
211
|
+
do_pull(arg)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|