agentirc-cli 0.13.0__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.
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/CHANGELOG.md +22 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/PKG-INFO +2 -1
- agentirc_cli-0.14.0/agentirc/__init__.py +1 -0
- agentirc_cli-0.14.0/agentirc/__main__.py +5 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/cli.py +58 -1
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/irc_transport.py +5 -1
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/irc_transport.py +5 -1
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/agent_runner.py +42 -24
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/daemon.py +21 -2
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/irc_transport.py +5 -1
- agentirc_cli-0.14.0/agentirc/overview/__init__.py +1 -0
- agentirc_cli-0.14.0/agentirc/overview/collector.py +288 -0
- agentirc_cli-0.14.0/agentirc/overview/model.py +53 -0
- agentirc_cli-0.14.0/agentirc/overview/renderer_text.py +196 -0
- agentirc_cli-0.14.0/agentirc/overview/renderer_web.py +119 -0
- agentirc_cli-0.14.0/agentirc/overview/web/style.css +88 -0
- agentirc_cli-0.14.0/docs/overview.md +68 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/irc_transport.py +5 -1
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/pyproject.toml +3 -1
- agentirc_cli-0.14.0/tests/test_overview_cli.py +38 -0
- agentirc_cli-0.14.0/tests/test_overview_collector.py +102 -0
- agentirc_cli-0.14.0/tests/test_overview_model.py +62 -0
- agentirc_cli-0.14.0/tests/test_overview_renderer.py +160 -0
- agentirc_cli-0.14.0/tests/test_overview_web.py +50 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/uv.lock +12 -1
- agentirc_cli-0.13.0/agentirc/__init__.py +0 -1
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.gitignore +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/CLAUDE.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/CNAME +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/Gemfile +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/LICENSE +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/README.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/_config.yml +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/supervisor.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/copilot/webhook.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/message_buffer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/skill/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/skill/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/skill/irc_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/socket_server.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/supervisor.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/clients/opencode/webhook.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/learn_prompt.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/channel.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/ircd.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/server_link.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/skill.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/skills/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/ci.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/cli.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/configuration.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/context-management.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/irc-tools.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/overview.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/setup.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/supervisor.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/clients/opencode/webhooks.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/copilot-backend.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/design.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/grow-your-agent.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/opencode-backend.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases/10-grow-your-agent.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/index.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/__init__.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_opencode_daemon.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.13.0 → agentirc_cli-0.14.0}/tests/test_webhook.py +0 -0
|
@@ -4,6 +4,28 @@ 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
|
+
|
|
19
|
+
## [0.13.1] - 2026-03-30
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fix OpenCode agent crash (exit code -1) caused by 30s timeout on system prompt session/prompt call
|
|
25
|
+
- Capture stderr from opencode subprocess for debugging
|
|
26
|
+
- Add _running guard to busy-wait loops to prevent hang on process death
|
|
27
|
+
- Wrap _start_agent_runner with error handling so runner failures schedule retry instead of crashing daemon
|
|
28
|
+
|
|
7
29
|
## [0.13.0] - 2026-03-29
|
|
8
30
|
|
|
9
31
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 0.
|
|
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"
|
|
@@ -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
|
|
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="")
|
|
@@ -82,11 +82,15 @@ class IRCTransport:
|
|
|
82
82
|
async def send_who(self, target: str) -> None:
|
|
83
83
|
await self._send_raw(f"WHO {target}")
|
|
84
84
|
|
|
85
|
-
async def
|
|
85
|
+
async def send_raw(self, line: str) -> None:
|
|
86
|
+
"""Send a raw IRC line. Public for commands like HISTORY."""
|
|
86
87
|
if self._writer:
|
|
87
88
|
self._writer.write(f"{line}\r\n".encode())
|
|
88
89
|
await self._writer.drain()
|
|
89
90
|
|
|
91
|
+
async def _send_raw(self, line: str) -> None:
|
|
92
|
+
await self.send_raw(line)
|
|
93
|
+
|
|
90
94
|
async def _read_loop(self) -> None:
|
|
91
95
|
buf = ""
|
|
92
96
|
try:
|
|
@@ -82,11 +82,15 @@ class IRCTransport:
|
|
|
82
82
|
async def send_who(self, target: str) -> None:
|
|
83
83
|
await self._send_raw(f"WHO {target}")
|
|
84
84
|
|
|
85
|
-
async def
|
|
85
|
+
async def send_raw(self, line: str) -> None:
|
|
86
|
+
"""Send a raw IRC line. Public for commands like HISTORY."""
|
|
86
87
|
if self._writer:
|
|
87
88
|
self._writer.write(f"{line}\r\n".encode())
|
|
88
89
|
await self._writer.drain()
|
|
89
90
|
|
|
91
|
+
async def _send_raw(self, line: str) -> None:
|
|
92
|
+
await self.send_raw(line)
|
|
93
|
+
|
|
90
94
|
async def _read_loop(self) -> None:
|
|
91
95
|
buf = ""
|
|
92
96
|
try:
|
|
@@ -38,6 +38,7 @@ class OpenCodeAgentRunner:
|
|
|
38
38
|
self._prompt_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
39
39
|
self._task: asyncio.Task | None = None
|
|
40
40
|
self._reader_task: asyncio.Task | None = None
|
|
41
|
+
self._stderr_task: asyncio.Task | None = None
|
|
41
42
|
self._stopping = False
|
|
42
43
|
self._request_id = 0
|
|
43
44
|
self._pending: dict[int, asyncio.Future] = {}
|
|
@@ -68,13 +69,14 @@ class OpenCodeAgentRunner:
|
|
|
68
69
|
"opencode", "acp",
|
|
69
70
|
stdin=asyncio.subprocess.PIPE,
|
|
70
71
|
stdout=asyncio.subprocess.PIPE,
|
|
71
|
-
stderr=asyncio.subprocess.
|
|
72
|
+
stderr=asyncio.subprocess.PIPE,
|
|
72
73
|
limit=1024 * 1024, # 1MB line buffer
|
|
73
74
|
env=isolated_env,
|
|
74
75
|
)
|
|
75
76
|
|
|
76
|
-
# Start reading responses
|
|
77
|
+
# Start reading responses and stderr
|
|
77
78
|
self._reader_task = asyncio.create_task(self._read_loop())
|
|
79
|
+
self._stderr_task = asyncio.create_task(self._stderr_loop())
|
|
78
80
|
|
|
79
81
|
# Initialize with ACP protocol
|
|
80
82
|
resp = await self._send_request("initialize", {
|
|
@@ -102,30 +104,19 @@ class OpenCodeAgentRunner:
|
|
|
102
104
|
self._running = True
|
|
103
105
|
logger.info("OpenCode session started: %s", self._session_id)
|
|
104
106
|
|
|
105
|
-
# Send system prompt as the first turn so all subsequent turns
|
|
106
|
-
# are conditioned on it (ACP has no dedicated system instructions field)
|
|
107
|
-
if self.system_prompt:
|
|
108
|
-
self._busy = True
|
|
109
|
-
resp = await self._send_request("session/prompt", {
|
|
110
|
-
"sessionId": self._session_id,
|
|
111
|
-
"prompt": [{"type": "text", "text": self.system_prompt}],
|
|
112
|
-
})
|
|
113
|
-
# Wait for turn to finish before accepting user prompts
|
|
114
|
-
if resp.get("result", {}).get("stopReason"):
|
|
115
|
-
self._busy = False
|
|
116
|
-
self._accumulated_text = ""
|
|
117
|
-
else:
|
|
118
|
-
while self._busy:
|
|
119
|
-
await asyncio.sleep(0.1)
|
|
120
|
-
|
|
121
107
|
# Start the prompt processing loop
|
|
122
108
|
self._task = asyncio.create_task(self._prompt_loop())
|
|
123
109
|
|
|
110
|
+
# Queue system prompt as the first turn so all subsequent turns
|
|
111
|
+
# are conditioned on it (ACP has no dedicated system instructions field).
|
|
112
|
+
# Queued rather than awaited to avoid blocking start() on LLM completion.
|
|
113
|
+
if self.system_prompt:
|
|
114
|
+
await self.send_prompt(self.system_prompt)
|
|
115
|
+
|
|
124
116
|
if initial_prompt:
|
|
125
117
|
await self.send_prompt(initial_prompt)
|
|
126
118
|
except Exception:
|
|
127
|
-
|
|
128
|
-
self._isolated_home = None
|
|
119
|
+
await self.stop()
|
|
129
120
|
raise
|
|
130
121
|
|
|
131
122
|
async def stop(self) -> None:
|
|
@@ -147,6 +138,13 @@ class OpenCodeAgentRunner:
|
|
|
147
138
|
except asyncio.CancelledError:
|
|
148
139
|
pass
|
|
149
140
|
|
|
141
|
+
if self._stderr_task:
|
|
142
|
+
self._stderr_task.cancel()
|
|
143
|
+
try:
|
|
144
|
+
await self._stderr_task
|
|
145
|
+
except asyncio.CancelledError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
150
148
|
if self._process:
|
|
151
149
|
try:
|
|
152
150
|
self._process.terminate()
|
|
@@ -179,7 +177,7 @@ class OpenCodeAgentRunner:
|
|
|
179
177
|
self._request_id += 1
|
|
180
178
|
return self._request_id
|
|
181
179
|
|
|
182
|
-
async def _send_request(self, method: str, params: dict) -> dict:
|
|
180
|
+
async def _send_request(self, method: str, params: dict, timeout: float = 30) -> dict:
|
|
183
181
|
"""Send a JSON-RPC request and wait for the response."""
|
|
184
182
|
if not self._process or not self._process.stdin:
|
|
185
183
|
raise ConnectionError("ACP server not running")
|
|
@@ -196,7 +194,7 @@ class OpenCodeAgentRunner:
|
|
|
196
194
|
await self._process.stdin.drain()
|
|
197
195
|
|
|
198
196
|
try:
|
|
199
|
-
return await asyncio.wait_for(future, timeout=
|
|
197
|
+
return await asyncio.wait_for(future, timeout=timeout)
|
|
200
198
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
201
199
|
self._pending.pop(req_id, None)
|
|
202
200
|
if not future.done():
|
|
@@ -263,9 +261,29 @@ class OpenCodeAgentRunner:
|
|
|
263
261
|
|
|
264
262
|
self._running = False
|
|
265
263
|
|
|
264
|
+
# Cancel companion tasks so they don't outlive the process
|
|
265
|
+
for task in (self._task, self._stderr_task):
|
|
266
|
+
if task and not task.done():
|
|
267
|
+
task.cancel()
|
|
268
|
+
|
|
266
269
|
if not self._stopping and self.on_exit:
|
|
267
270
|
await self.on_exit(returncode)
|
|
268
271
|
|
|
272
|
+
async def _stderr_loop(self) -> None:
|
|
273
|
+
"""Log stderr output from the opencode process."""
|
|
274
|
+
if not self._process or not self._process.stderr:
|
|
275
|
+
return
|
|
276
|
+
try:
|
|
277
|
+
while True:
|
|
278
|
+
line = await self._process.stderr.readline()
|
|
279
|
+
if not line:
|
|
280
|
+
break
|
|
281
|
+
text = line.decode("utf-8", errors="replace").rstrip()
|
|
282
|
+
if text:
|
|
283
|
+
logger.warning("opencode stderr: %s", text)
|
|
284
|
+
except (asyncio.CancelledError, ConnectionError):
|
|
285
|
+
pass
|
|
286
|
+
|
|
269
287
|
async def _handle_notification(self, msg: dict) -> None:
|
|
270
288
|
"""Handle ACP server notifications."""
|
|
271
289
|
method = msg.get("method", "")
|
|
@@ -321,7 +339,7 @@ class OpenCodeAgentRunner:
|
|
|
321
339
|
resp = await self._send_request("session/prompt", {
|
|
322
340
|
"sessionId": self._session_id,
|
|
323
341
|
"prompt": [{"type": "text", "text": text}],
|
|
324
|
-
})
|
|
342
|
+
}, timeout=120)
|
|
325
343
|
|
|
326
344
|
# Check if response itself signals turn completion
|
|
327
345
|
result = resp.get("result", {})
|
|
@@ -338,7 +356,7 @@ class OpenCodeAgentRunner:
|
|
|
338
356
|
self._busy = False
|
|
339
357
|
|
|
340
358
|
# Wait for turn to complete (via notifications)
|
|
341
|
-
while self._busy:
|
|
359
|
+
while self._busy and self._running:
|
|
342
360
|
await asyncio.sleep(0.1)
|
|
343
361
|
|
|
344
362
|
except Exception:
|
|
@@ -131,7 +131,16 @@ class OpenCodeDaemon:
|
|
|
131
131
|
|
|
132
132
|
# 6. Optionally start the OpenCode agent runner
|
|
133
133
|
if not self.skip_opencode:
|
|
134
|
-
|
|
134
|
+
try:
|
|
135
|
+
await self._start_agent_runner()
|
|
136
|
+
except Exception:
|
|
137
|
+
logger.exception(
|
|
138
|
+
"Failed to start agent runner for %s, scheduling retry",
|
|
139
|
+
self.agent.nick,
|
|
140
|
+
)
|
|
141
|
+
self._agent_runner = None
|
|
142
|
+
self._crash_times.append(time.time())
|
|
143
|
+
asyncio.create_task(self._delayed_restart())
|
|
135
144
|
|
|
136
145
|
# 7. Sleep scheduler background task
|
|
137
146
|
self._sleep_task = asyncio.create_task(self._sleep_scheduler())
|
|
@@ -241,6 +250,8 @@ class OpenCodeDaemon:
|
|
|
241
250
|
on_exit=self._on_agent_exit,
|
|
242
251
|
on_message=self._on_agent_message,
|
|
243
252
|
)
|
|
253
|
+
# Absorb the system prompt response without relaying to IRC
|
|
254
|
+
self._mention_targets.append(None)
|
|
244
255
|
await self._agent_runner.start()
|
|
245
256
|
logger.info("OpenCodeAgentRunner started for %s", self.agent.nick)
|
|
246
257
|
|
|
@@ -366,7 +377,15 @@ class OpenCodeDaemon:
|
|
|
366
377
|
async def _delayed_restart(self) -> None:
|
|
367
378
|
await asyncio.sleep(CRASH_RESTART_DELAY)
|
|
368
379
|
if not self._circuit_open and self._transport is not None:
|
|
369
|
-
|
|
380
|
+
try:
|
|
381
|
+
await self._start_agent_runner()
|
|
382
|
+
except Exception:
|
|
383
|
+
logger.exception(
|
|
384
|
+
"Failed to restart agent runner for %s",
|
|
385
|
+
self.agent.nick,
|
|
386
|
+
)
|
|
387
|
+
# Record as a crash so the circuit breaker can track it
|
|
388
|
+
await self._on_agent_exit(-1)
|
|
370
389
|
|
|
371
390
|
# ------------------------------------------------------------------
|
|
372
391
|
# Supervisor callbacks
|
|
@@ -82,11 +82,15 @@ class IRCTransport:
|
|
|
82
82
|
async def send_who(self, target: str) -> None:
|
|
83
83
|
await self._send_raw(f"WHO {target}")
|
|
84
84
|
|
|
85
|
-
async def
|
|
85
|
+
async def send_raw(self, line: str) -> None:
|
|
86
|
+
"""Send a raw IRC line. Public for commands like HISTORY."""
|
|
86
87
|
if self._writer:
|
|
87
88
|
self._writer.write(f"{line}\r\n".encode())
|
|
88
89
|
await self._writer.drain()
|
|
89
90
|
|
|
91
|
+
async def _send_raw(self, line: str) -> None:
|
|
92
|
+
await self.send_raw(line)
|
|
93
|
+
|
|
90
94
|
async def _read_loop(self) -> None:
|
|
91
95
|
buf = ""
|
|
92
96
|
try:
|
|
@@ -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
|