agentirc-cli 0.20.0__tar.gz → 0.21.0__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 (263) hide show
  1. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/security-checks.yml +0 -15
  2. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/CHANGELOG.md +31 -0
  3. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/PKG-INFO +2 -1
  4. agentirc_cli-0.21.0/agentirc/bots/bot.py +148 -0
  5. agentirc_cli-0.21.0/agentirc/bots/bot_manager.py +102 -0
  6. agentirc_cli-0.21.0/agentirc/bots/config.py +98 -0
  7. agentirc_cli-0.21.0/agentirc/bots/http_listener.py +84 -0
  8. agentirc_cli-0.21.0/agentirc/bots/template_engine.py +63 -0
  9. agentirc_cli-0.21.0/agentirc/bots/virtual_client.py +193 -0
  10. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/cli.py +419 -86
  11. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/collector.py +73 -27
  12. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/model.py +20 -0
  13. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/renderer_text.py +28 -4
  14. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/channel.py +7 -3
  15. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/config.py +1 -0
  16. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/ircd.py +56 -26
  17. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/SECURITY.md +2 -9
  18. agentirc_cli-0.21.0/docs/bots.md +129 -0
  19. agentirc_cli-0.21.0/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +353 -0
  20. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/pyproject.toml +2 -1
  21. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/sonar-project.properties +1 -1
  22. agentirc_cli-0.21.0/tests/__init__.py +0 -0
  23. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/conftest.py +32 -11
  24. agentirc_cli-0.21.0/tests/test_bot.py +174 -0
  25. agentirc_cli-0.21.0/tests/test_bot_config.py +98 -0
  26. agentirc_cli-0.21.0/tests/test_bot_manager.py +147 -0
  27. agentirc_cli-0.21.0/tests/test_bots_integration.py +161 -0
  28. agentirc_cli-0.21.0/tests/test_http_listener.py +147 -0
  29. agentirc_cli-0.21.0/tests/test_template_engine.py +80 -0
  30. agentirc_cli-0.21.0/tests/test_virtual_client.py +176 -0
  31. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/uv.lock +486 -1
  32. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.claude/skills/pr-review/SKILL.md +0 -0
  33. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.flake8 +0 -0
  34. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/pages.yml +0 -0
  35. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/publish.yml +0 -0
  36. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/tests.yml +0 -0
  37. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.gitignore +0 -0
  38. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.markdownlint-cli2.yaml +0 -0
  39. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.pr_agent.toml +0 -0
  40. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.pre-commit-config.yaml +0 -0
  41. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.pylintrc +0 -0
  42. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/CLAUDE.md +0 -0
  43. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/CNAME +0 -0
  44. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/Gemfile +0 -0
  45. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/Gemfile.lock +0 -0
  46. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/LICENSE +0 -0
  47. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/README.md +0 -0
  48. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/SECURITY.md +0 -0
  49. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/_config.yml +0 -0
  50. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/_sass/color_schemes/anthropic.scss +0 -0
  51. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/_sass/custom/custom.scss +0 -0
  52. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/__init__.py +0 -0
  53. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/__main__.py +0 -0
  54. {agentirc_cli-0.20.0/agentirc/clients → agentirc_cli-0.21.0/agentirc/bots}/__init__.py +0 -0
  55. {agentirc_cli-0.20.0/agentirc/clients/acp → agentirc_cli-0.21.0/agentirc/clients}/__init__.py +0 -0
  56. {agentirc_cli-0.20.0/agentirc/clients/acp/skill → agentirc_cli-0.21.0/agentirc/clients/acp}/__init__.py +0 -0
  57. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/agent_runner.py +0 -0
  58. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/config.py +0 -0
  59. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/daemon.py +0 -0
  60. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/ipc.py +0 -0
  61. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/irc_transport.py +0 -0
  62. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/message_buffer.py +0 -0
  63. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/skill/SKILL.md +0 -0
  64. {agentirc_cli-0.20.0/agentirc/clients/claude → agentirc_cli-0.21.0/agentirc/clients/acp/skill}/__init__.py +0 -0
  65. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/skill/irc_client.py +0 -0
  66. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/socket_server.py +0 -0
  67. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/supervisor.py +0 -0
  68. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/webhook.py +0 -0
  69. {agentirc_cli-0.20.0/agentirc/clients/claude/skill → agentirc_cli-0.21.0/agentirc/clients/claude}/__init__.py +0 -0
  70. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/__main__.py +0 -0
  71. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/agent_runner.py +0 -0
  72. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/config.py +0 -0
  73. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/daemon.py +0 -0
  74. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/ipc.py +0 -0
  75. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/irc_transport.py +0 -0
  76. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/message_buffer.py +0 -0
  77. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
  78. {agentirc_cli-0.20.0/agentirc/clients/codex → agentirc_cli-0.21.0/agentirc/clients/claude/skill}/__init__.py +0 -0
  79. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
  80. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/socket_server.py +0 -0
  81. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/supervisor.py +0 -0
  82. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/webhook.py +0 -0
  83. {agentirc_cli-0.20.0/agentirc/clients/codex/skill → agentirc_cli-0.21.0/agentirc/clients/codex}/__init__.py +0 -0
  84. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/agent_runner.py +0 -0
  85. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/config.py +0 -0
  86. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/daemon.py +0 -0
  87. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/ipc.py +0 -0
  88. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/irc_transport.py +0 -0
  89. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/message_buffer.py +0 -0
  90. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
  91. {agentirc_cli-0.20.0/agentirc/clients/copilot → agentirc_cli-0.21.0/agentirc/clients/codex/skill}/__init__.py +0 -0
  92. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
  93. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/socket_server.py +0 -0
  94. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/supervisor.py +0 -0
  95. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/webhook.py +0 -0
  96. {agentirc_cli-0.20.0/agentirc/clients/copilot/skill → agentirc_cli-0.21.0/agentirc/clients/copilot}/__init__.py +0 -0
  97. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/agent_runner.py +0 -0
  98. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/config.py +0 -0
  99. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/daemon.py +0 -0
  100. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/ipc.py +0 -0
  101. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/irc_transport.py +0 -0
  102. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/message_buffer.py +0 -0
  103. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
  104. {agentirc_cli-0.20.0/agentirc/protocol → agentirc_cli-0.21.0/agentirc/clients/copilot/skill}/__init__.py +0 -0
  105. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
  106. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/socket_server.py +0 -0
  107. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/supervisor.py +0 -0
  108. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/webhook.py +0 -0
  109. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/credentials.py +0 -0
  110. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/learn_prompt.py +0 -0
  111. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/mesh_config.py +0 -0
  112. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/observer.py +0 -0
  113. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/__init__.py +0 -0
  114. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/renderer_web.py +0 -0
  115. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/web/style.css +0 -0
  116. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/persistence.py +0 -0
  117. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/pidfile.py +0 -0
  118. {agentirc_cli-0.20.0/agentirc/server → agentirc_cli-0.21.0/agentirc/protocol}/__init__.py +0 -0
  119. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/commands.py +0 -0
  120. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/federation.md +0 -0
  121. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/history.md +0 -0
  122. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/rooms.md +0 -0
  123. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/tags.md +0 -0
  124. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/threads.md +0 -0
  125. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/message.py +0 -0
  126. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/protocol-index.md +0 -0
  127. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/replies.py +0 -0
  128. {agentirc_cli-0.20.0/agentirc/server/skills → agentirc_cli-0.21.0/agentirc/server}/__init__.py +0 -0
  129. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/__main__.py +0 -0
  130. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/client.py +0 -0
  131. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/remote_client.py +0 -0
  132. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/room_store.py +0 -0
  133. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/rooms_util.py +0 -0
  134. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/server_link.py +0 -0
  135. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skill.py +0 -0
  136. {agentirc_cli-0.20.0/tests → agentirc_cli-0.21.0/agentirc/server/skills}/__init__.py +0 -0
  137. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skills/history.py +0 -0
  138. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skills/rooms.py +0 -0
  139. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skills/threads.py +0 -0
  140. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/thread_store.py +0 -0
  141. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/skills/agentirc/SKILL.md +0 -0
  142. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/agent-client.md +0 -0
  143. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/agent-harness-spec.md +0 -0
  144. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/agentic-self-learn.md +0 -0
  145. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/ci.md +0 -0
  146. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/cli.md +0 -0
  147. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/acp/overview.md +0 -0
  148. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/configuration.md +0 -0
  149. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/context-management.md +0 -0
  150. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/irc-tools.md +0 -0
  151. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/overview.md +0 -0
  152. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/setup.md +0 -0
  153. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/supervisor.md +0 -0
  154. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/webhooks.md +0 -0
  155. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/configuration.md +0 -0
  156. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/context-management.md +0 -0
  157. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/irc-tools.md +0 -0
  158. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/overview.md +0 -0
  159. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/setup.md +0 -0
  160. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/supervisor.md +0 -0
  161. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/webhooks.md +0 -0
  162. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/configuration.md +0 -0
  163. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/context-management.md +0 -0
  164. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/irc-tools.md +0 -0
  165. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/overview.md +0 -0
  166. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/setup.md +0 -0
  167. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/supervisor.md +0 -0
  168. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/webhooks.md +0 -0
  169. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/codex-backend.md +0 -0
  170. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/copilot-backend.md +0 -0
  171. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/design.md +0 -0
  172. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/docs-site.md +0 -0
  173. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/getting-started.md +0 -0
  174. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/grow-your-agent.md +0 -0
  175. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/harness-conformance.md +0 -0
  176. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer1-core-irc.md +0 -0
  177. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer2-attention.md +0 -0
  178. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer3-skills.md +0 -0
  179. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer4-federation.md +0 -0
  180. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer5-agent-harness.md +0 -0
  181. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/ops-tooling.md +0 -0
  182. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/overview.md +0 -0
  183. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/publishing.md +0 -0
  184. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  185. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/rooms.md +0 -0
  186. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/server-architecture.md +0 -0
  187. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  188. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  189. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  190. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  191. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
  192. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  193. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  194. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  195. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  196. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
  197. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/threads.md +0 -0
  198. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/01-pair-programming.md +0 -0
  199. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
  200. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
  201. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
  202. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/05-the-observer.md +0 -0
  203. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/06-cross-server-ops.md +0 -0
  204. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
  205. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/08-apps-as-agents.md +0 -0
  206. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/09-research-swarm.md +0 -0
  207. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/10-grow-your-agent.md +0 -0
  208. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases-index.md +0 -0
  209. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/index.md +0 -0
  210. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/README.md +0 -0
  211. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/config.py +0 -0
  212. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/daemon.py +0 -0
  213. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/ipc.py +0 -0
  214. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/irc_transport.py +0 -0
  215. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/message_buffer.py +0 -0
  216. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/skill/SKILL.md +0 -0
  217. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/skill/irc_client.py +0 -0
  218. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/socket_server.py +0 -0
  219. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/webhook.py +0 -0
  220. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  221. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/claude-code/skills/agentirc/SKILL.md +0 -0
  222. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  223. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
  224. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_acp_daemon.py +0 -0
  225. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_agent_runner.py +0 -0
  226. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_channel.py +0 -0
  227. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_codex_daemon.py +0 -0
  228. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_connection.py +0 -0
  229. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_copilot_daemon.py +0 -0
  230. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_daemon.py +0 -0
  231. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_daemon_config.py +0 -0
  232. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_daemon_ipc.py +0 -0
  233. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_discovery.py +0 -0
  234. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_federation.py +0 -0
  235. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_history.py +0 -0
  236. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_integration_layer5.py +0 -0
  237. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_ipc.py +0 -0
  238. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_irc_transport.py +0 -0
  239. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_link_reconnect.py +0 -0
  240. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_mentions.py +0 -0
  241. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_mesh_config.py +0 -0
  242. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_message.py +0 -0
  243. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_message_buffer.py +0 -0
  244. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_messaging.py +0 -0
  245. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_modes.py +0 -0
  246. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_cli.py +0 -0
  247. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_collector.py +0 -0
  248. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_model.py +0 -0
  249. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_renderer.py +0 -0
  250. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_web.py +0 -0
  251. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_persistence.py +0 -0
  252. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_room_persistence.py +0 -0
  253. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_rooms.py +0 -0
  254. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_rooms_federation.py +0 -0
  255. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_rooms_integration.py +0 -0
  256. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_setup_update_cli.py +0 -0
  257. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_skill_client.py +0 -0
  258. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_skills.py +0 -0
  259. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_socket_server.py +0 -0
  260. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_supervisor.py +0 -0
  261. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_thread_buffer.py +0 -0
  262. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_threads.py +0 -0
  263. {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_webhook.py +0 -0
@@ -18,8 +18,6 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
20
  - uses: actions/checkout@v4
21
- with:
22
- fetch-depth: 0
23
21
 
24
22
  - uses: astral-sh/setup-uv@v4
25
23
 
@@ -53,19 +51,6 @@ jobs:
53
51
  uv run pytest --cov=agentirc --cov-report=xml:coverage.xml --cov-report=term -v
54
52
  continue-on-error: true
55
53
 
56
- - name: SonarCloud Scan
57
- uses: SonarSource/sonarqube-scan-action@v7
58
- env:
59
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
61
- with:
62
- args: >
63
- -Dsonar.projectKey=OriNachum_AgentIRC
64
- -Dsonar.organization=${{ github.repository_owner }}
65
- -Dsonar.python.coverage.reportPaths=coverage.xml
66
- -Dsonar.python.bandit.reportPaths=bandit-results.json
67
- -Dsonar.python.pylint.reportPaths=pylint-results.json
68
-
69
54
  dependency-review:
70
55
  name: Dependency Review
71
56
  if: github.event_name == 'pull_request'
@@ -4,6 +4,37 @@ 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
+ ## [0.21.0] - 2026-04-04
8
+
9
+
10
+ ### Added
11
+
12
+ - Bots framework — server-managed virtual IRC users triggered by external events
13
+ - Inbound webhook support via companion HTTP listener on configurable port
14
+ - Bot CLI commands: create, start, stop, list, inspect
15
+ - Template engine for webhook payload rendering with {body.field} dot-path substitution
16
+ - Custom handler.py support for advanced bot logic
17
+ - Bot visibility in status and overview commands
18
+ - VirtualClient for bot IRC presence in channels
19
+
20
+
21
+ ### Changed
22
+
23
+ - Server now starts a companion HTTP listener for bot webhooks
24
+ - Overview collector and renderer include bot information
25
+ - Channel._local_members() excludes VirtualClient from auto-operator promotion
26
+
27
+ ## [0.20.1] - 2026-04-03
28
+
29
+
30
+ ### Changed
31
+
32
+ - SonarCloud uses Automatic Analysis instead of CI-based scanning — removes conflict and simplifies workflow
33
+
34
+ ### Fixed
35
+
36
+ - Remove SonarCloud CI step that conflicted with Automatic Analysis
37
+
7
38
  ## [0.20.0] - 2026-04-03
8
39
 
9
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 0.20.0
3
+ Version: 0.21.0
4
4
  Summary: 🌱 The space your agents deserve — an autonomous agent mesh where AI agents live, collaborate, and grow
5
5
  Project-URL: Homepage, https://github.com/OriNachum/agentirc
6
6
  Author: Ori Nachum
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
14
14
  Requires-Python: >=3.12
15
+ Requires-Dist: aiohttp>=3.9
15
16
  Requires-Dist: anthropic>=0.40
16
17
  Requires-Dist: claude-agent-sdk>=0.1
17
18
  Requires-Dist: mistune>=3.0
@@ -0,0 +1,148 @@
1
+ """Bot entity — ties together config, virtual client, and handler logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from agentirc.bots.config import BOTS_DIR, BotConfig
11
+ from agentirc.bots.template_engine import render_fallback, render_template
12
+ from agentirc.bots.virtual_client import VirtualClient
13
+
14
+ if TYPE_CHECKING:
15
+ from agentirc.server.ircd import IRCd
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Bot:
21
+ """A single bot instance managed by the server."""
22
+
23
+ def __init__(self, config: BotConfig, server: IRCd):
24
+ self.config = config
25
+ self.server = server
26
+ self.virtual_client: VirtualClient | None = None
27
+ self.active: bool = False
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ return self.config.name
32
+
33
+ @property
34
+ def webhook_url(self) -> str:
35
+ """Webhook URL always uses localhost since the listener binds to 127.0.0.1."""
36
+ port = self.server.config.webhook_port
37
+ return f"http://localhost:{port}/{self.config.name}"
38
+
39
+ async def start(self) -> None:
40
+ """Activate the bot: create virtual client and join channels."""
41
+ if self.active:
42
+ return
43
+
44
+ # Check for nick collision
45
+ if self.server.get_client(self.config.name):
46
+ raise ValueError(f"Nick {self.config.name!r} already in use")
47
+
48
+ self.virtual_client = VirtualClient(
49
+ nick=self.config.name,
50
+ user=self.config.name.split("-")[-1],
51
+ server=self.server,
52
+ )
53
+
54
+ for channel in self.config.channels:
55
+ await self.virtual_client.join_channel(channel)
56
+
57
+ self.active = True
58
+ logger.info("Bot %s started", self.config.name)
59
+
60
+ async def stop(self) -> None:
61
+ """Deactivate the bot: part channels and remove virtual client."""
62
+ if not self.active or not self.virtual_client:
63
+ return
64
+
65
+ for channel_name in list(ch.name for ch in self.virtual_client.channels):
66
+ await self.virtual_client.part_channel(channel_name)
67
+
68
+ self.virtual_client = None
69
+ self.active = False
70
+ logger.info("Bot %s stopped", self.config.name)
71
+
72
+ async def handle(self, payload: dict) -> str:
73
+ """Process an incoming webhook payload.
74
+
75
+ Returns the rendered message text.
76
+ """
77
+ if not self.active or not self.virtual_client:
78
+ raise RuntimeError(f"Bot {self.config.name} is not active")
79
+
80
+ # Try custom handler first
81
+ handler_path = BOTS_DIR / self.config.name / "handler.py"
82
+ if handler_path.is_file():
83
+ message = await self._run_custom_handler(handler_path, payload)
84
+ if message is None:
85
+ return "" # Handler chose to drop this event
86
+ else:
87
+ message = self._render_message(payload)
88
+
89
+ if not message:
90
+ return ""
91
+
92
+ # Prepend @mention if configured
93
+ if self.config.mention:
94
+ message = f"@{self.config.mention} {message}"
95
+
96
+ # Send to configured channels
97
+ for channel in self.config.channels:
98
+ await self.virtual_client.send_to_channel(channel, message)
99
+
100
+ # DM the owner if configured
101
+ if self.config.dm_owner and self.config.owner:
102
+ await self.virtual_client.send_dm(self.config.owner, message)
103
+
104
+ return message
105
+
106
+ def _render_message(self, payload: dict) -> str:
107
+ """Render message using template or fallback."""
108
+ if self.config.template:
109
+ rendered = render_template(self.config.template, payload)
110
+ if rendered is not None:
111
+ return rendered.strip()
112
+ return render_fallback(payload, self.config.fallback)
113
+
114
+ async def _run_custom_handler(
115
+ self,
116
+ handler_path: Path,
117
+ payload: dict,
118
+ ) -> str | None:
119
+ """Load and execute a custom handler.py.
120
+
121
+ Security: handler_path is always constructed as
122
+ BOTS_DIR / self.config.name / "handler.py" — the bot name
123
+ comes from a validated YAML config on disk, not from user input
124
+ or webhook payloads. This is equivalent to loading a plugin from
125
+ a trusted directory under ~/.agentirc/bots/.
126
+ """
127
+ # Verify the handler is inside the bots directory
128
+ try:
129
+ handler_path.resolve().relative_to(BOTS_DIR.resolve())
130
+ except ValueError:
131
+ logger.error("handler.py path %s is outside bots dir", handler_path)
132
+ return self._render_message(payload)
133
+
134
+ try:
135
+ spec = importlib.util.spec_from_file_location(
136
+ f"bot_handler_{self.config.name}",
137
+ handler_path,
138
+ )
139
+ module = importlib.util.module_from_spec(spec)
140
+ spec.loader.exec_module(module) # noqa: S102
141
+ handle_fn = getattr(module, "handle", None)
142
+ if handle_fn is None:
143
+ logger.error("handler.py for %s has no handle() function", self.config.name)
144
+ return self._render_message(payload)
145
+ return await handle_fn(payload, self)
146
+ except Exception:
147
+ logger.exception("Custom handler failed for bot %s", self.config.name)
148
+ return self._render_message(payload)
@@ -0,0 +1,102 @@
1
+ """BotManager — central registry for bot lifecycle and webhook dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from agentirc.bots.bot import Bot
9
+ from agentirc.bots.config import BOTS_DIR, BotConfig, load_bot_config, save_bot_config
10
+
11
+ if TYPE_CHECKING:
12
+ from agentirc.server.ircd import IRCd
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BotManager:
18
+ """Loads, starts, stops, and dispatches webhooks to bots."""
19
+
20
+ def __init__(self, server: IRCd):
21
+ self.server = server
22
+ self.bots: dict[str, Bot] = {} # name -> Bot
23
+
24
+ async def load_bots(self) -> None:
25
+ """Scan ~/.agentirc/bots/ and load all bot definitions."""
26
+ if not BOTS_DIR.is_dir():
27
+ return
28
+
29
+ for bot_dir in sorted(BOTS_DIR.iterdir()):
30
+ yaml_path = bot_dir / "bot.yaml"
31
+ if not yaml_path.is_file():
32
+ continue
33
+ try:
34
+ config = load_bot_config(yaml_path)
35
+ bot = Bot(config, self.server)
36
+ self.bots[config.name] = bot
37
+ await bot.start()
38
+ logger.info("Loaded bot %s", config.name)
39
+ except Exception:
40
+ logger.exception("Failed to load bot from %s", bot_dir)
41
+
42
+ async def create_bot(self, config: BotConfig) -> Bot:
43
+ """Create a new bot: write config to disk and start it."""
44
+ bot_dir = BOTS_DIR / config.name
45
+ save_bot_config(bot_dir / "bot.yaml", config)
46
+
47
+ bot = Bot(config, self.server)
48
+ self.bots[config.name] = bot
49
+ await bot.start()
50
+ return bot
51
+
52
+ async def start_bot(self, name: str) -> None:
53
+ """Start an existing stopped bot."""
54
+ bot = self.bots.get(name)
55
+ if not bot:
56
+ # Try loading from disk
57
+ yaml_path = BOTS_DIR / name / "bot.yaml"
58
+ if not yaml_path.is_file():
59
+ raise ValueError(f"Bot {name!r} not found")
60
+ config = load_bot_config(yaml_path)
61
+ bot = Bot(config, self.server)
62
+ self.bots[name] = bot
63
+
64
+ await bot.start()
65
+
66
+ async def stop_bot(self, name: str) -> None:
67
+ """Stop a running bot."""
68
+ bot = self.bots.get(name)
69
+ if not bot:
70
+ raise ValueError(f"Bot {name!r} not found")
71
+ await bot.stop()
72
+
73
+ async def stop_all(self) -> None:
74
+ """Stop all active bots."""
75
+ for bot in list(self.bots.values()):
76
+ try:
77
+ await bot.stop()
78
+ except Exception:
79
+ logger.exception("Failed to stop bot %s", bot.name)
80
+
81
+ def get_bot(self, name: str) -> Bot | None:
82
+ return self.bots.get(name)
83
+
84
+ def list_bots(self, owner: str | None = None) -> list[Bot]:
85
+ """List bots, optionally filtered by owner."""
86
+ bots = list(self.bots.values())
87
+ if owner:
88
+ bots = [b for b in bots if b.config.owner == owner]
89
+ return sorted(bots, key=lambda b: b.name)
90
+
91
+ async def dispatch(self, bot_name: str, payload: dict) -> str:
92
+ """Route an incoming webhook payload to the named bot.
93
+
94
+ Returns the rendered message text.
95
+ Raises ValueError if bot not found, RuntimeError if bot not active.
96
+ """
97
+ bot = self.bots.get(bot_name)
98
+ if not bot:
99
+ raise ValueError(f"Bot {bot_name!r} not found")
100
+ if not bot.active:
101
+ raise RuntimeError(f"Bot {bot_name!r} is not active")
102
+ return await bot.handle(payload)
@@ -0,0 +1,98 @@
1
+ """Bot configuration dataclasses and YAML loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ BOTS_DIR = Path(os.path.expanduser("~/.agentirc/bots"))
13
+
14
+
15
+ @dataclass
16
+ class BotConfig:
17
+ """Configuration for a single bot."""
18
+
19
+ name: str = ""
20
+ owner: str = ""
21
+ description: str = ""
22
+ created: str = ""
23
+ trigger_type: str = "webhook"
24
+ channels: list[str] = field(default_factory=list)
25
+ dm_owner: bool = False
26
+ mention: str | None = None
27
+ template: str | None = None
28
+ fallback: str = "json"
29
+
30
+ @property
31
+ def has_handler(self) -> bool:
32
+ """Whether a custom handler.py exists for this bot."""
33
+ bot_dir = BOTS_DIR / self.name
34
+ return (bot_dir / "handler.py").is_file()
35
+
36
+
37
+ def load_bot_config(path: Path) -> BotConfig:
38
+ """Load a bot config from a bot.yaml file."""
39
+ with open(path) as f:
40
+ raw = yaml.safe_load(f) or {}
41
+
42
+ bot_section = raw.get("bot", {})
43
+ trigger_section = raw.get("trigger", {})
44
+ output_section = raw.get("output", {})
45
+
46
+ return BotConfig(
47
+ name=bot_section.get("name", ""),
48
+ owner=bot_section.get("owner", ""),
49
+ description=bot_section.get("description", ""),
50
+ created=bot_section.get("created", ""),
51
+ trigger_type=trigger_section.get("type", "webhook"),
52
+ channels=output_section.get("channels", []),
53
+ dm_owner=output_section.get("dm_owner", False),
54
+ mention=output_section.get("mention"),
55
+ template=output_section.get("template"),
56
+ fallback=output_section.get("fallback", "json"),
57
+ )
58
+
59
+
60
+ def save_bot_config(path: Path, config: BotConfig) -> None:
61
+ """Serialize a BotConfig to YAML and write atomically."""
62
+ path.parent.mkdir(parents=True, exist_ok=True)
63
+
64
+ data = {
65
+ "bot": {
66
+ "name": config.name,
67
+ "owner": config.owner,
68
+ "description": config.description,
69
+ "created": config.created,
70
+ },
71
+ "trigger": {
72
+ "type": config.trigger_type,
73
+ },
74
+ "output": {
75
+ "channels": config.channels,
76
+ "dm_owner": config.dm_owner,
77
+ "mention": config.mention,
78
+ "template": config.template,
79
+ "fallback": config.fallback,
80
+ },
81
+ }
82
+
83
+ yaml_str = yaml.dump(data, default_flow_style=False)
84
+
85
+ fd, tmp_path = tempfile.mkstemp(
86
+ dir=str(path.parent),
87
+ suffix=".yaml.tmp",
88
+ )
89
+ try:
90
+ with os.fdopen(fd, "w") as f:
91
+ f.write(yaml_str)
92
+ os.replace(tmp_path, str(path))
93
+ except BaseException:
94
+ try:
95
+ os.unlink(tmp_path)
96
+ except OSError:
97
+ pass
98
+ raise
@@ -0,0 +1,84 @@
1
+ """Companion HTTP server for receiving inbound webhook POSTs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import TYPE_CHECKING
8
+
9
+ from aiohttp import web
10
+
11
+ if TYPE_CHECKING:
12
+ from agentirc.bots.bot_manager import BotManager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class HttpListener:
18
+ """Lightweight HTTP server that routes webhook POSTs to bots."""
19
+
20
+ def __init__(self, bot_manager: BotManager, host: str, port: int):
21
+ self.bot_manager = bot_manager
22
+ self.host = host
23
+ self.port = port
24
+ self._app: web.Application | None = None
25
+ self._runner: web.AppRunner | None = None
26
+
27
+ async def start(self) -> None:
28
+ self._app = web.Application()
29
+ self._app.router.add_get("/health", self._handle_health)
30
+ self._app.router.add_post("/{bot_name}", self._handle_webhook)
31
+
32
+ self._runner = web.AppRunner(self._app)
33
+ await self._runner.setup()
34
+ site = web.TCPSite(self._runner, self.host, self.port)
35
+ await site.start()
36
+ logger.info("Webhook HTTP listener started on %s:%d", self.host, self.port)
37
+
38
+ async def stop(self) -> None:
39
+ if self._runner:
40
+ await self._runner.cleanup()
41
+ self._runner = None
42
+ self._app = None
43
+
44
+ async def _handle_health(self, request: web.Request) -> web.Response:
45
+ return web.json_response({"status": "ok"})
46
+
47
+ async def _handle_webhook(self, request: web.Request) -> web.Response:
48
+ bot_name = request.match_info["bot_name"]
49
+
50
+ # Parse JSON body
51
+ try:
52
+ payload = await request.json()
53
+ except (json.JSONDecodeError, Exception):
54
+ return web.json_response(
55
+ {"error": "invalid JSON"},
56
+ status=400,
57
+ )
58
+
59
+ if not isinstance(payload, dict):
60
+ return web.json_response(
61
+ {"error": "payload must be a JSON object"},
62
+ status=400,
63
+ )
64
+
65
+ # Dispatch to bot
66
+ try:
67
+ message = await self.bot_manager.dispatch(bot_name, payload)
68
+ return web.json_response({"ok": True, "message": message})
69
+ except ValueError:
70
+ return web.json_response(
71
+ {"error": "bot not found"},
72
+ status=404,
73
+ )
74
+ except RuntimeError:
75
+ return web.json_response(
76
+ {"error": "bot not active"},
77
+ status=503,
78
+ )
79
+ except Exception:
80
+ logger.exception("Webhook handler error for bot %s", bot_name)
81
+ return web.json_response(
82
+ {"error": "internal error"},
83
+ status=500,
84
+ )
@@ -0,0 +1,63 @@
1
+ """Simple dot-path template engine for bot message rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+
8
+ _TOKEN_RE = re.compile(r"\{(body(?:\.[^}]+)?)\}")
9
+
10
+
11
+ def _resolve_path(data: dict, path: str) -> str | None:
12
+ """Walk a dot-separated path into a nested dict.
13
+
14
+ Returns the string representation of the value, or None if any
15
+ segment is missing.
16
+ """
17
+ parts = path.split(".")
18
+ current = data
19
+ for part in parts:
20
+ if isinstance(current, dict) and part in current:
21
+ current = current[part]
22
+ else:
23
+ return None
24
+ if current is None:
25
+ return "null"
26
+ return str(current)
27
+
28
+
29
+ def render_template(template: str, payload: dict) -> str | None:
30
+ """Render a template string with {body.field.subfield} tokens.
31
+
32
+ Args:
33
+ template: Template string with {body.x.y} placeholders.
34
+ payload: The webhook JSON payload (accessible as ``body``).
35
+
36
+ Returns:
37
+ The rendered string, or None if any token could not be resolved
38
+ (caller should fall back based on the bot's ``fallback`` config).
39
+ """
40
+ wrapper = {"body": payload}
41
+
42
+ def _replace(match: re.Match) -> str:
43
+ path = match.group(1)
44
+ value = _resolve_path(wrapper, path)
45
+ if value is None:
46
+ raise _UnresolvedToken(path)
47
+ return value
48
+
49
+ try:
50
+ return _TOKEN_RE.sub(_replace, template)
51
+ except _UnresolvedToken:
52
+ return None
53
+
54
+
55
+ def render_fallback(payload: dict, mode: str = "json") -> str:
56
+ """Render a payload using the fallback mode."""
57
+ if mode == "json":
58
+ return json.dumps(payload, indent=None, ensure_ascii=False)
59
+ return str(payload)
60
+
61
+
62
+ class _UnresolvedToken(Exception):
63
+ pass