cdx-manager 0.6.1 → 0.6.2

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.6.1-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.6.2-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
- **Run multiple Codex and Claude sessions from one terminal. Switch between accounts instantly.**
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 accounts, one tool.** Register as many Codex or Claude sessions as you need. Each one gets its own isolated auth environment no cross-contamination between accounts.
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 `claude` CLI installed and available in your PATH
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.1 sh install.sh
137
+ CDX_VERSION=v0.6.2 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 `claude`, default: `codex`) |
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 set <name> [--power low\|medium\|high\|xhigh\|max] [--permission review\|default\|auto\|full] [--fast on\|off] [--json]` | Persist launch settings for a session |
306
- | `cdx unset <name> (--power\|--permission\|--fast\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
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 Codex and Claude targets, including cross-provider handoff |
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`
@@ -8,6 +8,10 @@
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"
11
15
  }
12
16
  }
13
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.6.1"
7
+ version = "0.6.2"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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,6 +11,7 @@ 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):
@@ -21,6 +24,40 @@ def _read_claude_credentials(auth_home):
21
24
  return None
22
25
 
23
26
 
27
+ def _home_env_overrides(auth_home):
28
+ overrides = {
29
+ "HOME": auth_home,
30
+ "CLAUDE_CONFIG_DIR": auth_home,
31
+ "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS": "1",
32
+ }
33
+ if sys.platform == "win32":
34
+ overrides["USERPROFILE"] = auth_home
35
+ overrides["HOMEDRIVE"] = os.path.splitdrive(auth_home)[0] or "C:"
36
+ overrides["HOMEPATH"] = os.path.splitdrive(auth_home)[1] or auth_home
37
+ return overrides
38
+
39
+
40
+ def _refresh_claude_cli_credentials(auth_home, runner=None, env=None):
41
+ if not auth_home:
42
+ return
43
+ if os.environ.get("CDX_CLAUDE_SKIP_AUTH_STATUS_REFRESH") == "1":
44
+ return
45
+ runner = runner or subprocess.run
46
+ command = ["claude", "auth", "status"]
47
+ run_env = {**(env or os.environ), **_home_env_overrides(auth_home)}
48
+ try:
49
+ runner(
50
+ command,
51
+ env=run_env,
52
+ capture_output=True,
53
+ text=True,
54
+ timeout=CLAUDE_AUTH_STATUS_TIMEOUT_SECONDS,
55
+ check=False,
56
+ )
57
+ except (FileNotFoundError, subprocess.SubprocessError, OSError):
58
+ return
59
+
60
+
24
61
  def _format_reset_date(unix_seconds):
25
62
  dt = datetime.fromtimestamp(unix_seconds, tz=timezone.utc).astimezone()
26
63
  return f"{MONTH_ABBR[dt.month - 1]} {dt.day} {str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}"
@@ -109,8 +146,11 @@ def fetch_claude_rate_limit_headers(access_token):
109
146
  }
110
147
 
111
148
 
112
- def refresh_claude_session_status(session):
113
- creds = _read_claude_credentials(session.get("authHome", ""))
149
+ def refresh_claude_session_status(session, auth_refresher=None):
150
+ auth_home = session.get("authHome", "")
151
+ refresher = auth_refresher or _refresh_claude_cli_credentials
152
+ refresher(auth_home)
153
+ creds = _read_claude_credentials(auth_home)
114
154
  if not creds or not creds.get("accessToken"):
115
155
  return None
116
156
  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.1"
58
+ VERSION = "0.6.2"
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 set <name> [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--json]', '36', use_color)}",
77
- f" {_style('cdx unset <name> (--power|--permission|--fast|--all) [--json]', '36', use_color)}",
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
 
@@ -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_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> [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--json]"
54
- UNSET_USAGE = "Usage: cdx unset <name> (--power|--permission|--fast|--all) [--json]"
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"]) != 1:
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 {"name": parsed["names"][0], "settings": settings, "json": parsed["json"]}
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"]) != 1:
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 {"name": parsed["names"][0], "keys": keys, "json": parsed["json"]}
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},
@@ -766,7 +840,13 @@ def _resolve_confirmation(confirm_fn, name):
766
840
  def handle_add(rest, ctx):
767
841
  json_flag, args = _parse_json_flag(rest)
768
842
  parsed = _parse_add_args(args)
843
+ if parsed["provider"] == PROVIDER_OLLAMA and not parsed.get("model"):
844
+ raise CdxError("Usage: cdx add ollama <name> --model MODEL [--json]")
845
+ if parsed.get("model") and parsed["provider"] != PROVIDER_OLLAMA:
846
+ raise CdxError("--model is only supported when adding an ollama session.")
769
847
  session = ctx["service"]["create_session"](parsed["name"], parsed["provider"])
848
+ if parsed.get("model"):
849
+ session = ctx["service"]["set_launch_settings"](parsed["name"], {"model": parsed["model"]})
770
850
  message = f"Created session {parsed['name']} ({parsed['provider']})"
771
851
  _ensure_session_authentication(
772
852
  session,
@@ -882,9 +962,43 @@ def _format_launch_config(session):
882
962
  f"power: {launch.get('power') or 'default'}",
883
963
  f"permission: {launch.get('permission') or 'default'}",
884
964
  f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
965
+ f"model: {launch.get('model') or 'default'}",
885
966
  ])
886
967
 
887
968
 
969
+ def _resolve_bulk_launch_targets(parsed, service):
970
+ sessions = service["list_sessions"]()
971
+ by_name = {session["name"]: session for session in sessions}
972
+ if parsed.get("name"):
973
+ session = by_name.get(parsed["name"])
974
+ if not session:
975
+ raise CdxError(f"Unknown session: {parsed['name']}")
976
+ targets = [session]
977
+ elif parsed.get("sessions") == "all":
978
+ targets = sessions
979
+ elif parsed.get("sessions"):
980
+ targets = []
981
+ for name in parsed["sessions"]:
982
+ session = by_name.get(name)
983
+ if not session:
984
+ raise CdxError(f"Unknown session: {name}")
985
+ targets.append(session)
986
+ else:
987
+ targets = sessions
988
+ if parsed.get("provider"):
989
+ targets = [session for session in targets if session["provider"] == parsed["provider"]]
990
+ if not targets:
991
+ raise CdxError("No sessions matched.")
992
+ return targets
993
+
994
+
995
+ def _format_bulk_launch_summary(sessions):
996
+ names = [session["name"] for session in sessions]
997
+ if len(names) <= 8:
998
+ return ", ".join(names)
999
+ return ", ".join(names[:8]) + f", +{len(names) - 8} more"
1000
+
1001
+
888
1002
  def _format_duration_ms(value):
889
1003
  if value is None:
890
1004
  return "-"
@@ -1002,30 +1116,77 @@ def _format_history(entries, use_color=False):
1002
1116
  ])
1003
1117
 
1004
1118
 
1005
- def handle_set(rest, ctx):
1006
- parsed = _parse_set_args(rest)
1007
- session = ctx["service"]["set_launch_settings"](parsed["name"], parsed["settings"])
1008
- message = f"Updated launch settings for {parsed['name']}"
1119
+ def _apply_launch_settings(parsed, ctx, action="set"):
1120
+ targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
1121
+ sessions = [
1122
+ ctx["service"]["set_launch_settings"](session["name"], parsed["settings"])
1123
+ for session in targets
1124
+ ]
1125
+ if len(sessions) == 1:
1126
+ session = sessions[0]
1127
+ message = f"Updated launch settings for {session['name']}"
1128
+ else:
1129
+ message = f"Updated launch settings for {len(sessions)} sessions: {_format_bulk_launch_summary(sessions)}"
1009
1130
  if parsed["json"]:
1010
- _write_json(ctx, _json_success("set", message, session=session, launch=session.get("launch") or {}))
1131
+ payload = _json_success(
1132
+ action,
1133
+ message,
1134
+ updated_count=len(sessions),
1135
+ session=sessions[0] if len(sessions) == 1 else None,
1136
+ sessions=sessions,
1137
+ launch=sessions[0].get("launch") or {} if len(sessions) == 1 else None,
1138
+ )
1139
+ _write_json(ctx, payload)
1011
1140
  return 0
1012
1141
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
1013
- ctx["out"](f"{_format_launch_config(session)}\n")
1142
+ if len(sessions) == 1:
1143
+ ctx["out"](f"{_format_launch_config(sessions[0])}\n")
1014
1144
  return 0
1015
1145
 
1016
1146
 
1017
- def handle_unset(rest, ctx):
1018
- parsed = _parse_unset_args(rest)
1019
- session = ctx["service"]["unset_launch_settings"](parsed["name"], parsed["keys"])
1020
- message = f"Cleared launch settings for {parsed['name']}"
1147
+ def _clear_launch_settings(parsed, ctx, action="unset"):
1148
+ targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
1149
+ sessions = [
1150
+ ctx["service"]["unset_launch_settings"](session["name"], parsed["keys"])
1151
+ for session in targets
1152
+ ]
1153
+ if len(sessions) == 1:
1154
+ session = sessions[0]
1155
+ message = f"Cleared launch settings for {session['name']}"
1156
+ else:
1157
+ message = f"Cleared launch settings for {len(sessions)} sessions: {_format_bulk_launch_summary(sessions)}"
1021
1158
  if parsed["json"]:
1022
- _write_json(ctx, _json_success("unset", message, session=session, launch=session.get("launch") or {}))
1159
+ payload = _json_success(
1160
+ action,
1161
+ message,
1162
+ updated_count=len(sessions),
1163
+ session=sessions[0] if len(sessions) == 1 else None,
1164
+ sessions=sessions,
1165
+ launch=sessions[0].get("launch") or {} if len(sessions) == 1 else None,
1166
+ )
1167
+ _write_json(ctx, payload)
1023
1168
  return 0
1024
1169
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
1025
- ctx["out"](f"{_format_launch_config(session)}\n")
1170
+ if len(sessions) == 1:
1171
+ ctx["out"](f"{_format_launch_config(sessions[0])}\n")
1026
1172
  return 0
1027
1173
 
1028
1174
 
1175
+ def handle_set(rest, ctx):
1176
+ return _apply_launch_settings(_parse_set_args(rest), ctx)
1177
+
1178
+
1179
+ def handle_unset(rest, ctx):
1180
+ return _clear_launch_settings(_parse_unset_args(rest), ctx)
1181
+
1182
+
1183
+ def handle_launch_setting_alias(kind, rest, ctx):
1184
+ parsed = _parse_launch_setting_alias_args(kind, rest)
1185
+ if parsed.get("settings"):
1186
+ return _apply_launch_settings(parsed, ctx, action=kind)
1187
+ return _clear_launch_settings(parsed, ctx, action=kind)
1188
+
1189
+
1029
1190
  def handle_config(rest, ctx):
1030
1191
  parsed = _parse_config_args(rest)
1031
1192
  session = ctx["service"]["get_session"](parsed["name"])
@@ -1505,10 +1666,11 @@ def handle_login(rest, ctx):
1505
1666
  session = ctx["service"]["get_session"](args[0])
1506
1667
  if not session:
1507
1668
  raise CdxError(f"Unknown session: {args[0]}")
1508
- _run_interactive_provider_command(
1509
- session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
1510
- signal_emitter=ctx.get("signal_emitter")
1511
- )
1669
+ if session["provider"] not in (PROVIDER_ANTIGRAVITY, PROVIDER_OLLAMA):
1670
+ _run_interactive_provider_command(
1671
+ session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
1672
+ signal_emitter=ctx.get("signal_emitter")
1673
+ )
1512
1674
  _run_interactive_provider_command(
1513
1675
  session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
1514
1676
  signal_emitter=ctx.get("signal_emitter")
@@ -1533,6 +1695,10 @@ def handle_logout(rest, ctx):
1533
1695
  session = ctx["service"]["get_session"](args[0])
1534
1696
  if not session:
1535
1697
  raise CdxError(f"Unknown session: {args[0]}")
1698
+ if session["provider"] == PROVIDER_ANTIGRAVITY:
1699
+ raise CdxError("Antigravity logout is managed inside agy. Launch the session and run /logout.")
1700
+ if session["provider"] == PROVIDER_OLLAMA:
1701
+ raise CdxError("Ollama sessions do not use cdx-managed authentication.")
1536
1702
  _run_interactive_provider_command(
1537
1703
  session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
1538
1704
  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 set <name> --power medium --permission default --fast off', '36', use_color)}",
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
- PROVIDERS = (PROVIDER_CODEX, PROVIDER_CLAUDE)
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):
@@ -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
 
@@ -35,6 +35,19 @@ def _home_env_overrides(auth_home):
35
35
  directory via USERPROFILE (and falls back to HOMEDRIVE+HOMEPATH), so we
36
36
  set all three to ensure profile isolation works regardless of the platform.
37
37
  """
38
+ overrides = {
39
+ "HOME": auth_home,
40
+ "CLAUDE_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 _antigravity_env_overrides(auth_home):
38
51
  overrides = {"HOME": auth_home}
39
52
  if sys.platform == "win32":
40
53
  overrides["USERPROFILE"] = auth_home
@@ -85,6 +98,18 @@ def _launch_config_args(session):
85
98
  if permission:
86
99
  args += LAUNCH_PERMISSION_ARGS[PROVIDER_CLAUDE].get(permission, [])
87
100
  return args
101
+ if provider == PROVIDER_ANTIGRAVITY:
102
+ if permission == "review":
103
+ args += ["--sandbox"]
104
+ if permission == "full":
105
+ args += ["--dangerously-skip-permissions"]
106
+ return args
107
+ if provider == PROVIDER_OLLAMA:
108
+ if power:
109
+ args += ["--think", power if power in ("low", "medium", "high") else "high"]
110
+ if permission == "full":
111
+ args += ["--experimental-yolo"]
112
+ return args
88
113
  if power:
89
114
  args += ["-c", f'model_reasoning_effort="{power}"']
90
115
  if permission:
@@ -171,6 +196,35 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
171
196
  },
172
197
  "label": "claude",
173
198
  }, env=env)
199
+ if session["provider"] == PROVIDER_ANTIGRAVITY:
200
+ args = _launch_config_args(session)
201
+ if initial_prompt:
202
+ args += ["--prompt-interactive", initial_prompt]
203
+ return _wrap_launch_with_transcript(session, {
204
+ "command": "agy",
205
+ "args": args,
206
+ "options": {
207
+ "cwd": cwd,
208
+ "env": {**env, **_antigravity_env_overrides(_get_auth_home(session))},
209
+ },
210
+ "label": "antigravity",
211
+ }, env=env)
212
+ if session["provider"] == PROVIDER_OLLAMA:
213
+ launch = session.get("launch") or {}
214
+ model = launch.get("model") or session["name"]
215
+ ollama_env = {**env, "OLLAMA_NOHISTORY": "1"}
216
+ args = ["run", model] + _launch_config_args(session)
217
+ if initial_prompt:
218
+ args.append(initial_prompt)
219
+ return _wrap_launch_with_transcript(session, {
220
+ "command": "ollama",
221
+ "args": args,
222
+ "options": {
223
+ "cwd": cwd,
224
+ "env": ollama_env,
225
+ },
226
+ "label": "ollama",
227
+ }, env=env)
174
228
  args = ["--no-alt-screen", "--cd", cwd] + _launch_config_args(session)
175
229
  if initial_prompt:
176
230
  args.append(initial_prompt)
@@ -197,6 +251,13 @@ def _build_login_status_spec(session, env_override=None):
197
251
 
198
252
  return {"command": "claude", "args": ["auth", "status"], "env": env,
199
253
  "parser": parser, "label": "claude auth status"}
254
+ if session["provider"] == PROVIDER_ANTIGRAVITY:
255
+ env.update(_antigravity_env_overrides(_get_auth_home(session)))
256
+ return {"command": "agy", "args": ["--version"], "env": env,
257
+ "parser": lambda _output: True, "label": "antigravity cli"}
258
+ if session["provider"] == PROVIDER_OLLAMA:
259
+ return {"command": "ollama", "args": ["--version"], "env": {**env, "OLLAMA_NOHISTORY": "1"},
260
+ "parser": lambda _output: True, "label": "ollama cli"}
200
261
  env["CODEX_HOME"] = _get_auth_home(session)
201
262
 
202
263
  def parser(output):
@@ -215,6 +276,17 @@ def _build_auth_action_spec(session, action, cwd=None, env_override=None):
215
276
  env.update(_home_env_overrides(_get_auth_home(session)))
216
277
  return {"command": "claude", "args": ["auth", action],
217
278
  "options": {"cwd": cwd, "env": env}, "label": f"claude auth {action}"}
279
+ if session["provider"] == PROVIDER_ANTIGRAVITY:
280
+ if action == "logout":
281
+ raise CdxError("Antigravity logout is managed inside agy. Launch the session and run /logout.")
282
+ env.update(_antigravity_env_overrides(_get_auth_home(session)))
283
+ return {"command": "agy", "args": [],
284
+ "options": {"cwd": cwd, "env": env}, "label": "antigravity"}
285
+ if session["provider"] == PROVIDER_OLLAMA:
286
+ if action == "logout":
287
+ raise CdxError("Ollama sessions do not use cdx-managed authentication.")
288
+ return {"command": "ollama", "args": ["list"],
289
+ "options": {"cwd": cwd, "env": {**env, "OLLAMA_NOHISTORY": "1"}}, "label": "ollama"}
218
290
  env["CODEX_HOME"] = _get_auth_home(session)
219
291
  return {"command": "codex", "args": [action],
220
292
  "options": {"cwd": cwd, "env": env}, "label": f"codex {action}"}
@@ -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):
@@ -454,6 +487,8 @@ def create_session_service(options=None):
454
487
  _ensure_private_dir(auth_home)
455
488
  if normalized_provider == PROVIDER_CODEX:
456
489
  _seed_codex_auth_from_global(auth_home, env=env)
490
+ if normalized_provider == PROVIDER_CLAUDE:
491
+ _ensure_claude_attribution_disabled(auth_home)
457
492
  now = _local_now_iso()
458
493
  session = {
459
494
  "name": name,
@@ -736,7 +771,7 @@ def create_session_service(options=None):
736
771
  raise CdxError(f"Unknown session: {name}")
737
772
  if not keys:
738
773
  raise CdxError("At least one launch setting is required.")
739
- allowed = {"power", "permission", "fast"}
774
+ allowed = {"power", "permission", "fast", "model"}
740
775
  unknown = [key for key in keys if key not in allowed]
741
776
  if unknown:
742
777
  raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")