@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.
Files changed (327) hide show
  1. package/AGENTS.md +412 -0
  2. package/CHANGELOG.md +68 -0
  3. package/LICENSE +21 -0
  4. package/README.md +632 -0
  5. package/action.yml +53 -0
  6. package/app/.claude-plugin/plugin.json +44 -0
  7. package/app/ARCHITECTURE.md +306 -0
  8. package/app/CLAUDE.md.template +23 -0
  9. package/app/agents/ai-engineer.md +128 -0
  10. package/app/agents/backend-specialist.md +193 -0
  11. package/app/agents/business-intelligence.md +54 -0
  12. package/app/agents/chaos-monkey.md +67 -0
  13. package/app/agents/chief-of-staff.md +51 -0
  14. package/app/agents/code-archaeologist.md +127 -0
  15. package/app/agents/code-reviewer.md +184 -0
  16. package/app/agents/command-expert.md +131 -0
  17. package/app/agents/data-analyst.md +205 -0
  18. package/app/agents/data-scientist.md +151 -0
  19. package/app/agents/database-architect.md +317 -0
  20. package/app/agents/debugger.md +238 -0
  21. package/app/agents/devops-implementer.md +194 -0
  22. package/app/agents/documenter.md +364 -0
  23. package/app/agents/explorer-agent.md +145 -0
  24. package/app/agents/fact-checker.md +172 -0
  25. package/app/agents/frontend-specialist.md +209 -0
  26. package/app/agents/game-developer.md +216 -0
  27. package/app/agents/incident-responder.md +226 -0
  28. package/app/agents/infrastructure-architect.md +127 -0
  29. package/app/agents/infrastructure-validator.md +247 -0
  30. package/app/agents/llm-ops-engineer.md +237 -0
  31. package/app/agents/mcp-expert.md +228 -0
  32. package/app/agents/mcp-server-architect.md +195 -0
  33. package/app/agents/mcp-testing-engineer.md +292 -0
  34. package/app/agents/meta-architect.md +58 -0
  35. package/app/agents/ml-engineer.md +136 -0
  36. package/app/agents/mobile-developer.md +190 -0
  37. package/app/agents/night-watchman.md +55 -0
  38. package/app/agents/nlp-engineer.md +154 -0
  39. package/app/agents/orchestrator.md +437 -0
  40. package/app/agents/performance-optimizer.md +254 -0
  41. package/app/agents/predictive-analyst.md +57 -0
  42. package/app/agents/product-manager.md +194 -0
  43. package/app/agents/project-planner.md +287 -0
  44. package/app/agents/prompt-engineer.md +103 -0
  45. package/app/agents/qa-automation-engineer.md +182 -0
  46. package/app/agents/rag-engineer.md +201 -0
  47. package/app/agents/research-synthesizer.md +138 -0
  48. package/app/agents/search-specialist.md +101 -0
  49. package/app/agents/security-architect.md +62 -0
  50. package/app/agents/security-auditor.md +293 -0
  51. package/app/agents/seo-specialist.md +111 -0
  52. package/app/agents/system-governor.md +57 -0
  53. package/app/agents/tech-lead.md +62 -0
  54. package/app/agents/technical-researcher.md +103 -0
  55. package/app/agents/test-engineer.md +264 -0
  56. package/app/constitution.md +38 -0
  57. package/app/hooks/_profile-check.sh +11 -0
  58. package/app/hooks/guard-destructive.sh +74 -0
  59. package/app/hooks/guard-path.sh +73 -0
  60. package/app/hooks/post-tool-use.sh +35 -0
  61. package/app/hooks/pre-compact.sh +31 -0
  62. package/app/hooks/quality-check.sh +22 -0
  63. package/app/hooks/quality-gate.sh +49 -0
  64. package/app/hooks/save-session.sh +24 -0
  65. package/app/hooks/session-end.sh +37 -0
  66. package/app/hooks/session-start.sh +29 -0
  67. package/app/hooks/subagent-start.sh +16 -0
  68. package/app/hooks/subagent-stop.sh +16 -0
  69. package/app/hooks/track-usage.sh +50 -0
  70. package/app/hooks/user-prompt-submit.sh +25 -0
  71. package/app/hooks.json +178 -0
  72. package/app/mcp-defaults.json +23 -0
  73. package/app/output-styles/golden-rules.md +43 -0
  74. package/app/plugins/README.md +19 -0
  75. package/app/plugins/csharp-pack/README.md +11 -0
  76. package/app/plugins/csharp-pack/plugin.json +18 -0
  77. package/app/plugins/enterprise-pack/README.md +16 -0
  78. package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
  79. package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
  80. package/app/plugins/enterprise-pack/plugin.json +24 -0
  81. package/app/plugins/frontend-pack/README.md +14 -0
  82. package/app/plugins/frontend-pack/plugin.json +22 -0
  83. package/app/plugins/java-pack/README.md +11 -0
  84. package/app/plugins/java-pack/plugin.json +18 -0
  85. package/app/plugins/kotlin-pack/README.md +11 -0
  86. package/app/plugins/kotlin-pack/plugin.json +18 -0
  87. package/app/plugins/memory-pack/README.md +24 -0
  88. package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
  89. package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
  90. package/app/plugins/memory-pack/plugin.json +22 -0
  91. package/app/plugins/memory-pack/scripts/init_db.py +81 -0
  92. package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
  93. package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
  94. package/app/plugins/research-pack/README.md +14 -0
  95. package/app/plugins/research-pack/plugin.json +22 -0
  96. package/app/plugins/ruby-pack/README.md +11 -0
  97. package/app/plugins/ruby-pack/plugin.json +18 -0
  98. package/app/plugins/rust-pack/README.md +11 -0
  99. package/app/plugins/rust-pack/plugin.json +18 -0
  100. package/app/plugins/security-pack/README.md +15 -0
  101. package/app/plugins/security-pack/plugin.json +23 -0
  102. package/app/plugins/swift-pack/README.md +11 -0
  103. package/app/plugins/swift-pack/plugin.json +18 -0
  104. package/app/rules/claude-toolkit-rules.md +21 -0
  105. package/app/rules/git-conventions.md +5 -0
  106. package/app/rules/quality-gates.md +10 -0
  107. package/app/skills/_lib/__init__.py +1 -0
  108. package/app/skills/_lib/detect_utils.py +150 -0
  109. package/app/skills/agent-creator/SKILL.md +82 -0
  110. package/app/skills/analyze/SKILL.md +92 -0
  111. package/app/skills/analyze/scripts/complexity.py +165 -0
  112. package/app/skills/api-patterns/SKILL.md +305 -0
  113. package/app/skills/app-builder/SKILL.md +187 -0
  114. package/app/skills/architecture-audit/SKILL.md +141 -0
  115. package/app/skills/architecture-decision/SKILL.md +55 -0
  116. package/app/skills/architecture-decision/templates/adr-template.md +36 -0
  117. package/app/skills/biz-scan/SKILL.md +30 -0
  118. package/app/skills/briefing/SKILL.md +27 -0
  119. package/app/skills/build/SKILL.md +97 -0
  120. package/app/skills/build/scripts/detect-build.py +151 -0
  121. package/app/skills/chaos/SKILL.md +32 -0
  122. package/app/skills/ci/SKILL.md +77 -0
  123. package/app/skills/ci/scripts/ci-detect.py +135 -0
  124. package/app/skills/ci/templates/github-actions-node.yml +38 -0
  125. package/app/skills/ci/templates/github-actions-python.yml +42 -0
  126. package/app/skills/ci-cd-patterns/SKILL.md +299 -0
  127. package/app/skills/clean-code/SKILL.md +110 -0
  128. package/app/skills/clean-code/reference/dart.md +18 -0
  129. package/app/skills/clean-code/reference/go.md +23 -0
  130. package/app/skills/clean-code/reference/php.md +32 -0
  131. package/app/skills/clean-code/reference/python.md +180 -0
  132. package/app/skills/clean-code/reference/typescript.md +26 -0
  133. package/app/skills/command-creator/SKILL.md +83 -0
  134. package/app/skills/commit/SKILL.md +98 -0
  135. package/app/skills/commit/scripts/pre-commit-check.py +87 -0
  136. package/app/skills/commit/templates/conventional-commit.md +52 -0
  137. package/app/skills/csharp-patterns/SKILL.md +450 -0
  138. package/app/skills/database-patterns/SKILL.md +297 -0
  139. package/app/skills/debug/SKILL.md +154 -0
  140. package/app/skills/debug/scripts/error-parser.py +187 -0
  141. package/app/skills/debugging-tactics/SKILL.md +136 -0
  142. package/app/skills/deploy/SKILL.md +130 -0
  143. package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
  144. package/app/skills/deploy/templates/deployment-checklist.md +31 -0
  145. package/app/skills/design-an-interface/SKILL.md +105 -0
  146. package/app/skills/design-engineering/SKILL.md +260 -0
  147. package/app/skills/docker-devops/SKILL.md +303 -0
  148. package/app/skills/docs/SKILL.md +145 -0
  149. package/app/skills/docs/scripts/doc-inventory.py +176 -0
  150. package/app/skills/docs/templates/adr-template.md +36 -0
  151. package/app/skills/docs/templates/readme-template.md +67 -0
  152. package/app/skills/documentation-standards/SKILL.md +191 -0
  153. package/app/skills/ecommerce-patterns/SKILL.md +209 -0
  154. package/app/skills/evaluate/SKILL.md +132 -0
  155. package/app/skills/evolve/SKILL.md +27 -0
  156. package/app/skills/explain/SKILL.md +54 -0
  157. package/app/skills/explain/scripts/dependency-graph.py +215 -0
  158. package/app/skills/explore/SKILL.md +112 -0
  159. package/app/skills/explore/scripts/visualize.py +117 -0
  160. package/app/skills/fix/SKILL.md +78 -0
  161. package/app/skills/fix/scripts/error-classifier.py +191 -0
  162. package/app/skills/flutter-patterns/SKILL.md +254 -0
  163. package/app/skills/git-mastery/SKILL.md +70 -0
  164. package/app/skills/grill-me/SKILL.md +38 -0
  165. package/app/skills/health/SKILL.md +91 -0
  166. package/app/skills/health/scripts/health_check.py +162 -0
  167. package/app/skills/hive-mind/SKILL.md +56 -0
  168. package/app/skills/hook-creator/SKILL.md +107 -0
  169. package/app/skills/index/SKILL.md +74 -0
  170. package/app/skills/instinct-review/SKILL.md +77 -0
  171. package/app/skills/java-patterns/SKILL.md +442 -0
  172. package/app/skills/kotlin-patterns/SKILL.md +446 -0
  173. package/app/skills/lint/SKILL.md +103 -0
  174. package/app/skills/lint/scripts/detect-linters.py +112 -0
  175. package/app/skills/mcp-patterns/SKILL.md +270 -0
  176. package/app/skills/mem-search/SKILL.md +70 -0
  177. package/app/skills/migrate/SKILL.md +90 -0
  178. package/app/skills/migrate/scripts/migration-status.py +195 -0
  179. package/app/skills/migration-patterns/SKILL.md +260 -0
  180. package/app/skills/night-watch/SKILL.md +28 -0
  181. package/app/skills/observability-patterns/SKILL.md +203 -0
  182. package/app/skills/onboard/SKILL.md +76 -0
  183. package/app/skills/orchestrate/SKILL.md +86 -0
  184. package/app/skills/panic/SKILL.md +30 -0
  185. package/app/skills/performance-profiling/SKILL.md +59 -0
  186. package/app/skills/plan/SKILL.md +110 -0
  187. package/app/skills/plan/templates/plan-template.md +40 -0
  188. package/app/skills/plan-writing/SKILL.md +201 -0
  189. package/app/skills/plugin-creator/SKILL.md +78 -0
  190. package/app/skills/pr/SKILL.md +129 -0
  191. package/app/skills/pr/scripts/pr-summary.py +175 -0
  192. package/app/skills/prd-to-issues/SKILL.md +108 -0
  193. package/app/skills/prd-to-plan/SKILL.md +120 -0
  194. package/app/skills/predict/SKILL.md +30 -0
  195. package/app/skills/qa-session/SKILL.md +110 -0
  196. package/app/skills/rag-patterns/SKILL.md +203 -0
  197. package/app/skills/refactor/SKILL.md +124 -0
  198. package/app/skills/refactor/scripts/refactor-scan.py +210 -0
  199. package/app/skills/refactor-plan/SKILL.md +112 -0
  200. package/app/skills/repeat/SKILL.md +149 -0
  201. package/app/skills/research-mastery/SKILL.md +56 -0
  202. package/app/skills/review/SKILL.md +141 -0
  203. package/app/skills/review/scripts/diff-analyzer.py +170 -0
  204. package/app/skills/rollback/SKILL.md +87 -0
  205. package/app/skills/rollback/scripts/rollback_info.py +149 -0
  206. package/app/skills/ruby-patterns/SKILL.md +454 -0
  207. package/app/skills/rust-patterns/SKILL.md +446 -0
  208. package/app/skills/search/SKILL.md +64 -0
  209. package/app/skills/security-patterns/SKILL.md +91 -0
  210. package/app/skills/security-patterns/reference/authentication.md +37 -0
  211. package/app/skills/security-patterns/reference/authorization.md +22 -0
  212. package/app/skills/security-patterns/reference/input-validation.md +30 -0
  213. package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
  214. package/app/skills/skill-creator/SKILL.md +154 -0
  215. package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
  216. package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
  217. package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
  218. package/app/skills/subagent-development/SKILL.md +225 -0
  219. package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
  220. package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
  221. package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
  222. package/app/skills/swarm/SKILL.md +81 -0
  223. package/app/skills/swift-patterns/SKILL.md +500 -0
  224. package/app/skills/tdd/SKILL.md +174 -0
  225. package/app/skills/tdd/reference/deep-modules.md +32 -0
  226. package/app/skills/tdd/reference/interface-design.md +32 -0
  227. package/app/skills/tdd/reference/mocking.md +52 -0
  228. package/app/skills/tdd/reference/refactoring.md +10 -0
  229. package/app/skills/tdd/reference/tests.md +59 -0
  230. package/app/skills/teams/SKILL.md +101 -0
  231. package/app/skills/test/SKILL.md +107 -0
  232. package/app/skills/test/scripts/detect-runner.py +113 -0
  233. package/app/skills/testing-patterns/SKILL.md +73 -0
  234. package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
  235. package/app/skills/testing-patterns/reference/go-testing.md +52 -0
  236. package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
  237. package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
  238. package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
  239. package/app/skills/triage-issue/SKILL.md +120 -0
  240. package/app/skills/typescript-patterns/SKILL.md +256 -0
  241. package/app/skills/ubiquitous-language/SKILL.md +74 -0
  242. package/app/skills/verification-before-completion/SKILL.md +108 -0
  243. package/app/skills/workflow/SKILL.md +250 -0
  244. package/app/skills/write-a-prd/SKILL.md +129 -0
  245. package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
  246. package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
  247. package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
  248. package/app/templates/skill/generator/SKILL.md.template +40 -0
  249. package/app/templates/skill/knowledge/SKILL.md.template +52 -0
  250. package/app/templates/skill/linter/SKILL.md.template +34 -0
  251. package/app/templates/skill/reviewer/SKILL.md.template +51 -0
  252. package/app/templates/skill/workflow/SKILL.md.template +49 -0
  253. package/benchmarks/README.md +111 -0
  254. package/benchmarks/ecosystem-dashboard.json +148 -0
  255. package/benchmarks/ecosystem-harvest.json +148 -0
  256. package/benchmarks/results.json +38 -0
  257. package/benchmarks/run.py +351 -0
  258. package/bin/ai-toolkit.js +345 -0
  259. package/kb/best-practices/README.md +11 -0
  260. package/kb/howto/README.md +11 -0
  261. package/kb/procedures/maintenance-sop.md +306 -0
  262. package/kb/reference/agents-catalog.md +124 -0
  263. package/kb/reference/anti-pattern-registry-format.md +221 -0
  264. package/kb/reference/architecture-overview.md +232 -0
  265. package/kb/reference/benchmark-config.md +62 -0
  266. package/kb/reference/ci-integration.md +66 -0
  267. package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
  268. package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
  269. package/kb/reference/commands-catalog.md +21 -0
  270. package/kb/reference/distribution-model.md +63 -0
  271. package/kb/reference/global-install-model.md +56 -0
  272. package/kb/reference/hierarchical-override-pattern.md +200 -0
  273. package/kb/reference/hooks-catalog.md +306 -0
  274. package/kb/reference/integrations.md +88 -0
  275. package/kb/reference/language-packs.md +52 -0
  276. package/kb/reference/merge-friendly-install-model.md +58 -0
  277. package/kb/reference/plugin-pack-conventions.md +151 -0
  278. package/kb/reference/quick-wins-implementation-summary.md +70 -0
  279. package/kb/reference/skill-templates.md +50 -0
  280. package/kb/reference/skills-catalog.md +215 -0
  281. package/kb/reference/skills-unification.md +57 -0
  282. package/kb/reference/stats.md +69 -0
  283. package/kb/reference/sync.md +76 -0
  284. package/kb/troubleshooting/README.md +11 -0
  285. package/llms-full.txt +3068 -0
  286. package/llms.txt +39 -0
  287. package/package.json +75 -0
  288. package/scripts/_common.py +160 -0
  289. package/scripts/add_rule.py +50 -0
  290. package/scripts/benchmark_config.py +127 -0
  291. package/scripts/benchmark_ecosystem.py +288 -0
  292. package/scripts/check_deps.py +260 -0
  293. package/scripts/create_skill.py +118 -0
  294. package/scripts/doctor.py +504 -0
  295. package/scripts/eject.py +113 -0
  296. package/scripts/emission.py +256 -0
  297. package/scripts/evaluate_skills.py +260 -0
  298. package/scripts/frontmatter.py +58 -0
  299. package/scripts/generate_agents_md.py +91 -0
  300. package/scripts/generate_aider_conf.py +51 -0
  301. package/scripts/generate_cline.py +35 -0
  302. package/scripts/generate_copilot.py +30 -0
  303. package/scripts/generate_cursor_rules.py +35 -0
  304. package/scripts/generate_gemini.py +28 -0
  305. package/scripts/generate_llms_txt.py +164 -0
  306. package/scripts/generate_roo_modes.py +80 -0
  307. package/scripts/generate_windsurf.py +35 -0
  308. package/scripts/generator_base.py +140 -0
  309. package/scripts/harvest_ecosystem.py +50 -0
  310. package/scripts/inject_rule_cli.py +101 -0
  311. package/scripts/inject_section_cli.py +47 -0
  312. package/scripts/injection.py +180 -0
  313. package/scripts/install.py +236 -0
  314. package/scripts/install_git_hooks.py +71 -0
  315. package/scripts/install_steps/__init__.py +5 -0
  316. package/scripts/install_steps/ai_tools.py +261 -0
  317. package/scripts/install_steps/hooks.py +90 -0
  318. package/scripts/install_steps/markers.py +79 -0
  319. package/scripts/install_steps/symlinks.py +87 -0
  320. package/scripts/merge-hooks.py +192 -0
  321. package/scripts/plugin.py +642 -0
  322. package/scripts/plugin_schema.py +138 -0
  323. package/scripts/remove_rule.py +58 -0
  324. package/scripts/stats.py +81 -0
  325. package/scripts/sync.py +215 -0
  326. package/scripts/uninstall.py +292 -0
  327. 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()
@@ -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()
@@ -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()