@softspark/ai-toolkit 4.12.0 → 4.13.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/CHANGELOG.md +30 -0
- package/README.md +33 -15
- package/app/.claude-plugin/plugin.json +11 -19
- package/app/ARCHITECTURE.md +6 -6
- package/app/claude-app/global-instructions.md +10 -0
- package/app/claude-app/hooks/hooks.json +284 -0
- package/app/claude-app/skills/ai-toolkit-rules/SKILL.md +359 -0
- package/app/hooks/config-desync-guard.sh +63 -23
- package/app/plugins/README.md +4 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +91 -25
- package/bin/ai-toolkit.js +21 -3
- package/kb/planning/drop-cascade-hooks-after-sunset.md +13 -8
- package/kb/procedures/ecosystem-sync-sop.md +5 -5
- package/kb/procedures/maintenance-sop.md +42 -4
- package/kb/procedures/release-preparation-sop.md +5 -1
- package/kb/procedures/release-verification-sop.md +25 -9
- package/kb/reference/architecture-overview.md +7 -2
- package/kb/reference/claude-ecosystem-expansion-foundations.md +12 -3
- package/kb/reference/cli-reference.md +4 -2
- package/kb/reference/global-install-model.md +24 -3
- package/kb/reference/hooks-catalog.md +2 -3
- package/kb/reference/plugin-pack-conventions.md +5 -5
- package/kb/reference/skills-catalog.md +2 -0
- package/kb/reference/supported-tools-registry.md +30 -13
- package/kb/reference/unique-features.md +3 -2
- package/llms-full.txt +179 -60
- package/manifest.json +8 -8
- package/package.json +4 -3
- package/scripts/claude_app.py +347 -0
- package/scripts/doctor.py +85 -4
- package/scripts/ecosystem_tools.json +33 -5
- package/scripts/generate_devin_hooks.py +3 -4
- package/scripts/generate_windsurf_skills.py +5 -6
- package/scripts/install.py +23 -21
- package/scripts/install_steps/ai_tools.py +96 -6
- package/scripts/install_steps/install_state.py +2 -0
- package/scripts/update_projects.py +12 -1
- package/scripts/validate.py +34 -2
- package/AGENTS.md +0 -655
- package/scripts/generate_windsurf_hooks.py +0 -152
package/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.13.0",
|
|
3
3
|
"components": {
|
|
4
4
|
"agents": {
|
|
5
5
|
"description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"tags": ["core", "agents"]
|
|
10
10
|
},
|
|
11
11
|
"skills": {
|
|
12
|
-
"description": "
|
|
12
|
+
"description": "108 skills (32 task + 30 hybrid + 46 knowledge)",
|
|
13
13
|
"path": "app/skills",
|
|
14
14
|
"target": ".claude/skills",
|
|
15
15
|
"type": "symlink",
|
|
16
16
|
"tags": ["core", "skills", "commands"]
|
|
17
17
|
},
|
|
18
18
|
"hooks": {
|
|
19
|
-
"description": "28 hook entries across 14 lifecycle events",
|
|
19
|
+
"description": "28 hook entries across 14 lifecycle events plus statusLine",
|
|
20
20
|
"path": "app/hooks.json + app/hooks/*.sh",
|
|
21
21
|
"target": "~/.claude/settings.json (merge) + ~/.softspark/ai-toolkit/hooks/ (copy)",
|
|
22
22
|
"type": "merge+copy",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tags": ["plugins", "packs", "experimental"]
|
|
31
31
|
},
|
|
32
32
|
"constitution": {
|
|
33
|
-
"description": "
|
|
33
|
+
"description": "7-article machine-enforced safety constitution",
|
|
34
34
|
"path": "app/constitution.md",
|
|
35
35
|
"target": ".claude/constitution.md",
|
|
36
36
|
"type": "symlink",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"tags": ["core", "output-styles"]
|
|
52
52
|
},
|
|
53
53
|
"rules": {
|
|
54
|
-
"description": "
|
|
54
|
+
"description": "Rules synced to Claude Code rule files and exported as a Claude app plugin skill",
|
|
55
55
|
"path": "app/rules",
|
|
56
|
-
"target": "
|
|
57
|
-
"type": "
|
|
56
|
+
"target": "~/.claude/rules + .claude/rules + Claude app plugin skill",
|
|
57
|
+
"type": "sync+plugin",
|
|
58
58
|
"tags": ["core", "rules"]
|
|
59
59
|
}
|
|
60
60
|
},
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"default": true
|
|
91
91
|
},
|
|
92
92
|
"skills": {
|
|
93
|
-
"description": "
|
|
93
|
+
"description": "108 skills (task, hybrid, knowledge)",
|
|
94
94
|
"default": true
|
|
95
95
|
},
|
|
96
96
|
"rules-common": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "AI coding toolkit: 108 skills, 44 agents, 12
|
|
3
|
+
"version": "4.13.0",
|
|
4
|
+
"description": "AI coding toolkit: 108 skills, 44 agents, 12 developer-tool integrations plus Claude Chat/Cowork plugin export, machine-enforced safety constitution, SARIF audit, signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"generate:windsurf-rules": "python3 scripts/generate_windsurf_rules.py .",
|
|
67
67
|
"generate:roo-rules": "python3 scripts/generate_roo_rules.py .",
|
|
68
68
|
"generate:augment-rules": "python3 scripts/generate_augment_rules.py .",
|
|
69
|
-
"generate:
|
|
69
|
+
"generate:claude-app": "python3 scripts/claude_app.py sync",
|
|
70
|
+
"generate:all": "npm run generate:language-rules && npm run generate:claude-app && npm run generate:agents && npm run generate:opencode-agents && npm run generate:opencode-commands && npm run generate:cursor && npm run generate:cursor-mdc && npm run generate:windsurf && npm run generate:windsurf-rules && npm run generate:copilot && npm run generate:gemini && npm run generate:cline && npm run generate:roo && npm run generate:roo-rules && npm run generate:aider && npm run generate:augment-rules && npm run generate:llms"
|
|
70
71
|
},
|
|
71
72
|
"files": [
|
|
72
73
|
"bin/",
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build and verify the uploadable ai-toolkit plugin for the Claude app.
|
|
3
|
+
|
|
4
|
+
Claude Code reads ``~/.claude`` directly. Claude Chat and Cowork do not: they
|
|
5
|
+
load customizations through the Claude app plugin/skill UI. This command turns
|
|
6
|
+
the canonical ``app/`` sources into a self-contained plugin archive that can be
|
|
7
|
+
uploaded from Customize > Plugins.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
claude_app.py sync
|
|
11
|
+
claude_app.py export [--output FILE] [--no-custom-rules] [--verify]
|
|
12
|
+
claude_app.py verify
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import shutil
|
|
19
|
+
import stat
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import zipfile
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TOOLKIT_DIR = Path(__file__).resolve().parent.parent
|
|
28
|
+
APP_DIR = TOOLKIT_DIR / "app"
|
|
29
|
+
CLAUDE_APP_DIR = APP_DIR / "claude-app"
|
|
30
|
+
PLUGIN_HOOKS_PATH = CLAUDE_APP_DIR / "hooks" / "hooks.json"
|
|
31
|
+
PLUGIN_RULES_SKILL_PATH = CLAUDE_APP_DIR / "skills" / "ai-toolkit-rules" / "SKILL.md"
|
|
32
|
+
GLOBAL_INSTRUCTIONS_PATH = CLAUDE_APP_DIR / "global-instructions.md"
|
|
33
|
+
REGISTERED_RULES_DIR = Path.home() / ".softspark" / "ai-toolkit" / "rules"
|
|
34
|
+
|
|
35
|
+
PLUGIN_SCRIPT_FILES = (
|
|
36
|
+
"paths.py",
|
|
37
|
+
"session_state.py",
|
|
38
|
+
"test_cohesion.py",
|
|
39
|
+
"version_check.py",
|
|
40
|
+
)
|
|
41
|
+
RULE_FILES = tuple(sorted((APP_DIR / "rules").glob("*.md"))) + tuple(
|
|
42
|
+
sorted((APP_DIR / "rules" / "common").glob("*.md"))
|
|
43
|
+
)
|
|
44
|
+
SKIP_NAMES = {"__pycache__", ".DS_Store"}
|
|
45
|
+
FIXED_ZIP_TIME = (2026, 1, 1, 0, 0, 0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _write_text(path: Path, content: str) -> None:
|
|
49
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
path.write_text(content.rstrip() + "\n", encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _strip_frontmatter(content: str) -> str:
|
|
54
|
+
lines = content.splitlines()
|
|
55
|
+
if not lines or lines[0].strip() != "---":
|
|
56
|
+
return content.strip()
|
|
57
|
+
for index in range(1, len(lines)):
|
|
58
|
+
if lines[index].strip() == "---":
|
|
59
|
+
return "\n".join(lines[index + 1 :]).strip()
|
|
60
|
+
return content.strip()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def render_plugin_hooks() -> str:
|
|
64
|
+
"""Render plugin-owned hooks from the global-install hook manifest."""
|
|
65
|
+
source = json.loads((APP_DIR / "hooks.json").read_text(encoding="utf-8"))
|
|
66
|
+
hooks = source.get("hooks", {})
|
|
67
|
+
|
|
68
|
+
def adapt(value):
|
|
69
|
+
if isinstance(value, dict):
|
|
70
|
+
return {
|
|
71
|
+
key: adapt(item)
|
|
72
|
+
for key, item in value.items()
|
|
73
|
+
if key != "_source"
|
|
74
|
+
}
|
|
75
|
+
if isinstance(value, list):
|
|
76
|
+
return [adapt(item) for item in value]
|
|
77
|
+
if isinstance(value, str):
|
|
78
|
+
adapted = value.replace(
|
|
79
|
+
"$HOME/.softspark/ai-toolkit/hooks/",
|
|
80
|
+
"${CLAUDE_PLUGIN_ROOT}/hooks/",
|
|
81
|
+
)
|
|
82
|
+
if "${CLAUDE_PLUGIN_ROOT}/hooks/" in adapted:
|
|
83
|
+
adapted = f'AI_TOOLKIT_DIR="${{CLAUDE_PLUGIN_ROOT}}" {adapted}'
|
|
84
|
+
return adapted
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
return json.dumps({"hooks": adapt(hooks)}, indent=2, ensure_ascii=False) + "\n"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_rules_skill(extra_rules: tuple[Path, ...] = ()) -> str:
|
|
91
|
+
"""Render always-relevant engineering rules as a Claude app skill."""
|
|
92
|
+
parts = [
|
|
93
|
+
"---",
|
|
94
|
+
"name: ai-toolkit-rules",
|
|
95
|
+
'description: "Mandatory engineering, security, testing, git, performance, quality, and response rules. Claude MUST load this skill for every technical, coding, debugging, review, architecture, DevOps, data, or file-editing task in Chat or Cowork."',
|
|
96
|
+
"user-invocable: true",
|
|
97
|
+
"---",
|
|
98
|
+
"",
|
|
99
|
+
"# AI Toolkit Rules",
|
|
100
|
+
"",
|
|
101
|
+
"Apply every relevant rule below before acting. Treat MUST/NEVER language as mandatory.",
|
|
102
|
+
]
|
|
103
|
+
for rule_file in (*RULE_FILES, *extra_rules):
|
|
104
|
+
try:
|
|
105
|
+
label = rule_file.relative_to(TOOLKIT_DIR)
|
|
106
|
+
except ValueError:
|
|
107
|
+
label = Path("registered") / rule_file.name
|
|
108
|
+
body = _strip_frontmatter(rule_file.read_text(encoding="utf-8"))
|
|
109
|
+
parts.extend(("", f"## Source: `{label}`", "", body))
|
|
110
|
+
return "\n".join(parts).rstrip() + "\n"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def render_global_instructions() -> str:
|
|
114
|
+
return """# AI Toolkit — Claude app global instructions
|
|
115
|
+
|
|
116
|
+
For every technical, coding, debugging, review, architecture, DevOps, data,
|
|
117
|
+
or file-editing task, load and follow the installed `ai-toolkit-rules` skill
|
|
118
|
+
before acting. Load the relevant language-rule and workflow skills as needed.
|
|
119
|
+
Use ai-toolkit sub-agents for specialist work in Cowork. Before claiming a
|
|
120
|
+
task is complete, run fresh verification and report the evidence.
|
|
121
|
+
|
|
122
|
+
In regular Chat, hooks and sub-agents are unavailable; skills remain the
|
|
123
|
+
authoritative customization surface. In Cowork, also honor the plugin hooks.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def sync_generated() -> None:
|
|
128
|
+
"""Refresh committed Claude app plugin assets from canonical sources."""
|
|
129
|
+
_write_text(PLUGIN_HOOKS_PATH, render_plugin_hooks())
|
|
130
|
+
_write_text(PLUGIN_RULES_SKILL_PATH, render_rules_skill())
|
|
131
|
+
_write_text(GLOBAL_INSTRUCTIONS_PATH, render_global_instructions())
|
|
132
|
+
print("Synced: app/claude-app/hooks/hooks.json")
|
|
133
|
+
print("Synced: app/claude-app/skills/ai-toolkit-rules/SKILL.md")
|
|
134
|
+
print("Synced: app/claude-app/global-instructions.md")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _copy_tree(source: Path, destination: Path) -> None:
|
|
138
|
+
for path in sorted(source.rglob("*")):
|
|
139
|
+
relative = path.relative_to(source)
|
|
140
|
+
if any(part in SKIP_NAMES or part.endswith(".pyc") for part in relative.parts):
|
|
141
|
+
continue
|
|
142
|
+
target = destination / relative
|
|
143
|
+
if path.is_dir():
|
|
144
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
elif path.is_file():
|
|
146
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
shutil.copy2(path, target)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def stage_plugin(destination: Path, include_custom_rules: bool = True) -> int:
|
|
151
|
+
"""Create a clean plugin tree and return the registered-rule count."""
|
|
152
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
(destination / ".claude-plugin").mkdir(parents=True, exist_ok=True)
|
|
154
|
+
shutil.copy2(
|
|
155
|
+
APP_DIR / ".claude-plugin" / "plugin.json",
|
|
156
|
+
destination / ".claude-plugin" / "plugin.json",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
for dirname in ("skills", "hooks", "output-styles"):
|
|
160
|
+
_copy_tree(APP_DIR / dirname, destination / dirname)
|
|
161
|
+
|
|
162
|
+
agents_dir = destination / "agents"
|
|
163
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
for agent in sorted((APP_DIR / "agents").glob("*.md")):
|
|
165
|
+
shutil.copy2(agent, agents_dir / agent.name)
|
|
166
|
+
|
|
167
|
+
app_assets = destination / "claude-app"
|
|
168
|
+
_write_text(app_assets / "hooks" / "hooks.json", render_plugin_hooks())
|
|
169
|
+
_write_text(
|
|
170
|
+
app_assets / "skills" / "ai-toolkit-rules" / "SKILL.md",
|
|
171
|
+
render_rules_skill(),
|
|
172
|
+
)
|
|
173
|
+
_write_text(app_assets / "global-instructions.md", render_global_instructions())
|
|
174
|
+
|
|
175
|
+
scripts_dir = destination / "scripts"
|
|
176
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
for filename in PLUGIN_SCRIPT_FILES:
|
|
178
|
+
shutil.copy2(TOOLKIT_DIR / "scripts" / filename, scripts_dir / filename)
|
|
179
|
+
shutil.copy2(TOOLKIT_DIR / "package.json", destination / "package.json")
|
|
180
|
+
shutil.copy2(TOOLKIT_DIR / "LICENSE", destination / "LICENSE")
|
|
181
|
+
|
|
182
|
+
registered = ()
|
|
183
|
+
if include_custom_rules and REGISTERED_RULES_DIR.is_dir():
|
|
184
|
+
registered = tuple(sorted(REGISTERED_RULES_DIR.glob("*.md")))
|
|
185
|
+
if registered:
|
|
186
|
+
_write_text(
|
|
187
|
+
app_assets / "skills" / "registered-rules" / "SKILL.md",
|
|
188
|
+
render_registered_rules_skill(registered),
|
|
189
|
+
)
|
|
190
|
+
return len(registered)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def render_registered_rules_skill(rules: tuple[Path, ...]) -> str:
|
|
194
|
+
parts = [
|
|
195
|
+
"---",
|
|
196
|
+
"name: registered-rules",
|
|
197
|
+
'description: "User-registered ai-toolkit rules. Claude MUST load this skill for technical work when the installed bundle contains organization or personal standards."',
|
|
198
|
+
"user-invocable: true",
|
|
199
|
+
"---",
|
|
200
|
+
"",
|
|
201
|
+
"# Registered AI Toolkit Rules",
|
|
202
|
+
]
|
|
203
|
+
for rule_file in rules:
|
|
204
|
+
parts.extend(
|
|
205
|
+
(
|
|
206
|
+
"",
|
|
207
|
+
f"## `{rule_file.name}`",
|
|
208
|
+
"",
|
|
209
|
+
_strip_frontmatter(rule_file.read_text(encoding="utf-8")),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
return "\n".join(parts).rstrip() + "\n"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _validate_staged_plugin(plugin_dir: Path) -> list[str]:
|
|
216
|
+
errors: list[str] = []
|
|
217
|
+
manifest_path = plugin_dir / ".claude-plugin" / "plugin.json"
|
|
218
|
+
try:
|
|
219
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
220
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
221
|
+
return [f"invalid plugin manifest: {exc}"]
|
|
222
|
+
|
|
223
|
+
if not isinstance(manifest.get("repository"), str):
|
|
224
|
+
errors.append("plugin manifest repository must be a URL string")
|
|
225
|
+
for field in ("npm", "install", "marketplace", "claude-code"):
|
|
226
|
+
if field in manifest:
|
|
227
|
+
errors.append(f"plugin manifest contains ignored field: {field}")
|
|
228
|
+
|
|
229
|
+
hooks_path = plugin_dir / str(manifest.get("hooks", "")).removeprefix("./")
|
|
230
|
+
skills_path = plugin_dir / str(manifest.get("skills", "")).removeprefix("./")
|
|
231
|
+
if not hooks_path.is_file():
|
|
232
|
+
errors.append(f"plugin hook config missing: {hooks_path.relative_to(plugin_dir)}")
|
|
233
|
+
if not skills_path.is_dir():
|
|
234
|
+
errors.append(f"plugin skill path missing: {skills_path.relative_to(plugin_dir)}")
|
|
235
|
+
|
|
236
|
+
if hooks_path.is_file():
|
|
237
|
+
hooks_text = hooks_path.read_text(encoding="utf-8")
|
|
238
|
+
if "$HOME/.softspark/ai-toolkit/hooks/" in hooks_text:
|
|
239
|
+
errors.append("plugin hooks still reference the Claude Code global install")
|
|
240
|
+
if "${CLAUDE_PLUGIN_ROOT}/hooks/" not in hooks_text:
|
|
241
|
+
errors.append("plugin hooks do not reference CLAUDE_PLUGIN_ROOT")
|
|
242
|
+
try:
|
|
243
|
+
json.loads(hooks_text)
|
|
244
|
+
except json.JSONDecodeError as exc:
|
|
245
|
+
errors.append(f"invalid plugin hooks JSON: {exc}")
|
|
246
|
+
|
|
247
|
+
for agent in (plugin_dir / "agents").glob("*.md"):
|
|
248
|
+
if not agent.read_text(encoding="utf-8").startswith("---\n"):
|
|
249
|
+
errors.append(f"agent lacks frontmatter: {agent.name}")
|
|
250
|
+
return errors
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_claude_validator(plugin_dir: Path) -> bool:
|
|
254
|
+
claude = shutil.which("claude")
|
|
255
|
+
if not claude:
|
|
256
|
+
print("ERROR: claude CLI not found; cannot run official plugin validator", file=sys.stderr)
|
|
257
|
+
return False
|
|
258
|
+
result = subprocess.run(
|
|
259
|
+
[claude, "plugin", "validate", str(plugin_dir), "--strict"]
|
|
260
|
+
)
|
|
261
|
+
return result.returncode == 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def verify_plugin() -> bool:
|
|
265
|
+
with tempfile.TemporaryDirectory(prefix="ai-toolkit-claude-app-") as tmp:
|
|
266
|
+
staged = Path(tmp) / "ai-toolkit"
|
|
267
|
+
stage_plugin(staged, include_custom_rules=False)
|
|
268
|
+
errors = _validate_staged_plugin(staged)
|
|
269
|
+
for error in errors:
|
|
270
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
271
|
+
if errors:
|
|
272
|
+
return False
|
|
273
|
+
if not _run_claude_validator(staged):
|
|
274
|
+
return False
|
|
275
|
+
print("Claude app plugin validation passed")
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _archive_file(zip_file: zipfile.ZipFile, path: Path, arcname: str) -> None:
|
|
280
|
+
data = path.read_bytes()
|
|
281
|
+
mode = path.stat().st_mode
|
|
282
|
+
permissions = 0o755 if mode & stat.S_IXUSR else 0o644
|
|
283
|
+
info = zipfile.ZipInfo(arcname, FIXED_ZIP_TIME)
|
|
284
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
285
|
+
info.external_attr = (stat.S_IFREG | permissions) << 16
|
|
286
|
+
zip_file.writestr(info, data)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def export_plugin(output: Path, include_custom_rules: bool, verify: bool) -> bool:
|
|
290
|
+
output = output.expanduser().resolve()
|
|
291
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
with tempfile.TemporaryDirectory(prefix="ai-toolkit-claude-app-") as tmp:
|
|
293
|
+
staged = Path(tmp) / "ai-toolkit"
|
|
294
|
+
registered_count = stage_plugin(staged, include_custom_rules=include_custom_rules)
|
|
295
|
+
errors = _validate_staged_plugin(staged)
|
|
296
|
+
if errors:
|
|
297
|
+
for error in errors:
|
|
298
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
299
|
+
return False
|
|
300
|
+
if verify and not _run_claude_validator(staged):
|
|
301
|
+
return False
|
|
302
|
+
with zipfile.ZipFile(output, "w") as archive:
|
|
303
|
+
for path in sorted(staged.rglob("*")):
|
|
304
|
+
if path.is_file():
|
|
305
|
+
_archive_file(archive, path, path.relative_to(staged).as_posix())
|
|
306
|
+
|
|
307
|
+
instructions = output.with_name(f"{output.stem}-global-instructions.md")
|
|
308
|
+
_write_text(instructions, render_global_instructions())
|
|
309
|
+
print(f"Created: {output}")
|
|
310
|
+
print(f"Created: {instructions}")
|
|
311
|
+
if registered_count:
|
|
312
|
+
print(f"Included registered rules: {registered_count} (local bundle; review before sharing)")
|
|
313
|
+
print("Next: Claude > Customize > Plugins > + > Upload plugin")
|
|
314
|
+
print("Then paste the instructions file into Settings > Cowork > Global instructions")
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
319
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
320
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
321
|
+
subparsers.add_parser("sync", help="refresh committed Claude app plugin assets")
|
|
322
|
+
subparsers.add_parser("verify", help="run structural and official plugin validation")
|
|
323
|
+
export = subparsers.add_parser("export", help="build an uploadable Claude app plugin ZIP")
|
|
324
|
+
export.add_argument("--output", default="ai-toolkit-claude-app.zip")
|
|
325
|
+
export.add_argument("--no-custom-rules", action="store_true")
|
|
326
|
+
export.add_argument("--verify", action="store_true")
|
|
327
|
+
return parser.parse_args(argv)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def main(argv: list[str] | None = None) -> int:
|
|
331
|
+
args = parse_args(argv)
|
|
332
|
+
if args.action == "sync":
|
|
333
|
+
sync_generated()
|
|
334
|
+
return 0
|
|
335
|
+
if args.action == "verify":
|
|
336
|
+
return 0 if verify_plugin() else 1
|
|
337
|
+
if args.action == "export":
|
|
338
|
+
return 0 if export_plugin(
|
|
339
|
+
Path(args.output),
|
|
340
|
+
include_custom_rules=not args.no_custom_rules,
|
|
341
|
+
verify=args.verify,
|
|
342
|
+
) else 1
|
|
343
|
+
return 2
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
sys.exit(main())
|
package/scripts/doctor.py
CHANGED
|
@@ -79,6 +79,10 @@ EXPECTED_HOOKS = [
|
|
|
79
79
|
|
|
80
80
|
PLANNED_ASSETS = [
|
|
81
81
|
toolkit_dir / "app" / ".claude-plugin" / "plugin.json",
|
|
82
|
+
toolkit_dir / "app" / "claude-app" / "hooks" / "hooks.json",
|
|
83
|
+
toolkit_dir / "app" / "claude-app" / "skills" / "ai-toolkit-rules" / "SKILL.md",
|
|
84
|
+
toolkit_dir / "app" / "claude-app" / "global-instructions.md",
|
|
85
|
+
toolkit_dir / "scripts" / "claude_app.py",
|
|
82
86
|
toolkit_dir / "scripts" / "benchmark_ecosystem.py",
|
|
83
87
|
toolkit_dir / "scripts" / "harvest_ecosystem.py",
|
|
84
88
|
toolkit_dir / "kb" / "reference" / "claude-ecosystem-benchmark-snapshot.md",
|
|
@@ -218,14 +222,91 @@ def _check_symlinks(dr: DiagResult, fix_mode: bool, directory: Path,
|
|
|
218
222
|
dr.fail(f"{label}: no symlinks found")
|
|
219
223
|
|
|
220
224
|
|
|
225
|
+
def _without_source_tags(value: object) -> object:
|
|
226
|
+
"""Return a JSON-compatible hook value without private source markers."""
|
|
227
|
+
if isinstance(value, dict):
|
|
228
|
+
return {
|
|
229
|
+
key: _without_source_tags(item)
|
|
230
|
+
for key, item in value.items()
|
|
231
|
+
if key != "_source"
|
|
232
|
+
}
|
|
233
|
+
if isinstance(value, list):
|
|
234
|
+
return [_without_source_tags(item) for item in value]
|
|
235
|
+
return value
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _hook_signature(entry: dict) -> str:
|
|
239
|
+
"""Build a stable behavior signature for tagged or legacy hook entries."""
|
|
240
|
+
return json.dumps(
|
|
241
|
+
_without_source_tags(entry),
|
|
242
|
+
ensure_ascii=False,
|
|
243
|
+
sort_keys=True,
|
|
244
|
+
separators=(",", ":"),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _has_toolkit_source(entry: dict) -> bool:
|
|
249
|
+
if entry.get("_source") == "ai-toolkit":
|
|
250
|
+
return True
|
|
251
|
+
return any(
|
|
252
|
+
isinstance(handler, dict) and handler.get("_source") == "ai-toolkit"
|
|
253
|
+
for handler in entry.get("hooks", [])
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _installed_toolkit_hook_count(settings: dict) -> tuple[int, int]:
|
|
258
|
+
"""Count canonical hooks even if Claude removed private `_source` tags."""
|
|
259
|
+
source_path = toolkit_dir / "app" / "hooks.json"
|
|
260
|
+
try:
|
|
261
|
+
source_hooks = json.loads(source_path.read_text(encoding="utf-8")).get(
|
|
262
|
+
"hooks", {}
|
|
263
|
+
)
|
|
264
|
+
except (OSError, json.JSONDecodeError):
|
|
265
|
+
return 0, 0
|
|
266
|
+
|
|
267
|
+
expected = sum(
|
|
268
|
+
len(entries) for entries in source_hooks.values() if isinstance(entries, list)
|
|
269
|
+
)
|
|
270
|
+
signatures = {
|
|
271
|
+
event: {
|
|
272
|
+
_hook_signature(entry)
|
|
273
|
+
for entry in entries
|
|
274
|
+
if isinstance(entry, dict)
|
|
275
|
+
}
|
|
276
|
+
for event, entries in source_hooks.items()
|
|
277
|
+
if isinstance(entries, list)
|
|
278
|
+
}
|
|
279
|
+
installed = 0
|
|
280
|
+
for event, entries in settings.get("hooks", {}).items():
|
|
281
|
+
if not isinstance(entries, list):
|
|
282
|
+
continue
|
|
283
|
+
for entry in entries:
|
|
284
|
+
if not isinstance(entry, dict):
|
|
285
|
+
continue
|
|
286
|
+
if _has_toolkit_source(entry) or _hook_signature(entry) in signatures.get(
|
|
287
|
+
event, set()
|
|
288
|
+
):
|
|
289
|
+
installed += 1
|
|
290
|
+
return installed, expected
|
|
291
|
+
|
|
292
|
+
|
|
221
293
|
def _check_settings_json(dr: DiagResult) -> None:
|
|
222
294
|
"""Check settings.json hooks and legacy hooks.json."""
|
|
223
295
|
settings_json = CLAUDE_DIR / "settings.json"
|
|
224
296
|
if settings_json.is_file():
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
dr.
|
|
297
|
+
try:
|
|
298
|
+
settings = json.loads(settings_json.read_text(encoding="utf-8"))
|
|
299
|
+
except (OSError, json.JSONDecodeError):
|
|
300
|
+
dr.fail("settings.json is not valid JSON")
|
|
301
|
+
settings = {}
|
|
302
|
+
hook_count, expected_count = _installed_toolkit_hook_count(settings)
|
|
303
|
+
if expected_count and hook_count == expected_count:
|
|
304
|
+
dr.ok(f"settings.json: {hook_count}/{expected_count} toolkit hook entries")
|
|
305
|
+
elif hook_count:
|
|
306
|
+
dr.warn(
|
|
307
|
+
f"settings.json has {hook_count}/{expected_count} toolkit hook entries "
|
|
308
|
+
"(run: ai-toolkit update --only hooks)"
|
|
309
|
+
)
|
|
229
310
|
else:
|
|
230
311
|
dr.warn("settings.json exists but has no toolkit hooks")
|
|
231
312
|
else:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"description": "Authoritative registry of tools ai-toolkit integrates with. Consumed by scripts/ecosystem_doctor.py to detect upstream doc/version drift.",
|
|
4
|
-
"last_updated": "2026-
|
|
4
|
+
"last_updated": "2026-07-10",
|
|
5
5
|
"tools": [
|
|
6
6
|
{
|
|
7
7
|
"id": "claude-code",
|
|
@@ -70,6 +70,37 @@
|
|
|
70
70
|
"command": "claude --version"
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
"id": "claude-app",
|
|
75
|
+
"display_name": "Claude Chat / Cowork",
|
|
76
|
+
"kind": "app",
|
|
77
|
+
"urls": {
|
|
78
|
+
"docs": "https://support.claude.com/en/articles/13345190-get-started-with-claude-cowork",
|
|
79
|
+
"release_notes": "https://support.claude.com/en/articles/13837440-use-plugins-in-claude",
|
|
80
|
+
"changelog": null
|
|
81
|
+
},
|
|
82
|
+
"config_paths": [
|
|
83
|
+
"UI: Settings > Cowork > Global instructions",
|
|
84
|
+
"UI: Cowork folder instructions",
|
|
85
|
+
"UI: Customize > Skills",
|
|
86
|
+
"UI: Customize > Plugins",
|
|
87
|
+
".claude-plugin/plugin.json",
|
|
88
|
+
"claude-app/hooks/hooks.json",
|
|
89
|
+
"claude-app/skills/*/SKILL.md"
|
|
90
|
+
],
|
|
91
|
+
"our_generators": [
|
|
92
|
+
"scripts/claude_app.py"
|
|
93
|
+
],
|
|
94
|
+
"status_note": "Claude Chat/Desktop/Cowork does not scan ~/.claude/rules, CLAUDE.md, or Claude Code's user settings. ai-toolkit exports an uploadable plugin ZIP plus Cowork global instructions. Skills work in Chat and Cowork; plugin hooks and sub-agents run only in Cowork.",
|
|
95
|
+
"capability_markers": [
|
|
96
|
+
"Global instructions",
|
|
97
|
+
"Folder instructions",
|
|
98
|
+
"plugins",
|
|
99
|
+
"skills",
|
|
100
|
+
"sub-agents"
|
|
101
|
+
],
|
|
102
|
+
"version_probe": null
|
|
103
|
+
},
|
|
73
104
|
{
|
|
74
105
|
"id": "cursor",
|
|
75
106
|
"display_name": "Cursor",
|
|
@@ -134,7 +165,6 @@
|
|
|
134
165
|
"config_paths": [
|
|
135
166
|
".devin/rules/*.md",
|
|
136
167
|
".devin/workflows/*.md",
|
|
137
|
-
".devin/skills/*/SKILL.md",
|
|
138
168
|
".devin/hooks.v1.json",
|
|
139
169
|
".devin/config.json",
|
|
140
170
|
".devin/config.local.json",
|
|
@@ -153,10 +183,9 @@
|
|
|
153
183
|
"scripts/generate_windsurf.py",
|
|
154
184
|
"scripts/generate_windsurf_rules.py",
|
|
155
185
|
"scripts/generate_windsurf_skills.py",
|
|
156
|
-
"scripts/generate_windsurf_hooks.py",
|
|
157
186
|
"scripts/generate_devin_hooks.py"
|
|
158
187
|
],
|
|
159
|
-
"status_note": "Windsurf rebranded to Devin Desktop on 2026-06-02. .devin/ is the primary
|
|
188
|
+
"status_note": "Windsurf rebranded to Devin Desktop on 2026-06-02. .devin/ is the primary rules/workflows/hooks tree; legacy .windsurf rules remain a read fallback. Cascade and its .windsurf/hooks.json format ended on 2026-07-01, so ai-toolkit now emits only .devin/hooks.v1.json. Current Devin skill docs list eight scanned paths (.agents/skills recommended, plus .devin/skills, .github/skills, .claude/skills, .cursor/skills, .codex/skills, .cognition/skills, .windsurf/skills); ai-toolkit emits a single canonical pointer under .windsurf/skills and no longer also writes .devin/skills. Devin CLI also imports ~/.claude/settings.json hooks when read_config_from.claude is enabled.",
|
|
160
189
|
"capability_markers": [
|
|
161
190
|
"Cascade",
|
|
162
191
|
"windsurfrules",
|
|
@@ -469,7 +498,6 @@
|
|
|
469
498
|
],
|
|
470
499
|
"status_note": "Antigravity CLI (GA 2026-05-19) reads workspace skills from .agents/skills/ (plural); the IDE still uses .agent/skills/ (singular) — our generator dual-emits the skill pointer to both. CLI hooks live in .agents/hooks.json with Claude-style event names (PreToolUse, not BeforeTool) and a JSON stdin/stdout decision contract. CLI MCP config is .agents/mcp_config.json with a required serverUrl field (url/httpUrl rejected silently); the GLOBAL MCP config path is reported inconsistently across sources (~/.gemini/antigravity-cli/ vs ~/.gemini/config/), so only the workspace path is registered until verified via agy inspect. Official docs are fully JS-rendered — the doctor is blind to them; verify changes manually each sync.",
|
|
471
500
|
"capability_markers": [
|
|
472
|
-
"Antigravity",
|
|
473
501
|
"agent manager",
|
|
474
502
|
"artifacts",
|
|
475
503
|
"MCP",
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
Devin CLI uses a hook format **compatible with Claude Code hooks**
|
|
5
5
|
(docs.devin.ai/cli/extensibility/hooks/overview). This generator is the
|
|
6
|
-
replacement for the
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
regenerated onto this new file.
|
|
6
|
+
replacement for the removed Cascade ``.windsurf/hooks.json`` surface, which
|
|
7
|
+
stopped working on 2026-07-01. Devin Local and the standalone Devin CLI do not
|
|
8
|
+
read that legacy file as a fallback, so hooks are emitted here.
|
|
10
9
|
|
|
11
10
|
Output file: ``<target>/.devin/hooks.v1.json``. Per the Devin docs the
|
|
12
11
|
standalone ``hooks.v1.json`` file's entire contents ARE the hooks object —
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate a Devin
|
|
2
|
+
"""Generate a Devin/Windsurf skill pointer.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
and ``.
|
|
6
|
-
|
|
7
|
-
global install path).
|
|
4
|
+
Current Devin docs list ``.windsurf/skills/`` as a supported cross-tool skill
|
|
5
|
+
path and do not list ``.devin/skills/``. Pass an explicit ``skill_root`` for a
|
|
6
|
+
single alternate location (used by the legacy HOME-scoped Windsurf install).
|
|
8
7
|
"""
|
|
9
8
|
from __future__ import annotations
|
|
10
9
|
|
|
@@ -14,7 +13,7 @@ from pathlib import Path
|
|
|
14
13
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
14
|
from skill_pointer import POINTER_SKILL_NAME, write_pointer_skill
|
|
16
15
|
|
|
17
|
-
DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".
|
|
16
|
+
DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".windsurf/skills",)
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def generate(target_dir: Path, *, emit_skill_pointer: bool = True,
|