@softspark/ai-toolkit 2.12.0 → 3.0.1

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 (42) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +25 -8
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/skills/hook-creator/SKILL.md +84 -8
  5. package/app/skills/skill-creator/SKILL.md +8 -4
  6. package/benchmarks/ecosystem-doctor-snapshot.json +395 -0
  7. package/kb/history/completed/deep-coverage-v3-20260423.md +160 -0
  8. package/kb/history/completed/ecosystem-deep-sweep-20260423.md +273 -0
  9. package/kb/procedures/ecosystem-sync-sop.md +255 -0
  10. package/kb/procedures/maintenance-sop.md +13 -2
  11. package/kb/procedures/release-preparation-sop.md +94 -12
  12. package/kb/procedures/release-verification-sop.md +112 -8
  13. package/kb/reference/global-install-model.md +15 -2
  14. package/kb/reference/supported-tools-registry.md +229 -0
  15. package/llms-full.txt +1175 -24
  16. package/llms.txt +4 -0
  17. package/manifest.json +1 -1
  18. package/package.json +4 -1
  19. package/scripts/ecosystem_doctor.py +348 -0
  20. package/scripts/ecosystem_tools.json +500 -0
  21. package/scripts/generate_aider_conf.py +26 -1
  22. package/scripts/generate_antigravity.py +77 -8
  23. package/scripts/generate_augment_agents.py +161 -0
  24. package/scripts/generate_augment_commands.py +160 -0
  25. package/scripts/generate_augment_hooks.py +162 -0
  26. package/scripts/generate_augment_skills.py +98 -0
  27. package/scripts/generate_cline_rules.py +96 -9
  28. package/scripts/generate_codex_hooks.py +13 -2
  29. package/scripts/generate_codex_skills.py +195 -0
  30. package/scripts/generate_copilot.py +296 -18
  31. package/scripts/generate_cursor_agents.py +144 -0
  32. package/scripts/generate_cursor_hooks.py +155 -0
  33. package/scripts/generate_cursor_mdc.py +20 -8
  34. package/scripts/generate_gemini_commands.py +158 -0
  35. package/scripts/generate_gemini_hooks.py +159 -0
  36. package/scripts/generate_gemini_skills.py +98 -0
  37. package/scripts/generate_roo_modes.py +42 -1
  38. package/scripts/generate_windsurf_hooks.py +143 -0
  39. package/scripts/generate_windsurf_rules.py +162 -10
  40. package/scripts/install.py +11 -2
  41. package/scripts/install_steps/ai_tools.py +120 -5
  42. package/scripts/validate.py +20 -3
package/llms.txt CHANGED
@@ -13,10 +13,13 @@
13
13
 
14
14
  - [Best Practices](kb/best-practices/README.md)
15
15
  - [No Hardcoded Counts in Secondary Docs](kb/best-practices/no-hardcoded-counts.md)
16
+ - [Plan: Deep Coverage v3.0 — 100% Native Surface Utilization](kb/history/completed/deep-coverage-v3-20260423.md)
17
+ - [Plan: Ecosystem Deep Sweep — All 12 Supported Tools](kb/history/completed/ecosystem-deep-sweep-20260423.md)
16
18
  - [Plan: Enterprise Config Inheritance — Multi-Repo Governance with `extends`](kb/history/completed/enterprise-config-inheritance-plan-20260412.md)
17
19
  - [Plan: Offline-First SLM Profile — Lightweight Mode for Local Models](kb/history/completed/offline-slm-profile-plan-20260411.md)
18
20
  - [How-To Guides](kb/howto/README.md)
19
21
  - [Plan: Cloud Security Pack — Multi-Cloud Audit](kb/planning/cloud-security-pack-plan.md)
22
+ - [SOP: Ecosystem Sync](kb/procedures/ecosystem-sync-sop.md)
20
23
  - [SOP: Claude Toolkit Maintenance](kb/procedures/maintenance-sop.md)
21
24
  - [SOP: Release Preparation](kb/procedures/release-preparation-sop.md)
22
25
  - [SOP: Release Verification](kb/procedures/release-verification-sop.md)
@@ -52,6 +55,7 @@
52
55
  - [Skills Catalog](kb/reference/skills-catalog.md)
53
56
  - [Skills Unification Model](kb/reference/skills-unification.md)
54
57
  - [Usage Statistics](kb/reference/stats.md)
58
+ - [Supported Tools Registry](kb/reference/supported-tools-registry.md)
55
59
  - [Config Sync](kb/reference/sync.md)
56
60
  - [Unique Features & Differentiators](kb/reference/unique-features.md)
57
61
  - [Troubleshooting](kb/troubleshooting/README.md)
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.12.0",
2
+ "version": "3.0.1",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "2.12.0",
3
+ "version": "3.0.1",
4
4
  "description": "AI coding toolkit: 99 skills, 44 agents, 12-editor write-through (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo, Aider, Augment, Antigravity, Codex, opencode), machine-enforced safety constitution, SARIF audit, signed npm provenance.",
5
5
  "keywords": [
6
6
  "claude",
@@ -47,6 +47,9 @@
47
47
  "evaluate": "python3 scripts/evaluate_skills.py",
48
48
  "benchmark:ecosystem": "python3 scripts/benchmark_ecosystem.py --offline",
49
49
  "benchmark:harvest": "python3 scripts/harvest_ecosystem.py --offline",
50
+ "ecosystem:doctor": "python3 scripts/ecosystem_doctor.py --format text",
51
+ "ecosystem:doctor:offline": "python3 scripts/ecosystem_doctor.py --offline --format text",
52
+ "ecosystem:doctor:update": "python3 scripts/ecosystem_doctor.py --update",
50
53
  "generate:agents": "python3 scripts/generate_agents_md.py > AGENTS.md",
51
54
  "generate:cursor": "python3 scripts/generate_cursor_rules.py > .cursorrules",
52
55
  "generate:llms": "python3 scripts/generate_llms_txt.py > llms.txt && python3 scripts/generate_llms_txt.py --full > llms-full.txt",
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env python3
2
+ """Ecosystem Doctor — detects upstream drift across supported tools.
3
+
4
+ Reads scripts/ecosystem_tools.json, probes each tool's documentation page
5
+ and (when configured) its CLI version, diffs against benchmarks/ecosystem-doctor-snapshot.json,
6
+ and reports what changed since the last run.
7
+
8
+ Stdlib-only. JSON output by default; `--format text` for human view.
9
+ Non-zero exit in --check mode when drift is detected.
10
+
11
+ Usage:
12
+ ecosystem_doctor.py # check all tools, JSON to stdout
13
+ ecosystem_doctor.py --format text # human-readable
14
+ ecosystem_doctor.py --tool cursor # single tool
15
+ ecosystem_doctor.py --offline # skip network probes
16
+ ecosystem_doctor.py --update # refresh snapshot (overwrite)
17
+ ecosystem_doctor.py --check # exit 1 on any drift
18
+
19
+ Exit codes:
20
+ 0 no drift, or --update mode
21
+ 1 drift detected (in --check mode) or script error
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import hashlib
27
+ import json
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ import sys
32
+ import urllib.error
33
+ import urllib.request
34
+ from pathlib import Path
35
+
36
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
37
+ from _common import toolkit_dir
38
+
39
+ REGISTRY_PATH = toolkit_dir / "scripts" / "ecosystem_tools.json"
40
+ SNAPSHOT_PATH = toolkit_dir / "benchmarks" / "ecosystem-doctor-snapshot.json"
41
+
42
+ USER_AGENT = "ai-toolkit-ecosystem-doctor/1.0 (+https://github.com/softspark/ai-toolkit)"
43
+ HTTP_TIMEOUT = 15
44
+
45
+ HEADING_RE = re.compile(r"<h([1-3])[^>]*>(.*?)</h\1>", re.IGNORECASE | re.DOTALL)
46
+ MARKDOWN_HEADING_RE = re.compile(r"^(#{1,3})\s+(.+?)\s*$", re.MULTILINE)
47
+ TAG_RE = re.compile(r"<[^>]+>")
48
+ WHITESPACE_RE = re.compile(r"\s+")
49
+
50
+
51
+ def load_registry() -> dict:
52
+ """Read tool registry JSON."""
53
+ if not REGISTRY_PATH.is_file():
54
+ raise FileNotFoundError(f"Registry not found: {REGISTRY_PATH}")
55
+ return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
56
+
57
+
58
+ def load_snapshot() -> dict:
59
+ """Read last-seen snapshot or empty dict."""
60
+ if not SNAPSHOT_PATH.is_file():
61
+ return {"schema_version": 1, "tools": {}}
62
+ return json.loads(SNAPSHOT_PATH.read_text(encoding="utf-8"))
63
+
64
+
65
+ def save_snapshot(snapshot: dict) -> None:
66
+ """Write snapshot atomically."""
67
+ SNAPSHOT_PATH.parent.mkdir(parents=True, exist_ok=True)
68
+ tmp = SNAPSHOT_PATH.with_suffix(".tmp")
69
+ tmp.write_text(json.dumps(snapshot, indent=2, sort_keys=True) + "\n", encoding="utf-8")
70
+ tmp.replace(SNAPSHOT_PATH)
71
+
72
+
73
+ def fetch_url(url: str) -> tuple[str, str | None]:
74
+ """Fetch a URL. Returns (content, error). On failure, content is empty and error is set."""
75
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "text/html, text/plain, */*"})
76
+ try:
77
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp:
78
+ raw = resp.read()
79
+ charset = resp.headers.get_content_charset() or "utf-8"
80
+ return raw.decode(charset, errors="replace"), None
81
+ except urllib.error.HTTPError as e:
82
+ return "", f"HTTP {e.code}"
83
+ except urllib.error.URLError as e:
84
+ return "", f"URL error: {e.reason}"
85
+ except TimeoutError:
86
+ return "", f"timeout after {HTTP_TIMEOUT}s"
87
+ except Exception as e: # noqa: BLE001 — single-point error capture, safe degradation
88
+ return "", f"{type(e).__name__}: {e}"
89
+
90
+
91
+ def strip_html(s: str) -> str:
92
+ """Remove tags, collapse whitespace."""
93
+ return WHITESPACE_RE.sub(" ", TAG_RE.sub("", s)).strip()
94
+
95
+
96
+ def extract_headings(content: str) -> list[str]:
97
+ """Extract unique H1-H3 headings from HTML or Markdown.
98
+
99
+ Returns a sorted list (stable for diff).
100
+ """
101
+ found: set[str] = set()
102
+ for match in HEADING_RE.finditer(content):
103
+ text = strip_html(match.group(2))
104
+ if text:
105
+ found.add(text)
106
+ for match in MARKDOWN_HEADING_RE.finditer(content):
107
+ text = match.group(2).strip()
108
+ if text:
109
+ found.add(text)
110
+ return sorted(found)
111
+
112
+
113
+ def detect_markers(content: str, markers: list[str]) -> dict[str, bool]:
114
+ """For each expected marker, record whether it appears in content (case-insensitive)."""
115
+ lower = content.lower()
116
+ return {m: (m.lower() in lower) for m in markers}
117
+
118
+
119
+ def content_hash(content: str) -> str:
120
+ """SHA-256 of fetched content (hex, truncated)."""
121
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
122
+
123
+
124
+ def probe_version(probe: dict | None) -> str | None:
125
+ """Run the tool's version command. Returns None if not configured or command missing."""
126
+ if not probe or probe.get("kind") != "command":
127
+ return None
128
+ cmd = probe.get("command", "")
129
+ if not cmd:
130
+ return None
131
+ binary = cmd.split()[0]
132
+ if shutil.which(binary) is None:
133
+ return None
134
+ try:
135
+ result = subprocess.run(cmd.split(), capture_output=True, text=True, timeout=10)
136
+ return (result.stdout or result.stderr).strip().splitlines()[0] if result.returncode == 0 else None
137
+ except (subprocess.TimeoutExpired, OSError):
138
+ return None
139
+
140
+
141
+ def diff_lists(old: list[str], new: list[str]) -> tuple[list[str], list[str]]:
142
+ """Return (added, removed) between two sorted lists."""
143
+ old_set, new_set = set(old), set(new)
144
+ return sorted(new_set - old_set), sorted(old_set - new_set)
145
+
146
+
147
+ def check_tool(tool: dict, last_seen: dict, offline: bool) -> dict:
148
+ """Probe one tool and compare against its last-seen state. Returns a report dict.
149
+
150
+ `last_seen` is the per-tool state dict (keys: docs_hash, headings, markers, version).
151
+ """
152
+ tool_id = tool["id"]
153
+ previous = last_seen
154
+ report: dict = {
155
+ "id": tool_id,
156
+ "display_name": tool["display_name"],
157
+ "kind": tool.get("kind", "editor"),
158
+ "docs_url": tool["urls"].get("docs"),
159
+ "drift": [],
160
+ "current": {},
161
+ "errors": [],
162
+ }
163
+
164
+ # --- Static checks (offline-safe) -----------------------------------------
165
+ generators = tool.get("our_generators", [])
166
+ report["current"]["generator_count"] = len(generators)
167
+ missing_generators = [g for g in generators if not (toolkit_dir / g).is_file()]
168
+ if missing_generators:
169
+ report["errors"].append(f"Declared generators missing on disk: {missing_generators}")
170
+
171
+ # --- Dynamic checks (network) ---------------------------------------------
172
+ if not offline and tool["urls"].get("docs"):
173
+ content, err = fetch_url(tool["urls"]["docs"])
174
+ if err:
175
+ report["errors"].append(f"docs fetch: {err}")
176
+ else:
177
+ report["current"]["docs_hash"] = content_hash(content)
178
+ headings = extract_headings(content)
179
+ report["current"]["heading_count"] = len(headings)
180
+
181
+ # Diff headings vs previous run
182
+ prev_headings = previous.get("headings", [])
183
+ added, removed = diff_lists(prev_headings, headings)
184
+ if added:
185
+ report["drift"].append({"kind": "headings_added", "items": added[:20]})
186
+ if removed:
187
+ report["drift"].append({"kind": "headings_removed", "items": removed[:20]})
188
+
189
+ report["_headings_snapshot"] = headings # consumed by --update
190
+
191
+ # Marker presence check
192
+ markers_seen = detect_markers(content, tool.get("capability_markers", []))
193
+ report["current"]["markers"] = markers_seen
194
+ prev_markers = previous.get("markers", {})
195
+ marker_changes: list[str] = []
196
+ for m, seen in markers_seen.items():
197
+ if m in prev_markers and prev_markers[m] != seen:
198
+ marker_changes.append(f"{m}: {'+' if seen else '-'}")
199
+ if marker_changes:
200
+ report["drift"].append({"kind": "marker_flips", "items": marker_changes})
201
+
202
+ # Content hash diff (any change at all)
203
+ if previous.get("docs_hash") and previous["docs_hash"] != report["current"]["docs_hash"]:
204
+ if not added and not removed and not marker_changes:
205
+ report["drift"].append({"kind": "content_changed_no_heading_delta"})
206
+
207
+ # --- Version probe --------------------------------------------------------
208
+ if not offline:
209
+ version = probe_version(tool.get("version_probe"))
210
+ if version is not None:
211
+ report["current"]["version"] = version
212
+ prev_version = previous.get("version")
213
+ if prev_version and prev_version != version:
214
+ report["drift"].append({"kind": "version_changed", "old": prev_version, "new": version})
215
+
216
+ return report
217
+
218
+
219
+ def render_text(report: list[dict]) -> str:
220
+ """Render report as human-readable text."""
221
+ out: list[str] = ["# Ecosystem Doctor Report", ""]
222
+ clean: list[dict] = []
223
+ drifted: list[dict] = []
224
+ errored: list[dict] = []
225
+ for r in report:
226
+ if r.get("errors"):
227
+ errored.append(r)
228
+ elif r.get("drift"):
229
+ drifted.append(r)
230
+ else:
231
+ clean.append(r)
232
+
233
+ out.append(f"Tools checked: {len(report)}")
234
+ out.append(f" Clean: {len(clean)}")
235
+ out.append(f" Drift: {len(drifted)}")
236
+ out.append(f" Errored: {len(errored)}")
237
+ out.append("")
238
+
239
+ if drifted:
240
+ out.append("## Drift detected")
241
+ out.append("")
242
+ for r in drifted:
243
+ out.append(f"### {r['display_name']} ({r['id']})")
244
+ if r.get("docs_url"):
245
+ out.append(f" Docs: {r['docs_url']}")
246
+ for d in r["drift"]:
247
+ kind = d["kind"]
248
+ if kind == "headings_added":
249
+ out.append(f" + Added headings ({len(d['items'])}):")
250
+ for item in d["items"]:
251
+ out.append(f" - {item}")
252
+ elif kind == "headings_removed":
253
+ out.append(f" - Removed headings ({len(d['items'])}):")
254
+ for item in d["items"]:
255
+ out.append(f" - {item}")
256
+ elif kind == "marker_flips":
257
+ out.append(f" * Marker changes: {', '.join(d['items'])}")
258
+ elif kind == "version_changed":
259
+ out.append(f" * Version: {d['old']} -> {d['new']}")
260
+ elif kind == "content_changed_no_heading_delta":
261
+ out.append(" * Content changed (no heading delta) — minor edits or reordering")
262
+ out.append("")
263
+
264
+ if errored:
265
+ out.append("## Errors")
266
+ out.append("")
267
+ for r in errored:
268
+ out.append(f"- {r['display_name']} ({r['id']}): {'; '.join(r['errors'])}")
269
+ out.append("")
270
+
271
+ if clean and not drifted and not errored:
272
+ out.append("All tools match the last snapshot. No action needed.")
273
+
274
+ return "\n".join(out)
275
+
276
+
277
+ def update_snapshot(snapshot: dict, reports: list[dict]) -> dict:
278
+ """Merge doctor reports back into the snapshot for the next run."""
279
+ snapshot.setdefault("tools", {})
280
+ for r in reports:
281
+ if r.get("errors"):
282
+ continue # do not overwrite last-known-good state with error state
283
+ tool_state = snapshot["tools"].get(r["id"], {})
284
+ current = r.get("current", {})
285
+ if "docs_hash" in current:
286
+ tool_state["docs_hash"] = current["docs_hash"]
287
+ if "markers" in current:
288
+ tool_state["markers"] = current["markers"]
289
+ if "version" in current:
290
+ tool_state["version"] = current["version"]
291
+ if "_headings_snapshot" in r:
292
+ tool_state["headings"] = r["_headings_snapshot"]
293
+ snapshot["tools"][r["id"]] = tool_state
294
+ from datetime import datetime, timezone
295
+ snapshot["last_run"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
296
+ return snapshot
297
+
298
+
299
+ def main() -> None:
300
+ parser = argparse.ArgumentParser(description="Detect upstream drift in tools ai-toolkit integrates with.")
301
+ parser.add_argument("--tool", help="Check only this tool id (default: all)")
302
+ parser.add_argument("--offline", action="store_true", help="Skip network probes (static checks only)")
303
+ parser.add_argument("--format", choices=["json", "text"], default="json", help="Output format")
304
+ parser.add_argument("--update", action="store_true", help="Write current state to snapshot after check")
305
+ parser.add_argument("--check", action="store_true", help="Exit 1 when drift is detected")
306
+ args = parser.parse_args()
307
+
308
+ registry = load_registry()
309
+ snapshot = load_snapshot()
310
+
311
+ tools = registry.get("tools", [])
312
+ if args.tool:
313
+ tools = [t for t in tools if t["id"] == args.tool]
314
+ if not tools:
315
+ print(f"Unknown tool id: {args.tool}", file=sys.stderr)
316
+ sys.exit(1)
317
+
318
+ snapshot_tools = snapshot.get("tools", {})
319
+ reports = [check_tool(t, snapshot_tools.get(t["id"], {}), args.offline) for t in tools]
320
+
321
+ if args.update:
322
+ snapshot = update_snapshot(snapshot, reports)
323
+ save_snapshot(snapshot)
324
+
325
+ # Strip internal fields before serializing
326
+ for r in reports:
327
+ r.pop("_headings_snapshot", None)
328
+
329
+ if args.format == "text":
330
+ print(render_text(reports))
331
+ else:
332
+ print(json.dumps({"reports": reports}, indent=2, sort_keys=True))
333
+
334
+ if args.check:
335
+ # Content-hash-only drift (timestamps, ads, CSRF nonces) is expected noise
336
+ # on dynamic docs pages. Gate only on structural drift + errors.
337
+ def is_structural(drift_entry: dict) -> bool:
338
+ return drift_entry.get("kind") != "content_changed_no_heading_delta"
339
+
340
+ any_drift = any(
341
+ any(is_structural(d) for d in r.get("drift", [])) or r.get("errors")
342
+ for r in reports
343
+ )
344
+ sys.exit(1 if any_drift else 0)
345
+
346
+
347
+ if __name__ == "__main__":
348
+ main()