@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,504 @@
1
+ #!/usr/bin/env python3
2
+ """ai-toolkit doctor -- Installation health and configuration diagnostics.
3
+
4
+ Checks:
5
+ 1. Environment prerequisites (node, bash, python3, bats) + check_deps
6
+ 2. Global install integrity (symlinks, settings.json hooks)
7
+ 3. Hook scripts (existence, executable)
8
+ 4. Hook configuration (valid event names)
9
+ 5. Generated artifacts (AGENTS.md, llms.txt staleness)
10
+ 6. Planned assets
11
+ 7. Benchmark freshness
12
+ 8. Stale rules
13
+
14
+ Exit codes:
15
+ 0 all checks pass
16
+ 1 one or more checks failed
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import re
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
29
+ from _common import toolkit_dir
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Constants
34
+ # ---------------------------------------------------------------------------
35
+
36
+ CLAUDE_DIR = Path.home() / ".claude"
37
+ HOOKS_DIR = Path.home() / ".ai-toolkit" / "hooks"
38
+ RULES_DIR = Path.home() / ".ai-toolkit" / "rules"
39
+ BENCHMARK_DASHBOARD = toolkit_dir / "benchmarks" / "ecosystem-dashboard.json"
40
+
41
+ VALID_EVENTS = frozenset({
42
+ "SessionStart", "Notification", "PreToolUse", "PostToolUse", "Stop",
43
+ "PreCompact", "SubagentStop", "UserPromptSubmit", "TaskCompleted",
44
+ "TeammateIdle", "SubagentStart", "SessionEnd", "PermissionRequest", "Setup",
45
+ })
46
+
47
+ EXPECTED_HOOKS = [
48
+ "guard-destructive.sh",
49
+ "guard-path.sh",
50
+ "post-tool-use.sh",
51
+ "quality-check.sh",
52
+ "quality-gate.sh",
53
+ "save-session.sh",
54
+ "session-start.sh",
55
+ "pre-compact.sh",
56
+ "user-prompt-submit.sh",
57
+ "track-usage.sh",
58
+ "subagent-start.sh",
59
+ "subagent-stop.sh",
60
+ "session-end.sh",
61
+ ]
62
+
63
+ PLANNED_ASSETS = [
64
+ toolkit_dir / "app" / ".claude-plugin" / "plugin.json",
65
+ toolkit_dir / "scripts" / "benchmark_ecosystem.py",
66
+ toolkit_dir / "scripts" / "harvest_ecosystem.py",
67
+ toolkit_dir / "kb" / "reference" / "claude-ecosystem-benchmark-snapshot.md",
68
+ toolkit_dir / "kb" / "reference" / "plugin-pack-conventions.md",
69
+ toolkit_dir / "app" / "skills" / "plugin-creator" / "SKILL.md",
70
+ toolkit_dir / "app" / "skills" / "hook-creator" / "SKILL.md",
71
+ toolkit_dir / "app" / "skills" / "command-creator" / "SKILL.md",
72
+ toolkit_dir / "app" / "skills" / "agent-creator" / "SKILL.md",
73
+ ]
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Status helpers
78
+ # ---------------------------------------------------------------------------
79
+
80
+ class DiagResult:
81
+ """Accumulates pass/fail/warn/fix/skip counts."""
82
+
83
+ def __init__(self) -> None:
84
+ self.errors = 0
85
+ self.warnings = 0
86
+
87
+ def ok(self, msg: str) -> None:
88
+ print(f" OK: {msg}")
89
+
90
+ def fail(self, msg: str) -> None:
91
+ print(f" FAIL: {msg}")
92
+ self.errors += 1
93
+
94
+ def warn(self, msg: str) -> None:
95
+ print(f" WARN: {msg}")
96
+ self.warnings += 1
97
+
98
+ def skip(self, msg: str) -> None:
99
+ print(f" SKIP: {msg}")
100
+
101
+ def fixed(self, msg: str) -> None:
102
+ print(f" FIXED: {msg}")
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Version extraction
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def _get_version(binary: str) -> str:
110
+ """Get a version string from a binary."""
111
+ try:
112
+ result = subprocess.run(
113
+ [binary, "--version"],
114
+ capture_output=True,
115
+ text=True,
116
+ timeout=5,
117
+ )
118
+ output = result.stdout.strip() or result.stderr.strip()
119
+ m = re.search(r"(\d+\.\d+\.\d+)", output)
120
+ return m.group(1) if m else "unknown"
121
+ except (FileNotFoundError, subprocess.TimeoutExpired):
122
+ return ""
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Check 1: Environment
127
+ # ---------------------------------------------------------------------------
128
+
129
+ def check_environment(dr: DiagResult, fix_mode: bool) -> None:
130
+ """Check that required and optional binaries are present."""
131
+ print("## Environment")
132
+
133
+ # Basic binary checks (matching original bash behavior)
134
+ for binary, label, required in [
135
+ ("node", "node", True),
136
+ ("bash", "bash", True),
137
+ ("python3", "python3", False),
138
+ ("bats", "bats", False),
139
+ ]:
140
+ if shutil.which(binary):
141
+ version = _get_version(binary)
142
+ ver_str = f" {version}" if version else ""
143
+ dr.ok(f"{label}{ver_str}")
144
+ else:
145
+ if required:
146
+ dr.fail(f"{label} not found")
147
+ else:
148
+ dr.warn(f"{label} not found" + (" (needed for hook merge)" if binary == "python3" else " (needed for tests)"))
149
+
150
+ # Enhanced: also run check_deps logic
151
+ try:
152
+ from check_deps import check_deps as run_check_deps
153
+
154
+ results = run_check_deps(verbose=False)
155
+ if not results["all_ok"]:
156
+ for dep in results["required"]:
157
+ if not dep["found"] or not dep["version_ok"]:
158
+ hint = f" (fix: {dep['install_hint']})" if dep["install_hint"] else ""
159
+ dr.warn(f"check_deps: {dep['name']} -- {dep['reason']}{hint}")
160
+ except ImportError:
161
+ dr.warn("check_deps.py not available for enhanced dependency check")
162
+
163
+ print()
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Check 2: Global Install
168
+ # ---------------------------------------------------------------------------
169
+
170
+ def _check_symlinks(dr: DiagResult, fix_mode: bool, directory: Path,
171
+ source_subdir: str, label: str, glob_pattern: str,
172
+ is_file_check: bool) -> None:
173
+ """Check symlinks in a directory, optionally fixing broken ones."""
174
+ if not directory.is_dir():
175
+ dr.fail(f"{label} directory missing")
176
+ return
177
+
178
+ link_count = 0
179
+ broken_count = 0
180
+ for item in sorted(directory.glob(glob_pattern) if glob_pattern != "*" else directory.iterdir()):
181
+ if not item.is_symlink():
182
+ continue
183
+ link_count += 1
184
+ if not item.exists():
185
+ if fix_mode:
186
+ item.unlink()
187
+ source = toolkit_dir / "app" / source_subdir / item.name
188
+ check_fn = source.is_file if is_file_check else source.is_dir
189
+ if check_fn():
190
+ item.symlink_to(source)
191
+ dr.fixed(f"re-linked {label[:-1]}: {item.name}")
192
+ else:
193
+ dr.fixed(f"removed broken {label[:-1]} symlink: {item.name}")
194
+ else:
195
+ broken_count += 1
196
+ dr.warn(f"broken symlink: {item}")
197
+
198
+ if link_count > 0 and broken_count == 0:
199
+ dr.ok(f"{label}: {link_count} symlinks (0 broken)")
200
+ elif link_count == 0:
201
+ dr.fail(f"{label}: no symlinks found")
202
+
203
+
204
+ def _check_settings_json(dr: DiagResult) -> None:
205
+ """Check settings.json hooks and legacy hooks.json."""
206
+ settings_json = CLAUDE_DIR / "settings.json"
207
+ if settings_json.is_file():
208
+ content = settings_json.read_text(encoding="utf-8")
209
+ if '"_source"' in content and '"ai-toolkit"' in content:
210
+ hook_count = content.count('"ai-toolkit"')
211
+ dr.ok(f"settings.json: {hook_count} toolkit hook entries")
212
+ else:
213
+ dr.warn("settings.json exists but has no toolkit hooks")
214
+ else:
215
+ dr.fail("settings.json not found")
216
+
217
+ legacy_hooks = CLAUDE_DIR / "hooks.json"
218
+ if legacy_hooks.is_file():
219
+ dr.warn("legacy hooks.json still exists (run: ai-toolkit update)")
220
+
221
+
222
+ def check_global_install(dr: DiagResult, fix_mode: bool) -> None:
223
+ """Check symlinks and settings.json in ~/.claude/."""
224
+ print("## Global Install")
225
+
226
+ if CLAUDE_DIR.is_dir():
227
+ dr.ok(f"{CLAUDE_DIR} exists")
228
+ else:
229
+ dr.fail(f"{CLAUDE_DIR} not found (run: ai-toolkit install)")
230
+ print()
231
+ return
232
+
233
+ _check_symlinks(dr, fix_mode, CLAUDE_DIR / "agents", "agents", "agents", "*.md", True)
234
+ _check_symlinks(dr, fix_mode, CLAUDE_DIR / "skills", "skills", "skills", "*", False)
235
+ _check_settings_json(dr)
236
+
237
+ print()
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Check 3: Hook Scripts
242
+ # ---------------------------------------------------------------------------
243
+
244
+ def check_hook_scripts(dr: DiagResult, fix_mode: bool) -> None:
245
+ """Check that expected hook scripts exist and are executable."""
246
+ print("## Hook Scripts")
247
+
248
+ if not HOOKS_DIR.is_dir():
249
+ dr.fail(f"hook scripts directory missing: {HOOKS_DIR}")
250
+ print()
251
+ return
252
+
253
+ for hook in EXPECTED_HOOKS:
254
+ hook_path = HOOKS_DIR / hook
255
+ source_path = toolkit_dir / "app" / "hooks" / hook
256
+
257
+ if hook_path.is_file() and os.access(hook_path, os.X_OK):
258
+ dr.ok(hook)
259
+ elif hook_path.is_file():
260
+ if fix_mode:
261
+ hook_path.chmod(hook_path.stat().st_mode | 0o111)
262
+ dr.fixed(f"{hook} made executable")
263
+ else:
264
+ dr.warn(f"{hook} exists but is not executable")
265
+ else:
266
+ if fix_mode and source_path.is_file():
267
+ HOOKS_DIR.mkdir(parents=True, exist_ok=True)
268
+ import shutil as _shutil
269
+ _shutil.copy2(source_path, hook_path)
270
+ hook_path.chmod(hook_path.stat().st_mode | 0o111)
271
+ dr.fixed(f"restored {hook}")
272
+ else:
273
+ dr.fail(f"{hook} missing from {HOOKS_DIR}")
274
+
275
+ print()
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Check 4: Hook Configuration
280
+ # ---------------------------------------------------------------------------
281
+
282
+ def check_hook_configuration(dr: DiagResult) -> None:
283
+ """Validate event names in app/hooks.json."""
284
+ print("## Hook Configuration")
285
+
286
+ hooks_file = toolkit_dir / "app" / "hooks.json"
287
+ if not hooks_file.is_file():
288
+ dr.fail("app/hooks.json not found")
289
+ print()
290
+ return
291
+
292
+ try:
293
+ with open(hooks_file, encoding="utf-8") as f:
294
+ data = json.load(f)
295
+ except (json.JSONDecodeError, OSError):
296
+ dr.warn("could not parse hook events from app/hooks.json")
297
+ print()
298
+ return
299
+
300
+ hooks = data.get("hooks", {})
301
+ if not hooks:
302
+ dr.warn("could not parse hook events from app/hooks.json")
303
+ else:
304
+ for event in hooks:
305
+ if event in VALID_EVENTS:
306
+ dr.ok(f"event: {event}")
307
+ else:
308
+ dr.fail(f"unknown hook event: {event}")
309
+
310
+ print()
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # Check 5: Generated Artifacts
315
+ # ---------------------------------------------------------------------------
316
+
317
+ def check_generated_artifacts(dr: DiagResult, fix_mode: bool) -> None:
318
+ """Check existence and staleness of generated artifacts."""
319
+ print("## Generated Artifacts")
320
+
321
+ artifacts = {
322
+ "AGENTS.md": ("generate_agents_md.py", []),
323
+ "llms.txt": ("generate_llms_txt.py", []),
324
+ "llms-full.txt": ("generate_llms_txt.py", ["--full"]),
325
+ }
326
+
327
+ for artifact, (gen_script, gen_args) in artifacts.items():
328
+ path = toolkit_dir / artifact
329
+ if path.is_file():
330
+ dr.ok(f"{artifact} exists")
331
+ else:
332
+ if fix_mode:
333
+ result = subprocess.run(
334
+ ["python3", str(toolkit_dir / "scripts" / gen_script)] + gen_args,
335
+ capture_output=True,
336
+ text=True,
337
+ )
338
+ if result.returncode == 0:
339
+ path.write_text(result.stdout, encoding="utf-8")
340
+ dr.fixed(f"regenerated {artifact}")
341
+ else:
342
+ dr.fail(f"could not regenerate {artifact}")
343
+ else:
344
+ dr.warn(f"{artifact} missing (run: ai-toolkit generate-all)")
345
+
346
+ # Check if AGENTS.md is stale (agents added/removed since generation)
347
+ agents_md = toolkit_dir / "AGENTS.md"
348
+ if agents_md.is_file():
349
+ agents_dir = toolkit_dir / "app" / "agents"
350
+ actual_agents = sum(1 for f in agents_dir.glob("*.md") if f.is_file()) if agents_dir.is_dir() else 0
351
+ content = agents_md.read_text(encoding="utf-8")
352
+ mentioned_agents = len(re.findall(r"^### `", content, re.MULTILINE))
353
+ if actual_agents != mentioned_agents:
354
+ if fix_mode:
355
+ result = subprocess.run(
356
+ ["python3", str(toolkit_dir / "scripts" / "generate_agents_md.py")],
357
+ capture_output=True,
358
+ text=True,
359
+ )
360
+ if result.returncode == 0:
361
+ agents_md.write_text(result.stdout, encoding="utf-8")
362
+ dr.fixed(f"regenerated stale AGENTS.md ({mentioned_agents} -> {actual_agents} entries)")
363
+ else:
364
+ dr.warn(f"AGENTS.md may be stale: {mentioned_agents} entries vs {actual_agents} agent files")
365
+
366
+ print()
367
+
368
+
369
+ # ---------------------------------------------------------------------------
370
+ # Check 6: Planned Assets
371
+ # ---------------------------------------------------------------------------
372
+
373
+ def check_planned_assets(dr: DiagResult) -> None:
374
+ """Check that planned assets exist and are non-empty."""
375
+ print("## Planned Assets")
376
+
377
+ for asset in PLANNED_ASSETS:
378
+ if asset.is_file() and asset.stat().st_size > 0:
379
+ dr.ok(f"{asset.name} present")
380
+ else:
381
+ rel = str(asset.relative_to(toolkit_dir))
382
+ dr.fail(f"missing or empty asset: {rel}")
383
+
384
+ # Plugin pack count
385
+ plugins_dir = toolkit_dir / "app" / "plugins"
386
+ if plugins_dir.is_dir():
387
+ pack_count = sum(1 for d in plugins_dir.iterdir() if d.is_dir())
388
+ if pack_count > 0:
389
+ dr.ok(f"plugin packs: {pack_count} experimental pack(s) present")
390
+ else:
391
+ dr.warn("no plugin packs found in app/plugins")
392
+ else:
393
+ dr.warn("no plugin packs found in app/plugins")
394
+
395
+ print()
396
+
397
+
398
+ # ---------------------------------------------------------------------------
399
+ # Check 7: Benchmark Freshness
400
+ # ---------------------------------------------------------------------------
401
+
402
+ def check_benchmark_freshness(dr: DiagResult) -> None:
403
+ """Check benchmark dashboard freshness."""
404
+ print("## Benchmark Freshness")
405
+
406
+ if BENCHMARK_DASHBOARD.is_file():
407
+ try:
408
+ with open(BENCHMARK_DASHBOARD, encoding="utf-8") as f:
409
+ data = json.load(f)
410
+ freshness = data.get("freshness", {})
411
+ status = freshness.get("status", "unknown")
412
+ age = freshness.get("age_days", "?")
413
+ threshold = freshness.get("stale_threshold_days", "?")
414
+
415
+ if status == "fresh":
416
+ dr.ok(f"benchmark dashboard is fresh ({age} day(s) old, threshold {threshold})")
417
+ elif status == "aging":
418
+ dr.warn(f"benchmark dashboard is aging ({age} day(s) old, threshold {threshold})")
419
+ elif status == "stale":
420
+ dr.warn(f"benchmark dashboard is stale ({age} day(s) old, threshold {threshold}) -- run: python3 scripts/harvest_ecosystem.py")
421
+ else:
422
+ dr.warn("benchmark dashboard freshness unknown")
423
+ except (json.JSONDecodeError, OSError):
424
+ dr.warn("could not parse benchmark dashboard freshness")
425
+ else:
426
+ dr.fail("benchmark dashboard missing: benchmarks/ecosystem-dashboard.json")
427
+
428
+ harvest = toolkit_dir / "benchmarks" / "ecosystem-harvest.json"
429
+ if harvest.is_file():
430
+ dr.ok("ecosystem-harvest.json present")
431
+ else:
432
+ dr.warn("ecosystem-harvest.json missing (run: python3 scripts/harvest_ecosystem.py)")
433
+
434
+ print()
435
+
436
+
437
+ # ---------------------------------------------------------------------------
438
+ # Check 8: Stale Rules
439
+ # ---------------------------------------------------------------------------
440
+
441
+ def check_stale_rules(dr: DiagResult, fix_mode: bool) -> None:
442
+ """Check for stale symlinks and empty rule files."""
443
+ print()
444
+ print("## 8. Stale Rules")
445
+
446
+ if not RULES_DIR.is_dir():
447
+ dr.skip("No rules directory")
448
+ return
449
+
450
+ stale = 0
451
+ for rule_file in sorted(RULES_DIR.iterdir()):
452
+ # Check for stale symlinks
453
+ if rule_file.is_symlink() and not rule_file.exists():
454
+ print(f" WARNING: Stale symlink: {rule_file}")
455
+ stale += 1
456
+ if fix_mode:
457
+ rule_file.unlink()
458
+ print(" FIXED: removed stale symlink")
459
+ elif rule_file.is_file() and rule_file.stat().st_size == 0:
460
+ print(f" WARNING: Empty rule file: {rule_file}")
461
+ stale += 1
462
+
463
+ if stale == 0:
464
+ dr.ok("All rules healthy")
465
+
466
+
467
+ # ---------------------------------------------------------------------------
468
+ # Main
469
+ # ---------------------------------------------------------------------------
470
+
471
+ def main() -> None:
472
+ fix_mode = "--fix" in sys.argv[1:]
473
+
474
+ print("ai-toolkit doctor")
475
+ print("========================")
476
+ print()
477
+
478
+ dr = DiagResult()
479
+
480
+ check_environment(dr, fix_mode)
481
+ check_global_install(dr, fix_mode)
482
+ check_hook_scripts(dr, fix_mode)
483
+ check_hook_configuration(dr)
484
+ check_generated_artifacts(dr, fix_mode)
485
+ check_planned_assets(dr)
486
+ check_benchmark_freshness(dr)
487
+ check_stale_rules(dr, fix_mode)
488
+
489
+ # Summary
490
+ print("========================")
491
+ if fix_mode:
492
+ print("Mode: --fix (auto-repair enabled)")
493
+ print(f"Errors: {dr.errors} | Warnings: {dr.warnings}")
494
+
495
+ if dr.errors > 0:
496
+ print("HEALTH CHECK FAILED")
497
+ sys.exit(1)
498
+ else:
499
+ print("HEALTH CHECK PASSED")
500
+ sys.exit(0)
501
+
502
+
503
+ if __name__ == "__main__":
504
+ main()
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """ai-toolkit eject -- Export standalone toolkit config.
3
+
4
+ Creates a self-contained copy of the toolkit configuration in the
5
+ target directory with no symlinks and no dependency on ai-toolkit.
6
+
7
+ What it exports:
8
+ - .claude/agents/*.md (real files, not symlinks)
9
+ - .claude/skills/*/ (real directories, not symlinks)
10
+ - .claude/CLAUDE.md (inlined rules)
11
+ - .claude/constitution.md (full content)
12
+ - .claude/ARCHITECTURE.md (full content)
13
+
14
+ Usage:
15
+ ai-toolkit eject [target-dir]
16
+ target-dir defaults to current working directory
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import shutil
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
25
+ from _common import app_dir, inject_rule
26
+
27
+
28
+ def main() -> None:
29
+ """Eject toolkit into a standalone directory."""
30
+ target_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
31
+
32
+ print("ai-toolkit eject")
33
+ print("========================")
34
+ print(f"Source: {app_dir.parent}")
35
+ print(f"Target: {target_dir.resolve()}")
36
+ print()
37
+
38
+ claude_dir = target_dir / ".claude"
39
+ claude_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ # -- Agents: copy as real files ------------------------------------------
42
+ print("## Agents")
43
+ agents_out = claude_dir / "agents"
44
+ agents_out.mkdir(parents=True, exist_ok=True)
45
+ agent_count = 0
46
+ agents_src = app_dir / "agents"
47
+ if agents_src.is_dir():
48
+ for agent in sorted(agents_src.glob("*.md")):
49
+ dest = agents_out / agent.name
50
+ if dest.is_symlink():
51
+ dest.unlink()
52
+ shutil.copy2(agent, dest)
53
+ agent_count += 1
54
+ print(f" Copied: {agent_count} agents")
55
+
56
+ # -- Skills: copy as real directories ------------------------------------
57
+ print("## Skills")
58
+ skills_out = claude_dir / "skills"
59
+ skills_out.mkdir(parents=True, exist_ok=True)
60
+ skill_count = 0
61
+ skills_src = app_dir / "skills"
62
+ if skills_src.is_dir():
63
+ for skill in sorted(skills_src.iterdir()):
64
+ if not skill.is_dir():
65
+ continue
66
+ dest = skills_out / skill.name
67
+ if dest.is_symlink():
68
+ dest.unlink()
69
+ if dest.exists():
70
+ shutil.rmtree(dest)
71
+ shutil.copytree(skill, dest)
72
+ skill_count += 1
73
+ print(f" Copied: {skill_count} skills")
74
+
75
+ # -- CLAUDE.md: inline all rules -----------------------------------------
76
+ print("## Rules")
77
+ claude_md = claude_dir / "CLAUDE.md"
78
+ if not claude_md.exists():
79
+ claude_md.touch()
80
+
81
+ rule_count = 0
82
+ rules_src = app_dir / "rules"
83
+ if rules_src.is_dir():
84
+ for rule in sorted(rules_src.glob("*.md")):
85
+ inject_rule(rule, target_dir)
86
+ rule_count += 1
87
+ print(f" Inlined: {rule_count} rules into CLAUDE.md")
88
+
89
+ # -- Constitution and Architecture ---------------------------------------
90
+ print("## Config Files")
91
+ for filename in ("constitution.md", "ARCHITECTURE.md"):
92
+ src = app_dir / filename
93
+ if src.is_file():
94
+ dest = claude_dir / filename
95
+ if dest.is_symlink():
96
+ dest.unlink()
97
+ shutil.copy2(src, dest)
98
+ print(f" Copied: {filename}")
99
+
100
+ # -- Summary -------------------------------------------------------------
101
+ print()
102
+ print("========================")
103
+ print(f"Ejected: {agent_count} agents, {skill_count} skills, {rule_count} rules")
104
+ print()
105
+ print(f"The toolkit is now standalone in {target_dir.resolve()}/.claude/")
106
+ print("You can safely run: npm uninstall -g @softspark/ai-toolkit")
107
+ print()
108
+ print("Note: hooks are NOT ejected (they require settings.json merge).")
109
+ print("To keep hooks working, keep ai-toolkit installed globally.")
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()