@softspark/ai-toolkit 4.29.2 → 4.30.2
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 +82 -0
- package/README.md +44 -18
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/ARCHITECTURE.md +2 -2
- package/app/mcp-templates/README.md +7 -2
- package/app/mcp-templates/rag-mcp-legal.json +11 -0
- package/app/mcp-templates/rag-mcp.json +11 -0
- package/app/surface.json +1 -0
- package/benchmarks/ecosystem-doctor-snapshot.json +29 -17
- package/bin/ai-toolkit.js +8 -0
- package/kb/history/completed/dsh-integration-plan-superseded.md +322 -0
- package/kb/history/completed/dsh-native-install-target-plan.md +331 -0
- package/kb/procedures/ecosystem-sync-sop.md +7 -5
- package/kb/procedures/maintenance-sop.md +1 -1
- package/kb/procedures/release-verification-sop.md +35 -5
- package/kb/reference/architecture-overview.md +24 -5
- package/kb/reference/cli-reference.md +1 -1
- package/kb/reference/dsh-compatibility.md +183 -0
- package/kb/reference/manifest-install.md +112 -5
- package/kb/reference/mcp-templates.md +11 -4
- package/kb/reference/plugin-pack-conventions.md +35 -18
- package/kb/reference/supported-tools-registry.md +30 -6
- package/llms-full.txt +1110 -50
- package/llms.txt +3 -0
- package/manifest.json +2 -2
- package/package.json +2 -2
- package/scripts/codex_skill_adapter.py +673 -34
- package/scripts/config_resolver.py +80 -14
- package/scripts/doctor.py +98 -20
- package/scripts/ecosystem_tools.json +51 -1
- package/scripts/generate_codex_skills.py +22 -20
- package/scripts/install.py +30 -13
- package/scripts/install_steps/ai_tools.py +97 -33
- package/scripts/install_steps/dsh.py +5063 -0
- package/scripts/install_steps/install_state.py +1645 -57
- package/scripts/mcp_editors.py +5 -2
- package/scripts/plugin.py +2495 -163
- package/scripts/plugin_mcp.py +279 -0
- package/scripts/plugin_rules.py +389 -0
- package/scripts/plugin_schema.py +139 -23
- package/scripts/uninstall.py +47 -4
- package/scripts/validate.py +421 -0
|
@@ -91,6 +91,7 @@ def resolve_extends(
|
|
|
91
91
|
project_root: str | Path,
|
|
92
92
|
*,
|
|
93
93
|
refresh: bool = False,
|
|
94
|
+
persistent: bool = True,
|
|
94
95
|
) -> ResolutionResult:
|
|
95
96
|
"""Resolve an extends chain into an ordered list of base configs.
|
|
96
97
|
|
|
@@ -98,6 +99,9 @@ def resolve_extends(
|
|
|
98
99
|
extends_value: The extends string from .softspark-toolkit.json.
|
|
99
100
|
project_root: The project directory (for resolving relative paths).
|
|
100
101
|
refresh: Force re-fetch ignoring cache.
|
|
102
|
+
persistent: Allow remote sources to populate the config cache. When
|
|
103
|
+
false, cached sources remain readable and cache misses resolve in
|
|
104
|
+
an isolated staging directory that is removed before return.
|
|
101
105
|
|
|
102
106
|
Returns:
|
|
103
107
|
ResolutionResult with ordered configs (deepest ancestor first).
|
|
@@ -106,7 +110,26 @@ def resolve_extends(
|
|
|
106
110
|
ConfigResolverError: On resolution failure.
|
|
107
111
|
"""
|
|
108
112
|
result = ResolutionResult()
|
|
109
|
-
|
|
113
|
+
if persistent:
|
|
114
|
+
_resolve_chain(
|
|
115
|
+
extends_value,
|
|
116
|
+
Path(project_root),
|
|
117
|
+
set(),
|
|
118
|
+
result,
|
|
119
|
+
refresh=refresh,
|
|
120
|
+
staging_root=None,
|
|
121
|
+
)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
with tempfile.TemporaryDirectory(prefix="ai-toolkit-config-preview-") as tmp:
|
|
125
|
+
_resolve_chain(
|
|
126
|
+
extends_value,
|
|
127
|
+
Path(project_root),
|
|
128
|
+
set(),
|
|
129
|
+
result,
|
|
130
|
+
refresh=refresh,
|
|
131
|
+
staging_root=Path(tmp),
|
|
132
|
+
)
|
|
110
133
|
return result
|
|
111
134
|
|
|
112
135
|
|
|
@@ -148,6 +171,7 @@ def _resolve_chain(
|
|
|
148
171
|
result: ResolutionResult,
|
|
149
172
|
*,
|
|
150
173
|
refresh: bool = False,
|
|
174
|
+
staging_root: Path | None,
|
|
151
175
|
) -> None:
|
|
152
176
|
"""Recursively resolve extends chain with cycle + depth detection."""
|
|
153
177
|
# Cycle detection
|
|
@@ -169,7 +193,13 @@ def _resolve_chain(
|
|
|
169
193
|
visited.add(canonical)
|
|
170
194
|
|
|
171
195
|
# Resolve this source
|
|
172
|
-
base_config = _resolve_source(
|
|
196
|
+
base_config = _resolve_source(
|
|
197
|
+
extends_value,
|
|
198
|
+
project_root,
|
|
199
|
+
result,
|
|
200
|
+
refresh=refresh,
|
|
201
|
+
staging_root=staging_root,
|
|
202
|
+
)
|
|
173
203
|
_validate_resolved_base(base_config)
|
|
174
204
|
|
|
175
205
|
# Recurse if this base also extends something
|
|
@@ -180,6 +210,7 @@ def _resolve_chain(
|
|
|
180
210
|
visited,
|
|
181
211
|
result,
|
|
182
212
|
refresh=refresh,
|
|
213
|
+
staging_root=staging_root,
|
|
183
214
|
)
|
|
184
215
|
|
|
185
216
|
# Append after recursion (deepest ancestor first)
|
|
@@ -205,14 +236,25 @@ def _resolve_source(
|
|
|
205
236
|
result: ResolutionResult,
|
|
206
237
|
*,
|
|
207
238
|
refresh: bool = False,
|
|
239
|
+
staging_root: Path | None,
|
|
208
240
|
) -> BaseConfig:
|
|
209
241
|
"""Resolve a single extends source."""
|
|
210
242
|
if source.startswith("git+"):
|
|
211
|
-
return _resolve_git(
|
|
243
|
+
return _resolve_git(
|
|
244
|
+
source,
|
|
245
|
+
result,
|
|
246
|
+
refresh=refresh,
|
|
247
|
+
staging_root=staging_root,
|
|
248
|
+
)
|
|
212
249
|
if source.startswith(".") or source.startswith("/") or source.startswith("~"):
|
|
213
250
|
return _resolve_local(source, project_root)
|
|
214
251
|
# Default: npm package
|
|
215
|
-
return _resolve_npm(
|
|
252
|
+
return _resolve_npm(
|
|
253
|
+
source,
|
|
254
|
+
result,
|
|
255
|
+
refresh=refresh,
|
|
256
|
+
staging_root=staging_root,
|
|
257
|
+
)
|
|
216
258
|
|
|
217
259
|
|
|
218
260
|
# ---------------------------------------------------------------------------
|
|
@@ -224,6 +266,7 @@ def _resolve_npm(
|
|
|
224
266
|
result: ResolutionResult,
|
|
225
267
|
*,
|
|
226
268
|
refresh: bool = False,
|
|
269
|
+
staging_root: Path | None,
|
|
227
270
|
) -> BaseConfig:
|
|
228
271
|
"""Resolve from npm registry via npm pack."""
|
|
229
272
|
package_name, version_spec = _parse_npm_source(source)
|
|
@@ -270,8 +313,13 @@ def _resolve_npm(
|
|
|
270
313
|
tarball = tarballs[0]
|
|
271
314
|
version = _extract_version_from_tarball(tarball.name, package_name)
|
|
272
315
|
|
|
273
|
-
|
|
274
|
-
|
|
316
|
+
dest = _remote_destination(
|
|
317
|
+
cache_dir / version,
|
|
318
|
+
staging_root,
|
|
319
|
+
"npm",
|
|
320
|
+
pack_source,
|
|
321
|
+
version,
|
|
322
|
+
)
|
|
275
323
|
dest.mkdir(parents=True, exist_ok=True)
|
|
276
324
|
_extract_tarball(tarball, dest)
|
|
277
325
|
|
|
@@ -366,6 +414,7 @@ def _resolve_git(
|
|
|
366
414
|
result: ResolutionResult,
|
|
367
415
|
*,
|
|
368
416
|
refresh: bool = False,
|
|
417
|
+
staging_root: Path | None,
|
|
369
418
|
) -> BaseConfig:
|
|
370
419
|
"""Resolve from git URL (git+https://...)."""
|
|
371
420
|
url = source.removeprefix("git+")
|
|
@@ -379,16 +428,19 @@ def _resolve_git(
|
|
|
379
428
|
if not refresh and cache_dir.is_dir() and (cache_dir / CONFIG_FILENAME).is_file():
|
|
380
429
|
return _load_cached_config(cache_dir, source)
|
|
381
430
|
|
|
382
|
-
|
|
383
|
-
|
|
431
|
+
destination = _remote_destination(
|
|
432
|
+
cache_dir,
|
|
433
|
+
staging_root,
|
|
434
|
+
"git",
|
|
435
|
+
url,
|
|
436
|
+
)
|
|
437
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
384
438
|
try:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
shutil.rmtree(cache_dir)
|
|
388
|
-
cache_dir.mkdir(parents=True)
|
|
439
|
+
if destination.is_dir():
|
|
440
|
+
shutil.rmtree(destination)
|
|
389
441
|
|
|
390
442
|
proc = subprocess.run(
|
|
391
|
-
["git", "clone", "--depth", "1", url, str(
|
|
443
|
+
["git", "clone", "--depth", "1", url, str(destination)],
|
|
392
444
|
capture_output=True,
|
|
393
445
|
text=True,
|
|
394
446
|
timeout=120,
|
|
@@ -404,7 +456,7 @@ def _resolve_git(
|
|
|
404
456
|
f"git clone failed: {proc.stderr.strip()}"
|
|
405
457
|
)
|
|
406
458
|
|
|
407
|
-
return _load_cached_config(
|
|
459
|
+
return _load_cached_config(destination, source)
|
|
408
460
|
|
|
409
461
|
|
|
410
462
|
# ---------------------------------------------------------------------------
|
|
@@ -431,6 +483,20 @@ def _resolve_local(source: str, project_root: Path) -> BaseConfig:
|
|
|
431
483
|
# Shared helpers
|
|
432
484
|
# ---------------------------------------------------------------------------
|
|
433
485
|
|
|
486
|
+
def _remote_destination(
|
|
487
|
+
persistent_path: Path,
|
|
488
|
+
staging_root: Path | None,
|
|
489
|
+
source_kind: str,
|
|
490
|
+
source: str,
|
|
491
|
+
version: str = "",
|
|
492
|
+
) -> Path:
|
|
493
|
+
"""Choose a persistent cache path or an isolated preview path."""
|
|
494
|
+
if staging_root is None:
|
|
495
|
+
return persistent_path
|
|
496
|
+
source_key = hashlib.sha256(source.encode()).hexdigest()[:16]
|
|
497
|
+
destination = staging_root / source_kind / source_key
|
|
498
|
+
return destination / version if version else destination
|
|
499
|
+
|
|
434
500
|
def _load_cached_config(
|
|
435
501
|
config_dir: Path,
|
|
436
502
|
source: str,
|
package/scripts/doctor.py
CHANGED
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
Checks:
|
|
9
9
|
1. Environment prerequisites (node, bash, python3, bats) + check_deps
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4. Hook
|
|
13
|
-
5.
|
|
14
|
-
6.
|
|
15
|
-
7.
|
|
16
|
-
8.
|
|
17
|
-
9.
|
|
18
|
-
10.
|
|
19
|
-
11.
|
|
10
|
+
2. Local AI runtime availability and versions (credential-blind)
|
|
11
|
+
3. Global install integrity (symlinks, settings.json hooks)
|
|
12
|
+
4. Hook scripts (existence, executable)
|
|
13
|
+
5. Hook configuration (valid event names)
|
|
14
|
+
6. Generated artifacts (AGENTS.md, llms.txt staleness)
|
|
15
|
+
7. Planned assets
|
|
16
|
+
8. Benchmark freshness
|
|
17
|
+
9. Stale rules
|
|
18
|
+
10. URL hook sources
|
|
19
|
+
11. Language rules drift (project-local)
|
|
20
|
+
12. Plugin double-load (Claude app plugin vs global install)
|
|
20
21
|
|
|
21
22
|
Exit codes:
|
|
22
23
|
0 all checks pass
|
|
@@ -24,6 +25,7 @@ Exit codes:
|
|
|
24
25
|
"""
|
|
25
26
|
from __future__ import annotations
|
|
26
27
|
|
|
28
|
+
from dataclasses import dataclass
|
|
27
29
|
import json
|
|
28
30
|
import os
|
|
29
31
|
import re
|
|
@@ -100,6 +102,32 @@ PLANNED_ASSETS = [
|
|
|
100
102
|
toolkit_dir / "app" / "skills" / "agent-creator" / "SKILL.md",
|
|
101
103
|
]
|
|
102
104
|
|
|
105
|
+
AI_RUNTIME_BINARIES = (
|
|
106
|
+
("dsh", "DSH"),
|
|
107
|
+
("codex", "Codex"),
|
|
108
|
+
("claude", "Claude Code"),
|
|
109
|
+
("copilot", "GitHub Copilot"),
|
|
110
|
+
)
|
|
111
|
+
AI_RUNTIME_VERSION_TIMEOUT_SECONDS = 5
|
|
112
|
+
_SEMVER_NUMERIC_IDENTIFIER = r"(?:0|[1-9][0-9]*)"
|
|
113
|
+
_SEMVER_PRERELEASE_IDENTIFIER = (
|
|
114
|
+
rf"(?:{_SEMVER_NUMERIC_IDENTIFIER}|"
|
|
115
|
+
r"[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
|
|
116
|
+
)
|
|
117
|
+
_SEMVER_BUILD_IDENTIFIER = r"[0-9A-Za-z-]+"
|
|
118
|
+
SEMVER_OUTPUT = re.compile(
|
|
119
|
+
r"(?<![0-9A-Za-z.+-])v?"
|
|
120
|
+
rf"(?P<version>{_SEMVER_NUMERIC_IDENTIFIER}\."
|
|
121
|
+
rf"{_SEMVER_NUMERIC_IDENTIFIER}\."
|
|
122
|
+
rf"{_SEMVER_NUMERIC_IDENTIFIER}"
|
|
123
|
+
rf"(?:-{_SEMVER_PRERELEASE_IDENTIFIER}"
|
|
124
|
+
rf"(?:\.{_SEMVER_PRERELEASE_IDENTIFIER})*)?"
|
|
125
|
+
rf"(?:\+{_SEMVER_BUILD_IDENTIFIER}"
|
|
126
|
+
rf"(?:\.{_SEMVER_BUILD_IDENTIFIER})*)?)"
|
|
127
|
+
r"(?![0-9A-Za-z.+-])"
|
|
128
|
+
)
|
|
129
|
+
VERSION_LIKE_OUTPUT = re.compile(r"(?<![0-9])[0-9]+\.[0-9]+\.[^\s,;()]+")
|
|
130
|
+
|
|
103
131
|
|
|
104
132
|
# ---------------------------------------------------------------------------
|
|
105
133
|
# Status helpers
|
|
@@ -134,20 +162,53 @@ class DiagResult:
|
|
|
134
162
|
# Version extraction
|
|
135
163
|
# ---------------------------------------------------------------------------
|
|
136
164
|
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
@dataclass(frozen=True, slots=True)
|
|
166
|
+
class VersionProbe:
|
|
167
|
+
"""Result of a bounded, non-authenticating ``--version`` probe."""
|
|
168
|
+
|
|
169
|
+
version: str | None
|
|
170
|
+
error: str | None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _probe_version(executable: str) -> VersionProbe:
|
|
174
|
+
"""Run the exact discovered executable and parse its SemVer output."""
|
|
139
175
|
try:
|
|
140
176
|
result = subprocess.run(
|
|
141
|
-
[
|
|
177
|
+
[executable, "--version"],
|
|
142
178
|
capture_output=True,
|
|
143
179
|
text=True,
|
|
144
|
-
timeout=
|
|
180
|
+
timeout=AI_RUNTIME_VERSION_TIMEOUT_SECONDS,
|
|
145
181
|
)
|
|
182
|
+
except FileNotFoundError:
|
|
183
|
+
return VersionProbe(None, "executable disappeared before version check")
|
|
184
|
+
except subprocess.TimeoutExpired:
|
|
185
|
+
return VersionProbe(None, "version check timed out")
|
|
186
|
+
except UnicodeError:
|
|
187
|
+
return VersionProbe(None, "version output is not valid text")
|
|
188
|
+
except OSError as error:
|
|
189
|
+
detail = error.strerror or type(error).__name__
|
|
190
|
+
return VersionProbe(None, f"version check failed: {detail}")
|
|
191
|
+
|
|
192
|
+
if result.returncode != 0:
|
|
193
|
+
return VersionProbe(
|
|
194
|
+
None,
|
|
195
|
+
f"version command exited with status {result.returncode}",
|
|
196
|
+
)
|
|
197
|
+
try:
|
|
146
198
|
output = result.stdout.strip() or result.stderr.strip()
|
|
147
|
-
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
except UnicodeError:
|
|
200
|
+
return VersionProbe(None, "version output is not valid text")
|
|
201
|
+
match = SEMVER_OUTPUT.search(output)
|
|
202
|
+
if not match:
|
|
203
|
+
if VERSION_LIKE_OUTPUT.search(output):
|
|
204
|
+
return VersionProbe(None, "version output contained invalid SemVer")
|
|
205
|
+
return VersionProbe(None, "version output did not contain SemVer")
|
|
206
|
+
return VersionProbe(match.group("version"), None)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _get_version(executable: str) -> str:
|
|
210
|
+
"""Compatibility helper for required environment binary reporting."""
|
|
211
|
+
return _probe_version(executable).version or ""
|
|
151
212
|
|
|
152
213
|
|
|
153
214
|
# ---------------------------------------------------------------------------
|
|
@@ -165,8 +226,8 @@ def check_environment(dr: DiagResult, fix_mode: bool) -> None:
|
|
|
165
226
|
("python3", "python3", False),
|
|
166
227
|
("bats", "bats", False),
|
|
167
228
|
]:
|
|
168
|
-
if shutil.which(binary):
|
|
169
|
-
version = _get_version(
|
|
229
|
+
if executable := shutil.which(binary):
|
|
230
|
+
version = _get_version(executable)
|
|
170
231
|
ver_str = f" {version}" if version else ""
|
|
171
232
|
dr.ok(f"{label}{ver_str}")
|
|
172
233
|
else:
|
|
@@ -191,6 +252,22 @@ def check_environment(dr: DiagResult, fix_mode: bool) -> None:
|
|
|
191
252
|
print()
|
|
192
253
|
|
|
193
254
|
|
|
255
|
+
def check_ai_runtimes(dr: DiagResult) -> None:
|
|
256
|
+
"""Report local AI runtime availability without inspecting login state."""
|
|
257
|
+
print("## AI Runtimes")
|
|
258
|
+
for binary, label in AI_RUNTIME_BINARIES:
|
|
259
|
+
executable = shutil.which(binary)
|
|
260
|
+
if not executable:
|
|
261
|
+
dr.skip(f"{label} ({binary}) not found")
|
|
262
|
+
continue
|
|
263
|
+
probe = _probe_version(executable)
|
|
264
|
+
if probe.version:
|
|
265
|
+
dr.ok(f"{label} ({binary}) {probe.version}")
|
|
266
|
+
else:
|
|
267
|
+
dr.warn(f"{label} ({binary}) detected but {probe.error}")
|
|
268
|
+
print()
|
|
269
|
+
|
|
270
|
+
|
|
194
271
|
# ---------------------------------------------------------------------------
|
|
195
272
|
# Check 2: Global Install
|
|
196
273
|
# ---------------------------------------------------------------------------
|
|
@@ -839,6 +916,7 @@ def main() -> None:
|
|
|
839
916
|
dr = DiagResult()
|
|
840
917
|
|
|
841
918
|
check_environment(dr, fix_mode)
|
|
919
|
+
check_ai_runtimes(dr)
|
|
842
920
|
check_global_install(dr, fix_mode)
|
|
843
921
|
check_hook_scripts(dr, fix_mode)
|
|
844
922
|
check_hook_configuration(dr)
|
|
@@ -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-09-01",
|
|
5
5
|
"tools": [
|
|
6
6
|
{
|
|
7
7
|
"id": "claude-code",
|
|
@@ -130,6 +130,56 @@
|
|
|
130
130
|
],
|
|
131
131
|
"version_probe": null
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
"id": "dsh",
|
|
135
|
+
"display_name": "DeepSeek Harness",
|
|
136
|
+
"kind": "harness",
|
|
137
|
+
"status": "developer-preview",
|
|
138
|
+
"selection_policy": "explicit-only",
|
|
139
|
+
"reviewed_version": "0.1.1-rc.2",
|
|
140
|
+
"excluded_from": [
|
|
141
|
+
"editors-all",
|
|
142
|
+
"auto-detect",
|
|
143
|
+
"defaults"
|
|
144
|
+
],
|
|
145
|
+
"urls": {
|
|
146
|
+
"docs": "https://deepseek-harness.github.io/deepseek-harness/",
|
|
147
|
+
"release_notes": "https://github.com/deepseek-ai/deepseek-harness/releases",
|
|
148
|
+
"reviewed_release": "https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.1-rc.2",
|
|
149
|
+
"reviewed_cli_docs": "https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.1-rc.2/apps/cli/reference/README.md",
|
|
150
|
+
"reviewed_skill_docs": "https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.1-rc.2/docs/subsystems/skills.md"
|
|
151
|
+
},
|
|
152
|
+
"config_paths": [
|
|
153
|
+
".agents/skills/*/SKILL.md",
|
|
154
|
+
"$DSH_HOME/profiles/<profile>/package.json",
|
|
155
|
+
"$DSH_HOME/profiles/<profile>/node_modules/@softspark/dsh-*",
|
|
156
|
+
"$DSH_HOME/.agent-presets/softspark-orchestrator"
|
|
157
|
+
],
|
|
158
|
+
"our_generators": [
|
|
159
|
+
"scripts/generate_codex_skills.py"
|
|
160
|
+
],
|
|
161
|
+
"our_lifecycle": [
|
|
162
|
+
"scripts/install_steps/dsh.py"
|
|
163
|
+
],
|
|
164
|
+
"lifecycle_commands": [
|
|
165
|
+
"ai-toolkit dsh install --profile web",
|
|
166
|
+
"ai-toolkit dsh update --profile web",
|
|
167
|
+
"ai-toolkit dsh doctor --profile web",
|
|
168
|
+
"ai-toolkit dsh uninstall --profile web"
|
|
169
|
+
],
|
|
170
|
+
"status_note": "Community compatibility target maintained by SoftSpark. DeepSeek AI has not endorsed this integration. ai-toolkit reuses its native skill generator for project .agents/skills output and manages exact @softspark/dsh-codex@1.0.0 and @softspark/dsh-orchestrator@1.0.1 packages only through explicit profile lifecycle commands. Upstream has newer prereleases, but this entry remains pinned to the reviewed DSH 0.1.1-rc.2 contract pending qualification.",
|
|
171
|
+
"capability_markers": [
|
|
172
|
+
"Developer preview",
|
|
173
|
+
"profiles",
|
|
174
|
+
"dsh plugin",
|
|
175
|
+
"skills",
|
|
176
|
+
"Agent Preset"
|
|
177
|
+
],
|
|
178
|
+
"version_probe": {
|
|
179
|
+
"kind": "command",
|
|
180
|
+
"command": "dsh --version"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
133
183
|
{
|
|
134
184
|
"id": "cursor",
|
|
135
185
|
"display_name": "Cursor",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Copyright 2024-2026 Lukasz Krzemien (biuro@softspark.eu)
|
|
4
4
|
# Source: https://github.com/softspark/ai-toolkit
|
|
5
5
|
|
|
6
|
-
"""Mirror the ai-toolkit skill catalogue into
|
|
6
|
+
"""Mirror the ai-toolkit skill catalogue into ``.agents/skills/``.
|
|
7
7
|
|
|
8
8
|
OpenAI Codex CLI discovers Agent Skills from ``.agents/skills/`` in the
|
|
9
9
|
repository tree, plus user/admin/system skill locations. Unlike the Augment
|
|
@@ -13,7 +13,8 @@ on disk, so this generator syncs every skill in ``app/skills/`` into
|
|
|
13
13
|
|
|
14
14
|
The standalone generator keeps ``enable_codex_skills=False`` as a compatibility
|
|
15
15
|
default. Selecting Codex in the main installer installs this catalog
|
|
16
|
-
automatically; ``--codex-skills`` remains an explicit refresh option.
|
|
16
|
+
automatically; ``--codex-skills`` remains an explicit refresh option. DSH
|
|
17
|
+
reuses the same one-level managed surface without receiving other Codex config.
|
|
17
18
|
|
|
18
19
|
Implementation:
|
|
19
20
|
* Native Codex-compatible skills are symlinked to canonical ``app/skills``.
|
|
@@ -40,7 +41,7 @@ from pathlib import Path
|
|
|
40
41
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
41
42
|
from codex_skill_adapter import (
|
|
42
43
|
cleanup_codex_skills,
|
|
43
|
-
|
|
44
|
+
managed_skill_surface_transaction,
|
|
44
45
|
sync_codex_skill,
|
|
45
46
|
unmanaged_codex_skill_names,
|
|
46
47
|
)
|
|
@@ -104,27 +105,28 @@ def generate(target_dir: Path, enable_codex_skills: bool = False) -> None:
|
|
|
104
105
|
if not enable_codex_skills:
|
|
105
106
|
return
|
|
106
107
|
|
|
107
|
-
codex_skills_dir = prepare_codex_skills_dir(target_dir)
|
|
108
|
-
|
|
109
108
|
sources = _iter_source_skills()
|
|
110
|
-
user_names = unmanaged_codex_skill_names(codex_skills_dir, skills_dir)
|
|
111
|
-
|
|
112
109
|
linked = 0
|
|
113
110
|
adapted = 0
|
|
114
111
|
skipped = 0
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
with managed_skill_surface_transaction(target_dir, skills_dir) as transaction:
|
|
113
|
+
codex_skills_dir = transaction.skills_dst
|
|
114
|
+
user_names = unmanaged_codex_skill_names(codex_skills_dir, skills_dir)
|
|
115
|
+
|
|
116
|
+
for skill in sources:
|
|
117
|
+
if skill.name in user_names:
|
|
118
|
+
skipped += 1
|
|
119
|
+
continue
|
|
120
|
+
mode = sync_codex_skill(skill, codex_skills_dir)
|
|
121
|
+
if mode == "linked":
|
|
122
|
+
linked += 1
|
|
123
|
+
elif mode == "adapted":
|
|
124
|
+
adapted += 1
|
|
125
|
+
else:
|
|
126
|
+
skipped += 1
|
|
127
|
+
|
|
128
|
+
cleanup_codex_skills(codex_skills_dir, skills_dir, user_names)
|
|
129
|
+
transaction.commit({"codex"})
|
|
128
130
|
|
|
129
131
|
print(
|
|
130
132
|
f" Codex skill mirror: {_count_entries(codex_skills_dir)} skills "
|
package/scripts/install.py
CHANGED
|
@@ -76,7 +76,7 @@ from config_resolver import (
|
|
|
76
76
|
)
|
|
77
77
|
from config_merger import ConfigMergeError, merge_config_chain
|
|
78
78
|
from config_validator import validate_project_config
|
|
79
|
-
from config_lock import save_lock_file
|
|
79
|
+
from config_lock import LOCK_FILENAME, save_lock_file
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
# ---------------------------------------------------------------------------
|
|
@@ -317,7 +317,7 @@ VALID_LANGS = {"python", "typescript", "golang", "go", "rust", "java", "kotlin",
|
|
|
317
317
|
|
|
318
318
|
def validate_args(cfg: dict) -> None:
|
|
319
319
|
"""Validate parsed arguments — exit non-zero on invalid values."""
|
|
320
|
-
from install_steps.ai_tools import
|
|
320
|
+
from install_steps.ai_tools import LOCAL_ONLY_EDITORS, SELECTABLE_EDITORS
|
|
321
321
|
|
|
322
322
|
errors: list[str] = []
|
|
323
323
|
|
|
@@ -339,8 +339,13 @@ def validate_args(cfg: dict) -> None:
|
|
|
339
339
|
if cfg["editors"] and cfg["editors"] != "all":
|
|
340
340
|
for e in cfg["editors"].split(","):
|
|
341
341
|
e = e.strip()
|
|
342
|
-
if e and e not in
|
|
343
|
-
errors.append(
|
|
342
|
+
if e and e not in SELECTABLE_EDITORS:
|
|
343
|
+
errors.append(
|
|
344
|
+
f"Unknown editor: '{e}' "
|
|
345
|
+
f"(valid: {', '.join(SELECTABLE_EDITORS)}, all)"
|
|
346
|
+
)
|
|
347
|
+
if e in LOCAL_ONLY_EDITORS and not cfg["local"]:
|
|
348
|
+
errors.append(f"Editor '{e}' is project-local and requires --local")
|
|
344
349
|
|
|
345
350
|
# Validate --lang
|
|
346
351
|
if cfg["lang"]:
|
|
@@ -536,6 +541,8 @@ def resolve_extends_config(
|
|
|
536
541
|
project_dir: Path,
|
|
537
542
|
config_path: str = "",
|
|
538
543
|
refresh: bool = False,
|
|
544
|
+
*,
|
|
545
|
+
persist_lock: bool = True,
|
|
539
546
|
) -> dict | None:
|
|
540
547
|
"""Resolve .softspark-toolkit.json extends and return merged config.
|
|
541
548
|
|
|
@@ -574,7 +581,12 @@ def resolve_extends_config(
|
|
|
574
581
|
print(f" Resolving extends: {extends}...")
|
|
575
582
|
|
|
576
583
|
try:
|
|
577
|
-
result = resolve_extends(
|
|
584
|
+
result = resolve_extends(
|
|
585
|
+
extends,
|
|
586
|
+
config_root,
|
|
587
|
+
refresh=refresh,
|
|
588
|
+
persistent=persist_lock,
|
|
589
|
+
)
|
|
578
590
|
except ConfigResolverError as e:
|
|
579
591
|
print(f" ✗ Resolution failed: {e}")
|
|
580
592
|
sys.exit(1)
|
|
@@ -611,13 +623,15 @@ def resolve_extends_config(
|
|
|
611
623
|
"overrides_applied": merge_result.overrides_applied,
|
|
612
624
|
}
|
|
613
625
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
626
|
+
if persist_lock:
|
|
627
|
+
lock_path = save_lock_file(
|
|
628
|
+
config_root,
|
|
629
|
+
config_metas,
|
|
630
|
+
ai_toolkit_version=_get_toolkit_version(),
|
|
631
|
+
)
|
|
632
|
+
print(f" Saved: {lock_path.name}")
|
|
633
|
+
else:
|
|
634
|
+
print(f" Would save: {LOCK_FILENAME} (dry-run)")
|
|
621
635
|
|
|
622
636
|
return merge_result.merged
|
|
623
637
|
|
|
@@ -736,7 +750,10 @@ def main() -> None:
|
|
|
736
750
|
config_path_arg: str = cfg["config"]
|
|
737
751
|
refresh_base: bool = cfg["refresh_base"]
|
|
738
752
|
merged_config = resolve_extends_config(
|
|
739
|
-
project_dir,
|
|
753
|
+
project_dir,
|
|
754
|
+
config_path=config_path_arg,
|
|
755
|
+
refresh=refresh_base,
|
|
756
|
+
persist_lock=not dry_run,
|
|
740
757
|
)
|
|
741
758
|
if merged_config:
|
|
742
759
|
cfg = _apply_merged_config(merged_config, cfg)
|