claude-code-tools 1.30.4__tar.gz → 1.31.0__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.30.4 → claude_code_tools-1.31.0}/PKG-INFO +1 -1
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/aichat.py +53 -2
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_claude_to_codex.py +5 -1
- claude_code_tools-1.31.0/claude_code_tools/port_codex_provider.py +268 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_render.py +29 -17
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_service.py +140 -5
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/local-llm-setup.md +18 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/pyproject.toml +2 -2
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/.gitignore +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/LICENSE +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/README.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/config.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/http_frontend.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/mattermost_bot.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/readonly.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/relay.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/serve.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/agent_tunnel/turn_log.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/__init__.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/__main__.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/activity.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/cache.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/cli.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/detect.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/filters.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/model.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/render.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/amux/scan.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/cli_passthrough.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_app_server_rpc.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_callback.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_cli.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_generation.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_legacy.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_models.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_process.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_reservation.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_resume.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_retry.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_reuse.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_state.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_wait.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/codex_server_worker.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/export_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/find_codex_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/github_wake.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/github_watch_daemon.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/github_watch_store.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/helper_sessions.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/issue_reply_delivery.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/legacy_session_wrappers.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/move_account.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/migrations.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/node_menu_ui.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_claude_noise.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_codex_flatten.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_codex_to_claude.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/resolve_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/resolve_session_names.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/resolve_session_render.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/search_index.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_cli_resolution.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_resolution.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_selection.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/session_utils.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/tmux_cli_controller.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/trim_session.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_contract.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_rendering.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_snapshots.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/workflow_validation.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/agent-tunnel-spec.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/lmsh.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/hatch_build.py +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/node_ui/menu.js +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/node_ui/package.json +0 -0
|
@@ -1858,12 +1858,31 @@ def _write_lines_atomically(
|
|
|
1858
1858
|
@click.argument("session", required=True)
|
|
1859
1859
|
@click.option("--claude-home", help="Path to Claude home directory")
|
|
1860
1860
|
@click.option("--codex-home", help="Path to Codex home directory")
|
|
1861
|
+
@click.option(
|
|
1862
|
+
"--provider",
|
|
1863
|
+
help=(
|
|
1864
|
+
"Codex model provider for the new session (e.g. openai, "
|
|
1865
|
+
"local-fractal). With a Codex session: copy it onto this "
|
|
1866
|
+
"provider instead of porting to Claude. With a Claude session: "
|
|
1867
|
+
"the new Codex session's provider (default openai)."
|
|
1868
|
+
),
|
|
1869
|
+
)
|
|
1870
|
+
@click.option(
|
|
1871
|
+
"--model",
|
|
1872
|
+
help=(
|
|
1873
|
+
"Model to resume the new Codex session with; added to the "
|
|
1874
|
+
"printed resume command as -m (Codex otherwise resumes with "
|
|
1875
|
+
"the session's last model)."
|
|
1876
|
+
),
|
|
1877
|
+
)
|
|
1861
1878
|
@click.pass_context
|
|
1862
1879
|
def port_session(
|
|
1863
1880
|
ctx: click.Context,
|
|
1864
1881
|
session: str,
|
|
1865
1882
|
claude_home: "str | None",
|
|
1866
1883
|
codex_home: "str | None",
|
|
1884
|
+
provider: "str | None",
|
|
1885
|
+
model: "str | None",
|
|
1867
1886
|
) -> None:
|
|
1868
1887
|
"""Port a session to the OTHER agent (codex <-> claude).
|
|
1869
1888
|
|
|
@@ -1872,6 +1891,14 @@ def port_session(
|
|
|
1872
1891
|
Claude sessions are converted into a flattened, resumable Codex
|
|
1873
1892
|
rollout.
|
|
1874
1893
|
|
|
1894
|
+
With --provider, a Codex session is instead copied to a new Codex
|
|
1895
|
+
session bound to that model provider (Codex only resumes a thread
|
|
1896
|
+
under the provider it was created with). OpenAI-only encrypted
|
|
1897
|
+
reasoning and compaction summaries (and a local model's reasoning)
|
|
1898
|
+
are dropped; everything else is kept. Use --provider openai to move
|
|
1899
|
+
a local-model session back, with --model naming the model to resume
|
|
1900
|
+
with.
|
|
1901
|
+
|
|
1875
1902
|
SESSION may be a full session id, a partial id (prefix, middle,
|
|
1876
1903
|
or suffix fragment), a session name (set with /rename), a
|
|
1877
1904
|
rollout filename fragment (e.g. 2026-03-25T14-50), or a session
|
|
@@ -1883,6 +1910,8 @@ def port_session(
|
|
|
1883
1910
|
aichat port abc123 --claude-home ~/.claude-alt
|
|
1884
1911
|
aichat port my-session-name
|
|
1885
1912
|
aichat port 2026-03-25T14-50
|
|
1913
|
+
aichat port 01a1a29a --provider local-fractal
|
|
1914
|
+
aichat port 01a1a2aa --provider openai --model gpt-6.1-sol
|
|
1886
1915
|
"""
|
|
1887
1916
|
import sys
|
|
1888
1917
|
|
|
@@ -1893,6 +1922,7 @@ def port_session(
|
|
|
1893
1922
|
port_claude_session,
|
|
1894
1923
|
port_codex_session,
|
|
1895
1924
|
resolve_port_session,
|
|
1925
|
+
retarget_codex_session,
|
|
1896
1926
|
)
|
|
1897
1927
|
|
|
1898
1928
|
# Group-level --claude-home/--codex-home (given BEFORE the
|
|
@@ -1924,13 +1954,34 @@ def port_session(
|
|
|
1924
1954
|
display.error(str(e))
|
|
1925
1955
|
sys.exit(1)
|
|
1926
1956
|
|
|
1927
|
-
|
|
1957
|
+
if model and resolved.agent == "codex" and not provider:
|
|
1958
|
+
display.error("--model only applies to a Codex target: add --provider")
|
|
1959
|
+
sys.exit(1)
|
|
1960
|
+
target_agent = "codex" if (resolved.agent == "claude" or provider) else "claude"
|
|
1961
|
+
display.resolved(resolved, target_agent)
|
|
1928
1962
|
if resolved.agent == "claude":
|
|
1929
1963
|
try:
|
|
1930
|
-
with display.porting(resolved.agent):
|
|
1964
|
+
with display.porting(resolved.agent, target_agent):
|
|
1931
1965
|
result = port_claude_session(
|
|
1932
1966
|
resolved.session_file,
|
|
1933
1967
|
codex_home=codex_home,
|
|
1968
|
+
provider=provider,
|
|
1969
|
+
model=model or "",
|
|
1970
|
+
)
|
|
1971
|
+
except PortSessionError as e:
|
|
1972
|
+
display.error(str(e))
|
|
1973
|
+
sys.exit(1)
|
|
1974
|
+
display.complete(result, resolved.agent)
|
|
1975
|
+
return
|
|
1976
|
+
|
|
1977
|
+
if provider:
|
|
1978
|
+
try:
|
|
1979
|
+
with display.porting(resolved.agent, target_agent):
|
|
1980
|
+
result = retarget_codex_session(
|
|
1981
|
+
resolved.session_file,
|
|
1982
|
+
provider,
|
|
1983
|
+
codex_home=codex_home,
|
|
1984
|
+
model=model or "",
|
|
1934
1985
|
)
|
|
1935
1986
|
except PortSessionError as e:
|
|
1936
1987
|
display.error(str(e))
|
{claude_code_tools-1.30.4 → claude_code_tools-1.31.0}/claude_code_tools/port_claude_to_codex.py
RENAMED
|
@@ -756,6 +756,7 @@ def _append_history_transactional(
|
|
|
756
756
|
def port_claude_session_to_codex(
|
|
757
757
|
claude_session_file: Union[str, Path],
|
|
758
758
|
codex_home: Optional[Union[str, Path]] = None,
|
|
759
|
+
model_provider: str = CODEX_MODEL_PROVIDER,
|
|
759
760
|
) -> tuple[str, Path]:
|
|
760
761
|
"""Convert a Claude Code session into a new Codex rollout file.
|
|
761
762
|
|
|
@@ -771,6 +772,9 @@ def port_claude_session_to_codex(
|
|
|
771
772
|
claude_session_file: Path to the Claude session JSONL file.
|
|
772
773
|
codex_home: Optional Codex home directory (defaults to
|
|
773
774
|
``~/.codex``).
|
|
775
|
+
model_provider: Codex model provider the new rollout is bound
|
|
776
|
+
to (Codex only resumes a thread under the provider it
|
|
777
|
+
records). Defaults to ``openai``.
|
|
774
778
|
|
|
775
779
|
Returns:
|
|
776
780
|
Tuple ``(new_session_id, new_file_path)``.
|
|
@@ -829,7 +833,7 @@ def port_claude_session_to_codex(
|
|
|
829
833
|
"cwd": cwd,
|
|
830
834
|
"originator": PORT_ORIGINATOR,
|
|
831
835
|
"cli_version": "aichat-port",
|
|
832
|
-
"model_provider":
|
|
836
|
+
"model_provider": model_provider,
|
|
833
837
|
}
|
|
834
838
|
if meta["branch"]:
|
|
835
839
|
payload["git"] = {"branch": meta["branch"]}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Re-target a Codex rollout at another model provider.
|
|
2
|
+
|
|
3
|
+
Codex records the model provider a thread was created with in the
|
|
4
|
+
rollout's ``session_meta`` line (``"model_provider": "openai"``), and the
|
|
5
|
+
Codex TUI refuses to resume a thread under a different provider
|
|
6
|
+
("This chat uses openai, but your current provider is ..."). This module
|
|
7
|
+
writes a NEW rollout, forked from the original, that is bound to the
|
|
8
|
+
requested provider instead, so a session can move between OpenAI and a
|
|
9
|
+
local model (or between any two providers) in either direction.
|
|
10
|
+
|
|
11
|
+
Everything in the transcript is kept except what one provider cannot
|
|
12
|
+
take from another: reasoning items (OpenAI's are encrypted; a local
|
|
13
|
+
model's carry raw ``reasoning_text`` content, which OpenAI rejects as
|
|
14
|
+
input) and any item carrying ``encrypted_content``, such as OpenAI's
|
|
15
|
+
encrypted compaction summaries. They are dropped from the top-level
|
|
16
|
+
history and from the ``replacement_history`` / ``guardian_history``
|
|
17
|
+
lists inside ``compacted`` records. Visible messages, tool calls, tool
|
|
18
|
+
output and plaintext summaries all survive.
|
|
19
|
+
|
|
20
|
+
Codex also resumes with the model a session last used, which belongs to
|
|
21
|
+
the old provider; :class:`RetargetStats` reports it so the caller can
|
|
22
|
+
tell the user to pick a model with ``-m``.
|
|
23
|
+
|
|
24
|
+
The source rollout is never modified. The new rollout goes to the same
|
|
25
|
+
Codex home as the source (or ``codex_home`` when given) and is appended
|
|
26
|
+
to that home's ``history.jsonl`` like every other ported session.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import datetime
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
from collections import Counter
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, Iterator, Optional, Union
|
|
38
|
+
|
|
39
|
+
from claude_code_tools.port_claude_to_codex import (
|
|
40
|
+
_append_history_transactional,
|
|
41
|
+
_new_codex_session_id,
|
|
42
|
+
_utc_now_codex_timestamp,
|
|
43
|
+
)
|
|
44
|
+
from claude_code_tools.port_codex_to_claude import _write_transcript_atomic
|
|
45
|
+
from claude_code_tools.session_utils import get_codex_home
|
|
46
|
+
|
|
47
|
+
# Provider ids are TOML table keys in Codex's config ([model_providers.X])
|
|
48
|
+
# and appear unquoted on resume command lines.
|
|
49
|
+
_PROVIDER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")
|
|
50
|
+
|
|
51
|
+
# History lists inside a ``compacted`` record that hold response items.
|
|
52
|
+
_COMPACTED_HISTORY_KEYS = ("replacement_history", "guardian_history")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class RetargetStats:
|
|
57
|
+
"""What changed while re-targeting a rollout.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
source_provider: Provider recorded in the source rollout.
|
|
61
|
+
dropped: Count of dropped provider-only items, by item type
|
|
62
|
+
(e.g. ``{"reasoning": 192, "compaction": 2}``).
|
|
63
|
+
last_model: Model of the session's last turn ("" if none).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
source_provider: str
|
|
67
|
+
dropped: Counter = field(default_factory=Counter)
|
|
68
|
+
last_model: str = ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate_provider(provider: str) -> str:
|
|
72
|
+
"""Check a provider id and return it.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
provider: Provider id such as ``openai`` or ``local-fractal``.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The provider id, unchanged.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If the id is empty or not a plain identifier.
|
|
82
|
+
"""
|
|
83
|
+
if not _PROVIDER_RE.match(provider or ""):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Invalid provider {provider!r}: use letters, digits, '-' or '_' "
|
|
86
|
+
"(e.g. openai, local-fractal)"
|
|
87
|
+
)
|
|
88
|
+
return provider
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_provider_only(item: Any) -> bool:
|
|
92
|
+
"""True for a response item another provider cannot take as input."""
|
|
93
|
+
return isinstance(item, dict) and (
|
|
94
|
+
item.get("type") == "reasoning" or bool(item.get("encrypted_content")))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _codex_home_of(rollout: Path) -> Optional[Path]:
|
|
98
|
+
"""Codex home holding ``rollout``.
|
|
99
|
+
|
|
100
|
+
Live rollouts sit in ``<home>/sessions/Y/M/D/``; archived ones in
|
|
101
|
+
``<home>/archived_sessions/``.
|
|
102
|
+
"""
|
|
103
|
+
parents = rollout.parents
|
|
104
|
+
if len(parents) >= 5 and parents[3].name == "sessions":
|
|
105
|
+
return parents[4]
|
|
106
|
+
if len(parents) >= 2 and parents[0].name == "archived_sessions":
|
|
107
|
+
return parents[1]
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _read_session_meta(rollout: Path) -> dict[str, Any]:
|
|
112
|
+
"""Return the first line's session_meta payload, or raise."""
|
|
113
|
+
with open(rollout, "r", encoding="utf-8") as f:
|
|
114
|
+
first = f.readline()
|
|
115
|
+
try:
|
|
116
|
+
record = json.loads(first)
|
|
117
|
+
except ValueError:
|
|
118
|
+
record = None
|
|
119
|
+
if not isinstance(record, dict) or record.get("type") != "session_meta" \
|
|
120
|
+
or not isinstance(record.get("payload"), dict):
|
|
121
|
+
raise ValueError(f"Not a Codex rollout (no session_meta first): {rollout}")
|
|
122
|
+
return record["payload"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _first_user_text(record: dict[str, Any]) -> Optional[str]:
|
|
126
|
+
"""The text of a user_message event, if ``record`` is one."""
|
|
127
|
+
payload = record.get("payload")
|
|
128
|
+
if record.get("type") == "event_msg" and isinstance(payload, dict) \
|
|
129
|
+
and payload.get("type") == "user_message":
|
|
130
|
+
message = payload.get("message")
|
|
131
|
+
if isinstance(message, str) and message.strip():
|
|
132
|
+
return message
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _retarget_lines(
|
|
137
|
+
rollout: Path,
|
|
138
|
+
*,
|
|
139
|
+
provider: str,
|
|
140
|
+
new_id: str,
|
|
141
|
+
now_iso: str,
|
|
142
|
+
stats: RetargetStats,
|
|
143
|
+
preview: dict[str, Optional[str]],
|
|
144
|
+
) -> Iterator[dict[str, Any]]:
|
|
145
|
+
"""Stream the source rollout's records, re-targeted at ``provider``."""
|
|
146
|
+
first = True
|
|
147
|
+
with open(rollout, "r", encoding="utf-8") as f:
|
|
148
|
+
for raw in f:
|
|
149
|
+
if not raw.strip():
|
|
150
|
+
continue
|
|
151
|
+
record = json.loads(raw)
|
|
152
|
+
if not isinstance(record, dict):
|
|
153
|
+
yield record
|
|
154
|
+
continue
|
|
155
|
+
kind = record.get("type")
|
|
156
|
+
payload = record.get("payload")
|
|
157
|
+
if kind == "session_meta" and isinstance(payload, dict):
|
|
158
|
+
payload = {**payload, "model_provider": provider}
|
|
159
|
+
if first:
|
|
160
|
+
old_id = payload.get("id")
|
|
161
|
+
payload.update(id=new_id, session_id=new_id,
|
|
162
|
+
timestamp=now_iso, forked_from_id=old_id)
|
|
163
|
+
record = {
|
|
164
|
+
**record,
|
|
165
|
+
"timestamp": now_iso,
|
|
166
|
+
"payload": payload,
|
|
167
|
+
"continue_metadata": {
|
|
168
|
+
"parent_session_id": old_id,
|
|
169
|
+
"parent_session_file": str(rollout.absolute()),
|
|
170
|
+
"continued_at": now_iso,
|
|
171
|
+
"retargeted_from_provider": stats.source_provider,
|
|
172
|
+
"retargeted_to_provider": provider,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
else: # lineage line of the thread this one forked from
|
|
176
|
+
record = {**record, "payload": payload}
|
|
177
|
+
first = False
|
|
178
|
+
elif kind == "turn_context" and isinstance(payload, dict):
|
|
179
|
+
if isinstance(payload.get("model"), str):
|
|
180
|
+
stats.last_model = payload["model"]
|
|
181
|
+
elif kind == "response_item" and _is_provider_only(payload):
|
|
182
|
+
stats.dropped[str(payload.get("type"))] += 1
|
|
183
|
+
continue
|
|
184
|
+
elif kind == "compacted" and isinstance(payload, dict):
|
|
185
|
+
payload = dict(payload)
|
|
186
|
+
for key in _COMPACTED_HISTORY_KEYS:
|
|
187
|
+
items = payload.get(key)
|
|
188
|
+
if isinstance(items, list):
|
|
189
|
+
kept = []
|
|
190
|
+
for x in items:
|
|
191
|
+
if isinstance(x, dict) and _is_provider_only(x):
|
|
192
|
+
stats.dropped[str(x.get("type"))] += 1
|
|
193
|
+
else:
|
|
194
|
+
kept.append(x)
|
|
195
|
+
payload[key] = kept
|
|
196
|
+
record = {**record, "payload": payload}
|
|
197
|
+
elif kind == "event_msg" and isinstance(payload, dict) \
|
|
198
|
+
and payload.get("type") == "thread_settings_applied":
|
|
199
|
+
settings = payload.get("thread_settings")
|
|
200
|
+
if isinstance(settings, dict) and "model_provider_id" in settings:
|
|
201
|
+
record = {**record, "payload": {
|
|
202
|
+
**payload,
|
|
203
|
+
"thread_settings": {**settings,
|
|
204
|
+
"model_provider_id": provider}}}
|
|
205
|
+
if preview["text"] is None:
|
|
206
|
+
preview["text"] = _first_user_text(record)
|
|
207
|
+
yield record
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def retarget_codex_session(
|
|
211
|
+
rollout_file: Union[str, Path],
|
|
212
|
+
provider: str,
|
|
213
|
+
codex_home: Optional[Union[str, Path]] = None,
|
|
214
|
+
) -> tuple[str, Path, RetargetStats]:
|
|
215
|
+
"""Write a copy of a Codex rollout bound to another model provider.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
rollout_file: Path to the source Codex rollout JSONL file.
|
|
219
|
+
provider: Target provider id (``openai``, ``local-fractal``, ...).
|
|
220
|
+
codex_home: Codex home for the new rollout. Defaults to the
|
|
221
|
+
source rollout's own home, else ``~/.codex``.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Tuple ``(new_session_id, new_file_path, stats)``.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
FileNotFoundError: If the source rollout does not exist.
|
|
228
|
+
ValueError: If the provider id is invalid, the file is not a
|
|
229
|
+
Codex rollout, or it already uses ``provider``.
|
|
230
|
+
OSError: On filesystem failures (no partial file is left, and
|
|
231
|
+
the history entry is rolled back).
|
|
232
|
+
"""
|
|
233
|
+
validate_provider(provider)
|
|
234
|
+
source = Path(rollout_file).expanduser()
|
|
235
|
+
if not source.is_file():
|
|
236
|
+
raise FileNotFoundError(f"Codex session not found: {source}")
|
|
237
|
+
meta = _read_session_meta(source)
|
|
238
|
+
source_provider = str(meta.get("model_provider") or "openai")
|
|
239
|
+
if source_provider == provider:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"Session already uses provider {provider!r}; nothing to change"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if codex_home is not None:
|
|
245
|
+
home_dir = get_codex_home(str(codex_home))
|
|
246
|
+
else:
|
|
247
|
+
home_dir = _codex_home_of(source.absolute()) or get_codex_home(None)
|
|
248
|
+
new_id = _new_codex_session_id()
|
|
249
|
+
now_iso = _utc_now_codex_timestamp()
|
|
250
|
+
now_local = datetime.datetime.now()
|
|
251
|
+
day_dir = home_dir / "sessions" / now_local.strftime("%Y/%m/%d")
|
|
252
|
+
day_dir.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
stamp = now_local.strftime("%Y-%m-%dT%H-%M-%S")
|
|
254
|
+
out_path = day_dir / f"rollout-{stamp}-{new_id}.jsonl"
|
|
255
|
+
|
|
256
|
+
stats = RetargetStats(source_provider=source_provider)
|
|
257
|
+
preview: dict[str, Optional[str]] = {"text": None}
|
|
258
|
+
_write_transcript_atomic(
|
|
259
|
+
out_path,
|
|
260
|
+
_retarget_lines(source, provider=provider, new_id=new_id,
|
|
261
|
+
now_iso=now_iso, stats=stats, preview=preview),
|
|
262
|
+
)
|
|
263
|
+
_append_history_transactional(
|
|
264
|
+
home_dir, new_id,
|
|
265
|
+
preview["text"] or f"Codex session moved to provider {provider}",
|
|
266
|
+
out_path,
|
|
267
|
+
)
|
|
268
|
+
return new_id, out_path, stats
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import shlex
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
@@ -106,9 +107,9 @@ def _target_agent(source_agent: str) -> str:
|
|
|
106
107
|
return "codex" if source_agent == "claude" else "claude"
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
def _target_label(source_agent: str) -> str:
|
|
110
|
+
def _target_label(source_agent: str, target_agent: str = "") -> str:
|
|
110
111
|
"""Return the product label for a destination agent."""
|
|
111
|
-
target = _target_agent(source_agent)
|
|
112
|
+
target = target_agent or _target_agent(source_agent)
|
|
112
113
|
return "Claude Code" if target == "claude" else "Codex"
|
|
113
114
|
|
|
114
115
|
|
|
@@ -161,15 +162,16 @@ class PortDisplay:
|
|
|
161
162
|
session_file=Path(record.session_file),
|
|
162
163
|
)
|
|
163
164
|
|
|
164
|
-
def resolved(self, session: "ResolvedSession") -> None:
|
|
165
|
+
def resolved(self, session: "ResolvedSession", target_agent: str = "") -> None:
|
|
165
166
|
"""Print the chosen source and detected direction."""
|
|
166
167
|
source = _agent_label(session.agent)
|
|
167
|
-
target = _target_label(session.agent)
|
|
168
|
+
target = _target_label(session.agent, target_agent)
|
|
168
169
|
self.console.print(
|
|
169
170
|
Text.assemble(
|
|
170
171
|
(" ✓ ", "bold green"),
|
|
171
172
|
(f"Detected source agent: {session.agent}", "bold"),
|
|
172
|
-
|
|
173
|
+
" — copying to a new Codex session on another provider"
|
|
174
|
+
if target_agent == session.agent else f" — porting to {target}",
|
|
173
175
|
)
|
|
174
176
|
)
|
|
175
177
|
self.console.print(
|
|
@@ -177,9 +179,9 @@ class PortDisplay:
|
|
|
177
179
|
)
|
|
178
180
|
self.console.print(f"[bold cyan]2/3[/] Porting {source} → {target}")
|
|
179
181
|
|
|
180
|
-
def porting(self, source_agent: str) -> Status:
|
|
182
|
+
def porting(self, source_agent: str, target_agent: str = "") -> Status:
|
|
181
183
|
"""Return the conversion-phase spinner."""
|
|
182
|
-
target = _target_label(source_agent)
|
|
184
|
+
target = _target_label(source_agent, target_agent)
|
|
183
185
|
return self.console.status(
|
|
184
186
|
f"[dim]Converting transcript and creating {target} session...[/dim]",
|
|
185
187
|
spinner="dots",
|
|
@@ -187,7 +189,7 @@ class PortDisplay:
|
|
|
187
189
|
|
|
188
190
|
def complete(self, result: "PortResult", source_agent: str) -> None:
|
|
189
191
|
"""Render the final destination details and resume command."""
|
|
190
|
-
target_agent = _target_agent(source_agent)
|
|
192
|
+
target_agent = result.target_agent or _target_agent(source_agent)
|
|
191
193
|
target = _agent_label(target_agent)
|
|
192
194
|
self.console.print("[bold cyan]3/3[/] Ready")
|
|
193
195
|
|
|
@@ -200,6 +202,8 @@ class PortDisplay:
|
|
|
200
202
|
)
|
|
201
203
|
table.add_row("Session cwd:", Text(result.cwd or "Unknown"))
|
|
202
204
|
table.add_row("Output file:", Text(str(result.output_file)))
|
|
205
|
+
if result.provider:
|
|
206
|
+
table.add_row("Model provider:", Text(result.provider))
|
|
203
207
|
self.console.print(
|
|
204
208
|
Panel(
|
|
205
209
|
table,
|
|
@@ -215,18 +219,29 @@ class PortDisplay:
|
|
|
215
219
|
overflow="ignore",
|
|
216
220
|
)
|
|
217
221
|
self.console.print(command, soft_wrap=True)
|
|
218
|
-
|
|
222
|
+
for note in result.notes:
|
|
223
|
+
self.console.print(Text(f" {note}", style="dim"))
|
|
224
|
+
if target_agent == "codex" and source_agent == "claude":
|
|
219
225
|
self.console.print()
|
|
220
226
|
self.console.print(
|
|
221
227
|
"[dim]Tip: Codex's /import is also available as an "
|
|
222
228
|
"interactive alternative.[/dim]"
|
|
223
229
|
)
|
|
224
|
-
|
|
230
|
+
# The follow-up command keeps every flag of the resume hint
|
|
231
|
+
# (provider, model, CODEX_HOME), with the id left to paste. The
|
|
232
|
+
# services build the hint as `cd <quoted cwd> && <command>`.
|
|
233
|
+
prefix = f"cd {shlex.quote(result.cwd)} && "
|
|
234
|
+
command = ""
|
|
235
|
+
if result.resume_hint.startswith(prefix):
|
|
236
|
+
command = result.resume_hint[len(prefix):].replace(
|
|
237
|
+
result.new_session_id, "<paste session ID>")
|
|
238
|
+
self.offer_session_id_copy(result.new_session_id, target_agent, command)
|
|
225
239
|
|
|
226
240
|
def offer_session_id_copy(
|
|
227
241
|
self,
|
|
228
242
|
session_id: str,
|
|
229
243
|
target_agent: str,
|
|
244
|
+
command: str = "",
|
|
230
245
|
) -> None:
|
|
231
246
|
"""Offer to copy a newly created session ID in interactive terminals."""
|
|
232
247
|
if not self.console.is_terminal or not sys.stdin.isatty():
|
|
@@ -250,14 +265,11 @@ class PortDisplay:
|
|
|
250
265
|
self.console.print(
|
|
251
266
|
"[bold green]✓ Session ID copied to clipboard.[/bold green]"
|
|
252
267
|
)
|
|
253
|
-
|
|
268
|
+
if not command:
|
|
269
|
+
command = ("codex resume" if target_agent == "codex"
|
|
270
|
+
else "claude --resume") + " <paste session ID>"
|
|
254
271
|
self.console.print("[bold]Now you can run:[/bold]")
|
|
255
|
-
self.console.print(
|
|
256
|
-
Text(
|
|
257
|
-
f" {command} <paste session ID>",
|
|
258
|
-
style="bold cyan",
|
|
259
|
-
)
|
|
260
|
-
)
|
|
272
|
+
self.console.print(Text(f" {command}", style="bold cyan"))
|
|
261
273
|
return
|
|
262
274
|
self.console.print(
|
|
263
275
|
"[yellow]Could not find a working system clipboard command. "
|