@softspark/ai-toolkit 4.15.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +117 -0
- package/CHANGELOG.md +43 -0
- package/README.md +19 -13
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/ARCHITECTURE.md +4 -3
- package/app/hooks/_hook-io.sh +18 -3
- package/app/hooks/ai-toolkit-statusline.sh +30 -5
- package/app/hooks/filter-tool-output.sh +76 -0
- package/app/hooks/governance-capture.sh +1 -1
- package/app/hooks/guard-path.sh +2 -2
- package/app/hooks/post-tool-use.sh +5 -3
- package/app/hooks/pre-compact-save.sh +4 -3
- package/app/hooks/quality-gate.sh +12 -1
- package/app/hooks/revert-guard.sh +5 -2
- package/app/hooks/save-session.sh +4 -2
- package/app/hooks/session-end.sh +36 -4
- package/app/hooks/session-start.sh +11 -5
- package/app/hooks.json +10 -0
- package/app/output-filter-policy.json +15 -0
- package/app/skills/brand-voice/scripts/measure.py +7 -5
- package/benchmarks/ecosystem-doctor-snapshot.json +22 -22
- package/benchmarks/output-filter/README.md +11 -0
- package/benchmarks/output-filter/scenarios.json +25 -0
- package/bin/ai-toolkit.js +2 -0
- package/kb/history/completed/native-tool-output-filter-plan.md +517 -0
- package/kb/procedures/release-preparation-sop.md +6 -5
- package/kb/reference/architecture-overview.md +6 -5
- package/kb/reference/cli-reference.md +19 -2
- package/kb/reference/codex-cli-compatibility.md +1 -0
- package/kb/reference/copilot-compatibility.md +173 -0
- package/kb/reference/enterprise-config-guide.md +28 -2
- package/kb/reference/global-install-model.md +6 -2
- package/kb/reference/hooks-catalog.md +105 -16
- package/kb/reference/opencode-compatibility.md +1 -0
- package/kb/reference/supported-tools-registry.md +10 -5
- package/kb/reference/tool-output-filter.md +288 -0
- package/kb/reference/windows-support.md +4 -3
- package/llms-full.txt +1182 -40
- package/llms.txt +3 -0
- package/manifest.json +9 -6
- package/package.json +3 -2
- package/scripts/benchmark_output_filter.py +343 -0
- package/scripts/check_deps.py +16 -0
- package/scripts/claude_app.py +30 -2
- package/scripts/config_cli.py +4 -4
- package/scripts/config_lock.py +120 -14
- package/scripts/config_merger.py +103 -20
- package/scripts/config_resolver.py +22 -2
- package/scripts/config_validator.py +268 -16
- package/scripts/copilot_legacy_hashes.json +338 -0
- package/scripts/doctor.py +1 -0
- package/scripts/generate_codex_hooks.py +2 -0
- package/scripts/generate_copilot.py +464 -71
- package/scripts/generate_copilot_hooks.py +124 -7
- package/scripts/generate_gemini_hooks.py +33 -10
- package/scripts/generate_opencode_plugin.py +28 -12
- package/scripts/install_steps/ai_tools.py +115 -3
- package/scripts/install_steps/hooks.py +25 -1
- package/scripts/output_filter_cli.py +347 -0
- package/scripts/output_filter_hook.py +23 -0
- package/scripts/plugin_schema.py +27 -1
- package/scripts/schemas/ai-toolkit-config.schema.json +83 -5
- package/scripts/session_state.py +156 -42
- package/scripts/tool_output_filter/__init__.py +33 -0
- package/scripts/tool_output_filter/contracts.py +173 -0
- package/scripts/tool_output_filter/engine.py +260 -0
- package/scripts/tool_output_filter/hook_runtime.py +369 -0
- package/scripts/tool_output_filter/input.py +56 -0
- package/scripts/tool_output_filter/invariants.py +40 -0
- package/scripts/tool_output_filter/policy.py +153 -0
- package/scripts/tool_output_filter/profiles/__init__.py +68 -0
- package/scripts/tool_output_filter/profiles/repeat_lines.py +71 -0
- package/scripts/tool_output_filter/profiles/tap_success.py +154 -0
- package/scripts/tool_output_filter/recovery.py +846 -0
- package/scripts/tool_output_filter/telemetry.py +13 -0
- package/scripts/uninstall.py +96 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Metadata-only telemetry boundary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .contracts import FilterTelemetry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TelemetrySink:
|
|
9
|
+
"""Receives content-free filter decision events."""
|
|
10
|
+
|
|
11
|
+
def record(self, event: FilterTelemetry) -> None:
|
|
12
|
+
"""Record one event without raising into the filter path."""
|
|
13
|
+
raise NotImplementedError
|
package/scripts/uninstall.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Safely remove ai-toolkit-managed runtime customizations.
|
|
2
|
+
"""Safely remove ai-toolkit-managed runtime data and customizations.
|
|
3
3
|
|
|
4
4
|
The default scope is the current user's global install. ``--local`` targets a
|
|
5
5
|
project, while an explicit legacy positional target scans both project and
|
|
6
6
|
home-style locations for backward compatibility. Only files, symlinks, JSON
|
|
7
7
|
handlers, and marker blocks with verifiable ai-toolkit ownership are removed.
|
|
8
|
+
Global scope also removes validated output-filter recovery files while keeping
|
|
9
|
+
foreign content in the same session trees.
|
|
8
10
|
|
|
9
11
|
Usage:
|
|
10
12
|
python3 scripts/uninstall.py [--yes] [--local|--global] [--target DIR]
|
|
@@ -31,6 +33,8 @@ from typing import Any
|
|
|
31
33
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
32
34
|
from _common import app_dir, toolkit_dir
|
|
33
35
|
from injection import strip_all_sections, strip_section, trim_trailing_blanks
|
|
36
|
+
from tool_output_filter.recovery import clean_owned_recovery_tree
|
|
37
|
+
from tool_output_filter.recovery import count_owned_recovery_artifacts
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
CODEX_AGENT_MARKER = "# ai-toolkit-managed: codex-agent"
|
|
@@ -555,6 +559,45 @@ def _discover_claude_hooks(claude_dir: Path) -> list[tuple[str, str]]:
|
|
|
555
559
|
return []
|
|
556
560
|
|
|
557
561
|
|
|
562
|
+
_OUTPUT_FILTER_POLICY_NAME = "ai-toolkit-output-filter.json"
|
|
563
|
+
_OUTPUT_FILTER_OWNER_NAME = ".ai-toolkit-output-filter.owner"
|
|
564
|
+
_OUTPUT_FILTER_OWNER_MARKER = b"ai-toolkit-output-filter-policy-v1\n"
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _managed_output_filter_policy(claude_dir: Path) -> list[Path]:
|
|
568
|
+
"""Return the managed per-project policy pair, or [] when unmanaged."""
|
|
569
|
+
policy = claude_dir / _OUTPUT_FILTER_POLICY_NAME
|
|
570
|
+
owner = claude_dir / _OUTPUT_FILTER_OWNER_NAME
|
|
571
|
+
if owner.is_symlink() or not owner.is_file():
|
|
572
|
+
return []
|
|
573
|
+
try:
|
|
574
|
+
if owner.read_bytes() != _OUTPUT_FILTER_OWNER_MARKER:
|
|
575
|
+
return []
|
|
576
|
+
except OSError:
|
|
577
|
+
return []
|
|
578
|
+
managed = [owner]
|
|
579
|
+
if not policy.is_symlink() and policy.is_file():
|
|
580
|
+
managed.insert(0, policy)
|
|
581
|
+
return managed
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _discover_output_filter_policy(claude_dir: Path) -> list[tuple[str, str]]:
|
|
585
|
+
if not _managed_output_filter_policy(claude_dir):
|
|
586
|
+
return []
|
|
587
|
+
return [(
|
|
588
|
+
f"Managed: {_OUTPUT_FILTER_POLICY_NAME} (project output-filter policy)",
|
|
589
|
+
"output-filter-policy",
|
|
590
|
+
)]
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _remove_output_filter_policy(claude_dir: Path, trusted_root: Path) -> None:
|
|
594
|
+
managed = _managed_output_filter_policy(claude_dir)
|
|
595
|
+
for path in managed:
|
|
596
|
+
_safe_unlink(path, trusted_root)
|
|
597
|
+
if managed:
|
|
598
|
+
print(f" Removed: .claude/{_OUTPUT_FILTER_POLICY_NAME} (managed)")
|
|
599
|
+
|
|
600
|
+
|
|
558
601
|
def _discover_claude_markers(claude_dir: Path) -> list[tuple[str, str]]:
|
|
559
602
|
found: list[tuple[str, str]] = []
|
|
560
603
|
for item in ("constitution.md", "ARCHITECTURE.md"):
|
|
@@ -579,6 +622,7 @@ def discover_components(claude_dir: Path) -> list[tuple[str, str]]:
|
|
|
579
622
|
found.append((f"Symlinks: skills/ ({len(skill_links)} toolkit directories)", "skill-link"))
|
|
580
623
|
found.extend(_discover_claude_hooks(claude_dir))
|
|
581
624
|
found.extend(_discover_claude_markers(claude_dir))
|
|
625
|
+
found.extend(_discover_output_filter_policy(claude_dir))
|
|
582
626
|
return found
|
|
583
627
|
|
|
584
628
|
|
|
@@ -663,6 +707,7 @@ def remove_components(claude_dir: Path, trusted_root: Path) -> None:
|
|
|
663
707
|
)
|
|
664
708
|
_remove_claude_hooks(claude_dir, trusted_root)
|
|
665
709
|
_remove_claude_markers(claude_dir, trusted_root)
|
|
710
|
+
_remove_output_filter_policy(claude_dir, trusted_root)
|
|
666
711
|
|
|
667
712
|
|
|
668
713
|
# ---------------------------------------------------------------------------
|
|
@@ -1087,6 +1132,36 @@ def _remove_copilot(surface: CopilotSurface) -> None:
|
|
|
1087
1132
|
# Scope resolution, safety preflight, CLI
|
|
1088
1133
|
# ---------------------------------------------------------------------------
|
|
1089
1134
|
|
|
1135
|
+
def _discover_recovery(sessions_root: Path) -> list[tuple[str, str]]:
|
|
1136
|
+
try:
|
|
1137
|
+
os.lstat(sessions_root)
|
|
1138
|
+
except FileNotFoundError:
|
|
1139
|
+
return []
|
|
1140
|
+
artifact_count = count_owned_recovery_artifacts(sessions_root)
|
|
1141
|
+
if artifact_count == 0:
|
|
1142
|
+
return []
|
|
1143
|
+
noun = "artifact" if artifact_count == 1 else "artifacts"
|
|
1144
|
+
return [
|
|
1145
|
+
(
|
|
1146
|
+
"Recovery: sessions/*/output-filter "
|
|
1147
|
+
f"({artifact_count} owned {noun}) under {sessions_root}",
|
|
1148
|
+
"output-filter-recovery",
|
|
1149
|
+
)
|
|
1150
|
+
]
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def _remove_recovery(sessions_root: Path) -> None:
|
|
1154
|
+
"""Remove owned recovery last so its failure rolls back other surfaces."""
|
|
1155
|
+
try:
|
|
1156
|
+
removed = clean_owned_recovery_tree(sessions_root)
|
|
1157
|
+
except OSError as error:
|
|
1158
|
+
raise RuntimeError(
|
|
1159
|
+
"output-filter recovery cleanup failed; an I/O fault after "
|
|
1160
|
+
"preflight may have left recovery cleanup partial"
|
|
1161
|
+
) from error
|
|
1162
|
+
print(f" Removed: {removed} output-filter recovery file(s)")
|
|
1163
|
+
|
|
1164
|
+
|
|
1090
1165
|
def _configured_home(env_name: str, fallback: Path, *, strict_absolute: bool) -> Path:
|
|
1091
1166
|
value = os.environ.get(env_name)
|
|
1092
1167
|
if not value:
|
|
@@ -1203,6 +1278,8 @@ def _transaction_specs(
|
|
|
1203
1278
|
(claude / "hooks.json", False),
|
|
1204
1279
|
(claude / "constitution.md", False),
|
|
1205
1280
|
(claude / "ARCHITECTURE.md", False),
|
|
1281
|
+
(claude / _OUTPUT_FILTER_POLICY_NAME, False),
|
|
1282
|
+
(claude / _OUTPUT_FILTER_OWNER_NAME, False),
|
|
1206
1283
|
):
|
|
1207
1284
|
add(path, recursive, claude_boundary)
|
|
1208
1285
|
for surface in codex:
|
|
@@ -1240,8 +1317,8 @@ def _transaction_specs(
|
|
|
1240
1317
|
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
1241
1318
|
parser = argparse.ArgumentParser(
|
|
1242
1319
|
description=(
|
|
1243
|
-
"Remove only ai-toolkit-managed Claude, Codex, and
|
|
1244
|
-
"
|
|
1320
|
+
"Remove only ai-toolkit-managed Claude, Codex, Copilot, and "
|
|
1321
|
+
"output-filter recovery data while preserving user-owned content."
|
|
1245
1322
|
),
|
|
1246
1323
|
epilog=(
|
|
1247
1324
|
"Global Codex and Copilot locations honor CODEX_HOME and "
|
|
@@ -1290,6 +1367,17 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1290
1367
|
components.extend(_discover_codex(surface))
|
|
1291
1368
|
for surface in copilot:
|
|
1292
1369
|
components.extend(_discover_copilot(surface))
|
|
1370
|
+
recovery_root = (
|
|
1371
|
+
target / ".softspark" / "ai-toolkit" / "sessions"
|
|
1372
|
+
if scope in {"global", "both"}
|
|
1373
|
+
else None
|
|
1374
|
+
)
|
|
1375
|
+
recovery_components = (
|
|
1376
|
+
_discover_recovery(recovery_root)
|
|
1377
|
+
if recovery_root is not None
|
|
1378
|
+
else []
|
|
1379
|
+
)
|
|
1380
|
+
components.extend(recovery_components)
|
|
1293
1381
|
except (OSError, RuntimeError, UnicodeError) as error:
|
|
1294
1382
|
print(f"Error: {error}", file=sys.stderr)
|
|
1295
1383
|
raise SystemExit(1) from error
|
|
@@ -1338,6 +1426,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1338
1426
|
for surface in copilot:
|
|
1339
1427
|
_preflight(target, claude, [], [surface])
|
|
1340
1428
|
_remove_copilot(surface)
|
|
1429
|
+
if recovery_root is not None and recovery_components:
|
|
1430
|
+
# The recovery API preflights its complete tree before the first
|
|
1431
|
+
# unlink. A later I/O fault can still leave recovery partially
|
|
1432
|
+
# cleaned; the transaction restores every other runtime surface.
|
|
1433
|
+
_remove_recovery(recovery_root)
|
|
1341
1434
|
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
|
|
1342
1435
|
rollback_error: RuntimeError | None = None
|
|
1343
1436
|
if transaction is not None:
|