@softspark/ai-toolkit 1.2.0 → 1.3.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 +5 -29
- package/CHANGELOG.md +39 -0
- package/README.md +170 -27
- package/app/.claude-plugin/plugin.json +2 -2
- package/app/ARCHITECTURE.md +35 -19
- package/app/agents/ai-engineer.md +47 -5
- package/app/agents/fact-checker.md +1 -1
- package/app/agents/llm-ops-engineer.md +2 -2
- package/app/agents/{mcp-server-architect.md → mcp-specialist.md} +124 -18
- package/app/agents/mcp-testing-engineer.md +2 -2
- package/app/agents/nlp-engineer.md +1 -1
- package/app/agents/orchestrator.md +3 -4
- package/app/agents/project-planner.md +1 -1
- package/app/agents/technical-researcher.md +71 -21
- package/app/hooks/commit-quality.sh +53 -0
- package/app/hooks/governance-capture.sh +77 -0
- package/app/hooks/guard-config.sh +46 -0
- package/app/hooks/mcp-health.sh +53 -0
- package/app/hooks/pre-compact-save.sh +34 -0
- package/app/hooks/session-context.sh +60 -0
- package/app/hooks.json +60 -0
- package/app/mcp-templates/README.md +105 -0
- package/app/mcp-templates/brave-search.json +13 -0
- package/app/mcp-templates/cloudflare.json +14 -0
- package/app/mcp-templates/context7.json +10 -0
- package/app/mcp-templates/custom-template.json +13 -0
- package/app/mcp-templates/datadog.json +15 -0
- package/app/mcp-templates/docker.json +10 -0
- package/app/mcp-templates/fetch.json +10 -0
- package/app/mcp-templates/filesystem.json +10 -0
- package/app/mcp-templates/git.json +10 -0
- package/app/mcp-templates/github.json +13 -0
- package/app/mcp-templates/google-drive.json +15 -0
- package/app/mcp-templates/google-maps.json +13 -0
- package/app/mcp-templates/grafana.json +14 -0
- package/app/mcp-templates/linear.json +13 -0
- package/app/mcp-templates/memory.json +10 -0
- package/app/mcp-templates/notion.json +13 -0
- package/app/mcp-templates/postgres.json +10 -0
- package/app/mcp-templates/puppeteer.json +10 -0
- package/app/mcp-templates/redis.json +13 -0
- package/app/mcp-templates/sentry.json +14 -0
- package/app/mcp-templates/sequential-thinking.json +10 -0
- package/app/mcp-templates/slack.json +14 -0
- package/app/mcp-templates/sqlite.json +10 -0
- package/app/mcp-templates/supabase.json +14 -0
- package/app/mcp-templates/vercel.json +13 -0
- package/app/plugins/research-pack/README.md +1 -1
- package/app/plugins/research-pack/plugin.json +1 -1
- package/app/rules/claude-toolkit-rules.md +1 -1
- package/app/rules/common/coding-style.md +52 -0
- package/app/rules/common/git-workflow.md +45 -0
- package/app/rules/common/performance.md +49 -0
- package/app/rules/common/security.md +59 -0
- package/app/rules/common/testing.md +53 -0
- package/app/rules/cpp/coding-style.md +55 -0
- package/app/rules/cpp/frameworks.md +55 -0
- package/app/rules/cpp/patterns.md +57 -0
- package/app/rules/cpp/security.md +61 -0
- package/app/rules/cpp/testing.md +58 -0
- package/app/rules/csharp/coding-style.md +54 -0
- package/app/rules/csharp/frameworks.md +60 -0
- package/app/rules/csharp/patterns.md +61 -0
- package/app/rules/csharp/security.md +60 -0
- package/app/rules/csharp/testing.md +58 -0
- package/app/rules/dart/coding-style.md +61 -0
- package/app/rules/dart/frameworks.md +59 -0
- package/app/rules/dart/patterns.md +62 -0
- package/app/rules/dart/security.md +60 -0
- package/app/rules/dart/testing.md +60 -0
- package/app/rules/golang/coding-style.md +50 -0
- package/app/rules/golang/frameworks.md +56 -0
- package/app/rules/golang/patterns.md +52 -0
- package/app/rules/golang/security.md +61 -0
- package/app/rules/golang/testing.md +54 -0
- package/app/rules/java/coding-style.md +56 -0
- package/app/rules/java/frameworks.md +54 -0
- package/app/rules/java/patterns.md +59 -0
- package/app/rules/java/security.md +60 -0
- package/app/rules/java/testing.md +55 -0
- package/app/rules/kotlin/coding-style.md +56 -0
- package/app/rules/kotlin/frameworks.md +54 -0
- package/app/rules/kotlin/patterns.md +54 -0
- package/app/rules/kotlin/security.md +62 -0
- package/app/rules/kotlin/testing.md +56 -0
- package/app/rules/php/coding-style.md +53 -0
- package/app/rules/php/frameworks.md +57 -0
- package/app/rules/php/patterns.md +63 -0
- package/app/rules/php/security.md +61 -0
- package/app/rules/php/testing.md +60 -0
- package/app/rules/python/coding-style.md +52 -0
- package/app/rules/python/frameworks.md +53 -0
- package/app/rules/python/patterns.md +53 -0
- package/app/rules/python/security.md +58 -0
- package/app/rules/python/testing.md +52 -0
- package/app/rules/ruby/coding-style.md +56 -0
- package/app/rules/ruby/frameworks.md +56 -0
- package/app/rules/ruby/patterns.md +61 -0
- package/app/rules/ruby/security.md +61 -0
- package/app/rules/ruby/testing.md +63 -0
- package/app/rules/rust/coding-style.md +52 -0
- package/app/rules/rust/frameworks.md +59 -0
- package/app/rules/rust/patterns.md +58 -0
- package/app/rules/rust/security.md +64 -0
- package/app/rules/rust/testing.md +54 -0
- package/app/rules/swift/coding-style.md +55 -0
- package/app/rules/swift/frameworks.md +63 -0
- package/app/rules/swift/patterns.md +63 -0
- package/app/rules/swift/security.md +63 -0
- package/app/rules/swift/testing.md +60 -0
- package/app/rules/typescript/coding-style.md +51 -0
- package/app/rules/typescript/frameworks.md +51 -0
- package/app/rules/typescript/patterns.md +51 -0
- package/app/rules/typescript/security.md +55 -0
- package/app/rules/typescript/testing.md +52 -0
- package/app/skills/app-builder/SKILL.md +1 -1
- package/app/skills/brand-voice/SKILL.md +92 -0
- package/app/skills/council/SKILL.md +133 -0
- package/app/skills/introspect/SKILL.md +125 -0
- package/bin/ai-toolkit.js +103 -2
- package/kb/reference/agents-catalog.md +10 -13
- package/kb/reference/architecture-overview.md +29 -6
- package/kb/reference/commands-catalog.md +4 -4
- package/kb/reference/competitive-features-implementation.md +677 -0
- package/kb/reference/extension-api.md +175 -0
- package/kb/reference/hooks-catalog.md +90 -14
- package/kb/reference/language-rules.md +167 -0
- package/kb/reference/manifest-install.md +165 -0
- package/kb/reference/mcp-templates.md +110 -0
- package/kb/reference/skills-catalog.md +9 -6
- package/llms-full.txt +1511 -59
- package/llms.txt +8 -3
- package/package.json +2 -2
- package/scripts/inject_hook_cli.py +330 -0
- package/scripts/install.py +241 -7
- package/scripts/install_steps/ai_tools.py +63 -1
- package/scripts/install_steps/detect_language.py +50 -0
- package/scripts/install_steps/install_state.py +117 -0
- package/scripts/mcp_manager.py +206 -0
- package/app/agents/mcp-expert.md +0 -228
- package/app/agents/rag-engineer.md +0 -201
- package/app/agents/research-synthesizer.md +0 -138
package/scripts/install.py
CHANGED
|
@@ -30,12 +30,13 @@ Options:
|
|
|
30
30
|
--reset Wipe and recreate local configs
|
|
31
31
|
--profile <p> minimal|standard|strict
|
|
32
32
|
--persona <p> backend-lead|frontend-lead|devops-eng|junior-dev
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
--modules <list> Install specific modules (comma-separated)
|
|
34
|
+
--auto-detect Detect project languages and install matching rules
|
|
35
|
+
--status Show installed modules and exit
|
|
36
36
|
"""
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
+
import json
|
|
39
40
|
import sys
|
|
40
41
|
from pathlib import Path
|
|
41
42
|
|
|
@@ -48,6 +49,134 @@ from install_steps.symlinks import install_agents, install_skills, clean_legacy_
|
|
|
48
49
|
from install_steps.hooks import install_hooks
|
|
49
50
|
from install_steps.markers import install_marker_files, inject_rules
|
|
50
51
|
from install_steps.ai_tools import install_ai_tools, install_local_project, run_script
|
|
52
|
+
from install_steps.install_state import (
|
|
53
|
+
load_state,
|
|
54
|
+
record_install,
|
|
55
|
+
get_installed_modules,
|
|
56
|
+
get_installed_profile,
|
|
57
|
+
print_status,
|
|
58
|
+
)
|
|
59
|
+
from install_steps.detect_language import detect_languages
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Manifest helpers
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def _load_manifest() -> dict:
|
|
67
|
+
"""Load manifest.json from toolkit root."""
|
|
68
|
+
manifest_path = toolkit_dir / "manifest.json"
|
|
69
|
+
if not manifest_path.is_file():
|
|
70
|
+
return {}
|
|
71
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
72
|
+
return json.load(f)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_manifest_profiles() -> dict[str, list[str]]:
|
|
76
|
+
"""Return profiles from manifest.json."""
|
|
77
|
+
manifest = _load_manifest()
|
|
78
|
+
return manifest.get("profiles", {})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_manifest_modules() -> dict[str, dict]:
|
|
82
|
+
"""Return modules from manifest.json."""
|
|
83
|
+
manifest = _load_manifest()
|
|
84
|
+
return manifest.get("modules", {})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve_modules_from_profile(profile: str) -> list[str]:
|
|
88
|
+
"""Resolve a profile name to a list of module names.
|
|
89
|
+
|
|
90
|
+
Returns an empty list if the profile is not found in manifest.
|
|
91
|
+
"""
|
|
92
|
+
profiles = _get_manifest_profiles()
|
|
93
|
+
return profiles.get(profile, [])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_requested_modules(
|
|
97
|
+
modules_arg: str,
|
|
98
|
+
profile: str,
|
|
99
|
+
auto_detect: bool,
|
|
100
|
+
project_dir: Path,
|
|
101
|
+
) -> list[str] | None:
|
|
102
|
+
"""Determine which modules to install based on CLI flags.
|
|
103
|
+
|
|
104
|
+
Only activates when ``--modules`` or ``--auto-detect`` is passed.
|
|
105
|
+
A bare ``--profile`` without those flags returns None so the
|
|
106
|
+
legacy code path handles it unchanged.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
A sorted list of module names to install, or None if no
|
|
110
|
+
module-level flags were provided (legacy mode).
|
|
111
|
+
"""
|
|
112
|
+
# If neither --modules nor --auto-detect was given, stay in legacy mode.
|
|
113
|
+
# This ensures --profile alone behaves exactly as before.
|
|
114
|
+
if not modules_arg and not auto_detect:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
manifest_modules = _get_manifest_modules()
|
|
118
|
+
result: set[str] = set()
|
|
119
|
+
|
|
120
|
+
# Always include required modules
|
|
121
|
+
for name, cfg in manifest_modules.items():
|
|
122
|
+
if cfg.get("required"):
|
|
123
|
+
result.add(name)
|
|
124
|
+
|
|
125
|
+
# --modules flag takes precedence
|
|
126
|
+
if modules_arg:
|
|
127
|
+
for name in modules_arg.split(","):
|
|
128
|
+
name = name.strip()
|
|
129
|
+
if name and name in manifest_modules:
|
|
130
|
+
result.add(name)
|
|
131
|
+
elif name:
|
|
132
|
+
print(f"Warning: unknown module '{name}' (skipped)")
|
|
133
|
+
if auto_detect:
|
|
134
|
+
detected = detect_languages(project_dir, toolkit_dir)
|
|
135
|
+
result.update(detected)
|
|
136
|
+
return sorted(result)
|
|
137
|
+
|
|
138
|
+
# --auto-detect (without --modules): add profile or defaults + detected
|
|
139
|
+
if auto_detect:
|
|
140
|
+
if profile:
|
|
141
|
+
profile_modules = resolve_modules_from_profile(profile)
|
|
142
|
+
if profile_modules:
|
|
143
|
+
result.update(profile_modules)
|
|
144
|
+
else:
|
|
145
|
+
for name, cfg in manifest_modules.items():
|
|
146
|
+
if cfg.get("default"):
|
|
147
|
+
result.add(name)
|
|
148
|
+
detected = detect_languages(project_dir, toolkit_dir)
|
|
149
|
+
result.update(detected)
|
|
150
|
+
return sorted(result)
|
|
151
|
+
|
|
152
|
+
# Should not reach here, but return None for safety
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def modules_to_component_filter(modules: list[str]) -> str:
|
|
157
|
+
"""Convert module names to a comma-separated component filter string.
|
|
158
|
+
|
|
159
|
+
Maps module names back to the component names used by the existing
|
|
160
|
+
``--only`` system (e.g. "agents", "skills", "hooks", "rules").
|
|
161
|
+
This bridges the new module system with the old component system.
|
|
162
|
+
"""
|
|
163
|
+
components: set[str] = set()
|
|
164
|
+
|
|
165
|
+
for module in modules:
|
|
166
|
+
if module == "core":
|
|
167
|
+
# Core maps to hooks, constitution, architecture, output-styles
|
|
168
|
+
components.update(["hooks", "constitution", "architecture", "rules"])
|
|
169
|
+
elif module == "agents":
|
|
170
|
+
components.add("agents")
|
|
171
|
+
elif module == "skills":
|
|
172
|
+
components.add("skills")
|
|
173
|
+
elif module.startswith("rules-"):
|
|
174
|
+
components.add("rules")
|
|
175
|
+
elif module == "mcp-templates":
|
|
176
|
+
# MCP templates don't map to a legacy component
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
return ",".join(sorted(components))
|
|
51
180
|
|
|
52
181
|
|
|
53
182
|
# ---------------------------------------------------------------------------
|
|
@@ -65,6 +194,9 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
65
194
|
"reset": False,
|
|
66
195
|
"profile": "",
|
|
67
196
|
"persona": "",
|
|
197
|
+
"modules": "",
|
|
198
|
+
"auto_detect": False,
|
|
199
|
+
"status": False,
|
|
68
200
|
}
|
|
69
201
|
i = 0
|
|
70
202
|
while i < len(argv):
|
|
@@ -75,6 +207,10 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
75
207
|
cfg["local"] = True
|
|
76
208
|
elif arg == "--reset":
|
|
77
209
|
cfg["reset"] = True
|
|
210
|
+
elif arg == "--auto-detect":
|
|
211
|
+
cfg["auto_detect"] = True
|
|
212
|
+
elif arg == "--status":
|
|
213
|
+
cfg["status"] = True
|
|
78
214
|
elif arg.startswith("--only="):
|
|
79
215
|
cfg["only"] = arg.split("=", 1)[1]
|
|
80
216
|
elif arg == "--only":
|
|
@@ -95,6 +231,11 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
95
231
|
elif arg == "--persona":
|
|
96
232
|
i += 1
|
|
97
233
|
cfg["persona"] = argv[i] if i < len(argv) else ""
|
|
234
|
+
elif arg.startswith("--modules="):
|
|
235
|
+
cfg["modules"] = arg.split("=", 1)[1]
|
|
236
|
+
elif arg == "--modules":
|
|
237
|
+
i += 1
|
|
238
|
+
cfg["modules"] = argv[i] if i < len(argv) else ""
|
|
98
239
|
elif arg.startswith("-"):
|
|
99
240
|
print(f"Unknown option: {arg}")
|
|
100
241
|
sys.exit(1)
|
|
@@ -139,7 +280,8 @@ def resolve_profile(profile: str, only: str) -> str:
|
|
|
139
280
|
|
|
140
281
|
|
|
141
282
|
def print_banner(target_dir: Path, rules_dir: Path, profile: str,
|
|
142
|
-
only: str, skip: str, dry_run: bool
|
|
283
|
+
only: str, skip: str, dry_run: bool,
|
|
284
|
+
modules: list[str] | None = None) -> None:
|
|
143
285
|
print("AI Toolkit Installer")
|
|
144
286
|
print("========================")
|
|
145
287
|
print(f"Toolkit: {toolkit_dir}")
|
|
@@ -147,6 +289,8 @@ def print_banner(target_dir: Path, rules_dir: Path, profile: str,
|
|
|
147
289
|
print(f"Rules: {rules_dir}")
|
|
148
290
|
if profile:
|
|
149
291
|
print(f"Profile: {profile}")
|
|
292
|
+
if modules is not None:
|
|
293
|
+
print(f"Modules: {', '.join(modules)}")
|
|
150
294
|
if only:
|
|
151
295
|
print(f"Only: {only}")
|
|
152
296
|
if skip:
|
|
@@ -165,6 +309,7 @@ def print_summary() -> None:
|
|
|
165
309
|
print(" 2. Per project: ai-toolkit install --local (or: ai-toolkit update --local)")
|
|
166
310
|
print(" 3. To update: npm install -g @softspark/ai-toolkit@latest && ai-toolkit update")
|
|
167
311
|
print(" 4. To register rules from other tools: ai-toolkit add-rule <rule.md>")
|
|
312
|
+
print(" 5. Check install state: ai-toolkit status")
|
|
168
313
|
|
|
169
314
|
|
|
170
315
|
# ---------------------------------------------------------------------------
|
|
@@ -224,6 +369,20 @@ def install_strict_git_hooks(profile: str, local: bool, dry_run: bool) -> None:
|
|
|
224
369
|
print(f" Strict profile: git hooks installed in {cwd}")
|
|
225
370
|
|
|
226
371
|
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
# Version helper
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
def _get_toolkit_version() -> str:
|
|
377
|
+
"""Read version from package.json."""
|
|
378
|
+
pkg = toolkit_dir / "package.json"
|
|
379
|
+
if pkg.is_file():
|
|
380
|
+
with open(pkg, encoding="utf-8") as f:
|
|
381
|
+
data = json.load(f)
|
|
382
|
+
return data.get("version", "unknown")
|
|
383
|
+
return "unknown"
|
|
384
|
+
|
|
385
|
+
|
|
227
386
|
# ---------------------------------------------------------------------------
|
|
228
387
|
# Main
|
|
229
388
|
# ---------------------------------------------------------------------------
|
|
@@ -231,6 +390,11 @@ def install_strict_git_hooks(profile: str, local: bool, dry_run: bool) -> None:
|
|
|
231
390
|
def main() -> None:
|
|
232
391
|
cfg = parse_args(sys.argv[1:])
|
|
233
392
|
|
|
393
|
+
# Handle --status early exit
|
|
394
|
+
if cfg["status"]:
|
|
395
|
+
print_status()
|
|
396
|
+
sys.exit(0)
|
|
397
|
+
|
|
234
398
|
target_dir: Path = cfg["target_dir"]
|
|
235
399
|
only: str = cfg["only"]
|
|
236
400
|
skip: str = cfg["skip"]
|
|
@@ -239,13 +403,35 @@ def main() -> None:
|
|
|
239
403
|
reset: bool = cfg["reset"]
|
|
240
404
|
profile: str = cfg["profile"]
|
|
241
405
|
persona: str = cfg["persona"]
|
|
406
|
+
modules_arg: str = cfg["modules"]
|
|
407
|
+
auto_detect: bool = cfg["auto_detect"]
|
|
242
408
|
|
|
243
409
|
rules_dir = Path.home() / ".ai-toolkit" / "rules"
|
|
244
410
|
hooks_scripts_dir = Path.home() / ".ai-toolkit" / "hooks"
|
|
245
411
|
|
|
246
|
-
|
|
412
|
+
# Resolve modules if module-level flags are present
|
|
413
|
+
# --auto-detect only makes sense with --local (language rules are project-specific)
|
|
414
|
+
if auto_detect and not local:
|
|
415
|
+
print("Warning: --auto-detect requires --local (language rules are project-specific). Adding --local.")
|
|
416
|
+
local = True
|
|
417
|
+
project_dir = Path.cwd() if local else target_dir
|
|
418
|
+
resolved_modules = resolve_requested_modules(
|
|
419
|
+
modules_arg, profile, auto_detect, project_dir,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# If module-level flags produced a module list, bridge to legacy --only
|
|
423
|
+
if resolved_modules is not None:
|
|
424
|
+
component_filter = modules_to_component_filter(resolved_modules)
|
|
425
|
+
if component_filter and not only:
|
|
426
|
+
only = component_filter
|
|
427
|
+
|
|
428
|
+
# Legacy profile resolution (only when no module-level override)
|
|
429
|
+
if resolved_modules is None:
|
|
430
|
+
only = resolve_profile(profile, only)
|
|
431
|
+
|
|
247
432
|
check_dependencies()
|
|
248
|
-
print_banner(target_dir, rules_dir, profile, only, skip, dry_run
|
|
433
|
+
print_banner(target_dir, rules_dir, profile, only, skip, dry_run,
|
|
434
|
+
modules=resolved_modules)
|
|
249
435
|
|
|
250
436
|
if not dry_run:
|
|
251
437
|
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -255,12 +441,60 @@ def main() -> None:
|
|
|
255
441
|
install_ai_tools(target_dir, rules_dir, only, skip, dry_run)
|
|
256
442
|
|
|
257
443
|
if local:
|
|
258
|
-
|
|
444
|
+
# Pass language modules for --auto-detect / --modules rules-*
|
|
445
|
+
lang_modules = [m for m in (resolved_modules or []) if m.startswith("rules-")]
|
|
446
|
+
install_local_project(rules_dir, dry_run, reset, lang_modules or None)
|
|
259
447
|
|
|
260
448
|
install_persona(target_dir, persona, dry_run)
|
|
261
449
|
install_strict_git_hooks(profile, local, dry_run)
|
|
450
|
+
|
|
451
|
+
# Record install state (skip for dry-run)
|
|
452
|
+
if not dry_run:
|
|
453
|
+
auto_detected = None
|
|
454
|
+
if auto_detect:
|
|
455
|
+
auto_detected = detect_languages(project_dir, toolkit_dir)
|
|
456
|
+
|
|
457
|
+
# Determine what modules to record
|
|
458
|
+
if resolved_modules is not None:
|
|
459
|
+
record_modules = resolved_modules
|
|
460
|
+
else:
|
|
461
|
+
# Legacy mode: infer modules from profile/only
|
|
462
|
+
record_modules = _infer_modules_from_legacy(profile, only)
|
|
463
|
+
|
|
464
|
+
record_install(
|
|
465
|
+
version=_get_toolkit_version(),
|
|
466
|
+
modules=record_modules,
|
|
467
|
+
profile=profile or "standard",
|
|
468
|
+
auto_detected=auto_detected,
|
|
469
|
+
)
|
|
470
|
+
|
|
262
471
|
print_summary()
|
|
263
472
|
|
|
264
473
|
|
|
474
|
+
def _infer_modules_from_legacy(profile: str, only: str) -> list[str]:
|
|
475
|
+
"""Best-effort inference of module names from legacy flags.
|
|
476
|
+
|
|
477
|
+
Used when the user did not pass --modules or a manifest-aware
|
|
478
|
+
--profile, so we approximate from what was actually installed.
|
|
479
|
+
"""
|
|
480
|
+
modules: set[str] = {"core"} # core is always installed
|
|
481
|
+
|
|
482
|
+
if only:
|
|
483
|
+
components = {c.strip() for c in only.split(",")}
|
|
484
|
+
else:
|
|
485
|
+
# Standard install: all main components
|
|
486
|
+
components = {"agents", "skills", "hooks", "constitution",
|
|
487
|
+
"architecture", "rules"}
|
|
488
|
+
|
|
489
|
+
if "agents" in components:
|
|
490
|
+
modules.add("agents")
|
|
491
|
+
if "skills" in components:
|
|
492
|
+
modules.add("skills")
|
|
493
|
+
if "rules" in components:
|
|
494
|
+
modules.add("rules-common")
|
|
495
|
+
|
|
496
|
+
return sorted(modules)
|
|
497
|
+
|
|
498
|
+
|
|
265
499
|
if __name__ == "__main__":
|
|
266
500
|
main()
|
|
@@ -120,7 +120,8 @@ def run_script(script_name: str, *args: str, capture: bool = False) -> str:
|
|
|
120
120
|
return result.stdout if capture else ""
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def install_local_project(rules_dir: Path, dry_run: bool, reset: bool
|
|
123
|
+
def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
124
|
+
language_modules: list[str] | None = None) -> None:
|
|
124
125
|
"""Install project-local configs."""
|
|
125
126
|
cwd = Path.cwd()
|
|
126
127
|
print()
|
|
@@ -131,6 +132,8 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool) -> None:
|
|
|
131
132
|
|
|
132
133
|
if dry_run:
|
|
133
134
|
_install_local_dry_run(reset)
|
|
135
|
+
if language_modules:
|
|
136
|
+
print(f" Would inject language rules: {', '.join(language_modules)}")
|
|
134
137
|
return
|
|
135
138
|
|
|
136
139
|
(cwd / ".claude").mkdir(parents=True, exist_ok=True)
|
|
@@ -156,9 +159,68 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool) -> None:
|
|
|
156
159
|
)
|
|
157
160
|
print(" Injected: .claude/constitution.md")
|
|
158
161
|
|
|
162
|
+
# Inject language-specific rules into project CLAUDE.md
|
|
163
|
+
_inject_language_rules(cwd, language_modules)
|
|
164
|
+
|
|
159
165
|
_create_local_ai_tool_configs(cwd, rules_dir)
|
|
160
166
|
|
|
161
167
|
|
|
168
|
+
def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
|
|
169
|
+
"""Inject language-specific rules into project's CLAUDE.md.
|
|
170
|
+
|
|
171
|
+
Concatenates all .md files from matching app/rules/<lang>/ directories
|
|
172
|
+
into a single marker-injected section in the project's .claude/CLAUDE.md.
|
|
173
|
+
Also always includes common rules.
|
|
174
|
+
"""
|
|
175
|
+
if not language_modules:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
rules_src = app_dir / "rules"
|
|
179
|
+
if not rules_src.is_dir():
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# Always include common + detected languages
|
|
183
|
+
lang_dirs: list[str] = ["common"]
|
|
184
|
+
for mod in language_modules:
|
|
185
|
+
# module name format: "rules-typescript" → dir "typescript"
|
|
186
|
+
if mod.startswith("rules-"):
|
|
187
|
+
lang_dirs.append(mod[6:])
|
|
188
|
+
|
|
189
|
+
# Collect all rule content
|
|
190
|
+
parts: list[str] = []
|
|
191
|
+
for lang_dir in lang_dirs:
|
|
192
|
+
lang_path = rules_src / lang_dir
|
|
193
|
+
if not lang_path.is_dir():
|
|
194
|
+
continue
|
|
195
|
+
for rule_file in sorted(lang_path.glob("*.md")):
|
|
196
|
+
content = rule_file.read_text(encoding="utf-8").strip()
|
|
197
|
+
# Strip YAML frontmatter
|
|
198
|
+
if content.startswith("---"):
|
|
199
|
+
end = content.find("---", 3)
|
|
200
|
+
if end != -1:
|
|
201
|
+
content = content[end + 3:].strip()
|
|
202
|
+
if content:
|
|
203
|
+
parts.append(content)
|
|
204
|
+
|
|
205
|
+
if not parts:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Write concatenated rules to a temp file, then inject as a section
|
|
209
|
+
import tempfile
|
|
210
|
+
combined = "\n\n".join(parts)
|
|
211
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False,
|
|
212
|
+
encoding="utf-8") as tmp:
|
|
213
|
+
tmp.write(combined)
|
|
214
|
+
tmp_path = Path(tmp.name)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
inject_section(tmp_path, cwd / ".claude" / "CLAUDE.md", "language-rules")
|
|
218
|
+
detected = ", ".join(d for d in lang_dirs if d != "common")
|
|
219
|
+
print(f" Injected: language rules (common + {detected})")
|
|
220
|
+
finally:
|
|
221
|
+
tmp_path.unlink(missing_ok=True)
|
|
222
|
+
|
|
223
|
+
|
|
162
224
|
def _install_local_dry_run(reset: bool) -> None:
|
|
163
225
|
if reset:
|
|
164
226
|
print(" Would remove: CLAUDE.md, .claude/settings.local.json")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Detect project language from file markers in the manifest."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_manifest_modules(toolkit_dir: Path) -> dict:
|
|
9
|
+
"""Load the modules section from manifest.json."""
|
|
10
|
+
manifest_path = toolkit_dir / "manifest.json"
|
|
11
|
+
if not manifest_path.is_file():
|
|
12
|
+
return {}
|
|
13
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
14
|
+
data = json.load(f)
|
|
15
|
+
return data.get("modules", {})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _glob_matches(project_dir: Path, pattern: str) -> bool:
|
|
19
|
+
"""Check if a glob pattern matches any file in the project directory.
|
|
20
|
+
|
|
21
|
+
Only checks the top-level directory (no recursive descent)
|
|
22
|
+
to keep detection fast and predictable.
|
|
23
|
+
"""
|
|
24
|
+
return any(project_dir.glob(pattern))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_languages(project_dir: Path, toolkit_dir: Path) -> list[str]:
|
|
28
|
+
"""Return list of detected language module names.
|
|
29
|
+
|
|
30
|
+
Scans ``project_dir`` for marker files defined in each module's
|
|
31
|
+
``auto_detect`` list inside ``manifest.json``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Sorted list of matching module names, e.g.
|
|
35
|
+
``["rules-python", "rules-typescript"]``.
|
|
36
|
+
"""
|
|
37
|
+
modules = _load_manifest_modules(toolkit_dir)
|
|
38
|
+
detected: list[str] = []
|
|
39
|
+
|
|
40
|
+
for module_name, module_cfg in modules.items():
|
|
41
|
+
markers = module_cfg.get("auto_detect")
|
|
42
|
+
if not markers:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
for pattern in markers:
|
|
46
|
+
if _glob_matches(project_dir, pattern):
|
|
47
|
+
detected.append(module_name)
|
|
48
|
+
break # one match is enough for this module
|
|
49
|
+
|
|
50
|
+
return sorted(detected)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Track installed modules and versions in ~/.ai-toolkit/state.json."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
STATE_FILENAME = "state.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _state_path() -> Path:
|
|
12
|
+
"""Return the canonical path to state.json."""
|
|
13
|
+
return Path.home() / ".ai-toolkit" / STATE_FILENAME
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_state() -> dict:
|
|
17
|
+
"""Load state from ~/.ai-toolkit/state.json.
|
|
18
|
+
|
|
19
|
+
Returns an empty dict if the file does not exist or is malformed.
|
|
20
|
+
"""
|
|
21
|
+
path = _state_path()
|
|
22
|
+
if not path.is_file():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
with open(path, encoding="utf-8") as f:
|
|
26
|
+
data = json.load(f)
|
|
27
|
+
if not isinstance(data, dict):
|
|
28
|
+
return {}
|
|
29
|
+
return data
|
|
30
|
+
except (json.JSONDecodeError, OSError):
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_state(state: dict) -> None:
|
|
35
|
+
"""Save state to ~/.ai-toolkit/state.json.
|
|
36
|
+
|
|
37
|
+
Creates the parent directory if it does not exist.
|
|
38
|
+
"""
|
|
39
|
+
path = _state_path()
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
42
|
+
json.dump(state, f, indent=2)
|
|
43
|
+
f.write("\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_installed_modules() -> list[str]:
|
|
47
|
+
"""Return list of installed module names from state, or empty list."""
|
|
48
|
+
state = load_state()
|
|
49
|
+
modules = state.get("installed_modules", [])
|
|
50
|
+
if isinstance(modules, list):
|
|
51
|
+
return modules
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_installed_profile() -> str:
|
|
56
|
+
"""Return the profile name from state, or empty string."""
|
|
57
|
+
state = load_state()
|
|
58
|
+
return state.get("profile", "")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _now_iso() -> str:
|
|
62
|
+
"""Return current UTC time in ISO 8601 format."""
|
|
63
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def record_install(
|
|
67
|
+
version: str,
|
|
68
|
+
modules: list[str],
|
|
69
|
+
profile: str,
|
|
70
|
+
auto_detected: list[str] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Record a successful install in state.json.
|
|
73
|
+
|
|
74
|
+
If state already exists, preserves ``installed_at`` and updates
|
|
75
|
+
``last_updated``. Otherwise sets both timestamps.
|
|
76
|
+
"""
|
|
77
|
+
state = load_state()
|
|
78
|
+
now = _now_iso()
|
|
79
|
+
|
|
80
|
+
state["installed_version"] = version
|
|
81
|
+
if "installed_at" not in state:
|
|
82
|
+
state["installed_at"] = now
|
|
83
|
+
state["last_updated"] = now
|
|
84
|
+
state["installed_modules"] = sorted(set(modules))
|
|
85
|
+
state["profile"] = profile
|
|
86
|
+
if auto_detected is not None:
|
|
87
|
+
state["auto_detected_languages"] = sorted(auto_detected)
|
|
88
|
+
|
|
89
|
+
save_state(state)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def print_status() -> None:
|
|
93
|
+
"""Print a human-readable summary of the install state."""
|
|
94
|
+
state = load_state()
|
|
95
|
+
if not state:
|
|
96
|
+
print("No install state found (~/.ai-toolkit/state.json missing).")
|
|
97
|
+
print("Run 'ai-toolkit install' to set up the toolkit.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
print("AI Toolkit Install Status")
|
|
101
|
+
print("=========================")
|
|
102
|
+
print(f" Version: {state.get('installed_version', 'unknown')}")
|
|
103
|
+
print(f" Installed: {state.get('installed_at', 'unknown')}")
|
|
104
|
+
print(f" Updated: {state.get('last_updated', 'unknown')}")
|
|
105
|
+
print(f" Profile: {state.get('profile', 'unknown')}")
|
|
106
|
+
|
|
107
|
+
modules = state.get("installed_modules", [])
|
|
108
|
+
if modules:
|
|
109
|
+
print(f" Modules: {', '.join(modules)}")
|
|
110
|
+
else:
|
|
111
|
+
print(" Modules: (none recorded)")
|
|
112
|
+
|
|
113
|
+
detected = state.get("auto_detected_languages", [])
|
|
114
|
+
if detected:
|
|
115
|
+
# Strip "rules-" prefix for readability
|
|
116
|
+
langs = [m.replace("rules-", "") for m in detected]
|
|
117
|
+
print(f" Detected: {', '.join(langs)}")
|