cdx-manager 0.25.8 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -6
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_9.md +22 -0
- package/changelogs/CHANGELOGS_0_26_0.md +31 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +27 -1
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +69 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/antigravity_status.py +161 -0
- package/src/claude_remote_token.py +45 -0
- package/src/cli.py +14 -1
- package/src/cli_args.py +9 -4
- package/src/cli_commands.py +3 -2
- package/src/cli_helpers.py +1 -0
- package/src/commands/auth.py +25 -0
- package/src/commands/conversation.py +34 -0
- package/src/commands/maintenance.py +4 -1
- package/src/commands/runs.py +16 -3
- package/src/config.py +15 -0
- package/src/conversation.py +181 -0
- package/src/interactive_usage.py +6 -12
- package/src/ollama_remote.py +175 -0
- package/src/provider_runtime.py +15 -0
- package/src/remote_run.py +69 -25
- package/src/remote_worker.py +85 -33
- package/src/run_command.py +9 -0
- package/src/run_registry.py +5 -0
- package/src/run_usage.py +63 -2
- package/src/session_service.py +8 -1
- package/src/session_status.py +17 -0
- package/src/status_view.py +1 -0
- package/src/update_all.py +9 -3
- package/src/workspace_sandbox.py +126 -6
package/src/cli.py
CHANGED
|
@@ -21,6 +21,7 @@ from .cli_commands import (
|
|
|
21
21
|
handle_config,
|
|
22
22
|
handle_configs,
|
|
23
23
|
handle_context,
|
|
24
|
+
handle_conversation,
|
|
24
25
|
handle_copy,
|
|
25
26
|
handle_disable,
|
|
26
27
|
handle_disk,
|
|
@@ -167,6 +168,7 @@ _COMMAND_HANDLERS = {
|
|
|
167
168
|
"export": handle_export,
|
|
168
169
|
"handoff": handle_handoff,
|
|
169
170
|
"history": handle_history,
|
|
171
|
+
"conversation": handle_conversation,
|
|
170
172
|
"import": handle_import,
|
|
171
173
|
"label": handle_label,
|
|
172
174
|
"last": handle_last,
|
|
@@ -201,6 +203,7 @@ _COMMAND_HANDLERS = {
|
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
_COMMAND_ALIASES = {
|
|
206
|
+
"show": "config",
|
|
204
207
|
"ren": "rename",
|
|
205
208
|
"mv": "rename",
|
|
206
209
|
}
|
|
@@ -237,15 +240,18 @@ def _print_help(use_color=False):
|
|
|
237
240
|
_style("Usage:", "1", use_color),
|
|
238
241
|
f" {_style('cdx', '36', use_color)}",
|
|
239
242
|
f" {_style('cdx --json', '36', use_color)}",
|
|
243
|
+
f" {_style('cdx ls [--json]', '36', use_color)}",
|
|
240
244
|
f" {_style('cdx status [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
|
|
241
245
|
f" {_style('cdx status --small|-s [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
|
|
242
246
|
f" {_style('cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
|
|
243
247
|
f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
|
|
244
248
|
f" {_style('cdx agents [--max-age-hours N] [--json]', '36', use_color)}",
|
|
245
249
|
f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
|
|
250
|
+
f" {_style('cdx auth claude-token <profile> [--json]', '36', use_color)}",
|
|
246
251
|
f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
|
|
247
252
|
f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
|
|
248
253
|
f" {_style('cdx run [session] [--remote SSH_TARGET] --cwd PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
|
|
254
|
+
" Remote Ollama: text generation only, not an autonomous coding agent; no repository edits. Requires an installed worker model; no authentication.",
|
|
249
255
|
f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json', '36', use_color)}",
|
|
250
256
|
f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
|
|
251
257
|
f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
|
|
@@ -256,15 +262,17 @@ def _print_help(use_color=False):
|
|
|
256
262
|
f" {_style('cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]', '36', use_color)}",
|
|
257
263
|
f" {_style('cdx disk [profiles] [--candidates] [--json]', '36', use_color)}",
|
|
258
264
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
265
|
+
f" {_style('cdx show <name> [--json]', '36', use_color)}",
|
|
259
266
|
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
260
267
|
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
261
268
|
f" {_style(SET_USAGE[len('Usage: '):], '36', use_color)}",
|
|
262
|
-
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
269
|
+
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--max-parallel|--all) [--json]', '36', use_color)}",
|
|
263
270
|
f" {_style('cdx history backfill-projects [--mapping FILE] [--dry-run|--apply] [--json]', '36', use_color)}",
|
|
264
271
|
f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
265
272
|
f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
|
|
266
273
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
267
274
|
f" {_style('cdx resume <name> [--project PATH] [--conversation <id>] [--json]', '36', use_color)}",
|
|
275
|
+
f" {_style('cdx conversation <name> [--conversation ID] [--cursor CURSOR] [--limit N] --json', '36', use_color)}",
|
|
268
276
|
f" {_style('cdx can-resume <name> [--conversation <id>] [--json]', '36', use_color)}",
|
|
269
277
|
f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
|
|
270
278
|
f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
|
|
@@ -578,6 +586,11 @@ def main(argv, options=None):
|
|
|
578
586
|
# directory cannot be resolved (a service or task with a minimal environment).
|
|
579
587
|
service = options.get("service") or create_session_service({"env": env})
|
|
580
588
|
|
|
589
|
+
if argv[:1] == ["ls"]:
|
|
590
|
+
if argv[1:] not in ([], ["--json"]):
|
|
591
|
+
raise CdxError("Usage: cdx ls [--json]")
|
|
592
|
+
argv = argv[1:]
|
|
593
|
+
|
|
581
594
|
if argv == ["--json"]:
|
|
582
595
|
rows = service["format_list_rows"]()
|
|
583
596
|
notices = _get_update_notices(service, env, options)
|
package/src/cli_args.py
CHANGED
|
@@ -16,6 +16,7 @@ from .config import (
|
|
|
16
16
|
PROVIDER_CODEX,
|
|
17
17
|
PROVIDERS,
|
|
18
18
|
REASONING_EFFORT_VALUES,
|
|
19
|
+
normalize_launch_max_parallel,
|
|
19
20
|
normalize_permission,
|
|
20
21
|
parse_on_off,
|
|
21
22
|
)
|
|
@@ -34,8 +35,8 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
|
|
|
34
35
|
MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
|
|
35
36
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]"
|
|
36
37
|
LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
|
|
37
|
-
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--agent-state on|off] [--notify-preview on|off] [--scrub-env on|off] [--shared-memory on|off] [--retro on|off] [--policy use|ask-first|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
|
|
38
|
-
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--agent-state|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
|
|
38
|
+
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--agent-state on|off] [--notify-preview on|off] [--scrub-env on|off] [--shared-memory on|off] [--retro on|off] [--policy use|ask-first|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--max-parallel 0..5] [--json]"
|
|
39
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--agent-state|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--max-parallel|--all) [--json]"
|
|
39
40
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
40
41
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
41
42
|
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
@@ -49,6 +50,7 @@ SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
|
49
50
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
50
51
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
51
52
|
RUN_USAGE = "Usage: cdx run [session] [--remote SSH_TARGET] [--remote-prefix COMMAND] (--cwd|--dir) PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
|
|
53
|
+
RUN_USAGE += "\nRemote Ollama generates text only; it is not an autonomous coding agent and does not edit the repository. Requires an installed worker model; no authentication."
|
|
52
54
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
53
55
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
54
56
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
@@ -285,6 +287,7 @@ def _parse_set_args(args):
|
|
|
285
287
|
"--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
|
|
286
288
|
"--extra-args": {"key": "extra_args", "type": "str", "default": None},
|
|
287
289
|
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
290
|
+
"--max-parallel": {"key": "max_parallel", "type": "str", "default": None, "transform": normalize_launch_max_parallel},
|
|
288
291
|
"--policy": {"key": "policy", "type": "str", "default": None},
|
|
289
292
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
290
293
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
@@ -302,7 +305,7 @@ def _parse_set_args(args):
|
|
|
302
305
|
raise CdxError(SET_USAGE)
|
|
303
306
|
settings = {
|
|
304
307
|
key: parsed[key]
|
|
305
|
-
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy")
|
|
308
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel")
|
|
306
309
|
if parsed[key] is not None
|
|
307
310
|
}
|
|
308
311
|
if not settings:
|
|
@@ -335,6 +338,7 @@ def _parse_unset_args(args):
|
|
|
335
338
|
"--budget": {"key": "budget", "type": "bool", "default": False},
|
|
336
339
|
"--extra-args": {"key": "extra_args", "type": "bool", "default": False},
|
|
337
340
|
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
341
|
+
"--max-parallel": {"key": "max_parallel", "type": "bool", "default": False},
|
|
338
342
|
"--policy": {"key": "policy", "type": "bool", "default": False},
|
|
339
343
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
340
344
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
@@ -353,7 +357,7 @@ def _parse_unset_args(args):
|
|
|
353
357
|
raise CdxError(UNSET_USAGE)
|
|
354
358
|
_UNSET_KEYS = (
|
|
355
359
|
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro",
|
|
356
|
-
"model", "fallback_model", "budget", "extra_args", "priority", "policy",
|
|
360
|
+
"model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel",
|
|
357
361
|
)
|
|
358
362
|
keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
|
|
359
363
|
if not keys:
|
|
@@ -593,6 +597,7 @@ def cdx_schema():
|
|
|
593
597
|
"logics_manager_update_available",
|
|
594
598
|
"update_version_check_failed",
|
|
595
599
|
"update_version_mismatch",
|
|
600
|
+
"update_unverified",
|
|
596
601
|
"logics_manager_missing",
|
|
597
602
|
"logics_viewer_failed",
|
|
598
603
|
"logics_viewer_root_invalid",
|
package/src/cli_commands.py
CHANGED
|
@@ -57,8 +57,6 @@ from .commands.backup import ( # noqa: F401
|
|
|
57
57
|
handle_export,
|
|
58
58
|
handle_import,
|
|
59
59
|
)
|
|
60
|
-
|
|
61
|
-
# Re-exported for cli.py and tests; see src/commands/__init__.py.
|
|
62
60
|
from .commands.context_memory import ( # noqa: F401
|
|
63
61
|
_list_memory_entries,
|
|
64
62
|
_memory_payload,
|
|
@@ -67,6 +65,9 @@ from .commands.context_memory import ( # noqa: F401
|
|
|
67
65
|
handle_context,
|
|
68
66
|
handle_memory,
|
|
69
67
|
)
|
|
68
|
+
|
|
69
|
+
# Re-exported for cli.py and tests; see src/commands/__init__.py.
|
|
70
|
+
from .commands.conversation import handle_conversation as handle_conversation
|
|
70
71
|
from .commands.launch import ( # re-export for cli.py / tests
|
|
71
72
|
_format_resume_capability as _format_resume_capability,
|
|
72
73
|
)
|
package/src/cli_helpers.py
CHANGED
package/src/commands/auth.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import sys
|
|
6
7
|
import time
|
|
7
8
|
import uuid
|
|
8
9
|
|
|
@@ -53,6 +54,30 @@ def _refresh_result(session, diagnostic):
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def handle_auth(rest, ctx):
|
|
57
|
+
if rest and rest[0] == "claude-token":
|
|
58
|
+
json_flag, args = _parse_json_flag(rest[1:])
|
|
59
|
+
if len(args) != 1:
|
|
60
|
+
raise CdxError("Usage: cdx auth claude-token <profile> [--json]")
|
|
61
|
+
session = ctx["service"]["get_session"](args[0])
|
|
62
|
+
if not session or session["provider"] != PROVIDER_CLAUDE:
|
|
63
|
+
raise CdxError("Select an existing Claude profile for cdx auth claude-token.")
|
|
64
|
+
if ctx["stdin_is_tty"]:
|
|
65
|
+
raise CdxError("Read the Claude setup token from standard input, without an interactive terminal.")
|
|
66
|
+
from ..claude_remote_token import store_token
|
|
67
|
+
stream = ctx.get("prompt_stdin") or sys.stdin
|
|
68
|
+
token = stream.read(16385)
|
|
69
|
+
if isinstance(token, bytes):
|
|
70
|
+
try:
|
|
71
|
+
token = token.decode("utf-8")
|
|
72
|
+
except UnicodeDecodeError as error:
|
|
73
|
+
raise CdxError("Provide one Claude setup token on standard input.") from error
|
|
74
|
+
store_token(session, token)
|
|
75
|
+
message = f"Stored remote Claude setup token for {session['name']}"
|
|
76
|
+
if json_flag:
|
|
77
|
+
_write_json(ctx, _json_success("auth.claude-token", message))
|
|
78
|
+
else:
|
|
79
|
+
ctx["out"](message + "\n")
|
|
80
|
+
return 0
|
|
56
81
|
parsed = _parse_auth_args(rest)
|
|
57
82
|
target = parsed["target"]
|
|
58
83
|
if target == "all":
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""JSON conversation pages for interactive sessions."""
|
|
2
|
+
|
|
3
|
+
from ..cli_args import _parse_flag_args
|
|
4
|
+
from ..cli_helpers import _json_success, _write_json
|
|
5
|
+
from ..conversation import read_conversation
|
|
6
|
+
from ..errors import CdxError
|
|
7
|
+
from ..provider_runtime import _get_auth_home
|
|
8
|
+
|
|
9
|
+
CONVERSATION_USAGE = "Usage: cdx conversation <name> [--conversation ID] [--cursor CURSOR] [--limit N] --json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_conversation(rest, ctx):
|
|
13
|
+
parsed = _parse_flag_args(rest, {
|
|
14
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
15
|
+
"--conversation": {"key": "conversation", "type": "str"},
|
|
16
|
+
"--cursor": {"key": "cursor", "type": "str"},
|
|
17
|
+
"--limit": {"key": "limit", "type": "str", "default": "100"},
|
|
18
|
+
}, CONVERSATION_USAGE, positionals_key="names", max_positionals=1)
|
|
19
|
+
if not parsed["json"] or len(parsed["names"]) != 1:
|
|
20
|
+
raise CdxError(CONVERSATION_USAGE)
|
|
21
|
+
session = ctx["service"]["get_session"](parsed["names"][0])
|
|
22
|
+
if not session:
|
|
23
|
+
raise CdxError(f"Unknown session: {parsed['names'][0]}")
|
|
24
|
+
try:
|
|
25
|
+
limit = int(parsed["limit"])
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise CdxError(CONVERSATION_USAGE) from None
|
|
28
|
+
page = read_conversation(
|
|
29
|
+
session["provider"], _get_auth_home(session),
|
|
30
|
+
conversation_id=parsed["conversation"] or (session.get("conversation") or {}).get("id"),
|
|
31
|
+
cursor=parsed["cursor"], limit=limit,
|
|
32
|
+
)
|
|
33
|
+
_write_json(ctx, _json_success("conversation", "Conversation page loaded.", session=session["name"], **page))
|
|
34
|
+
return 0
|
|
@@ -53,6 +53,8 @@ def _format_update_all_result(result, use_color=False):
|
|
|
53
53
|
return _warn(f"skipped: {result['name']} (blocked by {result['blocked_by']})", use_color)
|
|
54
54
|
if result.get("returncode") == 0:
|
|
55
55
|
versions = f" ({result['version_before']} -> {result['version_after']})" if "version_after" in result else ""
|
|
56
|
+
if result.get("warning"):
|
|
57
|
+
return _warn(f"done: {result['name']}{versions}\n {result['warning']['message']}", use_color)
|
|
56
58
|
return _success(f"done: {result['name']}{versions}", use_color)
|
|
57
59
|
detail = str(result.get("stderr") or result.get("stdout") or "No error output.").strip().replace("\n", " ")
|
|
58
60
|
detail = detail[-500:]
|
|
@@ -709,7 +711,8 @@ def handle_update(rest, ctx):
|
|
|
709
711
|
failed = [row for row in results if row.get("returncode") != 0]
|
|
710
712
|
message = "Applied workstation update plan" if not failed else "Workstation update plan completed with failures"
|
|
711
713
|
if parsed["json"]:
|
|
712
|
-
|
|
714
|
+
warnings = [dict(row["warning"], name=row["name"]) for row in results if row.get("warning")]
|
|
715
|
+
_write_json(ctx, _json_success("update.all", message, warnings=warnings, plan=plan, results=results, failed=len(failed)))
|
|
713
716
|
else:
|
|
714
717
|
ctx["out"](f"{_success(message, ctx['use_color']) if not failed else _warn(message, ctx['use_color'])}\n")
|
|
715
718
|
return 1 if failed else 0
|
package/src/commands/runs.py
CHANGED
|
@@ -713,16 +713,26 @@ def handle_run(rest, ctx):
|
|
|
713
713
|
session = selected["session"]
|
|
714
714
|
selection_row = selected.get("row") or {}
|
|
715
715
|
|
|
716
|
+
if parsed.get("remote") and session["provider"] == "antigravity":
|
|
717
|
+
parsed["isolate_workspace"] = True
|
|
716
718
|
remote_version = None
|
|
717
719
|
if parsed.get("remote"):
|
|
718
|
-
if session["provider"]
|
|
719
|
-
raise CdxError("--remote is only available for Codex profiles.", code="remote_provider_unsupported")
|
|
720
|
+
if session["provider"] not in ("codex", "claude", "ollama", "antigravity"):
|
|
721
|
+
raise CdxError("--remote is only available for Codex, Claude, Ollama and Antigravity profiles.", code="remote_provider_unsupported")
|
|
720
722
|
if parsed.get("continue_run"):
|
|
721
723
|
raise CdxError("--remote requires a fresh run without --continue-run.")
|
|
724
|
+
if session["provider"] == "ollama" and not (parsed.get("model") or (session.get("launch") or {}).get("model")):
|
|
725
|
+
raise CdxError("Remote Ollama text generation requires --model or a configured local model.")
|
|
726
|
+
if session["provider"] == "claude":
|
|
727
|
+
from ..claude_remote_token import read_token
|
|
728
|
+
read_token(session)
|
|
729
|
+
parsed["isolate_workspace"] = True
|
|
722
730
|
remote_version = preflight(
|
|
723
731
|
parsed["remote"], {**os.environ, **(ctx.get("env") or {})},
|
|
724
732
|
remote_prefix=parsed.get("remote_prefix"), isolate_workspace=parsed.get("isolate_workspace", False),
|
|
725
|
-
cwd=parsed["cwd"], network=parsed.get("network_access", False), writable=parsed.get("permission") != "review"
|
|
733
|
+
cwd=parsed["cwd"], network=parsed.get("network_access", False), writable=parsed.get("permission") != "review",
|
|
734
|
+
**({"provider": "ollama", "model": parsed.get("model") or (session.get("launch") or {}).get("model")}
|
|
735
|
+
if session["provider"] == "ollama" else {"provider": session["provider"]}))
|
|
726
736
|
cwd = os.path.abspath(parsed["cwd"])
|
|
727
737
|
if not parsed.get("remote") and not os.path.isdir(cwd):
|
|
728
738
|
raise CdxError(f"Invalid cwd: {parsed['cwd']}", code="invalid_cwd")
|
|
@@ -1074,6 +1084,9 @@ def handle_run(rest, ctx):
|
|
|
1074
1084
|
message = f"Every eligible session was rate limited: {', '.join(attempted)}."
|
|
1075
1085
|
elif failover_error_code == "failover_limit_reached":
|
|
1076
1086
|
message = f"Failover stopped after {MAX_FAILOVER_TRANSITIONS} transitions: {', '.join(attempted)}."
|
|
1087
|
+
elif run_info.get("provider_error"):
|
|
1088
|
+
failover_error_code = run_info["provider_error"]
|
|
1089
|
+
message = run_info["provider_message"]
|
|
1077
1090
|
elif run_info.get("timed_out"):
|
|
1078
1091
|
message = "Provider process timed out."
|
|
1079
1092
|
else:
|
package/src/config.py
CHANGED
|
@@ -78,6 +78,21 @@ def normalize_permission(value):
|
|
|
78
78
|
return text if text in PERMISSION_VALUES else None
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def normalize_launch_max_parallel(value):
|
|
82
|
+
"""Validate the optional per-profile automatic mission limit."""
|
|
83
|
+
from .errors import CdxError
|
|
84
|
+
|
|
85
|
+
if isinstance(value, bool) or not isinstance(value, (int, str)):
|
|
86
|
+
raise CdxError("Max parallel must be an integer from 0 to 5.")
|
|
87
|
+
try:
|
|
88
|
+
limit = int(value)
|
|
89
|
+
except ValueError as error:
|
|
90
|
+
raise CdxError("Max parallel must be an integer from 0 to 5.") from error
|
|
91
|
+
if not 0 <= limit <= 5:
|
|
92
|
+
raise CdxError("Max parallel must be an integer from 0 to 5.")
|
|
93
|
+
return limit
|
|
94
|
+
|
|
95
|
+
|
|
81
96
|
def get_cdx_home(env=None):
|
|
82
97
|
if env is None:
|
|
83
98
|
env = os.environ
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Incremental, read-only conversation pages from provider JSONL transcripts."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from .config import PROVIDERS
|
|
11
|
+
from .errors import CdxError
|
|
12
|
+
from .interactive_usage import conversation_transcript, resolve_transcript
|
|
13
|
+
|
|
14
|
+
MAX_LINE_BYTES = 1024 * 1024
|
|
15
|
+
MAX_PAGE_BYTES = 8 * 1024 * 1024
|
|
16
|
+
REDACTED = "[REDACTED]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _redact(text):
|
|
20
|
+
# There is no shared transcript redactor; launch argument redaction only
|
|
21
|
+
# hides prompt argv. Do not read credential stores to discover secrets.
|
|
22
|
+
text = re.sub(r"-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)", REDACTED, text, flags=re.S)
|
|
23
|
+
text = re.sub(r"(?i)\b(?:[\w.-]*(?:password|passwd|secret|api[_-]?key|token)|authorization)\b[\"']?\s*[:=]\s*(?:\"[^\"]*\"|'[^']*'|[^\s,;}]+)", REDACTED, text)
|
|
24
|
+
text = re.sub(r"(?i)\b(?:bearer|basic)\s+[a-z0-9._~+/=-]+", REDACTED, text)
|
|
25
|
+
text = re.sub(r"\b(?:sk-[a-zA-Z0-9_-]+|gh[pousr]_[a-zA-Z0-9]+|github_pat_[a-zA-Z0-9_]+|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|xox[baprs]-[a-zA-Z0-9-]+|AIza[a-zA-Z0-9_-]+)\b", REDACTED, text)
|
|
26
|
+
text = re.sub(r"\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b", REDACTED, text)
|
|
27
|
+
text = re.sub(r"https?://(?:hooks\.slack\.com/services|(?:discord(?:app)?\.com)/api/webhooks)/[^\s\"'<>]+", REDACTED, text)
|
|
28
|
+
return re.sub(r"(https?://)[^\s/@]+:[^\s/@]+@", r"\1[REDACTED]@", text)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _entry(role, text="", name=None, summary=None, at=None):
|
|
32
|
+
return {"role": role, "text": _redact(text), "tool_name": _redact(name) if name else None,
|
|
33
|
+
"tool_summary": summary, "at": at, "id": None}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _tool_name(message, names, result=False):
|
|
37
|
+
identifier = message.get("call_id") or message.get("tool_use_id") or message.get("id")
|
|
38
|
+
key = hashlib.sha256(identifier.encode()).hexdigest() if isinstance(identifier, str) else None
|
|
39
|
+
name = message.get("name") or (names.get(key) if result else None)
|
|
40
|
+
name = _redact(name) if isinstance(name, str) else "tool"
|
|
41
|
+
if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_.:/-]{0,99}", name):
|
|
42
|
+
name = "tool"
|
|
43
|
+
if key and not result:
|
|
44
|
+
if len(names) >= 100:
|
|
45
|
+
names.pop(next(iter(names)))
|
|
46
|
+
names[key] = name
|
|
47
|
+
return name
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _entries(provider, record, names):
|
|
51
|
+
if not isinstance(record, dict):
|
|
52
|
+
return []
|
|
53
|
+
stamp = record.get("timestamp")
|
|
54
|
+
try:
|
|
55
|
+
datetime.fromisoformat(stamp.replace("Z", "+00:00"))
|
|
56
|
+
except (AttributeError, TypeError, ValueError):
|
|
57
|
+
stamp = None
|
|
58
|
+
if provider == "codex":
|
|
59
|
+
# event_msg repeats response_item messages; consume only canonical items.
|
|
60
|
+
if record.get("type") != "response_item":
|
|
61
|
+
return []
|
|
62
|
+
message = record.get("payload")
|
|
63
|
+
else:
|
|
64
|
+
message = record.get("message")
|
|
65
|
+
if not isinstance(message, dict):
|
|
66
|
+
return []
|
|
67
|
+
kind = message.get("type")
|
|
68
|
+
if isinstance(kind, str) and (kind.endswith("_call") or kind.endswith("_call_output")):
|
|
69
|
+
result = kind.endswith("output")
|
|
70
|
+
tool = message
|
|
71
|
+
if not message.get("name") and kind not in ("function_call", "custom_tool_call", "function_call_output", "custom_tool_call_output"):
|
|
72
|
+
tool = {**message, "name": kind.removesuffix("_output").removesuffix("_call")}
|
|
73
|
+
name = _tool_name(tool, names, result)
|
|
74
|
+
return [_entry("tool", name=name, summary="Result recorded" if result else "Call recorded", at=stamp)]
|
|
75
|
+
role = message.get("role") or record.get("type")
|
|
76
|
+
if role not in ("user", "assistant", "tool"):
|
|
77
|
+
return []
|
|
78
|
+
if role == "tool":
|
|
79
|
+
return [_entry("tool", name=_tool_name(message, names, True), summary="Result recorded", at=stamp)]
|
|
80
|
+
content = message.get("content")
|
|
81
|
+
if isinstance(content, str):
|
|
82
|
+
return [_entry(role, content, at=stamp)]
|
|
83
|
+
if not isinstance(content, list):
|
|
84
|
+
return []
|
|
85
|
+
entries = []
|
|
86
|
+
for block in content:
|
|
87
|
+
if not isinstance(block, dict):
|
|
88
|
+
continue
|
|
89
|
+
kind = block.get("type")
|
|
90
|
+
if kind in ("text", "input_text", "output_text") and isinstance(block.get("text"), str):
|
|
91
|
+
entries.append(_entry(role, block["text"], at=stamp))
|
|
92
|
+
elif kind in ("tool_use", "tool_result"):
|
|
93
|
+
name = _tool_name(block, names, kind == "tool_result")
|
|
94
|
+
entries.append(_entry("tool", name=name, summary="Call recorded" if kind == "tool_use" else "Result recorded", at=stamp))
|
|
95
|
+
return entries
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _anchor(handle, offset):
|
|
99
|
+
handle.seek(max(0, offset - 128))
|
|
100
|
+
return hashlib.sha256(handle.read(min(offset, 128))).hexdigest()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def read_conversation(provider, auth_home=None, *, conversation_id=None, started_at=None,
|
|
104
|
+
terminal_transcript=None, cursor=None, limit=100):
|
|
105
|
+
"""Return a JSON-serializable page; pass next_cursor back unchanged to poll.
|
|
106
|
+
|
|
107
|
+
Entries follow file/block order. Cursors bind a byte offset and block index
|
|
108
|
+
to the resolved file; reset_required means discard the old cursor and page
|
|
109
|
+
again from the beginning. Incomplete trailing lines are retried next time.
|
|
110
|
+
Tool bodies and reasoning blocks are never returned. Ollama PTY captures
|
|
111
|
+
cannot reliably distinguish roles and are explicitly unsupported.
|
|
112
|
+
"""
|
|
113
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= 1000:
|
|
114
|
+
raise CdxError("Conversation limit must be an integer between 1 and 1000.")
|
|
115
|
+
page = {"status": "ok", "reason": None, "provider": provider, "entries": [], "next_cursor": None, "has_more": False}
|
|
116
|
+
if provider not in PROVIDERS or provider == "ollama":
|
|
117
|
+
return {**page, "status": "unsupported", "reason": "no_structured_transcript_reader"}
|
|
118
|
+
# An explicit conversation must never fall back to an unrelated recent file.
|
|
119
|
+
if conversation_id:
|
|
120
|
+
path = conversation_transcript(provider, auth_home, conversation_id)
|
|
121
|
+
else:
|
|
122
|
+
path, _match = resolve_transcript(provider, auth_home, started_at, terminal_transcript=terminal_transcript)
|
|
123
|
+
if not path:
|
|
124
|
+
return {**page, "status": "unavailable", "reason": "transcript_not_found"}
|
|
125
|
+
try:
|
|
126
|
+
with open(path, "rb") as handle:
|
|
127
|
+
stat = os.fstat(handle.fileno())
|
|
128
|
+
identity = hashlib.sha256(f"{provider}:{os.path.realpath(path)}:{stat.st_dev}:{stat.st_ino}".encode()).hexdigest()
|
|
129
|
+
offset, index, names = 0, 0, {}
|
|
130
|
+
if cursor is not None:
|
|
131
|
+
try:
|
|
132
|
+
if not isinstance(cursor, str) or len(cursor) > 65536:
|
|
133
|
+
raise ValueError
|
|
134
|
+
state = json.loads(base64.urlsafe_b64decode(cursor.encode("ascii")))
|
|
135
|
+
offset, index = state["offset"], state["index"]
|
|
136
|
+
names = state["names"]
|
|
137
|
+
if (not isinstance(names, dict) or len(names) > 100
|
|
138
|
+
or any(not isinstance(k, str) or not re.fullmatch(r"[0-9a-f]{64}", k)
|
|
139
|
+
or not isinstance(v, str) or not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_.:/-]{0,99}", v)
|
|
140
|
+
or _redact(v) != v for k, v in names.items())):
|
|
141
|
+
raise ValueError
|
|
142
|
+
if (state["v"] != 1 or state["file"] != identity
|
|
143
|
+
or type(offset) is not int or type(index) is not int
|
|
144
|
+
or not 0 <= offset <= stat.st_size or not 0 <= index <= MAX_LINE_BYTES
|
|
145
|
+
or state["anchor"] != _anchor(handle, offset)):
|
|
146
|
+
raise ValueError
|
|
147
|
+
except (ValueError, TypeError, KeyError, AttributeError, UnicodeError):
|
|
148
|
+
return {**page, "status": "reset_required", "reason": "invalid_or_stale_cursor"}
|
|
149
|
+
handle.seek(offset)
|
|
150
|
+
scanned = 0
|
|
151
|
+
while len(page["entries"]) < limit and scanned < MAX_PAGE_BYTES:
|
|
152
|
+
start = handle.tell()
|
|
153
|
+
line = handle.readline(MAX_LINE_BYTES + 1)
|
|
154
|
+
if len(line) > MAX_LINE_BYTES:
|
|
155
|
+
return {**page, "status": "unavailable", "reason": "record_too_large", "entries": []}
|
|
156
|
+
if not line or not line.endswith(b"\n"):
|
|
157
|
+
break
|
|
158
|
+
scanned += len(line)
|
|
159
|
+
try:
|
|
160
|
+
entries = _entries(provider, json.loads(line), names)
|
|
161
|
+
except (ValueError, UnicodeError):
|
|
162
|
+
entries = []
|
|
163
|
+
for block_index in range(index, len(entries)):
|
|
164
|
+
entry = entries[block_index]
|
|
165
|
+
entry["id"] = f"{identity[:16]}:{start}:{block_index}"
|
|
166
|
+
page["entries"].append(entry)
|
|
167
|
+
if len(page["entries"]) == limit:
|
|
168
|
+
index = block_index + 1
|
|
169
|
+
break
|
|
170
|
+
else:
|
|
171
|
+
index = len(entries)
|
|
172
|
+
if index < len(entries):
|
|
173
|
+
offset = start
|
|
174
|
+
break
|
|
175
|
+
offset, index = handle.tell(), 0
|
|
176
|
+
page["has_more"] = offset < stat.st_size
|
|
177
|
+
state = {"v": 1, "file": identity, "offset": offset, "index": index, "anchor": _anchor(handle, offset), "names": names}
|
|
178
|
+
page["next_cursor"] = base64.urlsafe_b64encode(json.dumps(state, separators=(",", ":")).encode()).decode()
|
|
179
|
+
return page
|
|
180
|
+
except OSError:
|
|
181
|
+
return {**page, "status": "unavailable", "reason": "transcript_unreadable", "entries": []}
|
package/src/interactive_usage.py
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
"""Best-effort usage readers for interactive provider sessions.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- Antigravity has no retrievable source. Its conversation history is
|
|
10
|
-
schema-less binary protobuf under `~/.gemini/antigravity-cli/conversations/`
|
|
11
|
-
with no `.proto` available to decode field numbers into token counts, and its
|
|
12
|
-
logs are plain glog with no usage-bearing line at any verbosity `agy --help`
|
|
13
|
-
exposes. This was investigated and is a dead end, not an oversight -- do not
|
|
14
|
-
spend the afternoon rediscovering it.
|
|
3
|
+
- Claude and Codex expose per-session token counts in JSONL transcripts.
|
|
4
|
+
- Ollama exposes counts in the verbose terminal output captured by cdx.
|
|
5
|
+
- Antigravity exposes quota through interactive statusLine stdin (collected by
|
|
6
|
+
antigravity_status), and per-run tokens through `agy -p --output-format json`
|
|
7
|
+
(read by run_usage). Interactive per-run token counts remain unavailable;
|
|
8
|
+
quota changes are not token measurements.
|
|
15
9
|
"""
|
|
16
10
|
|
|
17
11
|
import json
|