claude-code-tools 1.29.4__tar.gz → 1.29.6__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.4 → claude_code_tools-1.29.6}/PKG-INFO +10 -1
  2. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/README.md +9 -0
  3. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/__init__.py +1 -1
  4. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/github_wake.py +82 -20
  5. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/github_watch_daemon.py +7 -1
  6. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/github_watch_store.py +38 -5
  7. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/issue_reply_delivery.py +123 -50
  8. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/port_claude_to_codex.py +5 -15
  9. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/pyproject.toml +2 -2
  10. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/.gitignore +0 -0
  11. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/LICENSE +0 -0
  12. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/action_rpc.py +0 -0
  13. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  14. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/backends.py +0 -0
  15. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/cli.py +0 -0
  16. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/config.py +0 -0
  17. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/convert.py +0 -0
  18. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  19. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/http_frontend.py +0 -0
  20. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/locking.py +0 -0
  21. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/mattermost_bot.py +0 -0
  22. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/paths.py +0 -0
  23. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/readonly.py +0 -0
  24. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/registry.py +0 -0
  25. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/relay.py +0 -0
  26. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/serve.py +0 -0
  27. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/session.py +0 -0
  28. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/store.py +0 -0
  29. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  30. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/trust.py +0 -0
  31. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/agent_tunnel/turn_log.py +0 -0
  32. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/aichat.py +0 -0
  33. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/__init__.py +0 -0
  34. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/__main__.py +0 -0
  35. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/activity.py +0 -0
  36. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/cache.py +0 -0
  37. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/cli.py +0 -0
  38. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/detect.py +0 -0
  39. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/filters.py +0 -0
  40. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/model.py +0 -0
  41. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/render.py +0 -0
  42. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/amux/scan.py +0 -0
  43. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/claude_continue.py +0 -0
  44. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/cli_passthrough.py +0 -0
  45. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_app_server_rpc.py +0 -0
  46. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_continue.py +0 -0
  47. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server.py +0 -0
  48. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_callback.py +0 -0
  49. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_cli.py +0 -0
  50. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_fingerprint.py +0 -0
  51. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_generation.py +0 -0
  52. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_legacy.py +0 -0
  53. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_models.py +0 -0
  54. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_process.py +0 -0
  55. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_reservation.py +0 -0
  56. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_resume.py +0 -0
  57. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_retry.py +0 -0
  58. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_reuse.py +0 -0
  59. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_state.py +0 -0
  60. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_supervisor.py +0 -0
  61. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_wait.py +0 -0
  62. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/codex_server_worker.py +0 -0
  63. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/config.py +0 -0
  64. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/csv2gsheet.py +0 -0
  65. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/delete_session.py +0 -0
  66. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/dotenv_vault.py +0 -0
  67. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/env_safe.py +0 -0
  68. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/export_all.py +0 -0
  69. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/export_claude_session.py +0 -0
  70. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/export_codex_session.py +0 -0
  71. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/export_session.py +0 -0
  72. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/find_claude_session.py +0 -0
  73. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/find_codex_session.py +0 -0
  74. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/find_original_session.py +0 -0
  75. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/find_session.py +0 -0
  76. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/find_trimmed_sessions.py +0 -0
  77. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/fix_session.py +0 -0
  78. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/gdoc2docx.py +0 -0
  79. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/gdoc2md.py +0 -0
  80. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/gsheet2csv.py +0 -0
  81. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/helper_sessions.py +0 -0
  82. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/legacy_session_wrappers.py +0 -0
  83. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/md2gdoc.py +0 -0
  84. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/move_account.py +0 -0
  85. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/__init__.py +0 -0
  86. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/cli.py +0 -0
  87. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/hooks.py +0 -0
  88. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/migrations.py +0 -0
  89. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/models.py +0 -0
  90. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/prompt_detect.py +0 -0
  91. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/store.py +0 -0
  92. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/msg/watcher.py +0 -0
  93. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/node_menu_ui.py +0 -0
  94. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/port_claude_noise.py +0 -0
  95. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/port_codex_flatten.py +0 -0
  96. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/port_codex_to_claude.py +0 -0
  97. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/port_render.py +0 -0
  98. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/port_service.py +0 -0
  99. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/resolve_session.py +0 -0
  100. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/resolve_session_names.py +0 -0
  101. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/resolve_session_render.py +0 -0
  102. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/search_index.py +0 -0
  103. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_cli_resolution.py +0 -0
  104. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_lineage.py +0 -0
  105. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_menu.py +0 -0
  106. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_menu_cli.py +0 -0
  107. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_resolution.py +0 -0
  108. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_selection.py +0 -0
  109. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/session_utils.py +0 -0
  110. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/smart_trim.py +0 -0
  111. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/smart_trim_core.py +0 -0
  112. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/tmux_cli_controller.py +0 -0
  113. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/tmux_execution_helpers.py +0 -0
  114. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/tmux_remote_controller.py +0 -0
  115. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/trim_in_place.py +0 -0
  116. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/trim_session.py +0 -0
  117. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/trim_session_claude.py +0 -0
  118. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/trim_session_codex.py +0 -0
  119. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli.py +0 -0
  120. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_contract.py +0 -0
  121. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_formatting.py +0 -0
  122. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  123. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_manifest.py +0 -0
  124. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_projection.py +0 -0
  125. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_rendering.py +0 -0
  126. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_snapshots.py +0 -0
  127. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  128. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_processes.py +0 -0
  129. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_runs.py +0 -0
  130. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_store_io.py +0 -0
  131. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/claude_code_tools/workflow_validation.py +0 -0
  132. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/agent-tunnel-spec.md +0 -0
  133. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/claude-code-tmux-tutorials.md +0 -0
  134. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/dot-zshrc.md +0 -0
  135. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/find-claude-session.md +0 -0
  136. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/intercom-spec.md +0 -0
  137. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/lmsh.md +0 -0
  138. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/local-llm-setup.md +0 -0
  139. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/tmux-cli-instructions.md +0 -0
  140. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/docs/vault-documentation.md +0 -0
  141. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/hatch_build.py +0 -0
  142. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/node_ui/action_config.js +0 -0
  143. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/node_ui/menu.js +0 -0
  144. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/node_ui/package-lock.json +0 -0
  145. {claude_code_tools-1.29.4 → claude_code_tools-1.29.6}/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.4
3
+ Version: 1.29.6
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -47,6 +47,15 @@ Everything — installation, every tool, plugins, and guides — lives in the
47
47
  docs. Click a card below to jump to a feature, or
48
48
  **[read the full docs](https://pchalasani.github.io/claude-code-tools/)**.
49
49
 
50
+ Skills install with one command (`-s` picks one skill, `-g` installs for
51
+ every project). Most work in any agent; a few are Claude Code and Codex
52
+ only (see the [Plugins page](https://pchalasani.github.io/claude-code-tools/getting-started/plugins/)).
53
+ Needs Node.js 22.20 or newer for `npx`:
54
+
55
+ ```bash
56
+ npx skills add pchalasani/claude-code-tools -s voxtype-install
57
+ ```
58
+
50
59
  <div align="center">
51
60
 
52
61
  <table>
@@ -21,6 +21,15 @@ Everything — installation, every tool, plugins, and guides — lives in the
21
21
  docs. Click a card below to jump to a feature, or
22
22
  **[read the full docs](https://pchalasani.github.io/claude-code-tools/)**.
23
23
 
24
+ Skills install with one command (`-s` picks one skill, `-g` installs for
25
+ every project). Most work in any agent; a few are Claude Code and Codex
26
+ only (see the [Plugins page](https://pchalasani.github.io/claude-code-tools/getting-started/plugins/)).
27
+ Needs Node.js 22.20 or newer for `npx`:
28
+
29
+ ```bash
30
+ npx skills add pchalasani/claude-code-tools -s voxtype-install
31
+ ```
32
+
24
33
  <div align="center">
25
34
 
26
35
  <table>
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "1.29.4"
3
+ __version__ = "1.29.6"
@@ -19,8 +19,13 @@ from urllib.parse import urlparse
19
19
 
20
20
  import click
21
21
 
22
+ from claude_code_tools import __version__
22
23
  from claude_code_tools.codex_server_process import process_identity
23
- from claude_code_tools.github_watch_store import IssueWatch, WatchStore
24
+ from claude_code_tools.github_watch_store import (
25
+ IssueWatch,
26
+ WatcherStatus,
27
+ WatchStore,
28
+ )
24
29
  from claude_code_tools.issue_reply_delivery import (
25
30
  _MAX_MONITOR_MESSAGE_BYTES,
26
31
  _MONITOR_ACK,
@@ -31,6 +36,7 @@ from claude_code_tools.issue_reply_delivery import (
31
36
 
32
37
  _DAEMON_START_SECONDS = 5.0
33
38
  _DAEMON_STOP_SECONDS = 5.0
39
+ _DAEMON_REPLACE_SECONDS = 30.0
34
40
  _MAX_GH_OUTPUT_BYTES = 1024 * 1024
35
41
 
36
42
 
@@ -41,6 +47,11 @@ _MAX_GH_OUTPUT_BYTES = 1024 * 1024
41
47
  @click.option("--start", is_flag=True, help="Start the shared watcher.")
42
48
  @click.option("--stop", is_flag=True, help="Stop the shared watcher.")
43
49
  @click.option("--cancel", metavar="WATCH_ID", help="Cancel one pending wakeup.")
50
+ @click.option(
51
+ "--codex-home",
52
+ type=click.Path(path_type=Path, exists=True, file_okay=False),
53
+ help="Codex account directory of this session (default: $CODEX_HOME).",
54
+ )
44
55
  @click.option(
45
56
  "--claude-monitor",
46
57
  is_flag=True,
@@ -53,9 +64,15 @@ def cli(
53
64
  start: bool,
54
65
  stop: bool,
55
66
  cancel: str | None,
67
+ codex_home: Path | None,
56
68
  claude_monitor: bool,
57
69
  ) -> None:
58
- """Wake this session when ISSUE_URL receives its first future comment."""
70
+ """Wake this session when ISSUE_URL receives its first future comment.
71
+
72
+ In Codex, the reply is delivered with `codex queue` to the session named
73
+ by CODEX_THREAD_ID. In Claude Code, run with --claude-monitor under the
74
+ Monitor tool.
75
+ """
59
76
  modes = sum((show_status, start, stop, cancel is not None))
60
77
  if modes > 1 or (modes and issue_url is not None) or (modes and claude_monitor):
61
78
  raise click.UsageError(
@@ -63,6 +80,8 @@ def cli(
63
80
  )
64
81
  if as_json and not show_status:
65
82
  raise click.UsageError("--json requires --status")
83
+ if codex_home is not None and (modes or claude_monitor):
84
+ raise click.UsageError("--codex-home applies only to Codex reply wakeups")
66
85
  if show_status:
67
86
  _show_status(as_json)
68
87
  return
@@ -83,21 +102,20 @@ def cli(
83
102
  if claude_monitor:
84
103
  _run_claude_monitor(issue_url)
85
104
  return
86
- _register(issue_url)
105
+ try:
106
+ target = codex_target_from_environment(codex_home)
107
+ except DeliveryConfigurationError as exc:
108
+ raise click.ClickException(str(exc)) from exc
109
+ _register(issue_url, target)
87
110
 
88
111
 
89
112
  def _register(
90
113
  issue_url: str,
91
- target: DeliveryTarget | None = None,
114
+ target: DeliveryTarget,
92
115
  announce: bool = True,
93
116
  preserve_on_start_failure: bool = True,
94
117
  ) -> IssueWatch:
95
118
  """Validate and durably register one existing issue URL."""
96
- if target is None:
97
- try:
98
- target = codex_target_from_environment()
99
- except DeliveryConfigurationError as exc:
100
- raise click.ClickException(str(exc)) from exc
101
119
  repository, issue_number, host = _parse_issue_url(issue_url)
102
120
  canonical_url = _verify_issue(repository, issue_number, host)
103
121
  store = WatchStore()
@@ -325,19 +343,46 @@ def _stop_watcher(store: WatchStore) -> None:
325
343
  return
326
344
  assert status is not None
327
345
  os.kill(status.pid, signal.SIGTERM)
328
- deadline = time.monotonic() + _DAEMON_STOP_SECONDS
346
+ if not _wait_for_exit(status, _DAEMON_STOP_SECONDS):
347
+ raise click.ClickException("watcher did not stop after SIGTERM")
348
+ click.echo("Watcher stopped.")
349
+
350
+
351
+ def _replace_watcher(status: WatcherStatus) -> None:
352
+ """End a watcher of another version, forcing it if a poll keeps it busy.
353
+
354
+ Its state is durable, so a forced stop can at most repeat a delivery.
355
+ """
356
+ if process_identity(status.pid) != status.process_identity:
357
+ return
358
+ os.kill(status.pid, signal.SIGTERM)
359
+ if _wait_for_exit(status, _DAEMON_REPLACE_SECONDS):
360
+ return
361
+ if process_identity(status.pid) == status.process_identity:
362
+ os.kill(status.pid, signal.SIGKILL)
363
+ if not _wait_for_exit(status, _DAEMON_STOP_SECONDS):
364
+ raise RuntimeError("watcher of another version did not exit")
365
+
366
+
367
+ def _wait_for_exit(status: WatcherStatus, seconds: float) -> bool:
368
+ """Wait until the exact recorded watcher process is gone."""
369
+ deadline = time.monotonic() + seconds
329
370
  while time.monotonic() < deadline:
330
371
  if process_identity(status.pid) != status.process_identity:
331
- click.echo("Watcher stopped.")
332
- return
372
+ return True
333
373
  time.sleep(0.05)
334
- raise click.ClickException("watcher did not stop after SIGTERM")
374
+ return False
335
375
 
336
376
 
337
- def _ensure_watcher(store: WatchStore) -> bool:
338
- """Start one detached watcher and wait for its certified heartbeat."""
339
- if _watcher_is_healthy(store.watcher_status()):
340
- return False
377
+ def _ensure_watcher(store: WatchStore, replace_other_version: bool = True) -> bool:
378
+ """Start one detached watcher and wait for its certified heartbeat.
379
+
380
+ A healthy watcher left running by another installed version is replaced
381
+ once, since it may not know the delivery adapter this version records.
382
+ """
383
+ status = store.watcher_status()
384
+ if _watcher_is_healthy(status):
385
+ return _adopt_watcher(store, status, replace_other_version)
341
386
  log = open(store.log_path, "ab", buffering=0)
342
387
  try:
343
388
  process = subprocess.Popen(
@@ -353,8 +398,9 @@ def _ensure_watcher(store: WatchStore) -> bool:
353
398
  deadline = time.monotonic() + _DAEMON_START_SECONDS
354
399
  while time.monotonic() < deadline:
355
400
  if process.poll() is not None:
356
- if _watcher_is_healthy(store.watcher_status()):
357
- return False
401
+ status = store.watcher_status()
402
+ if _watcher_is_healthy(status):
403
+ return _adopt_watcher(store, status, replace_other_version)
358
404
  raise RuntimeError(f"watcher exited with status {process.returncode}")
359
405
  status = store.watcher_status()
360
406
  if (
@@ -364,13 +410,29 @@ def _ensure_watcher(store: WatchStore) -> bool:
364
410
  ):
365
411
  return True
366
412
  if _watcher_is_healthy(status):
367
- return False
413
+ return _adopt_watcher(store, status, replace_other_version)
368
414
  time.sleep(0.05)
369
415
  process.terminate()
370
416
  process.wait(timeout=2)
371
417
  raise RuntimeError("watcher did not publish a healthy heartbeat")
372
418
 
373
419
 
420
+ def _adopt_watcher(
421
+ store: WatchStore,
422
+ status: WatcherStatus | None,
423
+ replace_other_version: bool,
424
+ ) -> bool:
425
+ """Reuse a healthy watcher of this version, or replace it once."""
426
+ if status is not None and status.code_version == __version__:
427
+ return False
428
+ if status is None or not replace_other_version:
429
+ raise RuntimeError(
430
+ "a watcher from another claude-code-tools version is still running"
431
+ )
432
+ _replace_watcher(status)
433
+ return _ensure_watcher(store, replace_other_version=False)
434
+
435
+
374
436
  def _watcher_is_healthy(status: object) -> bool:
375
437
  """Return whether durable status still identifies its exact process."""
376
438
  if status is None:
@@ -1,4 +1,4 @@
1
- """One shared daemon that wakes Codex when watched GitHub issues get replies."""
1
+ """One shared daemon that wakes agent sessions when watched issues get replies."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -17,6 +17,7 @@ from datetime import UTC, datetime, timedelta
17
17
  from pathlib import Path
18
18
  from types import FrameType
19
19
 
20
+ from claude_code_tools import __version__
20
21
  from claude_code_tools.codex_server_process import process_identity
21
22
  from claude_code_tools.github_watch_store import (
22
23
  IssueWatch,
@@ -25,6 +26,7 @@ from claude_code_tools.github_watch_store import (
25
26
  )
26
27
  from claude_code_tools.issue_reply_delivery import (
27
28
  IssueReply,
29
+ UnsupportedDeliveryTarget,
28
30
  deliver_issue_reply,
29
31
  )
30
32
 
@@ -114,6 +116,7 @@ class GitHubWatchDaemon:
114
116
  self.pid,
115
117
  self.process_identity,
116
118
  self.started_at,
119
+ __version__,
117
120
  )
118
121
 
119
122
  def _poll_once(self) -> None:
@@ -294,6 +297,9 @@ class GitHubWatchDaemon:
294
297
  """Deliver one idempotent reply notification into the origin thread."""
295
298
  try:
296
299
  deliver_issue_reply(watch, comment)
300
+ except UnsupportedDeliveryTarget as exc:
301
+ self.store.mark_failed(watch.watch_id, _diagnostic(exc))
302
+ return False
297
303
  except Exception as exc:
298
304
  self.store.mark_retry(watch.watch_id, _diagnostic(exc))
299
305
  return False
@@ -50,6 +50,7 @@ class WatcherStatus:
50
50
  process_identity: str
51
51
  started_at: str
52
52
  heartbeat_at: str
53
+ code_version: str | None = None
53
54
 
54
55
 
55
56
  @dataclass(frozen=True)
@@ -114,6 +115,7 @@ class WatchStore:
114
115
  with self._connect() as connection:
115
116
  connection.executescript(_SCHEMA)
116
117
  _migrate_repository_cursors(connection)
118
+ _migrate_watcher_status(connection)
117
119
 
118
120
  @property
119
121
  def lock_path(self) -> Path:
@@ -242,6 +244,18 @@ class WatchStore:
242
244
  [(watch_id,) for watch_id in watch_ids],
243
245
  )
244
246
 
247
+ def mark_failed(self, watch_id: str, error: str) -> None:
248
+ """Stop retrying one watch whose delivery can never succeed."""
249
+ with self._connect() as connection:
250
+ connection.execute(
251
+ """
252
+ UPDATE issue_watches
253
+ SET status = 'failed', last_error = ?
254
+ WHERE watch_id = ? AND status IN ('pending', 'retrying')
255
+ """,
256
+ (error[:4096], watch_id),
257
+ )
258
+
245
259
  def cancel(self, watch_id: str) -> bool:
246
260
  """Cancel one pending watch by full ID or unambiguous prefix."""
247
261
  with self._connect() as connection:
@@ -300,23 +314,32 @@ class WatchStore:
300
314
  pid: int,
301
315
  process_identity: str,
302
316
  started_at: str,
317
+ code_version: str,
303
318
  ) -> None:
304
- """Publish ownership and liveness for the lock-holding daemon."""
319
+ """Publish ownership, liveness, and code version for the daemon."""
305
320
  with self._connect() as connection:
306
321
  connection.execute(
307
322
  """
308
323
  INSERT INTO watcher_status(
309
324
  singleton, instance_id, pid, process_identity,
310
- started_at, heartbeat_at
311
- ) VALUES (1, ?, ?, ?, ?, ?)
325
+ started_at, heartbeat_at, code_version
326
+ ) VALUES (1, ?, ?, ?, ?, ?, ?)
312
327
  ON CONFLICT(singleton) DO UPDATE SET
313
328
  instance_id = excluded.instance_id,
314
329
  pid = excluded.pid,
315
330
  process_identity = excluded.process_identity,
316
331
  started_at = excluded.started_at,
317
- heartbeat_at = excluded.heartbeat_at
332
+ heartbeat_at = excluded.heartbeat_at,
333
+ code_version = excluded.code_version
318
334
  """,
319
- (instance_id, pid, process_identity, started_at, _now()),
335
+ (
336
+ instance_id,
337
+ pid,
338
+ process_identity,
339
+ started_at,
340
+ _now(),
341
+ code_version,
342
+ ),
320
343
  )
321
344
 
322
345
  def watcher_status(self) -> WatcherStatus | None:
@@ -333,6 +356,7 @@ class WatchStore:
333
356
  process_identity=row["process_identity"],
334
357
  started_at=row["started_at"],
335
358
  heartbeat_at=row["heartbeat_at"],
359
+ code_version=row["code_version"],
336
360
  )
337
361
 
338
362
  @contextmanager
@@ -385,6 +409,15 @@ def _migrate_repository_cursors(connection: sqlite3.Connection) -> None:
385
409
  connection.execute("ALTER TABLE repository_cursors ADD COLUMN etag TEXT")
386
410
 
387
411
 
412
+ def _migrate_watcher_status(connection: sqlite3.Connection) -> None:
413
+ """Record which code version a watcher runs; older watchers leave NULL."""
414
+ columns = {
415
+ row["name"] for row in connection.execute("PRAGMA table_info(watcher_status)")
416
+ }
417
+ if "code_version" not in columns:
418
+ connection.execute("ALTER TABLE watcher_status ADD COLUMN code_version TEXT")
419
+
420
+
388
421
  def _now() -> str:
389
422
  """Return a stable UTC timestamp."""
390
423
  return datetime.now(UTC).isoformat(timespec="microseconds")
@@ -5,14 +5,12 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import re
8
+ import shutil
8
9
  import socket
10
+ import subprocess
9
11
  from dataclasses import dataclass
12
+ from pathlib import Path
10
13
 
11
- from claude_code_tools.codex_app_server_rpc import (
12
- deliver_thread_message,
13
- socket_path_from_endpoint,
14
- verify_thread_loaded,
15
- )
16
14
  from claude_code_tools.github_watch_store import IssueWatch
17
15
 
18
16
  _THREAD_ID = re.compile(
@@ -47,45 +45,100 @@ class DeliveryConfigurationError(RuntimeError):
47
45
  """The current agent session cannot receive durable notifications."""
48
46
 
49
47
 
50
- def codex_target_from_environment() -> DeliveryTarget:
51
- """Preflight and capture the current dynamic Codex target."""
52
- thread_id = os.environ.get("CODEX_THREAD_ID", "").strip()
53
- endpoint = os.environ.get("CCTOOLS_CODEX_CALLBACK_ENDPOINT", "").strip()
48
+ class UnsupportedDeliveryTarget(DeliveryConfigurationError):
49
+ """A stored watch names an adapter this version can never deliver to."""
50
+
51
+
52
+ def codex_target_from_environment(codex_home: Path | None = None) -> DeliveryTarget:
53
+ """Preflight and capture the current native Codex session as a queue target.
54
+
55
+ Args:
56
+ codex_home: Codex account directory; defaults to ``$CODEX_HOME`` and
57
+ then to the one ``~/.codex*`` directory holding this thread.
58
+
59
+ Returns:
60
+ A ``codex-queue-v1`` target naming the thread, account, and executable.
61
+
62
+ Raises:
63
+ DeliveryConfigurationError: This shell is not a usable Codex session.
64
+ """
65
+ thread_id = os.environ.get("CODEX_THREAD_ID", "").strip().lower()
54
66
  if not _THREAD_ID.fullmatch(thread_id):
55
67
  raise DeliveryConfigurationError(
56
- "github-wake requires a Codex tool shell launched through "
57
- "codex-dynamic"
68
+ "github-wake requires a Codex session shell (CODEX_THREAD_ID is "
69
+ "unset); in Claude Code use --claude-monitor"
58
70
  )
59
- if not endpoint:
60
- endpoint = _managed_codex_endpoint(thread_id)
71
+ home = _codex_home_for_thread(thread_id, codex_home)
72
+ found = shutil.which("codex")
73
+ if found is None:
74
+ raise DeliveryConfigurationError("cannot find the codex executable")
75
+ # The shared watcher runs elsewhere, so a relative PATH hit must not leak.
76
+ executable = os.path.abspath(found)
61
77
  try:
62
- socket_path = socket_path_from_endpoint(endpoint)
63
- verify_thread_loaded(socket_path, thread_id)
64
- except Exception as exc:
78
+ completed = subprocess.run(
79
+ [executable, "queue", "--help"],
80
+ stdin=subprocess.DEVNULL,
81
+ capture_output=True,
82
+ text=True,
83
+ timeout=30,
84
+ check=False,
85
+ env={**os.environ, "CODEX_HOME": str(home)},
86
+ )
87
+ except (OSError, subprocess.TimeoutExpired) as exc:
88
+ raise DeliveryConfigurationError(f"cannot run codex: {exc}") from exc
89
+ if (
90
+ completed.returncode != 0
91
+ or "--thread" not in completed.stdout
92
+ or "--message" not in completed.stdout
93
+ ):
65
94
  raise DeliveryConfigurationError(
66
- f"cannot arm a reply notification for this Codex thread: {exc}"
67
- ) from exc
95
+ "the installed codex does not support 'codex queue --thread "
96
+ "--message'; upgrade Codex"
97
+ )
68
98
  return DeliveryTarget(
69
- kind="codex-app-server-v1",
70
- payload={"endpoint": endpoint, "threadId": thread_id.lower()},
99
+ kind="codex-queue-v1",
100
+ payload={
101
+ "codexHome": str(home),
102
+ "executable": executable,
103
+ "threadId": thread_id,
104
+ },
71
105
  )
72
106
 
73
107
 
74
- def _managed_codex_endpoint(thread_id: str) -> str:
75
- """Find the unique managed App Server that already owns one thread."""
76
- from claude_code_tools.codex_server_resume import resume_server_paths
108
+ def _codex_home_for_thread(
109
+ thread_id: str,
110
+ codex_home: Path | None,
111
+ search_root: Path | None = None,
112
+ ) -> Path:
113
+ """Resolve the Codex account directory that owns one session thread.
77
114
 
78
- try:
79
- paths = resume_server_paths(["resume", thread_id], os.environ, ())
80
- except Exception as exc:
81
- raise DeliveryConfigurationError(
82
- f"cannot locate this thread's managed App Server: {exc}"
83
- ) from exc
84
- if paths is None:
85
- raise DeliveryConfigurationError(
86
- "this thread is not loaded by a compatible codex-dynamic App Server"
87
- )
88
- return paths.endpoint
115
+ Codex does not export CODEX_HOME to tool shells, so without an explicit
116
+ directory the unique ``.codex*`` account under ``search_root`` (default:
117
+ the home directory) holding the rollout is used.
118
+ """
119
+ root = search_root or Path.home()
120
+ explicit = codex_home or (
121
+ Path(os.environ["CODEX_HOME"]) if os.environ.get("CODEX_HOME") else None
122
+ )
123
+ candidates = (
124
+ [explicit.expanduser()]
125
+ if explicit is not None
126
+ else sorted(root.glob(".codex*"))
127
+ )
128
+ pattern = f"sessions/*/*/*/rollout-*-{thread_id}.jsonl"
129
+ owners = {
130
+ path.resolve()
131
+ for path in candidates
132
+ if path.is_dir() and any(path.glob(pattern))
133
+ }
134
+ if len(owners) == 1:
135
+ return owners.pop()
136
+ searched = ", ".join(str(path) for path in candidates) or str(root)
137
+ problem = "several account directories" if owners else "no account directory"
138
+ raise DeliveryConfigurationError(
139
+ f"{problem} among {searched} holds Codex thread {thread_id}; pass "
140
+ "--codex-home with this session's account directory"
141
+ )
89
142
 
90
143
 
91
144
  def deliver_issue_reply(watch: IssueWatch, reply: IssueReply) -> None:
@@ -93,27 +146,47 @@ def deliver_issue_reply(watch: IssueWatch, reply: IssueReply) -> None:
93
146
  if watch.target_kind == "claude-monitor-v1":
94
147
  _deliver_claude_monitor(watch, reply)
95
148
  return
96
- if watch.target_kind == "codex-app-server-v1":
97
- _deliver_codex(watch, reply)
149
+ if watch.target_kind == "codex-queue-v1":
150
+ _deliver_codex_queue(watch, reply)
98
151
  return
99
- raise DeliveryConfigurationError(
100
- f"unsupported issue-reply delivery adapter: {watch.target_kind}"
152
+ raise UnsupportedDeliveryTarget(
153
+ f"unsupported issue-reply delivery adapter: {watch.target_kind}; "
154
+ "re-arm the wakeup with github-wake"
101
155
  )
102
156
 
103
157
 
104
- def _deliver_codex(watch: IssueWatch, reply: IssueReply) -> None:
105
- """Deliver one reply through the Codex App Server."""
106
- endpoint = watch.target.get("endpoint")
107
- thread_id = watch.target.get("threadId")
108
- if not endpoint or not thread_id:
109
- raise DeliveryConfigurationError("Codex delivery target is incomplete")
110
- client_id = f"github-wake:{watch.watch_id}:{reply.comment_id}"
111
- deliver_thread_message(
112
- socket_path_from_endpoint(endpoint),
113
- thread_id,
114
- client_id,
115
- _codex_notification_message(watch, reply),
158
+ def _deliver_codex_queue(watch: IssueWatch, reply: IssueReply) -> None:
159
+ """Queue one reply on the native Codex session and require confirmation."""
160
+ target = watch.target
161
+ codex_home = target.get("codexHome")
162
+ executable = target.get("executable")
163
+ thread_id = target.get("threadId")
164
+ if not codex_home or not executable or not thread_id:
165
+ raise DeliveryConfigurationError("Codex queue target is incomplete")
166
+ completed = subprocess.run(
167
+ [
168
+ executable,
169
+ "queue",
170
+ "--thread",
171
+ thread_id,
172
+ "--message",
173
+ _codex_notification_message(watch, reply),
174
+ ],
175
+ stdin=subprocess.DEVNULL,
176
+ capture_output=True,
177
+ text=True,
178
+ timeout=60,
179
+ check=False,
180
+ env={**os.environ, "CODEX_HOME": codex_home},
181
+ )
182
+ if completed.returncode != 0:
183
+ diagnostic = " ".join(completed.stderr.split())[:1000]
184
+ raise RuntimeError(f"codex queue failed: {diagnostic}")
185
+ confirmation = re.compile(
186
+ r"Queued message [0-9a-f-]{36} for thread " + re.escape(thread_id) + r"\."
116
187
  )
188
+ if not confirmation.search(completed.stdout):
189
+ raise RuntimeError("codex queue printed no acceptance confirmation")
117
190
 
118
191
 
119
192
  def _deliver_claude_monitor(watch: IssueWatch, reply: IssueReply) -> None:
@@ -514,18 +514,6 @@ def _ensure_leading_user(
514
514
  yield msg
515
515
 
516
516
 
517
- def _new_assistant_message_id() -> str:
518
- """Generate an assistant message id in the modern Codex format.
519
-
520
- Real rollout assistant items carry ids like ``msg_<50 hex chars>``
521
- (25 random bytes, hex-encoded, behind a ``msg_`` prefix).
522
-
523
- Returns:
524
- A fresh id string matching ``msg_[0-9a-f]{50}``.
525
- """
526
- return "msg_" + secrets.token_hex(25)
527
-
528
-
529
517
  def _make_codex_item(
530
518
  msg: dict[str, Any], fallback_timestamp: str, turn_id: str
531
519
  ) -> dict[str, Any]:
@@ -534,8 +522,11 @@ def _make_codex_item(
534
522
  Shapes match real modern rollouts exactly: user messages carry
535
523
  ``input_text`` content blocks plus the turn-id metadata
536
524
  passthrough; assistant messages carry ``output_text`` content
537
- plus the modern assistant payload fields (``id``, ``phase`` and
538
- the same turn-id passthrough) that Codex itself writes.
525
+ plus the modern assistant payload fields (``phase`` and the same
526
+ turn-id passthrough) that Codex itself writes. Assistant items
527
+ deliberately omit ``id``: Codex replays item ids to the Responses
528
+ API, which then looks them up as server-persisted items, and an
529
+ id that the server never issued fails the turn with HTTP 400.
539
530
 
540
531
  Args:
541
532
  msg: Flattened message dict (role/text/timestamp).
@@ -558,7 +549,6 @@ def _make_codex_item(
558
549
  else:
559
550
  payload = {
560
551
  "type": "message",
561
- "id": _new_assistant_message_id(),
562
552
  "role": role,
563
553
  "content": [
564
554
  {"type": "output_text", "text": msg["text"]}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "1.29.4"
3
+ version = "1.29.6"
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.4"
116
+ version = "1.29.6"
117
117
  tag_format = "v$version"
118
118
  version_files = [
119
119
  "pyproject.toml:version",