agentirc-cli 1.0.4__tar.gz → 1.0.5__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 (268) hide show
  1. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/CHANGELOG.md +16 -0
  2. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/PKG-INFO +1 -1
  3. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/cli.py +84 -73
  4. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/agent_runner.py +39 -41
  5. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/irc_transport.py +54 -34
  6. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/socket_server.py +33 -15
  7. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/irc_transport.py +54 -34
  8. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/socket_server.py +33 -15
  9. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/agent_runner.py +32 -39
  10. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/irc_transport.py +54 -34
  11. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/socket_server.py +33 -15
  12. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/agent_runner.py +12 -8
  13. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/irc_transport.py +54 -34
  14. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/socket_server.py +33 -15
  15. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/ircd.py +33 -32
  16. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/server_link.py +5 -0
  17. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/skills/rooms.py +97 -84
  18. agentirc_cli-1.0.5/packages/agent-harness/irc_transport.py +209 -0
  19. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/socket_server.py +33 -15
  20. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/pyproject.toml +1 -1
  21. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/sonar-project.properties +1 -1
  22. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/uv.lock +1 -1
  23. agentirc_cli-1.0.4/packages/agent-harness/irc_transport.py +0 -176
  24. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.claude/skills/pr-review/SKILL.md +0 -0
  25. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.flake8 +0 -0
  26. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.github/workflows/pages.yml +0 -0
  27. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.github/workflows/publish.yml +0 -0
  28. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.github/workflows/security-checks.yml +0 -0
  29. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.github/workflows/tests.yml +0 -0
  30. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.gitignore +0 -0
  31. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.markdownlint-cli2.yaml +0 -0
  32. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.pr_agent.toml +0 -0
  33. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.pre-commit-config.yaml +0 -0
  34. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/.pylintrc +0 -0
  35. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/CLAUDE.md +0 -0
  36. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/CNAME +0 -0
  37. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/Gemfile +0 -0
  38. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/Gemfile.lock +0 -0
  39. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/LICENSE +0 -0
  40. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/README.md +0 -0
  41. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/SECURITY.md +0 -0
  42. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/_config.yml +0 -0
  43. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/_sass/color_schemes/anthropic.scss +0 -0
  44. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/_sass/custom/custom.scss +0 -0
  45. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/__init__.py +0 -0
  46. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/__main__.py +0 -0
  47. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/__init__.py +0 -0
  48. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/bot.py +0 -0
  49. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/bot_manager.py +0 -0
  50. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/config.py +0 -0
  51. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/http_listener.py +0 -0
  52. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/template_engine.py +0 -0
  53. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/bots/virtual_client.py +0 -0
  54. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/__init__.py +0 -0
  55. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/__init__.py +0 -0
  56. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/config.py +0 -0
  57. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/daemon.py +0 -0
  58. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/ipc.py +0 -0
  59. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/message_buffer.py +0 -0
  60. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/skill/SKILL.md +0 -0
  61. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/skill/__init__.py +0 -0
  62. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/skill/irc_client.py +0 -0
  63. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/supervisor.py +0 -0
  64. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/acp/webhook.py +0 -0
  65. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/__init__.py +0 -0
  66. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/__main__.py +0 -0
  67. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/agent_runner.py +0 -0
  68. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/config.py +0 -0
  69. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/daemon.py +0 -0
  70. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/ipc.py +0 -0
  71. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/message_buffer.py +0 -0
  72. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/skill/SKILL.md +0 -0
  73. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/skill/__init__.py +0 -0
  74. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/skill/irc_client.py +0 -0
  75. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/supervisor.py +0 -0
  76. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/claude/webhook.py +0 -0
  77. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/__init__.py +0 -0
  78. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/config.py +0 -0
  79. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/daemon.py +0 -0
  80. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/ipc.py +0 -0
  81. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/message_buffer.py +0 -0
  82. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/skill/SKILL.md +0 -0
  83. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/skill/__init__.py +0 -0
  84. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/skill/irc_client.py +0 -0
  85. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/supervisor.py +0 -0
  86. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/codex/webhook.py +0 -0
  87. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/__init__.py +0 -0
  88. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/config.py +0 -0
  89. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/daemon.py +0 -0
  90. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/ipc.py +0 -0
  91. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/message_buffer.py +0 -0
  92. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/skill/SKILL.md +0 -0
  93. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/skill/__init__.py +0 -0
  94. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/skill/irc_client.py +0 -0
  95. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/supervisor.py +0 -0
  96. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/clients/copilot/webhook.py +0 -0
  97. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/credentials.py +0 -0
  98. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/learn_prompt.py +0 -0
  99. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/mesh_config.py +0 -0
  100. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/observer.py +0 -0
  101. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/overview/__init__.py +0 -0
  102. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/overview/collector.py +0 -0
  103. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/overview/model.py +0 -0
  104. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/overview/renderer_text.py +0 -0
  105. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/overview/renderer_web.py +0 -0
  106. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/overview/web/style.css +0 -0
  107. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/persistence.py +0 -0
  108. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/pidfile.py +0 -0
  109. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/__init__.py +0 -0
  110. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/commands.py +0 -0
  111. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/extensions/federation.md +0 -0
  112. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/extensions/history.md +0 -0
  113. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/extensions/rooms.md +0 -0
  114. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/extensions/tags.md +0 -0
  115. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/extensions/threads.md +0 -0
  116. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/message.py +0 -0
  117. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/protocol-index.md +0 -0
  118. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/protocol/replies.py +0 -0
  119. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/__init__.py +0 -0
  120. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/__main__.py +0 -0
  121. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/channel.py +0 -0
  122. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/client.py +0 -0
  123. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/config.py +0 -0
  124. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/remote_client.py +0 -0
  125. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/room_store.py +0 -0
  126. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/rooms_util.py +0 -0
  127. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/skill.py +0 -0
  128. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/skills/__init__.py +0 -0
  129. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/skills/history.py +0 -0
  130. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/skills/threads.py +0 -0
  131. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/server/thread_store.py +0 -0
  132. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/culture/skills/culture/SKILL.md +0 -0
  133. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/SECURITY.md +0 -0
  134. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/agent-client.md +0 -0
  135. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/agent-harness-spec.md +0 -0
  136. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/agentic-self-learn.md +0 -0
  137. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/bots.md +0 -0
  138. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/ci.md +0 -0
  139. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/cli.md +0 -0
  140. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/acp/overview.md +0 -0
  141. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/configuration.md +0 -0
  142. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/context-management.md +0 -0
  143. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/irc-tools.md +0 -0
  144. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/overview.md +0 -0
  145. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/setup.md +0 -0
  146. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/supervisor.md +0 -0
  147. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/claude/webhooks.md +0 -0
  148. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/configuration.md +0 -0
  149. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/context-management.md +0 -0
  150. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/irc-tools.md +0 -0
  151. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/overview.md +0 -0
  152. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/setup.md +0 -0
  153. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/supervisor.md +0 -0
  154. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/codex/webhooks.md +0 -0
  155. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/configuration.md +0 -0
  156. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/context-management.md +0 -0
  157. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/irc-tools.md +0 -0
  158. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/overview.md +0 -0
  159. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/setup.md +0 -0
  160. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/supervisor.md +0 -0
  161. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/clients/copilot/webhooks.md +0 -0
  162. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/codex-backend.md +0 -0
  163. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/copilot-backend.md +0 -0
  164. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/design.md +0 -0
  165. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/docs-site.md +0 -0
  166. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/getting-started.md +0 -0
  167. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/grow-your-agent.md +0 -0
  168. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/harness-conformance.md +0 -0
  169. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/layer1-core-irc.md +0 -0
  170. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/layer2-attention.md +0 -0
  171. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/layer3-skills.md +0 -0
  172. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/layer4-federation.md +0 -0
  173. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/layer5-agent-harness.md +0 -0
  174. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/ops-tooling.md +0 -0
  175. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/overview.md +0 -0
  176. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/publishing.md +0 -0
  177. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  178. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/rooms.md +0 -0
  179. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/server-architecture.md +0 -0
  180. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  181. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  182. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  183. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  184. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
  185. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
  186. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
  187. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  188. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  189. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  190. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  191. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
  192. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
  193. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
  194. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
  195. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/threads.md +0 -0
  196. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/01-pair-programming.md +0 -0
  197. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/02-code-review-ensemble.md +0 -0
  198. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/03-cross-server-delegation.md +0 -0
  199. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/04-knowledge-propagation.md +0 -0
  200. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/05-the-observer.md +0 -0
  201. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/06-cross-server-ops.md +0 -0
  202. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/07-supervisor-intervention.md +0 -0
  203. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/08-apps-as-agents.md +0 -0
  204. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/09-research-swarm.md +0 -0
  205. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases/10-grow-your-agent.md +0 -0
  206. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/docs/use-cases-index.md +0 -0
  207. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/index.md +0 -0
  208. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/README.md +0 -0
  209. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/config.py +0 -0
  210. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/daemon.py +0 -0
  211. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/ipc.py +0 -0
  212. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/message_buffer.py +0 -0
  213. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/skill/SKILL.md +0 -0
  214. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/skill/irc_client.py +0 -0
  215. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/packages/agent-harness/webhook.py +0 -0
  216. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  217. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/plugins/claude-code/skills/culture/SKILL.md +0 -0
  218. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  219. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
  220. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/__init__.py +0 -0
  221. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/conftest.py +0 -0
  222. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_acp_daemon.py +0 -0
  223. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_agent_runner.py +0 -0
  224. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_bot.py +0 -0
  225. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_bot_config.py +0 -0
  226. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_bot_manager.py +0 -0
  227. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_bots_integration.py +0 -0
  228. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_channel.py +0 -0
  229. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_codex_daemon.py +0 -0
  230. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_connection.py +0 -0
  231. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_copilot_daemon.py +0 -0
  232. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_daemon.py +0 -0
  233. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_daemon_config.py +0 -0
  234. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_daemon_ipc.py +0 -0
  235. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_discovery.py +0 -0
  236. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_federation.py +0 -0
  237. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_history.py +0 -0
  238. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_http_listener.py +0 -0
  239. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_integration_layer5.py +0 -0
  240. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_ipc.py +0 -0
  241. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_irc_transport.py +0 -0
  242. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_link_reconnect.py +0 -0
  243. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_mentions.py +0 -0
  244. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_mesh_config.py +0 -0
  245. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_message.py +0 -0
  246. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_message_buffer.py +0 -0
  247. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_messaging.py +0 -0
  248. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_modes.py +0 -0
  249. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_overview_cli.py +0 -0
  250. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_overview_collector.py +0 -0
  251. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_overview_model.py +0 -0
  252. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_overview_renderer.py +0 -0
  253. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_overview_web.py +0 -0
  254. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_persistence.py +0 -0
  255. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_room_persistence.py +0 -0
  256. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_rooms.py +0 -0
  257. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_rooms_federation.py +0 -0
  258. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_rooms_integration.py +0 -0
  259. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_setup_update_cli.py +0 -0
  260. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_skill_client.py +0 -0
  261. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_skills.py +0 -0
  262. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_socket_server.py +0 -0
  263. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_supervisor.py +0 -0
  264. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_template_engine.py +0 -0
  265. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_thread_buffer.py +0 -0
  266. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_threads.py +0 -0
  267. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_virtual_client.py +0 -0
  268. {agentirc_cli-1.0.4 → agentirc_cli-1.0.5}/tests/test_webhook.py +0 -0
@@ -4,6 +4,22 @@ 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
+ ## [1.0.5] - 2026-04-05
8
+
9
+
10
+ ### Changed
11
+
12
+ - Extract helper methods from socket_server _handle_client (all backends)
13
+ - Convert irc_transport _handle to dispatch table (all backends)
14
+ - Extract _auto_approve and _flush_accumulated_text in codex/acp agent_runner
15
+ - Extract _handle_session_update and _extract_response_text in acp/copilot agent_runner
16
+ - Decompose _handle_roommeta into query/update methods in rooms.py
17
+ - Extract _merge_room_metadata in server_link.py
18
+ - Extract _attempt_single_reconnect in ircd.py
19
+ - Extract _create_agent_config and _try_ipc_shutdown/_try_pid_shutdown in cli.py
20
+ - Update packages/agent-harness templates to match backend features
21
+ - Add socket_server and irc_transport to sonar CPD exclusions
22
+
7
23
  ## [1.0.4] - 2026-04-05
8
24
 
9
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 1.0.4
3
+ Version: 1.0.5
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
@@ -525,55 +525,27 @@ def _server_status(args: argparse.Namespace) -> None:
525
525
  # -----------------------------------------------------------------------
526
526
 
527
527
 
528
- def _cmd_init(args: argparse.Namespace) -> None:
529
- config = load_config_or_default(args.config)
530
-
531
- # Determine server name
532
- server_name = args.server or config.server.name or "culture"
533
-
534
- # Determine agent suffix
535
- if args.nick:
536
- suffix = args.nick
537
- else:
538
- dirname = os.path.basename(os.getcwd())
539
- suffix = sanitize_agent_name(dirname)
540
-
541
- full_nick = f"{server_name}-{suffix}"
542
-
543
- # Check for collision
544
- for existing in config.agents:
545
- if existing.nick == full_nick:
546
- channels = existing.channels if isinstance(existing.channels, list) else []
547
- print(f"Agent '{full_nick}' already exists in config", file=sys.stderr)
548
- print(f" Directory: {existing.directory}", file=sys.stderr)
549
- print(f" Backend: {existing.agent}", file=sys.stderr)
550
- print(f" Channels: {', '.join(channels)}", file=sys.stderr)
551
- print(f" Model: {existing.model}", file=sys.stderr)
552
- print(f" Config: {args.config}", file=sys.stderr)
553
- print(file=sys.stderr)
554
- print(f"Start with: culture start {full_nick}", file=sys.stderr)
555
- sys.exit(1)
556
-
557
- # Use backend-specific config for correct defaults
528
+ def _create_agent_config(args: argparse.Namespace, full_nick: str) -> "AgentConfig":
529
+ """Build a backend-specific AgentConfig from CLI args."""
558
530
  if args.agent == "codex":
559
531
  from culture.clients.codex.config import AgentConfig as CodexAgentConfig
560
532
 
561
- agent = CodexAgentConfig(
533
+ return CodexAgentConfig(
562
534
  nick=full_nick,
563
535
  agent="codex",
564
536
  directory=os.getcwd(),
565
537
  channels=["#general"],
566
538
  )
567
- elif args.agent == "copilot":
539
+ if args.agent == "copilot":
568
540
  from culture.clients.copilot.config import AgentConfig as CopilotAgentConfig
569
541
 
570
- agent = CopilotAgentConfig(
542
+ return CopilotAgentConfig(
571
543
  nick=full_nick,
572
544
  agent="copilot",
573
545
  directory=os.getcwd(),
574
546
  channels=["#general"],
575
547
  )
576
- elif args.agent == "acp":
548
+ if args.agent == "acp":
577
549
  import json as _json
578
550
 
579
551
  from culture.clients.acp.config import AgentConfig as ACPAgentConfig
@@ -583,7 +555,6 @@ def _cmd_init(args: argparse.Namespace) -> None:
583
555
  try:
584
556
  acp_cmd = _json.loads(args.acp_command)
585
557
  except _json.JSONDecodeError:
586
- # Treat as a simple command name (e.g. "cline --acp")
587
558
  acp_cmd = args.acp_command.split()
588
559
  if (
589
560
  not isinstance(acp_cmd, list)
@@ -592,20 +563,51 @@ def _cmd_init(args: argparse.Namespace) -> None:
592
563
  ):
593
564
  print("Error: --acp-command must be a non-empty list of strings", file=sys.stderr)
594
565
  sys.exit(1)
595
- agent = ACPAgentConfig(
566
+ return ACPAgentConfig(
596
567
  nick=full_nick,
597
568
  agent="acp",
598
569
  acp_command=acp_cmd,
599
570
  directory=os.getcwd(),
600
571
  channels=["#general"],
601
572
  )
573
+ return AgentConfig(
574
+ nick=full_nick,
575
+ agent=args.agent,
576
+ directory=os.getcwd(),
577
+ channels=["#general"],
578
+ )
579
+
580
+
581
+ def _cmd_init(args: argparse.Namespace) -> None:
582
+ config = load_config_or_default(args.config)
583
+
584
+ # Determine server name
585
+ server_name = args.server or config.server.name or "culture"
586
+
587
+ # Determine agent suffix
588
+ if args.nick:
589
+ suffix = args.nick
602
590
  else:
603
- agent = AgentConfig(
604
- nick=full_nick,
605
- agent=args.agent,
606
- directory=os.getcwd(),
607
- channels=["#general"],
608
- )
591
+ dirname = os.path.basename(os.getcwd())
592
+ suffix = sanitize_agent_name(dirname)
593
+
594
+ full_nick = f"{server_name}-{suffix}"
595
+
596
+ # Check for collision
597
+ for existing in config.agents:
598
+ if existing.nick == full_nick:
599
+ channels = existing.channels if isinstance(existing.channels, list) else []
600
+ print(f"Agent '{full_nick}' already exists in config", file=sys.stderr)
601
+ print(f" Directory: {existing.directory}", file=sys.stderr)
602
+ print(f" Backend: {existing.agent}", file=sys.stderr)
603
+ print(f" Channels: {', '.join(channels)}", file=sys.stderr)
604
+ print(f" Model: {existing.model}", file=sys.stderr)
605
+ print(f" Config: {args.config}", file=sys.stderr)
606
+ print(file=sys.stderr)
607
+ print(f"Start with: culture start {full_nick}", file=sys.stderr)
608
+ sys.exit(1)
609
+
610
+ agent = _create_agent_config(args, full_nick)
609
611
 
610
612
  add_agent_to_config(args.config, agent, server_name=server_name)
611
613
 
@@ -853,35 +855,46 @@ def _cmd_stop(args: argparse.Namespace) -> None:
853
855
 
854
856
  def _stop_agent(nick: str) -> None:
855
857
  """Stop a single agent by trying IPC shutdown first, then PID file."""
856
- # Try Unix socket IPC shutdown
857
858
  socket_path = os.path.join(
858
859
  os.environ.get("XDG_RUNTIME_DIR", "/tmp"),
859
860
  f"culture-{nick}.sock",
860
861
  )
861
862
 
862
- if os.path.exists(socket_path):
863
- try:
864
- success = asyncio.run(_ipc_shutdown(socket_path))
865
- if success:
866
- print(f"Agent '{nick}' shutdown requested via IPC")
867
- # Wait for process to exit
868
- pid_name = f"agent-{nick}"
869
- pid = read_pid(pid_name)
870
- if pid:
871
- for _ in range(50):
872
- if not is_process_alive(pid):
873
- remove_pid(pid_name)
874
- print(f"Agent '{nick}' stopped")
875
- return
876
- time.sleep(0.1)
877
- # If still alive after 5s, fall through to SIGTERM
878
- else:
879
- print(f"Agent '{nick}' stopped")
880
- return
881
- except Exception:
882
- pass # Fall through to PID-based stop
863
+ if _try_ipc_shutdown(nick, socket_path):
864
+ return
865
+
866
+ _try_pid_shutdown(nick)
867
+
868
+
869
+ def _try_ipc_shutdown(nick: str, socket_path: str) -> bool:
870
+ """Attempt graceful IPC shutdown. Return True if the agent stopped."""
871
+ if not os.path.exists(socket_path):
872
+ return False
873
+ try:
874
+ success = asyncio.run(_ipc_shutdown(socket_path))
875
+ if not success:
876
+ return False
877
+ except Exception:
878
+ return False
883
879
 
884
- # Fall back to PID file
880
+ print(f"Agent '{nick}' shutdown requested via IPC")
881
+ pid_name = f"agent-{nick}"
882
+ pid = read_pid(pid_name)
883
+ if not pid:
884
+ print(f"Agent '{nick}' stopped")
885
+ return True
886
+ for _ in range(50):
887
+ if not is_process_alive(pid):
888
+ remove_pid(pid_name)
889
+ print(f"Agent '{nick}' stopped")
890
+ return True
891
+ time.sleep(0.1)
892
+ # Still alive after 5s — fall through to PID-based stop
893
+ return False
894
+
895
+
896
+ def _try_pid_shutdown(nick: str) -> None:
897
+ """Stop an agent via PID file with SIGTERM/SIGKILL fallback."""
885
898
  pid_name = f"agent-{nick}"
886
899
  pid = read_pid(pid_name)
887
900
 
@@ -907,16 +920,14 @@ def _stop_agent(nick: str) -> None:
907
920
  # Force kill
908
921
  if sys.platform == "win32":
909
922
  print(f"Agent '{nick}' did not stop gracefully, terminating")
910
- try:
911
- os.kill(pid, signal.SIGTERM)
912
- except ProcessLookupError:
913
- pass
923
+ sig = signal.SIGTERM
914
924
  else:
915
925
  print(f"Agent '{nick}' did not stop gracefully, sending SIGKILL")
916
- try:
917
- os.kill(pid, signal.SIGKILL)
918
- except ProcessLookupError:
919
- pass
926
+ sig = signal.SIGKILL
927
+ try:
928
+ os.kill(pid, sig)
929
+ except ProcessLookupError:
930
+ pass
920
931
  remove_pid(pid_name)
921
932
  print(f"Agent '{nick}' killed")
922
933
 
@@ -299,42 +299,50 @@ class ACPAgentRunner:
299
299
  params = msg.get("params", {})
300
300
 
301
301
  if method == "session/update":
302
- # ACP wraps updates in params.update (not flat in params)
303
- update = params.get("update", params)
304
- update_type = update.get("sessionUpdate", "")
305
-
306
- if update_type in ("agent_message_chunk", "agent_thought_chunk"):
307
- # Accumulate streaming text (both message and thought chunks)
308
- self._busy = True
309
- content = update.get("content", {})
310
- if update_type == "agent_message_chunk" and content.get("type") == "text":
311
- self._accumulated_text += content.get("text", "")
312
-
313
- # Check for stopReason — indicates turn is complete
314
- if "stopReason" in update:
315
- self._busy = False
316
- if self.on_message and self._accumulated_text:
317
- msg_dict = {
318
- "type": "assistant",
319
- "model": self.model,
320
- "content": [{"type": "text", "text": self._accumulated_text}],
321
- }
322
- await self.on_message(msg_dict)
323
- self._accumulated_text = ""
302
+ await self._handle_session_update(params)
324
303
 
325
304
  elif method == "session/request_permission":
326
- # Auto-approve all permission requests (file changes, commands, etc.)
327
- req_id = msg.get("id")
328
- if req_id is not None:
329
- resp = {"jsonrpc": "2.0", "id": req_id, "result": {"approved": True}}
330
- if self._process and self._process.stdin:
331
- line = json.dumps(resp) + "\n"
332
- self._process.stdin.write(line.encode())
333
- await self._process.stdin.drain()
305
+ await self._auto_approve(msg)
334
306
 
335
307
  elif method == "error":
336
308
  logger.error("ACP error (%s): %s", self.acp_command[0], params)
337
309
 
310
+ async def _handle_session_update(self, params: dict) -> None:
311
+ """Process a session/update notification."""
312
+ update = params.get("update", params)
313
+ update_type = update.get("sessionUpdate", "")
314
+
315
+ if update_type in ("agent_message_chunk", "agent_thought_chunk"):
316
+ self._busy = True
317
+ content = update.get("content", {})
318
+ if update_type == "agent_message_chunk" and content.get("type") == "text":
319
+ self._accumulated_text += content.get("text", "")
320
+
321
+ if "stopReason" in update:
322
+ self._busy = False
323
+ await self._flush_accumulated_text()
324
+
325
+ async def _flush_accumulated_text(self) -> None:
326
+ """Fire on_message with any accumulated text and reset the buffer."""
327
+ if self.on_message and self._accumulated_text:
328
+ msg_dict = {
329
+ "type": "assistant",
330
+ "model": self.model,
331
+ "content": [{"type": "text", "text": self._accumulated_text}],
332
+ }
333
+ await self.on_message(msg_dict)
334
+ self._accumulated_text = ""
335
+
336
+ async def _auto_approve(self, msg: dict) -> None:
337
+ """Auto-approve a permission request from the ACP process."""
338
+ req_id = msg.get("id")
339
+ if req_id is not None:
340
+ resp = {"jsonrpc": "2.0", "id": req_id, "result": {"approved": True}}
341
+ if self._process and self._process.stdin:
342
+ line = json.dumps(resp) + "\n"
343
+ self._process.stdin.write(line.encode())
344
+ await self._process.stdin.drain()
345
+
338
346
  async def _prompt_loop(self) -> None:
339
347
  """Process queued prompts one at a time."""
340
348
  try:
@@ -354,21 +362,11 @@ class ACPAgentRunner:
354
362
  timeout=120,
355
363
  )
356
364
 
357
- # Check if response itself signals turn completion
358
365
  result = resp.get("result", {})
359
366
  if "stopReason" in result:
360
- # Turn completed — flush any accumulated text
361
- if self.on_message and self._accumulated_text:
362
- msg_dict = {
363
- "type": "assistant",
364
- "model": self.model,
365
- "content": [{"type": "text", "text": self._accumulated_text}],
366
- }
367
- await self.on_message(msg_dict)
368
- self._accumulated_text = ""
367
+ await self._flush_accumulated_text()
369
368
  self._busy = False
370
369
 
371
- # Wait for turn to complete (via notifications)
372
370
  while self._busy and self._running:
373
371
  await asyncio.sleep(0.1)
374
372
 
@@ -41,6 +41,13 @@ class IRCTransport:
41
41
  self._reconnecting = False
42
42
  self._should_run = False
43
43
  self._background_tasks: set[asyncio.Task] = set()
44
+ self._cmd_handlers: dict[str, Callable] = {
45
+ "PING": self._on_ping,
46
+ "001": self._on_welcome,
47
+ "PRIVMSG": self._on_privmsg,
48
+ "NOTICE": self._on_notice,
49
+ "ROOMINVITE": self._on_roominvite,
50
+ }
44
51
 
45
52
  async def connect(self) -> None:
46
53
  self._should_run = True
@@ -152,37 +159,50 @@ class IRCTransport:
152
159
  delay = min(delay * 2, 60)
153
160
 
154
161
  async def _handle(self, msg: Message) -> None:
155
- if msg.command == "PING":
156
- token = msg.params[0] if msg.params else ""
157
- await self._send_raw(f"PONG :{token}")
158
- elif msg.command == "001":
159
- self.connected = True
160
- for channel in self.channels:
161
- await self._send_raw(f"JOIN {channel}")
162
- # Announce agent tags on connect
163
- if self.tags:
164
- tags_str = ",".join(self.tags)
165
- await self._send_raw(f"TAGS {self.nick} {tags_str}")
166
- elif msg.command == "PRIVMSG" and len(msg.params) >= 2:
167
- target = msg.params[0]
168
- text = msg.params[1]
169
- sender = msg.prefix.split("!")[0] if msg.prefix else "unknown"
170
- if sender == self.nick:
171
- return
172
- if target.startswith("#"):
173
- self.buffer.add(target, sender, text)
174
- else:
175
- self.buffer.add(f"DM:{sender}", sender, text)
176
- if self.on_mention and f"@{self.nick}" in text:
177
- self.on_mention(target, sender, text)
178
- elif msg.command == "NOTICE" and len(msg.params) >= 2:
179
- target = msg.params[0]
180
- text = msg.params[1]
181
- sender = msg.prefix.split("!")[0] if msg.prefix else "server"
182
- if target.startswith("#"):
183
- self.buffer.add(target, sender, text)
184
- elif msg.command == "ROOMINVITE" and len(msg.params) >= 3:
185
- channel = msg.params[0]
186
- meta_text = msg.params[2]
187
- if self.on_roominvite:
188
- self.on_roominvite(channel, meta_text)
162
+ handler = self._cmd_handlers.get(msg.command)
163
+ if handler:
164
+ await handler(msg)
165
+
166
+ async def _on_ping(self, msg: Message) -> None:
167
+ token = msg.params[0] if msg.params else ""
168
+ await self._send_raw(f"PONG :{token}")
169
+
170
+ async def _on_welcome(self, msg: Message) -> None:
171
+ self.connected = True
172
+ for channel in self.channels:
173
+ await self._send_raw(f"JOIN {channel}")
174
+ if self.tags:
175
+ tags_str = ",".join(self.tags)
176
+ await self._send_raw(f"TAGS {self.nick} {tags_str}")
177
+
178
+ async def _on_privmsg(self, msg: Message) -> None:
179
+ if len(msg.params) < 2:
180
+ return
181
+ target = msg.params[0]
182
+ text = msg.params[1]
183
+ sender = msg.prefix.split("!")[0] if msg.prefix else "unknown"
184
+ if sender == self.nick:
185
+ return
186
+ if target.startswith("#"):
187
+ self.buffer.add(target, sender, text)
188
+ else:
189
+ self.buffer.add(f"DM:{sender}", sender, text)
190
+ if self.on_mention and f"@{self.nick}" in text:
191
+ self.on_mention(target, sender, text)
192
+
193
+ async def _on_notice(self, msg: Message) -> None:
194
+ if len(msg.params) < 2:
195
+ return
196
+ target = msg.params[0]
197
+ text = msg.params[1]
198
+ sender = msg.prefix.split("!")[0] if msg.prefix else "server"
199
+ if target.startswith("#"):
200
+ self.buffer.add(target, sender, text)
201
+
202
+ async def _on_roominvite(self, msg: Message) -> None:
203
+ if len(msg.params) < 3:
204
+ return
205
+ channel = msg.params[0]
206
+ meta_text = msg.params[2]
207
+ if self.on_roominvite:
208
+ self.on_roominvite(channel, meta_text)
@@ -61,7 +61,14 @@ class SocketServer:
61
61
  self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
62
62
  ) -> None:
63
63
  self._clients.append(writer)
64
- # Drain any queued whispers that arrived before this client connected.
64
+ try:
65
+ await self._drain_queued_whispers(writer)
66
+ await self._process_client_messages(reader, writer)
67
+ finally:
68
+ self._cleanup_client(writer)
69
+
70
+ async def _drain_queued_whispers(self, writer: asyncio.StreamWriter) -> None:
71
+ """Deliver any whispers queued before this client connected."""
65
72
  while not self._whisper_queue.empty():
66
73
  try:
67
74
  data = self._whisper_queue.get_nowait()
@@ -71,6 +78,11 @@ class SocketServer:
71
78
  break
72
79
  except (ConnectionError, BrokenPipeError, OSError):
73
80
  break
81
+
82
+ async def _process_client_messages(
83
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
84
+ ) -> None:
85
+ """Read and dispatch IPC messages until the client disconnects."""
74
86
  try:
75
87
  while True:
76
88
  line = await reader.readline()
@@ -85,20 +97,26 @@ class SocketServer:
85
97
  await writer.drain()
86
98
  except Exception as exc:
87
99
  logger.exception("Handler error for message: %s", msg)
88
- try:
89
- request_id = msg.get("id") if isinstance(msg, dict) else None
90
- err_resp = make_response(request_id or "", ok=False, error=str(exc))
91
- writer.write(encode_message(err_resp))
92
- await writer.drain()
93
- except (ConnectionError, BrokenPipeError, OSError):
100
+ if not await self._send_error_response(msg, exc, writer):
94
101
  break
95
102
  except (ConnectionError, asyncio.IncompleteReadError):
96
103
  pass
97
- finally:
98
- if writer in self._clients:
99
- self._clients.remove(writer)
100
- writer.close()
101
- try:
102
- await writer.wait_closed()
103
- except (ConnectionError, BrokenPipeError, OSError):
104
- pass
104
+
105
+ async def _send_error_response(
106
+ self, msg: dict, exc: Exception, writer: asyncio.StreamWriter
107
+ ) -> bool:
108
+ """Send an error response to the client. Return False if the connection broke."""
109
+ try:
110
+ request_id = msg.get("id") if isinstance(msg, dict) else None
111
+ err_resp = make_response(request_id or "", ok=False, error=str(exc))
112
+ writer.write(encode_message(err_resp))
113
+ await writer.drain()
114
+ return True
115
+ except (ConnectionError, BrokenPipeError, OSError):
116
+ return False
117
+
118
+ def _cleanup_client(self, writer: asyncio.StreamWriter) -> None:
119
+ """Remove client from the active list and close the connection."""
120
+ if writer in self._clients:
121
+ self._clients.remove(writer)
122
+ writer.close()
@@ -41,6 +41,13 @@ class IRCTransport:
41
41
  self._reconnecting = False
42
42
  self._should_run = False
43
43
  self._background_tasks: set[asyncio.Task] = set()
44
+ self._cmd_handlers: dict[str, Callable] = {
45
+ "PING": self._on_ping,
46
+ "001": self._on_welcome,
47
+ "PRIVMSG": self._on_privmsg,
48
+ "NOTICE": self._on_notice,
49
+ "ROOMINVITE": self._on_roominvite,
50
+ }
44
51
 
45
52
  async def connect(self) -> None:
46
53
  self._should_run = True
@@ -152,37 +159,50 @@ class IRCTransport:
152
159
  delay = min(delay * 2, 60)
153
160
 
154
161
  async def _handle(self, msg: Message) -> None:
155
- if msg.command == "PING":
156
- token = msg.params[0] if msg.params else ""
157
- await self._send_raw(f"PONG :{token}")
158
- elif msg.command == "001":
159
- self.connected = True
160
- for channel in self.channels:
161
- await self._send_raw(f"JOIN {channel}")
162
- # Announce agent tags on connect
163
- if self.tags:
164
- tags_str = ",".join(self.tags)
165
- await self._send_raw(f"TAGS {self.nick} {tags_str}")
166
- elif msg.command == "PRIVMSG" and len(msg.params) >= 2:
167
- target = msg.params[0]
168
- text = msg.params[1]
169
- sender = msg.prefix.split("!")[0] if msg.prefix else "unknown"
170
- if sender == self.nick:
171
- return
172
- if target.startswith("#"):
173
- self.buffer.add(target, sender, text)
174
- else:
175
- self.buffer.add(f"DM:{sender}", sender, text)
176
- if self.on_mention and f"@{self.nick}" in text:
177
- self.on_mention(target, sender, text)
178
- elif msg.command == "NOTICE" and len(msg.params) >= 2:
179
- target = msg.params[0]
180
- text = msg.params[1]
181
- sender = msg.prefix.split("!")[0] if msg.prefix else "server"
182
- if target.startswith("#"):
183
- self.buffer.add(target, sender, text)
184
- elif msg.command == "ROOMINVITE" and len(msg.params) >= 3:
185
- channel = msg.params[0]
186
- meta_text = msg.params[2]
187
- if self.on_roominvite:
188
- self.on_roominvite(channel, meta_text)
162
+ handler = self._cmd_handlers.get(msg.command)
163
+ if handler:
164
+ await handler(msg)
165
+
166
+ async def _on_ping(self, msg: Message) -> None:
167
+ token = msg.params[0] if msg.params else ""
168
+ await self._send_raw(f"PONG :{token}")
169
+
170
+ async def _on_welcome(self, msg: Message) -> None:
171
+ self.connected = True
172
+ for channel in self.channels:
173
+ await self._send_raw(f"JOIN {channel}")
174
+ if self.tags:
175
+ tags_str = ",".join(self.tags)
176
+ await self._send_raw(f"TAGS {self.nick} {tags_str}")
177
+
178
+ async def _on_privmsg(self, msg: Message) -> None:
179
+ if len(msg.params) < 2:
180
+ return
181
+ target = msg.params[0]
182
+ text = msg.params[1]
183
+ sender = msg.prefix.split("!")[0] if msg.prefix else "unknown"
184
+ if sender == self.nick:
185
+ return
186
+ if target.startswith("#"):
187
+ self.buffer.add(target, sender, text)
188
+ else:
189
+ self.buffer.add(f"DM:{sender}", sender, text)
190
+ if self.on_mention and f"@{self.nick}" in text:
191
+ self.on_mention(target, sender, text)
192
+
193
+ async def _on_notice(self, msg: Message) -> None:
194
+ if len(msg.params) < 2:
195
+ return
196
+ target = msg.params[0]
197
+ text = msg.params[1]
198
+ sender = msg.prefix.split("!")[0] if msg.prefix else "server"
199
+ if target.startswith("#"):
200
+ self.buffer.add(target, sender, text)
201
+
202
+ async def _on_roominvite(self, msg: Message) -> None:
203
+ if len(msg.params) < 3:
204
+ return
205
+ channel = msg.params[0]
206
+ meta_text = msg.params[2]
207
+ if self.on_roominvite:
208
+ self.on_roominvite(channel, meta_text)