@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,642 @@
1
+ #!/usr/bin/env python3
2
+ """ai-toolkit plugin — install, remove, update, clean, and list plugin packs.
3
+
4
+ Usage:
5
+ plugin.py install <pack-name> [<pack-name> ...]
6
+ plugin.py install --all
7
+ plugin.py remove <pack-name> [<pack-name> ...]
8
+ plugin.py remove --all
9
+ plugin.py update <pack-name> [<pack-name> ...]
10
+ plugin.py update --all
11
+ plugin.py clean <pack-name> [--days N]
12
+ plugin.py list
13
+ plugin.py status
14
+
15
+ Actions:
16
+ install Copy plugin hooks/scripts, verify agents+skills are linked
17
+ remove Remove plugin hooks/scripts, leave core agents+skills intact
18
+ update Re-install plugin (remove + install), --all updates all installed
19
+ clean Prune old data (e.g. memory-pack observations older than --days N, default 90)
20
+ list Show available plugin packs with status
21
+ status Show what's currently installed
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import os
27
+ import shutil
28
+ import sqlite3 as sqlite
29
+ import subprocess
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
34
+ from _common import toolkit_dir, app_dir
35
+ from plugin_schema import resolve_hook_event, validate_manifest, validate_references
36
+
37
+
38
+ PLUGINS_DIR = app_dir / "plugins"
39
+ CLAUDE_DIR = Path.home() / ".claude"
40
+ HOOKS_DIR = Path.home() / ".ai-toolkit" / "hooks"
41
+ PLUGINS_STATE_FILE = Path.home() / ".ai-toolkit" / "plugins.json"
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # State management
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def load_state() -> dict:
49
+ """Load installed plugins state."""
50
+ if PLUGINS_STATE_FILE.is_file():
51
+ try:
52
+ with open(PLUGINS_STATE_FILE) as f:
53
+ return json.load(f)
54
+ except (json.JSONDecodeError, OSError):
55
+ pass
56
+ return {"installed": []}
57
+
58
+
59
+ def save_state(state: dict) -> None:
60
+ """Save installed plugins state."""
61
+ PLUGINS_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
62
+ with open(PLUGINS_STATE_FILE, "w") as f:
63
+ json.dump(state, f, indent=2)
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Plugin discovery
68
+ # ---------------------------------------------------------------------------
69
+
70
+ def list_available() -> list[dict]:
71
+ """List all available plugin packs."""
72
+ packs: list[dict] = []
73
+ if not PLUGINS_DIR.is_dir():
74
+ return packs
75
+ for d in sorted(PLUGINS_DIR.iterdir()):
76
+ manifest = d / "plugin.json"
77
+ if not manifest.is_file():
78
+ continue
79
+ try:
80
+ with open(manifest) as f:
81
+ data = json.load(f)
82
+ data["_dir"] = str(d)
83
+ packs.append(data)
84
+ except (json.JSONDecodeError, OSError):
85
+ continue
86
+ return packs
87
+
88
+
89
+ def find_pack(name: str) -> dict | None:
90
+ """Find a plugin pack by name."""
91
+ for pack in list_available():
92
+ if pack["name"] == name:
93
+ return pack
94
+ return None
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Install
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def _link_agents(includes: dict, pack_dir: Path, installed_items: list[str]) -> None:
102
+ """Verify and link referenced agents from a plugin pack."""
103
+ for agent in includes.get("agents", []):
104
+ agent_file = CLAUDE_DIR / "agents" / f"{agent}.md"
105
+ source_file = app_dir / "agents" / f"{agent}.md"
106
+ if agent_file.exists() or agent_file.is_symlink():
107
+ print(f" OK agent: {agent}")
108
+ elif source_file.is_file():
109
+ agent_file.parent.mkdir(parents=True, exist_ok=True)
110
+ agent_file.symlink_to(source_file)
111
+ print(f" Linked agent: {agent}")
112
+ installed_items.append(f"agent:{agent}")
113
+ else:
114
+ print(f" WARN agent not found: {agent}")
115
+
116
+
117
+ def _link_skills(includes: dict, pack_dir: Path, installed_items: list[str]) -> None:
118
+ """Verify and link referenced skills from a plugin pack."""
119
+ for skill in includes.get("skills", []):
120
+ skill_dir = CLAUDE_DIR / "skills" / skill
121
+ source_dir = app_dir / "skills" / skill
122
+ if skill_dir.exists() or skill_dir.is_symlink():
123
+ print(f" OK skill: {skill}")
124
+ elif source_dir.is_dir():
125
+ skill_dir.parent.mkdir(parents=True, exist_ok=True)
126
+ skill_dir.symlink_to(source_dir)
127
+ print(f" Linked skill: {skill}")
128
+ installed_items.append(f"skill:{skill}")
129
+ else:
130
+ plugin_skill = pack_dir / "skills" / skill / "SKILL.md"
131
+ if plugin_skill.is_file():
132
+ skill_dir.parent.mkdir(parents=True, exist_ok=True)
133
+ skill_dir.symlink_to(pack_dir / "skills" / skill)
134
+ print(f" Linked skill (from plugin): {skill}")
135
+ installed_items.append(f"skill:{skill}")
136
+ else:
137
+ print(f" WARN skill not found: {skill}")
138
+
139
+
140
+ def _copy_hooks(name: str, pack_dir: Path, installed_items: list[str]) -> None:
141
+ """Copy plugin-specific hook scripts."""
142
+ plugin_hooks_dir = pack_dir / "hooks"
143
+ if not plugin_hooks_dir.is_dir():
144
+ return
145
+ HOOKS_DIR.mkdir(parents=True, exist_ok=True)
146
+ for hook_file in sorted(plugin_hooks_dir.glob("*.sh")):
147
+ dest = HOOKS_DIR / f"plugin-{name}-{hook_file.name}"
148
+ shutil.copy2(hook_file, dest)
149
+ dest.chmod(dest.stat().st_mode | 0o111)
150
+ print(f" Copied hook: {hook_file.name} -> {dest.name}")
151
+ installed_items.append(f"hook:{dest.name}")
152
+
153
+
154
+ def _copy_scripts(name: str, pack_dir: Path, installed_items: list[str]) -> None:
155
+ """Copy plugin-specific scripts and run init if present."""
156
+ plugin_scripts_dir = pack_dir / "scripts"
157
+ if not plugin_scripts_dir.is_dir():
158
+ return
159
+ scripts_dest = Path.home() / ".ai-toolkit" / "plugin-scripts" / name
160
+ scripts_dest.mkdir(parents=True, exist_ok=True)
161
+ for script_file in sorted(plugin_scripts_dir.iterdir()):
162
+ if script_file.name.startswith("__"):
163
+ continue
164
+ dest = scripts_dest / script_file.name
165
+ shutil.copy2(script_file, dest)
166
+ if script_file.suffix in (".py", ".sh"):
167
+ dest.chmod(dest.stat().st_mode | 0o111)
168
+ print(f" Copied script: {script_file.name}")
169
+ installed_items.append(f"script:{dest}")
170
+
171
+ init_script = plugin_scripts_dir / "init_db.py"
172
+ if init_script.is_file():
173
+ result = subprocess.run(
174
+ ["python3", str(init_script)],
175
+ capture_output=True, text=True,
176
+ )
177
+ if result.returncode == 0:
178
+ print(f" Init: {result.stdout.strip()}")
179
+ else:
180
+ print(f" WARN init failed: {result.stderr.strip()}")
181
+
182
+
183
+ def install_pack(name: str) -> bool:
184
+ """Install a single plugin pack. Returns True if successful."""
185
+ pack = find_pack(name)
186
+ if not pack:
187
+ print(f" ERROR: plugin pack '{name}' not found")
188
+ print(f" Available: {', '.join(p['name'] for p in list_available())}")
189
+ return False
190
+
191
+ pack_dir = Path(pack["_dir"])
192
+ includes = pack.get("includes", {})
193
+ installed_items: list[str] = []
194
+
195
+ print(f" Installing: {name} ({pack.get('description', '')})")
196
+
197
+ _link_agents(includes, pack_dir, installed_items)
198
+ _link_skills(includes, pack_dir, installed_items)
199
+ _copy_hooks(name, pack_dir, installed_items)
200
+ _copy_scripts(name, pack_dir, installed_items)
201
+
202
+ plugin_hooks_dir = pack_dir / "hooks"
203
+ if plugin_hooks_dir.is_dir() and any(plugin_hooks_dir.glob("*.sh")):
204
+ _inject_plugin_hooks(name, pack_dir)
205
+
206
+ state = load_state()
207
+ if name not in state["installed"]:
208
+ state["installed"].append(name)
209
+ save_state(state)
210
+
211
+ print(f" Done: {name} ({len(installed_items)} items)")
212
+ return True
213
+
214
+
215
+ def _inject_plugin_hooks(name: str, pack_dir: Path) -> None:
216
+ """Register plugin hooks in settings.json via manual JSON merge."""
217
+ settings_path = CLAUDE_DIR / "settings.json"
218
+ if not settings_path.is_file():
219
+ return
220
+
221
+ try:
222
+ with open(settings_path) as f:
223
+ settings = json.load(f)
224
+ except (json.JSONDecodeError, OSError):
225
+ return
226
+
227
+ hooks = settings.setdefault("hooks", {})
228
+ plugin_hooks_dir = pack_dir / "hooks"
229
+
230
+ for hook_file in sorted(plugin_hooks_dir.glob("*.sh")):
231
+ # Determine event from hook filename or manifest
232
+ event = _guess_event(hook_file.name, name)
233
+ if not event:
234
+ continue
235
+
236
+ dest_path = HOOKS_DIR / f"plugin-{name}-{hook_file.name}"
237
+ entry = {
238
+ "_source": f"ai-toolkit-plugin-{name}",
239
+ "matcher": "",
240
+ "hooks": [
241
+ {
242
+ "type": "command",
243
+ "command": str(dest_path),
244
+ }
245
+ ],
246
+ }
247
+
248
+ event_hooks = hooks.setdefault(event, [])
249
+ # Remove old entries from this plugin
250
+ event_hooks = [h for h in event_hooks if h.get("_source") != f"ai-toolkit-plugin-{name}"]
251
+ event_hooks.append(entry)
252
+ hooks[event] = event_hooks
253
+
254
+ with open(settings_path, "w") as f:
255
+ json.dump(settings, f, indent=4)
256
+ print(f" Merged hooks into settings.json")
257
+
258
+
259
+ def _guess_event(hook_filename: str, pack_name: str) -> str:
260
+ """Map hook filename to Claude Code event.
261
+
262
+ Uses hook_events from the plugin manifest if available,
263
+ falls back to filename-based guessing.
264
+ """
265
+ pack = find_pack(pack_name)
266
+ if pack:
267
+ return resolve_hook_event(hook_filename, pack)
268
+ # Fallback for unknown packs
269
+ return resolve_hook_event(hook_filename, {})
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Remove
274
+ # ---------------------------------------------------------------------------
275
+
276
+ def remove_pack(name: str) -> bool:
277
+ """Remove a plugin pack. Returns True if successful."""
278
+ state = load_state()
279
+ if name not in state["installed"]:
280
+ print(f" Plugin '{name}' is not installed")
281
+ return False
282
+
283
+ print(f" Removing: {name}")
284
+
285
+ # 1. Remove plugin hooks from ~/.ai-toolkit/hooks/
286
+ removed = 0
287
+ for hook in HOOKS_DIR.glob(f"plugin-{name}-*.sh"):
288
+ hook.unlink()
289
+ print(f" Removed hook: {hook.name}")
290
+ removed += 1
291
+
292
+ # 2. Remove plugin scripts
293
+ scripts_dir = Path.home() / ".ai-toolkit" / "plugin-scripts" / name
294
+ if scripts_dir.is_dir():
295
+ shutil.rmtree(scripts_dir)
296
+ print(f" Removed scripts: {scripts_dir}")
297
+ removed += 1
298
+
299
+ # 3. Strip plugin hooks from settings.json
300
+ _strip_plugin_hooks(name)
301
+
302
+ # 4. Update state
303
+ state["installed"] = [p for p in state["installed"] if p != name]
304
+ save_state(state)
305
+
306
+ print(f" Done: removed {name}")
307
+ return True
308
+
309
+
310
+ def _strip_plugin_hooks(name: str) -> None:
311
+ """Remove plugin hooks from settings.json."""
312
+ settings_path = CLAUDE_DIR / "settings.json"
313
+ if not settings_path.is_file():
314
+ return
315
+
316
+ try:
317
+ with open(settings_path) as f:
318
+ settings = json.load(f)
319
+ except (json.JSONDecodeError, OSError):
320
+ return
321
+
322
+ source_tag = f"ai-toolkit-plugin-{name}"
323
+ hooks = settings.get("hooks", {})
324
+ changed = False
325
+
326
+ for event in list(hooks.keys()):
327
+ original = hooks[event]
328
+ filtered = [h for h in original if h.get("_source") != source_tag]
329
+ if len(filtered) != len(original):
330
+ hooks[event] = filtered
331
+ changed = True
332
+ if not hooks[event]:
333
+ del hooks[event]
334
+
335
+ if changed:
336
+ with open(settings_path, "w") as f:
337
+ json.dump(settings, f, indent=4)
338
+ print(f" Stripped hooks from settings.json")
339
+
340
+
341
+ # ---------------------------------------------------------------------------
342
+ # Update
343
+ # ---------------------------------------------------------------------------
344
+
345
+ def update_pack(name: str) -> bool:
346
+ """Update a single plugin pack (remove + install). Returns True if successful."""
347
+ state = load_state()
348
+ if name not in state["installed"]:
349
+ print(f" Plugin '{name}' is not installed — use 'install' instead")
350
+ return False
351
+
352
+ print(f" Updating: {name}")
353
+ remove_pack(name)
354
+ return install_pack(name)
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Clean
359
+ # ---------------------------------------------------------------------------
360
+
361
+ MEMORY_DB = Path.home() / ".ai-toolkit" / "memory.db"
362
+
363
+ # Map plugin names to their clean logic
364
+ CLEANABLE_PLUGINS = {"memory-pack"}
365
+
366
+
367
+ def clean_pack(name: str, days: int = 90) -> bool:
368
+ """Prune old data for a plugin. Returns True if successful."""
369
+ state = load_state()
370
+ if name not in state["installed"]:
371
+ print(f" Plugin '{name}' is not installed")
372
+ return False
373
+
374
+ if name not in CLEANABLE_PLUGINS:
375
+ print(f" Plugin '{name}' has no data to clean")
376
+ return False
377
+
378
+ if name == "memory-pack":
379
+ return _clean_memory_pack(days)
380
+
381
+ return False
382
+
383
+
384
+ def _clean_memory_pack(days: int) -> bool:
385
+ """Prune memory-pack observations older than N days."""
386
+ if not MEMORY_DB.is_file():
387
+ print(" No memory database found")
388
+ return False
389
+
390
+ try:
391
+ conn = sqlite.connect(str(MEMORY_DB))
392
+ cur = conn.cursor()
393
+
394
+ # Count before
395
+ before = cur.execute("SELECT COUNT(*) FROM observations").fetchone()[0]
396
+
397
+ # Delete old observations
398
+ cur.execute(
399
+ "DELETE FROM observations WHERE created_at < datetime('now', ?)",
400
+ (f"-{days} days",),
401
+ )
402
+ pruned_obs = cur.rowcount
403
+
404
+ # Delete orphan sessions
405
+ cur.execute(
406
+ "DELETE FROM sessions WHERE session_id NOT IN "
407
+ "(SELECT DISTINCT session_id FROM observations) "
408
+ "AND ended_at IS NOT NULL"
409
+ )
410
+ pruned_sessions = cur.rowcount
411
+
412
+ conn.commit()
413
+ conn.execute("VACUUM")
414
+ conn.close()
415
+
416
+ after = before - pruned_obs
417
+ print(f" Cleaned memory-pack (older than {days} days):")
418
+ print(f" Observations: {before} -> {after} (pruned {pruned_obs})")
419
+ print(f" Sessions pruned: {pruned_sessions}")
420
+ print(f" DB size: {_human_size(MEMORY_DB.stat().st_size)}")
421
+ return True
422
+ except sqlite.Error as e:
423
+ print(f" ERROR: {e}")
424
+ return False
425
+
426
+
427
+ def _human_size(size_bytes: int) -> str:
428
+ """Format bytes as human-readable size."""
429
+ for unit in ("B", "KB", "MB", "GB"):
430
+ if size_bytes < 1024:
431
+ return f"{size_bytes:.1f} {unit}" if unit != "B" else f"{size_bytes} B"
432
+ size_bytes /= 1024
433
+ return f"{size_bytes:.1f} TB"
434
+
435
+
436
+ # ---------------------------------------------------------------------------
437
+ # List / Status
438
+ # ---------------------------------------------------------------------------
439
+
440
+ def cmd_list() -> None:
441
+ """List available plugin packs."""
442
+ packs = list_available()
443
+ state = load_state()
444
+
445
+ print("Available plugin packs:")
446
+ print()
447
+ print(f" {'Name':<20} {'Domain':<12} {'Status':<14} {'Agents':>7} {'Skills':>7} {'Hooks':>6} Installed")
448
+ print(f" {'-'*20} {'-'*12} {'-'*14} {'-'*7} {'-'*7} {'-'*6} {'-'*9}")
449
+
450
+ for pack in packs:
451
+ inc = pack.get("includes", {})
452
+ installed = "YES" if pack["name"] in state["installed"] else ""
453
+ print(
454
+ f" {pack['name']:<20} {pack.get('domain',''):<12} {pack.get('status',''):<14}"
455
+ f" {len(inc.get('agents',[])):>7} {len(inc.get('skills',[])):>7} {len(inc.get('hooks',[])):>6}"
456
+ f" {installed}"
457
+ )
458
+
459
+ print()
460
+ print(f" Total: {len(packs)} packs, {len(state['installed'])} installed")
461
+ print()
462
+ print(" Install: ai-toolkit plugin install <name>")
463
+ print(" Install all: ai-toolkit plugin install --all")
464
+ print(" Update: ai-toolkit plugin update <name>")
465
+ print(" Update all: ai-toolkit plugin update --all")
466
+ print(" Clean: ai-toolkit plugin clean <name> [--days N]")
467
+ print(" Remove: ai-toolkit plugin remove <name>")
468
+
469
+
470
+ def cmd_status() -> None:
471
+ """Show installed plugins with details."""
472
+ state = load_state()
473
+ if not state["installed"]:
474
+ print("No plugins installed.")
475
+ print("Run: ai-toolkit plugin list")
476
+ return
477
+
478
+ print("Installed plugins:")
479
+ for name in state["installed"]:
480
+ pack = find_pack(name)
481
+ if pack:
482
+ print(f" {name}: {pack.get('description', '')}")
483
+ # Check hooks
484
+ hooks = list(HOOKS_DIR.glob(f"plugin-{name}-*.sh"))
485
+ if hooks:
486
+ print(f" Hooks: {', '.join(h.name for h in hooks)}")
487
+ # Show memory-pack DB stats
488
+ if name == "memory-pack":
489
+ _show_memory_stats()
490
+ else:
491
+ print(f" {name}: (manifest not found — orphaned?)")
492
+
493
+
494
+ def _show_memory_stats() -> None:
495
+ """Show memory-pack database statistics."""
496
+ if not MEMORY_DB.is_file():
497
+ print(" DB: not initialized")
498
+ return
499
+ try:
500
+ conn = sqlite.connect(str(MEMORY_DB))
501
+ cur = conn.cursor()
502
+ obs_count = cur.execute("SELECT COUNT(*) FROM observations").fetchone()[0]
503
+ sess_count = cur.execute("SELECT COUNT(*) FROM sessions").fetchone()[0]
504
+ oldest = cur.execute("SELECT MIN(created_at) FROM observations").fetchone()[0]
505
+ newest = cur.execute("SELECT MAX(created_at) FROM observations").fetchone()[0]
506
+ conn.close()
507
+ db_size = _human_size(MEMORY_DB.stat().st_size)
508
+ print(f" DB: {db_size} | {obs_count} observations | {sess_count} sessions")
509
+ if oldest:
510
+ print(f" Range: {oldest} — {newest}")
511
+ except sqlite.Error:
512
+ print(f" DB: {_human_size(MEMORY_DB.stat().st_size)} (error reading stats)")
513
+
514
+
515
+ # ---------------------------------------------------------------------------
516
+ # Main
517
+ # ---------------------------------------------------------------------------
518
+
519
+ def _cmd_install(args: list[str]) -> None:
520
+ if not args:
521
+ print("Usage: ai-toolkit plugin install <pack-name> [...]")
522
+ print(" ai-toolkit plugin install --all")
523
+ sys.exit(1)
524
+ if "--all" in args:
525
+ packs = list_available()
526
+ print(f"Installing all {len(packs)} plugin packs...\n")
527
+ ok = 0
528
+ for pack in packs:
529
+ if install_pack(pack["name"]):
530
+ ok += 1
531
+ print()
532
+ print(f"Installed: {ok}/{len(packs)} packs")
533
+ else:
534
+ for name in args:
535
+ install_pack(name)
536
+ print()
537
+
538
+
539
+ def _cmd_remove(args: list[str]) -> None:
540
+ if not args:
541
+ print("Usage: ai-toolkit plugin remove <pack-name> [...]")
542
+ print(" ai-toolkit plugin remove --all")
543
+ sys.exit(1)
544
+ if "--all" in args:
545
+ state = load_state()
546
+ names = list(state["installed"])
547
+ if not names:
548
+ print("No plugins installed.")
549
+ return
550
+ for name in names:
551
+ remove_pack(name)
552
+ print()
553
+ else:
554
+ for name in args:
555
+ remove_pack(name)
556
+ print()
557
+
558
+
559
+ def _cmd_update(args: list[str]) -> None:
560
+ if not args:
561
+ print("Usage: ai-toolkit plugin update <pack-name> [...]")
562
+ print(" ai-toolkit plugin update --all")
563
+ sys.exit(1)
564
+ if "--all" in args:
565
+ state = load_state()
566
+ names = list(state["installed"])
567
+ if not names:
568
+ print("No plugins installed.")
569
+ return
570
+ print(f"Updating {len(names)} installed plugin(s)...\n")
571
+ ok = 0
572
+ for name in names:
573
+ if update_pack(name):
574
+ ok += 1
575
+ print()
576
+ print(f"Updated: {ok}/{len(names)} packs")
577
+ else:
578
+ for name in args:
579
+ update_pack(name)
580
+ print()
581
+
582
+
583
+ def _parse_clean_args(args: list[str]) -> tuple[list[str], int]:
584
+ days = 90
585
+ pack_names: list[str] = []
586
+ i = 0
587
+ while i < len(args):
588
+ if args[i] == "--days" and i + 1 < len(args):
589
+ try:
590
+ days = int(args[i + 1])
591
+ except ValueError:
592
+ print(f" ERROR: --days requires a number, got '{args[i + 1]}'")
593
+ sys.exit(1)
594
+ i += 2
595
+ else:
596
+ pack_names.append(args[i])
597
+ i += 1
598
+ return pack_names, days
599
+
600
+
601
+ def _cmd_clean(args: list[str]) -> None:
602
+ if not args:
603
+ print("Usage: ai-toolkit plugin clean <pack-name> [--days N]")
604
+ print(" Default: prune data older than 90 days")
605
+ sys.exit(1)
606
+ pack_names, days = _parse_clean_args(args)
607
+ if not pack_names:
608
+ print("Usage: ai-toolkit plugin clean <pack-name> [--days N]")
609
+ sys.exit(1)
610
+ for name in pack_names:
611
+ clean_pack(name, days)
612
+ print()
613
+
614
+
615
+ def main() -> None:
616
+ if len(sys.argv) < 2:
617
+ print(__doc__)
618
+ sys.exit(1)
619
+
620
+ action = sys.argv[1]
621
+ args = sys.argv[2:]
622
+
623
+ dispatch = {
624
+ "list": lambda: cmd_list(),
625
+ "status": lambda: cmd_status(),
626
+ "install": lambda: _cmd_install(args),
627
+ "remove": lambda: _cmd_remove(args),
628
+ "update": lambda: _cmd_update(args),
629
+ "clean": lambda: _cmd_clean(args),
630
+ }
631
+
632
+ handler = dispatch.get(action)
633
+ if handler:
634
+ handler()
635
+ else:
636
+ print(f"Unknown action: {action}")
637
+ print("Actions: install, remove, update, clean, list, status")
638
+ sys.exit(1)
639
+
640
+
641
+ if __name__ == "__main__":
642
+ main()