cdx-manager 0.6.1 → 0.6.3
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 +25 -10
- package/changelogs/CHANGELOGS_0_6_2.md +42 -0
- package/changelogs/CHANGELOGS_0_6_3.md +28 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_usage.py +52 -2
- package/src/cli.py +10 -5
- package/src/cli_commands.py +283 -31
- package/src/cli_render.py +1 -1
- package/src/config.py +3 -1
- package/src/provider_runtime.py +146 -9
- package/src/session_service.py +39 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
|
-
**Run multiple Codex and
|
|
5
|
+
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
7
7
|
If you use AI coding tools at scale ; multiple accounts, multiple providers : you know the friction: re-authenticating, losing context, juggling environment variables. `cdx` removes all of that.
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ One command to launch any session. Zero auth juggling.
|
|
|
32
32
|
|
|
33
33
|
## What it does
|
|
34
34
|
|
|
35
|
-
- **Multiple
|
|
35
|
+
- **Multiple providers, one tool.** Register as many Codex, Claude, Antigravity, or Ollama sessions as you need. Codex and Claude get isolated auth environments; Antigravity is launchable through `agy` with OS-keyring auth; Ollama runs local models through `ollama run`.
|
|
36
36
|
- **Instant launch.** `cdx work` opens your "work" session. `cdx personal` opens another. No config files to edit mid-flow.
|
|
37
37
|
- **Quick relaunch.** `cdx last` reopens the most recently launched assistant profile.
|
|
38
38
|
- **Auth guardrails.** `cdx` checks authentication before launching. If a session is not logged in, it tells you exactly what to run — no silent failures.
|
|
@@ -54,7 +54,9 @@ One command to launch any session. Zero auth juggling.
|
|
|
54
54
|
- Python 3.9+, zero runtime dependencies.
|
|
55
55
|
- Environment isolation per session:
|
|
56
56
|
- Codex sessions override `CODEX_HOME` to a dedicated profile directory.
|
|
57
|
-
- Claude sessions override `HOME` to a dedicated profile directory.
|
|
57
|
+
- Claude sessions override `HOME` to a dedicated profile directory and disable Claude Code commit co-author attribution by default.
|
|
58
|
+
- Antigravity sessions launch `agy` with a dedicated `HOME` for file-based settings; Google account credentials may still be stored in the OS keyring by Antigravity itself.
|
|
59
|
+
- Ollama sessions use the local Ollama server and launch `ollama run <model>`; set a model with `cdx set <name> --model <model>`.
|
|
58
60
|
- New Codex sessions seed their auth home from your existing global `~/.codex/auth.json` when available, so an already logged-in Codex CLI can be reused without giving up per-session isolation afterward.
|
|
59
61
|
- Persistence:
|
|
60
62
|
- Session registry at `~/.cdx/sessions.json` (versioned JSON store).
|
|
@@ -82,7 +84,7 @@ One command to launch any session. Zero auth juggling.
|
|
|
82
84
|
|
|
83
85
|
- Node.js 18+ with npm
|
|
84
86
|
- Python 3.9+
|
|
85
|
-
- `codex` and/or `
|
|
87
|
+
- `codex`, `claude`, `agy`, and/or `ollama` CLI installed and available in your PATH
|
|
86
88
|
|
|
87
89
|
On Windows, the npm launcher looks for Python in this order: `py -3`, `python`, then `python3`. Make sure at least one of those commands resolves to Python 3.
|
|
88
90
|
|
|
@@ -132,7 +134,7 @@ For a specific version:
|
|
|
132
134
|
|
|
133
135
|
```bash
|
|
134
136
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
135
|
-
CDX_VERSION=v0.6.
|
|
137
|
+
CDX_VERSION=v0.6.3 sh install.sh
|
|
136
138
|
```
|
|
137
139
|
|
|
138
140
|
From source:
|
|
@@ -227,6 +229,9 @@ cdx add work
|
|
|
227
229
|
# Register a Claude session
|
|
228
230
|
cdx add claude personal
|
|
229
231
|
|
|
232
|
+
# Register an Ollama session
|
|
233
|
+
cdx add ollama local --model llama3.2
|
|
234
|
+
|
|
230
235
|
# List all sessions
|
|
231
236
|
cdx
|
|
232
237
|
|
|
@@ -256,6 +261,11 @@ By default, `cdx` launches provider CLIs without forcing model effort, permissio
|
|
|
256
261
|
```bash
|
|
257
262
|
cdx set work --power medium --permission full --fast off
|
|
258
263
|
cdx set personal --power low --permission review
|
|
264
|
+
cdx set --sessions all --permission auto
|
|
265
|
+
cdx set --provider ollama --model llama3.2
|
|
266
|
+
cdx power all low
|
|
267
|
+
cdx perm provider:claude review
|
|
268
|
+
cdx model provider:ollama llama3.2
|
|
259
269
|
cdx config work
|
|
260
270
|
```
|
|
261
271
|
|
|
@@ -263,7 +273,11 @@ Those values are stored on the session and reapplied every time you run `cdx wor
|
|
|
263
273
|
|
|
264
274
|
```bash
|
|
265
275
|
cdx unset work --power
|
|
276
|
+
cdx unset --sessions work,personal --fast
|
|
277
|
+
cdx unset --provider claude --permission
|
|
266
278
|
cdx unset work --all
|
|
279
|
+
cdx power all default
|
|
280
|
+
cdx model provider:ollama default
|
|
267
281
|
```
|
|
268
282
|
|
|
269
283
|
`--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` uses low effort when no explicit power is set.
|
|
@@ -294,7 +308,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
294
308
|
| `cdx --json` | List all sessions as a machine-readable JSON payload |
|
|
295
309
|
| `cdx <name>` | Launch a session (checks auth first) |
|
|
296
310
|
| `cdx <name> [--json]` | Launch a session; `--json` returns a structured success payload after the interactive run ends |
|
|
297
|
-
| `cdx add [provider] <name> [--json]` | Register a new session (`provider`: `codex` or `
|
|
311
|
+
| `cdx add [provider] <name> [--model MODEL] [--json]` | Register a new session (`provider`: `codex`, `claude`, `antigravity`, or `ollama`; Ollama requires `--model`) |
|
|
298
312
|
| `cdx cp <source> <dest> [--json]` | Copy a session into another session name, overwriting the destination if it exists |
|
|
299
313
|
| `cdx ren <source> <dest> [--json]` | Rename a session and move its auth data |
|
|
300
314
|
| `cdx login <name> [--json]` | Re-authenticate a session (logout + login) |
|
|
@@ -302,13 +316,14 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
302
316
|
| `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
|
|
303
317
|
| `cdx enable <name> [--json]` | Re-enable a disabled session |
|
|
304
318
|
| `cdx config <name> [--json]` | Show persistent launch settings for a session |
|
|
305
|
-
| `cdx
|
|
306
|
-
| `cdx
|
|
319
|
+
| `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
|
|
320
|
+
| `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power low\|medium\|high\|xhigh\|max] [--permission review\|default\|auto\|full] [--fast on\|off] [--model MODEL] [--json]` | Persist launch settings for one or more sessions |
|
|
321
|
+
| `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--model\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
|
|
307
322
|
| `cdx history [name] [--limit N] [--summary] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Show recent launch history or aggregate total launch time per assistant, optionally filtered by period |
|
|
308
323
|
| `cdx last [--json]` | Launch the most recent existing session from launch history |
|
|
309
324
|
| `cdx context show\|path\|init\|edit\|clear\|set [text...] [--json]` | Manage the shared Markdown context for the current workspace |
|
|
310
325
|
| `cdx handoff <name> [--json]` | Install the current workspace context into a target session and launch it unless `--json` is used |
|
|
311
|
-
| `cdx handoff <source> <target> [--json]` | Build shared context from the source session's latest launch transcript, install it into the target session, and launch the target unless `--json` is used; supports
|
|
326
|
+
| `cdx handoff <source> <target> [--json]` | Build shared context from the source session's latest launch transcript, install it into the target session, and launch the target unless `--json` is used; supports cross-provider handoff |
|
|
312
327
|
| `cdx rmv <name> [--force] [--json]` | Remove a session and its auth data (prompts for confirmation unless `--force`) |
|
|
313
328
|
| `cdx clean [name] [--json]` | Clear launch transcript logs for one session or all sessions |
|
|
314
329
|
| `cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Export sessions to a portable bundle; `--include-auth` encrypts auth data with a passphrase |
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog (`0.6.1 -> 0.6.2`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-29
|
|
4
|
+
|
|
5
|
+
## Provider Support
|
|
6
|
+
|
|
7
|
+
- Added Antigravity provider support through the `agy` CLI with isolated session homes, launch settings, and transcript capture.
|
|
8
|
+
- Added Ollama provider support through `ollama run`, including model-backed session configuration and transcript capture.
|
|
9
|
+
- Added explicit `cdx add ollama <name> --model MODEL` registration so Ollama sessions do not depend on ambiguous session-name model defaults.
|
|
10
|
+
|
|
11
|
+
## Launch Settings
|
|
12
|
+
|
|
13
|
+
- Added bulk launch setting updates for all sessions, provider-filtered sessions, and named session subsets.
|
|
14
|
+
- Added shortcut launch setting commands for power, permission, fast mode, and model changes.
|
|
15
|
+
- Updated help and README command documentation for provider launch settings.
|
|
16
|
+
|
|
17
|
+
## Claude Behavior
|
|
18
|
+
|
|
19
|
+
- Fixed Claude auth reuse and status refresh behavior.
|
|
20
|
+
- Disabled Claude commit attribution by default for managed Claude sessions.
|
|
21
|
+
|
|
22
|
+
## Runtime Reliability
|
|
23
|
+
|
|
24
|
+
- Set `OLLAMA_NOHISTORY=1` for Ollama launch, status, and auth-check operations to avoid local history-file permission failures.
|
|
25
|
+
- Preserved provider-specific launch arguments across the transcript wrapper and fallback paths.
|
|
26
|
+
|
|
27
|
+
## Release Metadata and Documentation
|
|
28
|
+
|
|
29
|
+
- Updated package metadata, CLI version output, README badge, and pinned installer example to `v0.6.2`.
|
|
30
|
+
|
|
31
|
+
## Validation and Regression Coverage
|
|
32
|
+
|
|
33
|
+
- Added coverage for Antigravity launch specs and auth checks.
|
|
34
|
+
- Added coverage for Ollama session creation, model persistence, launch specs, and no-history environment handling.
|
|
35
|
+
- Added coverage for bulk and shortcut launch setting commands.
|
|
36
|
+
- Added coverage for Claude auth/status behavior and commit-attribution defaults.
|
|
37
|
+
|
|
38
|
+
## Validation and Regression Evidence
|
|
39
|
+
|
|
40
|
+
- `npm run lint`
|
|
41
|
+
- `npm test`
|
|
42
|
+
- `npm pack --dry-run`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog (`0.6.2 -> 0.6.3`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-29
|
|
4
|
+
|
|
5
|
+
## Claude Authentication
|
|
6
|
+
|
|
7
|
+
- Fixed Claude Code 2.1.145 isolated auth handling by using `ANTHROPIC_CONFIG_DIR` instead of the older `CLAUDE_CONFIG_DIR` override.
|
|
8
|
+
- Removed leaked `CODEX_HOME` from Claude auth, launch, and status environments so Claude sessions do not write nested `.cdx` state inside isolated Claude homes.
|
|
9
|
+
- Stopped running a destructive Claude logout before `cdx login <name>`, preserving existing credentials when reauthenticating a session.
|
|
10
|
+
- Added profile email hints to Claude login so account-specific sessions open the expected Claude account in the browser.
|
|
11
|
+
|
|
12
|
+
## Claude Token Fallback
|
|
13
|
+
|
|
14
|
+
- Added automatic `claude setup-token` fallback when browser login completes but does not create isolated credentials.
|
|
15
|
+
- Captured the one-time setup token through a temporary transcript, wrote it to `claude-home/credentials/default.json`, and removed the temporary transcript after extraction.
|
|
16
|
+
- Added support for the new Anthropic `credentials/default.json` OAuth format during auth probes, launches, status refreshes, and auth bundle exports.
|
|
17
|
+
|
|
18
|
+
## Release Metadata and Documentation
|
|
19
|
+
|
|
20
|
+
- Updated package metadata, CLI version output, README badge, and pinned installer example to `v0.6.3`.
|
|
21
|
+
|
|
22
|
+
## Validation and Regression Coverage
|
|
23
|
+
|
|
24
|
+
- Added regression coverage for Claude login without pre-logout, email-hinted login, setup-token fallback, modern Anthropic credentials, and cleaned Claude environments.
|
|
25
|
+
|
|
26
|
+
## Validation and Regression Evidence
|
|
27
|
+
|
|
28
|
+
- `python3 -m unittest discover -s test -p 'test_*_py.py'`
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
"v0.4.0": {
|
|
9
9
|
"github_tarball_sha256": "47a99ff2663f4fee33339098d2cfe7c27fbd0f74a16a1f6711f3003287409ae8",
|
|
10
10
|
"github_zip_sha256": "ae7eff748e569c621ef203abb6b395fbf893370471e962252e8c781e7751e5c8"
|
|
11
|
+
},
|
|
12
|
+
"v0.6.1": {
|
|
13
|
+
"github_tarball_sha256": "5ae4032894e1806ffb3a713e555c12b2faa3bfa6fd7c9fa468664a8577abe827",
|
|
14
|
+
"github_zip_sha256": "7949e99dcfe21dc4e2b82f720b515aea8f0ac9f2059142032d488bcfbc4dfaf1"
|
|
15
|
+
},
|
|
16
|
+
"v0.6.2": {
|
|
17
|
+
"github_tarball_sha256": "0a00fba132453d265a72fa551bff750a9a400011f895d9da33149b335324e02e",
|
|
18
|
+
"github_zip_sha256": "424b1c9652da5054bdc4cf26f31764ea34ddc4609d6eb8075efbf8068112cd50"
|
|
11
19
|
}
|
|
12
20
|
}
|
|
13
21
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/claude_usage.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
3
5
|
import urllib.request
|
|
4
6
|
import urllib.error
|
|
5
7
|
from datetime import datetime, timezone
|
|
@@ -9,9 +11,20 @@ from .errors import CdxError
|
|
|
9
11
|
MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
10
12
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
11
13
|
CLAUDE_STATUS_PROBE_MODEL = os.environ.get("CDX_CLAUDE_STATUS_MODEL", "claude-haiku-4-5-20251001")
|
|
14
|
+
CLAUDE_AUTH_STATUS_TIMEOUT_SECONDS = 15
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def _read_claude_credentials(auth_home):
|
|
18
|
+
anthropic_cred_path = os.path.join(auth_home, "credentials", "default.json")
|
|
19
|
+
try:
|
|
20
|
+
with open(anthropic_cred_path, "r", encoding="utf-8") as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
token = data.get("access_token") if isinstance(data, dict) else None
|
|
23
|
+
if token:
|
|
24
|
+
return {"accessToken": token}
|
|
25
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
15
28
|
cred_path = os.path.join(auth_home, ".claude", ".credentials.json")
|
|
16
29
|
try:
|
|
17
30
|
with open(cred_path, "r", encoding="utf-8") as f:
|
|
@@ -21,6 +34,40 @@ def _read_claude_credentials(auth_home):
|
|
|
21
34
|
return None
|
|
22
35
|
|
|
23
36
|
|
|
37
|
+
def _home_env_overrides(auth_home):
|
|
38
|
+
overrides = {
|
|
39
|
+
"HOME": auth_home,
|
|
40
|
+
"ANTHROPIC_CONFIG_DIR": auth_home,
|
|
41
|
+
"CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS": "1",
|
|
42
|
+
}
|
|
43
|
+
if sys.platform == "win32":
|
|
44
|
+
overrides["USERPROFILE"] = auth_home
|
|
45
|
+
overrides["HOMEDRIVE"] = os.path.splitdrive(auth_home)[0] or "C:"
|
|
46
|
+
overrides["HOMEPATH"] = os.path.splitdrive(auth_home)[1] or auth_home
|
|
47
|
+
return overrides
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _refresh_claude_cli_credentials(auth_home, runner=None, env=None):
|
|
51
|
+
if not auth_home:
|
|
52
|
+
return
|
|
53
|
+
if os.environ.get("CDX_CLAUDE_SKIP_AUTH_STATUS_REFRESH") == "1":
|
|
54
|
+
return
|
|
55
|
+
runner = runner or subprocess.run
|
|
56
|
+
command = ["claude", "auth", "status"]
|
|
57
|
+
run_env = {**(env or os.environ), **_home_env_overrides(auth_home)}
|
|
58
|
+
try:
|
|
59
|
+
runner(
|
|
60
|
+
command,
|
|
61
|
+
env=run_env,
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=CLAUDE_AUTH_STATUS_TIMEOUT_SECONDS,
|
|
65
|
+
check=False,
|
|
66
|
+
)
|
|
67
|
+
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
|
|
24
71
|
def _format_reset_date(unix_seconds):
|
|
25
72
|
dt = datetime.fromtimestamp(unix_seconds, tz=timezone.utc).astimezone()
|
|
26
73
|
return f"{MONTH_ABBR[dt.month - 1]} {dt.day} {str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}"
|
|
@@ -109,8 +156,11 @@ def fetch_claude_rate_limit_headers(access_token):
|
|
|
109
156
|
}
|
|
110
157
|
|
|
111
158
|
|
|
112
|
-
def refresh_claude_session_status(session):
|
|
113
|
-
|
|
159
|
+
def refresh_claude_session_status(session, auth_refresher=None):
|
|
160
|
+
auth_home = session.get("authHome", "")
|
|
161
|
+
refresher = auth_refresher or _refresh_claude_cli_credentials
|
|
162
|
+
refresher(auth_home)
|
|
163
|
+
creds = _read_claude_credentials(auth_home)
|
|
114
164
|
if not creds or not creds.get("accessToken"):
|
|
115
165
|
return None
|
|
116
166
|
return fetch_claude_rate_limit_headers(creds["accessToken"])
|
package/src/cli.py
CHANGED
|
@@ -23,6 +23,7 @@ from .cli_commands import (
|
|
|
23
23
|
handle_launch,
|
|
24
24
|
handle_login,
|
|
25
25
|
handle_logout,
|
|
26
|
+
handle_launch_setting_alias,
|
|
26
27
|
handle_notify,
|
|
27
28
|
handle_remove,
|
|
28
29
|
handle_repair,
|
|
@@ -54,7 +55,7 @@ from .status_view import (
|
|
|
54
55
|
)
|
|
55
56
|
from .update_check import check_for_update
|
|
56
57
|
|
|
57
|
-
VERSION = "0.6.
|
|
58
|
+
VERSION = "0.6.3"
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
# ---------------------------------------------------------------------------
|
|
@@ -73,13 +74,14 @@ def _print_help(use_color=False):
|
|
|
73
74
|
f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
|
|
74
75
|
f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
|
|
75
76
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
76
|
-
f" {_style('cdx
|
|
77
|
-
f" {_style('cdx
|
|
77
|
+
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
78
|
+
f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--model MODEL] [--json]', '36', use_color)}",
|
|
79
|
+
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--all) [--json]', '36', use_color)}",
|
|
78
80
|
f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
79
81
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
80
82
|
f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
|
|
81
83
|
f" {_style('cdx handoff <source> <target> [--json]', '36', use_color)}",
|
|
82
|
-
f" {_style('cdx add [provider] <name> [--json]', '36', use_color)}",
|
|
84
|
+
f" {_style('cdx add [provider] <name> [--model MODEL] [--json]', '36', use_color)}",
|
|
83
85
|
f" {_style('cdx cp <source> <dest> [--json]', '36', use_color)}",
|
|
84
86
|
f" {_style('cdx ren <source> <dest> [--json]', '36', use_color)}",
|
|
85
87
|
f" {_style('cdx login <name> [--json]', '36', use_color)}",
|
|
@@ -231,7 +233,7 @@ def main(argv, options=None):
|
|
|
231
233
|
"version": VERSION,
|
|
232
234
|
"cwd": options.get("cwd") or os.getcwd(),
|
|
233
235
|
"update_notice": _get_update_notice(service, env, options) if command not in (
|
|
234
|
-
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "context", "config", "set", "unset", "history", "handoff", "login", "logout", "disable", "enable", "export", "import", "help", "version"
|
|
236
|
+
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "context", "config", "set", "unset", "power", "perm", "fast", "model", "history", "handoff", "login", "logout", "disable", "enable", "export", "import", "help", "version"
|
|
235
237
|
) else None,
|
|
236
238
|
"use_color": use_color,
|
|
237
239
|
}
|
|
@@ -292,6 +294,9 @@ def main(argv, options=None):
|
|
|
292
294
|
if command == "unset":
|
|
293
295
|
return handle_unset(rest, ctx)
|
|
294
296
|
|
|
297
|
+
if command in ("power", "perm", "fast", "model"):
|
|
298
|
+
return handle_launch_setting_alias(command, rest, ctx)
|
|
299
|
+
|
|
295
300
|
if command == "history":
|
|
296
301
|
return handle_history(rest, ctx)
|
|
297
302
|
|
package/src/cli_commands.py
CHANGED
|
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
|
|
|
9
9
|
|
|
10
10
|
from .claude_refresh import _refresh_claude_sessions
|
|
11
11
|
from .cli_render import _dim, _info, _success, _warn
|
|
12
|
-
from .config import PROVIDER_CODEX
|
|
12
|
+
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA, PROVIDERS
|
|
13
13
|
from .context_store import (
|
|
14
14
|
clear_context,
|
|
15
15
|
edit_context,
|
|
@@ -50,8 +50,9 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
|
|
|
50
50
|
IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
51
51
|
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
|
|
52
52
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
53
|
-
SET_USAGE = "Usage: cdx set <name
|
|
54
|
-
UNSET_USAGE = "Usage: cdx unset <name
|
|
53
|
+
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--model MODEL] [--json]"
|
|
54
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--all) [--json]"
|
|
55
|
+
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
55
56
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
56
57
|
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
57
58
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
@@ -413,17 +414,17 @@ def _parse_flag_args(args, schema, usage, positionals_key=None, max_positionals=
|
|
|
413
414
|
def _parse_add_args(args):
|
|
414
415
|
parsed = _parse_flag_args(
|
|
415
416
|
args,
|
|
416
|
-
{},
|
|
417
|
-
"Usage: cdx add [provider] <name> [--json]",
|
|
417
|
+
{"--model": {"key": "model", "type": "str", "default": None}},
|
|
418
|
+
"Usage: cdx add [provider] <name> [--model MODEL] [--json]",
|
|
418
419
|
positionals_key="values",
|
|
419
420
|
max_positionals=2,
|
|
420
421
|
)
|
|
421
422
|
values = parsed["values"]
|
|
422
423
|
if len(values) == 1:
|
|
423
|
-
return {"provider": PROVIDER_CODEX, "name": values[0]}
|
|
424
|
+
return {"provider": PROVIDER_CODEX, "name": values[0], "model": parsed["model"]}
|
|
424
425
|
if len(values) == 2:
|
|
425
|
-
return {"provider": values[0], "name": values[1]}
|
|
426
|
-
raise CdxError("Usage: cdx add [provider] <name> [--json]")
|
|
426
|
+
return {"provider": values[0], "name": values[1], "model": parsed["model"]}
|
|
427
|
+
raise CdxError("Usage: cdx add [provider] <name> [--model MODEL] [--json]")
|
|
427
428
|
|
|
428
429
|
|
|
429
430
|
def _parse_copy_args(args):
|
|
@@ -470,18 +471,33 @@ def _parse_set_args(args):
|
|
|
470
471
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
471
472
|
"--permission": {"key": "permission", "type": "str", "default": None},
|
|
472
473
|
"--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
474
|
+
"--model": {"key": "model", "type": "str", "default": None},
|
|
475
|
+
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
476
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
473
477
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
474
478
|
}, SET_USAGE, positionals_key="names", max_positionals=1)
|
|
475
|
-
if len(parsed["names"])
|
|
479
|
+
if len(parsed["names"]) > 1:
|
|
480
|
+
raise CdxError(SET_USAGE)
|
|
481
|
+
if parsed["names"] and parsed["sessions"]:
|
|
482
|
+
raise CdxError(SET_USAGE)
|
|
483
|
+
if parsed["names"] and parsed["provider"]:
|
|
484
|
+
raise CdxError(SET_USAGE)
|
|
485
|
+
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
476
486
|
raise CdxError(SET_USAGE)
|
|
477
487
|
settings = {
|
|
478
488
|
key: parsed[key]
|
|
479
|
-
for key in ("power", "permission", "fast")
|
|
489
|
+
for key in ("power", "permission", "fast", "model")
|
|
480
490
|
if parsed[key] is not None
|
|
481
491
|
}
|
|
482
492
|
if not settings:
|
|
483
493
|
raise CdxError(SET_USAGE)
|
|
484
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
496
|
+
"sessions": parsed["sessions"],
|
|
497
|
+
"provider": parsed["provider"],
|
|
498
|
+
"settings": settings,
|
|
499
|
+
"json": parsed["json"],
|
|
500
|
+
}
|
|
485
501
|
|
|
486
502
|
|
|
487
503
|
def _parse_unset_args(args):
|
|
@@ -489,17 +505,32 @@ def _parse_unset_args(args):
|
|
|
489
505
|
"--power": {"key": "power", "type": "bool", "default": False},
|
|
490
506
|
"--permission": {"key": "permission", "type": "bool", "default": False},
|
|
491
507
|
"--fast": {"key": "fast", "type": "bool", "default": False},
|
|
508
|
+
"--model": {"key": "model", "type": "bool", "default": False},
|
|
492
509
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
510
|
+
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
511
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
|
|
493
512
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
494
513
|
}, UNSET_USAGE, positionals_key="names", max_positionals=1)
|
|
495
|
-
if len(parsed["names"])
|
|
514
|
+
if len(parsed["names"]) > 1:
|
|
515
|
+
raise CdxError(UNSET_USAGE)
|
|
516
|
+
if parsed["names"] and parsed["sessions"]:
|
|
517
|
+
raise CdxError(UNSET_USAGE)
|
|
518
|
+
if parsed["names"] and parsed["provider"]:
|
|
519
|
+
raise CdxError(UNSET_USAGE)
|
|
520
|
+
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
496
521
|
raise CdxError(UNSET_USAGE)
|
|
497
|
-
keys = ["power", "permission", "fast"] if parsed["all"] else [
|
|
498
|
-
key for key in ("power", "permission", "fast") if parsed[key]
|
|
522
|
+
keys = ["power", "permission", "fast", "model"] if parsed["all"] else [
|
|
523
|
+
key for key in ("power", "permission", "fast", "model") if parsed[key]
|
|
499
524
|
]
|
|
500
525
|
if not keys:
|
|
501
526
|
raise CdxError(UNSET_USAGE)
|
|
502
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
529
|
+
"sessions": parsed["sessions"],
|
|
530
|
+
"provider": parsed["provider"],
|
|
531
|
+
"keys": keys,
|
|
532
|
+
"json": parsed["json"],
|
|
533
|
+
}
|
|
503
534
|
|
|
504
535
|
|
|
505
536
|
def _parse_config_args(args):
|
|
@@ -674,6 +705,49 @@ def _parse_session_names(value):
|
|
|
674
705
|
return names
|
|
675
706
|
|
|
676
707
|
|
|
708
|
+
def _parse_set_unset_sessions(value):
|
|
709
|
+
text = str(value or "").strip()
|
|
710
|
+
if not text:
|
|
711
|
+
raise CdxError("At least one session name is required in --sessions.")
|
|
712
|
+
if text.lower() == "all":
|
|
713
|
+
return "all"
|
|
714
|
+
return _parse_session_names(text)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _parse_provider_filter(value, usage):
|
|
718
|
+
provider = str(value or "").strip()
|
|
719
|
+
if provider not in PROVIDERS:
|
|
720
|
+
raise CdxError(usage)
|
|
721
|
+
return provider
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _parse_launch_setting_target(value, usage):
|
|
725
|
+
text = str(value or "").strip()
|
|
726
|
+
if not text:
|
|
727
|
+
raise CdxError(usage)
|
|
728
|
+
if text == "all":
|
|
729
|
+
return {"name": None, "sessions": "all", "provider": None}
|
|
730
|
+
if text.startswith("provider:"):
|
|
731
|
+
return {"name": None, "sessions": None, "provider": _parse_provider_filter(text.split(":", 1)[1], usage)}
|
|
732
|
+
if "," in text:
|
|
733
|
+
return {"name": None, "sessions": _parse_session_names(text), "provider": None}
|
|
734
|
+
return {"name": text, "sessions": None, "provider": None}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _parse_launch_setting_alias_args(kind, args):
|
|
738
|
+
json_flag, cleaned = _parse_json_flag(args)
|
|
739
|
+
if len(cleaned) != 2:
|
|
740
|
+
raise CdxError(SETTING_ALIAS_USAGE)
|
|
741
|
+
target = _parse_launch_setting_target(cleaned[0], SETTING_ALIAS_USAGE)
|
|
742
|
+
value = cleaned[1]
|
|
743
|
+
field = "permission" if kind == "perm" else kind
|
|
744
|
+
if value == "default":
|
|
745
|
+
return {**target, "keys": [field], "json": json_flag, "alias": kind}
|
|
746
|
+
if field == "fast":
|
|
747
|
+
value = _parse_fast_value(value)
|
|
748
|
+
return {**target, "settings": {field: value}, "json": json_flag, "alias": kind}
|
|
749
|
+
|
|
750
|
+
|
|
677
751
|
def _parse_update_args(args):
|
|
678
752
|
parsed = _parse_flag_args(args, {
|
|
679
753
|
"--check": {"key": "check", "type": "bool", "default": False},
|
|
@@ -763,10 +837,82 @@ def _resolve_confirmation(confirm_fn, name):
|
|
|
763
837
|
return confirmed
|
|
764
838
|
|
|
765
839
|
|
|
840
|
+
def _extract_claude_oauth_token(text):
|
|
841
|
+
if not text:
|
|
842
|
+
return None
|
|
843
|
+
patterns = [
|
|
844
|
+
r"CLAUDE_CODE_OAUTH_TOKEN=([^\s\"']+)",
|
|
845
|
+
r"(sk-ant-oat[0-9A-Za-z._-]+)",
|
|
846
|
+
]
|
|
847
|
+
for pattern in patterns:
|
|
848
|
+
match = re.search(pattern, text)
|
|
849
|
+
if match:
|
|
850
|
+
return match.group(1).strip()
|
|
851
|
+
return None
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _write_claude_oauth_token(auth_home, token):
|
|
855
|
+
cred_dir = os.path.join(auth_home, "credentials")
|
|
856
|
+
os.makedirs(cred_dir, exist_ok=True)
|
|
857
|
+
cred_path = os.path.join(cred_dir, "default.json")
|
|
858
|
+
payload = {
|
|
859
|
+
"version": "1.0",
|
|
860
|
+
"type": "oauth_token",
|
|
861
|
+
"access_token": token,
|
|
862
|
+
}
|
|
863
|
+
with open(cred_path, "w", encoding="utf-8") as handle:
|
|
864
|
+
json.dump(payload, handle, indent=2)
|
|
865
|
+
handle.write("\n")
|
|
866
|
+
try:
|
|
867
|
+
os.chmod(cred_path, 0o600)
|
|
868
|
+
except OSError:
|
|
869
|
+
pass
|
|
870
|
+
return cred_path
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _bootstrap_claude_setup_token(session, ctx):
|
|
874
|
+
ctx["out"](
|
|
875
|
+
"Claude login did not create isolated credentials; falling back to claude setup-token.\n"
|
|
876
|
+
)
|
|
877
|
+
run_info = _run_interactive_provider_command(
|
|
878
|
+
session,
|
|
879
|
+
"setup-token",
|
|
880
|
+
spawn=ctx.get("spawn"),
|
|
881
|
+
env_override=ctx.get("env"),
|
|
882
|
+
signal_emitter=ctx.get("signal_emitter"),
|
|
883
|
+
)
|
|
884
|
+
transcript_path = run_info.get("transcript_path")
|
|
885
|
+
if not transcript_path or not os.path.isfile(transcript_path):
|
|
886
|
+
raise CdxError(
|
|
887
|
+
"Claude setup-token completed, but cdx could not capture the token. "
|
|
888
|
+
"Run claude setup-token and save the token under credentials/default.json."
|
|
889
|
+
)
|
|
890
|
+
try:
|
|
891
|
+
with open(transcript_path, "r", encoding="utf-8", errors="replace") as handle:
|
|
892
|
+
transcript = handle.read()
|
|
893
|
+
finally:
|
|
894
|
+
try:
|
|
895
|
+
os.remove(transcript_path)
|
|
896
|
+
except OSError:
|
|
897
|
+
pass
|
|
898
|
+
token = _extract_claude_oauth_token(transcript)
|
|
899
|
+
if not token:
|
|
900
|
+
raise CdxError(
|
|
901
|
+
"Claude setup-token completed, but cdx could not find CLAUDE_CODE_OAUTH_TOKEN in the output."
|
|
902
|
+
)
|
|
903
|
+
return _write_claude_oauth_token(session.get("authHome") or "", token)
|
|
904
|
+
|
|
905
|
+
|
|
766
906
|
def handle_add(rest, ctx):
|
|
767
907
|
json_flag, args = _parse_json_flag(rest)
|
|
768
908
|
parsed = _parse_add_args(args)
|
|
909
|
+
if parsed["provider"] == PROVIDER_OLLAMA and not parsed.get("model"):
|
|
910
|
+
raise CdxError("Usage: cdx add ollama <name> --model MODEL [--json]")
|
|
911
|
+
if parsed.get("model") and parsed["provider"] != PROVIDER_OLLAMA:
|
|
912
|
+
raise CdxError("--model is only supported when adding an ollama session.")
|
|
769
913
|
session = ctx["service"]["create_session"](parsed["name"], parsed["provider"])
|
|
914
|
+
if parsed.get("model"):
|
|
915
|
+
session = ctx["service"]["set_launch_settings"](parsed["name"], {"model": parsed["model"]})
|
|
770
916
|
message = f"Created session {parsed['name']} ({parsed['provider']})"
|
|
771
917
|
_ensure_session_authentication(
|
|
772
918
|
session,
|
|
@@ -882,9 +1028,43 @@ def _format_launch_config(session):
|
|
|
882
1028
|
f"power: {launch.get('power') or 'default'}",
|
|
883
1029
|
f"permission: {launch.get('permission') or 'default'}",
|
|
884
1030
|
f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
|
|
1031
|
+
f"model: {launch.get('model') or 'default'}",
|
|
885
1032
|
])
|
|
886
1033
|
|
|
887
1034
|
|
|
1035
|
+
def _resolve_bulk_launch_targets(parsed, service):
|
|
1036
|
+
sessions = service["list_sessions"]()
|
|
1037
|
+
by_name = {session["name"]: session for session in sessions}
|
|
1038
|
+
if parsed.get("name"):
|
|
1039
|
+
session = by_name.get(parsed["name"])
|
|
1040
|
+
if not session:
|
|
1041
|
+
raise CdxError(f"Unknown session: {parsed['name']}")
|
|
1042
|
+
targets = [session]
|
|
1043
|
+
elif parsed.get("sessions") == "all":
|
|
1044
|
+
targets = sessions
|
|
1045
|
+
elif parsed.get("sessions"):
|
|
1046
|
+
targets = []
|
|
1047
|
+
for name in parsed["sessions"]:
|
|
1048
|
+
session = by_name.get(name)
|
|
1049
|
+
if not session:
|
|
1050
|
+
raise CdxError(f"Unknown session: {name}")
|
|
1051
|
+
targets.append(session)
|
|
1052
|
+
else:
|
|
1053
|
+
targets = sessions
|
|
1054
|
+
if parsed.get("provider"):
|
|
1055
|
+
targets = [session for session in targets if session["provider"] == parsed["provider"]]
|
|
1056
|
+
if not targets:
|
|
1057
|
+
raise CdxError("No sessions matched.")
|
|
1058
|
+
return targets
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def _format_bulk_launch_summary(sessions):
|
|
1062
|
+
names = [session["name"] for session in sessions]
|
|
1063
|
+
if len(names) <= 8:
|
|
1064
|
+
return ", ".join(names)
|
|
1065
|
+
return ", ".join(names[:8]) + f", +{len(names) - 8} more"
|
|
1066
|
+
|
|
1067
|
+
|
|
888
1068
|
def _format_duration_ms(value):
|
|
889
1069
|
if value is None:
|
|
890
1070
|
return "-"
|
|
@@ -1002,30 +1182,77 @@ def _format_history(entries, use_color=False):
|
|
|
1002
1182
|
])
|
|
1003
1183
|
|
|
1004
1184
|
|
|
1005
|
-
def
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1185
|
+
def _apply_launch_settings(parsed, ctx, action="set"):
|
|
1186
|
+
targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
|
|
1187
|
+
sessions = [
|
|
1188
|
+
ctx["service"]["set_launch_settings"](session["name"], parsed["settings"])
|
|
1189
|
+
for session in targets
|
|
1190
|
+
]
|
|
1191
|
+
if len(sessions) == 1:
|
|
1192
|
+
session = sessions[0]
|
|
1193
|
+
message = f"Updated launch settings for {session['name']}"
|
|
1194
|
+
else:
|
|
1195
|
+
message = f"Updated launch settings for {len(sessions)} sessions: {_format_bulk_launch_summary(sessions)}"
|
|
1009
1196
|
if parsed["json"]:
|
|
1010
|
-
|
|
1197
|
+
payload = _json_success(
|
|
1198
|
+
action,
|
|
1199
|
+
message,
|
|
1200
|
+
updated_count=len(sessions),
|
|
1201
|
+
session=sessions[0] if len(sessions) == 1 else None,
|
|
1202
|
+
sessions=sessions,
|
|
1203
|
+
launch=sessions[0].get("launch") or {} if len(sessions) == 1 else None,
|
|
1204
|
+
)
|
|
1205
|
+
_write_json(ctx, payload)
|
|
1011
1206
|
return 0
|
|
1012
1207
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
1013
|
-
|
|
1208
|
+
if len(sessions) == 1:
|
|
1209
|
+
ctx["out"](f"{_format_launch_config(sessions[0])}\n")
|
|
1014
1210
|
return 0
|
|
1015
1211
|
|
|
1016
1212
|
|
|
1017
|
-
def
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1213
|
+
def _clear_launch_settings(parsed, ctx, action="unset"):
|
|
1214
|
+
targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
|
|
1215
|
+
sessions = [
|
|
1216
|
+
ctx["service"]["unset_launch_settings"](session["name"], parsed["keys"])
|
|
1217
|
+
for session in targets
|
|
1218
|
+
]
|
|
1219
|
+
if len(sessions) == 1:
|
|
1220
|
+
session = sessions[0]
|
|
1221
|
+
message = f"Cleared launch settings for {session['name']}"
|
|
1222
|
+
else:
|
|
1223
|
+
message = f"Cleared launch settings for {len(sessions)} sessions: {_format_bulk_launch_summary(sessions)}"
|
|
1021
1224
|
if parsed["json"]:
|
|
1022
|
-
|
|
1225
|
+
payload = _json_success(
|
|
1226
|
+
action,
|
|
1227
|
+
message,
|
|
1228
|
+
updated_count=len(sessions),
|
|
1229
|
+
session=sessions[0] if len(sessions) == 1 else None,
|
|
1230
|
+
sessions=sessions,
|
|
1231
|
+
launch=sessions[0].get("launch") or {} if len(sessions) == 1 else None,
|
|
1232
|
+
)
|
|
1233
|
+
_write_json(ctx, payload)
|
|
1023
1234
|
return 0
|
|
1024
1235
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
1025
|
-
|
|
1236
|
+
if len(sessions) == 1:
|
|
1237
|
+
ctx["out"](f"{_format_launch_config(sessions[0])}\n")
|
|
1026
1238
|
return 0
|
|
1027
1239
|
|
|
1028
1240
|
|
|
1241
|
+
def handle_set(rest, ctx):
|
|
1242
|
+
return _apply_launch_settings(_parse_set_args(rest), ctx)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def handle_unset(rest, ctx):
|
|
1246
|
+
return _clear_launch_settings(_parse_unset_args(rest), ctx)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def handle_launch_setting_alias(kind, rest, ctx):
|
|
1250
|
+
parsed = _parse_launch_setting_alias_args(kind, rest)
|
|
1251
|
+
if parsed.get("settings"):
|
|
1252
|
+
return _apply_launch_settings(parsed, ctx, action=kind)
|
|
1253
|
+
return _clear_launch_settings(parsed, ctx, action=kind)
|
|
1254
|
+
|
|
1255
|
+
|
|
1029
1256
|
def handle_config(rest, ctx):
|
|
1030
1257
|
parsed = _parse_config_args(rest)
|
|
1031
1258
|
session = ctx["service"]["get_session"](parsed["name"])
|
|
@@ -1505,14 +1732,35 @@ def handle_login(rest, ctx):
|
|
|
1505
1732
|
session = ctx["service"]["get_session"](args[0])
|
|
1506
1733
|
if not session:
|
|
1507
1734
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1735
|
+
if session["provider"] == PROVIDER_CODEX:
|
|
1736
|
+
_run_interactive_provider_command(
|
|
1737
|
+
session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
1738
|
+
signal_emitter=ctx.get("signal_emitter")
|
|
1739
|
+
)
|
|
1512
1740
|
_run_interactive_provider_command(
|
|
1513
1741
|
session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
1514
1742
|
signal_emitter=ctx.get("signal_emitter")
|
|
1515
1743
|
)
|
|
1744
|
+
auth_probe = _ensure_session_authentication(
|
|
1745
|
+
session,
|
|
1746
|
+
ctx["service"],
|
|
1747
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
1748
|
+
env_override=ctx.get("env"),
|
|
1749
|
+
behavior="probe-only",
|
|
1750
|
+
)
|
|
1751
|
+
if not auth_probe.get("authenticated") and session["provider"] == PROVIDER_CLAUDE:
|
|
1752
|
+
_bootstrap_claude_setup_token(session, ctx)
|
|
1753
|
+
auth_probe = _ensure_session_authentication(
|
|
1754
|
+
session,
|
|
1755
|
+
ctx["service"],
|
|
1756
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
1757
|
+
env_override=ctx.get("env"),
|
|
1758
|
+
behavior="probe-only",
|
|
1759
|
+
)
|
|
1760
|
+
if not auth_probe.get("authenticated"):
|
|
1761
|
+
raise CdxError(
|
|
1762
|
+
f"Login command completed, but session {session['name']} is still not authenticated."
|
|
1763
|
+
)
|
|
1516
1764
|
now = _local_now_iso()
|
|
1517
1765
|
ctx["service"]["update_auth_state"](args[0], lambda auth: {
|
|
1518
1766
|
**auth, "status": "authenticated",
|
|
@@ -1533,6 +1781,10 @@ def handle_logout(rest, ctx):
|
|
|
1533
1781
|
session = ctx["service"]["get_session"](args[0])
|
|
1534
1782
|
if not session:
|
|
1535
1783
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
1784
|
+
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
1785
|
+
raise CdxError("Antigravity logout is managed inside agy. Launch the session and run /logout.")
|
|
1786
|
+
if session["provider"] == PROVIDER_OLLAMA:
|
|
1787
|
+
raise CdxError("Ollama sessions do not use cdx-managed authentication.")
|
|
1536
1788
|
_run_interactive_provider_command(
|
|
1537
1789
|
session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
1538
1790
|
signal_emitter=ctx.get("signal_emitter")
|
package/src/cli_render.py
CHANGED
|
@@ -145,7 +145,7 @@ def _format_sessions(service, use_color=False):
|
|
|
145
145
|
_style("Next actions:", "1", use_color),
|
|
146
146
|
f" {_style('cdx status', '36', use_color)}",
|
|
147
147
|
f" {_style('cdx ready', '36', use_color)}",
|
|
148
|
-
f" {_style('cdx
|
|
148
|
+
f" {_style('cdx perm all default', '36', use_color)}",
|
|
149
149
|
f" {_style('cdx handoff <source> <target>', '36', use_color)}",
|
|
150
150
|
f" {_style('cdx history', '36', use_color)}",
|
|
151
151
|
f" {_style('cdx help', '36', use_color)}",
|
package/src/config.py
CHANGED
|
@@ -3,7 +3,9 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
PROVIDER_CODEX = "codex"
|
|
5
5
|
PROVIDER_CLAUDE = "claude"
|
|
6
|
-
|
|
6
|
+
PROVIDER_ANTIGRAVITY = "antigravity"
|
|
7
|
+
PROVIDER_OLLAMA = "ollama"
|
|
8
|
+
PROVIDERS = (PROVIDER_CODEX, PROVIDER_CLAUDE, PROVIDER_ANTIGRAVITY, PROVIDER_OLLAMA)
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def get_cdx_home(env=None):
|
package/src/provider_runtime.py
CHANGED
|
@@ -7,7 +7,7 @@ import subprocess
|
|
|
7
7
|
import sys
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
|
|
10
|
-
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX
|
|
10
|
+
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
11
11
|
from .errors import CdxError
|
|
12
12
|
|
|
13
13
|
|
|
@@ -31,10 +31,68 @@ LAUNCH_PERMISSION_ARGS = {
|
|
|
31
31
|
def _home_env_overrides(auth_home):
|
|
32
32
|
"""Return env vars that point the claude CLI to the given home directory.
|
|
33
33
|
|
|
34
|
-
On Unix, only HOME is needed.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
On Unix, only HOME is needed. Claude Code resolves its auth files relative
|
|
35
|
+
to HOME; forcing CLAUDE_CONFIG_DIR to this directory makes current Claude
|
|
36
|
+
Code builds ignore otherwise valid isolated credentials. On Windows, Node.js
|
|
37
|
+
resolves the home directory via USERPROFILE (and falls back to
|
|
38
|
+
HOMEDRIVE+HOMEPATH), so we set all three to ensure profile isolation works
|
|
39
|
+
regardless of the platform.
|
|
37
40
|
"""
|
|
41
|
+
overrides = {
|
|
42
|
+
"HOME": auth_home,
|
|
43
|
+
"ANTHROPIC_CONFIG_DIR": auth_home,
|
|
44
|
+
"CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS": "1",
|
|
45
|
+
}
|
|
46
|
+
if sys.platform == "win32":
|
|
47
|
+
overrides["USERPROFILE"] = auth_home
|
|
48
|
+
overrides["HOMEDRIVE"] = os.path.splitdrive(auth_home)[0] or "C:"
|
|
49
|
+
overrides["HOMEPATH"] = os.path.splitdrive(auth_home)[1] or auth_home
|
|
50
|
+
return overrides
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _anthropic_profile_name():
|
|
54
|
+
return "default"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _anthropic_credentials_path(auth_home):
|
|
58
|
+
return os.path.join(auth_home, "credentials", f"{_anthropic_profile_name()}.json")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _claude_env(base_env, auth_home):
|
|
62
|
+
env = {**base_env, **_home_env_overrides(auth_home)}
|
|
63
|
+
env.pop("CLAUDE_CONFIG_DIR", None)
|
|
64
|
+
env.pop("CODEX_HOME", None)
|
|
65
|
+
env.setdefault("ANTHROPIC_PROFILE", _anthropic_profile_name())
|
|
66
|
+
return env
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_anthropic_oauth_token(auth_home):
|
|
70
|
+
try:
|
|
71
|
+
with open(_anthropic_credentials_path(auth_home), "r", encoding="utf-8") as handle:
|
|
72
|
+
credentials = json.load(handle)
|
|
73
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
74
|
+
return None
|
|
75
|
+
token = credentials.get("access_token") if isinstance(credentials, dict) else None
|
|
76
|
+
if not token:
|
|
77
|
+
return None
|
|
78
|
+
return str(token)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _read_claude_account_email(auth_home):
|
|
82
|
+
config_path = os.path.join(auth_home, ".claude.json")
|
|
83
|
+
try:
|
|
84
|
+
with open(config_path, "r", encoding="utf-8") as handle:
|
|
85
|
+
config = json.load(handle)
|
|
86
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
87
|
+
return None
|
|
88
|
+
account = config.get("oauthAccount") if isinstance(config, dict) else None
|
|
89
|
+
email = account.get("emailAddress") if isinstance(account, dict) else None
|
|
90
|
+
if not email:
|
|
91
|
+
return None
|
|
92
|
+
return str(email).strip()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _antigravity_env_overrides(auth_home):
|
|
38
96
|
overrides = {"HOME": auth_home}
|
|
39
97
|
if sys.platform == "win32":
|
|
40
98
|
overrides["USERPROFILE"] = auth_home
|
|
@@ -85,6 +143,18 @@ def _launch_config_args(session):
|
|
|
85
143
|
if permission:
|
|
86
144
|
args += LAUNCH_PERMISSION_ARGS[PROVIDER_CLAUDE].get(permission, [])
|
|
87
145
|
return args
|
|
146
|
+
if provider == PROVIDER_ANTIGRAVITY:
|
|
147
|
+
if permission == "review":
|
|
148
|
+
args += ["--sandbox"]
|
|
149
|
+
if permission == "full":
|
|
150
|
+
args += ["--dangerously-skip-permissions"]
|
|
151
|
+
return args
|
|
152
|
+
if provider == PROVIDER_OLLAMA:
|
|
153
|
+
if power:
|
|
154
|
+
args += ["--think", power if power in ("low", "medium", "high") else "high"]
|
|
155
|
+
if permission == "full":
|
|
156
|
+
args += ["--experimental-yolo"]
|
|
157
|
+
return args
|
|
88
158
|
if power:
|
|
89
159
|
args += ["-c", f'model_reasoning_effort="{power}"']
|
|
90
160
|
if permission:
|
|
@@ -162,15 +232,49 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
162
232
|
args = ["--name", session["name"]] + _launch_config_args(session)
|
|
163
233
|
if initial_prompt:
|
|
164
234
|
args.append(initial_prompt)
|
|
235
|
+
auth_home = _get_auth_home(session)
|
|
236
|
+
claude_env = _claude_env(env, auth_home)
|
|
237
|
+
oauth_token = _read_anthropic_oauth_token(auth_home)
|
|
238
|
+
if oauth_token:
|
|
239
|
+
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
165
240
|
return _wrap_launch_with_transcript(session, {
|
|
166
241
|
"command": "claude",
|
|
167
242
|
"args": args,
|
|
168
243
|
"options": {
|
|
169
244
|
"cwd": cwd,
|
|
170
|
-
"env":
|
|
245
|
+
"env": claude_env,
|
|
171
246
|
},
|
|
172
247
|
"label": "claude",
|
|
173
248
|
}, env=env)
|
|
249
|
+
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
250
|
+
args = _launch_config_args(session)
|
|
251
|
+
if initial_prompt:
|
|
252
|
+
args += ["--prompt-interactive", initial_prompt]
|
|
253
|
+
return _wrap_launch_with_transcript(session, {
|
|
254
|
+
"command": "agy",
|
|
255
|
+
"args": args,
|
|
256
|
+
"options": {
|
|
257
|
+
"cwd": cwd,
|
|
258
|
+
"env": {**env, **_antigravity_env_overrides(_get_auth_home(session))},
|
|
259
|
+
},
|
|
260
|
+
"label": "antigravity",
|
|
261
|
+
}, env=env)
|
|
262
|
+
if session["provider"] == PROVIDER_OLLAMA:
|
|
263
|
+
launch = session.get("launch") or {}
|
|
264
|
+
model = launch.get("model") or session["name"]
|
|
265
|
+
ollama_env = {**env, "OLLAMA_NOHISTORY": "1"}
|
|
266
|
+
args = ["run", model] + _launch_config_args(session)
|
|
267
|
+
if initial_prompt:
|
|
268
|
+
args.append(initial_prompt)
|
|
269
|
+
return _wrap_launch_with_transcript(session, {
|
|
270
|
+
"command": "ollama",
|
|
271
|
+
"args": args,
|
|
272
|
+
"options": {
|
|
273
|
+
"cwd": cwd,
|
|
274
|
+
"env": ollama_env,
|
|
275
|
+
},
|
|
276
|
+
"label": "ollama",
|
|
277
|
+
}, env=env)
|
|
174
278
|
args = ["--no-alt-screen", "--cd", cwd] + _launch_config_args(session)
|
|
175
279
|
if initial_prompt:
|
|
176
280
|
args.append(initial_prompt)
|
|
@@ -187,7 +291,11 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
187
291
|
def _build_login_status_spec(session, env_override=None):
|
|
188
292
|
env = {**os.environ, **(env_override or {})}
|
|
189
293
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
190
|
-
|
|
294
|
+
auth_home = _get_auth_home(session)
|
|
295
|
+
env = _claude_env(env, auth_home)
|
|
296
|
+
oauth_token = _read_anthropic_oauth_token(auth_home)
|
|
297
|
+
if oauth_token:
|
|
298
|
+
env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
191
299
|
|
|
192
300
|
def parser(output):
|
|
193
301
|
try:
|
|
@@ -197,6 +305,13 @@ def _build_login_status_spec(session, env_override=None):
|
|
|
197
305
|
|
|
198
306
|
return {"command": "claude", "args": ["auth", "status"], "env": env,
|
|
199
307
|
"parser": parser, "label": "claude auth status"}
|
|
308
|
+
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
309
|
+
env.update(_antigravity_env_overrides(_get_auth_home(session)))
|
|
310
|
+
return {"command": "agy", "args": ["--version"], "env": env,
|
|
311
|
+
"parser": lambda _output: True, "label": "antigravity cli"}
|
|
312
|
+
if session["provider"] == PROVIDER_OLLAMA:
|
|
313
|
+
return {"command": "ollama", "args": ["--version"], "env": {**env, "OLLAMA_NOHISTORY": "1"},
|
|
314
|
+
"parser": lambda _output: True, "label": "ollama cli"}
|
|
200
315
|
env["CODEX_HOME"] = _get_auth_home(session)
|
|
201
316
|
|
|
202
317
|
def parser(output):
|
|
@@ -212,9 +327,31 @@ def _build_auth_action_spec(session, action, cwd=None, env_override=None):
|
|
|
212
327
|
cwd = cwd or os.getcwd()
|
|
213
328
|
env = {**os.environ, **(env_override or {})}
|
|
214
329
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
330
|
+
auth_home = _get_auth_home(session)
|
|
331
|
+
env = _claude_env(env, auth_home)
|
|
332
|
+
args = ["auth", action]
|
|
333
|
+
label = f"claude auth {action}"
|
|
334
|
+
if action == "setup-token":
|
|
335
|
+
spec = {"command": "claude", "args": ["setup-token"],
|
|
336
|
+
"options": {"cwd": cwd, "env": env}, "label": "claude setup-token"}
|
|
337
|
+
return _wrap_launch_with_transcript(session, spec, env=env)
|
|
338
|
+
if action == "login":
|
|
339
|
+
email = _read_claude_account_email(auth_home)
|
|
340
|
+
if email:
|
|
341
|
+
args += ["--email", email]
|
|
342
|
+
return {"command": "claude", "args": args,
|
|
343
|
+
"options": {"cwd": cwd, "env": env}, "label": label}
|
|
344
|
+
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
345
|
+
if action == "logout":
|
|
346
|
+
raise CdxError("Antigravity logout is managed inside agy. Launch the session and run /logout.")
|
|
347
|
+
env.update(_antigravity_env_overrides(_get_auth_home(session)))
|
|
348
|
+
return {"command": "agy", "args": [],
|
|
349
|
+
"options": {"cwd": cwd, "env": env}, "label": "antigravity"}
|
|
350
|
+
if session["provider"] == PROVIDER_OLLAMA:
|
|
351
|
+
if action == "logout":
|
|
352
|
+
raise CdxError("Ollama sessions do not use cdx-managed authentication.")
|
|
353
|
+
return {"command": "ollama", "args": ["list"],
|
|
354
|
+
"options": {"cwd": cwd, "env": {**env, "OLLAMA_NOHISTORY": "1"}}, "label": "ollama"}
|
|
218
355
|
env["CODEX_HOME"] = _get_auth_home(session)
|
|
219
356
|
return {"command": "codex", "args": [action],
|
|
220
357
|
"options": {"cwd": cwd, "env": env}, "label": f"codex {action}"}
|
package/src/session_service.py
CHANGED
|
@@ -9,7 +9,7 @@ from datetime import datetime, timezone
|
|
|
9
9
|
from urllib.parse import quote
|
|
10
10
|
|
|
11
11
|
from .backup_bundle import decode_bundle, encode_bundle
|
|
12
|
-
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDERS, get_cdx_home
|
|
12
|
+
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDERS, get_cdx_home
|
|
13
13
|
from .codex_usage import fetch_codex_rate_limits
|
|
14
14
|
from .errors import CdxError
|
|
15
15
|
from .session_store import create_session_store
|
|
@@ -55,6 +55,7 @@ STATUS_CACHE_TTL_SECONDS = 60
|
|
|
55
55
|
CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
|
|
56
56
|
LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
|
|
57
57
|
LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
|
|
58
|
+
MAX_LAUNCH_MODEL_LENGTH = 128
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def _encode(name):
|
|
@@ -112,6 +113,13 @@ def _normalize_launch_settings(settings):
|
|
|
112
113
|
normalized["fast"] = False
|
|
113
114
|
else:
|
|
114
115
|
raise CdxError(f"Unsupported fast value: {settings['fast']}")
|
|
116
|
+
if "model" in settings and settings["model"] is not None:
|
|
117
|
+
model = str(settings["model"]).strip()
|
|
118
|
+
if not model:
|
|
119
|
+
raise CdxError("Model cannot be empty.")
|
|
120
|
+
if len(model) > MAX_LAUNCH_MODEL_LENGTH or any(ord(ch) < 32 or ord(ch) == 127 for ch in model):
|
|
121
|
+
raise CdxError("Model contains unsupported characters.")
|
|
122
|
+
normalized["model"] = model
|
|
115
123
|
return normalized
|
|
116
124
|
|
|
117
125
|
|
|
@@ -322,6 +330,29 @@ def _read_expected_account_email(auth_home):
|
|
|
322
330
|
return None
|
|
323
331
|
|
|
324
332
|
|
|
333
|
+
def _ensure_claude_attribution_disabled(auth_home):
|
|
334
|
+
settings_dir = os.path.join(auth_home, ".claude")
|
|
335
|
+
settings_path = os.path.join(settings_dir, "settings.json")
|
|
336
|
+
try:
|
|
337
|
+
os.makedirs(settings_dir, exist_ok=True)
|
|
338
|
+
with open(settings_path, "r", encoding="utf-8") as handle:
|
|
339
|
+
settings = json.load(handle)
|
|
340
|
+
except FileNotFoundError:
|
|
341
|
+
settings = {}
|
|
342
|
+
except (OSError, json.JSONDecodeError):
|
|
343
|
+
settings = {}
|
|
344
|
+
if not isinstance(settings, dict):
|
|
345
|
+
settings = {}
|
|
346
|
+
settings["includeCoAuthoredBy"] = False
|
|
347
|
+
try:
|
|
348
|
+
with open(settings_path, "w", encoding="utf-8") as handle:
|
|
349
|
+
json.dump(settings, handle, indent=2, sort_keys=True)
|
|
350
|
+
handle.write("\n")
|
|
351
|
+
except OSError:
|
|
352
|
+
return False
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
|
|
325
356
|
def create_session_service(options=None):
|
|
326
357
|
if options is None:
|
|
327
358
|
options = {}
|
|
@@ -337,6 +368,8 @@ def create_session_service(options=None):
|
|
|
337
368
|
root = _get_session_root(name)
|
|
338
369
|
if provider == PROVIDER_CLAUDE:
|
|
339
370
|
return os.path.join(root, "claude-home")
|
|
371
|
+
if provider == PROVIDER_ANTIGRAVITY:
|
|
372
|
+
return os.path.join(root, "antigravity-home")
|
|
340
373
|
return root
|
|
341
374
|
|
|
342
375
|
def _normalize_provider(provider):
|
|
@@ -400,6 +433,8 @@ def create_session_service(options=None):
|
|
|
400
433
|
def _auth_bundle_paths(provider):
|
|
401
434
|
if provider == PROVIDER_CLAUDE:
|
|
402
435
|
return [
|
|
436
|
+
"claude-home/configs/default.json",
|
|
437
|
+
"claude-home/credentials/default.json",
|
|
403
438
|
"claude-home/.claude/.credentials.json",
|
|
404
439
|
"claude-home/.claude.json",
|
|
405
440
|
"claude-home/auth.json",
|
|
@@ -454,6 +489,8 @@ def create_session_service(options=None):
|
|
|
454
489
|
_ensure_private_dir(auth_home)
|
|
455
490
|
if normalized_provider == PROVIDER_CODEX:
|
|
456
491
|
_seed_codex_auth_from_global(auth_home, env=env)
|
|
492
|
+
if normalized_provider == PROVIDER_CLAUDE:
|
|
493
|
+
_ensure_claude_attribution_disabled(auth_home)
|
|
457
494
|
now = _local_now_iso()
|
|
458
495
|
session = {
|
|
459
496
|
"name": name,
|
|
@@ -736,7 +773,7 @@ def create_session_service(options=None):
|
|
|
736
773
|
raise CdxError(f"Unknown session: {name}")
|
|
737
774
|
if not keys:
|
|
738
775
|
raise CdxError("At least one launch setting is required.")
|
|
739
|
-
allowed = {"power", "permission", "fast"}
|
|
776
|
+
allowed = {"power", "permission", "fast", "model"}
|
|
740
777
|
unknown = [key for key in keys if key not in allowed]
|
|
741
778
|
if unknown:
|
|
742
779
|
raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
|