claude-code-tools 1.21.0__tar.gz → 1.23.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.21.0 → claude_code_tools-1.23.0}/PKG-INFO +1 -1
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/aichat.py +44 -38
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/export_session.py +60 -2
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/node_menu_ui.py +103 -4
- claude_code_tools-1.23.0/claude_code_tools/port_render.py +363 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/port_service.py +46 -4
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/search_index.py +79 -15
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/session_resolution.py +34 -9
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/tmux_cli_controller.py +5 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/pyproject.toml +2 -2
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/.gitignore +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/LICENSE +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/README.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/config.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_cli.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_generation.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_legacy.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_models.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_process.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_reservation.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_retry.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_reuse.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_state.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/codex_server_worker.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/find_codex_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/port_claude_noise.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/port_claude_to_codex.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/port_codex_flatten.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/port_codex_to_claude.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/resolve_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/resolve_session_names.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/resolve_session_render.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/session_utils.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/trim_session.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_contract.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_rendering.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_snapshots.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/claude_code_tools/workflow_validation.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/agent-tunnel-spec.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/lmsh.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/local-llm-setup.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/msg-phase-a-plan.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/msg-plan-v2.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/hatch_build.py +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/node_ui/menu.js +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.21.0 → claude_code_tools-1.23.0}/node_ui/package.json +0 -0
|
@@ -1768,7 +1768,9 @@ def port_session(
|
|
|
1768
1768
|
"""
|
|
1769
1769
|
import sys
|
|
1770
1770
|
|
|
1771
|
+
from claude_code_tools.port_render import PortDisplay
|
|
1771
1772
|
from claude_code_tools.port_service import (
|
|
1773
|
+
PortAmbiguityError,
|
|
1772
1774
|
PortSessionError,
|
|
1773
1775
|
port_claude_session,
|
|
1774
1776
|
port_codex_session,
|
|
@@ -1782,56 +1784,54 @@ def port_session(
|
|
|
1782
1784
|
claude_home = claude_home or root_obj.get("claude_home")
|
|
1783
1785
|
codex_home = codex_home or root_obj.get("codex_home")
|
|
1784
1786
|
|
|
1787
|
+
display = PortDisplay(session)
|
|
1788
|
+
display.start()
|
|
1785
1789
|
try:
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1790
|
+
with display.resolving():
|
|
1791
|
+
resolved = resolve_port_session(
|
|
1792
|
+
session,
|
|
1793
|
+
claude_home=claude_home,
|
|
1794
|
+
codex_home=codex_home,
|
|
1795
|
+
)
|
|
1796
|
+
except PortAmbiguityError as error:
|
|
1797
|
+
try:
|
|
1798
|
+
resolved = display.choose_candidate(error)
|
|
1799
|
+
except (EOFError, KeyboardInterrupt):
|
|
1800
|
+
display.error(str(error))
|
|
1801
|
+
sys.exit(1)
|
|
1802
|
+
if resolved is None:
|
|
1803
|
+
display.cancelled()
|
|
1804
|
+
sys.exit(1)
|
|
1789
1805
|
except PortSessionError as e:
|
|
1790
|
-
|
|
1806
|
+
display.error(str(e))
|
|
1791
1807
|
sys.exit(1)
|
|
1792
1808
|
|
|
1809
|
+
display.resolved(resolved)
|
|
1793
1810
|
if resolved.agent == "claude":
|
|
1794
|
-
print("Detected source agent: claude — porting to Codex")
|
|
1795
1811
|
try:
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1812
|
+
with display.porting(resolved.agent):
|
|
1813
|
+
result = port_claude_session(
|
|
1814
|
+
resolved.session_file,
|
|
1815
|
+
codex_home=codex_home,
|
|
1816
|
+
)
|
|
1799
1817
|
except PortSessionError as e:
|
|
1800
|
-
|
|
1818
|
+
display.error(str(e))
|
|
1801
1819
|
sys.exit(1)
|
|
1802
|
-
|
|
1803
|
-
print()
|
|
1804
|
-
print(f"New Codex session id: {result.new_session_id}")
|
|
1805
|
-
print(f"Output file: {result.output_file}")
|
|
1806
|
-
print(f"Session cwd: {result.cwd}")
|
|
1807
|
-
print()
|
|
1808
|
-
print("To resume:")
|
|
1809
|
-
print(f" {result.resume_hint}")
|
|
1810
|
-
print()
|
|
1811
|
-
print(
|
|
1812
|
-
"Tip: codex's /import can also import Claude sessions "
|
|
1813
|
-
"natively (interactive alternative)."
|
|
1814
|
-
)
|
|
1820
|
+
display.complete(result, resolved.agent)
|
|
1815
1821
|
return
|
|
1816
1822
|
|
|
1817
|
-
print("Detected source agent: codex — porting to Claude Code")
|
|
1818
1823
|
try:
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1824
|
+
with display.porting(resolved.agent):
|
|
1825
|
+
result = port_codex_session(
|
|
1826
|
+
resolved.session_file,
|
|
1827
|
+
claude_home=claude_home,
|
|
1828
|
+
)
|
|
1822
1829
|
except PortSessionError as e:
|
|
1823
1830
|
# Expected failure modes (missing/empty session, filesystem
|
|
1824
1831
|
# or encoding errors) become a clean error, not a traceback.
|
|
1825
|
-
|
|
1832
|
+
display.error(str(e))
|
|
1826
1833
|
sys.exit(1)
|
|
1827
|
-
|
|
1828
|
-
print()
|
|
1829
|
-
print(f"New Claude session id: {result.new_session_id}")
|
|
1830
|
-
print(f"Output file: {result.output_file}")
|
|
1831
|
-
print(f"Session cwd: {result.cwd}")
|
|
1832
|
-
print()
|
|
1833
|
-
print("To resume:")
|
|
1834
|
-
print(f" {result.resume_hint}")
|
|
1834
|
+
display.complete(result, resolved.agent)
|
|
1835
1835
|
|
|
1836
1836
|
|
|
1837
1837
|
@main.command("query")
|
|
@@ -2710,6 +2710,8 @@ def index_stats(index, cwd, claude_home, codex_home):
|
|
|
2710
2710
|
help='Limit number of results displayed')
|
|
2711
2711
|
@click.option('--no-original', is_flag=True, help='Exclude original sessions')
|
|
2712
2712
|
@click.option('--sub-agent', is_flag=True, help='Include sub-agent sessions (additive)')
|
|
2713
|
+
@click.option('--exec-runs', 'exec_runs', is_flag=True,
|
|
2714
|
+
help='Include headless `codex exec` runs (additive)')
|
|
2713
2715
|
@click.option('--no-trimmed', is_flag=True, help='Exclude trimmed sessions')
|
|
2714
2716
|
@click.option('--no-rollover', is_flag=True, help='Exclude rollover sessions')
|
|
2715
2717
|
@click.option('--live', is_flag=True, help='Show only currently running sessions')
|
|
@@ -2725,14 +2727,14 @@ def index_stats(index, cwd, claude_home, codex_home):
|
|
|
2725
2727
|
help='Output as JSONL for AI agents. Fields per line: session_id, '
|
|
2726
2728
|
'agent, project, branch, cwd, lines, created, modified, '
|
|
2727
2729
|
'first_msg, last_msg, file_path, derivation_type, '
|
|
2728
|
-
'is_sidechain, snippet')
|
|
2730
|
+
'is_sidechain, is_exec_run, snippet')
|
|
2729
2731
|
@click.option('--by-time', 'by_time', is_flag=True,
|
|
2730
2732
|
help='Sort results by last-modified time (default: sort by relevance)')
|
|
2731
2733
|
@click.argument('query', required=False)
|
|
2732
2734
|
def search(
|
|
2733
2735
|
claude_home_arg, codex_home_arg, global_search, filter_dir, filter_branch,
|
|
2734
|
-
num_results, no_original, sub_agent, no_trimmed, no_rollover, live,
|
|
2735
|
-
after, before, agent, json_output, by_time, query
|
|
2736
|
+
num_results, no_original, sub_agent, exec_runs, no_trimmed, no_rollover, live,
|
|
2737
|
+
min_lines, after, before, agent, json_output, by_time, query
|
|
2736
2738
|
):
|
|
2737
2739
|
"""Launch interactive TUI for full-text session search.
|
|
2738
2740
|
|
|
@@ -2828,6 +2830,8 @@ def search(
|
|
|
2828
2830
|
rust_args.append("--no-original")
|
|
2829
2831
|
if sub_agent:
|
|
2830
2832
|
rust_args.append("--sub-agent")
|
|
2833
|
+
if exec_runs:
|
|
2834
|
+
rust_args.append("--exec-runs")
|
|
2831
2835
|
if live:
|
|
2832
2836
|
rust_args.append("--live")
|
|
2833
2837
|
if no_trimmed:
|
|
@@ -3008,6 +3012,8 @@ def search(
|
|
|
3008
3012
|
rust_args.append("--no-original")
|
|
3009
3013
|
if filter_state.get("include_sub"):
|
|
3010
3014
|
rust_args.append("--sub-agent")
|
|
3015
|
+
if filter_state.get("include_exec"):
|
|
3016
|
+
rust_args.append("--exec-runs")
|
|
3011
3017
|
if filter_state.get("include_live_only"):
|
|
3012
3018
|
rust_args.append("--live")
|
|
3013
3019
|
if not filter_state.get("include_trimmed", True):
|
|
@@ -408,6 +408,45 @@ def extract_first_last_messages(
|
|
|
408
408
|
return first_msg, last_msg, first_user_msg
|
|
409
409
|
|
|
410
410
|
|
|
411
|
+
def _is_codex_subagent_payload(payload: dict[str, Any]) -> bool:
|
|
412
|
+
"""Return True when a Codex ``session_meta`` payload describes a sub-agent.
|
|
413
|
+
|
|
414
|
+
Codex names every thread ``rollout-*.jsonl``, so sub-agent threads can only
|
|
415
|
+
be told apart by their spawn metadata. A spawned thread records
|
|
416
|
+
``thread_source: "subagent"``, a ``source`` object keyed by ``subagent``,
|
|
417
|
+
and the id of the thread that spawned it.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
payload: The ``payload`` object of a ``session_meta`` record.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
True when the payload carries any sub-agent spawn marker.
|
|
424
|
+
"""
|
|
425
|
+
if payload.get("thread_source") == "subagent":
|
|
426
|
+
return True
|
|
427
|
+
source = payload.get("source")
|
|
428
|
+
if isinstance(source, dict) and "subagent" in source:
|
|
429
|
+
return True
|
|
430
|
+
return _nonempty_str(payload.get("parent_thread_id"))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _is_codex_exec_payload(payload: dict[str, Any]) -> bool:
|
|
434
|
+
"""Return True when a Codex ``session_meta`` payload describes a headless run.
|
|
435
|
+
|
|
436
|
+
Codex records how a thread was launched: ``"exec"`` for a headless
|
|
437
|
+
``codex exec`` run (typically spawned by an orchestrating agent or script),
|
|
438
|
+
``"cli"`` for the interactive TUI. Sub-agent spawns carry an object here
|
|
439
|
+
instead, and are reported by :func:`_is_codex_subagent_payload`.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
payload: The ``payload`` object of a ``session_meta`` record.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
True when the thread was launched headlessly.
|
|
446
|
+
"""
|
|
447
|
+
return payload.get("source") == "exec"
|
|
448
|
+
|
|
449
|
+
|
|
411
450
|
def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
412
451
|
"""
|
|
413
452
|
Extract metadata from a session JSONL file.
|
|
@@ -425,9 +464,12 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
425
464
|
Returns:
|
|
426
465
|
Dict with extracted metadata
|
|
427
466
|
"""
|
|
428
|
-
# Detect sidechain from filename pattern (agent-* prefix)
|
|
467
|
+
# Detect sidechain from filename pattern (agent-* prefix).
|
|
429
468
|
# This is more reliable than checking isSidechain field in JSON,
|
|
430
|
-
# which can be set on individual messages within main sessions
|
|
469
|
+
# which can be set on individual messages within main sessions.
|
|
470
|
+
# Claude sub-agent transcripts are written as agent-*.jsonl; Codex writes
|
|
471
|
+
# every thread as rollout-*.jsonl, so Codex sub-agents are detected from
|
|
472
|
+
# the session_meta record below instead.
|
|
431
473
|
is_sidechain = session_file.name.startswith("agent-")
|
|
432
474
|
|
|
433
475
|
metadata: dict[str, Any] = {
|
|
@@ -438,6 +480,7 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
438
480
|
"branch": None,
|
|
439
481
|
"derivation_type": None,
|
|
440
482
|
"is_sidechain": is_sidechain,
|
|
483
|
+
"is_exec_run": False, # Codex: launched headlessly via `codex exec`
|
|
441
484
|
"session_type": None, # "helper" for SDK/headless sessions
|
|
442
485
|
"parent_session_id": None,
|
|
443
486
|
"parent_session_file": None,
|
|
@@ -451,6 +494,13 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
451
494
|
# Track session start timestamp from JSON metadata
|
|
452
495
|
session_start_timestamp: str | None = None
|
|
453
496
|
|
|
497
|
+
# A forked Codex rollout replays its ancestors' session_meta records into
|
|
498
|
+
# the same file (1951 of 6880 real rollouts carry more than one, up to 29).
|
|
499
|
+
# Only the first one describes THIS session, so the launch-kind flags are
|
|
500
|
+
# taken from it alone -- otherwise an interactive fork of a headless
|
|
501
|
+
# ancestor inherits is_exec_run and vanishes from default search results.
|
|
502
|
+
codex_meta_seen = False
|
|
503
|
+
|
|
454
504
|
try:
|
|
455
505
|
with open(
|
|
456
506
|
session_file, "r", encoding="utf-8", errors="replace"
|
|
@@ -554,6 +604,12 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
554
604
|
metadata["cwd"] = payload["cwd"]
|
|
555
605
|
if _nonempty_str(payload.get("id")):
|
|
556
606
|
metadata["session_id"] = payload["id"]
|
|
607
|
+
if not codex_meta_seen:
|
|
608
|
+
codex_meta_seen = True
|
|
609
|
+
if _is_codex_subagent_payload(payload):
|
|
610
|
+
metadata["is_sidechain"] = True
|
|
611
|
+
if _is_codex_exec_payload(payload):
|
|
612
|
+
metadata["is_exec_run"] = True
|
|
557
613
|
if session_start_timestamp is None and _nonempty_str(
|
|
558
614
|
data.get("timestamp")
|
|
559
615
|
):
|
|
@@ -701,6 +757,8 @@ def generate_yaml_frontmatter(metadata: dict[str, Any]) -> str:
|
|
|
701
757
|
yaml_data["derivation_type"] = metadata["derivation_type"]
|
|
702
758
|
if metadata.get("is_sidechain"):
|
|
703
759
|
yaml_data["is_sidechain"] = metadata["is_sidechain"]
|
|
760
|
+
if metadata.get("is_exec_run"):
|
|
761
|
+
yaml_data["is_exec_run"] = metadata["is_exec_run"]
|
|
704
762
|
if metadata.get("parent_session_id"):
|
|
705
763
|
yaml_data["parent_session_id"] = metadata["parent_session_id"]
|
|
706
764
|
if metadata.get("parent_session_file"):
|
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
|
+
import shlex
|
|
13
|
+
import shutil
|
|
12
14
|
import subprocess
|
|
13
15
|
import sys
|
|
14
16
|
import tempfile
|
|
@@ -17,11 +19,90 @@ from typing import Any, Callable, Dict, Iterable, List
|
|
|
17
19
|
|
|
18
20
|
SessionDict = Dict[str, Any]
|
|
19
21
|
|
|
22
|
+
#: Runtime npm packages ``menu.js`` imports directly. A checkout that is
|
|
23
|
+
#: missing any of them cannot render a menu, so we check before spawning Node
|
|
24
|
+
#: rather than letting Node print a raw ``ERR_MODULE_NOT_FOUND`` stack trace.
|
|
25
|
+
NODE_UI_REQUIRED_PACKAGES = (
|
|
26
|
+
"chalk",
|
|
27
|
+
"figures",
|
|
28
|
+
"ink",
|
|
29
|
+
"ink-select-input",
|
|
30
|
+
"meow",
|
|
31
|
+
"react",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _node_ui_dir() -> Path:
|
|
36
|
+
"""Return the directory holding the bundled Node UI."""
|
|
37
|
+
here = Path(__file__).resolve()
|
|
38
|
+
return here.parent.parent / "node_ui"
|
|
39
|
+
|
|
20
40
|
|
|
21
41
|
def _node_script_path() -> Path:
|
|
22
42
|
"""Return path to the bundled Node UI script."""
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
return _node_ui_dir() / "menu.js"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _missing_node_ui_packages(node_ui_dir: Path | None = None) -> List[str]:
|
|
47
|
+
"""Return the required Node UI packages that are not installed.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
node_ui_dir: Directory to inspect. Defaults to the bundled ``node_ui``.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Names of missing packages, empty when every requirement is present.
|
|
54
|
+
"""
|
|
55
|
+
root = node_ui_dir if node_ui_dir is not None else _node_ui_dir()
|
|
56
|
+
node_modules = root / "node_modules"
|
|
57
|
+
if not node_modules.is_dir():
|
|
58
|
+
return list(NODE_UI_REQUIRED_PACKAGES)
|
|
59
|
+
return [
|
|
60
|
+
name
|
|
61
|
+
for name in NODE_UI_REQUIRED_PACKAGES
|
|
62
|
+
if not (node_modules / name).is_dir()
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _quote_path(path: Path) -> str:
|
|
67
|
+
"""Quote a path for the shell the user is most likely pasting into.
|
|
68
|
+
|
|
69
|
+
``shlex.quote`` emits POSIX single quotes, which ``cmd.exe`` treats as
|
|
70
|
+
literal characters, so a Windows path containing spaces would be split.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
path: Path to embed in a suggested command line.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The path, quoted only when it needs to be.
|
|
77
|
+
"""
|
|
78
|
+
text = str(path)
|
|
79
|
+
if os.name == "nt":
|
|
80
|
+
# cmd.exe splits on & | ^ < > as well as spaces, so always quote.
|
|
81
|
+
return f'"{text}"'
|
|
82
|
+
return shlex.quote(text)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _no_node_message() -> str:
|
|
86
|
+
"""Build the message shown when the Node runtime is unavailable."""
|
|
87
|
+
return (
|
|
88
|
+
"aichat could not start its interactive menu: 'node' was not found "
|
|
89
|
+
"on PATH.\nInstall Node.js (>=18) and try again."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _node_ui_setup_message(missing: List[str], node_ui_dir: Path) -> str:
|
|
94
|
+
"""Build an actionable message for a Node UI with missing dependencies."""
|
|
95
|
+
return (
|
|
96
|
+
"aichat could not start its interactive menu: the Node UI "
|
|
97
|
+
f"dependencies are missing ({', '.join(missing)}).\n"
|
|
98
|
+
f"Expected them in: {node_ui_dir / 'node_modules'}\n"
|
|
99
|
+
"\n"
|
|
100
|
+
"This happens when aichat runs from a source checkout (an editable\n"
|
|
101
|
+
"install) whose Node dependencies were never installed. Install them\n"
|
|
102
|
+
"with either of:\n"
|
|
103
|
+
f" npm ci --prefix {_quote_path(node_ui_dir)} --omit=dev\n"
|
|
104
|
+
" make install # from that checkout (needs GNU make)\n"
|
|
105
|
+
)
|
|
25
106
|
|
|
26
107
|
|
|
27
108
|
def _write_payload(
|
|
@@ -63,15 +144,33 @@ def _write_payload(
|
|
|
63
144
|
def _run_node(data_path: Path, out_path: Path, stderr_mode: bool = False) -> int:
|
|
64
145
|
"""Invoke the Node UI process.
|
|
65
146
|
|
|
66
|
-
Returns the process return code.
|
|
147
|
+
Returns the process return code. A missing Node runtime or missing Node UI
|
|
148
|
+
dependencies are reported as an actionable message and return code 1.
|
|
67
149
|
"""
|
|
150
|
+
# Check the runtime before its packages: on a machine without Node, telling
|
|
151
|
+
# the user to run npm is useless advice.
|
|
152
|
+
if shutil.which("node") is None:
|
|
153
|
+
print(_no_node_message(), file=sys.stderr)
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
node_ui_dir = _node_ui_dir()
|
|
157
|
+
missing = _missing_node_ui_packages(node_ui_dir)
|
|
158
|
+
if missing:
|
|
159
|
+
print(_node_ui_setup_message(missing, node_ui_dir), file=sys.stderr)
|
|
160
|
+
return 1
|
|
161
|
+
|
|
68
162
|
script = _node_script_path()
|
|
69
163
|
cmd = ["node", str(script), "--data", str(data_path), "--out", str(out_path)]
|
|
70
164
|
env = os.environ.copy()
|
|
71
165
|
if stderr_mode:
|
|
72
166
|
env["NODE_UI_STDERR"] = "1"
|
|
73
167
|
|
|
74
|
-
|
|
168
|
+
try:
|
|
169
|
+
proc = subprocess.run(cmd, env=env)
|
|
170
|
+
except FileNotFoundError:
|
|
171
|
+
# Node disappeared between the check above and the spawn.
|
|
172
|
+
print(_no_node_message(), file=sys.stderr)
|
|
173
|
+
return 1
|
|
75
174
|
return proc.returncode
|
|
76
175
|
|
|
77
176
|
|