@softspark/ai-toolkit 1.5.0 → 1.6.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/llms.txt CHANGED
@@ -14,7 +14,12 @@
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
16
  - [How-To Guides](kb/howto/README.md)
17
+ - [Plan: Cloud Security Pack — Multi-Cloud Audit](kb/planning/cloud-security-pack-plan.md)
18
+ - [Plan: Enterprise Config Inheritance — Multi-Repo Governance with `extends`](kb/planning/enterprise-config-inheritance-plan.md)
19
+ - [Plan: Local Dashboard — `ai-toolkit ui`](kb/planning/local-dashboard-plan.md)
20
+ - [Plan: Offline-First SLM Profile — Lightweight Mode for Local Models](kb/planning/offline-slm-profile-plan.md)
17
21
  - [SOP: Claude Toolkit Maintenance](kb/procedures/maintenance-sop.md)
22
+ - [SOP: Release Preparation](kb/procedures/release-preparation-sop.md)
18
23
  - [SOP: Release Verification](kb/procedures/release-verification-sop.md)
19
24
  - [Agents Catalog](kb/reference/agents-catalog.md)
20
25
  - [Anti-Pattern Registry Format](kb/reference/anti-pattern-registry-format.md)
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.5.0",
2
+ "version": "1.6.0",
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": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Professional-grade AI coding toolkit: 92 skills, 44 agents, multi-platform support (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo Code, Aider, Augment, Google Antigravity), machine-enforced safety constitution, persona presets, skill security auditor, expanded lifecycle hooks, 11 plugin packs, and benchmark tooling.",
5
5
  "keywords": [
6
6
  "claude",
@@ -439,3 +439,99 @@ def write_rules(target_dir: Path, rules: dict[str, callable],
439
439
  (out_dir / filename).write_text(content_fn(), encoding="utf-8")
440
440
  tag = f" ({label})" if label else ""
441
441
  print(f" Generated: {subdir}/{filename}{tag}")
442
+
443
+
444
+ # ---------------------------------------------------------------------------
445
+ # Language → glob patterns for editors with file-type activation
446
+ # ---------------------------------------------------------------------------
447
+
448
+ LANG_GLOBS: dict[str, list[str]] = {
449
+ "python": ["**/*.py"],
450
+ "typescript": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
451
+ "golang": ["**/*.go"],
452
+ "rust": ["**/*.rs"],
453
+ "java": ["**/*.java"],
454
+ "kotlin": ["**/*.kt", "**/*.kts"],
455
+ "swift": ["**/*.swift"],
456
+ "dart": ["**/*.dart"],
457
+ "csharp": ["**/*.cs"],
458
+ "php": ["**/*.php"],
459
+ "cpp": ["**/*.cpp", "**/*.cc", "**/*.cxx", "**/*.hpp", "**/*.h"],
460
+ "ruby": ["**/*.rb"],
461
+ }
462
+
463
+
464
+ def _resolve_app_rules_dir() -> Path:
465
+ """Resolve path to app/rules/ directory."""
466
+ return Path(__file__).resolve().parent.parent / "app" / "rules"
467
+
468
+
469
+ def build_language_rules(
470
+ language_modules: list[str] | None,
471
+ ) -> dict[str, callable]:
472
+ """Build language-specific rule entries from app/rules/<lang>/.
473
+
474
+ Returns dict of filename -> callable (same shape as STANDARD_RULES).
475
+ Language modules use format ``rules-python``, ``rules-typescript``, etc.
476
+ Always includes ``common`` when any language is detected.
477
+ """
478
+ if not language_modules:
479
+ return {}
480
+
481
+ rules_src = _resolve_app_rules_dir()
482
+ if not rules_src.is_dir():
483
+ return {}
484
+
485
+ langs: list[str] = []
486
+ for mod in language_modules:
487
+ if mod.startswith("rules-"):
488
+ langs.append(mod[6:])
489
+ if not langs:
490
+ return {}
491
+
492
+ all_dirs: list[str] = ["common"]
493
+ for lang in langs:
494
+ if lang not in all_dirs:
495
+ all_dirs.append(lang)
496
+
497
+ result: dict[str, callable] = {}
498
+ for lang in all_dirs:
499
+ lang_path = rules_src / lang
500
+ if not lang_path.is_dir():
501
+ continue
502
+ parts: list[str] = []
503
+ for f in sorted(lang_path.glob("*.md")):
504
+ content = f.read_text(encoding="utf-8")
505
+ # Strip YAML frontmatter if present
506
+ if content.startswith("---"):
507
+ end = content.find("---", 3)
508
+ if end != -1:
509
+ content = content[end + 3:].lstrip("\n")
510
+ parts.append(content.rstrip("\n"))
511
+
512
+ if parts:
513
+ combined = "\n\n".join(parts) + "\n"
514
+ filename = f"{PREFIX}lang-{lang}.md"
515
+ # Capture value via default arg to avoid late-binding closure
516
+ result[filename] = (lambda c: lambda: c)(combined)
517
+
518
+ return result
519
+
520
+
521
+ def build_registered_rules(
522
+ rules_dir: Path | None,
523
+ ) -> dict[str, callable]:
524
+ """Build entries from user's registered rules (~/.ai-toolkit/rules/*.md).
525
+
526
+ Returns dict of filename -> callable (same shape as STANDARD_RULES).
527
+ """
528
+ if not rules_dir or not rules_dir.is_dir():
529
+ return {}
530
+
531
+ result: dict[str, callable] = {}
532
+ for rule_file in sorted(rules_dir.glob("*.md")):
533
+ content = rule_file.read_text(encoding="utf-8")
534
+ filename = f"{PREFIX}custom-{rule_file.stem}.md"
535
+ result[filename] = (lambda c: lambda: c)(content)
536
+
537
+ return result
@@ -18,12 +18,23 @@ import sys
18
18
  from pathlib import Path
19
19
 
20
20
  sys.path.insert(0, str(Path(__file__).resolve().parent))
21
- from dir_rules_shared import STANDARD_RULES, STANDARD_WORKFLOWS, write_rules
22
-
23
-
24
- def generate(target_dir: Path) -> None:
21
+ from dir_rules_shared import (
22
+ STANDARD_RULES,
23
+ STANDARD_WORKFLOWS,
24
+ build_language_rules,
25
+ build_registered_rules,
26
+ write_rules,
27
+ )
28
+
29
+
30
+ def generate(target_dir: Path, *,
31
+ language_modules: list[str] | None = None,
32
+ rules_dir: Path | None = None) -> None:
25
33
  """Write .agent/rules/ and .agent/workflows/ files to target_dir."""
26
- write_rules(target_dir, STANDARD_RULES, ".agent/rules")
34
+ rules = dict(STANDARD_RULES)
35
+ rules.update(build_language_rules(language_modules))
36
+ rules.update(build_registered_rules(rules_dir))
37
+ write_rules(target_dir, rules, ".agent/rules")
27
38
  write_rules(target_dir, STANDARD_WORKFLOWS, ".agent/workflows")
28
39
 
29
40
 
@@ -18,7 +18,10 @@ from pathlib import Path
18
18
 
19
19
  sys.path.insert(0, str(Path(__file__).resolve().parent))
20
20
  from dir_rules_shared import (
21
+ LANG_GLOBS,
21
22
  PREFIX,
23
+ build_language_rules,
24
+ build_registered_rules,
22
25
  cleanup_stale,
23
26
  rule_agents_and_skills,
24
27
  rule_code_style,
@@ -87,14 +90,45 @@ def _make_rules() -> dict[str, callable]:
87
90
  RULES = _make_rules()
88
91
 
89
92
 
90
- def generate(target_dir: Path) -> None:
93
+ def generate(target_dir: Path, *,
94
+ language_modules: list[str] | None = None,
95
+ rules_dir: Path | None = None) -> None:
91
96
  """Write .augment/rules/ai-toolkit-*.md files to target_dir."""
92
- rules_dir = target_dir / ".augment" / "rules"
93
- rules_dir.mkdir(parents=True, exist_ok=True)
94
- cleanup_stale(rules_dir, set(RULES.keys()) | {"ai-toolkit.md"})
95
-
96
- for filename, content_fn in RULES.items():
97
- (rules_dir / filename).write_text(content_fn(), encoding="utf-8")
97
+ out_dir = target_dir / ".augment" / "rules"
98
+ out_dir.mkdir(parents=True, exist_ok=True)
99
+
100
+ all_rules: dict[str, callable] = dict(RULES)
101
+
102
+ # Add language rules with Augment frontmatter
103
+ for filename, content_fn in build_language_rules(language_modules).items():
104
+ lang = filename.removeprefix(f"{PREFIX}lang-").removesuffix(".md")
105
+ globs = LANG_GLOBS.get(lang)
106
+ if globs:
107
+ all_rules[filename] = (lambda fn, l, g: lambda: _augment_wrap(
108
+ fn(),
109
+ description=f"{l.title()} language rules",
110
+ rule_type="auto_attached",
111
+ globs=g,
112
+ ))(content_fn, lang, globs)
113
+ else:
114
+ # common — always apply
115
+ all_rules[filename] = (lambda fn, l: lambda: _augment_wrap(
116
+ fn(),
117
+ description=f"{l.title()} language rules",
118
+ ))(content_fn, lang)
119
+
120
+ # Add registered rules with Augment frontmatter
121
+ for filename, content_fn in build_registered_rules(rules_dir).items():
122
+ name = filename.removeprefix(f"{PREFIX}custom-").removesuffix(".md")
123
+ all_rules[filename] = (lambda fn, n: lambda: _augment_wrap(
124
+ fn(),
125
+ description=f"Custom rule: {n}",
126
+ ))(content_fn, name)
127
+
128
+ cleanup_stale(out_dir, set(all_rules.keys()) | {"ai-toolkit.md"})
129
+
130
+ for filename, content_fn in all_rules.items():
131
+ (out_dir / filename).write_text(content_fn(), encoding="utf-8")
98
132
  print(f" Generated: .augment/rules/{filename}")
99
133
 
100
134
 
@@ -14,12 +14,22 @@ import sys
14
14
  from pathlib import Path
15
15
 
16
16
  sys.path.insert(0, str(Path(__file__).resolve().parent))
17
- from dir_rules_shared import STANDARD_RULES, write_rules
17
+ from dir_rules_shared import (
18
+ STANDARD_RULES,
19
+ build_language_rules,
20
+ build_registered_rules,
21
+ write_rules,
22
+ )
18
23
 
19
24
 
20
- def generate(target_dir: Path) -> None:
25
+ def generate(target_dir: Path, *,
26
+ language_modules: list[str] | None = None,
27
+ rules_dir: Path | None = None) -> None:
21
28
  """Write .cline/rules/*.md files to target_dir."""
22
- write_rules(target_dir, STANDARD_RULES, ".cline/rules")
29
+ rules = dict(STANDARD_RULES)
30
+ rules.update(build_language_rules(language_modules))
31
+ rules.update(build_registered_rules(rules_dir))
32
+ write_rules(target_dir, rules, ".cline/rules")
23
33
 
24
34
 
25
35
  def main() -> None:
@@ -20,7 +20,10 @@ from pathlib import Path
20
20
 
21
21
  sys.path.insert(0, str(Path(__file__).resolve().parent))
22
22
  from dir_rules_shared import (
23
+ LANG_GLOBS,
23
24
  PREFIX,
25
+ build_language_rules,
26
+ build_registered_rules,
24
27
  cleanup_stale,
25
28
  rule_agents_and_skills,
26
29
  rule_code_style,
@@ -100,21 +103,47 @@ RULES = _make_rules()
100
103
  # Main
101
104
  # ---------------------------------------------------------------------------
102
105
 
103
- def generate(target_dir: Path) -> None:
106
+ def generate(target_dir: Path, *,
107
+ language_modules: list[str] | None = None,
108
+ rules_dir: Path | None = None) -> None:
104
109
  """Write .cursor/rules/*.mdc files to target_dir."""
105
- rules_dir = target_dir / ".cursor" / "rules"
106
- rules_dir.mkdir(parents=True, exist_ok=True)
110
+ out_dir = target_dir / ".cursor" / "rules"
111
+ out_dir.mkdir(parents=True, exist_ok=True)
112
+
113
+ all_rules: dict[str, callable] = dict(RULES)
114
+
115
+ # Add language rules wrapped in .mdc frontmatter
116
+ for filename, content_fn in build_language_rules(language_modules).items():
117
+ lang = filename.removeprefix(f"{PREFIX}lang-").removesuffix(".md")
118
+ globs = LANG_GLOBS.get(lang)
119
+ mdc_name = filename.replace(".md", ".mdc")
120
+ all_rules[mdc_name] = (lambda fn, l, g: lambda: _mdc(
121
+ fn(),
122
+ description=f"{l.title()} language rules",
123
+ globs=g if g else None,
124
+ always_apply=not g,
125
+ ))(content_fn, lang, globs)
126
+
127
+ # Add registered rules wrapped in .mdc frontmatter
128
+ for filename, content_fn in build_registered_rules(rules_dir).items():
129
+ name = filename.removeprefix(f"{PREFIX}custom-").removesuffix(".md")
130
+ mdc_name = filename.replace(".md", ".mdc")
131
+ all_rules[mdc_name] = (lambda fn, n: lambda: _mdc(
132
+ fn(),
133
+ description=f"Custom rule: {n}",
134
+ always_apply=True,
135
+ ))(content_fn, name)
107
136
 
108
137
  # Clean stale ai-toolkit-*.mdc files
109
- current = set(RULES.keys())
110
- if rules_dir.is_dir():
111
- for f in rules_dir.iterdir():
138
+ current = set(all_rules.keys())
139
+ if out_dir.is_dir():
140
+ for f in out_dir.iterdir():
112
141
  if f.name.startswith(PREFIX) and f.suffix == ".mdc" and f.name not in current:
113
142
  f.unlink()
114
143
  print(f" Removed stale: .cursor/rules/{f.name}")
115
144
 
116
- for filename, content_fn in RULES.items():
117
- (rules_dir / filename).write_text(content_fn(), encoding="utf-8")
145
+ for filename, content_fn in all_rules.items():
146
+ (out_dir / filename).write_text(content_fn(), encoding="utf-8")
118
147
  print(f" Generated: .cursor/rules/{filename}")
119
148
 
120
149
 
@@ -13,12 +13,22 @@ import sys
13
13
  from pathlib import Path
14
14
 
15
15
  sys.path.insert(0, str(Path(__file__).resolve().parent))
16
- from dir_rules_shared import STANDARD_RULES, write_rules
16
+ from dir_rules_shared import (
17
+ STANDARD_RULES,
18
+ build_language_rules,
19
+ build_registered_rules,
20
+ write_rules,
21
+ )
17
22
 
18
23
 
19
- def generate(target_dir: Path) -> None:
24
+ def generate(target_dir: Path, *,
25
+ language_modules: list[str] | None = None,
26
+ rules_dir: Path | None = None) -> None:
20
27
  """Write .roo/rules/*.md files to target_dir."""
21
- write_rules(target_dir, STANDARD_RULES, ".roo/rules")
28
+ rules = dict(STANDARD_RULES)
29
+ rules.update(build_language_rules(language_modules))
30
+ rules.update(build_registered_rules(rules_dir))
31
+ write_rules(target_dir, rules, ".roo/rules")
22
32
 
23
33
 
24
34
  def main() -> None:
@@ -14,12 +14,22 @@ import sys
14
14
  from pathlib import Path
15
15
 
16
16
  sys.path.insert(0, str(Path(__file__).resolve().parent))
17
- from dir_rules_shared import STANDARD_RULES, write_rules
17
+ from dir_rules_shared import (
18
+ STANDARD_RULES,
19
+ build_language_rules,
20
+ build_registered_rules,
21
+ write_rules,
22
+ )
18
23
 
19
24
 
20
- def generate(target_dir: Path) -> None:
25
+ def generate(target_dir: Path, *,
26
+ language_modules: list[str] | None = None,
27
+ rules_dir: Path | None = None) -> None:
21
28
  """Write .windsurf/rules/*.md files to target_dir."""
22
- write_rules(target_dir, STANDARD_RULES, ".windsurf/rules")
29
+ rules = dict(STANDARD_RULES)
30
+ rules.update(build_language_rules(language_modules))
31
+ rules.update(build_registered_rules(rules_dir))
32
+ write_rules(target_dir, rules, ".windsurf/rules")
23
33
 
24
34
 
25
35
  def main() -> None:
@@ -228,7 +228,8 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
228
228
  _inject_language_rules(cwd, language_modules)
229
229
 
230
230
  # Install editor configs only for resolved editors
231
- _create_local_ai_tool_configs(cwd, rules_dir, resolved_editors)
231
+ _create_local_ai_tool_configs(cwd, rules_dir, resolved_editors,
232
+ language_modules=language_modules)
232
233
 
233
234
 
234
235
  def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
@@ -396,7 +397,8 @@ def _create_local_settings(cwd: Path, reset: bool) -> None:
396
397
 
397
398
 
398
399
  def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
399
- editors: list[str]) -> None:
400
+ editors: list[str],
401
+ language_modules: list[str] | None = None) -> None:
400
402
  eds = set(editors)
401
403
 
402
404
  if "copilot" in eds:
@@ -413,7 +415,8 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
413
415
  rules_dir,
414
416
  )
415
417
  from generate_cursor_mdc import generate as gen_cursor_mdc
416
- gen_cursor_mdc(cwd)
418
+ gen_cursor_mdc(cwd, language_modules=language_modules,
419
+ rules_dir=rules_dir)
417
420
 
418
421
  if "windsurf" in eds:
419
422
  inject_with_rules(
@@ -422,7 +425,8 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
422
425
  rules_dir,
423
426
  )
424
427
  from generate_windsurf_rules import generate as gen_windsurf_rules
425
- gen_windsurf_rules(cwd)
428
+ gen_windsurf_rules(cwd, language_modules=language_modules,
429
+ rules_dir=rules_dir)
426
430
 
427
431
  if "cline" in eds:
428
432
  inject_with_rules(
@@ -431,14 +435,16 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
431
435
  rules_dir,
432
436
  )
433
437
  from generate_cline_rules import generate as gen_cline_rules
434
- gen_cline_rules(cwd)
438
+ gen_cline_rules(cwd, language_modules=language_modules,
439
+ rules_dir=rules_dir)
435
440
 
436
441
  if "roo" in eds:
437
442
  roo_output = run_script("generate-roo-modes.sh", capture=True)
438
443
  (cwd / ".roomodes").write_text(roo_output, encoding="utf-8")
439
444
  print(" Created: .roomodes")
440
445
  from generate_roo_rules import generate as gen_roo_rules
441
- gen_roo_rules(cwd)
446
+ gen_roo_rules(cwd, language_modules=language_modules,
447
+ rules_dir=rules_dir)
442
448
 
443
449
  if "aider" in eds:
444
450
  aider_output = run_script("generate-aider-conf.sh", capture=True)
@@ -448,10 +454,12 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
448
454
 
449
455
  if "augment" in eds:
450
456
  from generate_augment_rules import generate as gen_augment_rules
451
- gen_augment_rules(cwd)
457
+ gen_augment_rules(cwd, language_modules=language_modules,
458
+ rules_dir=rules_dir)
452
459
 
453
460
  if "antigravity" in eds:
454
461
  from generate_antigravity import generate as gen_antigravity
455
- gen_antigravity(cwd)
462
+ gen_antigravity(cwd, language_modules=language_modules,
463
+ rules_dir=rules_dir)
456
464
 
457
465
  run_script("install-git-hooks.sh", str(cwd))
@@ -46,6 +46,7 @@ VALID_HOOK_EVENTS = frozenset({
46
46
 
47
47
  VALID_KB_CATEGORIES = frozenset({
48
48
  "reference", "howto", "procedures", "troubleshooting", "best-practices",
49
+ "planning",
49
50
  })
50
51
 
51
52
  PLANNED_ASSETS = [