cdx-manager 0.25.8 → 0.25.9
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/README.md +31 -6
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_9.md +22 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +27 -1
- package/checksums/release-archives.json +11 -0
- package/docs/cli.md +69 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/antigravity_status.py +161 -0
- package/src/claude_remote_token.py +45 -0
- package/src/cli.py +2 -0
- package/src/cli_args.py +2 -0
- package/src/commands/auth.py +25 -0
- package/src/commands/maintenance.py +4 -1
- package/src/commands/runs.py +16 -3
- package/src/interactive_usage.py +6 -12
- package/src/ollama_remote.py +175 -0
- package/src/provider_runtime.py +15 -0
- package/src/remote_run.py +69 -25
- package/src/remote_worker.py +85 -33
- package/src/run_command.py +9 -0
- package/src/run_registry.py +5 -0
- package/src/run_usage.py +63 -2
- package/src/session_service.py +4 -0
- package/src/session_status.py +16 -0
- package/src/status_view.py +1 -0
- package/src/update_all.py +9 -3
- package/src/workspace_sandbox.py +126 -6
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
|
@@ -89,7 +89,7 @@ Recovery does not require `--failover`, which still controls account quota exhau
|
|
|
89
89
|
|
|
90
90
|
Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). CDX Manager is [MIT licensed](LICENSE).
|
|
91
91
|
|
|
92
|
-
### Run Codex on an SSH worker
|
|
92
|
+
### Run Codex or Ollama on an SSH worker
|
|
93
93
|
|
|
94
94
|
```sh
|
|
95
95
|
cdx run work --remote agent@worker --cwd /srv/project --prompt 'Run the checks' --json
|
|
@@ -106,12 +106,12 @@ not synchronized. Use `--project` to explicitly attribute a remote checkout to a
|
|
|
106
106
|
project. Remote runs do not retry or fail over automatically, including when
|
|
107
107
|
`--failover` is supplied.
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
For Codex: a POSIX controller with `ssh` and Codex on PATH, and a Linux worker
|
|
110
110
|
with Python 3, memory-backed `/dev/shm`, and exactly the same `codex --version`.
|
|
111
111
|
Configure SSH keys, host trust and aliases in advance; the connection uses batch
|
|
112
112
|
mode and bounded connection/keepalive timeouts. Provider refusal, unreachable
|
|
113
|
-
workers and version mismatches fail before sending credentials.
|
|
114
|
-
|
|
113
|
+
workers and version mismatches fail before sending credentials. Codex requires an OAuth
|
|
114
|
+
profile. Remote continuation is refused because the temporary home
|
|
115
115
|
does not preserve conversation rollouts. Codex's native permission sandbox still
|
|
116
116
|
applies to ordinary remote runs.
|
|
117
117
|
|
|
@@ -123,7 +123,32 @@ Only a short, double-quoted Python bootstrap is sent in the SSH command line;
|
|
|
123
123
|
the worker and shared sandbox source are framed on stdin before the payload.
|
|
124
124
|
Neither source size nor prompt size consumes cmd.exe's 8191-character command limit.
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
Remote Ollama profiles also support headless text generation:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cdx run local --remote worker --cwd /srv/project --model qwen3:8b --prompt-file question.txt --isolate-workspace --json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Here `local` is an existing Ollama profile. The named model must already be
|
|
133
|
+
installed on the worker's unauthenticated `127.0.0.1:11434` Ollama API; CDX
|
|
134
|
+
never downloads a missing model and rejects cloud-backed models. The prompt
|
|
135
|
+
file is read on the launching host and sent as content over SSH. This path
|
|
136
|
+
uses `/api/show` and `/api/chat` with `stream: false`, without an Ollama CLI or
|
|
137
|
+
Codex installation. Results identify `execution_mode: "text_generation"` and
|
|
138
|
+
include generated text in `result`, plus `prompt_eval_count` / `eval_count`
|
|
139
|
+
and normalized input/output token counts in `usage`.
|
|
140
|
+
|
|
141
|
+
Ollama is not an autonomous coding agent and does not edit the repository.
|
|
142
|
+
With `--isolate-workspace`, the checkout is read-only and the client has its
|
|
143
|
+
own network namespace without network access, including when `--network-access`
|
|
144
|
+
is supplied. A private Unix socket connects it to a supervisor-owned relay
|
|
145
|
+
with the fixed loopback destination and only the selected model's capability
|
|
146
|
+
and non-streaming inference calls allowed. Pull, delete, push, arbitrary URLs,
|
|
147
|
+
proxying and redirects are refused. No `~/.ollama` directory or model files
|
|
148
|
+
are mounted. The relay is removed on completion or cancellation. Remote
|
|
149
|
+
continuation remains unsupported; an explicit model or profile model is required.
|
|
150
|
+
|
|
151
|
+
Codex `--remote` supports `--isolate-workspace` using the same local isolation mechanism:
|
|
127
152
|
bubblewrap, pasta (passt), nftables, Python, the checkout/toolchain allowlist,
|
|
128
153
|
and an actual namespace/network setup using a dummy credential. Missing dependencies,
|
|
129
154
|
namespace denial or invalid checkouts fail before sending real credentials, without
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.25.
|
|
1
|
+
0.25.9
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# CDX Manager 0.25.9
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-03
|
|
4
|
+
|
|
5
|
+
This release expands isolated remote provider support and improves Antigravity
|
|
6
|
+
quota, usage reporting, and update feedback.
|
|
7
|
+
|
|
8
|
+
## Remote runs
|
|
9
|
+
|
|
10
|
+
- Integrate worker-owned Antigravity headless runs with mandatory isolation, a volatile home, restricted inference networking, and redacted output. Keep worker credentials on the worker and preserve local profile authentication. Live worker acceptance measurements remain required before claiming production support. (#98)
|
|
11
|
+
- Enable isolated remote Claude runs with a dedicated OAuth setup token stored on the controller and transported through stdin. Use a fresh memory-backed home per attempt, restrict inference traffic by default, and retain usage reporting and output redaction. (#95)
|
|
12
|
+
- Add remote Ollama text generation with an explicitly selected installed worker model through a restricted local relay. Limit requests to model inspection and non-streaming chat; keep the checkout read-only and report generated text and token usage. This mode does not edit repositories or act as an autonomous coding agent. (#94)
|
|
13
|
+
|
|
14
|
+
## Antigravity status and updates
|
|
15
|
+
|
|
16
|
+
- Show Antigravity quota, reset times, and reading age from a local statusLine integration that preserves existing commands. Record native headless token counters, and identify stale or expired quota readings. (#96)
|
|
17
|
+
- Keep successful `agy update` commands successful when no newer version can be verified. Report an `update_unverified` warning while retaining unknown latest-version status; non-zero updater exits still fail. (#93)
|
|
18
|
+
|
|
19
|
+
## Maintenance
|
|
20
|
+
|
|
21
|
+
- Record tray companion checksums for v0.25.8. (`d0c8d33b671234cf648da2b21b7bb0bfb0ce302e`)
|
|
22
|
+
- Record release archive checksums for v0.25.8. (`20fb80c7d115500c8da5be433526e195063258d9`)
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
## Antigravity quota and run usage
|
|
4
|
+
|
|
5
|
+
- Install a local Antigravity statusLine command in new profiles and on interactive launch, merging settings and relaying the original stdin to any existing command so its output is preserved. Store only the reading time, plan tier and allowlisted quota fields in an atomic mode-600 profile file; never access Google internal endpoints or antigravity-oauth-token.
|
|
6
|
+
- Show Gemini 5-hour and weekly remaining quota, reset times and reading age in status. Include all bucket names, including third-party buckets, in JSON. Mark readings older than 24 hours stale and suppress availability after a bucket's reset has passed.
|
|
7
|
+
- Launch headless Antigravity with `agy -p --output-format json` and record its input, output, thinking, cache-read and reported total counters. Preserve the native total without assuming an undocumented cache or thinking subset relationship. Interactive quota is not per-run token usage.
|
|
8
|
+
- Cover synthetic native payloads, missing/unknown buckets, stale and expired readings, settings merges, existing command output, private file permissions and headless observation.
|
|
9
|
+
|
|
10
|
+
## Worker-owned Antigravity headless runs
|
|
11
|
+
|
|
12
|
+
- Allow Antigravity profiles in remote runs with mandatory bubblewrap, explicit `agy -p --output-format json`, an owner-only worker token mounted read-only, and a volatile HOME. Never transport worker credentials to the controller; redact worker output before SSH forwarding. Hold a worker token lock across each attempt and disable automatic CLI updates.
|
|
13
|
+
- Restrict inference to a conservative Google endpoint allowlist plus public DNS, including when tool network access is requested; retain private-range denial. The candidate list and read-only token policy require the live measurements documented in the mission fragment before claiming production acceptance.
|
|
14
|
+
- Fail on absent/unsafe worker login, expired sessions, missing/non-success JSON status and refused actions. Normalize JSON usage through `run_usage` and preserve existing run artifacts/history. Cover fake worker success, refusal, expiry, secret redaction, locking, cleanup, local profile authentication and mount/network construction.
|
|
15
|
+
|
|
16
|
+
## Remote Claude headless runs
|
|
17
|
+
|
|
18
|
+
- Add `cdx auth claude-token <profile>` to store a dedicated `claude setup-token` value from stdin in a controller-only mode-600 profile file. Missing or unsafe tokens stop remote runs with this command as the remedy; worker login is unnecessary.
|
|
19
|
+
- Allow remote Claude JSON headless execution with mandatory bubblewrap isolation, a fresh memory-backed HOME per attempt and no local profile mounts. Carry the token through SSH stdin and a sandbox stdin exec shim, exposing it only in Claude's environment, never worker credential files, argv or persisted output. Keep subprocess environment scrubbing, remove competing authentication/cloud/proxy sources, disable non-essential traffic and refuse `--bare`.
|
|
20
|
+
- Allow inference HTTPS to resolved `api.anthropic.com` addresses and DNS by default; retain public tool-network access only when requested. Cover fake-worker usage parsing, output redaction, absent/unsafe token files, stdin storage, isolation construction and volatile cleanup after success, failure and timeout.
|
|
21
|
+
|
|
3
22
|
## Isolated mission Git and RTK
|
|
4
23
|
|
|
5
24
|
- Give local and remote isolated linked worktrees a read-only Git view with their own HEAD/index and shared objects/refs, without traversing private host parents or exposing repository/global configuration, hooks, SSH keys or Git push credentials. Preserve supervisor-owned commits and pushes.
|
|
@@ -8,10 +27,11 @@
|
|
|
8
27
|
|
|
9
28
|
## Truthful workstation updates
|
|
10
29
|
|
|
30
|
+
- Keep a zero-exit `agy update` successful when no expected newer CLI version is known, even if its installed version does not advance and its output cannot confirm currency. Preserve `status: unknown` and `latest_version: null`, expose an `update_unverified` warning in JSON and text, and continue failing non-zero updater exits. No additional version source is assumed.
|
|
11
31
|
- Stop querying the editor release feed for the Antigravity CLI: its versions do not match `agy`. Keep `latest_version: null` and `status: unknown`, while still running the native `agy update` with `--yes` or confirmation. No read-only latest-version check is documented by `agy update --help`.
|
|
12
32
|
- Accept a successful native updater's `You are already on the latest version.` message when the installed version is unchanged, or verify a version increase against the published version when known. Preserve updater failures and unknown inventory status. Cover the real Antigravity output line, version increases, failures, and JSON/text uncertainty.
|
|
13
33
|
|
|
14
|
-
- Detect advertised native updater subcommands in both stdout and stderr; use Antigravity's known `agy update` command directly. Verify the installed CLI version after updating,
|
|
34
|
+
- Detect advertised native updater subcommands in both stdout and stderr; use Antigravity's known `agy update` command directly. Verify the installed CLI version after updating, retain strict verification for providers with a known expected release, and expose before/after versions without hiding major upgrades.
|
|
15
35
|
- Fall back from empty or failed Ollama vendor responses (including Linux HTTP 204) to the latest stable `ollama/ollama` GitHub release. Keep Linux updates manual with `update_available`, a null command, and a service-safety reason; never replace a potentially active binary or restart models automatically.
|
|
16
36
|
|
|
17
37
|
- Verify latest versions from npm, RTK GitHub releases, and Ollama vendor update endpoints with short read-only probes. Report `unknown` on unavailable evidence; reserve `up_to_date` for identical known versions, including Ponytail revisions.
|
|
@@ -57,3 +77,9 @@
|
|
|
57
77
|
- Keep snapshots independent of controller refresh during long missions, prohibit authentication writeback, enforce their original expiry, and remove temporary storage after success, failure and stop. Non-isolated local authentication remains unchanged.
|
|
58
78
|
- Reject rotating credentials at the sandbox boundary. Test snapshot contents, permissions, cleanup, refresh lock ownership, concurrent controller refresh, remote isolation through fake SSH and missing dependency refusal.
|
|
59
79
|
- The separately maintained remote-ops `scripts/workspace_sandbox.py` requires the same credential guard and run-private lifecycle integration; it is not shipped by this repository.
|
|
80
|
+
|
|
81
|
+
## Remote Ollama text generation
|
|
82
|
+
|
|
83
|
+
- Allow Ollama profiles in `cdx run --remote` with an explicitly selected, already installed local worker model and no authentication or Ollama/Codex CLI requirement. Read prompts on the launching host and use the HTTP capability/chat APIs with `stream: false`; retain generated text and prompt/evaluation token counters in run results and usage.
|
|
84
|
+
- Add a worker-supervised private Unix socket relay restricted to the fixed `127.0.0.1:11434` destination, selected model, `/api/show` and non-streaming `/api/chat`. Refuse model management, alternate destinations and cloud-backed models. Isolated clients have no network access and mount neither Ollama profiles nor model files; their checkout is read-only.
|
|
85
|
+
- State in CLI help and results that this is text generation, not an autonomous coding agent, with no repository edits. Cover success, usage, controller prompt content, missing models, forbidden relay calls, unreachable servers and sandbox relay construction with a fake HTTP API.
|
|
@@ -771,6 +771,17 @@
|
|
|
771
771
|
"x86_64-pc-windows-msvc": "be2e6ff8607351019447c6fbf0622254c2f8d8db81581007af71b74932e37f50",
|
|
772
772
|
"x86_64-unknown-linux-musl": "32de07569929ade136cf75ed8f0716e0f3b96a9c53c6632c628e13782d223518"
|
|
773
773
|
}
|
|
774
|
+
},
|
|
775
|
+
"v0.25.8": {
|
|
776
|
+
"github_tarball_sha256": "4a3e3eeb654a4590ea033e6d99337a57b9f255161ee2b9c9347e8def0d257868",
|
|
777
|
+
"github_zip_sha256": "ad2c7e8d1528f36f4be66320970522385ba3c0a473ab893475b174609ec05630",
|
|
778
|
+
"tray_assets": {
|
|
779
|
+
"aarch64-apple-darwin": "46e4e9ce2ce54d974dd3b77364fc97595b4d4388787c2e7486245374d342ff87",
|
|
780
|
+
"aarch64-unknown-linux-musl": "74afc63053dc3f6ca3fae010d8afc64c917cd37e71cc005dd8b9d3e9383b99e6",
|
|
781
|
+
"x86_64-apple-darwin": "990b305bf32d24e0126cb6f3fe42cd9858a6cd5dc460f41d0fb4419516324a40",
|
|
782
|
+
"x86_64-pc-windows-msvc": "9577f9dff7d457094276376f262904dad8c9aff95ed2ca87b834c9c900fb5493",
|
|
783
|
+
"x86_64-unknown-linux-musl": "db7e322037041c2a898043f0c7a691c22cab6a794aab5b2439a06ac91e92fa39"
|
|
784
|
+
}
|
|
774
785
|
}
|
|
775
786
|
}
|
|
776
787
|
}
|
package/docs/cli.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
| `cdx label <name> --clear [--json]` | Clear a session label |
|
|
20
20
|
| `cdx login <name> [--json]` | Re-authenticate a session (logout + login) |
|
|
21
21
|
| `cdx logout <name> [--json]` | Log out of a session |
|
|
22
|
+
| `cdx auth claude-token <profile> [--json]` | Store a dedicated remote Claude setup token from stdin in a private controller profile file |
|
|
22
23
|
| `cdx auth refresh <name\|all> [--json]` | Make a locked, non-generation Codex auth probe; reports `login_required` when interactive login is needed |
|
|
23
24
|
| `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
|
|
24
25
|
| `cdx enable <name> [--json]` | Re-enable a disabled session |
|
|
@@ -240,6 +241,74 @@ cdx run \
|
|
|
240
241
|
|
|
241
242
|
The result includes `launcher: "cdx"`, `run_id`, selected `session`, `provider`, `exit_code`, `duration_seconds`, absolute `transcript_path`, `stdout_path`, `stderr_path`, and normalized usage token fields. Codex headless runs use `codex exec --json`; Claude headless runs use `claude --print --output-format json`. Token counts are `null` when the provider does not expose a supported JSON or JSONL usage shape.
|
|
242
243
|
|
|
244
|
+
Remote Claude profiles require a dedicated long-lived token produced by `claude setup-token`
|
|
245
|
+
on the launching host. Supply only that token on standard input to
|
|
246
|
+
`cdx auth claude-token <profile>` (never put it in command arguments). This stores
|
|
247
|
+
`.cdx-remote-claude-token` with mode `0600` in the controller profile; ordinary
|
|
248
|
+
interactive Claude credentials and ambient API keys are not a fallback. A missing
|
|
249
|
+
or unsafe file stops before SSH and names this command.
|
|
250
|
+
|
|
251
|
+
Run `cdx run <profile> --remote worker --cwd /srv/project --prompt-file task.md --json`.
|
|
252
|
+
Claude always uses bubblewrap isolation, even without `--isolate-workspace`, and
|
|
253
|
+
requires matching Claude CLI versions on the controller and Linux worker, Python 3,
|
|
254
|
+
bubblewrap, pasta, nftables, working namespaces and memory-backed `/dev/shm`.
|
|
255
|
+
No worker login or profile mount is needed. Each attempt sends the token over SSH
|
|
256
|
+
stdin, then a private pipe through the sandbox to an exec shim that sets it only
|
|
257
|
+
in Claude's environment. It is never a worker credential file or command argument;
|
|
258
|
+
provider output is redacted before persistence. Fresh volatile HOME storage holds
|
|
259
|
+
`.claude`, `.claude.json`, caches and traces and is removed on exit, failure or stop.
|
|
260
|
+
`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1` and
|
|
261
|
+
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` remain set; API keys, proxy tokens,
|
|
262
|
+
proxy URLs and cloud-provider variables are excluded from the sandbox environment.
|
|
263
|
+
`--bare` is refused because it ignores OAuth. The outer sandbox owns tool permissions,
|
|
264
|
+
including a read-only checkout for `--permission review`.
|
|
265
|
+
|
|
266
|
+
Without `--network-access`, the trusted setup resolves `api.anthropic.com` and allows
|
|
267
|
+
HTTPS to those addresses plus public DNS, with other external destinations dropped.
|
|
268
|
+
`--network-access` retains the existing public tool-network policy. Address resolution
|
|
269
|
+
is fixed for the attempt; restart a run if the inference service changes addresses.
|
|
270
|
+
Usage and observed models come from Claude's JSON result. Fresh runs are required;
|
|
271
|
+
remote `--continue-run` remains unsupported.
|
|
272
|
+
|
|
273
|
+
Remote Antigravity profiles use the worker's existing login, created once by the
|
|
274
|
+
operator in that worker user's normal HOME. Run
|
|
275
|
+
`cdx run agy-profile --remote worker --cwd /srv/project --prompt-file task.md --json`.
|
|
276
|
+
Matching `agy` versions are required on both hosts. Bubblewrap is mandatory;
|
|
277
|
+
each attempt has a memory-backed HOME and mounts only the worker's mode-600
|
|
278
|
+
`.gemini/antigravity-cli/antigravity-oauth-token` authentication file, read-only.
|
|
279
|
+
The installation is read-only. No token is copied into the volatile HOME or sent
|
|
280
|
+
to the controller. A worker token lock excludes concurrent cdx attempts;
|
|
281
|
+
independent native agy launches must be kept out of the same worker account slot.
|
|
282
|
+
`AGY_CLI_DISABLE_AUTO_UPDATE=true` is set. Runs use `-p --output-format json`,
|
|
283
|
+
with closed stdin, and require JSON status `SUCCESS` without refused actions.
|
|
284
|
+
Missing login and expired-session errors require operator login on the worker;
|
|
285
|
+
cdx does not launch an authentication command. Usage is read from the JSON result.
|
|
286
|
+
|
|
287
|
+
The conservative HTTPS candidate list is `oauth2.googleapis.com`,
|
|
288
|
+
`cloudcode-pa.googleapis.com` and `daily-cloudcode-pa.googleapis.com`, resolved
|
|
289
|
+
on the worker before isolation, plus DNS to `1.1.1.1`/`9.9.9.9`. Other public
|
|
290
|
+
traffic is dropped even with `--network-access`; private ranges remain denied.
|
|
291
|
+
This list has **not** been accepted against live inference in the mission sandbox.
|
|
292
|
+
The token remains read-only pending a measured receipt showing whether refresh
|
|
293
|
+
writes it in place. See
|
|
294
|
+
`logics/runbook/fragments/2026-11-03-antigravity-44674fc58c.md` for evidence and
|
|
295
|
+
the host measurement command. Local Antigravity headless runs retain their profile HOME
|
|
296
|
+
and credentials; the worker login, token lock and volatile HOME policy apply only
|
|
297
|
+
to remote runs.
|
|
298
|
+
|
|
299
|
+
Remote Ollama profiles use the worker's unauthenticated HTTP API at
|
|
300
|
+
`127.0.0.1:11434`, with `/api/show` followed by `/api/chat` (`stream: false`).
|
|
301
|
+
Use `cdx run local --remote worker --cwd /srv/project --model qwen3:8b --prompt-file question.txt --isolate-workspace --json`.
|
|
302
|
+
The prompt file is read on the launching host; the model must already exist on
|
|
303
|
+
the worker. This is text generation only, not an autonomous coding agent, and
|
|
304
|
+
it does not edit the repository. The result includes `execution_mode`, an
|
|
305
|
+
`execution_note`, generated text in `result`, and `prompt_eval_count` / `eval_count`
|
|
306
|
+
alongside normalized `usage` counters. Missing models and unreachable APIs fail
|
|
307
|
+
without pulling models or authenticating. Isolated clients use a private Unix
|
|
308
|
+
socket relay restricted to capability and non-streaming inference calls for the
|
|
309
|
+
selected model, with a read-only checkout and no network or model-file mounts.
|
|
310
|
+
`--network-access` does not enable networking for this text-only client.
|
|
311
|
+
|
|
243
312
|
Known headless and completed interactive token usage is persisted in launch history and can be aggregated later. Interactive records are best-effort local data: Codex reads its latest cumulative rollout snapshot, Claude reads its assistant usage records, and Ollama — which writes no transcript of its own — is launched with `--verbose` so its per-response token counts land in the terminal capture cdx already takes. Antigravity reports no usage: its history is schema-less binary protobuf and its logs carry no usage line, so those sessions show no tokens by nature rather than by omission.
|
|
244
313
|
|
|
245
314
|
`--continue-run RUN_ID` resumes the exact provider conversation from a finished Claude or Codex run, inferring its profile and pinning its recorded model and effort. Existing targets and settings may be supplied only if they match. The parent transcript must still exist. `parent_run_id` links the new run to its parent in run results, the registry, and history; `conversation_id` identifies the provider conversation. Detached continuations preserve these settings through the cdx supervisor. Codex continuation usage is measured between exact transcript snapshots before and after the run, since exec resume can report conversation totals. If either boundary is unavailable, per-run usage stays unknown with `usage_attribution: "continuation_usage_unavailable"` in history instead of counting earlier turns again. Continuation does not use automatic capacity recovery or model fallback, and cannot be combined with `--failover` or `--isolate-workspace`.
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Local Antigravity statusLine capture; no credential or network access."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
STATE_NAME = "cdx-quota.json"
|
|
15
|
+
MARKER = "--cdx-antigravity-status"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _directory(home):
|
|
19
|
+
return os.path.join(home, ".gemini", "antigravity-cli")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _date(value):
|
|
23
|
+
try:
|
|
24
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
25
|
+
return parsed if parsed.tzinfo is not None else None
|
|
26
|
+
except (ValueError, TypeError):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _quota(value):
|
|
31
|
+
result = {}
|
|
32
|
+
if not isinstance(value, dict):
|
|
33
|
+
return result
|
|
34
|
+
for name, bucket in value.items():
|
|
35
|
+
if not isinstance(bucket, dict):
|
|
36
|
+
continue
|
|
37
|
+
clean = {}
|
|
38
|
+
fraction = bucket.get("remaining_fraction")
|
|
39
|
+
if isinstance(fraction, (int, float)) and not isinstance(fraction, bool) and math.isfinite(fraction) and 0 <= fraction <= 1:
|
|
40
|
+
clean["remaining_fraction"] = fraction
|
|
41
|
+
reset = _date(bucket.get("reset_time"))
|
|
42
|
+
if reset:
|
|
43
|
+
clean["reset_time"] = reset.isoformat()
|
|
44
|
+
seconds = bucket.get("reset_in_seconds")
|
|
45
|
+
if isinstance(seconds, (int, float)) and not isinstance(seconds, bool) and math.isfinite(seconds) and seconds >= 0:
|
|
46
|
+
clean["reset_in_seconds"] = seconds
|
|
47
|
+
result[name] = clean
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def capture(payload, state_path, now=None):
|
|
52
|
+
"""Persist only an allowlist, atomically with private permissions."""
|
|
53
|
+
if not isinstance(payload, dict):
|
|
54
|
+
return
|
|
55
|
+
now = now or datetime.now(timezone.utc)
|
|
56
|
+
state = {"time": now.isoformat(),
|
|
57
|
+
"plan_tier": payload.get("plan_tier") if isinstance(payload.get("plan_tier"), str) else None,
|
|
58
|
+
"quota": _quota(payload.get("quota"))}
|
|
59
|
+
directory = os.path.dirname(state_path)
|
|
60
|
+
os.makedirs(directory, exist_ok=True)
|
|
61
|
+
fd, temporary = tempfile.mkstemp(prefix=".cdx-quota-", dir=directory)
|
|
62
|
+
try:
|
|
63
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
64
|
+
json.dump(state, handle)
|
|
65
|
+
handle.write("\n")
|
|
66
|
+
os.replace(temporary, state_path)
|
|
67
|
+
finally:
|
|
68
|
+
if os.path.exists(temporary):
|
|
69
|
+
os.unlink(temporary)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def install(home):
|
|
73
|
+
"""Merge the hook without replacing unrelated settings or user output."""
|
|
74
|
+
from .agent_notify import _read_json, _write_json
|
|
75
|
+
|
|
76
|
+
path = os.path.join(_directory(home), "settings.json")
|
|
77
|
+
settings = _read_json(path)
|
|
78
|
+
if settings is None:
|
|
79
|
+
return False
|
|
80
|
+
status = settings.get("statusLine") or {}
|
|
81
|
+
if not isinstance(status, dict) or (status and status.get("type") != "command"):
|
|
82
|
+
return False
|
|
83
|
+
previous = status.get("command") or ""
|
|
84
|
+
if not isinstance(previous, str):
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
parts = shlex.split(previous, posix=os.name != "nt")
|
|
88
|
+
except ValueError:
|
|
89
|
+
parts = []
|
|
90
|
+
own_command = (len(parts) >= 3 and os.path.basename(parts[1].strip('"')) == "antigravity_status.py"
|
|
91
|
+
and parts[2] == MARKER)
|
|
92
|
+
if own_command:
|
|
93
|
+
index = parts.index("--previous") + 1 if "--previous" in parts else len(parts)
|
|
94
|
+
encoded = parts[index] if index < len(parts) else ""
|
|
95
|
+
else:
|
|
96
|
+
encoded = base64.urlsafe_b64encode(previous.encode()).decode()
|
|
97
|
+
args = [sys.executable, os.path.abspath(__file__), MARKER,
|
|
98
|
+
"--state", os.path.join(_directory(home), STATE_NAME)]
|
|
99
|
+
if encoded:
|
|
100
|
+
args += ["--previous", encoded]
|
|
101
|
+
command = subprocess.list2cmdline(args) if os.name == "nt" else shlex.join(args)
|
|
102
|
+
merged = {**status, "type": "command", "command": command}
|
|
103
|
+
if settings.get("statusLine") == merged:
|
|
104
|
+
return True
|
|
105
|
+
return _write_json(path, {**settings, "statusLine": merged})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read_status(home, now=None):
|
|
109
|
+
"""Expose every bucket; use only fresh Gemini windows for availability."""
|
|
110
|
+
now = now or datetime.now(timezone.utc)
|
|
111
|
+
try:
|
|
112
|
+
with open(os.path.join(_directory(home), STATE_NAME), encoding="utf-8") as handle:
|
|
113
|
+
state = json.load(handle)
|
|
114
|
+
except (OSError, ValueError):
|
|
115
|
+
state = {}
|
|
116
|
+
if not isinstance(state, dict):
|
|
117
|
+
state = {}
|
|
118
|
+
at = _date(state.get("time"))
|
|
119
|
+
age = max(0, (now - at).total_seconds()) if at else None
|
|
120
|
+
stale = age is not None and age > 24 * 60 * 60
|
|
121
|
+
quota = _quota(state.get("quota"))
|
|
122
|
+
for bucket in quota.values():
|
|
123
|
+
reset = _date(bucket.get("reset_time"))
|
|
124
|
+
valid = at is not None and not stale and reset is not None and reset > now
|
|
125
|
+
bucket["status"] = "stale" if stale else "current" if valid else "unknown"
|
|
126
|
+
if not valid:
|
|
127
|
+
bucket["remaining_fraction"] = None
|
|
128
|
+
result = {"updated_at": at.isoformat() if at else None, "reading_age_seconds": age,
|
|
129
|
+
"quota_stale": stale, "quota_status": "stale" if stale else "current" if at else "unknown",
|
|
130
|
+
"plan_tier": state.get("plan_tier"), "quota": quota}
|
|
131
|
+
for window, name in (("5h", "gemini-5h"), ("week", "gemini-weekly")):
|
|
132
|
+
bucket = quota.get(name, {})
|
|
133
|
+
reset = _date(bucket.get("reset_time"))
|
|
134
|
+
valid = at is not None and not stale and reset is not None and reset > now
|
|
135
|
+
fraction = bucket.get("remaining_fraction") if valid else None
|
|
136
|
+
result[f"remaining_{window}_pct"] = fraction * 100 if fraction is not None else None
|
|
137
|
+
result[f"reset_{window}_at"] = reset.astimezone().strftime("%b %d %H:%M") if valid else None
|
|
138
|
+
result[f"reset_{window}_at_iso"] = reset.isoformat() if valid else None
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main(stdin=None):
|
|
143
|
+
parser = argparse.ArgumentParser()
|
|
144
|
+
parser.add_argument(MARKER, action="store_true")
|
|
145
|
+
parser.add_argument("--state", required=True)
|
|
146
|
+
parser.add_argument("--previous", default="")
|
|
147
|
+
args = parser.parse_args()
|
|
148
|
+
raw = (stdin or sys.stdin).read()
|
|
149
|
+
try:
|
|
150
|
+
capture(json.loads(raw), args.state)
|
|
151
|
+
except (ValueError, OSError, TypeError):
|
|
152
|
+
# A failed capture must not suppress the user's existing status line.
|
|
153
|
+
pass
|
|
154
|
+
if args.previous:
|
|
155
|
+
command = base64.urlsafe_b64decode(args.previous).decode()
|
|
156
|
+
return subprocess.run(command, shell=True, input=raw, text=True, check=False).returncode
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Dedicated controller-side Claude setup token, never a worker profile."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import stat
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .errors import CdxError
|
|
10
|
+
|
|
11
|
+
TOKEN_FILE = '.cdx-remote-claude-token'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def store_token(session, token):
|
|
15
|
+
token = token.strip()
|
|
16
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,16384}', token):
|
|
17
|
+
raise CdxError('Provide one Claude setup token on standard input.')
|
|
18
|
+
home = Path(session['authHome'])
|
|
19
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
descriptor, temporary = tempfile.mkstemp(prefix='.claude-token-', dir=home)
|
|
21
|
+
try:
|
|
22
|
+
with os.fdopen(descriptor, 'w') as handle:
|
|
23
|
+
handle.write(token)
|
|
24
|
+
os.replace(temporary, home / TOKEN_FILE)
|
|
25
|
+
finally:
|
|
26
|
+
if os.path.exists(temporary):
|
|
27
|
+
os.unlink(temporary)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_token(session):
|
|
31
|
+
try:
|
|
32
|
+
path = Path(session['authHome']) / TOKEN_FILE
|
|
33
|
+
with open(path, opener=lambda name, flags: os.open(name, flags | getattr(os, "O_NOFOLLOW", 0))) as handle:
|
|
34
|
+
info = os.fstat(handle.fileno())
|
|
35
|
+
if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o600 or (hasattr(os, "getuid") and info.st_uid != os.getuid()):
|
|
36
|
+
raise ValueError('Unsafe token file')
|
|
37
|
+
token = handle.read(16385).strip()
|
|
38
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,16384}', token):
|
|
39
|
+
raise ValueError('Invalid token')
|
|
40
|
+
return token
|
|
41
|
+
except (OSError, ValueError) as error:
|
|
42
|
+
raise CdxError(
|
|
43
|
+
f"Remote Claude requires a dedicated setup token; run cdx auth claude-token {session['name']} "
|
|
44
|
+
"with the token from claude setup-token on standard input.", code='remote_claude_token_missing',
|
|
45
|
+
) from error
|
package/src/cli.py
CHANGED
|
@@ -243,9 +243,11 @@ def _print_help(use_color=False):
|
|
|
243
243
|
f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
|
|
244
244
|
f" {_style('cdx agents [--max-age-hours N] [--json]', '36', use_color)}",
|
|
245
245
|
f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
|
|
246
|
+
f" {_style('cdx auth claude-token <profile> [--json]', '36', use_color)}",
|
|
246
247
|
f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
|
|
247
248
|
f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
|
|
248
249
|
f" {_style('cdx run [session] [--remote SSH_TARGET] --cwd PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
|
|
250
|
+
" Remote Ollama: text generation only, not an autonomous coding agent; no repository edits. Requires an installed worker model; no authentication.",
|
|
249
251
|
f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json', '36', use_color)}",
|
|
250
252
|
f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
|
|
251
253
|
f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
|
package/src/cli_args.py
CHANGED
|
@@ -49,6 +49,7 @@ SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
|
49
49
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
50
50
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
51
51
|
RUN_USAGE = "Usage: cdx run [session] [--remote SSH_TARGET] [--remote-prefix COMMAND] (--cwd|--dir) PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
|
|
52
|
+
RUN_USAGE += "\nRemote Ollama generates text only; it is not an autonomous coding agent and does not edit the repository. Requires an installed worker model; no authentication."
|
|
52
53
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
53
54
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
54
55
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
@@ -593,6 +594,7 @@ def cdx_schema():
|
|
|
593
594
|
"logics_manager_update_available",
|
|
594
595
|
"update_version_check_failed",
|
|
595
596
|
"update_version_mismatch",
|
|
597
|
+
"update_unverified",
|
|
596
598
|
"logics_manager_missing",
|
|
597
599
|
"logics_viewer_failed",
|
|
598
600
|
"logics_viewer_root_invalid",
|
package/src/commands/auth.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import sys
|
|
6
7
|
import time
|
|
7
8
|
import uuid
|
|
8
9
|
|
|
@@ -53,6 +54,30 @@ def _refresh_result(session, diagnostic):
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def handle_auth(rest, ctx):
|
|
57
|
+
if rest and rest[0] == "claude-token":
|
|
58
|
+
json_flag, args = _parse_json_flag(rest[1:])
|
|
59
|
+
if len(args) != 1:
|
|
60
|
+
raise CdxError("Usage: cdx auth claude-token <profile> [--json]")
|
|
61
|
+
session = ctx["service"]["get_session"](args[0])
|
|
62
|
+
if not session or session["provider"] != PROVIDER_CLAUDE:
|
|
63
|
+
raise CdxError("Select an existing Claude profile for cdx auth claude-token.")
|
|
64
|
+
if ctx["stdin_is_tty"]:
|
|
65
|
+
raise CdxError("Read the Claude setup token from standard input, without an interactive terminal.")
|
|
66
|
+
from ..claude_remote_token import store_token
|
|
67
|
+
stream = ctx.get("prompt_stdin") or sys.stdin
|
|
68
|
+
token = stream.read(16385)
|
|
69
|
+
if isinstance(token, bytes):
|
|
70
|
+
try:
|
|
71
|
+
token = token.decode("utf-8")
|
|
72
|
+
except UnicodeDecodeError as error:
|
|
73
|
+
raise CdxError("Provide one Claude setup token on standard input.") from error
|
|
74
|
+
store_token(session, token)
|
|
75
|
+
message = f"Stored remote Claude setup token for {session['name']}"
|
|
76
|
+
if json_flag:
|
|
77
|
+
_write_json(ctx, _json_success("auth.claude-token", message))
|
|
78
|
+
else:
|
|
79
|
+
ctx["out"](message + "\n")
|
|
80
|
+
return 0
|
|
56
81
|
parsed = _parse_auth_args(rest)
|
|
57
82
|
target = parsed["target"]
|
|
58
83
|
if target == "all":
|
|
@@ -53,6 +53,8 @@ def _format_update_all_result(result, use_color=False):
|
|
|
53
53
|
return _warn(f"skipped: {result['name']} (blocked by {result['blocked_by']})", use_color)
|
|
54
54
|
if result.get("returncode") == 0:
|
|
55
55
|
versions = f" ({result['version_before']} -> {result['version_after']})" if "version_after" in result else ""
|
|
56
|
+
if result.get("warning"):
|
|
57
|
+
return _warn(f"done: {result['name']}{versions}\n {result['warning']['message']}", use_color)
|
|
56
58
|
return _success(f"done: {result['name']}{versions}", use_color)
|
|
57
59
|
detail = str(result.get("stderr") or result.get("stdout") or "No error output.").strip().replace("\n", " ")
|
|
58
60
|
detail = detail[-500:]
|
|
@@ -709,7 +711,8 @@ def handle_update(rest, ctx):
|
|
|
709
711
|
failed = [row for row in results if row.get("returncode") != 0]
|
|
710
712
|
message = "Applied workstation update plan" if not failed else "Workstation update plan completed with failures"
|
|
711
713
|
if parsed["json"]:
|
|
712
|
-
|
|
714
|
+
warnings = [dict(row["warning"], name=row["name"]) for row in results if row.get("warning")]
|
|
715
|
+
_write_json(ctx, _json_success("update.all", message, warnings=warnings, plan=plan, results=results, failed=len(failed)))
|
|
713
716
|
else:
|
|
714
717
|
ctx["out"](f"{_success(message, ctx['use_color']) if not failed else _warn(message, ctx['use_color'])}\n")
|
|
715
718
|
return 1 if failed else 0
|
package/src/commands/runs.py
CHANGED
|
@@ -713,16 +713,26 @@ def handle_run(rest, ctx):
|
|
|
713
713
|
session = selected["session"]
|
|
714
714
|
selection_row = selected.get("row") or {}
|
|
715
715
|
|
|
716
|
+
if parsed.get("remote") and session["provider"] == "antigravity":
|
|
717
|
+
parsed["isolate_workspace"] = True
|
|
716
718
|
remote_version = None
|
|
717
719
|
if parsed.get("remote"):
|
|
718
|
-
if session["provider"]
|
|
719
|
-
raise CdxError("--remote is only available for Codex profiles.", code="remote_provider_unsupported")
|
|
720
|
+
if session["provider"] not in ("codex", "claude", "ollama", "antigravity"):
|
|
721
|
+
raise CdxError("--remote is only available for Codex, Claude, Ollama and Antigravity profiles.", code="remote_provider_unsupported")
|
|
720
722
|
if parsed.get("continue_run"):
|
|
721
723
|
raise CdxError("--remote requires a fresh run without --continue-run.")
|
|
724
|
+
if session["provider"] == "ollama" and not (parsed.get("model") or (session.get("launch") or {}).get("model")):
|
|
725
|
+
raise CdxError("Remote Ollama text generation requires --model or a configured local model.")
|
|
726
|
+
if session["provider"] == "claude":
|
|
727
|
+
from ..claude_remote_token import read_token
|
|
728
|
+
read_token(session)
|
|
729
|
+
parsed["isolate_workspace"] = True
|
|
722
730
|
remote_version = preflight(
|
|
723
731
|
parsed["remote"], {**os.environ, **(ctx.get("env") or {})},
|
|
724
732
|
remote_prefix=parsed.get("remote_prefix"), isolate_workspace=parsed.get("isolate_workspace", False),
|
|
725
|
-
cwd=parsed["cwd"], network=parsed.get("network_access", False), writable=parsed.get("permission") != "review"
|
|
733
|
+
cwd=parsed["cwd"], network=parsed.get("network_access", False), writable=parsed.get("permission") != "review",
|
|
734
|
+
**({"provider": "ollama", "model": parsed.get("model") or (session.get("launch") or {}).get("model")}
|
|
735
|
+
if session["provider"] == "ollama" else {"provider": session["provider"]}))
|
|
726
736
|
cwd = os.path.abspath(parsed["cwd"])
|
|
727
737
|
if not parsed.get("remote") and not os.path.isdir(cwd):
|
|
728
738
|
raise CdxError(f"Invalid cwd: {parsed['cwd']}", code="invalid_cwd")
|
|
@@ -1074,6 +1084,9 @@ def handle_run(rest, ctx):
|
|
|
1074
1084
|
message = f"Every eligible session was rate limited: {', '.join(attempted)}."
|
|
1075
1085
|
elif failover_error_code == "failover_limit_reached":
|
|
1076
1086
|
message = f"Failover stopped after {MAX_FAILOVER_TRANSITIONS} transitions: {', '.join(attempted)}."
|
|
1087
|
+
elif run_info.get("provider_error"):
|
|
1088
|
+
failover_error_code = run_info["provider_error"]
|
|
1089
|
+
message = run_info["provider_message"]
|
|
1077
1090
|
elif run_info.get("timed_out"):
|
|
1078
1091
|
message = "Provider process timed out."
|
|
1079
1092
|
else:
|
package/src/interactive_usage.py
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
"""Best-effort usage readers for interactive provider sessions.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- Antigravity has no retrievable source. Its conversation history is
|
|
10
|
-
schema-less binary protobuf under `~/.gemini/antigravity-cli/conversations/`
|
|
11
|
-
with no `.proto` available to decode field numbers into token counts, and its
|
|
12
|
-
logs are plain glog with no usage-bearing line at any verbosity `agy --help`
|
|
13
|
-
exposes. This was investigated and is a dead end, not an oversight -- do not
|
|
14
|
-
spend the afternoon rediscovering it.
|
|
3
|
+
- Claude and Codex expose per-session token counts in JSONL transcripts.
|
|
4
|
+
- Ollama exposes counts in the verbose terminal output captured by cdx.
|
|
5
|
+
- Antigravity exposes quota through interactive statusLine stdin (collected by
|
|
6
|
+
antigravity_status), and per-run tokens through `agy -p --output-format json`
|
|
7
|
+
(read by run_usage). Interactive per-run token counts remain unavailable;
|
|
8
|
+
quota changes are not token measurements.
|
|
15
9
|
"""
|
|
16
10
|
|
|
17
11
|
import json
|