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.
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/security-checks.yml +0 -15
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/CHANGELOG.md +31 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/PKG-INFO +2 -1
- agentirc_cli-0.21.0/agentirc/bots/bot.py +148 -0
- agentirc_cli-0.21.0/agentirc/bots/bot_manager.py +102 -0
- agentirc_cli-0.21.0/agentirc/bots/config.py +98 -0
- agentirc_cli-0.21.0/agentirc/bots/http_listener.py +84 -0
- agentirc_cli-0.21.0/agentirc/bots/template_engine.py +63 -0
- agentirc_cli-0.21.0/agentirc/bots/virtual_client.py +193 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/cli.py +419 -86
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/collector.py +73 -27
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/model.py +20 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/renderer_text.py +28 -4
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/channel.py +7 -3
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/config.py +1 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/ircd.py +56 -26
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/SECURITY.md +2 -9
- agentirc_cli-0.21.0/docs/bots.md +129 -0
- agentirc_cli-0.21.0/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +353 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/pyproject.toml +2 -1
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/sonar-project.properties +1 -1
- agentirc_cli-0.21.0/tests/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/conftest.py +32 -11
- agentirc_cli-0.21.0/tests/test_bot.py +174 -0
- agentirc_cli-0.21.0/tests/test_bot_config.py +98 -0
- agentirc_cli-0.21.0/tests/test_bot_manager.py +147 -0
- agentirc_cli-0.21.0/tests/test_bots_integration.py +161 -0
- agentirc_cli-0.21.0/tests/test_http_listener.py +147 -0
- agentirc_cli-0.21.0/tests/test_template_engine.py +80 -0
- agentirc_cli-0.21.0/tests/test_virtual_client.py +176 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/uv.lock +486 -1
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.flake8 +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.gitignore +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/.pylintrc +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/CLAUDE.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/CNAME +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/Gemfile +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/LICENSE +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/README.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/SECURITY.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/_config.yml +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/__main__.py +0 -0
- {agentirc_cli-0.20.0/agentirc/clients → agentirc_cli-0.21.0/agentirc/bots}/__init__.py +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/acp → agentirc_cli-0.21.0/agentirc/clients}/__init__.py +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/acp/skill → agentirc_cli-0.21.0/agentirc/clients/acp}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/irc_transport.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/claude → agentirc_cli-0.21.0/agentirc/clients/acp/skill}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/socket_server.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/supervisor.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/acp/webhook.py +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/claude/skill → agentirc_cli-0.21.0/agentirc/clients/claude}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/codex → agentirc_cli-0.21.0/agentirc/clients/claude/skill}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/codex/skill → agentirc_cli-0.21.0/agentirc/clients/codex}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/copilot → agentirc_cli-0.21.0/agentirc/clients/codex/skill}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/supervisor.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.20.0/agentirc/clients/copilot/skill → agentirc_cli-0.21.0/agentirc/clients/copilot}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-0.20.0/agentirc/protocol → agentirc_cli-0.21.0/agentirc/clients/copilot/skill}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/clients/copilot/webhook.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/credentials.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/learn_prompt.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/mesh_config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/renderer_web.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/overview/web/style.css +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/persistence.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.20.0/agentirc/server → agentirc_cli-0.21.0/agentirc/protocol}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/tags.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/extensions/threads.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.20.0/agentirc/server/skills → agentirc_cli-0.21.0/agentirc/server}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/room_store.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/rooms_util.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/server_link.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skill.py +0 -0
- {agentirc_cli-0.20.0/tests → agentirc_cli-0.21.0/agentirc/server/skills}/__init__.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skills/rooms.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/skills/threads.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/server/thread_store.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/agentirc/skills/agentirc/SKILL.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/agentic-self-learn.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/ci.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/cli.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/acp/overview.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/copilot-backend.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/design.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/grow-your-agent.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/harness-conformance.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/ops-tooling.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/overview.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/rooms.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/threads.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases/10-grow-your-agent.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/index.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/claude-code/skills/agentirc/SKILL.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-0.20.0 → agentirc_cli-0.21.0}/tests/test_threads.py +0 -0
- {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.
|
|
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
|