@softspark/ai-toolkit 1.7.0 → 1.9.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.
@@ -0,0 +1,163 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft-07/schema#",
3
+ "$id": "https://softspark.github.io/ai-toolkit/schemas/ai-toolkit-config.json",
4
+ "title": "ai-toolkit project configuration",
5
+ "description": "Project-level configuration for ai-toolkit with optional inheritance via extends.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "JSON Schema reference (ignored at runtime)."
12
+ },
13
+ "extends": {
14
+ "type": "string",
15
+ "description": "Base config to inherit from. npm package name (@scope/pkg or @scope/pkg@version), git URL (git+https://...), or local path (./path or ../path).",
16
+ "examples": [
17
+ "@mycompany/ai-toolkit-config",
18
+ "@mycompany/ai-toolkit-config@^2.0.0",
19
+ "git+https://github.com/myco/ai-config.git",
20
+ "../shared-config"
21
+ ]
22
+ },
23
+ "name": {
24
+ "type": "string",
25
+ "description": "Config identity (required for base configs published as npm packages)."
26
+ },
27
+ "version": {
28
+ "type": "string",
29
+ "description": "Semantic version (required for base configs).",
30
+ "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?(\\+[a-zA-Z0-9.]+)?$"
31
+ },
32
+ "description": {
33
+ "type": "string",
34
+ "description": "Human-readable description of this configuration."
35
+ },
36
+ "profile": {
37
+ "type": "string",
38
+ "enum": ["minimal", "standard", "strict", "full", "offline-slm"],
39
+ "default": "standard",
40
+ "description": "Installation profile controlling which modules are installed."
41
+ },
42
+ "agents": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "description": "Agent configuration — enable, disable, or add custom agents.",
46
+ "properties": {
47
+ "enabled": {
48
+ "type": "array",
49
+ "items": { "type": "string" },
50
+ "description": "Agent names to enable (merged with base)."
51
+ },
52
+ "disabled": {
53
+ "type": "array",
54
+ "items": { "type": "string" },
55
+ "description": "Agent names to disable (blocked if base enforces them)."
56
+ },
57
+ "custom": {
58
+ "type": "array",
59
+ "items": { "type": "string" },
60
+ "description": "Paths to custom agent .md files (relative to config root)."
61
+ }
62
+ }
63
+ },
64
+ "rules": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "description": "Rule injection configuration.",
68
+ "properties": {
69
+ "inject": {
70
+ "type": "array",
71
+ "items": { "type": "string" },
72
+ "description": "Paths to rule .md files to inject into CLAUDE.md."
73
+ },
74
+ "remove": {
75
+ "type": "array",
76
+ "items": { "type": "string" },
77
+ "description": "Rule names to remove from base config."
78
+ }
79
+ }
80
+ },
81
+ "constitution": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "description": "Constitution amendments. Articles I-V are immutable. Base articles are immutable. Projects can only ADD new articles.",
85
+ "properties": {
86
+ "amendments": {
87
+ "type": "array",
88
+ "items": {
89
+ "type": "object",
90
+ "required": ["article", "title", "text"],
91
+ "additionalProperties": false,
92
+ "properties": {
93
+ "article": {
94
+ "type": "integer",
95
+ "minimum": 1,
96
+ "description": "Article number. 1-5 are reserved (immutable). Base articles are also immutable."
97
+ },
98
+ "title": {
99
+ "type": "string",
100
+ "description": "Article title."
101
+ },
102
+ "text": {
103
+ "type": "string",
104
+ "description": "Article text."
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "enforce": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "description": "Non-overridable constraints (base configs only). Projects inheriting this config must comply.",
115
+ "properties": {
116
+ "minHookProfile": {
117
+ "type": "string",
118
+ "enum": ["minimal", "standard", "strict"],
119
+ "description": "Minimum hook profile — projects cannot go below this."
120
+ },
121
+ "requiredPlugins": {
122
+ "type": "array",
123
+ "items": { "type": "string" },
124
+ "description": "Plugins that must be installed in all inheriting projects."
125
+ },
126
+ "forbidOverride": {
127
+ "type": "array",
128
+ "items": { "type": "string" },
129
+ "description": "Components that cannot be overridden by inheriting projects."
130
+ },
131
+ "requiredAgents": {
132
+ "type": "array",
133
+ "items": { "type": "string" },
134
+ "description": "Agents that must be enabled in all inheriting projects."
135
+ }
136
+ }
137
+ },
138
+ "overrides": {
139
+ "type": "object",
140
+ "additionalProperties": {
141
+ "type": "object",
142
+ "required": ["override", "justification"],
143
+ "properties": {
144
+ "override": {
145
+ "type": "boolean",
146
+ "const": true,
147
+ "description": "Must be true to confirm intentional override."
148
+ },
149
+ "justification": {
150
+ "type": "string",
151
+ "minLength": 20,
152
+ "description": "Explanation for why this override is needed (min 20 chars)."
153
+ },
154
+ "replacement": {
155
+ "type": "string",
156
+ "description": "Replacement value or 'skip' to disable."
157
+ }
158
+ }
159
+ },
160
+ "description": "Explicit overrides of base config components. Each override requires override:true + justification."
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ """Update all registered projects in parallel.
3
+
4
+ Reads ~/.ai-toolkit/projects.json and runs install.py --local in each
5
+ project directory concurrently using a thread pool.
6
+
7
+ Stdlib-only — no external dependencies.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import subprocess
13
+ import sys
14
+ import time
15
+ from concurrent.futures import ThreadPoolExecutor, as_completed
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+ from install_steps.project_registry import get_active_projects, prune_stale
21
+
22
+
23
+ def _update_project(project: dict[str, Any], install_script: str, extra_args: list[str]) -> dict:
24
+ """Run install --local in a single project. Returns result dict."""
25
+ project_path = project["path"]
26
+ start = time.monotonic()
27
+
28
+ try:
29
+ proc = subprocess.run(
30
+ ["python3", install_script, "--local"] + extra_args,
31
+ cwd=project_path,
32
+ capture_output=True,
33
+ text=True,
34
+ timeout=120,
35
+ )
36
+ elapsed = time.monotonic() - start
37
+ return {
38
+ "path": project_path,
39
+ "profile": project.get("profile", ""),
40
+ "extends": project.get("extends", ""),
41
+ "success": proc.returncode == 0,
42
+ "elapsed": round(elapsed, 1),
43
+ "output": proc.stdout,
44
+ "error": proc.stderr if proc.returncode != 0 else "",
45
+ }
46
+ except subprocess.TimeoutExpired:
47
+ return {
48
+ "path": project_path,
49
+ "success": False,
50
+ "elapsed": 120.0,
51
+ "output": "",
52
+ "error": "Timed out after 120s",
53
+ }
54
+ except OSError as e:
55
+ return {
56
+ "path": project_path,
57
+ "success": False,
58
+ "elapsed": 0,
59
+ "output": "",
60
+ "error": str(e),
61
+ }
62
+
63
+
64
+ def main() -> None:
65
+ """Update all registered projects."""
66
+ # Parse args
67
+ verbose = "--verbose" in sys.argv or "-v" in sys.argv
68
+ json_output = "--json" in sys.argv
69
+ extra_args = [a for a in sys.argv[1:] if a not in ("--verbose", "-v", "--json")]
70
+
71
+ # Prune stale projects first
72
+ pruned = prune_stale()
73
+ if pruned and not json_output:
74
+ for p in pruned:
75
+ print(f" Pruned stale project: {p}")
76
+
77
+ # Get active projects
78
+ projects = get_active_projects()
79
+
80
+ if not projects:
81
+ if json_output:
82
+ print(json.dumps({"projects": [], "summary": "No registered projects"}))
83
+ else:
84
+ print(" No registered projects.")
85
+ print(" Run 'ai-toolkit install --local' in a project to register it.")
86
+ sys.exit(0)
87
+
88
+ install_script = str(Path(__file__).resolve().parent / "install.py")
89
+
90
+ if not json_output:
91
+ print(f" Updating {len(projects)} registered project(s)...")
92
+ print()
93
+
94
+ # Run in parallel (max 8 workers — don't overwhelm the system)
95
+ max_workers = min(len(projects), 8)
96
+ results: list[dict] = []
97
+
98
+ with ThreadPoolExecutor(max_workers=max_workers) as pool:
99
+ futures = {
100
+ pool.submit(_update_project, p, install_script, extra_args): p
101
+ for p in projects
102
+ }
103
+ for future in as_completed(futures):
104
+ result = future.result()
105
+ results.append(result)
106
+
107
+ if not json_output:
108
+ status = "✓" if result["success"] else "✗"
109
+ path_short = result["path"].replace(str(Path.home()), "~")
110
+ extends_info = f" (extends: {result.get('extends', '')})" if result.get("extends") else ""
111
+ print(f" {status} {path_short}{extends_info} ({result['elapsed']}s)")
112
+
113
+ if verbose and result["output"]:
114
+ for line in result["output"].strip().split("\n"):
115
+ print(f" {line}")
116
+
117
+ if result.get("error"):
118
+ for line in result["error"].strip().split("\n"):
119
+ print(f" ERROR: {line}")
120
+
121
+ # Summary
122
+ passed = sum(1 for r in results if r["success"])
123
+ failed = len(results) - passed
124
+
125
+ if json_output:
126
+ print(json.dumps({
127
+ "projects": results,
128
+ "total": len(results),
129
+ "passed": passed,
130
+ "failed": failed,
131
+ }, indent=2))
132
+ else:
133
+ print()
134
+ print(f" Updated: {passed}/{len(results)} projects" +
135
+ (f" ({failed} failed)" if failed else ""))
136
+
137
+ sys.exit(1 if failed else 0)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()