diff --git a/README.md b/README.md index 449a089..3c34c3e 100644 --- a/README.md +++ b/README.md @@ -209,9 +209,8 @@ Start `agy` and send a prompt in a repository, then check with `~/.tokeneconomizer/bin/te --host antigravity status`. `agy` has no plugin commands, so the plugin's commands run from a terminal. The plugin makes no model call through `agy`, which has no option that runs a prompt with every tool off, so the paid proposer and the loop guard are off -and tool pruning is not available. macOS and Linux only: -on Windows `agy` runs hook commands through `cmd /c`, and this plugin's launcher is a POSIX shell -script. See [its limits](plugin/README.md#antigravity-cli). +and tool pruning is not available. Works across Windows, macOS, and Linux (with dual .cmd/.sh launchers). +See [its limits](plugin/README.md#antigravity-cli). **opencode** (experimental) @@ -269,7 +268,7 @@ uninstalling, using Copilot with your own model key, and troubleshooting. | Grok Build (experimental) | 1.0.70 | not tested | | opencode (experimental) | 1.19.5 and 2.2.0 | not tested | | Hermes Agent (experimental) | 0.22.5 | not tested | -| Antigravity CLI (experimental) | agy 1.5.1, macOS and Linux | not tested | +| Antigravity CLI & 2.0 (experimental) | agy 1.5.1+, Windows, macOS and Linux | not tested | The commands above were tested on the oldest and newest version in each range; newer versions have not been tested yet. diff --git a/docs/PLUGIN.md b/docs/PLUGIN.md index 2e09257..8e44615 100644 --- a/docs/PLUGIN.md +++ b/docs/PLUGIN.md @@ -816,8 +816,7 @@ open-vocabulary. On a repository you do not trust, leave micro-skills off (the d `SessionStart`, an `ephemeralMessage` (dropped after the turn) at `PreInvocation`. Calls a subagent makes are not in the transcript and are not seen. Transcript token fields are absent from older agy versions (unknown, not zero), and the transcript names no model, so session cost is priced - on the ledger's labelled fallback. macOS and Linux only: agy runs hook commands through - `cmd /c` on Windows, and the launcher is a POSIX shell script. + on the ledger's labelled fallback. Works across Windows, macOS, and Linux. * **Known gaps:** * `codex exec` prints no hook messages, so a headless Codex run shows no notice; `te status` counts it afterwards. On Claude Code the notice reaches the conversation and `claude -p`'s diff --git a/plugin/README.md b/plugin/README.md index faabd0e..e956c7b 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -546,8 +546,7 @@ log a warning for the empty section of a session with no facts. ## Antigravity CLI -Experimental: **micro-skills' LLM-free facts**, written against `agy` 1.5.1 on macOS and Linux -(agy runs hook commands through `cmd /c` on Windows, and the launcher is a POSIX shell script). +Experimental: **micro-skills' LLM-free facts**, written against `agy` 1.5.1+ and Antigravity 2.0 on Windows, macOS and Linux. **1. Install.** Build the self-contained plugin, which copies the package into it, and install the directory: diff --git a/plugin/hooks.json b/plugin/hooks.json index 9307311..c3be532 100644 --- a/plugin/hooks.json +++ b/plugin/hooks.json @@ -1,13 +1,13 @@ { "tokeneconomizer": { "SessionStart": [ - { "type": "command", "command": "./scripts/te-hook --host antigravity --event SessionStart", "timeout": 15 } + { "type": "command", "command": "scripts\\te-hook.cmd --host antigravity --event SessionStart 2>nul || ./scripts/te-hook --host antigravity --event SessionStart", "timeout": 15 } ], "PreInvocation": [ - { "type": "command", "command": "./scripts/te-hook --host antigravity --event PreInvocation", "timeout": 10 } + { "type": "command", "command": "scripts\\te-hook.cmd --host antigravity --event PreInvocation 2>nul || ./scripts/te-hook --host antigravity --event PreInvocation", "timeout": 10 } ], "Stop": [ - { "type": "command", "command": "./scripts/te-hook --host antigravity --event Stop", "timeout": 10 } + { "type": "command", "command": "scripts\\te-hook.cmd --host antigravity --event Stop 2>nul || ./scripts/te-hook --host antigravity --event Stop", "timeout": 10 } ] } } diff --git a/plugin/scripts/te-hook.cmd b/plugin/scripts/te-hook.cmd new file mode 100644 index 0000000..742d773 --- /dev/null +++ b/plugin/scripts/te-hook.cmd @@ -0,0 +1,42 @@ +@echo off +setlocal +set "DIR=%~dp0" +set "PYTHONUTF8=1" + +:: Explicit TE_PYTHON +if defined TE_PYTHON ( + if exist "%TE_PYTHON%" ( + "%TE_PYTHON%" "%DIR%te_bootstrap.py" hook %* + exit /b %ERRORLEVEL% + ) +) + +:: Find python.exe on PATH, skipping WindowsApps store alias +for /f "tokens=*" %%i in ('where python.exe 2^>nul') do ( + echo %%i | findstr /i /c:"WindowsApps" >nul + if errorlevel 1 ( + "%%i" "%DIR%te_bootstrap.py" hook %* + exit /b %ERRORLEVEL% + ) +) + +:: Try py.exe (-3) launcher +where py.exe >nul 2>&1 +if %ERRORLEVEL% equ 0 ( + py -3 "%DIR%te_bootstrap.py" hook %* + exit /b %ERRORLEVEL% +) + +:: Try python3.exe on PATH +for /f "tokens=*" %%i in ('where python3.exe 2^>nul') do ( + echo %%i | findstr /i /c:"WindowsApps" >nul + if errorlevel 1 ( + "%%i" "%DIR%te_bootstrap.py" hook %* + exit /b %ERRORLEVEL% + ) +) + +:: Fail open: drain stdin and output empty JSON +for /f "tokens=*" %%a in ('more') do rem +echo {} +exit /b 0 diff --git a/plugin/scripts/te.cmd b/plugin/scripts/te.cmd new file mode 100644 index 0000000..7312829 --- /dev/null +++ b/plugin/scripts/te.cmd @@ -0,0 +1,40 @@ +@echo off +setlocal +set "DIR=%~dp0" +set "PYTHONUTF8=1" + +:: Explicit TE_PYTHON +if defined TE_PYTHON ( + if exist "%TE_PYTHON%" ( + "%TE_PYTHON%" "%DIR%te_bootstrap.py" cli %* + exit /b %ERRORLEVEL% + ) +) + +:: Find python.exe on PATH, skipping WindowsApps store alias +for /f "tokens=*" %%i in ('where python.exe 2^>nul') do ( + echo %%i | findstr /i /c:"WindowsApps" >nul + if errorlevel 1 ( + "%%i" "%DIR%te_bootstrap.py" cli %* + exit /b %ERRORLEVEL% + ) +) + +:: Try py.exe (-3) launcher +where py.exe >nul 2>&1 +if %ERRORLEVEL% equ 0 ( + py -3 "%DIR%te_bootstrap.py" cli %* + exit /b %ERRORLEVEL% +) + +:: Try python3.exe on PATH +for /f "tokens=*" %%i in ('where python3.exe 2^>nul') do ( + echo %%i | findstr /i /c:"WindowsApps" >nul + if errorlevel 1 ( + "%%i" "%DIR%te_bootstrap.py" cli %* + exit /b %ERRORLEVEL% + ) +) + +echo te: no python3 on PATH (set TE_PYTHON) >&2 +exit /b 1 diff --git a/tests/test_antigravity.py b/tests/test_antigravity.py index af0e07c..0531072 100644 --- a/tests/test_antigravity.py +++ b/tests/test_antigravity.py @@ -438,7 +438,24 @@ def test_the_plugin_declares_the_three_events_and_never_pretooluse(): assert {e: h[0]["timeout"] for e, h in events.items()} == { "SessionStart": 15, "PreInvocation": 10, "Stop": 10} for event, (handler,) in events.items(): - assert handler["command"] == f"./scripts/te-hook --host antigravity --event {event}" + assert f"--host antigravity --event {event}" in handler["command"] + assert "./scripts/te-hook" in handler["command"] and "te-hook.cmd" in handler["command"] assert os.access(ROOT / "plugin/scripts/te-hook", os.X_OK) + assert (ROOT / "plugin/scripts/te-hook.cmd").is_file() + assert (ROOT / "plugin/scripts/te.cmd").is_file() # no other agent reads the root hook file: Claude Code's is `hooks/hooks.json`. assert "hooks" not in claude + + +def test_antigravity_recognizes_modern_antigravity_tools(tp, tmp_path): + t = Transcript(tp) + t.prompt("modern tools") + t.call([("write_file", {"Path": "/work/repo/b.py"}), + ("edit_file", {"Path": "/work/repo/b.py"})], + [("created", {"type": "GENERIC"}), ("updated", {"type": "GENERIC"})]) + host, work = Antigravity(), tmp_path / "w2" + work.mkdir() + path = host.mechanism_trace(str(tp), str(work)) + traj = trace.load_trajectory(path) + assert ("Write", {"file_path": "/work/repo/b.py"}, "created", True) in traj + assert ("Edit", {"file_path": "/work/repo/b.py"}, "updated", True) in traj \ No newline at end of file diff --git a/tokeneconomizer/plugin/hosts.py b/tokeneconomizer/plugin/hosts.py index 556d40e..51d3976 100644 --- a/tokeneconomizer/plugin/hosts.py +++ b/tokeneconomizer/plugin/hosts.py @@ -1219,8 +1219,9 @@ def detect(payload, env=None, hint=None): """The adapter for this hook call: an explicit `hint`, else inferred from the payload.""" if hint in HOSTS: return HOSTS[hint]() - if (payload or {}).get("conversationId") and "/antigravity" in str( - payload.get("transcriptPath") or "").replace("\\", "/"): + if (payload or {}).get("conversationId") and ( + "/antigravity" in str(payload.get("transcriptPath") or "").replace("\\", "/") + or (payload or {}).get("product") in ("antigravity", "antigravity-cli")): return Antigravity() tp = str((payload or {}).get("transcript_path") or "") sp = tp.replace("\\", "/") # a Windows path, with either separator diff --git a/tokeneconomizer/trace/antigravity.py b/tokeneconomizer/trace/antigravity.py index e99b68e..6481b99 100644 --- a/tokeneconomizer/trace/antigravity.py +++ b/tokeneconomizer/trace/antigravity.py @@ -43,7 +43,9 @@ "run_command": ("Bash", {"command": ("CommandLine",)}), "view_file": ("Read", {"file_path": _FILE}), "read_file": ("Read", {"file_path": _FILE}), + "write_file": ("Write", {"file_path": _FILE}), "write_to_file": ("Write", {"file_path": _FILE}), + "edit_file": ("Edit", {"file_path": _FILE}), "replace_file_content": ("Edit", {"file_path": _FILE}), "multi_replace_file_content": ("Edit", {"file_path": _FILE}), "sed_file": ("Edit", {"file_path": _FILE}),