@softspark/ai-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +412 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +632 -0
- package/action.yml +53 -0
- package/app/.claude-plugin/plugin.json +44 -0
- package/app/ARCHITECTURE.md +306 -0
- package/app/CLAUDE.md.template +23 -0
- package/app/agents/ai-engineer.md +128 -0
- package/app/agents/backend-specialist.md +193 -0
- package/app/agents/business-intelligence.md +54 -0
- package/app/agents/chaos-monkey.md +67 -0
- package/app/agents/chief-of-staff.md +51 -0
- package/app/agents/code-archaeologist.md +127 -0
- package/app/agents/code-reviewer.md +184 -0
- package/app/agents/command-expert.md +131 -0
- package/app/agents/data-analyst.md +205 -0
- package/app/agents/data-scientist.md +151 -0
- package/app/agents/database-architect.md +317 -0
- package/app/agents/debugger.md +238 -0
- package/app/agents/devops-implementer.md +194 -0
- package/app/agents/documenter.md +364 -0
- package/app/agents/explorer-agent.md +145 -0
- package/app/agents/fact-checker.md +172 -0
- package/app/agents/frontend-specialist.md +209 -0
- package/app/agents/game-developer.md +216 -0
- package/app/agents/incident-responder.md +226 -0
- package/app/agents/infrastructure-architect.md +127 -0
- package/app/agents/infrastructure-validator.md +247 -0
- package/app/agents/llm-ops-engineer.md +237 -0
- package/app/agents/mcp-expert.md +228 -0
- package/app/agents/mcp-server-architect.md +195 -0
- package/app/agents/mcp-testing-engineer.md +292 -0
- package/app/agents/meta-architect.md +58 -0
- package/app/agents/ml-engineer.md +136 -0
- package/app/agents/mobile-developer.md +190 -0
- package/app/agents/night-watchman.md +55 -0
- package/app/agents/nlp-engineer.md +154 -0
- package/app/agents/orchestrator.md +437 -0
- package/app/agents/performance-optimizer.md +254 -0
- package/app/agents/predictive-analyst.md +57 -0
- package/app/agents/product-manager.md +194 -0
- package/app/agents/project-planner.md +287 -0
- package/app/agents/prompt-engineer.md +103 -0
- package/app/agents/qa-automation-engineer.md +182 -0
- package/app/agents/rag-engineer.md +201 -0
- package/app/agents/research-synthesizer.md +138 -0
- package/app/agents/search-specialist.md +101 -0
- package/app/agents/security-architect.md +62 -0
- package/app/agents/security-auditor.md +293 -0
- package/app/agents/seo-specialist.md +111 -0
- package/app/agents/system-governor.md +57 -0
- package/app/agents/tech-lead.md +62 -0
- package/app/agents/technical-researcher.md +103 -0
- package/app/agents/test-engineer.md +264 -0
- package/app/constitution.md +38 -0
- package/app/hooks/_profile-check.sh +11 -0
- package/app/hooks/guard-destructive.sh +74 -0
- package/app/hooks/guard-path.sh +73 -0
- package/app/hooks/post-tool-use.sh +35 -0
- package/app/hooks/pre-compact.sh +31 -0
- package/app/hooks/quality-check.sh +22 -0
- package/app/hooks/quality-gate.sh +49 -0
- package/app/hooks/save-session.sh +24 -0
- package/app/hooks/session-end.sh +37 -0
- package/app/hooks/session-start.sh +29 -0
- package/app/hooks/subagent-start.sh +16 -0
- package/app/hooks/subagent-stop.sh +16 -0
- package/app/hooks/track-usage.sh +50 -0
- package/app/hooks/user-prompt-submit.sh +25 -0
- package/app/hooks.json +178 -0
- package/app/mcp-defaults.json +23 -0
- package/app/output-styles/golden-rules.md +43 -0
- package/app/plugins/README.md +19 -0
- package/app/plugins/csharp-pack/README.md +11 -0
- package/app/plugins/csharp-pack/plugin.json +18 -0
- package/app/plugins/enterprise-pack/README.md +16 -0
- package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
- package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
- package/app/plugins/enterprise-pack/plugin.json +24 -0
- package/app/plugins/frontend-pack/README.md +14 -0
- package/app/plugins/frontend-pack/plugin.json +22 -0
- package/app/plugins/java-pack/README.md +11 -0
- package/app/plugins/java-pack/plugin.json +18 -0
- package/app/plugins/kotlin-pack/README.md +11 -0
- package/app/plugins/kotlin-pack/plugin.json +18 -0
- package/app/plugins/memory-pack/README.md +24 -0
- package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
- package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
- package/app/plugins/memory-pack/plugin.json +22 -0
- package/app/plugins/memory-pack/scripts/init_db.py +81 -0
- package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
- package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
- package/app/plugins/research-pack/README.md +14 -0
- package/app/plugins/research-pack/plugin.json +22 -0
- package/app/plugins/ruby-pack/README.md +11 -0
- package/app/plugins/ruby-pack/plugin.json +18 -0
- package/app/plugins/rust-pack/README.md +11 -0
- package/app/plugins/rust-pack/plugin.json +18 -0
- package/app/plugins/security-pack/README.md +15 -0
- package/app/plugins/security-pack/plugin.json +23 -0
- package/app/plugins/swift-pack/README.md +11 -0
- package/app/plugins/swift-pack/plugin.json +18 -0
- package/app/rules/claude-toolkit-rules.md +21 -0
- package/app/rules/git-conventions.md +5 -0
- package/app/rules/quality-gates.md +10 -0
- package/app/skills/_lib/__init__.py +1 -0
- package/app/skills/_lib/detect_utils.py +150 -0
- package/app/skills/agent-creator/SKILL.md +82 -0
- package/app/skills/analyze/SKILL.md +92 -0
- package/app/skills/analyze/scripts/complexity.py +165 -0
- package/app/skills/api-patterns/SKILL.md +305 -0
- package/app/skills/app-builder/SKILL.md +187 -0
- package/app/skills/architecture-audit/SKILL.md +141 -0
- package/app/skills/architecture-decision/SKILL.md +55 -0
- package/app/skills/architecture-decision/templates/adr-template.md +36 -0
- package/app/skills/biz-scan/SKILL.md +30 -0
- package/app/skills/briefing/SKILL.md +27 -0
- package/app/skills/build/SKILL.md +97 -0
- package/app/skills/build/scripts/detect-build.py +151 -0
- package/app/skills/chaos/SKILL.md +32 -0
- package/app/skills/ci/SKILL.md +77 -0
- package/app/skills/ci/scripts/ci-detect.py +135 -0
- package/app/skills/ci/templates/github-actions-node.yml +38 -0
- package/app/skills/ci/templates/github-actions-python.yml +42 -0
- package/app/skills/ci-cd-patterns/SKILL.md +299 -0
- package/app/skills/clean-code/SKILL.md +110 -0
- package/app/skills/clean-code/reference/dart.md +18 -0
- package/app/skills/clean-code/reference/go.md +23 -0
- package/app/skills/clean-code/reference/php.md +32 -0
- package/app/skills/clean-code/reference/python.md +180 -0
- package/app/skills/clean-code/reference/typescript.md +26 -0
- package/app/skills/command-creator/SKILL.md +83 -0
- package/app/skills/commit/SKILL.md +98 -0
- package/app/skills/commit/scripts/pre-commit-check.py +87 -0
- package/app/skills/commit/templates/conventional-commit.md +52 -0
- package/app/skills/csharp-patterns/SKILL.md +450 -0
- package/app/skills/database-patterns/SKILL.md +297 -0
- package/app/skills/debug/SKILL.md +154 -0
- package/app/skills/debug/scripts/error-parser.py +187 -0
- package/app/skills/debugging-tactics/SKILL.md +136 -0
- package/app/skills/deploy/SKILL.md +130 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
- package/app/skills/deploy/templates/deployment-checklist.md +31 -0
- package/app/skills/design-an-interface/SKILL.md +105 -0
- package/app/skills/design-engineering/SKILL.md +260 -0
- package/app/skills/docker-devops/SKILL.md +303 -0
- package/app/skills/docs/SKILL.md +145 -0
- package/app/skills/docs/scripts/doc-inventory.py +176 -0
- package/app/skills/docs/templates/adr-template.md +36 -0
- package/app/skills/docs/templates/readme-template.md +67 -0
- package/app/skills/documentation-standards/SKILL.md +191 -0
- package/app/skills/ecommerce-patterns/SKILL.md +209 -0
- package/app/skills/evaluate/SKILL.md +132 -0
- package/app/skills/evolve/SKILL.md +27 -0
- package/app/skills/explain/SKILL.md +54 -0
- package/app/skills/explain/scripts/dependency-graph.py +215 -0
- package/app/skills/explore/SKILL.md +112 -0
- package/app/skills/explore/scripts/visualize.py +117 -0
- package/app/skills/fix/SKILL.md +78 -0
- package/app/skills/fix/scripts/error-classifier.py +191 -0
- package/app/skills/flutter-patterns/SKILL.md +254 -0
- package/app/skills/git-mastery/SKILL.md +70 -0
- package/app/skills/grill-me/SKILL.md +38 -0
- package/app/skills/health/SKILL.md +91 -0
- package/app/skills/health/scripts/health_check.py +162 -0
- package/app/skills/hive-mind/SKILL.md +56 -0
- package/app/skills/hook-creator/SKILL.md +107 -0
- package/app/skills/index/SKILL.md +74 -0
- package/app/skills/instinct-review/SKILL.md +77 -0
- package/app/skills/java-patterns/SKILL.md +442 -0
- package/app/skills/kotlin-patterns/SKILL.md +446 -0
- package/app/skills/lint/SKILL.md +103 -0
- package/app/skills/lint/scripts/detect-linters.py +112 -0
- package/app/skills/mcp-patterns/SKILL.md +270 -0
- package/app/skills/mem-search/SKILL.md +70 -0
- package/app/skills/migrate/SKILL.md +90 -0
- package/app/skills/migrate/scripts/migration-status.py +195 -0
- package/app/skills/migration-patterns/SKILL.md +260 -0
- package/app/skills/night-watch/SKILL.md +28 -0
- package/app/skills/observability-patterns/SKILL.md +203 -0
- package/app/skills/onboard/SKILL.md +76 -0
- package/app/skills/orchestrate/SKILL.md +86 -0
- package/app/skills/panic/SKILL.md +30 -0
- package/app/skills/performance-profiling/SKILL.md +59 -0
- package/app/skills/plan/SKILL.md +110 -0
- package/app/skills/plan/templates/plan-template.md +40 -0
- package/app/skills/plan-writing/SKILL.md +201 -0
- package/app/skills/plugin-creator/SKILL.md +78 -0
- package/app/skills/pr/SKILL.md +129 -0
- package/app/skills/pr/scripts/pr-summary.py +175 -0
- package/app/skills/prd-to-issues/SKILL.md +108 -0
- package/app/skills/prd-to-plan/SKILL.md +120 -0
- package/app/skills/predict/SKILL.md +30 -0
- package/app/skills/qa-session/SKILL.md +110 -0
- package/app/skills/rag-patterns/SKILL.md +203 -0
- package/app/skills/refactor/SKILL.md +124 -0
- package/app/skills/refactor/scripts/refactor-scan.py +210 -0
- package/app/skills/refactor-plan/SKILL.md +112 -0
- package/app/skills/repeat/SKILL.md +149 -0
- package/app/skills/research-mastery/SKILL.md +56 -0
- package/app/skills/review/SKILL.md +141 -0
- package/app/skills/review/scripts/diff-analyzer.py +170 -0
- package/app/skills/rollback/SKILL.md +87 -0
- package/app/skills/rollback/scripts/rollback_info.py +149 -0
- package/app/skills/ruby-patterns/SKILL.md +454 -0
- package/app/skills/rust-patterns/SKILL.md +446 -0
- package/app/skills/search/SKILL.md +64 -0
- package/app/skills/security-patterns/SKILL.md +91 -0
- package/app/skills/security-patterns/reference/authentication.md +37 -0
- package/app/skills/security-patterns/reference/authorization.md +22 -0
- package/app/skills/security-patterns/reference/input-validation.md +30 -0
- package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
- package/app/skills/skill-creator/SKILL.md +154 -0
- package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
- package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
- package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
- package/app/skills/subagent-development/SKILL.md +225 -0
- package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
- package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
- package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
- package/app/skills/swarm/SKILL.md +81 -0
- package/app/skills/swift-patterns/SKILL.md +500 -0
- package/app/skills/tdd/SKILL.md +174 -0
- package/app/skills/tdd/reference/deep-modules.md +32 -0
- package/app/skills/tdd/reference/interface-design.md +32 -0
- package/app/skills/tdd/reference/mocking.md +52 -0
- package/app/skills/tdd/reference/refactoring.md +10 -0
- package/app/skills/tdd/reference/tests.md +59 -0
- package/app/skills/teams/SKILL.md +101 -0
- package/app/skills/test/SKILL.md +107 -0
- package/app/skills/test/scripts/detect-runner.py +113 -0
- package/app/skills/testing-patterns/SKILL.md +73 -0
- package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
- package/app/skills/testing-patterns/reference/go-testing.md +52 -0
- package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
- package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
- package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
- package/app/skills/triage-issue/SKILL.md +120 -0
- package/app/skills/typescript-patterns/SKILL.md +256 -0
- package/app/skills/ubiquitous-language/SKILL.md +74 -0
- package/app/skills/verification-before-completion/SKILL.md +108 -0
- package/app/skills/workflow/SKILL.md +250 -0
- package/app/skills/write-a-prd/SKILL.md +129 -0
- package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
- package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
- package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
- package/app/templates/skill/generator/SKILL.md.template +40 -0
- package/app/templates/skill/knowledge/SKILL.md.template +52 -0
- package/app/templates/skill/linter/SKILL.md.template +34 -0
- package/app/templates/skill/reviewer/SKILL.md.template +51 -0
- package/app/templates/skill/workflow/SKILL.md.template +49 -0
- package/benchmarks/README.md +111 -0
- package/benchmarks/ecosystem-dashboard.json +148 -0
- package/benchmarks/ecosystem-harvest.json +148 -0
- package/benchmarks/results.json +38 -0
- package/benchmarks/run.py +351 -0
- package/bin/ai-toolkit.js +345 -0
- package/kb/best-practices/README.md +11 -0
- package/kb/howto/README.md +11 -0
- package/kb/procedures/maintenance-sop.md +306 -0
- package/kb/reference/agents-catalog.md +124 -0
- package/kb/reference/anti-pattern-registry-format.md +221 -0
- package/kb/reference/architecture-overview.md +232 -0
- package/kb/reference/benchmark-config.md +62 -0
- package/kb/reference/ci-integration.md +66 -0
- package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
- package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
- package/kb/reference/commands-catalog.md +21 -0
- package/kb/reference/distribution-model.md +63 -0
- package/kb/reference/global-install-model.md +56 -0
- package/kb/reference/hierarchical-override-pattern.md +200 -0
- package/kb/reference/hooks-catalog.md +306 -0
- package/kb/reference/integrations.md +88 -0
- package/kb/reference/language-packs.md +52 -0
- package/kb/reference/merge-friendly-install-model.md +58 -0
- package/kb/reference/plugin-pack-conventions.md +151 -0
- package/kb/reference/quick-wins-implementation-summary.md +70 -0
- package/kb/reference/skill-templates.md +50 -0
- package/kb/reference/skills-catalog.md +215 -0
- package/kb/reference/skills-unification.md +57 -0
- package/kb/reference/stats.md +69 -0
- package/kb/reference/sync.md +76 -0
- package/kb/troubleshooting/README.md +11 -0
- package/llms-full.txt +3068 -0
- package/llms.txt +39 -0
- package/package.json +75 -0
- package/scripts/_common.py +160 -0
- package/scripts/add_rule.py +50 -0
- package/scripts/benchmark_config.py +127 -0
- package/scripts/benchmark_ecosystem.py +288 -0
- package/scripts/check_deps.py +260 -0
- package/scripts/create_skill.py +118 -0
- package/scripts/doctor.py +504 -0
- package/scripts/eject.py +113 -0
- package/scripts/emission.py +256 -0
- package/scripts/evaluate_skills.py +260 -0
- package/scripts/frontmatter.py +58 -0
- package/scripts/generate_agents_md.py +91 -0
- package/scripts/generate_aider_conf.py +51 -0
- package/scripts/generate_cline.py +35 -0
- package/scripts/generate_copilot.py +30 -0
- package/scripts/generate_cursor_rules.py +35 -0
- package/scripts/generate_gemini.py +28 -0
- package/scripts/generate_llms_txt.py +164 -0
- package/scripts/generate_roo_modes.py +80 -0
- package/scripts/generate_windsurf.py +35 -0
- package/scripts/generator_base.py +140 -0
- package/scripts/harvest_ecosystem.py +50 -0
- package/scripts/inject_rule_cli.py +101 -0
- package/scripts/inject_section_cli.py +47 -0
- package/scripts/injection.py +180 -0
- package/scripts/install.py +236 -0
- package/scripts/install_git_hooks.py +71 -0
- package/scripts/install_steps/__init__.py +5 -0
- package/scripts/install_steps/ai_tools.py +261 -0
- package/scripts/install_steps/hooks.py +90 -0
- package/scripts/install_steps/markers.py +79 -0
- package/scripts/install_steps/symlinks.py +87 -0
- package/scripts/merge-hooks.py +192 -0
- package/scripts/plugin.py +642 -0
- package/scripts/plugin_schema.py +138 -0
- package/scripts/remove_rule.py +58 -0
- package/scripts/stats.py +81 -0
- package/scripts/sync.py +215 -0
- package/scripts/uninstall.py +292 -0
- package/scripts/validate.py +700 -0
|
@@ -0,0 +1,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()
|