@softspark/ai-toolkit 4.32.0 → 4.32.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 +51 -0
- package/README.md +19 -24
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/analyze/scripts/complexity.py +3 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +13 -6
- package/app/skills/docs/scripts/doc-inventory.py +3 -0
- package/app/skills/explain/scripts/dependency-graph.py +3 -0
- package/app/skills/migrate/scripts/migration-status.py +3 -0
- package/app/skills/refactor/scripts/refactor-scan.py +3 -0
- package/bin/ai-toolkit.js +5 -1
- package/kb/reference/cli-reference.md +1 -1
- package/kb/reference/plugin-pack-conventions.md +3 -3
- package/llms-full.txt +4 -4
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/antigravity_plugin.py +2 -1
- package/scripts/claude_app.py +2 -1
- package/scripts/codex_plugin.py +2 -1
- package/scripts/paths.py +18 -0
- package/scripts/plugin.py +128 -16
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.32.2 - Plugin exports land where you run the command (2026-09-04)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`claude-app export`, `codex-plugin export`, `antigravity-plugin export`
|
|
15
|
+
write to the shell's directory.** `bin/ai-toolkit.js` runs these three with
|
|
16
|
+
`cwd` inside the installed package so their asset paths resolve, which also
|
|
17
|
+
made every relative `--output`, and therefore every default
|
|
18
|
+
(`ai-toolkit-claude-app.zip`, `*-global-instructions.md`), appear next to
|
|
19
|
+
`node_modules/@softspark/ai-toolkit`. The wrapper now passes the user's
|
|
20
|
+
directory as `AI_TOOLKIT_USER_CWD` and `paths.user_path()` resolves relative
|
|
21
|
+
outputs against it; absolute paths and direct `python3 scripts/...` runs
|
|
22
|
+
are unchanged. Found the first time the v4.32.1 README instruction was
|
|
23
|
+
followed from `~/Downloads`.
|
|
24
|
+
- `.gitignore` ignores root-level `*.zip` and `*-global-instructions.md` so an
|
|
25
|
+
export run from the repository cannot be committed by accident.
|
|
26
|
+
- **Test count.** Bats increased from 1972 to 1974 (relative `--output` from
|
|
27
|
+
a foreign cwd for `claude-app` and `codex-plugin`).
|
|
28
|
+
|
|
29
|
+
## v4.32.1 - Plugin removal leaves nothing behind (2026-09-04)
|
|
30
|
+
|
|
31
|
+
Both fixes come from running the post-release SOPs on v4.32.0 in an isolated
|
|
32
|
+
sandbox; both defects predate that release (identical on v4.31.0).
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **`plugin remove` deletes the hooks and scripts a pack installed.** The
|
|
37
|
+
Claude and Codex install paths copied `hooks/plugin-<pack>-*` and
|
|
38
|
+
`plugin-scripts/<pack>/*` into `~/.softspark/ai-toolkit` but never recorded
|
|
39
|
+
them in `shared_asset_ownership`, so removal found no owner and preserved
|
|
40
|
+
every file as "untracked": each pack left 4-5 files behind. Install now
|
|
41
|
+
records sha256/mode/inode for what it copied; removal deletes only entries
|
|
42
|
+
this editor consumes that no other editor still uses and that are unchanged
|
|
43
|
+
on disk, preserves an edited file with a `WARN preserved changed plugin
|
|
44
|
+
asset` line, and drops the empty `plugin-scripts/<pack>/` directory.
|
|
45
|
+
`tests/test_plugin_asset_removal.bats` covers install, zero-residue removal,
|
|
46
|
+
user-edited preservation, two-editor sharing, and reinstall.
|
|
47
|
+
- **Five skill scripts answer `--help`.** `doc-inventory.py`,
|
|
48
|
+
`dependency-graph.py`, `migration-status.py`, `refactor-scan.py`, and
|
|
49
|
+
`complexity.py` treated `--help` as a path and returned a JSON error.
|
|
50
|
+
`tests/test_skill_scripts_help.bats` now probes every documented
|
|
51
|
+
`${CLAUDE_SKILL_DIR}` invocation with `--help` (exit 0, no traceback), the
|
|
52
|
+
same check as post-release SOP Phase 4b. The probe uses `perl alarm` as its
|
|
53
|
+
hang guard, not GNU `timeout`, which the macOS CI runner does not have.
|
|
54
|
+
- **`pre_deploy_check.py` no longer crashes on Linux.** It probed for docker
|
|
55
|
+
by running the shell builtin `command -v` through `subprocess`, which only
|
|
56
|
+
works on macOS (a `/usr/bin/command` binary exists there); on Linux it raised
|
|
57
|
+
`FileNotFoundError` before any check ran. Now `shutil.which`. Caught by the
|
|
58
|
+
new `--help` probe on the Ubuntu CI runner, which is the point of that test.
|
|
59
|
+
- **Test count.** Bats increased from 1966 to 1972.
|
|
60
|
+
|
|
10
61
|
## v4.32.0 - Context budget: scoped rules, scoped language skills, doctor budget checks (2026-09-04)
|
|
11
62
|
|
|
12
63
|
### Added
|
package/README.md
CHANGED
|
@@ -6,30 +6,25 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.32.2
|
|
12
|
+
|
|
13
|
+
**v4.32.2** makes plugin exports land where you run the command:
|
|
14
|
+
|
|
15
|
+
- `ai-toolkit claude-app export`, `codex-plugin export`, and
|
|
16
|
+
`antigravity-plugin export` write their ZIP (and the sibling
|
|
17
|
+
`*-global-instructions.md`) to the directory you typed the command in. They
|
|
18
|
+
used to appear inside `node_modules/@softspark/ai-toolkit` because the CLI
|
|
19
|
+
runs them with its own working directory; the wrapper now hands the shell's
|
|
20
|
+
directory to the exporters. Absolute `--output` paths are unchanged.
|
|
21
|
+
- Root-level `*.zip` and `*-global-instructions.md` are gitignored so an
|
|
22
|
+
export run from the repository is never committed.
|
|
23
|
+
- Still in this train: `plugin remove` leaves nothing behind, every skill
|
|
24
|
+
script answers `--help` (v4.32.1); path-scoped common rules, project-scoped
|
|
25
|
+
language skills, `doctor` context-budget and permission checks, one strict
|
|
26
|
+
frontmatter parser, `git-team` for `--profile strict` (v4.32.0). Test
|
|
27
|
+
count: 1972 -> 1974 bats + 348 pytest.
|
|
33
28
|
|
|
34
29
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
35
30
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "ai-toolkit",
|
|
4
4
|
"displayName": "AI Toolkit",
|
|
5
5
|
"description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
|
|
6
|
-
"version": "4.32.
|
|
6
|
+
"version": "4.32.2",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -125,6 +125,9 @@ def _summary(root: Path, all_files: list[Path]) -> tuple[int, int]:
|
|
|
125
125
|
|
|
126
126
|
def main() -> None:
|
|
127
127
|
"""Entry point: analyse directory and print text report to stdout."""
|
|
128
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
129
|
+
print("Usage: complexity.py [directory]\n\nReport per-file code complexity for directory (default: cwd).")
|
|
130
|
+
return
|
|
128
131
|
root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd().resolve()
|
|
129
132
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
130
133
|
|
|
@@ -16,6 +16,7 @@ Usage::
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
|
+
import shutil
|
|
19
20
|
import subprocess
|
|
20
21
|
import sys
|
|
21
22
|
from datetime import datetime, timezone
|
|
@@ -50,12 +51,15 @@ def _git_branch() -> str:
|
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def _docker_available() -> bool:
|
|
53
|
-
"""Return True if the ``docker`` command is on PATH.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
"""Return True if the ``docker`` command is on PATH.
|
|
55
|
+
|
|
56
|
+
``command -v`` is a shell builtin; running it through ``subprocess`` only
|
|
57
|
+
worked on macOS, which ships a ``/usr/bin/command`` binary. On Linux it
|
|
58
|
+
raised ``FileNotFoundError`` before any check ran (v4.32.1).
|
|
59
|
+
"""
|
|
60
|
+
if shutil.which("docker") is None:
|
|
61
|
+
return False
|
|
62
|
+
return _run(["docker", "--version"]).returncode == 0
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
def _docker_services_running() -> bool:
|
|
@@ -155,6 +159,9 @@ def _run_checks(env: str) -> _CheckCollector:
|
|
|
155
159
|
|
|
156
160
|
def main() -> None:
|
|
157
161
|
"""Entry point: run checks and print JSON result to stdout."""
|
|
162
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
163
|
+
print("Usage: pre_deploy_check.py [environment]\n\nRun pre-deployment checks for environment (default: staging) and print JSON.")
|
|
164
|
+
return
|
|
158
165
|
env = sys.argv[1] if len(sys.argv) > 1 else "staging"
|
|
159
166
|
branch = _git_branch()
|
|
160
167
|
collector = _run_checks(env)
|
|
@@ -130,6 +130,9 @@ def get_mtime(filepath: str) -> float:
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def main():
|
|
133
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
134
|
+
print("Usage: doc-inventory.py [directory]\n\nInventory documentation files under directory (default: cwd) as JSON.")
|
|
135
|
+
return
|
|
133
136
|
root = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.getcwd()
|
|
134
137
|
if not os.path.isdir(root):
|
|
135
138
|
print(json.dumps({"error": f"Not a directory: {root}"}))
|
|
@@ -159,6 +159,9 @@ def generate_mermaid(target_name: str, imports: list, imported_by: list) -> str:
|
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
def main():
|
|
162
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
163
|
+
print("Usage: dependency-graph.py <file_or_directory>\n\nPrint the import/dependency graph of a file or directory as JSON.")
|
|
164
|
+
return
|
|
162
165
|
if len(sys.argv) < 2:
|
|
163
166
|
print(json.dumps({"error": "Usage: dependency-graph.py <file_or_directory>"}))
|
|
164
167
|
sys.exit(1)
|
|
@@ -182,6 +182,9 @@ def detect(project_dir: Path) -> dict[str, object]:
|
|
|
182
182
|
|
|
183
183
|
def main() -> None:
|
|
184
184
|
"""Entry point: detect migration tool and print JSON result to stdout."""
|
|
185
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
186
|
+
print("Usage: migration-status.py [project_dir]\n\nDetect the migration tool in project_dir (default: cwd) and report its status as JSON.")
|
|
187
|
+
return
|
|
185
188
|
project_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
|
|
186
189
|
if not project_dir.is_dir():
|
|
187
190
|
print(json.dumps({"error": f"Not a directory: {project_dir}"}))
|
|
@@ -154,6 +154,9 @@ def analyze_file(filepath: str, project_root: str) -> dict:
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def main():
|
|
157
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
158
|
+
print("Usage: refactor-scan.py <file_or_directory>\n\nScan a file or directory for refactoring candidates and print JSON.")
|
|
159
|
+
return
|
|
157
160
|
if len(sys.argv) < 2:
|
|
158
161
|
print(json.dumps({"error": "Usage: refactor-scan.py <file_or_directory>"}))
|
|
159
162
|
sys.exit(1)
|
package/bin/ai-toolkit.js
CHANGED
|
@@ -222,10 +222,14 @@ function runGenerator(scriptName, extraArgs = []) {
|
|
|
222
222
|
*/
|
|
223
223
|
function run(script, args = [], opts = {}) {
|
|
224
224
|
requirePython();
|
|
225
|
+
// Commands flagged toolkitCwd run with cwd inside the installed package so
|
|
226
|
+
// their relative asset paths resolve; the user's own directory travels in
|
|
227
|
+
// AI_TOOLKIT_USER_CWD so a relative --output still lands where they typed
|
|
228
|
+
// the command, not inside node_modules (v4.32.2).
|
|
225
229
|
const result = spawnSync('python3', [script, ...args], {
|
|
226
230
|
stdio: 'inherit',
|
|
227
231
|
cwd: opts.cwd || CWD,
|
|
228
|
-
env: { ...process.env },
|
|
232
|
+
env: { ...process.env, AI_TOOLKIT_USER_CWD: CWD },
|
|
229
233
|
});
|
|
230
234
|
if (result.status !== 0) {
|
|
231
235
|
process.exit(result.status || 1);
|
|
@@ -30,7 +30,7 @@ Usage: ai-toolkit <command> [options]
|
|
|
30
30
|
| `doctor` | Diagnose install health, hooks, quick-win assets, artifact drift, context budget (est. resident tokens of the skill/agent listings and user rules, plus skills with zero recorded use; read-only, prints the `skillOverrides` key to paste), and permission rules (`permissions.allow` wildcards on interpreters, task runners, package installs, `gh api`, `curl`, `git fetch`, destructive commands; warns only, never edits) |
|
|
31
31
|
| `doctor --fix` | Auto-repair broken symlinks, missing hooks, stale artifacts |
|
|
32
32
|
| `eject [dir]` | Export standalone config (no symlinks, no toolkit dependency) |
|
|
33
|
-
| `claude-app export [--output FILE] [--no-custom-rules] [--verify]` | Build an uploadable Claude Chat/Desktop/Cowork plugin ZIP and global-instructions file |
|
|
33
|
+
| `claude-app export [--output FILE] [--no-custom-rules] [--verify]` | Writes to the directory you run the command in (relative `--output` and the default `ai-toolkit-claude-app.zip` + `*-global-instructions.md`; before v4.32.2 they landed inside `node_modules/@softspark/ai-toolkit`). Build an uploadable Claude Chat/Desktop/Cowork plugin ZIP and global-instructions file |
|
|
34
34
|
| `claude-app verify` | Validate a clean staged plugin with structural checks and the official Claude plugin validator |
|
|
35
35
|
| `codex-plugin export [--output FILE]` | Build a deterministic native Codex plugin ZIP with skills and self-contained hooks |
|
|
36
36
|
| `codex-plugin verify` | Validate a clean native Codex plugin stage without installing it or changing user configuration |
|
|
@@ -3,9 +3,9 @@ title: "Plugin Pack Conventions"
|
|
|
3
3
|
category: reference
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.4.0"
|
|
7
7
|
created: "2026-03-28"
|
|
8
|
-
last_updated: "2026-
|
|
8
|
+
last_updated: "2026-09-04"
|
|
9
9
|
description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across supported editors."
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -167,7 +167,7 @@ ai-toolkit plugin status --editor all # show installed packs with r
|
|
|
167
167
|
1. **Claude Code target**: strips plugin hook entries from `~/.claude/settings.json` and removes plugin-local rule sections from `~/.claude/CLAUDE.md`
|
|
168
168
|
2. **Codex target**: strips only command handlers carrying the exact `AI_TOOLKIT_HOOK_OWNER=ai-toolkit-plugin-<pack>` marker from `$CODEX_HOME/hooks.json`, removes owned `$CODEX_HOME/ai-toolkit-hooks/plugin-<pack>-*` assets, and removes the pack's marker-bounded sections from `$CODEX_HOME/AGENTS.md`
|
|
169
169
|
3. **Cursor/Gemini rules**: removes only exact content recorded in `rule_ownership`; foreign or user-modified files/sections are preserved with a warning
|
|
170
|
-
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack
|
|
170
|
+
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack, and only when the file on disk is byte-, mode- and inode-identical to what install recorded in `shared_asset_ownership`; a file the user edited is preserved and named in a `WARN preserved changed plugin asset` line. Before v4.32.1 the Claude and Codex install paths never recorded that ownership, so removal preserved every hook and script as "untracked" and each pack left 4-5 files behind (found by the post-release SOP on v4.32.0)
|
|
171
171
|
5. **Updates** `plugins.json` state per runtime
|
|
172
172
|
6. **Leaves** core agents/skills untouched (they belong to the base install)
|
|
173
173
|
7. **Leaves** plugin data intact (e.g. `memory.db` — use `clean` to prune)
|
package/llms-full.txt
CHANGED
|
@@ -10482,7 +10482,7 @@ Usage: ai-toolkit <command> [options]
|
|
|
10482
10482
|
| `doctor` | Diagnose install health, hooks, quick-win assets, artifact drift, context budget (est. resident tokens of the skill/agent listings and user rules, plus skills with zero recorded use; read-only, prints the `skillOverrides` key to paste), and permission rules (`permissions.allow` wildcards on interpreters, task runners, package installs, `gh api`, `curl`, `git fetch`, destructive commands; warns only, never edits) |
|
|
10483
10483
|
| `doctor --fix` | Auto-repair broken symlinks, missing hooks, stale artifacts |
|
|
10484
10484
|
| `eject [dir]` | Export standalone config (no symlinks, no toolkit dependency) |
|
|
10485
|
-
| `claude-app export [--output FILE] [--no-custom-rules] [--verify]` | Build an uploadable Claude Chat/Desktop/Cowork plugin ZIP and global-instructions file |
|
|
10485
|
+
| `claude-app export [--output FILE] [--no-custom-rules] [--verify]` | Writes to the directory you run the command in (relative `--output` and the default `ai-toolkit-claude-app.zip` + `*-global-instructions.md`; before v4.32.2 they landed inside `node_modules/@softspark/ai-toolkit`). Build an uploadable Claude Chat/Desktop/Cowork plugin ZIP and global-instructions file |
|
|
10486
10486
|
| `claude-app verify` | Validate a clean staged plugin with structural checks and the official Claude plugin validator |
|
|
10487
10487
|
| `codex-plugin export [--output FILE]` | Build a deterministic native Codex plugin ZIP with skills and self-contained hooks |
|
|
10488
10488
|
| `codex-plugin verify` | Validate a clean native Codex plugin stage without installing it or changing user configuration |
|
|
@@ -15855,9 +15855,9 @@ title: "Plugin Pack Conventions"
|
|
|
15855
15855
|
category: reference
|
|
15856
15856
|
service: ai-toolkit
|
|
15857
15857
|
tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
|
|
15858
|
-
version: "1.
|
|
15858
|
+
version: "1.4.0"
|
|
15859
15859
|
created: "2026-03-28"
|
|
15860
|
-
last_updated: "2026-
|
|
15860
|
+
last_updated: "2026-09-04"
|
|
15861
15861
|
description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across supported editors."
|
|
15862
15862
|
---
|
|
15863
15863
|
|
|
@@ -16019,7 +16019,7 @@ ai-toolkit plugin status --editor all # show installed packs with r
|
|
|
16019
16019
|
1. **Claude Code target**: strips plugin hook entries from `~/.claude/settings.json` and removes plugin-local rule sections from `~/.claude/CLAUDE.md`
|
|
16020
16020
|
2. **Codex target**: strips only command handlers carrying the exact `AI_TOOLKIT_HOOK_OWNER=ai-toolkit-plugin-<pack>` marker from `$CODEX_HOME/hooks.json`, removes owned `$CODEX_HOME/ai-toolkit-hooks/plugin-<pack>-*` assets, and removes the pack's marker-bounded sections from `$CODEX_HOME/AGENTS.md`
|
|
16021
16021
|
3. **Cursor/Gemini rules**: removes only exact content recorded in `rule_ownership`; foreign or user-modified files/sections are preserved with a warning
|
|
16022
|
-
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack
|
|
16022
|
+
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack, and only when the file on disk is byte-, mode- and inode-identical to what install recorded in `shared_asset_ownership`; a file the user edited is preserved and named in a `WARN preserved changed plugin asset` line. Before v4.32.1 the Claude and Codex install paths never recorded that ownership, so removal preserved every hook and script as "untracked" and each pack left 4-5 files behind (found by the post-release SOP on v4.32.0)
|
|
16023
16023
|
5. **Updates** `plugins.json` state per runtime
|
|
16024
16024
|
6. **Leaves** core agents/skills untouched (they belong to the base install)
|
|
16025
16025
|
7. **Leaves** plugin data intact (e.g. `memory.db` — use `clean` to prune)
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.32.
|
|
3
|
+
"version": "4.32.2",
|
|
4
4
|
"description": "AI coding toolkit: 109 skills, 44 agents, 14 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -22,6 +22,7 @@ from typing import Any
|
|
|
22
22
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
23
|
|
|
24
24
|
from dir_rules_shared import STANDARD_RULES
|
|
25
|
+
from paths import user_path
|
|
25
26
|
from emission import agents_dir
|
|
26
27
|
from generate_antigravity_agents import render_agent
|
|
27
28
|
from generate_antigravity_hooks import (
|
|
@@ -550,7 +551,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
550
551
|
or args.output_path
|
|
551
552
|
or "ai-toolkit-antigravity-plugin.zip"
|
|
552
553
|
)
|
|
553
|
-
path = export(output)
|
|
554
|
+
path = export(user_path(output))
|
|
554
555
|
print(f"Created: {path}")
|
|
555
556
|
print(
|
|
556
557
|
"Install under .agents/plugins/ai-toolkit/ (workspace), "
|
package/scripts/claude_app.py
CHANGED
|
@@ -29,6 +29,7 @@ from pathlib import Path
|
|
|
29
29
|
|
|
30
30
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
31
31
|
from frontmatter import split_frontmatter # noqa: E402
|
|
32
|
+
from paths import user_path # noqa: E402
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
TOOLKIT_DIR = Path(__file__).resolve().parent.parent
|
|
@@ -347,7 +348,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
347
348
|
return 0 if verify_plugin() else 1
|
|
348
349
|
if args.action == "export":
|
|
349
350
|
return 0 if export_plugin(
|
|
350
|
-
|
|
351
|
+
user_path(args.output),
|
|
351
352
|
include_custom_rules=not args.no_custom_rules,
|
|
352
353
|
verify=args.verify,
|
|
353
354
|
) else 1
|
package/scripts/codex_plugin.py
CHANGED
|
@@ -26,6 +26,7 @@ from typing import Any
|
|
|
26
26
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
27
|
|
|
28
28
|
from codex_skill_adapter import build_codex_skill_text
|
|
29
|
+
from paths import user_path
|
|
29
30
|
from generate_codex_hooks import (
|
|
30
31
|
CODEX_HOOKS,
|
|
31
32
|
_asset_names,
|
|
@@ -754,7 +755,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
754
755
|
def main(argv: list[str] | None = None) -> int:
|
|
755
756
|
args = parse_args(argv)
|
|
756
757
|
if args.action == "export":
|
|
757
|
-
return 0 if export_plugin(
|
|
758
|
+
return 0 if export_plugin(user_path(args.output)) else 1
|
|
758
759
|
if args.action == "verify":
|
|
759
760
|
return 0 if verify_plugin() else 1
|
|
760
761
|
return 2
|
package/scripts/paths.py
CHANGED
|
@@ -55,3 +55,21 @@ LEGACY_PROJECT_LOCK = ".ai-toolkit.lock.json"
|
|
|
55
55
|
|
|
56
56
|
# Base config filename (published in npm packages — unchanged)
|
|
57
57
|
BASE_CONFIG_FILENAME = "ai-toolkit.config.json"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def user_path(raw: "str | os.PathLike[str]") -> Path:
|
|
61
|
+
"""Resolve a user-supplied path against the directory the user ran the CLI in.
|
|
62
|
+
|
|
63
|
+
``bin/ai-toolkit.js`` runs some commands (``claude-app``, ``codex-plugin``,
|
|
64
|
+
``antigravity-plugin``) with ``cwd`` inside the installed package so their
|
|
65
|
+
relative asset paths resolve, and passes the shell's own directory as
|
|
66
|
+
``AI_TOOLKIT_USER_CWD``. Before v4.32.2 a relative ``--output`` (including
|
|
67
|
+
every default) was written next to ``node_modules/@softspark/ai-toolkit``.
|
|
68
|
+
Absolute paths pass through; direct ``python3 scripts/...`` runs, where the
|
|
69
|
+
variable is unset, resolve against the real cwd as before.
|
|
70
|
+
"""
|
|
71
|
+
path = Path(raw).expanduser()
|
|
72
|
+
if path.is_absolute():
|
|
73
|
+
return path
|
|
74
|
+
base = os.environ.get("AI_TOOLKIT_USER_CWD", "").strip()
|
|
75
|
+
return (Path(base) if base else Path.cwd()) / path
|
package/scripts/plugin.py
CHANGED
|
@@ -2006,6 +2006,16 @@ def _apply_asset_install(
|
|
|
2006
2006
|
"inode": version.inode,
|
|
2007
2007
|
}
|
|
2008
2008
|
print(f" Installed plugin {spec.kind}: {spec.path.name}")
|
|
2009
|
+
return _merge_asset_ownership(previous_ownership, name, editor, produced_entries)
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
def _merge_asset_ownership(
|
|
2013
|
+
previous_ownership: dict | None,
|
|
2014
|
+
name: str,
|
|
2015
|
+
editor: str,
|
|
2016
|
+
produced_entries: dict[str, dict],
|
|
2017
|
+
) -> dict:
|
|
2018
|
+
"""Fold one editor's freshly written assets into the pack's ownership record."""
|
|
2009
2019
|
entries = _asset_entries(previous_ownership, name)
|
|
2010
2020
|
entries.update(produced_entries)
|
|
2011
2021
|
consumers = _shared_asset_consumers(previous_ownership, name)
|
|
@@ -2022,6 +2032,37 @@ def _apply_asset_install(
|
|
|
2022
2032
|
}
|
|
2023
2033
|
|
|
2024
2034
|
|
|
2035
|
+
def _ownership_from_copied_assets(
|
|
2036
|
+
name: str,
|
|
2037
|
+
editor: str,
|
|
2038
|
+
pack_dir: Path,
|
|
2039
|
+
hook_specs: list[dict],
|
|
2040
|
+
previous_ownership: dict | None,
|
|
2041
|
+
) -> dict:
|
|
2042
|
+
"""Record what the non-transactional Claude/Codex install just copied.
|
|
2043
|
+
|
|
2044
|
+
``_copy_plugin_hook_scripts`` and ``_copy_plugin_scripts`` write the same
|
|
2045
|
+
paths ``_prepare_asset_specs`` describes but never recorded them, so
|
|
2046
|
+
removal had nothing to verify against and preserved every file as
|
|
2047
|
+
"untracked" (v4.32.1). Reading the versions back from disk after the copy
|
|
2048
|
+
gives removal the same sha256/mode/inode contract the JSON runtimes get.
|
|
2049
|
+
"""
|
|
2050
|
+
produced: dict[str, dict] = {}
|
|
2051
|
+
for spec in _prepare_asset_specs(name, pack_dir, hook_specs):
|
|
2052
|
+
version = _read_file_version(spec.path)
|
|
2053
|
+
if version is None:
|
|
2054
|
+
continue
|
|
2055
|
+
produced[spec.key] = {
|
|
2056
|
+
"path": str(spec.path),
|
|
2057
|
+
"kind": spec.kind,
|
|
2058
|
+
"sha256": hashlib.sha256(version.content).hexdigest(),
|
|
2059
|
+
"mode": version.mode,
|
|
2060
|
+
"device": version.device,
|
|
2061
|
+
"inode": version.inode,
|
|
2062
|
+
}
|
|
2063
|
+
return _merge_asset_ownership(previous_ownership, name, editor, produced)
|
|
2064
|
+
|
|
2065
|
+
|
|
2025
2066
|
def _preflight_asset_removal(
|
|
2026
2067
|
transaction: PluginFileTransaction,
|
|
2027
2068
|
ownership: dict | None,
|
|
@@ -2075,6 +2116,74 @@ def _apply_asset_removal(
|
|
|
2075
2116
|
print(f" WARN preserved changed or user-owned plugin asset: {path}")
|
|
2076
2117
|
|
|
2077
2118
|
|
|
2119
|
+
def _remove_owned_plugin_assets(state: dict | None, name: str, editor: str) -> None:
|
|
2120
|
+
"""Delete the hook and script files this pack installed for ``editor``.
|
|
2121
|
+
|
|
2122
|
+
Claude and Codex removal never consulted ``shared_asset_ownership``: the
|
|
2123
|
+
files were written on install, recorded, and then reported as "untracked"
|
|
2124
|
+
on remove because nothing looked the record up. Every pack left its hooks
|
|
2125
|
+
and ``plugin-scripts/<name>/`` behind (v4.32.1).
|
|
2126
|
+
|
|
2127
|
+
Same rules as the transactional runtimes: an entry is deleted only when
|
|
2128
|
+
this editor consumes it, no other editor still does, and the file on disk
|
|
2129
|
+
is byte- and inode-identical to what install recorded. Anything else
|
|
2130
|
+
(user edits, a file install never wrote, a symlink) is preserved and named.
|
|
2131
|
+
"""
|
|
2132
|
+
ownership = _shared_asset_ownership_for(state or {}, name)
|
|
2133
|
+
entries = _asset_entries(ownership, name)
|
|
2134
|
+
consumers = _shared_asset_consumers(ownership, name)
|
|
2135
|
+
editor_keys = set(consumers.get(editor, ()))
|
|
2136
|
+
other_keys = {
|
|
2137
|
+
key
|
|
2138
|
+
for consumer, keys in consumers.items()
|
|
2139
|
+
if consumer != editor
|
|
2140
|
+
for key in keys
|
|
2141
|
+
}
|
|
2142
|
+
scripts_root = TOOLKIT_DATA_DIR / "plugin-scripts" / name
|
|
2143
|
+
hook_prefix = f"plugin-{name}-"
|
|
2144
|
+
handled: set[Path] = set()
|
|
2145
|
+
|
|
2146
|
+
for key, entry in entries.items():
|
|
2147
|
+
raw_path = entry.get("path")
|
|
2148
|
+
if not isinstance(raw_path, str):
|
|
2149
|
+
continue
|
|
2150
|
+
path = Path(raw_path)
|
|
2151
|
+
handled.add(path.absolute())
|
|
2152
|
+
if key not in editor_keys or key in other_keys:
|
|
2153
|
+
continue
|
|
2154
|
+
kind = entry.get("kind")
|
|
2155
|
+
if kind == "hook":
|
|
2156
|
+
safe = path.parent.absolute() == HOOKS_DIR.absolute() and path.name.startswith(hook_prefix)
|
|
2157
|
+
elif kind == "script":
|
|
2158
|
+
safe = path.parent.absolute() == scripts_root.absolute()
|
|
2159
|
+
else:
|
|
2160
|
+
safe = False
|
|
2161
|
+
if not safe:
|
|
2162
|
+
print(f" WARN refused to remove plugin asset outside its owned root: {path}")
|
|
2163
|
+
continue
|
|
2164
|
+
if path.is_symlink():
|
|
2165
|
+
print(f" WARN preserved symlinked plugin asset: {path}")
|
|
2166
|
+
continue
|
|
2167
|
+
version = _read_file_version(path)
|
|
2168
|
+
if version is None:
|
|
2169
|
+
continue
|
|
2170
|
+
if not _asset_entry_matches(path, version, entry):
|
|
2171
|
+
print(f" WARN preserved changed plugin asset: {path}")
|
|
2172
|
+
continue
|
|
2173
|
+
path.unlink()
|
|
2174
|
+
print(f" Removed plugin asset: {path.name}")
|
|
2175
|
+
|
|
2176
|
+
for hook in sorted(HOOKS_DIR.glob(f"{hook_prefix}*")):
|
|
2177
|
+
if hook.absolute() not in handled:
|
|
2178
|
+
print(f" WARN preserved untracked plugin hook: {hook}")
|
|
2179
|
+
if scripts_root.is_dir():
|
|
2180
|
+
for leftover in sorted(scripts_root.iterdir()):
|
|
2181
|
+
if leftover.absolute() not in handled:
|
|
2182
|
+
print(f" WARN preserved untracked plugin script: {leftover}")
|
|
2183
|
+
if not any(scripts_root.iterdir()):
|
|
2184
|
+
scripts_root.rmdir()
|
|
2185
|
+
|
|
2186
|
+
|
|
2078
2187
|
def _state_after_removal(
|
|
2079
2188
|
state: dict,
|
|
2080
2189
|
editor: str,
|
|
@@ -2616,19 +2725,15 @@ def _remove_claude_pack_links(pack: dict, pack_dir: Path) -> None:
|
|
|
2616
2725
|
|
|
2617
2726
|
|
|
2618
2727
|
def remove_pack_claude(
|
|
2619
|
-
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool
|
|
2728
|
+
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool,
|
|
2729
|
+
state: dict | None = None,
|
|
2620
2730
|
) -> bool:
|
|
2621
2731
|
hook_specs = _resolve_pack_hooks(pack, pack_dir)
|
|
2622
2732
|
rule_specs = _resolve_pack_rules(pack, pack_dir)
|
|
2623
2733
|
_remove_claude_pack_links(pack, pack_dir)
|
|
2624
2734
|
|
|
2625
2735
|
if not keep_shared_assets:
|
|
2626
|
-
|
|
2627
|
-
print(f" WARN preserved untracked plugin hook: {hook}")
|
|
2628
|
-
|
|
2629
|
-
scripts_dir = TOOLKIT_DATA_DIR / "plugin-scripts" / name
|
|
2630
|
-
if scripts_dir.is_dir():
|
|
2631
|
-
print(f" WARN preserved untracked plugin scripts: {scripts_dir}")
|
|
2736
|
+
_remove_owned_plugin_assets(state, name, "claude")
|
|
2632
2737
|
|
|
2633
2738
|
if any(not spec["is_core"] for spec in hook_specs):
|
|
2634
2739
|
_strip_claude_hooks(name)
|
|
@@ -3000,7 +3105,8 @@ def install_pack_codex(name: str, pack: dict, pack_dir: Path) -> bool:
|
|
|
3000
3105
|
|
|
3001
3106
|
|
|
3002
3107
|
def remove_pack_codex(
|
|
3003
|
-
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool
|
|
3108
|
+
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool,
|
|
3109
|
+
state: dict | None = None,
|
|
3004
3110
|
) -> bool:
|
|
3005
3111
|
_assert_safe_codex_surface()
|
|
3006
3112
|
_strip_codex_hooks(name)
|
|
@@ -3016,14 +3122,10 @@ def remove_pack_codex(
|
|
|
3016
3122
|
print(f" WARN preserved user-owned Codex hook: {hook.name}")
|
|
3017
3123
|
|
|
3018
3124
|
if not keep_shared_assets:
|
|
3019
|
-
#
|
|
3020
|
-
#
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
scripts_dir = TOOLKIT_DATA_DIR / "plugin-scripts" / name
|
|
3025
|
-
if scripts_dir.is_dir():
|
|
3026
|
-
print(f" WARN preserved untracked plugin scripts: {scripts_dir}")
|
|
3125
|
+
# Paths used by releases before native Codex plugin assets moved under
|
|
3126
|
+
# $CODEX_HOME. Claude still owns these when installed for both, which
|
|
3127
|
+
# the ownership consumers encode; only Codex-consumed entries go.
|
|
3128
|
+
_remove_owned_plugin_assets(state, name, "codex")
|
|
3027
3129
|
|
|
3028
3130
|
_remove_codex_rules(name)
|
|
3029
3131
|
print(f" Done: removed {name} from codex")
|
|
@@ -3202,6 +3304,14 @@ def _install_pack_locked(
|
|
|
3202
3304
|
if transaction_snapshots is not None:
|
|
3203
3305
|
_restore_file_snapshots(transaction_snapshots)
|
|
3204
3306
|
return False
|
|
3307
|
+
if editor in ("claude", "codex"):
|
|
3308
|
+
asset_ownership = _ownership_from_copied_assets(
|
|
3309
|
+
name,
|
|
3310
|
+
editor,
|
|
3311
|
+
pack_dir,
|
|
3312
|
+
_resolve_pack_hooks(pack, pack_dir),
|
|
3313
|
+
_shared_asset_ownership_for(state, name),
|
|
3314
|
+
)
|
|
3205
3315
|
|
|
3206
3316
|
if hook_config_update is not None:
|
|
3207
3317
|
if transaction_snapshots is not None:
|
|
@@ -3362,6 +3472,7 @@ def _remove_pack_locked(
|
|
|
3362
3472
|
pack,
|
|
3363
3473
|
pack_dir,
|
|
3364
3474
|
keep_shared_assets=keep_shared_assets,
|
|
3475
|
+
state=state,
|
|
3365
3476
|
)
|
|
3366
3477
|
elif editor == "codex":
|
|
3367
3478
|
ok = remove_pack_codex(
|
|
@@ -3369,6 +3480,7 @@ def _remove_pack_locked(
|
|
|
3369
3480
|
pack,
|
|
3370
3481
|
pack_dir,
|
|
3371
3482
|
keep_shared_assets=keep_shared_assets,
|
|
3483
|
+
state=state,
|
|
3372
3484
|
)
|
|
3373
3485
|
elif editor in JSON_HOOK_RUNTIMES:
|
|
3374
3486
|
ok = remove_pack_json_runtime(
|