agentirc-cli 6.2.0__tar.gz → 6.2.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.
Files changed (355) hide show
  1. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/CHANGELOG.md +23 -0
  2. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/PKG-INFO +1 -1
  3. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/channel.py +45 -2
  4. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/app.py +57 -19
  5. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/client.py +77 -35
  6. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/widgets/chat.py +24 -1
  7. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/observer.py +16 -3
  8. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/cli/index.md +17 -0
  9. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/pyproject.toml +1 -1
  10. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_console_client.py +99 -1
  11. agentirc_cli-6.2.2/tests/test_console_fixes_224_227.py +199 -0
  12. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/uv.lock +1 -1
  13. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.claude/skills/pr-review/SKILL.md +0 -0
  14. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.claude/skills/run-tests/SKILL.md +0 -0
  15. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.claude/skills/run-tests/scripts/test.sh +0 -0
  16. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.flake8 +0 -0
  17. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.github/workflows/docs-check.yml +0 -0
  18. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.github/workflows/publish.yml +0 -0
  19. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.github/workflows/security-checks.yml +0 -0
  20. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.github/workflows/tests.yml +0 -0
  21. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.gitignore +0 -0
  22. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.markdownlint-cli2.yaml +0 -0
  23. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.pr_agent.toml +0 -0
  24. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.pre-commit-config.yaml +0 -0
  25. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/.pylintrc +0 -0
  26. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/CLAUDE.md +0 -0
  27. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/Gemfile +0 -0
  28. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/Gemfile.lock +0 -0
  29. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/LICENSE +0 -0
  30. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/README.md +0 -0
  31. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/SECURITY.md +0 -0
  32. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_config.agentirc.yml +0 -0
  33. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_config.base.yml +0 -0
  34. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_config.culture.yml +0 -0
  35. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_data/sites.yml +0 -0
  36. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_includes/head_custom.html +0 -0
  37. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_plugins/site_filter.rb +0 -0
  38. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_sass/color_schemes/anthropic.scss +0 -0
  39. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_sass/color_schemes/dark-terminal.scss +0 -0
  40. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/_sass/custom/custom.scss +0 -0
  41. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/IMG_3183.png +0 -0
  42. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/apple-touch-icon.png +0 -0
  43. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/favicon-16x16.png +0 -0
  44. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/favicon-32x32.png +0 -0
  45. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/favicon.ico +0 -0
  46. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/og-agentirc.png +0 -0
  47. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/assets/images/og-culture.png +0 -0
  48. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/__init__.py +0 -0
  49. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/__main__.py +0 -0
  50. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/CLAUDE.md +0 -0
  51. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/__init__.py +0 -0
  52. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/__main__.py +0 -0
  53. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/channel.py +0 -0
  54. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/client.py +0 -0
  55. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/config.py +0 -0
  56. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/docs/agentirc-architecture.md +0 -0
  57. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/docs/agentirc-features.md +0 -0
  58. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/docs/agentirc-skill.md +0 -0
  59. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/docs/agentirc.md +0 -0
  60. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/history_store.py +0 -0
  61. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/ircd.py +0 -0
  62. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/remote_client.py +0 -0
  63. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/room_store.py +0 -0
  64. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/rooms_util.py +0 -0
  65. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/server_link.py +0 -0
  66. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/skill.py +0 -0
  67. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/skills/__init__.py +0 -0
  68. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/skills/history.py +0 -0
  69. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/skills/icon.py +0 -0
  70. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/skills/rooms.py +0 -0
  71. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/skills/threads.py +0 -0
  72. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/agentirc/thread_store.py +0 -0
  73. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/aio.py +0 -0
  74. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/__init__.py +0 -0
  75. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/bot.py +0 -0
  76. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/bot_manager.py +0 -0
  77. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/config.py +0 -0
  78. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/http_listener.py +0 -0
  79. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/template_engine.py +0 -0
  80. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/bots/virtual_client.py +0 -0
  81. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/__init__.py +0 -0
  82. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/agent.py +0 -0
  83. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/bot.py +0 -0
  84. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/mesh.py +0 -0
  85. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/server.py +0 -0
  86. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/__init__.py +0 -0
  87. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/constants.py +0 -0
  88. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/display.py +0 -0
  89. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/formatting.py +0 -0
  90. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/ipc.py +0 -0
  91. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/mesh.py +0 -0
  92. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/shared/process.py +0 -0
  93. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/cli/skills.py +0 -0
  94. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/__init__.py +0 -0
  95. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/__init__.py +0 -0
  96. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/agent_runner.py +0 -0
  97. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/config.py +0 -0
  98. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/culture.yaml +0 -0
  99. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/daemon.py +0 -0
  100. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/ipc.py +0 -0
  101. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/irc_transport.py +0 -0
  102. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/message_buffer.py +0 -0
  103. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/skill/SKILL.md +0 -0
  104. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/skill/__init__.py +0 -0
  105. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/skill/irc_client.py +0 -0
  106. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/socket_server.py +0 -0
  107. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/supervisor.py +0 -0
  108. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/acp/webhook.py +0 -0
  109. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/__init__.py +0 -0
  110. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/__main__.py +0 -0
  111. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/agent_runner.py +0 -0
  112. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/config.py +0 -0
  113. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/culture.yaml +0 -0
  114. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/daemon.py +0 -0
  115. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/ipc.py +0 -0
  116. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/irc_transport.py +0 -0
  117. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/message_buffer.py +0 -0
  118. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/skill/SKILL.md +0 -0
  119. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/skill/__init__.py +0 -0
  120. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/skill/irc_client.py +0 -0
  121. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/socket_server.py +0 -0
  122. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/supervisor.py +0 -0
  123. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/claude/webhook.py +0 -0
  124. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/__init__.py +0 -0
  125. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/agent_runner.py +0 -0
  126. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/config.py +0 -0
  127. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/culture.yaml +0 -0
  128. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/daemon.py +0 -0
  129. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/ipc.py +0 -0
  130. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/irc_transport.py +0 -0
  131. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/message_buffer.py +0 -0
  132. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/skill/SKILL.md +0 -0
  133. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/skill/__init__.py +0 -0
  134. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/skill/irc_client.py +0 -0
  135. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/socket_server.py +0 -0
  136. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/supervisor.py +0 -0
  137. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/codex/webhook.py +0 -0
  138. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/__init__.py +0 -0
  139. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/agent_runner.py +0 -0
  140. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/config.py +0 -0
  141. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/culture.yaml +0 -0
  142. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/daemon.py +0 -0
  143. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/ipc.py +0 -0
  144. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/irc_transport.py +0 -0
  145. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/message_buffer.py +0 -0
  146. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/skill/SKILL.md +0 -0
  147. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/skill/__init__.py +0 -0
  148. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/skill/irc_client.py +0 -0
  149. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/socket_server.py +0 -0
  150. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/supervisor.py +0 -0
  151. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/clients/copilot/webhook.py +0 -0
  152. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/config.py +0 -0
  153. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/__init__.py +0 -0
  154. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/commands.py +0 -0
  155. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/status.py +0 -0
  156. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/widgets/__init__.py +0 -0
  157. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/widgets/info_panel.py +0 -0
  158. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/console/widgets/sidebar.py +0 -0
  159. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/credentials.py +0 -0
  160. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/formatting.py +0 -0
  161. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/learn_prompt.py +0 -0
  162. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/mesh_config.py +0 -0
  163. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/overview/__init__.py +0 -0
  164. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/overview/collector.py +0 -0
  165. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/overview/model.py +0 -0
  166. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/overview/renderer_text.py +0 -0
  167. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/overview/renderer_web.py +0 -0
  168. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/overview/web/style.css +0 -0
  169. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/persistence.py +0 -0
  170. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/pidfile.py +0 -0
  171. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/__init__.py +0 -0
  172. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/commands.py +0 -0
  173. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/extensions/federation.md +0 -0
  174. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/extensions/history.md +0 -0
  175. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/extensions/icons.md +0 -0
  176. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/extensions/rooms.md +0 -0
  177. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/extensions/tags.md +0 -0
  178. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/extensions/threads.md +0 -0
  179. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/message.py +0 -0
  180. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/protocol-index.md +0 -0
  181. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/protocol/replies.py +0 -0
  182. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/culture/skills/culture/SKILL.md +0 -0
  183. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/README.md +0 -0
  184. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/agentirc/architecture-overview.md +0 -0
  185. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/agentirc/index.md +0 -0
  186. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/agentirc/why-agentirc.md +0 -0
  187. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/agent-lifecycle.md +0 -0
  188. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/choose-a-harness.md +0 -0
  189. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/index.md +0 -0
  190. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/mental-model.md +0 -0
  191. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/operate.md +0 -0
  192. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/patterns.md +0 -0
  193. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/quickstart.md +0 -0
  194. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/reflective-development.md +0 -0
  195. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/vision-patterns-index.md +0 -0
  196. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/vision.md +0 -0
  197. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/culture/why-culture.md +0 -0
  198. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/architecture/agent-harness-spec.md +0 -0
  199. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/architecture/index.md +0 -0
  200. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/architecture/layers.md +0 -0
  201. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/architecture/threads.md +0 -0
  202. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/cli/commands.md +0 -0
  203. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/harnesses/acp.md +0 -0
  204. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/harnesses/claude.md +0 -0
  205. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/harnesses/codex.md +0 -0
  206. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/harnesses/copilot.md +0 -0
  207. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/harnesses/index.md +0 -0
  208. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/index.md +0 -0
  209. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/server/architecture.md +0 -0
  210. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/server/config.md +0 -0
  211. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/server/deployment.md +0 -0
  212. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/server/index.md +0 -0
  213. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/reference/server/security.md +0 -0
  214. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  215. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/concepts/federation.md +0 -0
  216. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/concepts/harnesses.md +0 -0
  217. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/concepts/humans-and-agents.md +0 -0
  218. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/concepts/index.md +0 -0
  219. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/concepts/persistence.md +0 -0
  220. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/concepts/rooms.md +0 -0
  221. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/demos/magic-demo.md +0 -0
  222. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/guides/first-session.md +0 -0
  223. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/guides/index.md +0 -0
  224. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/guides/join-as-human.md +0 -0
  225. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/guides/local-setup.md +0 -0
  226. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/guides/multi-machine.md +0 -0
  227. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/01-pair-programming.md +0 -0
  228. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
  229. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
  230. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
  231. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/05-the-observer.md +0 -0
  232. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
  233. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
  234. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
  235. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/09-research-swarm.md +0 -0
  236. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
  237. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/shared/use-cases-index.md +0 -0
  238. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  239. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  240. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  241. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  242. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
  243. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
  244. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
  245. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
  246. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
  247. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
  248. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
  249. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  250. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  251. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  252. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  253. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
  254. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
  255. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
  256. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
  257. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
  258. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
  259. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
  260. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
  261. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
  262. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
  263. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
  264. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
  265. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
  266. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/favicon.ico +0 -0
  267. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/README.md +0 -0
  268. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/config.py +0 -0
  269. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/culture.yaml +0 -0
  270. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/daemon.py +0 -0
  271. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/ipc.py +0 -0
  272. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/irc_transport.py +0 -0
  273. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/message_buffer.py +0 -0
  274. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/skill/SKILL.md +0 -0
  275. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/skill/irc_client.py +0 -0
  276. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/socket_server.py +0 -0
  277. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/packages/agent-harness/webhook.py +0 -0
  278. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  279. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/plugins/claude-code/skills/culture/SKILL.md +0 -0
  280. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  281. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
  282. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/robots.txt +0 -0
  283. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/sonar-project.properties +0 -0
  284. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/__init__.py +0 -0
  285. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/conftest.py +0 -0
  286. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_acp_daemon.py +0 -0
  287. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_agent_runner.py +0 -0
  288. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_archive.py +0 -0
  289. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_bot.py +0 -0
  290. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_bot_config.py +0 -0
  291. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_bot_manager.py +0 -0
  292. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_bots_integration.py +0 -0
  293. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_channel.py +0 -0
  294. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_channel_cli.py +0 -0
  295. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_codex_daemon.py +0 -0
  296. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_connection.py +0 -0
  297. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_console_commands.py +0 -0
  298. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_console_connection.py +0 -0
  299. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_console_icons.py +0 -0
  300. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_console_integration.py +0 -0
  301. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_console_status.py +0 -0
  302. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_copilot_daemon.py +0 -0
  303. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_credentials.py +0 -0
  304. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_culture_config.py +0 -0
  305. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_daemon.py +0 -0
  306. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_daemon_config.py +0 -0
  307. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_daemon_ipc.py +0 -0
  308. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_discovery.py +0 -0
  309. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_display.py +0 -0
  310. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_federation.py +0 -0
  311. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_history.py +0 -0
  312. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_http_listener.py +0 -0
  313. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_integration_layer5.py +0 -0
  314. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_ipc.py +0 -0
  315. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_irc_transport.py +0 -0
  316. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_learn_prompt.py +0 -0
  317. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_link_reconnect.py +0 -0
  318. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_manifest_config.py +0 -0
  319. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_mention_alias.py +0 -0
  320. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_mention_target_cleanup.py +0 -0
  321. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_mention_warning.py +0 -0
  322. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_mentions.py +0 -0
  323. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_mesh_config.py +0 -0
  324. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_mesh_readiness.py +0 -0
  325. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_message.py +0 -0
  326. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_message_buffer.py +0 -0
  327. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_messaging.py +0 -0
  328. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_migrate_cli.py +0 -0
  329. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_modes.py +0 -0
  330. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_overview_cli.py +0 -0
  331. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_overview_collector.py +0 -0
  332. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_overview_model.py +0 -0
  333. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_overview_renderer.py +0 -0
  334. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_overview_web.py +0 -0
  335. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_persistence.py +0 -0
  336. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_pidfile.py +0 -0
  337. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_poll_loop.py +0 -0
  338. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_register_cli.py +0 -0
  339. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_room_persistence.py +0 -0
  340. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_rooms.py +0 -0
  341. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_rooms_federation.py +0 -0
  342. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_rooms_integration.py +0 -0
  343. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_server_icon_skill.py +0 -0
  344. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_setup_update_cli.py +0 -0
  345. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_skill_client.py +0 -0
  346. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_skill_docs.py +0 -0
  347. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_skills.py +0 -0
  348. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_socket_server.py +0 -0
  349. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_supervisor.py +0 -0
  350. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_template_engine.py +0 -0
  351. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_thread_buffer.py +0 -0
  352. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_threads.py +0 -0
  353. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_virtual_client.py +0 -0
  354. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_wait_for_port.py +0 -0
  355. {agentirc_cli-6.2.0 → agentirc_cli-6.2.2}/tests/test_webhook.py +0 -0
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [6.2.2] - 2026-04-14
8
+
9
+
10
+ ### Fixed
11
+
12
+ - console: handle BrokenPipeError/ConnectionResetError in _send_raw; surface a red system notice in the chat panel instead of letting the asyncio task crash (#230)
13
+
14
+ ## [6.2.1] - 2026-04-13
15
+
16
+
17
+ ### Added
18
+
19
+ - Copy-paste guidance in help screen (Shift+drag bypasses TUI mouse capture in modern terminals)
20
+
21
+
22
+ ### Fixed
23
+
24
+ - #227: Tab now cycles channels (added priority=True to override Textual Screen focus-cycling)
25
+ - #226: Alt+Left/Right jump by word in chat input; Alt+Backspace deletes previous word
26
+ - #225: `culture channel message` interprets literal \n, \t, and \\ (escape-an-escape); observer splits multi-line text into one PRIVMSG per line and rejects all-empty-after-interpretation input with a non-zero exit
27
+ - #224: Exiting overview now reloads the current channel history (was empty)
28
+ - Help screen now opens on F1 (Ctrl+H stays as secondary — most terminals forward it as Backspace)
29
+
7
30
  ## [6.2.0] - 2026-04-12
8
31
 
9
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 6.2.0
3
+ Version: 6.2.2
4
4
  Summary: Legacy alias for culture — install culture instead
5
5
  Project-URL: Homepage, https://github.com/OriNachum/culture
6
6
  Author: Ori Nachum
@@ -208,6 +208,40 @@ def _cmd_read(args: argparse.Namespace) -> None:
208
208
  print(msg)
209
209
 
210
210
 
211
+ def _interpret_escapes(text: str) -> str:
212
+ """Convert shell-literal ``\\n`` / ``\\t`` / ``\\\\`` sequences to real chars.
213
+
214
+ Walks the string left-to-right so a preceding backslash escapes the next
215
+ character — ``\\\\n`` stays as the two chars ``\\`` + ``n``, while ``\\n``
216
+ becomes a real newline. Supported escapes: ``\\n`` → newline, ``\\t`` →
217
+ tab, ``\\\\`` → single backslash. Any other ``\\x`` pair is passed through
218
+ unchanged so we don't surprise users with ``\\x..`` / ``\\u....`` style
219
+ interpretation that ``codecs.decode(..., "unicode_escape")`` would do.
220
+ """
221
+ out: list[str] = []
222
+ i = 0
223
+ n = len(text)
224
+ while i < n:
225
+ ch = text[i]
226
+ if ch == "\\" and i + 1 < n:
227
+ nxt = text[i + 1]
228
+ if nxt == "n":
229
+ out.append("\n")
230
+ i += 2
231
+ continue
232
+ if nxt == "t":
233
+ out.append("\t")
234
+ i += 2
235
+ continue
236
+ if nxt == "\\":
237
+ out.append("\\")
238
+ i += 2
239
+ continue
240
+ out.append(ch)
241
+ i += 1
242
+ return "".join(out)
243
+
244
+
211
245
  def _cmd_message(args: argparse.Namespace) -> None:
212
246
  if not args.target.strip():
213
247
  print("Error: channel name cannot be empty", file=sys.stderr)
@@ -216,14 +250,23 @@ def _cmd_message(args: argparse.Namespace) -> None:
216
250
  print("Error: message text cannot be empty", file=sys.stderr)
217
251
  sys.exit(1)
218
252
  target = args.target if args.target.startswith("#") else f"#{args.target}"
253
+ text = _interpret_escapes(args.text)
254
+
255
+ # After escape interpretation, reject input that has no non-empty line —
256
+ # otherwise we'd print "Sent to ..." while nothing actually goes out.
257
+ if not any(line.strip() for line in text.split("\n")):
258
+ print(
259
+ "Error: message text has no non-empty line after escape interpretation", file=sys.stderr
260
+ )
261
+ sys.exit(1)
219
262
 
220
- resp = _try_ipc("irc_send", channel=target, message=args.text)
263
+ resp = _try_ipc("irc_send", channel=target, message=text)
221
264
  if resp and resp.get("ok"):
222
265
  print(f"Sent to {target}")
223
266
  return
224
267
 
225
268
  observer = get_observer(args.config)
226
- asyncio.run(observer.send_message(target, args.text))
269
+ asyncio.run(observer.send_message(target, text))
227
270
  print(f"Sent to {target}")
228
271
 
229
272
 
@@ -13,7 +13,7 @@ from textual.containers import Horizontal
13
13
  from textual.widgets import Footer, Header
14
14
 
15
15
  from culture.aio import maybe_await
16
- from culture.console.client import ConsoleIRCClient
16
+ from culture.console.client import ConsoleConnectionLost, ConsoleIRCClient
17
17
  from culture.console.commands import CommandType, parse_command
18
18
  from culture.console.status import query_all_agents
19
19
  from culture.console.widgets.chat import ChatPanel
@@ -35,11 +35,15 @@ class ConsoleApp(App):
35
35
  BINDINGS = [
36
36
  Binding("ctrl+o", "show_overview", "Overview", show=True),
37
37
  Binding("ctrl+s", "show_status", "Status", show=True),
38
- Binding("ctrl+h", "show_help", "Help", show=True),
38
+ Binding("f1", "show_help", "Help", show=True),
39
+ # Most terminals send 0x08 (backspace) for Ctrl+H, so this secondary
40
+ # bind only fires under terminals with modifyOtherKeys enabled.
41
+ Binding("ctrl+h", "show_help", "Help", show=False),
39
42
  Binding("escape", "back_to_chat", "Chat", show=True),
40
43
  Binding("ctrl+q", "quit_app", "Quit", show=True),
41
- Binding("tab", "next_channel", "Next channel", show=False),
42
- Binding("shift+tab", "prev_channel", "Prev channel", show=False),
44
+ # priority=True so Tab wins against Screen's default focus-cycling.
45
+ Binding("tab", "next_channel", "Next channel", show=False, priority=True),
46
+ Binding("shift+tab", "prev_channel", "Prev channel", show=False, priority=True),
43
47
  ]
44
48
 
45
49
  DEFAULT_CSS = """
@@ -70,6 +74,10 @@ class ConsoleApp(App):
70
74
  self._background_tasks: set[asyncio.Task] = set()
71
75
  self._status_poll_task: asyncio.Task | None = None
72
76
 
77
+ # Once the connection drops, show the "connection lost" notice exactly
78
+ # once — subsequent failing commands/channel-switches stay quiet.
79
+ self._connection_lost_notified: bool = False
80
+
73
81
  # Dispatch table for command execution
74
82
  self._command_handlers: dict[CommandType, Any] = {
75
83
  CommandType.CHAT: self._handle_chat,
@@ -201,13 +209,29 @@ class ConsoleApp(App):
201
209
  async def _execute_command(self, cmd) -> None: # noqa: ANN001
202
210
  """Dispatch a ParsedCommand to the appropriate handler."""
203
211
  handler = self._command_handlers.get(cmd.type)
204
- if handler:
205
- await maybe_await(handler(cmd))
206
- elif cmd.type in (CommandType.START, CommandType.STOP, CommandType.RESTART):
207
- self._handle_agent_management(cmd)
208
- elif cmd.type == CommandType.UNKNOWN:
209
- chat: ChatPanel = self.query_one(ChatPanel)
210
- chat.add_message(time.time(), "", "system", f"[red]Unknown command: {cmd.text}[/]")
212
+ try:
213
+ if handler:
214
+ await maybe_await(handler(cmd))
215
+ elif cmd.type in (CommandType.START, CommandType.STOP, CommandType.RESTART):
216
+ self._handle_agent_management(cmd)
217
+ elif cmd.type == CommandType.UNKNOWN:
218
+ chat: ChatPanel = self.query_one(ChatPanel)
219
+ chat.add_message(time.time(), "", "system", f"[red]Unknown command: {cmd.text}[/]")
220
+ except ConsoleConnectionLost:
221
+ self._notify_connection_lost()
222
+
223
+ def _notify_connection_lost(self) -> None:
224
+ """Post the 'connection lost' notice once per disconnect."""
225
+ if self._connection_lost_notified:
226
+ return
227
+ self._connection_lost_notified = True
228
+ chat: ChatPanel = self.query_one(ChatPanel)
229
+ chat.add_message(
230
+ time.time(),
231
+ "",
232
+ "system",
233
+ "[red]Connection to server lost. Restart the console to reconnect.[/]",
234
+ )
211
235
 
212
236
  # ------------------------------------------------------------------
213
237
  # Command handlers
@@ -413,11 +437,19 @@ class ConsoleApp(App):
413
437
  "[bold $warning]KEYBINDINGS[/]",
414
438
  "",
415
439
  " [bold]Tab / Shift+Tab[/] Cycle channels",
440
+ " [bold]Alt+←/→[/] Jump by word in input",
441
+ " [bold]Alt+Backspace[/] Delete previous word",
416
442
  " [bold]Ctrl+O[/] Overview",
417
443
  " [bold]Ctrl+S[/] Status",
418
- " [bold]Ctrl+H[/] Help",
444
+ " [bold]F1[/] Help (Ctrl+H on terminals that forward it)",
419
445
  " [bold]Escape[/] Back to chat",
420
446
  " [bold]Ctrl+Q[/] Quit",
447
+ "",
448
+ "[bold $warning]COPY-PASTE[/]",
449
+ "",
450
+ " Hold [bold]Shift[/] while dragging with the mouse to select text for copy-paste.",
451
+ " Most modern terminals (iTerm2, Kitty, Alacritty, WezTerm, GNOME Terminal,",
452
+ " Windows Terminal) let Shift bypass the TUI's mouse capture.",
421
453
  ]
422
454
  chat.set_content("Help", lines)
423
455
 
@@ -567,15 +599,17 @@ class ConsoleApp(App):
567
599
  ]
568
600
  sidebar.entities = entity_items
569
601
 
570
- def action_back_to_chat(self) -> None:
571
- """Return to the normal chat view."""
602
+ async def action_back_to_chat(self) -> None:
603
+ """Return to the normal chat view, reloading current channel history."""
572
604
  if self._current_view == "chat":
573
605
  return
606
+ if self._current_channel:
607
+ # Delegate: resets view, shows input, and reloads recent history —
608
+ # equivalent to running /read on the current channel.
609
+ await self._switch_to_channel(self._current_channel)
610
+ return
611
+ # No channel yet — just restore chat view and show the input.
574
612
  self._current_view = "chat"
575
- chat: ChatPanel = self.query_one(ChatPanel)
576
- chat.set_channel(self._current_channel)
577
-
578
- # Re-show input
579
613
  try:
580
614
  input_widget = self.query_one(self._CHAT_INPUT_ID)
581
615
  input_widget.display = True
@@ -675,7 +709,11 @@ class ConsoleApp(App):
675
709
  pass
676
710
 
677
711
  # Fetch recent history
678
- entries = await self._client.history(channel, limit=20)
712
+ try:
713
+ entries = await self._client.history(channel, limit=20)
714
+ except ConsoleConnectionLost:
715
+ self._notify_connection_lost()
716
+ return
679
717
  # Stale check: if user switched away during fetch, discard results
680
718
  if self._current_channel != channel:
681
719
  return
@@ -24,6 +24,10 @@ QUERY_TIMEOUT = 10.0
24
24
  REGISTER_TIMEOUT = 15.0
25
25
 
26
26
 
27
+ class ConsoleConnectionLost(ConnectionError):
28
+ """Raised by ConsoleIRCClient when the underlying socket is broken mid-send."""
29
+
30
+
27
31
  @dataclass
28
32
  class ChatMessage:
29
33
  """A buffered chat message from a channel or DM."""
@@ -100,35 +104,49 @@ class ConsoleIRCClient:
100
104
  timeout=REGISTER_TIMEOUT,
101
105
  )
102
106
 
103
- await self._send_raw(f"NICK {self.nick}")
104
- await self._send_raw(f"USER {self.nick} 0 * :{self.nick}")
105
-
106
- # Wait for RPL_WELCOME (001) before proceeding
107
- welcome_future: asyncio.Future[Message] = asyncio.get_running_loop().create_future()
108
- self._pending["001"] = welcome_future
107
+ try:
108
+ await self._send_raw(f"NICK {self.nick}")
109
+ await self._send_raw(f"USER {self.nick} 0 * :{self.nick}")
109
110
 
110
- # Start the read loop so the future can be resolved
111
- self._read_task = asyncio.create_task(self._read_loop())
111
+ # Wait for RPL_WELCOME (001) before proceeding
112
+ welcome_future: asyncio.Future[Message] = asyncio.get_running_loop().create_future()
113
+ self._pending["001"] = welcome_future
112
114
 
113
- try:
114
- await asyncio.wait_for(welcome_future, timeout=REGISTER_TIMEOUT)
115
- except asyncio.TimeoutError:
116
- self._pending.clear()
117
- if self._read_task:
118
- self._read_task.cancel()
119
- if self._writer:
120
- self._writer.close()
121
- self._writer = None
122
- self._reader = None
123
- raise ConnectionError("Timed out waiting for server welcome (001)")
115
+ # Start the read loop so the future can be resolved
116
+ self._read_task = asyncio.create_task(self._read_loop())
124
117
 
125
- # Set user mode
126
- if self.mode:
127
- await self._send_raw(f"MODE {self.nick} +{self.mode}")
118
+ try:
119
+ await asyncio.wait_for(welcome_future, timeout=REGISTER_TIMEOUT)
120
+ except asyncio.TimeoutError as e:
121
+ raise ConnectionError("Timed out waiting for server welcome (001)") from e
122
+
123
+ # Set user mode
124
+ if self.mode:
125
+ await self._send_raw(f"MODE {self.nick} +{self.mode}")
126
+
127
+ # Send ICON if provided
128
+ if self.icon:
129
+ await self._send_raw(f"ICON {self.icon}")
130
+ except BaseException:
131
+ # Any failure after open_connection: tear down the half-open state.
132
+ await self._teardown_connection()
133
+ raise
128
134
 
129
- # Send ICON if provided
130
- if self.icon:
131
- await self._send_raw(f"ICON {self.icon}")
135
+ async def _teardown_connection(self) -> None:
136
+ """Close writer, cancel reader, clear pending futures. Idempotent."""
137
+ self._pending.clear()
138
+ if self._read_task:
139
+ self._read_task.cancel()
140
+ await asyncio.gather(self._read_task, return_exceptions=True)
141
+ self._read_task = None
142
+ if self._writer:
143
+ try:
144
+ self._writer.close()
145
+ await self._writer.wait_closed()
146
+ except OSError:
147
+ pass
148
+ self._writer = None
149
+ self._reader = None
132
150
 
133
151
  async def disconnect(self) -> None:
134
152
  """Send QUIT and close the connection."""
@@ -186,18 +204,24 @@ class ConsoleIRCClient:
186
204
  Returns a sorted list of channel names.
187
205
  """
188
206
  key = "LIST"
207
+ pending_key = "323"
189
208
  self._collect_buffers[key] = []
190
209
  end_future: asyncio.Future[None] = asyncio.get_running_loop().create_future()
191
- self._pending["323"] = end_future
210
+ self._pending[pending_key] = end_future
192
211
 
193
- await self._send_raw("LIST")
212
+ try:
213
+ await self._send_raw("LIST")
214
+ except ConsoleConnectionLost:
215
+ self._pending.pop(pending_key, None)
216
+ self._collect_buffers.pop(key, None)
217
+ raise
194
218
 
195
219
  try:
196
220
  await asyncio.wait_for(end_future, timeout=QUERY_TIMEOUT)
197
221
  except asyncio.TimeoutError:
198
222
  pass
199
223
  finally:
200
- self._pending.pop("323", None)
224
+ self._pending.pop(pending_key, None)
201
225
 
202
226
  channels = self._collect_buffers.pop(key, [])
203
227
  return sorted(channels)
@@ -208,18 +232,24 @@ class ConsoleIRCClient:
208
232
  Returns a list of dicts with nick, user, host, server, flags, realname.
209
233
  """
210
234
  key = f"WHO {target}"
235
+ pending_key = f"315:{target}"
211
236
  self._collect_buffers[key] = []
212
237
  end_future: asyncio.Future[None] = asyncio.get_running_loop().create_future()
213
- self._pending[f"315:{target}"] = end_future
238
+ self._pending[pending_key] = end_future
214
239
 
215
- await self._send_raw(f"WHO {target}")
240
+ try:
241
+ await self._send_raw(f"WHO {target}")
242
+ except ConsoleConnectionLost:
243
+ self._pending.pop(pending_key, None)
244
+ self._collect_buffers.pop(key, None)
245
+ raise
216
246
 
217
247
  try:
218
248
  await asyncio.wait_for(end_future, timeout=QUERY_TIMEOUT)
219
249
  except asyncio.TimeoutError:
220
250
  pass
221
251
  finally:
222
- self._pending.pop(f"315:{target}", None)
252
+ self._pending.pop(pending_key, None)
223
253
 
224
254
  entries = self._collect_buffers.pop(key, [])
225
255
  return entries
@@ -230,18 +260,24 @@ class ConsoleIRCClient:
230
260
  Returns a list of dicts with channel, nick, timestamp, text.
231
261
  """
232
262
  key = f"HISTORY {channel}"
263
+ pending_key = f"HISTORYEND:{channel}"
233
264
  self._collect_buffers[key] = []
234
265
  end_future: asyncio.Future[None] = asyncio.get_running_loop().create_future()
235
- self._pending[f"HISTORYEND:{channel}"] = end_future
266
+ self._pending[pending_key] = end_future
236
267
 
237
- await self._send_raw(f"HISTORY RECENT {channel} {limit}")
268
+ try:
269
+ await self._send_raw(f"HISTORY RECENT {channel} {limit}")
270
+ except ConsoleConnectionLost:
271
+ self._pending.pop(pending_key, None)
272
+ self._collect_buffers.pop(key, None)
273
+ raise
238
274
 
239
275
  try:
240
276
  await asyncio.wait_for(end_future, timeout=QUERY_TIMEOUT)
241
277
  except asyncio.TimeoutError:
242
278
  pass
243
279
  finally:
244
- self._pending.pop(f"HISTORYEND:{channel}", None)
280
+ self._pending.pop(pending_key, None)
245
281
 
246
282
  entries = self._collect_buffers.pop(key, [])
247
283
  return entries
@@ -252,9 +288,15 @@ class ConsoleIRCClient:
252
288
 
253
289
  async def _send_raw(self, line: str) -> None:
254
290
  """Write a raw IRC line to the socket."""
255
- if self._writer:
291
+ if not self._writer:
292
+ return
293
+ try:
256
294
  self._writer.write(f"{line}\r\n".encode())
257
295
  await self._writer.drain()
296
+ except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError) as e:
297
+ self.connected = False
298
+ logger.warning("ConsoleIRCClient: send failed (%s)", e.__class__.__name__)
299
+ raise ConsoleConnectionLost(str(e)) from e
258
300
 
259
301
  async def _read_loop(self) -> None:
260
302
  """Background task: read lines from socket and dispatch to _handle."""
@@ -5,12 +5,35 @@ from __future__ import annotations
5
5
  from datetime import datetime
6
6
 
7
7
  from textual.app import ComposeResult
8
+ from textual.binding import Binding
8
9
  from textual.containers import Vertical
9
10
  from textual.message import Message
10
11
  from textual.widget import Widget
11
12
  from textual.widgets import Input, RichLog, Static
12
13
 
13
14
 
15
+ class ChatInput(Input):
16
+ """Input with Alt+Arrow word-jump and Alt+Backspace word-delete."""
17
+
18
+ BINDINGS = [
19
+ Binding("alt+left", "cursor_left_word", "Word left", show=False),
20
+ Binding("alt+right", "cursor_right_word", "Word right", show=False),
21
+ Binding(
22
+ "alt+shift+left",
23
+ "cursor_left_word(True)",
24
+ "Select word left",
25
+ show=False,
26
+ ),
27
+ Binding(
28
+ "alt+shift+right",
29
+ "cursor_right_word(True)",
30
+ "Select word right",
31
+ show=False,
32
+ ),
33
+ Binding("alt+backspace", "delete_left_word", "Delete word", show=False),
34
+ ]
35
+
36
+
14
37
  class ChatPanel(Widget):
15
38
  """Center panel showing the message log and an input field.
16
39
 
@@ -75,7 +98,7 @@ class ChatPanel(Widget):
75
98
  yield Static("", id="chat-header")
76
99
  with Vertical():
77
100
  yield RichLog(id="chat-log", markup=True, wrap=True, highlight=False)
78
- yield Input(placeholder="Type a message or /command…", id="chat-input")
101
+ yield ChatInput(placeholder="Type a message or /command…", id="chat-input")
79
102
 
80
103
  def on_mount(self) -> None:
81
104
  self._channel = ""
@@ -205,11 +205,23 @@ class IRCObserver:
205
205
  async def send_message(self, target: str, text: str) -> None:
206
206
  """Send a PRIVMSG to a channel or nick, then disconnect.
207
207
 
208
+ ``text`` is split on real ``\\n`` bytes into one PRIVMSG per line,
209
+ since an IRC PRIVMSG must be single-line per RFC 2812. Empty lines
210
+ (and any embedded ``\\r``) are dropped — IRC can't carry an empty
211
+ PRIVMSG body, and this keeps multi-line output from emitting no-op
212
+ frames. If every line is empty, the method returns without
213
+ connecting.
214
+
208
215
  Uses the same ephemeral connection pattern as the read commands.
209
216
  """
210
- # Sanitize CR/LF to prevent IRC command injection
217
+ # Strip CR and LF from the target to prevent IRC command injection
218
+ # (a newline in the target would let an attacker smuggle a second
219
+ # protocol line).
211
220
  target = target.replace("\r", "").replace("\n", "")
212
- text = text.replace("\r", "").replace("\n", " ")
221
+ # Split on real newlines; drop empty lines and strip CRs
222
+ lines = [ln for ln in text.replace("\r", "").split("\n") if ln]
223
+ if not lines:
224
+ return
213
225
 
214
226
  reader, writer, nick = await self._connect_and_register()
215
227
  try:
@@ -220,7 +232,8 @@ class IRCObserver:
220
232
  # Drain join responses
221
233
  await self._recv_lines(reader, timeout=1.0)
222
234
 
223
- writer.write(f"PRIVMSG {target} :{text}\r\n".encode())
235
+ for line in lines:
236
+ writer.write(f"PRIVMSG {target} :{line}\r\n".encode())
224
237
  await writer.drain()
225
238
  finally:
226
239
  await self._disconnect(writer)
@@ -244,6 +244,23 @@ culture channel message "#general" "hello from the CLI"
244
244
 
245
245
  Uses an ephemeral IRC connection — no daemon required.
246
246
 
247
+ **Multi-line messages.** The message text interprets `\n` as a newline and
248
+ `\t` as a tab, so the shell can pass multi-line input without needing
249
+ `$'...'` quoting. Each line is sent as a separate IRC `PRIVMSG` (required by
250
+ RFC 2812 — a single `PRIVMSG` can't span lines). Empty lines are dropped.
251
+
252
+ ```bash
253
+ culture channel message "#general" "line one\nline two\nline three"
254
+ # → three separate PRIVMSG lines on the channel
255
+ ```
256
+
257
+ To send a literal backslash-n (two characters) escape the backslash:
258
+
259
+ ```bash
260
+ culture channel message "#general" "use \\n in your string"
261
+ # → sends the text: use \n in your string
262
+ ```
263
+
247
264
  ### `culture agent message`
248
265
 
249
266
  Send a message directly to an agent.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentirc-cli"
3
- version = "6.2.0"
3
+ version = "6.2.2"
4
4
  description = "Legacy alias for culture — install culture instead"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,7 +10,7 @@ import asyncio
10
10
 
11
11
  import pytest
12
12
 
13
- from culture.console.client import ChatMessage, ConsoleIRCClient
13
+ from culture.console.client import ChatMessage, ConsoleConnectionLost, ConsoleIRCClient
14
14
 
15
15
  # ---------------------------------------------------------------------------
16
16
  # Helpers
@@ -296,3 +296,101 @@ async def test_history_returns_messages(server, make_client):
296
296
  assert any(e.get("text") == "history test message" for e in entries)
297
297
 
298
298
  await client.disconnect()
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Broken-pipe handling (issue #230)
303
+ # ---------------------------------------------------------------------------
304
+
305
+
306
+ @pytest.mark.asyncio
307
+ async def test_send_raw_raises_console_connection_lost_when_socket_broken(server):
308
+ """Writes to a broken socket surface as ConsoleConnectionLost, not BrokenPipeError.
309
+
310
+ Reproduces issue #230: iTerm / idle disconnects caused asyncio's
311
+ `StreamWriter.drain()` to raise `BrokenPipeError` which was never caught,
312
+ crashing the console command task. The fix wraps the write in
313
+ `_send_raw` and re-raises a typed `ConsoleConnectionLost`.
314
+ """
315
+ nick = "testserv-pipetest"
316
+ client = make_console_client(server, nick=nick)
317
+ await client.connect()
318
+ assert client.connected is True
319
+
320
+ # Force-close the server side of this client's socket. The server
321
+ # retains the asyncio StreamWriter on its Client object; closing it
322
+ # sends FIN to the console client and the next drain() there fails.
323
+ server_client = server.clients[nick]
324
+ server_client.writer.close()
325
+ try:
326
+ await server_client.writer.wait_closed()
327
+ except OSError:
328
+ pass
329
+
330
+ # Repeated writes are required — the first drain() often succeeds because
331
+ # data lands in the local kernel buffer before the RST is observed.
332
+ with pytest.raises(ConsoleConnectionLost):
333
+ for _ in range(20):
334
+ await client.send_privmsg("#nowhere", "x" * 512)
335
+ await asyncio.sleep(0.02)
336
+
337
+ assert client.connected is False
338
+ await client.disconnect()
339
+
340
+
341
+ @pytest.mark.asyncio
342
+ async def test_history_cleans_up_pending_buffers_on_connection_lost(server):
343
+ """A failed history() send must not leak _pending / _collect_buffers entries."""
344
+ nick = "testserv-histleak"
345
+ client = make_console_client(server, nick=nick)
346
+ await client.connect()
347
+
348
+ # Break the socket from the server side.
349
+ server.clients[nick].writer.close()
350
+ try:
351
+ await server.clients[nick].writer.wait_closed()
352
+ except OSError:
353
+ pass
354
+
355
+ # Drain writes until ConsoleConnectionLost is raised by history().
356
+ raised = False
357
+ for _ in range(20):
358
+ try:
359
+ await client.history("#ghost", limit=5)
360
+ except ConsoleConnectionLost:
361
+ raised = True
362
+ break
363
+ await asyncio.sleep(0.02)
364
+
365
+ assert raised, "history() should eventually raise ConsoleConnectionLost"
366
+ # No stale state left behind — would otherwise hang future queries / leak memory.
367
+ assert "HISTORYEND:#ghost" not in client._pending
368
+ assert "HISTORY #ghost" not in client._collect_buffers
369
+ await client.disconnect()
370
+
371
+
372
+ @pytest.mark.asyncio
373
+ async def test_connect_cleans_up_on_registration_failure(monkeypatch, server):
374
+ """A ConsoleConnectionLost mid-registration must close the writer, not leak it."""
375
+ client = make_console_client(server, nick="testserv-leaktest")
376
+
377
+ # Simulate a server drop between open_connection and the first NICK write.
378
+ original_send_raw = client._send_raw
379
+ calls = {"n": 0}
380
+
381
+ async def failing_send_raw(line: str) -> None:
382
+ calls["n"] += 1
383
+ if calls["n"] == 1:
384
+ raise ConsoleConnectionLost("simulated drop during registration")
385
+ await original_send_raw(line)
386
+
387
+ monkeypatch.setattr(client, "_send_raw", failing_send_raw)
388
+
389
+ with pytest.raises(ConsoleConnectionLost):
390
+ await client.connect()
391
+
392
+ # After failure, no half-open socket or dangling state.
393
+ assert client._writer is None
394
+ assert client._reader is None
395
+ assert client._read_task is None
396
+ assert client._pending == {}