claude-code-tools 1.29.0__tar.gz → 1.29.2__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.29.0 → claude_code_tools-1.29.2}/PKG-INFO +1 -1
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/config.py +8 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/mattermost_bot.py +100 -11
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/tmux_cli_controller.py +2 -5
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/tmux_remote_controller.py +166 -49
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/agent-tunnel-spec.md +8 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/tmux-cli-instructions.md +29 -14
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/pyproject.toml +2 -2
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/.gitignore +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/LICENSE +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/README.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/http_frontend.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/readonly.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/relay.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/serve.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/turn_log.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/aichat.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/__init__.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/__main__.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/activity.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/cache.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/cli.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/detect.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/filters.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/model.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/render.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/amux/scan.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/cli_passthrough.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_app_server_rpc.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_callback.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_cli.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_generation.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_legacy.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_models.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_process.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_reservation.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_resume.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_retry.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_reuse.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_state.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_wait.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_worker.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/export_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_codex_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/github_wake.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/github_watch_daemon.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/github_watch_store.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/helper_sessions.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/issue_reply_delivery.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/legacy_session_wrappers.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/move_account.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/migrations.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/node_menu_ui.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_claude_noise.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_claude_to_codex.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_codex_flatten.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_codex_to_claude.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_render.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_service.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/resolve_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/resolve_session_names.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/resolve_session_render.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/search_index.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_cli_resolution.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_resolution.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_selection.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_utils.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/trim_session.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_contract.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_rendering.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_snapshots.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_validation.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/lmsh.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/local-llm-setup.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/hatch_build.py +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/node_ui/menu.js +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/node_ui/package.json +0 -0
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/config.py
RENAMED
|
@@ -104,6 +104,10 @@ class MattermostConfig:
|
|
|
104
104
|
allowed_user_ids: list[str] = field(default_factory=list)
|
|
105
105
|
# false only for a self-signed test server.
|
|
106
106
|
verify_tls: bool = True
|
|
107
|
+
# Handle answered when a teammate just @-mentions the bot without naming
|
|
108
|
+
# a handle ("@bot how does X work?"). Empty = use the only shared session
|
|
109
|
+
# when exactly one is live, else ask which.
|
|
110
|
+
default_handle: str = ""
|
|
107
111
|
|
|
108
112
|
|
|
109
113
|
@dataclass
|
|
@@ -385,6 +389,10 @@ respond_to_dms = false
|
|
|
385
389
|
# channel_ids = [] # 26-char channel ids
|
|
386
390
|
# allowed_user_ids = [] # empty = anyone in the watched channels
|
|
387
391
|
# verify_tls = true
|
|
392
|
+
# Handle answered when someone just @-mentions the bot without naming one
|
|
393
|
+
# ("@bot how does X work?"). Unset = the only shared session when exactly
|
|
394
|
+
# one is live, else the bot asks which.
|
|
395
|
+
# default_handle = ""
|
|
388
396
|
|
|
389
397
|
[claude]
|
|
390
398
|
binary = "claude"
|
|
@@ -141,11 +141,23 @@ def parse_posted(event: dict[str, Any]) -> Optional[MMPost]:
|
|
|
141
141
|
)
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
def resolve_default_handle(cfg: TunnelConfig, live: Sequence[str]) -> str:
|
|
145
|
+
"""Handle to answer for a bare ``@bot`` mention.
|
|
146
|
+
|
|
147
|
+
The configured ``default_handle`` wins. With none set, a single shared
|
|
148
|
+
session is unambiguous; with several, the bot asks which one.
|
|
149
|
+
"""
|
|
150
|
+
if cfg.mattermost.default_handle:
|
|
151
|
+
return cfg.mattermost.default_handle
|
|
152
|
+
return live[0] if len(live) == 1 else ""
|
|
153
|
+
|
|
154
|
+
|
|
144
155
|
@dataclass
|
|
145
156
|
class Route:
|
|
146
157
|
"""What the bot should do with one post."""
|
|
147
158
|
|
|
148
|
-
|
|
159
|
+
# ignore | list | open | unknown_handle | which_handle | followup | close
|
|
160
|
+
action: str
|
|
149
161
|
thread_key: str = ""
|
|
150
162
|
root_id: str = ""
|
|
151
163
|
handle: str = ""
|
|
@@ -161,6 +173,7 @@ def route_post(
|
|
|
161
173
|
allowed_user_ids: Sequence[str],
|
|
162
174
|
is_bound: Callable[[str], bool],
|
|
163
175
|
handle_live: Callable[[str], bool],
|
|
176
|
+
default_handle: str = "",
|
|
164
177
|
) -> Route:
|
|
165
178
|
"""Decide what to do with a post (pure; no I/O).
|
|
166
179
|
|
|
@@ -172,6 +185,8 @@ def route_post(
|
|
|
172
185
|
allowed_user_ids: If non-empty, only these users are answered.
|
|
173
186
|
is_bound: Whether a thread key is bound to a session.
|
|
174
187
|
handle_live: Whether a handle is currently shared.
|
|
188
|
+
default_handle: Handle to use when the bot is @-mentioned without
|
|
189
|
+
one (see ``resolve_default_handle``); "" = ask which.
|
|
175
190
|
|
|
176
191
|
Returns:
|
|
177
192
|
The routing decision.
|
|
@@ -194,7 +209,18 @@ def route_post(
|
|
|
194
209
|
if post.root_id:
|
|
195
210
|
key = f"mm:{post.root_id}"
|
|
196
211
|
if not is_bound(key):
|
|
197
|
-
|
|
212
|
+
# Not our thread yet. Teammates still address the bot inside one
|
|
213
|
+
# (e.g. answering its "which session?" question), so an explicit
|
|
214
|
+
# handle or @bot mention opens a binding on that root; anything
|
|
215
|
+
# else stays ignored.
|
|
216
|
+
return _open_route(
|
|
217
|
+
text,
|
|
218
|
+
post_id=post.root_id,
|
|
219
|
+
bot_username=bot_username,
|
|
220
|
+
handle_live=handle_live,
|
|
221
|
+
default_handle=default_handle,
|
|
222
|
+
complain_unknown=False,
|
|
223
|
+
)
|
|
198
224
|
# A reply that opens with @someone-else (or @all/@channel/@here) is
|
|
199
225
|
# teammates talking among themselves. A leading @bot is stripped.
|
|
200
226
|
who = leading_mention(text)
|
|
@@ -208,21 +234,74 @@ def route_post(
|
|
|
208
234
|
return Route("close", thread_key=key, root_id=post.root_id)
|
|
209
235
|
return Route("followup", thread_key=key, root_id=post.root_id, text=text)
|
|
210
236
|
|
|
237
|
+
return _open_route(
|
|
238
|
+
text,
|
|
239
|
+
post_id=post.id,
|
|
240
|
+
bot_username=bot_username,
|
|
241
|
+
handle_live=handle_live,
|
|
242
|
+
default_handle=default_handle,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _open_route(
|
|
247
|
+
text: str,
|
|
248
|
+
*,
|
|
249
|
+
post_id: str,
|
|
250
|
+
bot_username: str,
|
|
251
|
+
handle_live: Callable[[str], bool],
|
|
252
|
+
default_handle: str,
|
|
253
|
+
complain_unknown: bool = True,
|
|
254
|
+
) -> Route:
|
|
255
|
+
"""Route a message that may open a thread on ``post_id`` (pure).
|
|
256
|
+
|
|
257
|
+
Used for a root post and for a message inside a thread the bot has not
|
|
258
|
+
bound yet: both are "someone addressing the bot in the channel".
|
|
259
|
+
``complain_unknown`` is off in the latter case, so ordinary chatter
|
|
260
|
+
there stays unanswered.
|
|
261
|
+
"""
|
|
262
|
+
ignore = Route("ignore")
|
|
263
|
+
# An @bot mention is how a teammate naturally addresses the bot. Strip
|
|
264
|
+
# it; whatever follows is the question (a handle may still lead it).
|
|
265
|
+
mentioned = False
|
|
266
|
+
who = leading_mention(text)
|
|
267
|
+
if who is not None:
|
|
268
|
+
if who in BROADCAST_MENTIONS or who != bot_username.lower():
|
|
269
|
+
return ignore # teammates talking to each other
|
|
270
|
+
mentioned = True
|
|
271
|
+
text = _MENTION_RE.sub("", text.lstrip(), count=1).strip()
|
|
272
|
+
|
|
211
273
|
if is_list_command(text):
|
|
212
|
-
return Route("list", root_id=
|
|
213
|
-
|
|
214
|
-
handle
|
|
274
|
+
return Route("list", root_id=post_id)
|
|
275
|
+
# Split on ANY whitespace: a handle followed by a newline is still a
|
|
276
|
+
# handle, and must not fall through to the default.
|
|
277
|
+
parts = text.split(None, 1)
|
|
278
|
+
handle = parts[0].strip().lower() if parts else ""
|
|
279
|
+
remainder = parts[1].strip() if len(parts) > 1 else ""
|
|
215
280
|
if handle_live(handle):
|
|
216
281
|
return Route(
|
|
217
282
|
"open",
|
|
218
|
-
thread_key=f"mm:{
|
|
219
|
-
root_id=
|
|
283
|
+
thread_key=f"mm:{post_id}",
|
|
284
|
+
root_id=post_id,
|
|
220
285
|
handle=handle,
|
|
221
|
-
text=remainder
|
|
286
|
+
text=remainder,
|
|
222
287
|
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
288
|
+
if mentioned:
|
|
289
|
+
# Addressed to the bot but no handle named: answer the default one,
|
|
290
|
+
# but only while it is actually shared.
|
|
291
|
+
if not default_handle or not handle_live(default_handle):
|
|
292
|
+
return Route("which_handle", root_id=post_id)
|
|
293
|
+
return Route(
|
|
294
|
+
"open",
|
|
295
|
+
thread_key=f"mm:{post_id}",
|
|
296
|
+
root_id=post_id,
|
|
297
|
+
handle=default_handle,
|
|
298
|
+
text=text,
|
|
299
|
+
)
|
|
300
|
+
# Only complain if it clearly looks like a handle attempt — and never
|
|
301
|
+
# inside someone else's thread, where a one-word reply ("thanks") would
|
|
302
|
+
# otherwise draw a "no live session" answer.
|
|
303
|
+
if complain_unknown and HANDLE_RE.match(handle) and not remainder:
|
|
304
|
+
return Route("unknown_handle", root_id=post_id, handle=handle)
|
|
226
305
|
return ignore
|
|
227
306
|
|
|
228
307
|
|
|
@@ -553,6 +632,9 @@ class _Router:
|
|
|
553
632
|
allowed_user_ids=self.cfg.mattermost.allowed_user_ids,
|
|
554
633
|
is_bound=lambda key: relay.store.get(key) is not None,
|
|
555
634
|
handle_live=lambda h: self.registry.get(h) is not None,
|
|
635
|
+
default_handle=resolve_default_handle(
|
|
636
|
+
self.cfg, [r.handle for r in self.registry.active()]
|
|
637
|
+
),
|
|
556
638
|
)
|
|
557
639
|
if route.action == "ignore":
|
|
558
640
|
return
|
|
@@ -560,6 +642,13 @@ class _Router:
|
|
|
560
642
|
sender = post.sender or "A teammate"
|
|
561
643
|
if route.action == "list":
|
|
562
644
|
await dest.send(relay.handles_text())
|
|
645
|
+
elif route.action == "which_handle":
|
|
646
|
+
live = self.registry.active()
|
|
647
|
+
example = live[0].handle if live else "handle"
|
|
648
|
+
await dest.send(
|
|
649
|
+
"Which session should I ask? Put its handle first, e.g. "
|
|
650
|
+
f"`{example} your question`.\n\n" + relay.handles_text()
|
|
651
|
+
)
|
|
563
652
|
elif route.action == "unknown_handle":
|
|
564
653
|
await dest.send(
|
|
565
654
|
f"No live session for handle `{route.handle}`. "
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/tmux_cli_controller.py
RENAMED
|
@@ -914,11 +914,8 @@ class CLI:
|
|
|
914
914
|
print(str(e))
|
|
915
915
|
else:
|
|
916
916
|
# Remote mode - kill window
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
print("Window killed")
|
|
920
|
-
except ValueError as e:
|
|
921
|
-
print(str(e))
|
|
917
|
+
self.controller.kill_window(window_id=pane)
|
|
918
|
+
print("Window killed")
|
|
922
919
|
|
|
923
920
|
def wait_idle(self, pane: Optional[str] = None, idle_time: float = 2.0,
|
|
924
921
|
timeout: Optional[int] = None):
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/tmux_remote_controller.py
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Remote Tmux Controller
|
|
4
4
|
|
|
5
5
|
Enables tmux-cli to work when run outside of tmux by:
|
|
6
|
-
-
|
|
6
|
+
- Creating a detached tmux session when launching a managed window
|
|
7
7
|
- Managing commands in separate tmux windows (not panes)
|
|
8
8
|
- Providing an API compatible with the local (pane) controller
|
|
9
9
|
"""
|
|
@@ -16,65 +16,141 @@ from typing import Optional, List, Dict, Tuple, Union, Any
|
|
|
16
16
|
|
|
17
17
|
class RemoteTmuxController:
|
|
18
18
|
"""Remote controller that manages a dedicated tmux session and windows."""
|
|
19
|
+
|
|
20
|
+
_ready_option = '@tmux_cli_ready'
|
|
19
21
|
|
|
20
22
|
def __init__(self, session_name: str = "remote-cli-session"):
|
|
21
|
-
"""Initialize with session name
|
|
23
|
+
"""Initialize with a managed session name without creating it."""
|
|
22
24
|
self.session_name = session_name
|
|
23
|
-
self.target_window: Optional[str] = None # e.g., "
|
|
25
|
+
self.target_window: Optional[str] = None # Stable window ID, e.g., "@1"
|
|
24
26
|
print(f"Note: tmux-cli is running outside tmux. Managing windows in session '{session_name}'.")
|
|
25
27
|
print("For better integration, consider running from inside a tmux session.")
|
|
26
28
|
print("Use 'tmux-cli attach' to view the remote session.")
|
|
27
|
-
self._ensure_session()
|
|
28
29
|
|
|
29
30
|
# ----------------------------
|
|
30
31
|
# Internal utilities
|
|
31
32
|
# ----------------------------
|
|
32
|
-
def _run_tmux(self, args: List[str]) -> Tuple[str, int]:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
def _run_tmux(self, args: List[str], include_stderr: bool = False) -> Tuple[str, int]:
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
['tmux'] + args,
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True
|
|
39
|
+
)
|
|
40
|
+
except OSError as exc:
|
|
41
|
+
raise RuntimeError(f"Could not run tmux {args[0]}: {exc}") from exc
|
|
42
|
+
output = result.stdout.strip()
|
|
43
|
+
if include_stderr and result.returncode != 0:
|
|
44
|
+
output = "\n".join(part for part in (output, result.stderr.strip()) if part)
|
|
45
|
+
return output, result.returncode
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _session_is_missing(error: str) -> bool:
|
|
49
|
+
"""Recognize tmux's absent-session and absent-server diagnostics only."""
|
|
50
|
+
return (
|
|
51
|
+
"can't find session:" in error
|
|
52
|
+
or error.startswith("no server running on ")
|
|
53
|
+
or (error.startswith("error connecting to ") and "(No such file or directory)" in error)
|
|
37
54
|
)
|
|
38
|
-
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _tmux_failure(action: str, output: str, code: int) -> RuntimeError:
|
|
58
|
+
detail = f": {output}" if output else ""
|
|
59
|
+
return RuntimeError(f"tmux {action} failed (exit {code}){detail}")
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _window_is_missing(error: str, window_id: str) -> bool:
|
|
63
|
+
"""Only classify a diagnostic for this exact stable ID as a vanished window."""
|
|
64
|
+
return error.strip() in (
|
|
65
|
+
f"no such window: {window_id}",
|
|
66
|
+
f"can't find window: {window_id}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _managed_session_exists(self) -> bool:
|
|
70
|
+
output, code = self._run_tmux(['has-session', '-t', f'={self.session_name}'], include_stderr=True)
|
|
71
|
+
if code == 0:
|
|
72
|
+
return True
|
|
73
|
+
if self._session_is_missing(output):
|
|
74
|
+
return False
|
|
75
|
+
raise self._tmux_failure('has-session', output, code)
|
|
39
76
|
|
|
40
77
|
def _ensure_session(self) -> None:
|
|
41
|
-
"""Create the session if
|
|
42
|
-
|
|
43
|
-
if code != 0:
|
|
78
|
+
"""Create the managed session if needed for a new window."""
|
|
79
|
+
if not self._managed_session_exists():
|
|
44
80
|
# Create a detached session using user's default shell
|
|
45
81
|
# Return the session name just to force creation
|
|
46
|
-
self._run_tmux([
|
|
82
|
+
output, code = self._run_tmux([
|
|
47
83
|
'new-session', '-d', '-s', self.session_name, '-P', '-F', '#{session_name}'
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if code2 == 0 and win:
|
|
56
|
-
self.target_window = win
|
|
84
|
+
], include_stderr=True)
|
|
85
|
+
if code != 0:
|
|
86
|
+
# Another launcher may have won the check/create race.
|
|
87
|
+
if output != f'duplicate session: {self.session_name}' or not self._managed_session_exists():
|
|
88
|
+
raise self._tmux_failure('new-session', output, code)
|
|
89
|
+
elif not output:
|
|
90
|
+
raise self._tmux_failure('new-session', output, code)
|
|
57
91
|
|
|
58
92
|
def _window_target(self, pane: Optional[str]) -> str:
|
|
59
93
|
"""Resolve user-provided pane/window hint to a tmux target.
|
|
60
94
|
Accepts:
|
|
61
|
-
- None -> use last target window if set else active window in session
|
|
95
|
+
- None -> use last target window if set else a ready active window in session
|
|
62
96
|
- digits (e.g., "1") -> session:index
|
|
63
97
|
- full tmux target (e.g., "name:1" or "name:1.0" or "%12") -> pass-through
|
|
64
98
|
"""
|
|
65
|
-
self._ensure_session()
|
|
66
99
|
if pane is None:
|
|
67
100
|
if self.target_window:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
# Window IDs can be reused after a server restart. Check both
|
|
102
|
+
# membership and readiness before trusting a cached default.
|
|
103
|
+
windows, code = self._run_tmux(
|
|
104
|
+
['list-windows', '-t', f'={self.session_name}',
|
|
105
|
+
'-F', '#{window_id}|#{@tmux_cli_ready}'],
|
|
106
|
+
include_stderr=True,
|
|
107
|
+
)
|
|
108
|
+
if code != 0 and not self._session_is_missing(windows):
|
|
109
|
+
raise self._tmux_failure('list-windows', windows, code)
|
|
110
|
+
expected = f'{self.target_window}|{self.target_window}'
|
|
111
|
+
if code == 0 and expected in windows.splitlines():
|
|
112
|
+
return self.target_window
|
|
113
|
+
self.target_window = None
|
|
114
|
+
if code != 0:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"No target pane/window specified; managed session "
|
|
117
|
+
f"'{self.session_name}' does not exist. "
|
|
118
|
+
"Launch a window or pass --pane."
|
|
119
|
+
)
|
|
120
|
+
# An existing managed session may have been created by an earlier CLI call.
|
|
121
|
+
win, code = self._run_tmux(
|
|
122
|
+
['display-message', '-p', '-t', f'={self.session_name}:',
|
|
123
|
+
'#{window_id}|#{@tmux_cli_ready}'],
|
|
124
|
+
include_stderr=True,
|
|
125
|
+
)
|
|
126
|
+
if code == 0:
|
|
127
|
+
if not win:
|
|
128
|
+
raise self._tmux_failure('display-message', win, code)
|
|
129
|
+
window_id, separator, ready = win.partition('|')
|
|
130
|
+
if not separator or not window_id.startswith('@'):
|
|
131
|
+
raise self._tmux_failure('display-message', win, code)
|
|
132
|
+
if ready == window_id:
|
|
133
|
+
self.target_window = window_id
|
|
134
|
+
return window_id
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"No target pane/window specified; managed session '{self.session_name}' "
|
|
137
|
+
"has no successfully launched active window. Launch a window or pass --pane."
|
|
138
|
+
)
|
|
139
|
+
if code != 0 and not (self._session_is_missing(win) or "can't find window:" in win):
|
|
140
|
+
raise self._tmux_failure('display-message', win, code)
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"No target pane/window specified; managed session '{self.session_name}' "
|
|
143
|
+
"does not exist or has no active window. Launch a window or pass --pane."
|
|
144
|
+
)
|
|
76
145
|
# If user supplied a simple index
|
|
77
|
-
if isinstance(pane,
|
|
146
|
+
if isinstance(pane, bool):
|
|
147
|
+
raise ValueError("Boolean --pane is not a window index")
|
|
148
|
+
if type(pane) is int or (isinstance(pane, str) and pane.isdigit()):
|
|
149
|
+
if not self._managed_session_exists():
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Managed session '{self.session_name}' does not exist; "
|
|
152
|
+
"launch a window or pass a full --pane target."
|
|
153
|
+
)
|
|
78
154
|
return f"{self.session_name}:{pane}"
|
|
79
155
|
# Otherwise assume user provided a pane/window target or pane id
|
|
80
156
|
return pane
|
|
@@ -92,7 +168,6 @@ class RemoteTmuxController:
|
|
|
92
168
|
Returns a list shaped similarly to local list_panes, with keys:
|
|
93
169
|
id (window target), index, title (window name), active (bool), size (N/A)
|
|
94
170
|
"""
|
|
95
|
-
self._ensure_session()
|
|
96
171
|
out, code = self._run_tmux([
|
|
97
172
|
'list-windows', '-t', self.session_name,
|
|
98
173
|
'-F', '#{window_index}|#{window_name}|#{window_active}|#{window_width}x#{window_height}'
|
|
@@ -117,17 +192,49 @@ class RemoteTmuxController:
|
|
|
117
192
|
"""Launch a command in a new window within the managed session.
|
|
118
193
|
Returns the window target (e.g., "session:1").
|
|
119
194
|
"""
|
|
120
|
-
self.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
args
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
195
|
+
previous_target = self.target_window
|
|
196
|
+
try:
|
|
197
|
+
self._ensure_session()
|
|
198
|
+
args = ['new-window', '-t', f'={self.session_name}:', '-P', '-F',
|
|
199
|
+
'#{session_name}:#{window_index}|#{window_id}']
|
|
200
|
+
if name:
|
|
201
|
+
args.extend(['-n', name])
|
|
202
|
+
if command:
|
|
203
|
+
args.append(command)
|
|
204
|
+
out, code = self._run_tmux(args, include_stderr=True)
|
|
205
|
+
if code != 0 or not out:
|
|
206
|
+
raise self._tmux_failure('new-window', out, code)
|
|
207
|
+
target, separator, window_id = out.partition('|')
|
|
208
|
+
if not separator or not target or not window_id.startswith('@'):
|
|
209
|
+
raise self._tmux_failure('new-window', out, code)
|
|
210
|
+
try:
|
|
211
|
+
marked, mark_code = self._run_tmux(
|
|
212
|
+
['set-option', '-w', '-t', window_id, self._ready_option, window_id],
|
|
213
|
+
include_stderr=True,
|
|
214
|
+
)
|
|
215
|
+
except RuntimeError as exc:
|
|
216
|
+
raise RuntimeError(
|
|
217
|
+
f"Ready mark for created window {window_id} could not be set; "
|
|
218
|
+
f"command may have started. {exc}"
|
|
219
|
+
) from exc
|
|
220
|
+
if mark_code != 0:
|
|
221
|
+
if self._window_is_missing(marked, window_id):
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
f"Command was launched in window {window_id}, but that window "
|
|
224
|
+
f"disappeared before its ready mark; no live target is available "
|
|
225
|
+
f"(tmux set-option exit {mark_code}: {marked}). "
|
|
226
|
+
"The command may have run; do not retry automatically."
|
|
227
|
+
)
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
f"{self._tmux_failure('set-option', marked, mark_code)}; "
|
|
230
|
+
f"created window {window_id}; command may have started. "
|
|
231
|
+
"Inspect the window before retrying."
|
|
232
|
+
)
|
|
233
|
+
self.target_window = window_id
|
|
234
|
+
return target
|
|
235
|
+
except Exception:
|
|
236
|
+
self.target_window = previous_target
|
|
237
|
+
raise
|
|
131
238
|
|
|
132
239
|
def send_keys(self, text: str, pane_id: Optional[str] = None, enter: bool = True,
|
|
133
240
|
delay_enter: Union[bool, float] = True, verify_enter: bool = True,
|
|
@@ -272,14 +379,25 @@ class RemoteTmuxController:
|
|
|
272
379
|
|
|
273
380
|
def kill_window(self, window_id: Optional[str] = None):
|
|
274
381
|
target = self._window_target(window_id)
|
|
382
|
+
cached_target = target
|
|
383
|
+
if self.target_window and target != self.target_window:
|
|
384
|
+
resolved, code = self._run_tmux(
|
|
385
|
+
['display-message', '-p', '-t', target, '#{window_id}']
|
|
386
|
+
)
|
|
387
|
+
if code == 0 and resolved:
|
|
388
|
+
cached_target = resolved
|
|
275
389
|
# Ensure the target refers to a window (not a %pane id)
|
|
276
390
|
# If user passed a pane id like %12, tmux can still resolve to its window
|
|
277
391
|
self._run_tmux(['kill-window', '-t', target])
|
|
278
|
-
if self.target_window ==
|
|
392
|
+
if self.target_window == cached_target:
|
|
279
393
|
self.target_window = None
|
|
280
394
|
|
|
281
395
|
def attach_session(self):
|
|
282
|
-
self.
|
|
396
|
+
if not self._managed_session_exists():
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"Managed session '{self.session_name}' does not exist; "
|
|
399
|
+
"launch a window before attaching."
|
|
400
|
+
)
|
|
283
401
|
# Attach will replace the current terminal view until the user detaches
|
|
284
402
|
subprocess.run(['tmux', 'attach-session', '-t', self.session_name])
|
|
285
403
|
|
|
@@ -289,7 +407,6 @@ class RemoteTmuxController:
|
|
|
289
407
|
|
|
290
408
|
def list_windows(self) -> List[Dict[str, str]]:
|
|
291
409
|
"""List all windows in the managed session with basic info."""
|
|
292
|
-
self._ensure_session()
|
|
293
410
|
out, code = self._run_tmux(['list-windows', '-t', self.session_name, '-F', '#{window_index}|#{window_name}|#{window_active}'])
|
|
294
411
|
if code != 0 or not out:
|
|
295
412
|
return []
|
|
@@ -236,6 +236,14 @@ Daemon + core — `claude_code_tools/agent_tunnel/`:
|
|
|
236
236
|
Mattermost routes the same way, with Mattermost's thread model: a root post
|
|
237
237
|
`\<handle\> [question]` in a watched channel (26-char `channel_ids`) opens a
|
|
238
238
|
thread keyed `mm:<root post id>`, and the bot answers as replies under it.
|
|
239
|
+
A root post that simply **@-mentions the bot** works too — the mention is
|
|
240
|
+
stripped and what follows is the question, answered against
|
|
241
|
+
`[mattermost] default_handle` (unset: the only shared session when exactly
|
|
242
|
+
one is live, else the bot replies asking which handle to use). A handle may
|
|
243
|
+
still lead the question after the mention.
|
|
244
|
+
A message inside a thread the bot has NOT bound is routed the same way as a
|
|
245
|
+
root post (so a teammate can answer the bot's "which session?" question
|
|
246
|
+
in place); anything else there is left alone.
|
|
239
247
|
Replies in a bound thread are follow-ups; a reply opening with
|
|
240
248
|
`@someone-else`, `@all`, `@channel` or `@here` is side-chat and ignored, and
|
|
241
249
|
a leading `@bot` is stripped. `!list`/`!handles` and `!done`/`!close`/`!end`
|
|
@@ -5,27 +5,40 @@ Automatically detects whether you're inside or outside tmux and uses the appropr
|
|
|
5
5
|
|
|
6
6
|
## Auto-Detection
|
|
7
7
|
- **Inside tmux (Local Mode)**: Manages panes in your current tmux window
|
|
8
|
-
- **Outside tmux (Remote Mode)**:
|
|
8
|
+
- **Outside tmux (Remote Mode)**: Manages windows in a separate tmux session, creating it when `launch` first needs it
|
|
9
|
+
|
|
10
|
+
Remote `status`, `list_panes`, and `list_windows` do not create a session.
|
|
11
|
+
An explicit full pane target (such as `%12` or `myapp:1.2`) works without a
|
|
12
|
+
managed session. Commands without a target require an active window from a
|
|
13
|
+
successful managed `launch`; otherwise, launch a window first or pass `--pane`.
|
|
14
|
+
`attach` requires the managed session to already exist.
|
|
9
15
|
|
|
10
16
|
## Prerequisites
|
|
11
17
|
- tmux must be installed
|
|
12
18
|
- The `tmux-cli` command must be available (installed via `uv tool install`)
|
|
13
19
|
|
|
14
|
-
##
|
|
20
|
+
## Target Identification
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
In local mode, panes can be specified in two simple ways:
|
|
17
23
|
- Just the pane number (e.g., `2`) - refers to pane 2 in the current window
|
|
18
24
|
- Full format: `session:window.pane` (e.g., `myapp:1.2`) - for any pane in any session
|
|
19
25
|
|
|
26
|
+
In remote mode, `launch` returns a window target such as `remote-cli-session:1`.
|
|
27
|
+
Use the returned target as `--pane`. A bare number such as `1` means a managed
|
|
28
|
+
window index, not a pane index; window indices can change if windows move.
|
|
29
|
+
Numeric `--pane=2` examples below show local mode unless labeled as remote.
|
|
30
|
+
|
|
20
31
|
## ⚠️ IMPORTANT: Always Launch a Shell First!
|
|
21
32
|
|
|
22
33
|
**Always launch zsh first** to prevent losing output when commands fail:
|
|
23
34
|
```bash
|
|
24
|
-
tmux-cli launch "zsh" # Do this FIRST
|
|
25
|
-
tmux-cli send "your-command" --pane=
|
|
35
|
+
tmux-cli launch "zsh" # Do this FIRST; remote example returns remote-cli-session:1
|
|
36
|
+
tmux-cli send "your-command" --pane=remote-cli-session:1 # Use the actual returned target
|
|
26
37
|
```
|
|
27
38
|
|
|
28
|
-
If
|
|
39
|
+
If a directly launched command exits before its remote window is marked ready,
|
|
40
|
+
`launch` reports that the command was launched but has no live target. It does
|
|
41
|
+
not retry the command. Launch a shell first to keep output available.
|
|
29
42
|
|
|
30
43
|
## Core Commands
|
|
31
44
|
|
|
@@ -33,7 +46,8 @@ If you launch a command directly and it errors, the pane closes immediately and
|
|
|
33
46
|
```bash
|
|
34
47
|
tmux-cli launch "command"
|
|
35
48
|
# Example: tmux-cli launch "python3"
|
|
36
|
-
#
|
|
49
|
+
# Local return: pane identifier (e.g., myapp:1.2)
|
|
50
|
+
# Remote return: window target (e.g., remote-cli-session:1)
|
|
37
51
|
```
|
|
38
52
|
|
|
39
53
|
### Send input to a pane
|
|
@@ -159,23 +173,23 @@ tmux-cli help
|
|
|
159
173
|
|
|
160
174
|
1. **ALWAYS launch a shell first** (prefer zsh) - this prevents losing output on errors:
|
|
161
175
|
```bash
|
|
162
|
-
tmux-cli launch "zsh" #
|
|
176
|
+
tmux-cli launch "zsh" # Remote: returns a window target - DO THIS FIRST!
|
|
163
177
|
```
|
|
164
178
|
|
|
165
179
|
2. Run your command in the shell:
|
|
166
180
|
```bash
|
|
167
|
-
tmux-cli send "python script.py" --pane=
|
|
181
|
+
tmux-cli send "python script.py" --pane=remote-cli-session:1 # Use the actual returned target
|
|
168
182
|
```
|
|
169
183
|
|
|
170
184
|
3. Interact with the program:
|
|
171
185
|
```bash
|
|
172
|
-
tmux-cli send "user input" --pane=
|
|
173
|
-
tmux-cli capture --pane=
|
|
186
|
+
tmux-cli send "user input" --pane=remote-cli-session:1
|
|
187
|
+
tmux-cli capture --pane=remote-cli-session:1 # Check output
|
|
174
188
|
```
|
|
175
189
|
|
|
176
190
|
4. Clean up when done:
|
|
177
191
|
```bash
|
|
178
|
-
tmux-cli kill --pane=
|
|
192
|
+
tmux-cli kill --pane=remote-cli-session:1
|
|
179
193
|
```
|
|
180
194
|
|
|
181
195
|
## Remote Mode Specific Commands
|
|
@@ -205,7 +219,8 @@ tmux-cli list_windows
|
|
|
205
219
|
- Use `capture` to check the current state before sending input
|
|
206
220
|
- Use `status` to see all available panes and their current state
|
|
207
221
|
- In local mode: Pane identifiers can be session:window.pane format (like `myapp:1.2`) or just pane indices like `1`, `2`
|
|
208
|
-
- In remote mode:
|
|
222
|
+
- In remote mode: Bare numbers like `0`, `1` are window indices; `launch` returns
|
|
223
|
+
a `session:window_index` target, such as `remote-cli-session:1`
|
|
209
224
|
- If you launch a command directly (not via shell), the pane/window closes when
|
|
210
225
|
the command exits
|
|
211
226
|
- **IMPORTANT**: The tool prevents you from killing your own pane/window to avoid
|
|
@@ -222,4 +237,4 @@ tmux-cli wait_idle --pane=2 --idle-time=3.0
|
|
|
222
237
|
|
|
223
238
|
# Now capture the result
|
|
224
239
|
tmux-cli capture --pane=2
|
|
225
|
-
```
|
|
240
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-code-tools"
|
|
3
|
-
version = "1.29.
|
|
3
|
+
version = "1.29.2"
|
|
4
4
|
description = "Collection of tools for working with Claude Code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -113,7 +113,7 @@ markers = [
|
|
|
113
113
|
|
|
114
114
|
[tool.commitizen]
|
|
115
115
|
name = "cz_conventional_commits"
|
|
116
|
-
version = "1.29.
|
|
116
|
+
version = "1.29.2"
|
|
117
117
|
tag_format = "v$version"
|
|
118
118
|
version_files = [
|
|
119
119
|
"pyproject.toml:version",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/__init__.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/backends.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/convert.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/discord_bot.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/locking.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/paths.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/readonly.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/registry.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/relay.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/serve.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/store.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/tmux.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/trust.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/agent_tunnel/turn_log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_app_server_rpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_callback.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_fingerprint.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_generation.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_legacy.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_models.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_process.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_reservation.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_resume.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_retry.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_reuse.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_state.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_supervisor.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_wait.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/codex_server_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/export_claude_session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/export_codex_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_claude_session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_codex_session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_original_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/find_trimmed_sessions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/github_watch_daemon.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/github_watch_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/issue_reply_delivery.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/legacy_session_wrappers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/msg/prompt_detect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_claude_noise.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_claude_to_codex.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_codex_flatten.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/port_codex_to_claude.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/resolve_session_names.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/resolve_session_render.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_cli_resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_resolution.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/session_selection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/tmux_execution_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/trim_session_claude.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/trim_session_codex.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_contract.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_formatting.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_manifest.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_projection.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_rendering.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_cli_snapshots.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_processes.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_store_io.py
RENAMED
|
File without changes
|
{claude_code_tools-1.29.0 → claude_code_tools-1.29.2}/claude_code_tools/workflow_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|