claude-code-tools 1.29.0__tar.gz → 1.29.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/PKG-INFO +1 -1
  2. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/__init__.py +1 -1
  3. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/tmux_cli_controller.py +2 -5
  4. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/tmux_remote_controller.py +166 -49
  5. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/tmux-cli-instructions.md +29 -14
  6. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/pyproject.toml +2 -2
  7. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/.gitignore +0 -0
  8. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/LICENSE +0 -0
  9. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/README.md +0 -0
  10. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/action_rpc.py +0 -0
  11. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  12. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/backends.py +0 -0
  13. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/cli.py +0 -0
  14. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/config.py +0 -0
  15. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/convert.py +0 -0
  16. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  17. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/http_frontend.py +0 -0
  18. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/locking.py +0 -0
  19. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/mattermost_bot.py +0 -0
  20. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/paths.py +0 -0
  21. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/readonly.py +0 -0
  22. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/registry.py +0 -0
  23. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/relay.py +0 -0
  24. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/serve.py +0 -0
  25. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/session.py +0 -0
  26. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/store.py +0 -0
  27. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  28. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/trust.py +0 -0
  29. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/agent_tunnel/turn_log.py +0 -0
  30. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/aichat.py +0 -0
  31. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/__init__.py +0 -0
  32. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/__main__.py +0 -0
  33. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/activity.py +0 -0
  34. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/cache.py +0 -0
  35. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/cli.py +0 -0
  36. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/detect.py +0 -0
  37. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/filters.py +0 -0
  38. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/model.py +0 -0
  39. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/render.py +0 -0
  40. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/amux/scan.py +0 -0
  41. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/claude_continue.py +0 -0
  42. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/cli_passthrough.py +0 -0
  43. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_app_server_rpc.py +0 -0
  44. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_continue.py +0 -0
  45. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server.py +0 -0
  46. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_callback.py +0 -0
  47. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_cli.py +0 -0
  48. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_fingerprint.py +0 -0
  49. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_generation.py +0 -0
  50. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_legacy.py +0 -0
  51. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_models.py +0 -0
  52. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_process.py +0 -0
  53. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_reservation.py +0 -0
  54. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_resume.py +0 -0
  55. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_retry.py +0 -0
  56. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_reuse.py +0 -0
  57. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_state.py +0 -0
  58. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_supervisor.py +0 -0
  59. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_wait.py +0 -0
  60. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/codex_server_worker.py +0 -0
  61. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/config.py +0 -0
  62. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/csv2gsheet.py +0 -0
  63. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/delete_session.py +0 -0
  64. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/dotenv_vault.py +0 -0
  65. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/env_safe.py +0 -0
  66. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/export_all.py +0 -0
  67. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/export_claude_session.py +0 -0
  68. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/export_codex_session.py +0 -0
  69. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/export_session.py +0 -0
  70. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/find_claude_session.py +0 -0
  71. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/find_codex_session.py +0 -0
  72. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/find_original_session.py +0 -0
  73. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/find_session.py +0 -0
  74. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/find_trimmed_sessions.py +0 -0
  75. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/fix_session.py +0 -0
  76. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/gdoc2docx.py +0 -0
  77. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/gdoc2md.py +0 -0
  78. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/github_wake.py +0 -0
  79. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/github_watch_daemon.py +0 -0
  80. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/github_watch_store.py +0 -0
  81. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/gsheet2csv.py +0 -0
  82. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/helper_sessions.py +0 -0
  83. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/issue_reply_delivery.py +0 -0
  84. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/legacy_session_wrappers.py +0 -0
  85. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/md2gdoc.py +0 -0
  86. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/move_account.py +0 -0
  87. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/__init__.py +0 -0
  88. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/cli.py +0 -0
  89. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/hooks.py +0 -0
  90. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/migrations.py +0 -0
  91. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/models.py +0 -0
  92. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/prompt_detect.py +0 -0
  93. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/store.py +0 -0
  94. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/msg/watcher.py +0 -0
  95. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/node_menu_ui.py +0 -0
  96. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/port_claude_noise.py +0 -0
  97. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/port_claude_to_codex.py +0 -0
  98. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/port_codex_flatten.py +0 -0
  99. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/port_codex_to_claude.py +0 -0
  100. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/port_render.py +0 -0
  101. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/port_service.py +0 -0
  102. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/resolve_session.py +0 -0
  103. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/resolve_session_names.py +0 -0
  104. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/resolve_session_render.py +0 -0
  105. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/search_index.py +0 -0
  106. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_cli_resolution.py +0 -0
  107. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_lineage.py +0 -0
  108. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_menu.py +0 -0
  109. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_menu_cli.py +0 -0
  110. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_resolution.py +0 -0
  111. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_selection.py +0 -0
  112. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/session_utils.py +0 -0
  113. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/smart_trim.py +0 -0
  114. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/smart_trim_core.py +0 -0
  115. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/tmux_execution_helpers.py +0 -0
  116. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/trim_in_place.py +0 -0
  117. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/trim_session.py +0 -0
  118. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/trim_session_claude.py +0 -0
  119. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/trim_session_codex.py +0 -0
  120. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli.py +0 -0
  121. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_contract.py +0 -0
  122. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_formatting.py +0 -0
  123. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  124. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_manifest.py +0 -0
  125. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_projection.py +0 -0
  126. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_rendering.py +0 -0
  127. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_snapshots.py +0 -0
  128. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  129. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_processes.py +0 -0
  130. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_runs.py +0 -0
  131. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_store_io.py +0 -0
  132. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/claude_code_tools/workflow_validation.py +0 -0
  133. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/agent-tunnel-spec.md +0 -0
  134. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/claude-code-tmux-tutorials.md +0 -0
  135. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/dot-zshrc.md +0 -0
  136. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/find-claude-session.md +0 -0
  137. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/intercom-spec.md +0 -0
  138. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/lmsh.md +0 -0
  139. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/local-llm-setup.md +0 -0
  140. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/docs/vault-documentation.md +0 -0
  141. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/hatch_build.py +0 -0
  142. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/node_ui/action_config.js +0 -0
  143. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/node_ui/menu.js +0 -0
  144. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/node_ui/package-lock.json +0 -0
  145. {claude_code_tools-1.29.0 → claude_code_tools-1.29.1}/node_ui/package.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 1.29.0
3
+ Version: 1.29.1
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "1.29.0"
3
+ __version__ = "1.29.1"
@@ -914,11 +914,8 @@ class CLI:
914
914
  print(str(e))
915
915
  else:
916
916
  # Remote mode - kill window
917
- try:
918
- self.controller.kill_window(window_id=pane)
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):
@@ -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
- - Auto-creating a detached tmux session on first use
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 and ensure the session exists."""
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., "session:0" (active pane in that window)
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
- result = subprocess.run(
34
- ['tmux'] + args,
35
- capture_output=True,
36
- text=True
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
- return result.stdout.strip(), result.returncode
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 it doesn't exist (detached)."""
42
- _, code = self._run_tmux(['has-session', '-t', self.session_name])
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
- # Remember first window as default target
50
- self.target_window = f"{self.session_name}:0"
51
- else:
52
- # If already exists and we don't have a target, set to active window
53
- if not self.target_window:
54
- win, code2 = self._run_tmux(['display-message', '-p', '-t', self.session_name, '#{session_name}:#{window_index}'])
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
- return self.target_window
69
- # Fallback to active window in session
70
- win, code = self._run_tmux(['display-message', '-p', '-t', self.session_name, '#{session_name}:#{window_index}'])
71
- if code == 0 and win:
72
- self.target_window = win
73
- return win
74
- # Final fallback: session:0
75
- return f"{self.session_name}:0"
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, str) and pane.isdigit():
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._ensure_session()
121
- args = ['new-window', '-t', self.session_name, '-P', '-F', '#{session_name}:#{window_index}']
122
- if name:
123
- args.extend(['-n', name])
124
- if command:
125
- args.append(command)
126
- out, code = self._run_tmux(args)
127
- if code == 0 and out:
128
- self.target_window = out
129
- return out
130
- return None
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 == target:
392
+ if self.target_window == cached_target:
279
393
  self.target_window = None
280
394
 
281
395
  def attach_session(self):
282
- self._ensure_session()
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 []
@@ -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)**: Creates and manages a separate tmux session with windows
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
- ## Pane Identification
20
+ ## Target Identification
15
21
 
16
- Panes can be specified in two simple ways:
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=2 # Then run commands
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 you launch a command directly and it errors, the pane closes immediately and you lose all output!
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
- # Returns: pane identifier (e.g., session:window.pane format like myapp:1.2)
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" # Returns pane identifier - DO THIS FIRST!
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=2
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=2
173
- tmux-cli capture --pane=2 # Check output
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=2
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: Window IDs can be indices like `0`, `1` or full form like `session:0.0`
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.0"
3
+ version = "1.29.1"
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.0"
116
+ version = "1.29.1"
117
117
  tag_format = "v$version"
118
118
  version_files = [
119
119
  "pyproject.toml:version",