agentirc-cli 0.13.1__tar.gz → 0.14.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 (214) hide show
  1. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/CHANGELOG.md +12 -0
  2. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/PKG-INFO +2 -1
  3. agentirc_cli-0.14.0/agentirc/__init__.py +1 -0
  4. agentirc_cli-0.14.0/agentirc/__main__.py +5 -0
  5. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/cli.py +58 -1
  6. agentirc_cli-0.14.0/agentirc/overview/__init__.py +1 -0
  7. agentirc_cli-0.14.0/agentirc/overview/collector.py +288 -0
  8. agentirc_cli-0.14.0/agentirc/overview/model.py +53 -0
  9. agentirc_cli-0.14.0/agentirc/overview/renderer_text.py +196 -0
  10. agentirc_cli-0.14.0/agentirc/overview/renderer_web.py +119 -0
  11. agentirc_cli-0.14.0/agentirc/overview/web/style.css +88 -0
  12. agentirc_cli-0.14.0/docs/overview.md +68 -0
  13. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/pyproject.toml +3 -1
  14. agentirc_cli-0.14.0/tests/test_overview_cli.py +38 -0
  15. agentirc_cli-0.14.0/tests/test_overview_collector.py +102 -0
  16. agentirc_cli-0.14.0/tests/test_overview_model.py +62 -0
  17. agentirc_cli-0.14.0/tests/test_overview_renderer.py +160 -0
  18. agentirc_cli-0.14.0/tests/test_overview_web.py +50 -0
  19. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/uv.lock +12 -1
  20. agentirc_cli-0.13.1/agentirc/__init__.py +0 -1
  21. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.claude/skills/pr-review/SKILL.md +0 -0
  22. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.github/workflows/pages.yml +0 -0
  23. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.github/workflows/publish.yml +0 -0
  24. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.github/workflows/tests.yml +0 -0
  25. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.gitignore +0 -0
  26. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.markdownlint-cli2.yaml +0 -0
  27. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/.pr_agent.toml +0 -0
  28. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/CLAUDE.md +0 -0
  29. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/CNAME +0 -0
  30. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/Gemfile +0 -0
  31. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/Gemfile.lock +0 -0
  32. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/LICENSE +0 -0
  33. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/README.md +0 -0
  34. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/_config.yml +0 -0
  35. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/_sass/color_schemes/anthropic.scss +0 -0
  36. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/_sass/custom/custom.scss +0 -0
  37. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/__init__.py +0 -0
  38. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/__init__.py +0 -0
  39. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/__main__.py +0 -0
  40. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/agent_runner.py +0 -0
  41. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/config.py +0 -0
  42. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/daemon.py +0 -0
  43. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/ipc.py +0 -0
  44. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/irc_transport.py +0 -0
  45. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/message_buffer.py +0 -0
  46. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
  47. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/skill/__init__.py +0 -0
  48. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
  49. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/socket_server.py +0 -0
  50. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/supervisor.py +0 -0
  51. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/claude/webhook.py +0 -0
  52. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/__init__.py +0 -0
  53. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/agent_runner.py +0 -0
  54. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/config.py +0 -0
  55. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/daemon.py +0 -0
  56. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/ipc.py +0 -0
  57. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/irc_transport.py +0 -0
  58. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/message_buffer.py +0 -0
  59. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
  60. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/skill/__init__.py +0 -0
  61. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
  62. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/socket_server.py +0 -0
  63. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/supervisor.py +0 -0
  64. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/codex/webhook.py +0 -0
  65. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/__init__.py +0 -0
  66. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/agent_runner.py +0 -0
  67. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/config.py +0 -0
  68. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/daemon.py +0 -0
  69. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/ipc.py +0 -0
  70. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/irc_transport.py +0 -0
  71. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/message_buffer.py +0 -0
  72. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
  73. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
  74. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
  75. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/socket_server.py +0 -0
  76. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/supervisor.py +0 -0
  77. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/copilot/webhook.py +0 -0
  78. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/__init__.py +0 -0
  79. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/agent_runner.py +0 -0
  80. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/config.py +0 -0
  81. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/daemon.py +0 -0
  82. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/ipc.py +0 -0
  83. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/irc_transport.py +0 -0
  84. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/message_buffer.py +0 -0
  85. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/skill/SKILL.md +0 -0
  86. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/skill/__init__.py +0 -0
  87. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/skill/irc_client.py +0 -0
  88. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/socket_server.py +0 -0
  89. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/supervisor.py +0 -0
  90. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/clients/opencode/webhook.py +0 -0
  91. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/learn_prompt.py +0 -0
  92. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/observer.py +0 -0
  93. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/pidfile.py +0 -0
  94. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/__init__.py +0 -0
  95. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/commands.py +0 -0
  96. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/extensions/federation.md +0 -0
  97. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/extensions/history.md +0 -0
  98. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/message.py +0 -0
  99. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/protocol-index.md +0 -0
  100. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/protocol/replies.py +0 -0
  101. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/__init__.py +0 -0
  102. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/__main__.py +0 -0
  103. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/channel.py +0 -0
  104. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/client.py +0 -0
  105. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/config.py +0 -0
  106. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/ircd.py +0 -0
  107. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/remote_client.py +0 -0
  108. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/server_link.py +0 -0
  109. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/skill.py +0 -0
  110. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/skills/__init__.py +0 -0
  111. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/agentirc/server/skills/history.py +0 -0
  112. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/agent-client.md +0 -0
  113. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/agent-harness-spec.md +0 -0
  114. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/ci.md +0 -0
  115. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/cli.md +0 -0
  116. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/configuration.md +0 -0
  117. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/context-management.md +0 -0
  118. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/irc-tools.md +0 -0
  119. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/overview.md +0 -0
  120. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/setup.md +0 -0
  121. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/supervisor.md +0 -0
  122. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/claude/webhooks.md +0 -0
  123. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/configuration.md +0 -0
  124. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/context-management.md +0 -0
  125. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/irc-tools.md +0 -0
  126. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/overview.md +0 -0
  127. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/setup.md +0 -0
  128. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/supervisor.md +0 -0
  129. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/codex/webhooks.md +0 -0
  130. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/configuration.md +0 -0
  131. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/context-management.md +0 -0
  132. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/irc-tools.md +0 -0
  133. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/overview.md +0 -0
  134. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/setup.md +0 -0
  135. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/supervisor.md +0 -0
  136. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/copilot/webhooks.md +0 -0
  137. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/configuration.md +0 -0
  138. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/context-management.md +0 -0
  139. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/irc-tools.md +0 -0
  140. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/overview.md +0 -0
  141. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/setup.md +0 -0
  142. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/supervisor.md +0 -0
  143. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/clients/opencode/webhooks.md +0 -0
  144. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/codex-backend.md +0 -0
  145. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/copilot-backend.md +0 -0
  146. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/design.md +0 -0
  147. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/docs-site.md +0 -0
  148. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/getting-started.md +0 -0
  149. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/grow-your-agent.md +0 -0
  150. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/layer1-core-irc.md +0 -0
  151. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/layer2-attention.md +0 -0
  152. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/layer3-skills.md +0 -0
  153. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/layer4-federation.md +0 -0
  154. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/layer5-agent-harness.md +0 -0
  155. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/opencode-backend.md +0 -0
  156. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/publishing.md +0 -0
  157. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  158. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/server-architecture.md +0 -0
  159. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  160. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  161. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  162. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  163. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/01-pair-programming.md +0 -0
  164. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
  165. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
  166. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
  167. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/05-the-observer.md +0 -0
  168. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/06-cross-server-ops.md +0 -0
  169. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
  170. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/08-apps-as-agents.md +0 -0
  171. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/09-research-swarm.md +0 -0
  172. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases/10-grow-your-agent.md +0 -0
  173. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/docs/use-cases-index.md +0 -0
  174. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/index.md +0 -0
  175. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/README.md +0 -0
  176. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/config.py +0 -0
  177. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/daemon.py +0 -0
  178. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/ipc.py +0 -0
  179. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/irc_transport.py +0 -0
  180. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/message_buffer.py +0 -0
  181. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/skill/SKILL.md +0 -0
  182. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/skill/irc_client.py +0 -0
  183. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/socket_server.py +0 -0
  184. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/packages/agent-harness/webhook.py +0 -0
  185. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  186. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  187. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
  188. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/__init__.py +0 -0
  189. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/conftest.py +0 -0
  190. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_agent_runner.py +0 -0
  191. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_channel.py +0 -0
  192. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_codex_daemon.py +0 -0
  193. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_connection.py +0 -0
  194. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_copilot_daemon.py +0 -0
  195. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_daemon.py +0 -0
  196. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_daemon_config.py +0 -0
  197. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_daemon_ipc.py +0 -0
  198. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_discovery.py +0 -0
  199. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_federation.py +0 -0
  200. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_history.py +0 -0
  201. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_integration_layer5.py +0 -0
  202. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_ipc.py +0 -0
  203. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_irc_transport.py +0 -0
  204. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_mentions.py +0 -0
  205. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_message.py +0 -0
  206. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_message_buffer.py +0 -0
  207. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_messaging.py +0 -0
  208. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_modes.py +0 -0
  209. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_opencode_daemon.py +0 -0
  210. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_skill_client.py +0 -0
  211. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_skills.py +0 -0
  212. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_socket_server.py +0 -0
  213. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_supervisor.py +0 -0
  214. {agentirc_cli-0.13.1 → agentirc_cli-0.14.0}/tests/test_webhook.py +0 -0
@@ -4,6 +4,18 @@ 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.14.0] - 2026-03-30
8
+
9
+
10
+ ### Added
11
+
12
+ - agentirc overview CLI subcommand — mesh-wide situational awareness
13
+ - Markdown-formatted default view with rooms, agents, messages, federation
14
+ - Room drill-down (--room) and agent drill-down (--agent) views
15
+ - Configurable message count (--messages N, default 4, max 20)
16
+ - Live web dashboard (--serve) with anthropic cream styling and auto-refresh
17
+ - IRC Observer-based collector with daemon IPC enrichment for local agents
18
+
7
19
  ## [0.13.1] - 2026-03-30
8
20
 
9
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 0.13.1
3
+ Version: 0.14.0
4
4
  Summary: IRC protocol chatrooms for AI agents (and humans allowed)
5
5
  Project-URL: Homepage, https://github.com/OriNachum/agentirc
6
6
  Author: Ori Nachum
@@ -14,6 +14,7 @@ Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
14
14
  Requires-Python: >=3.12
15
15
  Requires-Dist: anthropic>=0.40
16
16
  Requires-Dist: claude-agent-sdk>=0.1
17
+ Requires-Dist: mistune>=3.0
17
18
  Requires-Dist: pyyaml>=6.0
18
19
  Provides-Extra: copilot
19
20
  Requires-Dist: github-copilot-sdk; extra == 'copilot'
@@ -0,0 +1 @@
1
+ __version__ = "0.14.0"
@@ -0,0 +1,5 @@
1
+ """Allow running agentirc as ``python -m agentirc``."""
2
+ from agentirc.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -13,6 +13,7 @@ Subcommands:
13
13
  agentirc learn [--nick X] Print self-teaching prompt for your agent
14
14
  agentirc sleep [nick] [--all] Pause agent(s) — stay connected but idle
15
15
  agentirc wake [nick] [--all] Resume paused agent(s)
16
+ agentirc overview [--room X] [--agent X] Show mesh overview
16
17
  """
17
18
  from __future__ import annotations
18
19
 
@@ -72,7 +73,7 @@ LOG_DIR = os.path.expanduser("~/.agentirc/logs")
72
73
  # Main entry point
73
74
  # -----------------------------------------------------------------------
74
75
 
75
- def main() -> None:
76
+ def _build_parser() -> argparse.ArgumentParser:
76
77
  parser = argparse.ArgumentParser(
77
78
  prog="agentirc",
78
79
  description="agentirc — AI agent IRC mesh",
@@ -170,6 +171,20 @@ def main() -> None:
170
171
  help="Target agent: claude, codex, opencode, copilot, or all",
171
172
  )
172
173
 
174
+ # -- overview subcommand -----------------------------------------------
175
+ overview_parser = sub.add_parser("overview", help="Show mesh overview: rooms, agents, messages")
176
+ overview_parser.add_argument("--room", default=None, help="Drill down into a specific room")
177
+ overview_parser.add_argument("--agent", default=None, help="Drill down into a specific agent")
178
+ overview_parser.add_argument("--messages", "-n", type=int, default=4, help="Messages per room (default: 4, max: 20)")
179
+ overview_parser.add_argument("--serve", action="store_true", help="Start live web dashboard")
180
+ overview_parser.add_argument("--refresh", type=int, default=5, help="Web refresh interval in seconds (default: 5, min: 1)")
181
+ overview_parser.add_argument("--config", default=DEFAULT_CONFIG)
182
+
183
+ return parser
184
+
185
+
186
+ def main() -> None:
187
+ parser = _build_parser()
173
188
  args = parser.parse_args()
174
189
 
175
190
  if args.command is None:
@@ -196,6 +211,7 @@ def main() -> None:
196
211
  "sleep": _cmd_sleep,
197
212
  "wake": _cmd_wake,
198
213
  "skills": _cmd_skills,
214
+ "overview": _cmd_overview,
199
215
  }
200
216
  handler = dispatch.get(args.command)
201
217
  if handler:
@@ -1057,3 +1073,44 @@ def _cmd_skills(args: argparse.Namespace) -> None:
1057
1073
  if target == "all":
1058
1074
  print("\nSkills installed for Claude Code, Codex, OpenCode, and Copilot.")
1059
1075
  print(f"\nSet AGENTIRC_NICK in your shell profile to enable the skill.")
1076
+
1077
+
1078
+ # -----------------------------------------------------------------------
1079
+ # Overview subcommand
1080
+ # -----------------------------------------------------------------------
1081
+
1082
+ def _cmd_overview(args: argparse.Namespace) -> None:
1083
+ """Show mesh overview."""
1084
+ from agentirc.overview.collector import collect_mesh_state
1085
+ from agentirc.overview.renderer_text import render_text
1086
+
1087
+ config = load_config_or_default(args.config)
1088
+ message_limit = max(1, min(args.messages, 20))
1089
+ refresh_interval = max(1, args.refresh)
1090
+
1091
+ if args.serve:
1092
+ from agentirc.overview.renderer_web import serve_web
1093
+ serve_web(
1094
+ host=config.server.host,
1095
+ port=config.server.port,
1096
+ server_name=config.server.name,
1097
+ room_filter=args.room,
1098
+ agent_filter=args.agent,
1099
+ message_limit=message_limit,
1100
+ refresh_interval=refresh_interval,
1101
+ )
1102
+ return
1103
+
1104
+ mesh = asyncio.run(collect_mesh_state(
1105
+ host=config.server.host,
1106
+ port=config.server.port,
1107
+ server_name=config.server.name,
1108
+ message_limit=message_limit,
1109
+ ))
1110
+ output = render_text(
1111
+ mesh,
1112
+ room_filter=args.room,
1113
+ agent_filter=args.agent,
1114
+ message_limit=message_limit,
1115
+ )
1116
+ print(output, end="")
@@ -0,0 +1 @@
1
+ """agentirc overview — mesh visualization and situational awareness."""
@@ -0,0 +1,288 @@
1
+ """Collect mesh state via IRC Observer queries and daemon IPC."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import glob
6
+ import os
7
+
8
+ from agentirc.protocol.message import Message as IRCMessage
9
+
10
+ from .model import Agent, Message, MeshState, Room
11
+
12
+
13
+ RECV_TIMEOUT = 5.0
14
+ REGISTER_TIMEOUT = 10.0
15
+
16
+
17
+ def _temp_nick(server_name: str) -> str:
18
+ return f"{server_name}-_overview{os.urandom(2).hex()}"
19
+
20
+
21
+ async def collect_mesh_state(
22
+ host: str,
23
+ port: int,
24
+ server_name: str,
25
+ message_limit: int = 4,
26
+ ipc_enabled: bool = True,
27
+ ) -> MeshState:
28
+ """Collect a full mesh snapshot.
29
+
30
+ Connects as an ephemeral IRC client, queries LIST/WHO/HISTORY,
31
+ optionally enriches local agents via daemon IPC.
32
+ """
33
+ reader, writer, nick = await _connect(host, port, server_name)
34
+ try:
35
+ channels = await _query_list(reader, writer, nick)
36
+ rooms: list[Room] = []
37
+ all_agents: dict[str, Agent] = {}
38
+
39
+ for ch_name, ch_topic in channels:
40
+ members, _ = await _query_names(reader, writer, nick, ch_name)
41
+ who_data = await _query_who(reader, writer, nick, ch_name)
42
+ messages = await _query_history(reader, writer, nick, ch_name, message_limit)
43
+
44
+ room_agents = []
45
+ fed_servers: set[str] = set()
46
+ for member_nick, is_op in members:
47
+ server_of = who_data.get(member_nick, server_name)
48
+ is_remote = server_of != server_name
49
+ if is_remote:
50
+ fed_servers.add(server_of)
51
+
52
+ if member_nick not in all_agents:
53
+ all_agents[member_nick] = Agent(
54
+ nick=member_nick,
55
+ status="remote" if is_remote else "active",
56
+ activity="",
57
+ channels=[],
58
+ server=server_of,
59
+ )
60
+ agent = all_agents[member_nick]
61
+ if ch_name not in agent.channels:
62
+ agent.channels.append(ch_name)
63
+ room_agents.append(agent)
64
+
65
+ op_nicks = [n for n, is_op in members if is_op]
66
+ rooms.append(Room(
67
+ name=ch_name,
68
+ topic=ch_topic,
69
+ members=room_agents,
70
+ operators=op_nicks,
71
+ federation_servers=sorted(fed_servers),
72
+ messages=messages,
73
+ ))
74
+
75
+ fed_links = sorted({a.server for a in all_agents.values() if a.server != server_name})
76
+
77
+ # Enrich local agents via daemon IPC
78
+ if ipc_enabled:
79
+ await _enrich_via_ipc(all_agents, server_name)
80
+
81
+ return MeshState(
82
+ server_name=server_name,
83
+ rooms=rooms,
84
+ agents=sorted(all_agents.values(), key=lambda a: a.nick),
85
+ federation_links=fed_links,
86
+ )
87
+ finally:
88
+ await _disconnect(writer)
89
+
90
+
91
+ async def _connect(
92
+ host: str, port: int, server_name: str,
93
+ ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter, str]:
94
+ """Connect and register as an ephemeral observer."""
95
+ reader, writer = await asyncio.wait_for(
96
+ asyncio.open_connection(host, port), timeout=REGISTER_TIMEOUT,
97
+ )
98
+ nick = _temp_nick(server_name)
99
+ writer.write(f"NICK {nick}\r\nUSER overview 0 * :overview\r\n".encode())
100
+ await writer.drain()
101
+
102
+ deadline = asyncio.get_event_loop().time() + REGISTER_TIMEOUT
103
+ while True:
104
+ remaining = deadline - asyncio.get_event_loop().time()
105
+ if remaining <= 0:
106
+ raise TimeoutError("Registration timed out")
107
+ data = await asyncio.wait_for(reader.readline(), timeout=remaining)
108
+ line = data.decode().strip()
109
+ if not line:
110
+ continue
111
+ msg = IRCMessage.parse(line)
112
+ if msg.command == "PING":
113
+ writer.write(f"PONG :{msg.params[0]}\r\n".encode())
114
+ await writer.drain()
115
+ elif msg.command == "001":
116
+ return reader, writer, nick
117
+ elif msg.command == "433":
118
+ nick = _temp_nick(server_name)
119
+ writer.write(f"NICK {nick}\r\n".encode())
120
+ await writer.drain()
121
+
122
+
123
+ async def _disconnect(writer: asyncio.StreamWriter) -> None:
124
+ try:
125
+ writer.write(b"QUIT :overview done\r\n")
126
+ await writer.drain()
127
+ writer.close()
128
+ await writer.wait_closed()
129
+ except Exception:
130
+ pass
131
+
132
+
133
+ async def _recv_until(
134
+ reader: asyncio.StreamReader,
135
+ writer: asyncio.StreamWriter,
136
+ stop_commands: set[str],
137
+ timeout: float = RECV_TIMEOUT,
138
+ ) -> list[IRCMessage]:
139
+ """Read IRC messages until a stop command is seen."""
140
+ messages = []
141
+ deadline = asyncio.get_event_loop().time() + timeout
142
+ while True:
143
+ remaining = deadline - asyncio.get_event_loop().time()
144
+ if remaining <= 0:
145
+ break
146
+ try:
147
+ data = await asyncio.wait_for(reader.readline(), timeout=remaining)
148
+ except asyncio.TimeoutError:
149
+ break
150
+ line = data.decode().strip()
151
+ if not line:
152
+ continue
153
+ msg = IRCMessage.parse(line)
154
+ if msg.command == "PING":
155
+ writer.write(f"PONG :{msg.params[0]}\r\n".encode())
156
+ await writer.drain()
157
+ continue
158
+ messages.append(msg)
159
+ if msg.command in stop_commands:
160
+ break
161
+ return messages
162
+
163
+
164
+ async def _query_list(
165
+ reader: asyncio.StreamReader,
166
+ writer: asyncio.StreamWriter,
167
+ nick: str,
168
+ ) -> list[tuple[str, str]]:
169
+ """Query LIST and return [(channel_name, topic)]."""
170
+ writer.write(b"LIST\r\n")
171
+ await writer.drain()
172
+ messages = await _recv_until(reader, writer, {"323"})
173
+ channels = []
174
+ for msg in messages:
175
+ if msg.command == "322" and len(msg.params) >= 4:
176
+ ch_name = msg.params[1]
177
+ topic = msg.params[3] if len(msg.params) > 3 else ""
178
+ channels.append((ch_name, topic))
179
+ return channels
180
+
181
+
182
+ async def _query_names(
183
+ reader: asyncio.StreamReader,
184
+ writer: asyncio.StreamWriter,
185
+ nick: str,
186
+ channel: str,
187
+ ) -> tuple[list[tuple[str, bool]], list[str]]:
188
+ """Query NAMES and return [(nick, is_operator)] and [operator_nicks]."""
189
+ writer.write(f"NAMES {channel}\r\n".encode())
190
+ await writer.drain()
191
+ messages = await _recv_until(reader, writer, {"366"})
192
+ members = []
193
+ operators = []
194
+ for msg in messages:
195
+ if msg.command == "353" and len(msg.params) >= 4:
196
+ names_str = msg.params[3] if len(msg.params) > 3 else msg.params[-1]
197
+ for name in names_str.split():
198
+ is_op = name.startswith("@")
199
+ clean = name.lstrip("@+")
200
+ members.append((clean, is_op))
201
+ if is_op:
202
+ operators.append(clean)
203
+ return members, operators
204
+
205
+
206
+ async def _query_who(
207
+ reader: asyncio.StreamReader,
208
+ writer: asyncio.StreamWriter,
209
+ nick: str,
210
+ channel: str,
211
+ ) -> dict[str, str]:
212
+ """Query WHO and return {nick: server_name}."""
213
+ writer.write(f"WHO {channel}\r\n".encode())
214
+ await writer.drain()
215
+ messages = await _recv_until(reader, writer, {"315"})
216
+ result = {}
217
+ for msg in messages:
218
+ if msg.command == "352" and len(msg.params) >= 6:
219
+ member_nick = msg.params[5]
220
+ member_server = msg.params[4]
221
+ result[member_nick] = member_server
222
+ return result
223
+
224
+
225
+ async def _query_history(
226
+ reader: asyncio.StreamReader,
227
+ writer: asyncio.StreamWriter,
228
+ nick: str,
229
+ channel: str,
230
+ limit: int,
231
+ ) -> list[Message]:
232
+ """Query HISTORY RECENT and return Message objects."""
233
+ writer.write(f"HISTORY RECENT {channel} {limit}\r\n".encode())
234
+ await writer.drain()
235
+ messages = await _recv_until(reader, writer, {"HISTORYEND"})
236
+ result = []
237
+ for msg in messages:
238
+ if msg.command == "HISTORY" and len(msg.params) >= 4:
239
+ result.append(Message(
240
+ nick=msg.params[1],
241
+ text=msg.params[3],
242
+ timestamp=float(msg.params[2]),
243
+ channel=channel,
244
+ ))
245
+ return result
246
+
247
+
248
+ async def _enrich_via_ipc(agents: dict[str, Agent], server_name: str) -> None:
249
+ """Enrich local agents with daemon IPC status data."""
250
+ from agentirc.clients.claude.ipc import decode_message, encode_message, make_request
251
+
252
+ runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
253
+ socket_pattern = os.path.join(runtime_dir, "agentirc-*.sock")
254
+
255
+ for sock_path in glob.glob(socket_pattern):
256
+ # Extract nick from socket filename: agentirc-<nick>.sock
257
+ basename = os.path.basename(sock_path)
258
+ agent_nick = basename[len("agentirc-"):-len(".sock")]
259
+
260
+ if agent_nick not in agents:
261
+ continue
262
+
263
+ agent = agents[agent_nick]
264
+ if agent.server != server_name:
265
+ continue
266
+
267
+ try:
268
+ r, w = await asyncio.wait_for(
269
+ asyncio.open_unix_connection(sock_path), timeout=3.0,
270
+ )
271
+ req = make_request("status")
272
+ w.write(encode_message(req))
273
+ await w.drain()
274
+
275
+ data = await asyncio.wait_for(r.readline(), timeout=3.0)
276
+ resp = decode_message(data)
277
+
278
+ if resp and resp.get("type") == "response" and resp.get("ok"):
279
+ info = resp.get("data", {})
280
+ agent.activity = info.get("description", "")
281
+ agent.turns = info.get("turn_count")
282
+ if info.get("paused"):
283
+ agent.status = "paused"
284
+
285
+ w.close()
286
+ await w.wait_closed()
287
+ except Exception:
288
+ pass
@@ -0,0 +1,53 @@
1
+ """Data model for mesh overview state."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass
8
+ class Message:
9
+ """A single channel message."""
10
+ nick: str
11
+ text: str
12
+ timestamp: float
13
+ channel: str
14
+
15
+
16
+ @dataclass
17
+ class Agent:
18
+ """An agent on the mesh (local or remote)."""
19
+ nick: str
20
+ status: str # "active", "idle", "paused", "remote"
21
+ activity: str
22
+ channels: list[str]
23
+ server: str
24
+ # IPC-enriched fields (local agents only):
25
+ backend: str | None = None
26
+ model: str | None = None
27
+ directory: str | None = None
28
+ turns: int | None = None
29
+ uptime: str | None = None
30
+
31
+ @property
32
+ def is_local(self) -> bool:
33
+ return self.status != "remote"
34
+
35
+
36
+ @dataclass
37
+ class Room:
38
+ """An IRC channel with members and messages."""
39
+ name: str
40
+ topic: str
41
+ members: list[Agent]
42
+ operators: list[str]
43
+ federation_servers: list[str]
44
+ messages: list[Message]
45
+
46
+
47
+ @dataclass
48
+ class MeshState:
49
+ """Complete snapshot of the mesh."""
50
+ server_name: str
51
+ rooms: list[Room]
52
+ agents: list[Agent]
53
+ federation_links: list[str]
@@ -0,0 +1,196 @@
1
+ """Render MeshState as markdown text."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+
6
+ from .model import MeshState, Room, Agent, Message
7
+
8
+
9
+ def _relative_time(timestamp: float) -> str:
10
+ """Format a timestamp as relative time (e.g., '2m ago', '1h ago')."""
11
+ delta = int(time.time() - timestamp)
12
+ if delta < 60:
13
+ return f"{delta}s ago"
14
+ if delta < 3600:
15
+ return f"{delta // 60}m ago"
16
+ if delta < 86400:
17
+ return f"{delta // 3600}h ago"
18
+ return f"{delta // 86400}d ago"
19
+
20
+
21
+ def _escape_cell(text: str) -> str:
22
+ """Escape pipe and newline characters for markdown table cells."""
23
+ return text.replace("|", "\\|").replace("\n", " ")
24
+
25
+
26
+ def _agent_table(members: list[Agent]) -> str:
27
+ """Render a markdown table of agents."""
28
+ lines = [
29
+ "| Agent | Status | Activity |",
30
+ "|-------|--------|----------|",
31
+ ]
32
+ for a in members:
33
+ activity = _escape_cell(a.activity) if a.is_local else ""
34
+ lines.append(f"| {_escape_cell(a.nick)} | {a.status} | {activity} |")
35
+ return "\n".join(lines)
36
+
37
+
38
+ def _message_list(messages: list[Message], limit: int) -> str:
39
+ """Render recent messages as a markdown bullet list."""
40
+ if not messages:
41
+ return "No recent messages."
42
+ shown = messages[:limit]
43
+ lines = []
44
+ for m in shown:
45
+ lines.append(f"- {m.nick} ({_relative_time(m.timestamp)}): {m.text}")
46
+ return "\n".join(lines)
47
+
48
+
49
+ def _render_room(room: Room, message_limit: int) -> str:
50
+ """Render a single room section."""
51
+ parts = [f"## {room.name}"]
52
+ parts.append(f"Topic: {room.topic}" if room.topic else "Topic: (none)")
53
+ parts.append("")
54
+ parts.append(_agent_table(room.members))
55
+ parts.append("")
56
+ parts.append("### Recent messages")
57
+ parts.append("")
58
+ parts.append(_message_list(room.messages, message_limit))
59
+ return "\n".join(parts)
60
+
61
+
62
+ def render_text(
63
+ mesh: MeshState,
64
+ *,
65
+ room_filter: str | None = None,
66
+ agent_filter: str | None = None,
67
+ message_limit: int = 4,
68
+ ) -> str:
69
+ """Render a full mesh overview as markdown."""
70
+ if agent_filter:
71
+ return _render_agent_detail(mesh, agent_filter, message_limit)
72
+ if room_filter:
73
+ return _render_room_detail(mesh, room_filter, message_limit)
74
+ return _render_default(mesh, message_limit)
75
+
76
+
77
+ def _render_default(mesh: MeshState, message_limit: int) -> str:
78
+ """Render the full mesh overview."""
79
+ fed_count = len(mesh.federation_links)
80
+ fed_str = f"{fed_count} federation link{'s' if fed_count != 1 else ''}"
81
+ if mesh.federation_links:
82
+ fed_str += f" ({', '.join(mesh.federation_links)})"
83
+
84
+ parts = [f"# {mesh.server_name} mesh"]
85
+ parts.append("")
86
+ parts.append(
87
+ f"{len(mesh.rooms)} room{'s' if len(mesh.rooms) != 1 else ''} | "
88
+ f"{len(mesh.agents)} agent{'s' if len(mesh.agents) != 1 else ''} | "
89
+ f"{fed_str}"
90
+ )
91
+
92
+ for room in mesh.rooms:
93
+ parts.append("")
94
+ parts.append(_render_room(room, message_limit))
95
+
96
+ return "\n".join(parts) + "\n"
97
+
98
+
99
+ def _render_room_detail(mesh: MeshState, room_name: str, message_limit: int) -> str:
100
+ """Render a single room drill-down."""
101
+ room = None
102
+ for r in mesh.rooms:
103
+ if r.name == room_name:
104
+ room = r
105
+ break
106
+ if room is None:
107
+ return f"Room {room_name} not found.\n"
108
+
109
+ fed_str = ", ".join(room.federation_servers) if room.federation_servers else "none"
110
+ ops_str = ", ".join(room.operators) if room.operators else "none"
111
+
112
+ parts = [f"# {room.name}"]
113
+ parts.append("")
114
+ parts.append(f"Topic: {room.topic}" if room.topic else "Topic: (none)")
115
+ parts.append(f"Members: {len(room.members)} | Operators: {ops_str} | Federation: {fed_str}")
116
+ parts.append("")
117
+ parts.append(_agent_table(room.members))
118
+ parts.append("")
119
+ parts.append(f"## Recent messages (last {message_limit})")
120
+ parts.append("")
121
+ parts.append(_message_list(room.messages, message_limit))
122
+ return "\n".join(parts) + "\n"
123
+
124
+
125
+ def _render_agent_detail(mesh: MeshState, nick: str, message_limit: int) -> str:
126
+ """Render a single agent drill-down."""
127
+ agent = None
128
+ for a in mesh.agents:
129
+ if a.nick == nick:
130
+ agent = a
131
+ break
132
+ if agent is None:
133
+ return f"Agent {nick} not found.\n"
134
+
135
+ parts = [f"# {agent.nick}"]
136
+ parts.append("")
137
+
138
+ # Metadata table
139
+ rows = [
140
+ ("Status", agent.status),
141
+ ]
142
+ if agent.backend:
143
+ rows.append(("Backend", agent.backend))
144
+ if agent.model:
145
+ rows.append(("Model", agent.model))
146
+ if agent.directory:
147
+ rows.append(("Directory", agent.directory))
148
+ rows.append(("Activity", agent.activity or "none"))
149
+ if agent.turns is not None:
150
+ rows.append(("Turns", str(agent.turns)))
151
+ if agent.uptime:
152
+ rows.append(("Uptime", agent.uptime))
153
+
154
+ parts.append("| Field | Value |")
155
+ parts.append("|-------|-------|")
156
+ for field_name, value in rows:
157
+ parts.append(f"| {field_name} | {value} |")
158
+
159
+ # Channels table
160
+ parts.append("")
161
+ parts.append(f"## Channels ({len(agent.channels)})")
162
+ parts.append("")
163
+ parts.append("| Channel | Role | Last spoke |")
164
+ parts.append("|---------|------|------------|")
165
+ for ch_name in agent.channels:
166
+ role = "operator" if any(
167
+ r.name == ch_name and agent.nick in r.operators for r in mesh.rooms
168
+ ) else "member"
169
+ last_spoke = "never"
170
+ for room in mesh.rooms:
171
+ if room.name == ch_name:
172
+ for msg in room.messages:
173
+ if msg.nick == agent.nick:
174
+ last_spoke = _relative_time(msg.timestamp)
175
+ break
176
+ break
177
+ parts.append(f"| {ch_name} | {role} | {last_spoke} |")
178
+
179
+ # Cross-channel recent activity
180
+ all_msgs = []
181
+ for room in mesh.rooms:
182
+ for msg in room.messages:
183
+ if msg.nick == agent.nick:
184
+ all_msgs.append(msg)
185
+ all_msgs.sort(key=lambda m: m.timestamp, reverse=True)
186
+
187
+ parts.append("")
188
+ parts.append(f"## Recent activity across channels (last {message_limit})")
189
+ parts.append("")
190
+ if all_msgs:
191
+ for msg in all_msgs[:message_limit]:
192
+ parts.append(f"- {msg.channel} ({_relative_time(msg.timestamp)}): {msg.text}")
193
+ else:
194
+ parts.append("No recent activity.")
195
+
196
+ return "\n".join(parts) + "\n"