claude-code-tools 1.16.1__tar.gz → 1.17.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.16.1 → claude_code_tools-1.17.0}/PKG-INFO +6 -5
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/README.md +5 -4
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/aichat.py +73 -1
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_models.py +16 -10
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/export_session.py +101 -49
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/find_claude_session.py +116 -36
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/find_codex_session.py +163 -113
- claude_code_tools-1.17.0/claude_code_tools/resolve_session.py +832 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/session_utils.py +76 -32
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/trim_session.py +6 -2
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli.py +5 -5
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_contract.py +2 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_rendering.py +31 -6
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_snapshots.py +21 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_validation.py +1 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/pyproject.toml +2 -2
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/.gitignore +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/LICENSE +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/config.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_cli.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_generation.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_process.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_reuse.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_worker.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/node_menu_ui.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/search_index.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/tmux_cli_controller.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/agent-tunnel-spec.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/ai-agent-writing-skills.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/cc-codex-instructions.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/claude-code-chutes.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/ensemble-ideas.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/example-with-images.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/linked-in-20260102.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/lmsh.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/local-llm-setup.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/msg-phase-a-plan.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/msg-plan-v2.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/paper-writing-style-transfer-tools.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/reddit-aichat-resume-v2.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/reddit-aichat-resume.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/reddit-aichat.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/reddit-post.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/rollover-details.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/hatch_build.py +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/node_ui/menu.js +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/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.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: Collection of tools for working with Claude Code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -115,10 +115,11 @@ codex-workflows show RUN_ID
|
|
|
115
115
|
codex-workflows --all
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
The default list contains only active workflows
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
The default list contains only active workflows and identifies the project
|
|
119
|
+
folder from which each workflow was launched. Use `show RUN_ID` for its full
|
|
120
|
+
working directory, or `--all` to include completed and diagnostic history. The
|
|
121
|
+
command reads durable workflow state and process metadata. It never changes a
|
|
122
|
+
run, sends a signal, repairs state, or launches an agent. See the
|
|
122
123
|
[codex-workflows reference][codex-workflows-reference] for filters, JSON output,
|
|
123
124
|
and the complete versioned schema.
|
|
124
125
|
|
|
@@ -89,10 +89,11 @@ codex-workflows show RUN_ID
|
|
|
89
89
|
codex-workflows --all
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
The default list contains only active workflows
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
The default list contains only active workflows and identifies the project
|
|
93
|
+
folder from which each workflow was launched. Use `show RUN_ID` for its full
|
|
94
|
+
working directory, or `--all` to include completed and diagnostic history. The
|
|
95
|
+
command reads durable workflow state and process metadata. It never changes a
|
|
96
|
+
run, sends a signal, repairs state, or launches an agent. See the
|
|
96
97
|
[codex-workflows reference][codex-workflows-reference] for filters, JSON output,
|
|
97
98
|
and the complete versioned schema.
|
|
98
99
|
|
|
@@ -30,6 +30,26 @@ class SessionIDGroup(click.Group):
|
|
|
30
30
|
return super().parse_args(ctx, args)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
class ResolveCommand(click.Command):
|
|
34
|
+
"""Click command that renders resolver usage errors without usage text."""
|
|
35
|
+
|
|
36
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
37
|
+
"""Parse arguments, converting Click usage failures to resolver errors."""
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
from claude_code_tools.resolve_session import _render_error
|
|
41
|
+
|
|
42
|
+
raw_args = tuple(args)
|
|
43
|
+
try:
|
|
44
|
+
return super().parse_args(ctx, args)
|
|
45
|
+
except click.UsageError as error:
|
|
46
|
+
pretty = "--pretty" in raw_args or (
|
|
47
|
+
"--json" not in raw_args and sys.stdout.isatty()
|
|
48
|
+
)
|
|
49
|
+
_render_error("invalid_input", error.format_message(), pretty)
|
|
50
|
+
ctx.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
@click.group(cls=SessionIDGroup, invoke_without_command=True)
|
|
34
54
|
@click.version_option()
|
|
35
55
|
@click.option(
|
|
@@ -96,7 +116,11 @@ def main(ctx, claude_home, codex_home):
|
|
|
96
116
|
# Skip for build-index/clear-index to avoid double-indexing or state conflicts
|
|
97
117
|
# In JSON mode (-j/--json), suppress all output for clean parsing
|
|
98
118
|
skip_auto_index_cmds = ['build-index', 'clear-index', 'index-stats']
|
|
99
|
-
should_skip =
|
|
119
|
+
should_skip = (
|
|
120
|
+
ctx.invoked_subcommand == 'resolve'
|
|
121
|
+
or ctx.invoked_subcommand in skip_auto_index_cmds
|
|
122
|
+
or any(cmd in sys.argv for cmd in skip_auto_index_cmds)
|
|
123
|
+
)
|
|
100
124
|
json_mode = any(arg in sys.argv for arg in ['-j', '--json'])
|
|
101
125
|
if not should_skip:
|
|
102
126
|
try:
|
|
@@ -128,6 +152,54 @@ def main(ctx, claude_home, codex_home):
|
|
|
128
152
|
)
|
|
129
153
|
|
|
130
154
|
|
|
155
|
+
@main.command("resolve", cls=ResolveCommand)
|
|
156
|
+
@click.argument("query")
|
|
157
|
+
@click.option(
|
|
158
|
+
"--agent",
|
|
159
|
+
type=str,
|
|
160
|
+
metavar="claude|codex",
|
|
161
|
+
default="claude",
|
|
162
|
+
show_default=True,
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--home",
|
|
166
|
+
type=click.Path(readable=False),
|
|
167
|
+
help="Claude or Codex home directory to search.",
|
|
168
|
+
)
|
|
169
|
+
@click.option("--json", "json_output", is_flag=True, help="Force JSON output.")
|
|
170
|
+
@click.option(
|
|
171
|
+
"--pretty",
|
|
172
|
+
"pretty_output",
|
|
173
|
+
is_flag=True,
|
|
174
|
+
help="Force human-readable output.",
|
|
175
|
+
)
|
|
176
|
+
@click.pass_context
|
|
177
|
+
def resolve_session_cmd(
|
|
178
|
+
ctx: click.Context,
|
|
179
|
+
query: str,
|
|
180
|
+
agent: str,
|
|
181
|
+
home: str | None,
|
|
182
|
+
json_output: bool,
|
|
183
|
+
pretty_output: bool,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Resolve a name, full session ID, or partial session ID."""
|
|
186
|
+
import sys
|
|
187
|
+
|
|
188
|
+
from claude_code_tools.resolve_session import run
|
|
189
|
+
|
|
190
|
+
if json_output and pretty_output:
|
|
191
|
+
click.echo(
|
|
192
|
+
'{"error":"invalid_format",'
|
|
193
|
+
'"detail":"Choose only one of --json or --pretty."}'
|
|
194
|
+
)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
if home is None:
|
|
197
|
+
home_key = "codex_home" if agent.casefold() == "codex" else "claude_home"
|
|
198
|
+
home = ctx.ensure_object(dict).get(home_key)
|
|
199
|
+
fmt = "json" if json_output else "pretty" if pretty_output else "auto"
|
|
200
|
+
sys.exit(run(query, agent.lower(), home, fmt))
|
|
201
|
+
|
|
202
|
+
|
|
131
203
|
# Shared help text for find commands
|
|
132
204
|
_FIND_OPTIONS_COMMON = """
|
|
133
205
|
Options:
|
{claude_code_tools-1.16.1 → claude_code_tools-1.17.0}/claude_code_tools/codex_server_models.py
RENAMED
|
@@ -444,7 +444,7 @@ def clear_current_generation(base: ServerPaths) -> None:
|
|
|
444
444
|
|
|
445
445
|
|
|
446
446
|
def all_server_paths(base: ServerPaths) -> list[ServerPaths]:
|
|
447
|
-
"""Return bounded helper generations
|
|
447
|
+
"""Return bounded helper generations retaining state or a socket."""
|
|
448
448
|
info = _lstat(base.runtime_dir)
|
|
449
449
|
if info is None:
|
|
450
450
|
return [base]
|
|
@@ -474,12 +474,12 @@ def all_server_paths(base: ServerPaths) -> list[ServerPaths]:
|
|
|
474
474
|
raise CodexServerError("app-server runtime changed during inspection")
|
|
475
475
|
with os.scandir(directory_fd) as entries:
|
|
476
476
|
for entry in entries:
|
|
477
|
-
scanned += 1
|
|
478
|
-
if scanned > MAX_SERVER_DIRECTORY_ENTRIES:
|
|
479
|
-
raise CodexServerError("too many app-server runtime entries")
|
|
480
477
|
try:
|
|
481
478
|
generation = validate_generation(entry.name)
|
|
482
479
|
except ValueError:
|
|
480
|
+
scanned += 1
|
|
481
|
+
if scanned > MAX_SERVER_DIRECTORY_ENTRIES:
|
|
482
|
+
raise CodexServerError("too many app-server runtime entries")
|
|
483
483
|
continue
|
|
484
484
|
try:
|
|
485
485
|
entry_info = entry.stat(follow_symlinks=False)
|
|
@@ -497,6 +497,15 @@ def all_server_paths(base: ServerPaths) -> list[ServerPaths]:
|
|
|
497
497
|
raise CodexServerError(
|
|
498
498
|
f"app-server generation is owned by another user: {entry.name}"
|
|
499
499
|
)
|
|
500
|
+
paths = paths_for_generation(base, generation)
|
|
501
|
+
if (
|
|
502
|
+
_lstat(paths.state_path) is None
|
|
503
|
+
and _lstat(paths.socket_path) is None
|
|
504
|
+
):
|
|
505
|
+
continue
|
|
506
|
+
scanned += 1
|
|
507
|
+
if scanned > MAX_SERVER_DIRECTORY_ENTRIES:
|
|
508
|
+
raise CodexServerError("too many app-server runtime entries")
|
|
500
509
|
generations.append(generation)
|
|
501
510
|
if len(generations) > MAX_SERVER_GENERATION_DIRECTORIES:
|
|
502
511
|
raise CodexServerError(
|
|
@@ -510,13 +519,10 @@ def all_server_paths(base: ServerPaths) -> list[ServerPaths]:
|
|
|
510
519
|
def require_generation_capacity(base: ServerPaths, generation: str) -> None:
|
|
511
520
|
"""Bound retained servers before creating another generation."""
|
|
512
521
|
target = paths_for_generation(base, generation)
|
|
513
|
-
|
|
522
|
+
retained = all_server_paths(base)[1:]
|
|
523
|
+
if any(paths.generation == target.generation for paths in retained):
|
|
514
524
|
return
|
|
515
|
-
retained
|
|
516
|
-
_lstat(paths.state_path) is not None or _lstat(paths.socket_path) is not None
|
|
517
|
-
for paths in all_server_paths(base)[1:]
|
|
518
|
-
)
|
|
519
|
-
if retained >= MAX_SERVER_GENERATIONS:
|
|
525
|
+
if len(retained) >= MAX_SERVER_GENERATIONS:
|
|
520
526
|
raise CodexServerError(
|
|
521
527
|
"app-server generation limit reached; exit callback-ready sessions "
|
|
522
528
|
"and run `codex-server stop --force` before launching another"
|
|
@@ -97,7 +97,10 @@ def _get_last_line_timestamp(file_path: Path) -> Optional[str]:
|
|
|
97
97
|
|
|
98
98
|
# Parse JSON and extract timestamp
|
|
99
99
|
data = json.loads(last_line)
|
|
100
|
-
|
|
100
|
+
if not isinstance(data, dict):
|
|
101
|
+
return None
|
|
102
|
+
timestamp = data.get("timestamp")
|
|
103
|
+
return timestamp if isinstance(timestamp, str) and timestamp else None
|
|
101
104
|
|
|
102
105
|
except (OSError, IOError, json.JSONDecodeError, UnicodeDecodeError):
|
|
103
106
|
return None
|
|
@@ -114,6 +117,8 @@ def _extract_claude_message_text(data: dict) -> Optional[str]:
|
|
|
114
117
|
Extracted text or None if not a text message
|
|
115
118
|
"""
|
|
116
119
|
message = data.get("message", {})
|
|
120
|
+
if not isinstance(message, dict):
|
|
121
|
+
return None
|
|
117
122
|
content = message.get("content")
|
|
118
123
|
|
|
119
124
|
if not content:
|
|
@@ -129,8 +134,9 @@ def _extract_claude_message_text(data: dict) -> Optional[str]:
|
|
|
129
134
|
if isinstance(block, str) and block.strip():
|
|
130
135
|
return block.strip()
|
|
131
136
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
132
|
-
text = block.get("text"
|
|
133
|
-
if text:
|
|
137
|
+
text = block.get("text")
|
|
138
|
+
if isinstance(text, str) and text.strip():
|
|
139
|
+
text = text.strip()
|
|
134
140
|
return text
|
|
135
141
|
|
|
136
142
|
return None
|
|
@@ -147,6 +153,8 @@ def _extract_codex_message_text(data: dict) -> Optional[str]:
|
|
|
147
153
|
Extracted text or None if not a text message
|
|
148
154
|
"""
|
|
149
155
|
payload = data.get("payload", {})
|
|
156
|
+
if not isinstance(payload, dict):
|
|
157
|
+
return None
|
|
150
158
|
if payload.get("type") != "message":
|
|
151
159
|
return None
|
|
152
160
|
|
|
@@ -160,9 +168,9 @@ def _extract_codex_message_text(data: dict) -> Optional[str]:
|
|
|
160
168
|
block_type = block.get("type")
|
|
161
169
|
# Both input_text and output_text have text field
|
|
162
170
|
if block_type in ("input_text", "output_text"):
|
|
163
|
-
text = block.get("text"
|
|
164
|
-
if text:
|
|
165
|
-
return text
|
|
171
|
+
text = block.get("text")
|
|
172
|
+
if isinstance(text, str) and text.strip():
|
|
173
|
+
return text.strip()
|
|
166
174
|
|
|
167
175
|
return None
|
|
168
176
|
|
|
@@ -236,6 +244,8 @@ def extract_first_last_messages(
|
|
|
236
244
|
data = json.loads(line)
|
|
237
245
|
except json.JSONDecodeError:
|
|
238
246
|
continue
|
|
247
|
+
if not isinstance(data, dict):
|
|
248
|
+
continue
|
|
239
249
|
|
|
240
250
|
role: Optional[str] = None
|
|
241
251
|
text: Optional[str] = None
|
|
@@ -249,10 +259,17 @@ def extract_first_last_messages(
|
|
|
249
259
|
elif agent == "codex":
|
|
250
260
|
if data.get("type") == "response_item":
|
|
251
261
|
payload = data.get("payload", {})
|
|
252
|
-
if
|
|
253
|
-
|
|
262
|
+
if (
|
|
263
|
+
isinstance(payload, dict)
|
|
264
|
+
and payload.get("type") == "message"
|
|
265
|
+
):
|
|
266
|
+
payload_role = payload.get("role")
|
|
267
|
+
if isinstance(payload_role, str):
|
|
268
|
+
role = payload_role
|
|
254
269
|
text = _extract_codex_message_text(data)
|
|
255
|
-
|
|
270
|
+
raw_timestamp = data.get("timestamp")
|
|
271
|
+
if isinstance(raw_timestamp, str):
|
|
272
|
+
timestamp = raw_timestamp
|
|
256
273
|
|
|
257
274
|
if role and text:
|
|
258
275
|
msg_dict = {
|
|
@@ -334,7 +351,7 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
334
351
|
|
|
335
352
|
try:
|
|
336
353
|
with open(session_file, "r", encoding="utf-8") as f:
|
|
337
|
-
for
|
|
354
|
+
for line in f:
|
|
338
355
|
line = line.strip()
|
|
339
356
|
if not line:
|
|
340
357
|
continue
|
|
@@ -343,40 +360,61 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
343
360
|
data = json.loads(line)
|
|
344
361
|
except json.JSONDecodeError:
|
|
345
362
|
continue
|
|
363
|
+
if not isinstance(data, dict):
|
|
364
|
+
continue
|
|
346
365
|
|
|
347
|
-
# Extract cwd (first line
|
|
348
|
-
|
|
349
|
-
|
|
366
|
+
# Extract cwd (first line with a non-empty string value)
|
|
367
|
+
cwd = data.get("cwd")
|
|
368
|
+
if (
|
|
369
|
+
metadata["cwd"] is None
|
|
370
|
+
and isinstance(cwd, str)
|
|
371
|
+
and bool(cwd.strip())
|
|
372
|
+
):
|
|
373
|
+
metadata["cwd"] = cwd
|
|
350
374
|
|
|
351
375
|
# Extract git branch (first line that has it)
|
|
352
|
-
|
|
353
|
-
|
|
376
|
+
git_branch = data.get("gitBranch")
|
|
377
|
+
if (
|
|
378
|
+
metadata["branch"] is None
|
|
379
|
+
and isinstance(git_branch, str)
|
|
380
|
+
and bool(git_branch.strip())
|
|
381
|
+
):
|
|
382
|
+
metadata["branch"] = git_branch
|
|
354
383
|
|
|
355
384
|
# Extract session ID from sessionId field if available
|
|
356
|
-
|
|
357
|
-
|
|
385
|
+
session_id = data.get("sessionId")
|
|
386
|
+
if isinstance(session_id, str) and session_id.strip():
|
|
387
|
+
metadata["session_id"] = session_id
|
|
358
388
|
|
|
359
389
|
# Extract trim_metadata (for trimmed sessions)
|
|
360
|
-
|
|
361
|
-
|
|
390
|
+
tm = data.get("trim_metadata")
|
|
391
|
+
if isinstance(tm, dict):
|
|
362
392
|
metadata["derivation_type"] = "trimmed"
|
|
363
|
-
|
|
364
|
-
if
|
|
365
|
-
|
|
393
|
+
parent_file = tm.get("parent_file")
|
|
394
|
+
if isinstance(parent_file, str) and parent_file.strip():
|
|
395
|
+
metadata["parent_session_file"] = parent_file
|
|
396
|
+
parent_path = Path(parent_file)
|
|
366
397
|
metadata["parent_session_id"] = parent_path.stem
|
|
367
|
-
|
|
368
|
-
|
|
398
|
+
stats = tm.get("stats")
|
|
399
|
+
if isinstance(stats, dict):
|
|
400
|
+
metadata["trim_stats"] = stats
|
|
369
401
|
|
|
370
402
|
# Extract continue_metadata (for continued sessions)
|
|
371
|
-
|
|
372
|
-
|
|
403
|
+
cm = data.get("continue_metadata")
|
|
404
|
+
if isinstance(cm, dict):
|
|
373
405
|
metadata["derivation_type"] = "continued"
|
|
374
|
-
|
|
375
|
-
|
|
406
|
+
parent_id = cm.get("parent_session_id")
|
|
407
|
+
if isinstance(parent_id, str) and parent_id.strip():
|
|
408
|
+
metadata["parent_session_id"] = parent_id
|
|
409
|
+
parent_file = cm.get("parent_session_file")
|
|
410
|
+
if isinstance(parent_file, str) and parent_file.strip():
|
|
411
|
+
metadata["parent_session_file"] = parent_file
|
|
376
412
|
|
|
377
413
|
# Extract sessionType (e.g., "helper" for SDK/headless sessions)
|
|
378
414
|
if "sessionType" in data and metadata["session_type"] is None:
|
|
379
|
-
|
|
415
|
+
session_type = data.get("sessionType")
|
|
416
|
+
if isinstance(session_type, str) and session_type.strip():
|
|
417
|
+
metadata["session_type"] = session_type
|
|
380
418
|
|
|
381
419
|
# Extract git branch for Claude from file-history-snapshot metadata
|
|
382
420
|
if (
|
|
@@ -384,33 +422,47 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
384
422
|
and metadata["branch"] is None
|
|
385
423
|
and data.get("type") == "file-history-snapshot"
|
|
386
424
|
):
|
|
387
|
-
|
|
388
|
-
if
|
|
389
|
-
|
|
425
|
+
nested_metadata = data.get("metadata")
|
|
426
|
+
if isinstance(nested_metadata, dict):
|
|
427
|
+
git_info = nested_metadata.get("git")
|
|
428
|
+
if isinstance(git_info, dict):
|
|
429
|
+
branch = git_info.get("branch")
|
|
430
|
+
if isinstance(branch, str) and branch.strip():
|
|
431
|
+
metadata["branch"] = branch
|
|
390
432
|
|
|
391
433
|
# Extract git branch for Codex sessions from session_meta
|
|
392
434
|
if agent == "codex" and data.get("type") == "session_meta":
|
|
393
|
-
payload = data.get("payload"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
435
|
+
payload = data.get("payload")
|
|
436
|
+
if isinstance(payload, dict):
|
|
437
|
+
git_info = payload.get("git")
|
|
438
|
+
if isinstance(git_info, dict):
|
|
439
|
+
branch = git_info.get("branch")
|
|
440
|
+
if isinstance(branch, str) and branch.strip():
|
|
441
|
+
metadata["branch"] = branch
|
|
442
|
+
payload_cwd = payload.get("cwd")
|
|
443
|
+
if (
|
|
444
|
+
isinstance(payload_cwd, str)
|
|
445
|
+
and payload_cwd.strip()
|
|
446
|
+
):
|
|
447
|
+
metadata["cwd"] = payload_cwd
|
|
448
|
+
payload_id = payload.get("id")
|
|
449
|
+
if isinstance(payload_id, str) and payload_id.strip():
|
|
450
|
+
metadata["session_id"] = payload_id
|
|
403
451
|
|
|
404
452
|
# Extract session start timestamp from first entry with timestamp
|
|
405
|
-
|
|
406
|
-
|
|
453
|
+
timestamp = data.get("timestamp")
|
|
454
|
+
if (
|
|
455
|
+
session_start_timestamp is None
|
|
456
|
+
and isinstance(timestamp, str)
|
|
457
|
+
and timestamp.strip()
|
|
458
|
+
):
|
|
459
|
+
session_start_timestamp = timestamp
|
|
407
460
|
|
|
408
|
-
# Stop once we have the essential metadata (cwd and branch)
|
|
409
|
-
|
|
410
|
-
if (metadata["cwd"] and metadata["branch"]) or line_num >= 500:
|
|
461
|
+
# Stop once we have the essential metadata (cwd and branch).
|
|
462
|
+
if metadata["cwd"] and metadata["branch"]:
|
|
411
463
|
break
|
|
412
464
|
|
|
413
|
-
except (OSError, IOError):
|
|
465
|
+
except (OSError, IOError, UnicodeError):
|
|
414
466
|
pass
|
|
415
467
|
|
|
416
468
|
# Note: customTitle extraction is done in search_index.py's _extract_session_content
|
|
@@ -454,7 +506,7 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
454
506
|
try:
|
|
455
507
|
with open(session_file, "r", encoding="utf-8") as f:
|
|
456
508
|
metadata["lines"] = sum(1 for _ in f)
|
|
457
|
-
except (OSError, IOError):
|
|
509
|
+
except (OSError, IOError, UnicodeError):
|
|
458
510
|
metadata["lines"] = 0
|
|
459
511
|
|
|
460
512
|
# Derive project name from cwd
|