claude-code-tools 1.16.0__tar.gz → 1.16.1__tar.gz
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.
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/PKG-INFO +7 -5
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/README.md +6 -4
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server.py +96 -92
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server_cli.py +23 -23
- claude_code_tools-1.16.1/claude_code_tools/codex_server_generation.py +52 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server_models.py +217 -3
- claude_code_tools-1.16.1/claude_code_tools/codex_server_reuse.py +90 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server_worker.py +3 -2
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/pyproject.toml +2 -2
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/.gitignore +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/LICENSE +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/config.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/aichat.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server_process.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/export_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/find_codex_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/node_menu_ui.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/search_index.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/session_utils.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/tmux_cli_controller.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/trim_session.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_contract.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_rendering.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_snapshots.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/claude_code_tools/workflow_validation.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/agent-tunnel-spec.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/ai-agent-writing-skills.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/cc-codex-instructions.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/claude-code-chutes.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/ensemble-ideas.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/example-with-images.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/linked-in-20260102.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/lmsh.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/local-llm-setup.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/msg-phase-a-plan.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/msg-plan-v2.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/paper-writing-style-transfer-tools.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/reddit-aichat-resume-v2.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/reddit-aichat-resume.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/reddit-aichat.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/reddit-post.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/rollover-details.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/hatch_build.py +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/node_ui/menu.js +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.16.0 → claude_code_tools-1.16.1}/node_ui/package.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-tools
|
|
3
|
-
Version: 1.16.
|
|
3
|
+
Version: 1.16.1
|
|
4
4
|
Summary: Collection of tools for working with Claude Code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -98,10 +98,12 @@ codex-dynamic resume --last
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
`codex-dynamic` starts or reuses the local app server and then hands the
|
|
101
|
-
terminal directly to Codex.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`codex-server
|
|
101
|
+
terminal directly to Codex. After a plugin or Codex upgrade, the next launch
|
|
102
|
+
automatically starts a fresh server generation; existing sessions and workflow
|
|
103
|
+
callbacks stay connected to their old generation. No manual restart is needed.
|
|
104
|
+
Use `codex-server status` or `logs` for nondisruptive inspection. A forced stop
|
|
105
|
+
or restart cleans up every retained generation and disconnects its attached
|
|
106
|
+
TUIs, so use those commands only after exiting the sessions normally.
|
|
105
107
|
|
|
106
108
|
The Python package also installs `codex-workflows`, an observational dashboard
|
|
107
109
|
for durable workflow runs:
|
|
@@ -72,10 +72,12 @@ codex-dynamic resume --last
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`codex-dynamic` starts or reuses the local app server and then hands the
|
|
75
|
-
terminal directly to Codex.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`codex-server
|
|
75
|
+
terminal directly to Codex. After a plugin or Codex upgrade, the next launch
|
|
76
|
+
automatically starts a fresh server generation; existing sessions and workflow
|
|
77
|
+
callbacks stay connected to their old generation. No manual restart is needed.
|
|
78
|
+
Use `codex-server status` or `logs` for nondisruptive inspection. A forced stop
|
|
79
|
+
or restart cleans up every retained generation and disconnects its attached
|
|
80
|
+
TUIs, so use those commands only after exiting the sessions normally.
|
|
79
81
|
|
|
80
82
|
The Python package also installs `codex-workflows`, an observational dashboard
|
|
81
83
|
for durable workflow runs:
|
|
@@ -26,10 +26,11 @@ from claude_code_tools.codex_server_fingerprint import (
|
|
|
26
26
|
plugin_configuration_snapshot as _plugin_configuration_snapshot,
|
|
27
27
|
read_plugin_configuration as _read_plugin_configuration, # noqa: F401
|
|
28
28
|
)
|
|
29
|
+
from claude_code_tools.codex_server_generation import server_generation
|
|
29
30
|
|
|
30
31
|
from claude_code_tools.codex_server_models import (
|
|
32
|
+
CODEX_SERVER_GENERATION_ENV,
|
|
31
33
|
CODEX_SERVER_OPTIONS_ENV,
|
|
32
|
-
ENDPOINT,
|
|
33
34
|
FORCED_STOP_SECONDS,
|
|
34
35
|
GRACEFUL_STOP_SECONDS,
|
|
35
36
|
LOG_TRUNCATION_MARKER_MAX_BYTES,
|
|
@@ -43,14 +44,20 @@ from claude_code_tools.codex_server_models import (
|
|
|
43
44
|
ServerProbe,
|
|
44
45
|
ServerStatus,
|
|
45
46
|
StateFileError,
|
|
47
|
+
all_server_paths,
|
|
48
|
+
base_paths_from_env,
|
|
49
|
+
clear_current_generation,
|
|
46
50
|
log_tail,
|
|
47
51
|
log_tail_stream,
|
|
48
52
|
open_log_append,
|
|
49
53
|
open_log_reader,
|
|
50
54
|
paths_from_env,
|
|
55
|
+
paths_for_generation,
|
|
51
56
|
prepare_runtime,
|
|
57
|
+
publish_current_generation,
|
|
52
58
|
quarantine_invalid_state,
|
|
53
59
|
read_state,
|
|
60
|
+
require_generation_capacity,
|
|
54
61
|
remove_state,
|
|
55
62
|
write_state,
|
|
56
63
|
)
|
|
@@ -67,6 +74,11 @@ from claude_code_tools.codex_server_process import (
|
|
|
67
74
|
wait_for_process_group_exit,
|
|
68
75
|
wait_for_process_identity,
|
|
69
76
|
)
|
|
77
|
+
from claude_code_tools.codex_server_reuse import (
|
|
78
|
+
disconnect_refusal as _disconnect_refusal,
|
|
79
|
+
helper_restart_reason as _reuse_restart_reason,
|
|
80
|
+
require_external_compatible as _external_compatible,
|
|
81
|
+
)
|
|
70
82
|
|
|
71
83
|
|
|
72
84
|
UNKNOWN_SUBCOMMAND_MARKERS = (
|
|
@@ -166,6 +178,10 @@ def _command_env(env: Mapping[str, str], paths: ServerPaths) -> dict[str, str]:
|
|
|
166
178
|
"""Return a child environment pinned to the resolved Codex home."""
|
|
167
179
|
child_env = dict(env)
|
|
168
180
|
child_env["CODEX_HOME"] = str(paths.codex_home)
|
|
181
|
+
if paths.generation is None:
|
|
182
|
+
child_env.pop(CODEX_SERVER_GENERATION_ENV, None)
|
|
183
|
+
else:
|
|
184
|
+
child_env[CODEX_SERVER_GENERATION_ENV] = paths.generation
|
|
169
185
|
return child_env
|
|
170
186
|
|
|
171
187
|
|
|
@@ -345,6 +361,13 @@ def _probe_server(
|
|
|
345
361
|
paths: ServerPaths,
|
|
346
362
|
) -> ServerProbe:
|
|
347
363
|
"""Probe the app-server protocol, with an older-CLI socket fallback."""
|
|
364
|
+
if paths.generation is not None:
|
|
365
|
+
accepting = _socket_accepts(paths.socket_path)
|
|
366
|
+
return ServerProbe(
|
|
367
|
+
running=accepting,
|
|
368
|
+
method="socket" if accepting else None,
|
|
369
|
+
accepting=accepting,
|
|
370
|
+
)
|
|
348
371
|
if codex_path:
|
|
349
372
|
result = _run_command(
|
|
350
373
|
[codex_path, "app-server", "daemon", "version"],
|
|
@@ -508,66 +531,21 @@ def _helper_restart_reason(
|
|
|
508
531
|
probe: ServerProbe,
|
|
509
532
|
plugin_fingerprint: str,
|
|
510
533
|
paths: ServerPaths,
|
|
534
|
+
codex_options: Sequence[str] = (),
|
|
511
535
|
) -> str | None:
|
|
512
536
|
"""Explain why a helper listener cannot be safely reused."""
|
|
513
|
-
|
|
514
|
-
return "its durable supervisor exited"
|
|
515
|
-
if state.codex_path != codex_path:
|
|
516
|
-
return "the active Codex executable path changed"
|
|
517
|
-
if state.codex_executable_identity is None:
|
|
518
|
-
return "its Codex executable identity predates replacement detection"
|
|
519
|
-
if state.codex_executable_identity != codex_executable_identity:
|
|
520
|
-
return "the active Codex executable was replaced"
|
|
521
|
-
if _version_key(state.codex_version) != _version_key(codex_version):
|
|
522
|
-
return "the active Codex CLI version changed"
|
|
523
|
-
if probe.server_version is not None and _version_key(
|
|
524
|
-
probe.server_version
|
|
525
|
-
) != _version_key(codex_version):
|
|
526
|
-
return "the running app-server version differs from the Codex CLI"
|
|
527
|
-
if (probe.running or probe.accepting) and not _listener_matches_worker(
|
|
537
|
+
return _reuse_restart_reason(
|
|
528
538
|
state,
|
|
539
|
+
codex_path,
|
|
540
|
+
codex_executable_identity,
|
|
541
|
+
codex_version,
|
|
542
|
+
probe,
|
|
543
|
+
plugin_fingerprint,
|
|
529
544
|
paths,
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if state.plugin_fingerprint != plugin_fingerprint:
|
|
535
|
-
return "the Codex plugin or marketplace configuration changed"
|
|
536
|
-
return None
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _disconnect_refusal(action: str) -> CodexServerError:
|
|
540
|
-
"""Explain why a shared-server lifecycle action needs acknowledgement."""
|
|
541
|
-
return CodexServerError(
|
|
542
|
-
f"refusing to {action} the shared app server without --force: this "
|
|
543
|
-
"disconnects every codex-dynamic TUI, and Codex exits those sessions. "
|
|
544
|
-
"Exit connected sessions first, then retry with --force; their "
|
|
545
|
-
"transcripts remain resumable"
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
def _require_external_compatible(
|
|
550
|
-
codex_version: str,
|
|
551
|
-
probe: ServerProbe,
|
|
552
|
-
) -> None:
|
|
553
|
-
"""Reject external listeners, whose plugin snapshot is uncertifiable."""
|
|
554
|
-
server_key = _version_key(probe.server_version)
|
|
555
|
-
cli_key = _version_key(codex_version)
|
|
556
|
-
if server_key is None:
|
|
557
|
-
raise CodexServerError(
|
|
558
|
-
"an external app server is running, but its version could not be "
|
|
559
|
-
"verified; stop it before using codex-server"
|
|
560
|
-
)
|
|
561
|
-
if server_key != cli_key:
|
|
562
|
-
raise CodexServerError(
|
|
563
|
-
f"external app-server version {probe.server_version!r} does not "
|
|
564
|
-
f"match the selected Codex CLI {codex_version!r}; stop or restart "
|
|
565
|
-
"the external server"
|
|
566
|
-
)
|
|
567
|
-
raise CodexServerError(
|
|
568
|
-
"an external app server is running, but codex-server cannot verify "
|
|
569
|
-
"which plugin snapshot it loaded; stop it before using "
|
|
570
|
-
"codex-dynamic workflow callbacks"
|
|
545
|
+
codex_options,
|
|
546
|
+
_listener_matches_worker,
|
|
547
|
+
state_controller_matches,
|
|
548
|
+
_version_key,
|
|
571
549
|
)
|
|
572
550
|
|
|
573
551
|
|
|
@@ -646,7 +624,9 @@ def _certify_helper_boundary(
|
|
|
646
624
|
probe = _probe_server(codex_path, child_env, paths)
|
|
647
625
|
if not probe.running:
|
|
648
626
|
raise CodexServerError(f"the app-server listener vanished during {operation}")
|
|
649
|
-
if _version_key(probe.server_version) != _version_key(
|
|
627
|
+
if paths.generation is None and _version_key(probe.server_version) != _version_key(
|
|
628
|
+
codex_version
|
|
629
|
+
):
|
|
650
630
|
raise CodexServerError(
|
|
651
631
|
f"the app-server version changed or was uncertified during {operation}"
|
|
652
632
|
)
|
|
@@ -671,6 +651,10 @@ def _certified_helper_status(
|
|
|
671
651
|
probe: ServerProbe,
|
|
672
652
|
) -> ServerStatus:
|
|
673
653
|
"""Build a helper result only from final certified evidence."""
|
|
654
|
+
version = probe.server_version
|
|
655
|
+
if version is None:
|
|
656
|
+
key = _version_key(state.codex_version)
|
|
657
|
+
version = ".".join(map(str, key)) if key is not None else None
|
|
674
658
|
return ServerStatus(
|
|
675
659
|
status="running",
|
|
676
660
|
ownership="helper",
|
|
@@ -678,7 +662,7 @@ def _certified_helper_status(
|
|
|
678
662
|
pid=state.pid,
|
|
679
663
|
codex_path=state.codex_path,
|
|
680
664
|
codex_version=state.codex_version,
|
|
681
|
-
server_version=
|
|
665
|
+
server_version=version,
|
|
682
666
|
probe_method=probe.method,
|
|
683
667
|
)
|
|
684
668
|
|
|
@@ -747,17 +731,34 @@ def ensure_server(
|
|
|
747
731
|
) -> ServerStatus:
|
|
748
732
|
"""Reuse a compatible listener or start a supervised app server."""
|
|
749
733
|
active_env = dict(os.environ if env is None else env)
|
|
750
|
-
|
|
734
|
+
base_paths = base_paths_from_env(active_env)
|
|
751
735
|
codex_path = _resolve_codex(active_env)
|
|
752
736
|
codex_executable_identity = _codex_executable_identity(codex_path)
|
|
737
|
+
base_child_env = _command_env(active_env, base_paths)
|
|
738
|
+
codex_version = _require_compatible_codex(codex_path, base_child_env)
|
|
739
|
+
plugin_snapshot = _plugin_configuration_snapshot(base_paths, codex_options)
|
|
740
|
+
generation = server_generation(
|
|
741
|
+
codex_path,
|
|
742
|
+
codex_executable_identity,
|
|
743
|
+
codex_version,
|
|
744
|
+
plugin_snapshot.fingerprint,
|
|
745
|
+
codex_options,
|
|
746
|
+
)
|
|
747
|
+
paths = paths_for_generation(base_paths, generation)
|
|
748
|
+
require_generation_capacity(base_paths, generation)
|
|
753
749
|
child_env = _command_env(active_env, paths)
|
|
754
750
|
child_env[CODEX_SERVER_OPTIONS_ENV] = json.dumps(
|
|
755
751
|
list(codex_options),
|
|
756
752
|
separators=(",", ":"),
|
|
757
753
|
)
|
|
758
|
-
codex_version = _require_compatible_codex(codex_path, child_env)
|
|
759
754
|
with _lifecycle_lock(paths):
|
|
760
|
-
|
|
755
|
+
locked_snapshot = _plugin_configuration_snapshot(paths, codex_options)
|
|
756
|
+
if locked_snapshot.fingerprint != plugin_snapshot.fingerprint:
|
|
757
|
+
raise CodexServerError(
|
|
758
|
+
"the Codex plugin or marketplace snapshot changed during "
|
|
759
|
+
"app-server generation selection; retry after updates finish"
|
|
760
|
+
)
|
|
761
|
+
plugin_snapshot = locked_snapshot
|
|
761
762
|
plugin_fingerprint = plugin_snapshot.fingerprint
|
|
762
763
|
try:
|
|
763
764
|
state = _read_state(paths)
|
|
@@ -774,7 +775,7 @@ def ensure_server(
|
|
|
774
775
|
"shared app server cannot be reused because its socket "
|
|
775
776
|
"listener is not owned by the supervised worker"
|
|
776
777
|
)
|
|
777
|
-
|
|
778
|
+
_external_compatible(codex_version, probe, _version_key)
|
|
778
779
|
return status
|
|
779
780
|
if state is not None and _state_is_owned(state):
|
|
780
781
|
restart_reason = _helper_restart_reason(
|
|
@@ -785,6 +786,7 @@ def ensure_server(
|
|
|
785
786
|
probe,
|
|
786
787
|
plugin_fingerprint,
|
|
787
788
|
paths,
|
|
789
|
+
codex_options,
|
|
788
790
|
)
|
|
789
791
|
if restart_reason is None and not probe.running:
|
|
790
792
|
probe = _retry_helper_probe(
|
|
@@ -802,6 +804,7 @@ def ensure_server(
|
|
|
802
804
|
probe,
|
|
803
805
|
plugin_fingerprint,
|
|
804
806
|
paths,
|
|
807
|
+
codex_options,
|
|
805
808
|
)
|
|
806
809
|
if restart_reason is None:
|
|
807
810
|
certified_state, certified_probe = _certify_helper_boundary(
|
|
@@ -813,17 +816,16 @@ def ensure_server(
|
|
|
813
816
|
codex_options,
|
|
814
817
|
"app-server reuse checks",
|
|
815
818
|
)
|
|
816
|
-
|
|
819
|
+
result = _certified_helper_status(
|
|
817
820
|
paths,
|
|
818
821
|
certified_state,
|
|
819
822
|
certified_probe,
|
|
820
823
|
)
|
|
824
|
+
publish_current_generation(base_paths, generation)
|
|
825
|
+
return result
|
|
821
826
|
raise CodexServerError(
|
|
822
|
-
"
|
|
823
|
-
f"{restart_reason}
|
|
824
|
-
"because that would disconnect every codex-dynamic TUI. Exit "
|
|
825
|
-
"connected sessions, run `codex-server restart --force`, then "
|
|
826
|
-
"start or resume Codex"
|
|
827
|
+
"selected app-server generation cannot be reused because "
|
|
828
|
+
f"{restart_reason}; refusing to replace an uncertified listener"
|
|
827
829
|
)
|
|
828
830
|
|
|
829
831
|
if state_error:
|
|
@@ -888,11 +890,13 @@ def ensure_server(
|
|
|
888
890
|
codex_options,
|
|
889
891
|
"app-server startup checks",
|
|
890
892
|
)
|
|
891
|
-
|
|
893
|
+
result = _certified_helper_status(
|
|
892
894
|
paths,
|
|
893
895
|
certified_state,
|
|
894
896
|
certified_probe,
|
|
895
897
|
)
|
|
898
|
+
publish_current_generation(base_paths, generation)
|
|
899
|
+
return result
|
|
896
900
|
except BaseException as exc:
|
|
897
901
|
if starting is not None:
|
|
898
902
|
try:
|
|
@@ -910,14 +914,12 @@ def ensure_server(
|
|
|
910
914
|
raise
|
|
911
915
|
|
|
912
916
|
|
|
913
|
-
def
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
allow_disconnect: bool
|
|
917
|
+
def _stop_server_at(
|
|
918
|
+
active_env: Mapping[str, str],
|
|
919
|
+
paths: ServerPaths,
|
|
920
|
+
allow_disconnect: bool,
|
|
917
921
|
) -> ServerStatus:
|
|
918
|
-
"""Stop
|
|
919
|
-
active_env = dict(os.environ if env is None else env)
|
|
920
|
-
paths = _paths(active_env)
|
|
922
|
+
"""Stop one selected helper generation."""
|
|
921
923
|
try:
|
|
922
924
|
codex_path = _resolve_codex(active_env)
|
|
923
925
|
except CodexServerError:
|
|
@@ -954,6 +956,23 @@ def stop_server(
|
|
|
954
956
|
return ServerStatus(status="stopped", ownership=None, paths=paths)
|
|
955
957
|
|
|
956
958
|
|
|
959
|
+
def stop_server(
|
|
960
|
+
env: Mapping[str, str] | None = None,
|
|
961
|
+
*,
|
|
962
|
+
allow_disconnect: bool = False,
|
|
963
|
+
) -> ServerStatus:
|
|
964
|
+
"""Stop helper-owned servers after explicit disconnect acknowledgement."""
|
|
965
|
+
active_env = dict(os.environ if env is None else env)
|
|
966
|
+
selected = _paths(active_env)
|
|
967
|
+
if not allow_disconnect:
|
|
968
|
+
return _stop_server_at(active_env, selected, False)
|
|
969
|
+
base = base_paths_from_env(active_env)
|
|
970
|
+
for paths in all_server_paths(base):
|
|
971
|
+
_stop_server_at(active_env, paths, True)
|
|
972
|
+
clear_current_generation(base)
|
|
973
|
+
return ServerStatus(status="stopped", ownership=None, paths=selected)
|
|
974
|
+
|
|
975
|
+
|
|
957
976
|
def restart_server(
|
|
958
977
|
env: Mapping[str, str] | None = None,
|
|
959
978
|
*,
|
|
@@ -974,18 +993,3 @@ def restart_server(
|
|
|
974
993
|
codex_options = state.codex_options
|
|
975
994
|
stop_server(active_env, allow_disconnect=allow_disconnect)
|
|
976
995
|
return ensure_server(active_env, codex_options=codex_options)
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
__all__ = [
|
|
980
|
-
"ENDPOINT",
|
|
981
|
-
"CodexServerError",
|
|
982
|
-
"OwnedServer",
|
|
983
|
-
"ServerPaths",
|
|
984
|
-
"ServerProbe",
|
|
985
|
-
"ServerStatus",
|
|
986
|
-
"StateFileError",
|
|
987
|
-
"ensure_server",
|
|
988
|
-
"get_status",
|
|
989
|
-
"restart_server",
|
|
990
|
-
"stop_server",
|
|
991
|
-
]
|
|
@@ -12,7 +12,6 @@ from typing import BinaryIO, NoReturn, Sequence
|
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
14
|
from claude_code_tools.codex_server import (
|
|
15
|
-
ENDPOINT,
|
|
16
15
|
CodexServerError,
|
|
17
16
|
ServerStatus,
|
|
18
17
|
_command_env,
|
|
@@ -58,9 +57,6 @@ NON_TUI_COMMANDS = {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
CALLBACK_ENDPOINT_ENV = "CCTOOLS_CODEX_CALLBACK_ENDPOINT"
|
|
61
|
-
CALLBACK_SHELL_CONFIG = (
|
|
62
|
-
f'shell_environment_policy.set.{CALLBACK_ENDPOINT_ENV}="{ENDPOINT}"'
|
|
63
|
-
)
|
|
64
60
|
|
|
65
61
|
GLOBAL_OPTIONS_WITH_VALUES = {
|
|
66
62
|
"--add-dir",
|
|
@@ -107,20 +103,20 @@ def _echo_status(status: ServerStatus, json_output: bool) -> None:
|
|
|
107
103
|
click.echo(f"{status.status} (managed by codex-server, pid {status.pid})")
|
|
108
104
|
else:
|
|
109
105
|
click.echo(status.status)
|
|
110
|
-
click.echo(f"endpoint: {
|
|
106
|
+
click.echo(f"endpoint: {status.paths.endpoint}")
|
|
111
107
|
if status.detail:
|
|
112
108
|
click.echo(f"detail: {status.detail}")
|
|
113
109
|
|
|
114
110
|
|
|
115
111
|
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
116
112
|
def server_cli() -> None:
|
|
117
|
-
"""Manage
|
|
113
|
+
"""Manage generated app servers used for Codex workflow callbacks."""
|
|
118
114
|
|
|
119
115
|
|
|
120
116
|
@server_cli.command("start")
|
|
121
117
|
@click.option("--json", "json_output", is_flag=True, help="Emit JSON status.")
|
|
122
118
|
def start_command(json_output: bool) -> None:
|
|
123
|
-
"""Start the server
|
|
119
|
+
"""Start or reuse the server for the current Codex/plugin generation."""
|
|
124
120
|
try:
|
|
125
121
|
status = ensure_server()
|
|
126
122
|
except CodexServerError as exc:
|
|
@@ -143,11 +139,11 @@ def status_command(json_output: bool) -> None:
|
|
|
143
139
|
@click.option(
|
|
144
140
|
"--force",
|
|
145
141
|
is_flag=True,
|
|
146
|
-
help="
|
|
142
|
+
help="Stop all generations; every connected codex-dynamic TUI will exit.",
|
|
147
143
|
)
|
|
148
144
|
@click.option("--json", "json_output", is_flag=True, help="Emit JSON status.")
|
|
149
145
|
def stop_command(force: bool, json_output: bool) -> None:
|
|
150
|
-
"""Stop
|
|
146
|
+
"""Stop the current generation; use --force to stop all generations."""
|
|
151
147
|
try:
|
|
152
148
|
status = stop_server(allow_disconnect=force)
|
|
153
149
|
except CodexServerError as exc:
|
|
@@ -159,11 +155,11 @@ def stop_command(force: bool, json_output: bool) -> None:
|
|
|
159
155
|
@click.option(
|
|
160
156
|
"--force",
|
|
161
157
|
is_flag=True,
|
|
162
|
-
help="
|
|
158
|
+
help="Restart all generations; every connected codex-dynamic TUI will exit.",
|
|
163
159
|
)
|
|
164
160
|
@click.option("--json", "json_output", is_flag=True, help="Emit JSON status.")
|
|
165
161
|
def restart_command(force: bool, json_output: bool) -> None:
|
|
166
|
-
"""Restart
|
|
162
|
+
"""Restart the current generation; use --force to clean up all."""
|
|
167
163
|
try:
|
|
168
164
|
status = restart_server(allow_disconnect=force)
|
|
169
165
|
except CodexServerError as exc:
|
|
@@ -331,34 +327,38 @@ def dynamic_main() -> NoReturn:
|
|
|
331
327
|
raise SystemExit(2)
|
|
332
328
|
active_env = dict(os.environ)
|
|
333
329
|
use_remote = not (_is_information_only(arguments) or _is_non_tui_command(arguments))
|
|
330
|
+
endpoint: str | None = None
|
|
334
331
|
try:
|
|
335
332
|
codex_path = _resolve_codex(active_env)
|
|
336
333
|
if use_remote:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
ensure_server(
|
|
340
|
-
child_env,
|
|
334
|
+
status = ensure_server(
|
|
335
|
+
active_env,
|
|
341
336
|
codex_options=_server_configuration_options(arguments),
|
|
342
337
|
)
|
|
343
|
-
|
|
338
|
+
endpoint = status.paths.endpoint
|
|
339
|
+
child_env = _command_env(active_env, status.paths)
|
|
340
|
+
child_env[CALLBACK_ENDPOINT_ENV] = endpoint
|
|
344
341
|
else:
|
|
345
342
|
child_env = dict(active_env)
|
|
346
343
|
child_env.pop(CALLBACK_ENDPOINT_ENV, None)
|
|
347
344
|
except CodexServerError as exc:
|
|
348
345
|
click.echo(f"Error: {exc}", err=True)
|
|
349
346
|
raise SystemExit(1) from exc
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
if use_remote:
|
|
348
|
+
assert endpoint is not None
|
|
349
|
+
command = [
|
|
352
350
|
codex_path,
|
|
353
351
|
"--config",
|
|
354
|
-
|
|
352
|
+
(
|
|
353
|
+
f"shell_environment_policy.set.{CALLBACK_ENDPOINT_ENV}="
|
|
354
|
+
f"{json.dumps(endpoint)}"
|
|
355
|
+
),
|
|
355
356
|
"--remote",
|
|
356
|
-
|
|
357
|
+
endpoint,
|
|
357
358
|
*arguments,
|
|
358
359
|
]
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
360
|
+
else:
|
|
361
|
+
command = [codex_path, *arguments]
|
|
362
362
|
try:
|
|
363
363
|
os.execvpe(codex_path, command, child_env)
|
|
364
364
|
except OSError as exc:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Deterministic identities for independently running app-server generations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
GENERATION_PATTERN = re.compile(r"[0-9a-f]{24}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def server_generation(
|
|
15
|
+
codex_path: str,
|
|
16
|
+
executable_identity: str,
|
|
17
|
+
codex_version: str,
|
|
18
|
+
plugin_fingerprint: str,
|
|
19
|
+
codex_options: Sequence[str],
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Return the generation selected by every server-affecting input.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
codex_path: Canonical path to the selected Codex executable.
|
|
25
|
+
executable_identity: Stable identity of the executable file.
|
|
26
|
+
codex_version: Version reported by that executable.
|
|
27
|
+
plugin_fingerprint: Fingerprint of the effective plugin configuration.
|
|
28
|
+
codex_options: Global Codex options forwarded to the app server.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A short lowercase hexadecimal generation identifier.
|
|
32
|
+
"""
|
|
33
|
+
payload = json.dumps(
|
|
34
|
+
{
|
|
35
|
+
"codexOptions": list(codex_options),
|
|
36
|
+
"codexPath": codex_path,
|
|
37
|
+
"codexVersion": codex_version,
|
|
38
|
+
"executableIdentity": executable_identity,
|
|
39
|
+
"pluginFingerprint": plugin_fingerprint,
|
|
40
|
+
},
|
|
41
|
+
ensure_ascii=True,
|
|
42
|
+
separators=(",", ":"),
|
|
43
|
+
sort_keys=True,
|
|
44
|
+
).encode("utf-8")
|
|
45
|
+
return hashlib.sha256(payload).hexdigest()[:24]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_generation(value: str) -> str:
|
|
49
|
+
"""Validate an app-server generation received through the environment."""
|
|
50
|
+
if GENERATION_PATTERN.fullmatch(value) is None:
|
|
51
|
+
raise ValueError("app-server generation must be 24 lowercase hex characters")
|
|
52
|
+
return value
|