cdx-manager 0.17.0 → 0.18.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/README.md +64 -4
- package/changelogs/CHANGELOGS_0_17_1.md +29 -0
- package/changelogs/CHANGELOGS_0_18_0.md +82 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/agent_notify.py +71 -7
- package/src/cli.py +14 -3
- package/src/cli_args.py +5 -2
- package/src/cli_commands.py +3 -0
- package/src/cli_helpers.py +5 -4
- package/src/cli_hints.py +167 -0
- package/src/cli_render.py +1 -0
- package/src/commands/settings.py +29 -0
- package/src/commands/status.py +5 -1
- package/src/commands/tray.py +445 -0
- package/src/notify.py +24 -4
- package/src/session_service.py +13 -1
- package/src/tray_autostart.py +108 -0
- package/src/tray_capability.py +120 -0
- package/src/tray_contract.py +229 -0
- package/src/tray_events.py +210 -0
- package/src/tray_install.py +370 -0
- package/src/tray_instance.py +57 -0
- package/src/tray_shortcut.py +223 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
|
|
6
6
|
|
|
@@ -349,8 +349,8 @@ When a session's agent finishes a turn, or stops to wait for you, cdx raises a
|
|
|
349
349
|
desktop notification naming the session and the repository:
|
|
350
350
|
|
|
351
351
|
```text
|
|
352
|
-
✓ work1 logics-manager ·
|
|
353
|
-
✓ codex-a cdx-manager ·
|
|
352
|
+
✓ work1 logics-manager · needs your attention
|
|
353
|
+
✓ codex-a cdx-manager · turn complete
|
|
354
354
|
```
|
|
355
355
|
|
|
356
356
|
Off until you ask for it, per session:
|
|
@@ -359,6 +359,7 @@ Off until you ask for it, per session:
|
|
|
359
359
|
cdx set work1 --notify on # turn it on for one session
|
|
360
360
|
cdx set --all --notify on # turn it on everywhere
|
|
361
361
|
cdx set work1 --notify off # and back off; cdx removes what it installed
|
|
362
|
+
cdx set work1 --notify-preview on # include a short final-response preview
|
|
362
363
|
```
|
|
363
364
|
|
|
364
365
|
Turning it on records the setting. The wiring happens at that session's **next
|
|
@@ -372,6 +373,16 @@ It is off by default because turning it on writes into the provider's own
|
|
|
372
373
|
configuration and, on Codex, puts an approval prompt in front of you. That is
|
|
373
374
|
not something to do to sessions you already own without being asked.
|
|
374
375
|
|
|
376
|
+
Response previews are separately off by default. When enabled, a completion
|
|
377
|
+
notification includes at most 180 characters of the agent's final response.
|
|
378
|
+
That text can be visible on a lock screen, so enable it only for sessions whose
|
|
379
|
+
output is safe to expose. Previews never read transcripts and are not shown for
|
|
380
|
+
permission requests.
|
|
381
|
+
|
|
382
|
+
`cdx notify` is the internal hook target, called by the provider; it is not the
|
|
383
|
+
setup command. `cdx ready` is separate: it notifies you when the next cooling-
|
|
384
|
+
down session becomes usable.
|
|
385
|
+
|
|
375
386
|
Per platform: macOS uses `osascript`, Linux `notify-send`, Windows a toast, and
|
|
376
387
|
WSL reaches the Windows notification centre through interop. On a host with no
|
|
377
388
|
way to deliver one — a headless or SSH Linux session, or WSL with interop
|
|
@@ -489,7 +500,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
489
500
|
| `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
|
|
490
501
|
| `cdx update all [--yes] [--json]` | Check installed providers, RTK, and Ponytail across Codex profiles; show the plan and ask once before applying safe native/Homebrew updates and missing RTK/Ponytail setup |
|
|
491
502
|
| `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
|
|
492
|
-
| `cdx notify` |
|
|
503
|
+
| `cdx notify` | Internal provider hook target for agent alerts; use `cdx set <name> --notify on` to enable it |
|
|
493
504
|
| `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
|
|
494
505
|
| `cdx select --provider PROVIDER [--min-reasoning-effort minimal\|low\|medium\|high\|xhigh] [--min-power minimal\|low\|medium\|high\|xhigh] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
|
|
495
506
|
| `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT\|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json` | Run one headless task and return a stable JSON result; `--detach` returns the `run_id` at launch without waiting, `--failover` continues the task on the next account when this one hits its rate limit, `--prompt-file -` reads the prompt from stdin |
|
|
@@ -502,11 +513,60 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
502
513
|
| `cdx status [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show token usage table for all sessions; `--cached` skips live provider probes and returns only stored status |
|
|
503
514
|
| `cdx status --small [--refresh\|--cached] [--timeout SECONDS]` / `cdx status -s [--refresh\|--cached] [--timeout SECONDS]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
|
|
504
515
|
| `cdx status <name> [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show detailed usage breakdown for one session; `--cached` avoids live provider refreshes |
|
|
516
|
+
| `cdx tray status [--json] [--refresh]` | Emit the tray snapshot: one icon state, one line per enabled session, and how fresh each figure is. Reads stored status by default; `--refresh` is the only path that probes a provider |
|
|
517
|
+
| `cdx tray launch [--json]` | Start the installed tray companion, detached. Reports `tray_companion_not_installed` when nothing is recorded, or `tray_companion_missing` when the recorded companion is gone. `CDX_TRAY_BIN` points it at a locally built one |
|
|
518
|
+
| `cdx tray install [--json]` | Download the companion for this OS and architecture, verify its published checksum before unpacking, and record what was written. Refuses when no checksum is published for the asset |
|
|
519
|
+
| `cdx tray uninstall [--json]` | Remove exactly what `install` recorded, and nothing else |
|
|
520
|
+
| `cdx tray autostart [on\|off\|status] [--json]` | Start the companion at login, or stop doing so. Off until asked, idempotent, and the state is read back from the platform rather than from what CDX last intended |
|
|
521
|
+
| `cdx tray events [--json]` | List agent alerts a running companion has not shown yet. The companion reads these instead of the spool file, so a Windows tray can collect events from CDX in WSL over the same interop it already uses |
|
|
522
|
+
| `cdx tray ack <id>... [--json]` | Mark events as shown, idempotently |
|
|
523
|
+
| `cdx tray heartbeat [--json]` | Record that a companion is alive. `cdx notify` publishes to the tray only while this is fresh, and delivers directly otherwise |
|
|
524
|
+
| `cdx tray doctor [--json]` | Report companion, executable, version, running instance, autostart, and the two capabilities that fail silently — whether this desktop can draw a tray icon at all, and on Windows whether the Start Menu shortcut toasts depend on exists. Reads only; it never repairs |
|
|
505
525
|
| `cdx --help` | Show usage |
|
|
506
526
|
| `cdx --version` | Show version |
|
|
507
527
|
|
|
508
528
|
---
|
|
509
529
|
|
|
530
|
+
### Tray snapshot
|
|
531
|
+
|
|
532
|
+
`cdx tray status --json` is the contract an optional desktop tray companion reads. It adds nothing to the quota pipeline: it interprets the status CDX already stores.
|
|
533
|
+
|
|
534
|
+
- **Cached by default.** A tray polls, and a live provider probe serializes on the Codex auth lock, which the launcher holds for a whole interactive session. Polling it would be locked out at best and race a rotating OAuth refresh token at worst. Only `--refresh`, a deliberate user gesture, probes.
|
|
535
|
+
- **Freshness is a value, not an error.** Each session reports `fresh`, `stale`, `auth_locked`, or `unknown`. `auth_locked` means a session is running and its quota cannot be refreshed until it exits — pressing refresh will not help, and the snapshot says so instead of ageing silently.
|
|
536
|
+
- **The icon leaks nothing.** `icon` holds a state, a reason, and a count. Session names, providers, and figures live in `sessions`, which a companion shows only once the user opens the menu.
|
|
537
|
+
- **Versions drift on purpose.** The payload carries `schema.major`. A reader meeting a newer major keeps every field it understands and surfaces one update hint, so upgrading CDX never breaks an installed companion.
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{
|
|
541
|
+
"schema": { "name": "cdx.tray.snapshot", "major": 1, "minor": 0 },
|
|
542
|
+
"icon": { "state": "low", "reason": "fresh", "session_count": 2 },
|
|
543
|
+
"sessions": [
|
|
544
|
+
{ "name": "work", "provider": "codex", "available_pct": 18, "state": "low", "freshness": "fresh", "reset_at": "..." }
|
|
545
|
+
],
|
|
546
|
+
"refreshable": true,
|
|
547
|
+
"actions": ["refresh", "open_terminal"]
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Icon states are `ok` at 25% remaining and above, `low` below 25%, `critical` below 5%, and `unknown` when nothing was ever reported. The closed icon shows the most urgent session, and a session that never reported never outranks one with real news.
|
|
552
|
+
|
|
553
|
+
### Setup hints
|
|
554
|
+
|
|
555
|
+
`cdx` and `cdx status` may end with one dim line pointing at an optional surface you have not set up yet — the tray companion, or agent alerts. They exist because neither is discoverable from the output of the commands you already run.
|
|
556
|
+
|
|
557
|
+
They are deliberately hard to become noise. One hint per invocation at most, not again for six hours, and each one retires for good after three showings: if you saw it three times and did not act, that is an answer. A hint never appears in `--json`, never appears alongside an update notice, and is answered entirely from local state — nothing here probes a provider or the network. `CDX_NO_HINTS=1` turns them off outright.
|
|
558
|
+
|
|
559
|
+
### Tray companion: what vouches for it
|
|
560
|
+
|
|
561
|
+
The companion is a native binary distributed as a release asset, not part of the pip package. Its trust model is worth stating plainly rather than leaving you to assume the usual one.
|
|
562
|
+
|
|
563
|
+
- **The checksum is the vouching, not Apple or Microsoft.** The macOS bundle is signed with a *self-signed* certificate. That signature exists because Apple Silicon refuses to execute unsigned arm64 code at all — it is not a claim that Apple reviewed anything. What actually vouches for the asset is the SHA-256 published in `checksums/release-archives.json`, verified before a single byte is unpacked. An asset with no published checksum is refused rather than installed hopefully.
|
|
564
|
+
- **Install through `cdx tray install`, and no download link is offered.** This is a security property, not a convenience. A file fetched by CDX carries no macOS quarantine attribute and no Windows Mark-of-the-Web, so nothing puts Gatekeeper or SmartScreen between you and a companion you asked for. The same file fetched through a browser would carry both. That is why no direct download URL is published: it would be a worse path wearing the same clothes.
|
|
565
|
+
- **Nothing is silently replaced.** An update downloads, verifies, and *proves the replacement starts* before the working companion is touched. A corrupt asset or one built for the wrong architecture costs an error message, not a tray you can no longer launch. An interrupted update is a named, recoverable state that `cdx tray doctor` reports.
|
|
566
|
+
- **Uninstall removes only what install recorded**, from a record CDX wrote itself. A damaged record reads as absent rather than being guessed at, because that record drives deletion.
|
|
567
|
+
- **Linux carries no C library.** The Linux asset is statically linked against musl, so there is no glibc version to match and no `gtk`, `libxdo`, or `appindicator` package to install. Tray support is resolved at runtime by looking for the `org.kde.StatusNotifierWatcher` D-Bus name — never by matching a distribution or desktop name. A session without it is told so, and nothing is registered to start at login.
|
|
568
|
+
- **Version drift between CDX and the companion is normal**, since pip upgrades one and `cdx tray install` the other. Whichever is older keeps serving every field it understands and shows one update hint.
|
|
569
|
+
|
|
510
570
|
## JSON Output
|
|
511
571
|
|
|
512
572
|
`cdx-manager` can be consumed by other apps through its CLI JSON contract.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# CDX Manager 0.17.1
|
|
2
|
+
|
|
3
|
+
## Fixes
|
|
4
|
+
|
|
5
|
+
### Agent-alert setup now reports provider support accurately
|
|
6
|
+
|
|
7
|
+
`cdx set <name> --notify on` now distinguishes sessions where cdx can install
|
|
8
|
+
provider hooks (Claude and Codex) from providers that do not expose that
|
|
9
|
+
integration yet. A mixed bulk selection reports both outcomes instead of
|
|
10
|
+
promising a future hook installation that cannot happen.
|
|
11
|
+
|
|
12
|
+
The setting remains storable for unsupported providers, so bulk configuration
|
|
13
|
+
remains predictable and a future provider integration can honour the existing
|
|
14
|
+
opt-in.
|
|
15
|
+
|
|
16
|
+
### Permission-request notifications constrain tool names
|
|
17
|
+
|
|
18
|
+
Tool names supplied by provider hook payloads now use the same printable,
|
|
19
|
+
single-line normalization as response previews and are capped at 80 characters.
|
|
20
|
+
This keeps permission notifications readable and prevents malformed payloads
|
|
21
|
+
from expanding or controlling desktop notification text.
|
|
22
|
+
|
|
23
|
+
## Validation
|
|
24
|
+
|
|
25
|
+
- `npm run lint`
|
|
26
|
+
- `npm test`
|
|
27
|
+
- `npm run release:validate`
|
|
28
|
+
- `logics-manager lint --require-status`
|
|
29
|
+
- `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# CDX Manager 0.18.0
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
### An optional desktop tray companion
|
|
6
|
+
|
|
7
|
+
A native menu bar companion now shows remaining quota across every enabled
|
|
8
|
+
session without a terminal open. It is optional, off until installed, and adds
|
|
9
|
+
nothing to the quota pipeline: it reads the status CDX already stores through
|
|
10
|
+
`cdx tray status --json`.
|
|
11
|
+
|
|
12
|
+
`cdx tray` gained `status`, `install`, `launch`, `uninstall`, `autostart`,
|
|
13
|
+
`doctor`, `events`, `ack`, and `heartbeat`.
|
|
14
|
+
|
|
15
|
+
The polling is cached by default and never probes a provider on a timer. A live
|
|
16
|
+
probe serializes on the Codex auth lock, which an interactive session holds for
|
|
17
|
+
its whole run, and races a rotating OAuth refresh token. Only an explicit
|
|
18
|
+
refresh — a user gesture from the menu — probes, and the menu says so when a
|
|
19
|
+
running session makes refreshing pointless.
|
|
20
|
+
|
|
21
|
+
Freshness is reported rather than hidden. Each session reads `fresh`, `stale`,
|
|
22
|
+
`auth_locked`, or `unknown`, and a figure is never invented for a state that
|
|
23
|
+
knows nothing.
|
|
24
|
+
|
|
25
|
+
### Agent alerts delivered through the tray
|
|
26
|
+
|
|
27
|
+
When a companion is running, an agent finishing a turn raises one notification
|
|
28
|
+
from the tray instead of two from the tray and the direct path. The tray keeps a
|
|
29
|
+
bounded list of recent alerts and marks new ones beside the icon for 45 seconds.
|
|
30
|
+
|
|
31
|
+
Delivery falls back to the previous direct path whenever a companion is absent,
|
|
32
|
+
stale, or speaks a schema this CDX does not, so upgrading one side never leaves
|
|
33
|
+
the other silent.
|
|
34
|
+
|
|
35
|
+
On macOS the alert is posted from the signed CDX bundle, so it carries the CDX
|
|
36
|
+
name and icon and can be turned off under CDX in System Settings. On Windows it
|
|
37
|
+
goes out under CDX's own AppUserModelID once `cdx tray install` has written the
|
|
38
|
+
Start Menu shortcut, and under PowerShell's identifier until then.
|
|
39
|
+
|
|
40
|
+
### Setup hints in `cdx` and `cdx status`
|
|
41
|
+
|
|
42
|
+
Both commands may end with one dim line pointing at an optional surface not yet
|
|
43
|
+
set up — the tray companion, or agent alerts. One hint per invocation, not again
|
|
44
|
+
for six hours, retired for good after three showings, never in `--json`, never
|
|
45
|
+
alongside an update notice, and `CDX_NO_HINTS=1` turns them off.
|
|
46
|
+
|
|
47
|
+
## Platform support
|
|
48
|
+
|
|
49
|
+
- **macOS**: signed bundle, menu bar item, native notifications, LaunchAgent
|
|
50
|
+
autostart.
|
|
51
|
+
- **Windows**: taskbar icon that picks its glyph tint from the taskbar theme,
|
|
52
|
+
promotes itself out of the notification overflow once, and reads CDX from WSL
|
|
53
|
+
through interop with no X server and no localhost.
|
|
54
|
+
- **Linux**: StatusNotifierItem through ksni, resolved at runtime by looking for
|
|
55
|
+
`org.kde.StatusNotifierWatcher` rather than by matching a distribution name. A
|
|
56
|
+
desktop without one is told so instead of being left with an invisible icon.
|
|
57
|
+
The asset is statically linked against musl, so there is no glibc version to
|
|
58
|
+
match and no gtk, libxdo, or appindicator package to install.
|
|
59
|
+
|
|
60
|
+
## Trust model
|
|
61
|
+
|
|
62
|
+
`cdx tray install` fetches a release asset and verifies its published SHA-256
|
|
63
|
+
before a single byte is unpacked. The macOS bundle is self-signed: that
|
|
64
|
+
signature exists because Apple Silicon refuses to execute unsigned arm64 code,
|
|
65
|
+
not because Apple reviewed anything, and the published checksum is what vouches
|
|
66
|
+
for the asset. Installing through CDX is also what keeps the file free of the
|
|
67
|
+
macOS quarantine attribute and the Windows Mark of the Web, which is why no
|
|
68
|
+
browser download link is published.
|
|
69
|
+
|
|
70
|
+
An update downloads, verifies, and proves the replacement starts before the
|
|
71
|
+
working companion is touched. A failure leaves the previous one running, and
|
|
72
|
+
`cdx tray doctor` names the interrupted state.
|
|
73
|
+
|
|
74
|
+
## Validation
|
|
75
|
+
|
|
76
|
+
- `npm run lint`
|
|
77
|
+
- `npm test` — 766 Python tests
|
|
78
|
+
- `cargo test` — 45 Rust tests; `cargo clippy` clean with zero warnings on
|
|
79
|
+
aarch64-apple-darwin, x86_64-pc-windows-msvc, and x86_64-unknown-linux-gnu
|
|
80
|
+
- `scripts/tray-smoke.sh` on three hosts: macOS arm64 native, Windows serving
|
|
81
|
+
CDX through Ubuntu WSL, and Linux natively in that same WSL
|
|
82
|
+
- `logics-manager lint` and `logics-manager audit`
|
|
@@ -184,6 +184,14 @@
|
|
|
184
184
|
"v0.16.0": {
|
|
185
185
|
"github_tarball_sha256": "bdcd2808922410b20567ba86589d49166c5c2b9404325195b28457df80ca3dad",
|
|
186
186
|
"github_zip_sha256": "e89cb8c964304f0805d06da9d355d543f8a590e0febbf07e585a692ccf73ef99"
|
|
187
|
+
},
|
|
188
|
+
"v0.17.0": {
|
|
189
|
+
"github_tarball_sha256": "6f321106aa426590ce809788fab4f26401b4cc499a6c518a56a4f24f8bfb6c2e",
|
|
190
|
+
"github_zip_sha256": "72665a0006df40bb27301140b0ed6600a0d91b456facbb5a2aad2a917357ae75"
|
|
191
|
+
},
|
|
192
|
+
"v0.17.1": {
|
|
193
|
+
"github_tarball_sha256": "f1cc36d55f17d8e5c8ad0b0ebc419e812b8788742c3e45ce1137303924114dd7",
|
|
194
|
+
"github_zip_sha256": "29acb306ad76535d0db701504a53b9887b8a6b0b95d968634139f85952a34ec3"
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/agent_notify.py
CHANGED
|
@@ -15,17 +15,26 @@ import shutil
|
|
|
15
15
|
import subprocess
|
|
16
16
|
|
|
17
17
|
from .notify import notification_channel, send_desktop_notification
|
|
18
|
+
from .tray_events import publish
|
|
18
19
|
|
|
19
20
|
# Set on the provider process at launch; the hook runs as its child and inherits both.
|
|
20
21
|
SESSION_ENV = "CDX_SESSION_NAME"
|
|
21
22
|
ENABLED_ENV = "CDX_NOTIFY"
|
|
23
|
+
PREVIEW_ENV = "CDX_NOTIFY_PREVIEW"
|
|
22
24
|
|
|
23
25
|
# Our entries are recognised by the command they run, not by a marker key of our
|
|
24
26
|
# own: both providers validate this file against their own schema, and an extra
|
|
25
27
|
# key they do not know is a rejection risk for no gain.
|
|
26
28
|
HOOK_ARG = "notify"
|
|
27
29
|
|
|
28
|
-
_WAITING_EVENTS = {"notification", "permissionrequest"
|
|
30
|
+
_WAITING_EVENTS = {"notification", "permissionrequest"}
|
|
31
|
+
_PREVIEW_LIMIT = 180
|
|
32
|
+
_TOOL_NAME_LIMIT = 80
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def supports_agent_alerts(provider):
|
|
36
|
+
"""Whether cdx can provision provider hooks for this session."""
|
|
37
|
+
return provider in {"claude", "codex"}
|
|
29
38
|
|
|
30
39
|
|
|
31
40
|
def resolve_hook_command(env=None):
|
|
@@ -70,9 +79,29 @@ def compose_notification(payload, env=None, cwd=None):
|
|
|
70
79
|
return None
|
|
71
80
|
directory = payload.get("cwd") or payload.get("workspace") or cwd or os.getcwd()
|
|
72
81
|
event = str(payload.get("hook_event_name") or payload.get("eventName") or payload.get("type") or "").lower()
|
|
73
|
-
|
|
82
|
+
event = event.replace("_", "").replace("-", "")
|
|
83
|
+
state = "needs your attention" if event in _WAITING_EVENTS else "turn complete"
|
|
84
|
+
tool_name = _notification_text(payload.get("tool_name"), _TOOL_NAME_LIMIT)
|
|
85
|
+
if event == "permissionrequest" and tool_name:
|
|
86
|
+
state += f" ({tool_name})"
|
|
74
87
|
where = os.path.basename(str(directory).rstrip("/\\")) or directory
|
|
75
|
-
|
|
88
|
+
preview = _notification_preview(payload, event, env)
|
|
89
|
+
return f"✓ {session}", f"{where} · {state}" + (f" — {preview}" if preview else "")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _notification_preview(payload, event, env):
|
|
93
|
+
if event != "stop" or env.get(PREVIEW_ENV) != "1":
|
|
94
|
+
return ""
|
|
95
|
+
return _notification_text(payload.get("last_assistant_message"), _PREVIEW_LIMIT)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _notification_text(value, limit):
|
|
99
|
+
if not isinstance(value, str):
|
|
100
|
+
return ""
|
|
101
|
+
value = " ".join("".join(char if char.isprintable() else " " for char in value).split())
|
|
102
|
+
if len(value) <= limit:
|
|
103
|
+
return value
|
|
104
|
+
return value[:limit - 1].rstrip() + "…"
|
|
76
105
|
|
|
77
106
|
|
|
78
107
|
def handle_notify(rest, ctx):
|
|
@@ -82,12 +111,22 @@ def handle_notify(rest, ctx):
|
|
|
82
111
|
stdin_text = None
|
|
83
112
|
# "stdin" in ctx is the {"isTTY": ...} descriptor, not a stream.
|
|
84
113
|
reader = ctx.get("prompt_stdin") or sys.stdin
|
|
114
|
+
env = os.environ if ctx.get("env") is None else ctx["env"]
|
|
115
|
+
if ctx.get("stdin_is_tty") and not env.get(SESSION_ENV):
|
|
116
|
+
ctx["out"]("cdx notify is called by provider hooks. Enable agent alerts with: cdx set <name> --notify on\n")
|
|
117
|
+
return 0
|
|
85
118
|
if reader is not None and not ctx.get("stdin_is_tty"):
|
|
86
119
|
stdin_text = reader.read()
|
|
87
|
-
|
|
88
|
-
composed = compose_notification(
|
|
120
|
+
payload = read_hook_payload(rest, stdin_text)
|
|
121
|
+
composed = compose_notification(payload, env, ctx.get("cwd"))
|
|
89
122
|
if composed:
|
|
90
|
-
|
|
123
|
+
# A live tray becomes the sole owner of this alert, so the user sees
|
|
124
|
+
# one notification rather than two. Publication happens below the
|
|
125
|
+
# composition boundary on purpose: the tray receives the same
|
|
126
|
+
# already-sanitized title and message the direct path would send,
|
|
127
|
+
# and cannot be handed anything the privacy rules removed.
|
|
128
|
+
if not _published_to_tray(ctx, composed, payload):
|
|
129
|
+
send_desktop_notification(*composed, spawn_sync=ctx.get("spawn_sync"), env=env)
|
|
91
130
|
except Exception: # noqa: BLE001 - deliberate: the caller is an agent turn
|
|
92
131
|
pass
|
|
93
132
|
return 0
|
|
@@ -98,6 +137,8 @@ def launch_notify_env(session, enabled, env=None):
|
|
|
98
137
|
values = {SESSION_ENV: session["name"]}
|
|
99
138
|
if not enabled:
|
|
100
139
|
values[ENABLED_ENV] = "0"
|
|
140
|
+
elif (session.get("launch") or {}).get("notify_preview") is True:
|
|
141
|
+
values[PREVIEW_ENV] = "1"
|
|
101
142
|
return values
|
|
102
143
|
|
|
103
144
|
|
|
@@ -130,6 +171,8 @@ def provision(auth_home, provider, enabled, env=None, spawn_sync=None, base_dir=
|
|
|
130
171
|
# Nothing could be delivered on this host, so installing a hook would only
|
|
131
172
|
# buy the user an approval prompt for a feature that shows nothing.
|
|
132
173
|
enabled = False
|
|
174
|
+
if not supports_agent_alerts(provider):
|
|
175
|
+
return False
|
|
133
176
|
command = resolve_hook_command(env)
|
|
134
177
|
if provider == "claude":
|
|
135
178
|
return _apply_hooks(hook_config_path(auth_home, provider), enabled, command)
|
|
@@ -224,7 +267,7 @@ def _write_codex_plugin(auth_home, command, base_dir=None):
|
|
|
224
267
|
os.path.join(root, "hooks", "cdx-hooks.json"): {
|
|
225
268
|
"hooks": {
|
|
226
269
|
event: [{"hooks": [{"type": "command", "command": f"{command} {HOOK_ARG}"}]}]
|
|
227
|
-
for event in ("Stop", "
|
|
270
|
+
for event in ("Stop", "PermissionRequest")
|
|
228
271
|
},
|
|
229
272
|
},
|
|
230
273
|
}
|
|
@@ -322,3 +365,24 @@ def _write_json(path, document):
|
|
|
322
365
|
# An unwritable home is a skipped provisioning step, not a failed launch.
|
|
323
366
|
return False
|
|
324
367
|
return True
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _published_to_tray(ctx, composed, payload):
|
|
371
|
+
"""Hand the alert to a live tray, or say we did not.
|
|
372
|
+
|
|
373
|
+
False is the safe answer to everything: no base directory, no heartbeat, a
|
|
374
|
+
stale one, a schema mismatch, or an unwritable spool all mean the direct
|
|
375
|
+
path still runs. The only way to lose a notification here would be to
|
|
376
|
+
return True without having written it.
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
base_dir = (ctx.get("service") or {}).get("base_dir")
|
|
380
|
+
if not base_dir:
|
|
381
|
+
return False
|
|
382
|
+
event = str(
|
|
383
|
+
payload.get("hook_event_name") or payload.get("eventName") or payload.get("type") or ""
|
|
384
|
+
).lower().replace("_", "").replace("-", "")
|
|
385
|
+
kind = "attention" if event in _WAITING_EVENTS else "complete"
|
|
386
|
+
return publish(base_dir, composed[0], composed[1], kind=kind)
|
|
387
|
+
except Exception: # noqa: BLE001 - the caller is an agent turn
|
|
388
|
+
return False
|
package/src/cli.py
CHANGED
|
@@ -51,6 +51,7 @@ from .cli_commands import (
|
|
|
51
51
|
handle_set,
|
|
52
52
|
handle_stats,
|
|
53
53
|
handle_status,
|
|
54
|
+
handle_tray,
|
|
54
55
|
handle_unset,
|
|
55
56
|
handle_update,
|
|
56
57
|
handle_view,
|
|
@@ -59,6 +60,7 @@ from .cli_commands import (
|
|
|
59
60
|
_format_bytes as _format_disk_bytes,
|
|
60
61
|
)
|
|
61
62
|
from .cli_helpers import _update_notice_warnings
|
|
63
|
+
from .cli_hints import write_hint
|
|
62
64
|
from .cli_render import (
|
|
63
65
|
_format_sessions,
|
|
64
66
|
_pad_table,
|
|
@@ -171,6 +173,7 @@ _COMMAND_HANDLERS = {
|
|
|
171
173
|
"set": handle_set,
|
|
172
174
|
"stats": handle_stats,
|
|
173
175
|
"status": handle_status,
|
|
176
|
+
"tray": handle_tray,
|
|
174
177
|
"unset": handle_unset,
|
|
175
178
|
"update": handle_update,
|
|
176
179
|
"view": handle_view,
|
|
@@ -231,8 +234,8 @@ def _print_help(use_color=False):
|
|
|
231
234
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
232
235
|
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
233
236
|
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
234
|
-
f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
|
|
235
|
-
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
237
|
+
f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--notify-preview on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
|
|
238
|
+
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
236
239
|
f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
237
240
|
f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
238
241
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
@@ -257,9 +260,15 @@ def _print_help(use_color=False):
|
|
|
257
260
|
f" {_style('cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--check-provider-flags] [--json]', '36', use_color)}",
|
|
258
261
|
f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
|
|
259
262
|
f" {_style('cdx view [--json]', '36', use_color)}",
|
|
263
|
+
f" {_style('cdx tray status [--json] [--refresh]', '36', use_color)}",
|
|
264
|
+
f" {_style('cdx tray install|launch|uninstall|doctor [--json]', '36', use_color)}",
|
|
265
|
+
f" {_style('cdx tray autostart [on|off|status] [--json]', '36', use_color)}",
|
|
260
266
|
f" {_style('cdx update [all] [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
|
|
261
267
|
f" {_style('cdx ready [--refresh] [--json]', '36', use_color)}",
|
|
262
|
-
|
|
268
|
+
"",
|
|
269
|
+
_style("Notifications:", "1", use_color),
|
|
270
|
+
f" {_style('cdx set <name> --notify on', '36', use_color)} # agent finishes or needs attention",
|
|
271
|
+
f" {_style('cdx ready', '36', use_color)} # next session to become ready",
|
|
263
272
|
f" {_style('cdx <name> [--json]', '36', use_color)}",
|
|
264
273
|
f" {_style('cdx --help', '36', use_color)}",
|
|
265
274
|
f" {_style('cdx --version', '36', use_color)}",
|
|
@@ -548,6 +557,8 @@ def main(argv, options=None):
|
|
|
548
557
|
warning_text = _update_warning_text(notices)
|
|
549
558
|
if warning_text:
|
|
550
559
|
out(f"{_style(warning_text, '33', use_color)}\n")
|
|
560
|
+
# Last line, after anything the user actually asked for.
|
|
561
|
+
write_hint({"env": env, "out": out, "service": service, "use_color": use_color, "update_notices": notices})
|
|
551
562
|
return 0
|
|
552
563
|
|
|
553
564
|
command, *rest = argv
|
package/src/cli_args.py
CHANGED
|
@@ -63,6 +63,7 @@ RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
|
|
|
63
63
|
RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
|
|
64
64
|
RUN_TAIL_USAGE = "Usage: cdx run-tail <run_id> [--lines N] --json"
|
|
65
65
|
SCHEMA_USAGE = "Usage: cdx schema --json"
|
|
66
|
+
TRAY_USAGE = "Usage: cdx tray status [--json] [--refresh] [--beat] | cdx tray install|launch|uninstall|doctor [--json] | cdx tray autostart [on|off|status] [--json] | cdx tray events [--json] | cdx tray ack <id>... [--json] | cdx tray heartbeat [--json]"
|
|
66
67
|
|
|
67
68
|
RUN_TAIL_DEFAULT_LINES = 50
|
|
68
69
|
RUN_TAIL_MAX_LINES = 2000
|
|
@@ -249,6 +250,7 @@ def _parse_set_args(args):
|
|
|
249
250
|
"--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
250
251
|
"--logics": {"key": "logics", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
251
252
|
"--notify": {"key": "notify", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
253
|
+
"--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
252
254
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
253
255
|
"--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
|
|
254
256
|
"--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
|
|
@@ -270,7 +272,7 @@ def _parse_set_args(args):
|
|
|
270
272
|
raise CdxError(SET_USAGE)
|
|
271
273
|
settings = {
|
|
272
274
|
key: parsed[key]
|
|
273
|
-
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "model", "fallback_model", "budget", "extra_args", "priority")
|
|
275
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "model", "fallback_model", "budget", "extra_args", "priority")
|
|
274
276
|
if parsed[key] is not None
|
|
275
277
|
}
|
|
276
278
|
if not settings:
|
|
@@ -293,6 +295,7 @@ def _parse_unset_args(args):
|
|
|
293
295
|
"--rtk": {"key": "rtk", "type": "bool", "default": False},
|
|
294
296
|
"--logics": {"key": "logics", "type": "bool", "default": False},
|
|
295
297
|
"--notify": {"key": "notify", "type": "bool", "default": False},
|
|
298
|
+
"--notify-preview": {"key": "notify_preview", "type": "bool", "default": False},
|
|
296
299
|
"--model": {"key": "model", "type": "bool", "default": False},
|
|
297
300
|
"--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
|
|
298
301
|
"--budget": {"key": "budget", "type": "bool", "default": False},
|
|
@@ -314,7 +317,7 @@ def _parse_unset_args(args):
|
|
|
314
317
|
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
315
318
|
raise CdxError(UNSET_USAGE)
|
|
316
319
|
_UNSET_KEYS = (
|
|
317
|
-
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify",
|
|
320
|
+
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview",
|
|
318
321
|
"model", "fallback_model", "budget", "extra_args", "priority",
|
|
319
322
|
)
|
|
320
323
|
keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
|
package/src/cli_commands.py
CHANGED
package/src/cli_helpers.py
CHANGED
|
@@ -486,7 +486,8 @@ def _format_launch_config(session, use_color=False):
|
|
|
486
486
|
("fast", "Fast"),
|
|
487
487
|
("rtk", "RTK"),
|
|
488
488
|
("logics", "Logics"),
|
|
489
|
-
("notify", "
|
|
489
|
+
("notify", "Agent alerts"),
|
|
490
|
+
("notify_preview", "Response preview"),
|
|
490
491
|
("model", "Model"),
|
|
491
492
|
("fallback_model", "Fallback"),
|
|
492
493
|
("budget", "Budget"),
|
|
@@ -509,18 +510,18 @@ def _format_launch_config(session, use_color=False):
|
|
|
509
510
|
def _format_launch_settings_hint(name="<name>"):
|
|
510
511
|
return (
|
|
511
512
|
f"Set a value: cdx set {name} --power medium --permission auto "
|
|
512
|
-
"--fast on --rtk on --logics on --notify on --model MODEL --priority 80"
|
|
513
|
+
"--fast on --rtk on --logics on --notify on --notify-preview on --model MODEL --priority 80"
|
|
513
514
|
)
|
|
514
515
|
|
|
515
516
|
def _format_launch_setting_value(launch, key, use_color=False):
|
|
516
|
-
if key in ("fast", "rtk", "logics", "notify"):
|
|
517
|
+
if key in ("fast", "rtk", "logics", "notify", "notify_preview"):
|
|
517
518
|
if launch.get(key) is True:
|
|
518
519
|
return _style("on", "32", use_color)
|
|
519
520
|
if launch.get(key) is False:
|
|
520
521
|
return _style("off", "2", use_color)
|
|
521
522
|
if key == "logics":
|
|
522
523
|
return _dim("auto", use_color)
|
|
523
|
-
if key
|
|
524
|
+
if key in ("notify", "notify_preview"):
|
|
524
525
|
return _style("off", "2", use_color)
|
|
525
526
|
return _dim("default", use_color)
|
|
526
527
|
value = launch.get(key)
|