@softspark/ai-toolkit 1.9.0 → 2.0.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +33 -14
  2. package/README.md +18 -18
  3. package/app/ARCHITECTURE.md +3 -3
  4. package/app/hooks/governance-capture.sh +1 -1
  5. package/app/hooks/pre-compact-save.sh +1 -1
  6. package/app/hooks/session-context.sh +1 -1
  7. package/app/hooks/track-usage.sh +2 -2
  8. package/app/hooks.json +20 -20
  9. package/app/plugins/memory-pack/README.md +1 -1
  10. package/app/plugins/memory-pack/hooks/observation-capture.sh +2 -2
  11. package/app/plugins/memory-pack/hooks/session-summary.sh +1 -1
  12. package/app/plugins/memory-pack/scripts/init_db.py +2 -2
  13. package/app/plugins/memory-pack/skills/mem-search/SKILL.md +3 -3
  14. package/app/skills/hook-creator/SKILL.md +2 -2
  15. package/app/skills/mem-search/SKILL.md +3 -3
  16. package/app/skills/repeat/SKILL.md +1 -1
  17. package/bin/ai-toolkit.js +9 -4
  18. package/kb/history/completed/enterprise-config-inheritance-plan-20260412.md +32 -34
  19. package/kb/history/completed/offline-slm-profile-plan-20260411.md +12 -12
  20. package/kb/planning/cloud-security-pack-plan.md +3 -3
  21. package/kb/procedures/maintenance-sop.md +3 -3
  22. package/kb/reference/architecture-overview.md +5 -5
  23. package/kb/reference/benchmark-config.md +1 -1
  24. package/kb/reference/competitive-features-implementation.md +2 -2
  25. package/kb/reference/enterprise-config-guide.md +9 -9
  26. package/kb/reference/extension-api.md +1 -1
  27. package/kb/reference/global-install-model.md +2 -2
  28. package/kb/reference/hooks-catalog.md +30 -30
  29. package/kb/reference/integrations.md +3 -3
  30. package/kb/reference/manifest-install.md +4 -4
  31. package/kb/reference/plugin-pack-conventions.md +5 -5
  32. package/kb/reference/stats.md +3 -3
  33. package/kb/reference/sync.md +3 -3
  34. package/llms-full.txt +118 -899
  35. package/llms.txt +0 -1
  36. package/manifest.json +3 -3
  37. package/package.json +1 -1
  38. package/scripts/add_rule.py +3 -2
  39. package/scripts/benchmark_config.py +3 -1
  40. package/scripts/compile_slm.py +9 -7
  41. package/scripts/config_cli.py +7 -7
  42. package/scripts/config_lock.py +12 -4
  43. package/scripts/config_merger.py +1 -1
  44. package/scripts/config_resolver.py +20 -7
  45. package/scripts/config_scaffold.py +4 -4
  46. package/scripts/config_validator.py +3 -3
  47. package/scripts/dir_rules_shared.py +1 -1
  48. package/scripts/doctor.py +3 -2
  49. package/scripts/install.py +12 -7
  50. package/scripts/install_git_hooks.py +1 -1
  51. package/scripts/install_steps/ai_tools.py +4 -4
  52. package/scripts/install_steps/hooks.py +1 -1
  53. package/scripts/install_steps/install_state.py +8 -6
  54. package/scripts/install_steps/project_registry.py +5 -5
  55. package/scripts/migrate.py +199 -0
  56. package/scripts/paths.py +50 -0
  57. package/scripts/plugin.py +8 -6
  58. package/scripts/remove_rule.py +5 -4
  59. package/scripts/stats.py +3 -2
  60. package/scripts/sync.py +3 -2
  61. package/scripts/update_projects.py +1 -1
  62. package/scripts/version_check.py +5 -2
  63. package/kb/planning/local-dashboard-plan.md +0 -773
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """Migrate ai-toolkit data from ~/.ai-toolkit to ~/.softspark/ai-toolkit.
3
+
4
+ Called automatically by install.py and ai-toolkit.js on first run.
5
+ Can also be invoked directly: python3 scripts/migrate.py [--dry-run]
6
+
7
+ Migration steps:
8
+ 1. Detect legacy ~/.ai-toolkit directory
9
+ 2. Create ~/.softspark/ai-toolkit/
10
+ 3. Move all contents (state, hooks, rules, sessions, etc.)
11
+ 4. Leave ~/.ai-toolkit/.migrated marker with pointer to new location
12
+ 5. Migrate per-project .ai-toolkit.json → .softspark-toolkit.json (via registry)
13
+
14
+ Exit codes:
15
+ 0 Migration succeeded or nothing to migrate
16
+ 1 Migration failed
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import shutil
23
+ import sys
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
28
+ from paths import (
29
+ LEGACY_DATA_DIR,
30
+ LEGACY_PROJECT_CONFIG,
31
+ LEGACY_PROJECT_LOCK,
32
+ PROJECT_CONFIG_FILENAME,
33
+ PROJECT_LOCK_FILENAME,
34
+ SOFTSPARK_DIR,
35
+ TOOLKIT_DATA_DIR,
36
+ )
37
+
38
+
39
+ MIGRATED_MARKER = LEGACY_DATA_DIR / ".migrated"
40
+
41
+
42
+ def needs_migration() -> bool:
43
+ """Check if legacy directory exists and hasn't been migrated yet."""
44
+ if not LEGACY_DATA_DIR.is_dir():
45
+ return False
46
+ if MIGRATED_MARKER.is_file():
47
+ return False
48
+ # Don't migrate if AI_TOOLKIT_HOME is set (custom setup)
49
+ if os.environ.get("AI_TOOLKIT_HOME"):
50
+ return False
51
+ return True
52
+
53
+
54
+ def migrate_home_directory(dry_run: bool = False) -> bool:
55
+ """Migrate ~/.ai-toolkit → ~/.softspark/ai-toolkit.
56
+
57
+ Returns True if migration was performed, False if skipped.
58
+ """
59
+ if not needs_migration():
60
+ return False
61
+
62
+ print()
63
+ print("## Migrating to new directory structure")
64
+ print(f" {LEGACY_DATA_DIR} → {TOOLKIT_DATA_DIR}")
65
+ print()
66
+
67
+ if dry_run:
68
+ print(" (dry-run: no changes made)")
69
+ return False
70
+
71
+ # Create parent ~/.softspark/
72
+ SOFTSPARK_DIR.mkdir(parents=True, exist_ok=True)
73
+
74
+ if TOOLKIT_DATA_DIR.exists():
75
+ # New dir already exists (partial migration?) — merge carefully
76
+ _merge_directories(LEGACY_DATA_DIR, TOOLKIT_DATA_DIR)
77
+ else:
78
+ # Clean move
79
+ shutil.move(str(LEGACY_DATA_DIR), str(TOOLKIT_DATA_DIR))
80
+ # Recreate legacy dir for marker
81
+ LEGACY_DATA_DIR.mkdir(parents=True, exist_ok=True)
82
+
83
+ # Write migration marker
84
+ _write_marker()
85
+
86
+ print(f" Migrated: {TOOLKIT_DATA_DIR}")
87
+ return True
88
+
89
+
90
+ def _merge_directories(src: Path, dst: Path) -> None:
91
+ """Merge src into dst, preferring src files (newer)."""
92
+ for item in src.iterdir():
93
+ if item.name == ".migrated":
94
+ continue
95
+ dest_item = dst / item.name
96
+ if item.is_dir():
97
+ if dest_item.is_dir():
98
+ _merge_directories(item, dest_item)
99
+ else:
100
+ shutil.move(str(item), str(dest_item))
101
+ else:
102
+ # Overwrite with source (legacy has the latest data)
103
+ shutil.move(str(item), str(dest_item))
104
+
105
+
106
+ def _write_marker() -> None:
107
+ """Write .migrated marker pointing to new location."""
108
+ LEGACY_DATA_DIR.mkdir(parents=True, exist_ok=True)
109
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
110
+ marker_data = {
111
+ "migrated_to": str(TOOLKIT_DATA_DIR),
112
+ "migrated_at": now,
113
+ "message": "ai-toolkit data has moved to ~/.softspark/ai-toolkit/. "
114
+ "This directory is kept as a marker. Safe to delete.",
115
+ }
116
+ with open(MIGRATED_MARKER, "w", encoding="utf-8") as f:
117
+ json.dump(marker_data, f, indent=2)
118
+ f.write("\n")
119
+
120
+
121
+ def migrate_project_configs(dry_run: bool = False) -> int:
122
+ """Rename .ai-toolkit.json → .softspark-toolkit.json in registered projects.
123
+
124
+ Returns the number of projects migrated.
125
+ """
126
+ registry_file = TOOLKIT_DATA_DIR / "projects.json"
127
+ if not registry_file.is_file():
128
+ return 0
129
+
130
+ try:
131
+ with open(registry_file, encoding="utf-8") as f:
132
+ data = json.load(f)
133
+ projects = data.get("projects", [])
134
+ except (json.JSONDecodeError, OSError):
135
+ return 0
136
+
137
+ count = 0
138
+ for project in projects:
139
+ project_path = Path(project.get("path", ""))
140
+ if not project_path.is_dir():
141
+ continue
142
+
143
+ # Migrate .ai-toolkit.json → .softspark-toolkit.json
144
+ old_config = project_path / LEGACY_PROJECT_CONFIG
145
+ new_config = project_path / PROJECT_CONFIG_FILENAME
146
+ if old_config.is_file() and not new_config.is_file():
147
+ if dry_run:
148
+ print(f" Would rename: {old_config} → {new_config}")
149
+ else:
150
+ old_config.rename(new_config)
151
+ print(f" Renamed: {old_config.name} → {new_config.name} in {project_path}")
152
+ count += 1
153
+
154
+ # Migrate .ai-toolkit.lock.json → .softspark-toolkit.lock.json
155
+ old_lock = project_path / LEGACY_PROJECT_LOCK
156
+ new_lock = project_path / PROJECT_LOCK_FILENAME
157
+ if old_lock.is_file() and not new_lock.is_file():
158
+ if dry_run:
159
+ print(f" Would rename: {old_lock} → {new_lock}")
160
+ else:
161
+ old_lock.rename(new_lock)
162
+
163
+ return count
164
+
165
+
166
+ def run_full_migration(dry_run: bool = False) -> bool:
167
+ """Run complete migration: home directory + project configs.
168
+
169
+ Returns True if any migration was performed.
170
+ """
171
+ migrated = migrate_home_directory(dry_run=dry_run)
172
+
173
+ if migrated and not dry_run:
174
+ count = migrate_project_configs(dry_run=dry_run)
175
+ if count > 0:
176
+ print(f" Migrated {count} project config(s)")
177
+
178
+ return migrated
179
+
180
+
181
+ def main() -> None:
182
+ dry_run = "--dry-run" in sys.argv
183
+
184
+ if not needs_migration():
185
+ print("Nothing to migrate (no legacy ~/.ai-toolkit found or already migrated).")
186
+ sys.exit(0)
187
+
188
+ success = run_full_migration(dry_run=dry_run)
189
+
190
+ if success:
191
+ print()
192
+ print("Migration complete. The old ~/.ai-toolkit/ directory contains only a")
193
+ print("migration marker and can be safely deleted.")
194
+ elif not dry_run:
195
+ print("Migration skipped.")
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,50 @@
1
+ """Centralized path constants for ai-toolkit.
2
+
3
+ All scripts MUST import paths from here — never hardcode ~/.ai-toolkit or
4
+ ~/.softspark/ai-toolkit directly.
5
+
6
+ Supports override via AI_TOOLKIT_HOME env var for testing and custom setups.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Canonical paths
15
+ # ---------------------------------------------------------------------------
16
+
17
+ SOFTSPARK_DIR = Path(os.environ.get("SOFTSPARK_HOME", Path.home() / ".softspark"))
18
+
19
+ TOOLKIT_DATA_DIR = Path(
20
+ os.environ.get("AI_TOOLKIT_HOME", SOFTSPARK_DIR / "ai-toolkit")
21
+ )
22
+
23
+ # Legacy path (pre-v2.0) — used only by migration detection
24
+ LEGACY_DATA_DIR = Path.home() / ".ai-toolkit"
25
+
26
+ # Sub-directories under TOOLKIT_DATA_DIR
27
+ HOOKS_DIR = TOOLKIT_DATA_DIR / "hooks"
28
+ RULES_DIR = TOOLKIT_DATA_DIR / "rules"
29
+ SESSIONS_DIR = TOOLKIT_DATA_DIR / "sessions"
30
+ COMPACTIONS_DIR = TOOLKIT_DATA_DIR / "compactions"
31
+ CONFIG_CACHE_DIR = TOOLKIT_DATA_DIR / "config-cache"
32
+ COMPILED_DIR = TOOLKIT_DATA_DIR / "compiled"
33
+
34
+ # Files under TOOLKIT_DATA_DIR
35
+ STATE_FILE = TOOLKIT_DATA_DIR / "state.json"
36
+ PROJECTS_FILE = TOOLKIT_DATA_DIR / "projects.json"
37
+ STATS_FILE = TOOLKIT_DATA_DIR / "stats.json"
38
+ GOVERNANCE_LOG = TOOLKIT_DATA_DIR / "governance.log"
39
+ VERSION_CHECK_FILE = TOOLKIT_DATA_DIR / "version-check.json"
40
+
41
+ # Per-project config filenames
42
+ PROJECT_CONFIG_FILENAME = ".softspark-toolkit.json"
43
+ PROJECT_LOCK_FILENAME = ".softspark-toolkit.lock.json"
44
+
45
+ # Legacy per-project config filenames (for migration)
46
+ LEGACY_PROJECT_CONFIG = ".ai-toolkit.json"
47
+ LEGACY_PROJECT_LOCK = ".ai-toolkit.lock.json"
48
+
49
+ # Base config filename (published in npm packages — unchanged)
50
+ BASE_CONFIG_FILENAME = "ai-toolkit.config.json"
package/scripts/plugin.py CHANGED
@@ -35,10 +35,12 @@ from _common import toolkit_dir, app_dir
35
35
  from plugin_schema import resolve_hook_event, validate_manifest, validate_references
36
36
 
37
37
 
38
+ from paths import TOOLKIT_DATA_DIR, HOOKS_DIR as _HOOKS_DIR
39
+
38
40
  PLUGINS_DIR = app_dir / "plugins"
39
41
  CLAUDE_DIR = Path.home() / ".claude"
40
- HOOKS_DIR = Path.home() / ".ai-toolkit" / "hooks"
41
- PLUGINS_STATE_FILE = Path.home() / ".ai-toolkit" / "plugins.json"
42
+ HOOKS_DIR = _HOOKS_DIR
43
+ PLUGINS_STATE_FILE = TOOLKIT_DATA_DIR / "plugins.json"
42
44
 
43
45
 
44
46
  # ---------------------------------------------------------------------------
@@ -156,7 +158,7 @@ def _copy_scripts(name: str, pack_dir: Path, installed_items: list[str]) -> None
156
158
  plugin_scripts_dir = pack_dir / "scripts"
157
159
  if not plugin_scripts_dir.is_dir():
158
160
  return
159
- scripts_dest = Path.home() / ".ai-toolkit" / "plugin-scripts" / name
161
+ scripts_dest = TOOLKIT_DATA_DIR / "plugin-scripts" / name
160
162
  scripts_dest.mkdir(parents=True, exist_ok=True)
161
163
  for script_file in sorted(plugin_scripts_dir.iterdir()):
162
164
  if script_file.name.startswith("__"):
@@ -282,7 +284,7 @@ def remove_pack(name: str) -> bool:
282
284
 
283
285
  print(f" Removing: {name}")
284
286
 
285
- # 1. Remove plugin hooks from ~/.ai-toolkit/hooks/
287
+ # 1. Remove plugin hooks from ~/.softspark/ai-toolkit/hooks/
286
288
  removed = 0
287
289
  for hook in HOOKS_DIR.glob(f"plugin-{name}-*.sh"):
288
290
  hook.unlink()
@@ -290,7 +292,7 @@ def remove_pack(name: str) -> bool:
290
292
  removed += 1
291
293
 
292
294
  # 2. Remove plugin scripts
293
- scripts_dir = Path.home() / ".ai-toolkit" / "plugin-scripts" / name
295
+ scripts_dir = TOOLKIT_DATA_DIR / "plugin-scripts" / name
294
296
  if scripts_dir.is_dir():
295
297
  shutil.rmtree(scripts_dir)
296
298
  print(f" Removed scripts: {scripts_dir}")
@@ -358,7 +360,7 @@ def update_pack(name: str) -> bool:
358
360
  # Clean
359
361
  # ---------------------------------------------------------------------------
360
362
 
361
- MEMORY_DB = Path.home() / ".ai-toolkit" / "memory.db"
363
+ MEMORY_DB = TOOLKIT_DATA_DIR / "memory.db"
362
364
 
363
365
  # Map plugin names to their clean logic
364
366
  CLEANABLE_PLUGINS = {"memory-pack"}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """remove-rule -- Unregister a rule (opposite of add-rule).
3
3
 
4
- Removes the rule file from ~/.ai-toolkit/rules/ (so it is no longer
4
+ Removes the rule file from ~/.softspark/ai-toolkit/rules/ (so it is no longer
5
5
  re-applied on future 'ai-toolkit install' runs) AND strips its injected
6
6
  block from the target CLAUDE.md.
7
7
 
@@ -29,11 +29,12 @@ def main() -> None:
29
29
 
30
30
  rule_name = sys.argv[1]
31
31
  target_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path.home()
32
- rules_dir = Path.home() / ".ai-toolkit" / "rules"
32
+ from paths import RULES_DIR
33
+ rules_dir = RULES_DIR
33
34
 
34
35
  removed = 0
35
36
 
36
- # 1. Unregister from ~/.ai-toolkit/rules/
37
+ # 1. Unregister from ~/.softspark/ai-toolkit/rules/
37
38
  rule_file = rules_dir / f"{rule_name}.md"
38
39
  if rule_file.is_file():
39
40
  rule_file.unlink()
@@ -51,7 +52,7 @@ def main() -> None:
51
52
  if removed == 0:
52
53
  print()
53
54
  print("Nothing to unregister. To list registered rules:")
54
- print(" ls ~/.ai-toolkit/rules/")
55
+ print(f" ls {rules_dir}/")
55
56
 
56
57
 
57
58
  if __name__ == "__main__":
package/scripts/stats.py CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """ai-toolkit stats -- Show skill usage statistics.
3
3
 
4
- Reads ~/.ai-toolkit/stats.json (populated by track-usage.sh hook)
4
+ Reads ~/.softspark/ai-toolkit/stats.json (populated by track-usage.sh hook)
5
5
  and displays a sorted table of skill invocations.
6
6
 
7
7
  Options:
@@ -15,8 +15,9 @@ import sys
15
15
  from pathlib import Path
16
16
 
17
17
  sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ from paths import STATS_FILE as _STATS_FILE
18
19
 
19
- STATS_FILE = Path.home() / ".ai-toolkit" / "stats.json"
20
+ STATS_FILE = _STATS_FILE
20
21
 
21
22
 
22
23
  def main() -> None:
package/scripts/sync.py CHANGED
@@ -21,9 +21,10 @@ from pathlib import Path
21
21
 
22
22
  sys.path.insert(0, str(Path(__file__).resolve().parent))
23
23
  from _common import toolkit_dir
24
+ from paths import TOOLKIT_DATA_DIR, RULES_DIR as _RULES_DIR
24
25
 
25
- CONFIG_DIR = Path.home() / ".ai-toolkit"
26
- RULES_DIR = CONFIG_DIR / "rules"
26
+ CONFIG_DIR = TOOLKIT_DATA_DIR
27
+ RULES_DIR = _RULES_DIR
27
28
  GIST_ID_FILE = CONFIG_DIR / ".gist-id"
28
29
 
29
30
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Update all registered projects in parallel.
3
3
 
4
- Reads ~/.ai-toolkit/projects.json and runs install.py --local in each
4
+ Reads ~/.softspark/ai-toolkit/projects.json and runs install.py --local in each
5
5
  project directory concurrently using a thread pool.
6
6
 
7
7
  Stdlib-only — no external dependencies.
@@ -21,7 +21,10 @@ import sys
21
21
  import time
22
22
  from pathlib import Path
23
23
 
24
- CACHE_FILE = Path.home() / ".ai-toolkit" / "version-check.json"
24
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
25
+ from paths import VERSION_CHECK_FILE, STATE_FILE
26
+
27
+ CACHE_FILE = VERSION_CHECK_FILE
25
28
  CACHE_TTL = 86400 # 24 hours
26
29
  PACKAGE_NAME = "@softspark/ai-toolkit"
27
30
 
@@ -34,7 +37,7 @@ def _get_installed_version() -> str:
34
37
  installed/updated, while package.json may be newer if the npm package
35
38
  was upgraded but `ai-toolkit update` was not run yet.
36
39
  """
37
- state_file = Path.home() / ".ai-toolkit" / "state.json"
40
+ state_file = STATE_FILE
38
41
  if state_file.is_file():
39
42
  try:
40
43
  with open(state_file, encoding="utf-8") as f: