agentirc-cli 5.0.0__tar.gz → 5.0.2__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-5.0.0 → agentirc_cli-5.0.2}/CHANGELOG.md +26 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/PKG-INFO +1 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/mesh.py +44 -11
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/daemon.py +40 -3
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/irc_transport.py +39 -0
- {agentirc_cli-5.0.0/culture/clients/codex → agentirc_cli-5.0.2/culture/clients/acp}/message_buffer.py +8 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/skill/SKILL.md +22 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/skill/irc_client.py +15 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/daemon.py +40 -3
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/irc_transport.py +39 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/message_buffer.py +8 -0
- {agentirc_cli-5.0.0/plugins/claude-code/skills/irc → agentirc_cli-5.0.2/culture/clients/claude/skill}/SKILL.md +22 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/skill/irc_client.py +15 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/daemon.py +63 -6
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/irc_transport.py +39 -0
- {agentirc_cli-5.0.0/culture/clients/acp → agentirc_cli-5.0.2/culture/clients/codex}/message_buffer.py +8 -0
- {agentirc_cli-5.0.0/plugins/codex/skills/culture-irc → agentirc_cli-5.0.2/culture/clients/codex/skill}/SKILL.md +22 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/skill/irc_client.py +15 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/daemon.py +40 -3
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/irc_transport.py +39 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/message_buffer.py +8 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/skill/SKILL.md +22 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/skill/irc_client.py +15 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/credentials.py +11 -6
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/agent-harness-spec.md +2 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/daemon.py +50 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/irc_transport.py +39 -0
- agentirc_cli-5.0.2/packages/agent-harness/message_buffer.py +71 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/skill/SKILL.md +7 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/skill/irc_client.py +15 -1
- {agentirc_cli-5.0.0/culture/clients/claude/skill → agentirc_cli-5.0.2/plugins/claude-code/skills/irc}/SKILL.md +22 -0
- {agentirc_cli-5.0.0/culture/clients/codex/skill → agentirc_cli-5.0.2/plugins/codex/skills/culture-irc}/SKILL.md +22 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/pyproject.toml +1 -1
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_codex_daemon.py +32 -0
- agentirc_cli-5.0.2/tests/test_credentials.py +24 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_daemon_ipc.py +57 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_irc_transport.py +90 -0
- agentirc_cli-5.0.2/tests/test_mention_warning.py +62 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/uv.lock +1 -1
- agentirc_cli-5.0.0/packages/agent-harness/message_buffer.py +0 -63
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.flake8 +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.gitignore +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.pr_agent.toml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/.pylintrc +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/CLAUDE.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/CNAME +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/Gemfile +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/Gemfile.lock +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/LICENSE +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/README.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/SECURITY.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/_config.yml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/_includes/head_custom.html +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/assets/images/apple-touch-icon.png +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/assets/images/favicon-16x16.png +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/assets/images/favicon-32x32.png +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/assets/images/favicon.ico +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/__main__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/aio.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/bot.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/agent.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/bot.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/channel.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/constants.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/display.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/formatting.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/mesh.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/shared/process.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/cli/skills.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/culture.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/culture.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/culture.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/culture.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/app.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/commands.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/formatting.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/learn_prompt.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/mesh_config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/observer.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/overview/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/overview/collector.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/overview/model.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/overview/web/style.css +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/persistence.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/pidfile.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/commands.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/extensions/history.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/message.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/protocol/replies.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/__main__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/channel.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/history_store.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/ircd.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/remote_client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/room_store.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/rooms_util.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/server_link.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/skill.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/skills/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/skills/history.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/skills/icon.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/skills/rooms.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/skills/threads.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/server/thread_store.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/agent-lifecycle.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/agentic-self-learn.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/agents/decentralized-config.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/agent-client.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/harness-conformance.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/index.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/layer1-core-irc.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/layer2-attention.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/layer3-skills.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/layer4-federation.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/layer5-agent-harness.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/server-architecture.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/architecture/threads.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/channel-polling.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/acp/overview.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/acp/system-prompt.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/culture-cli.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/getting-started.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/index.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/SECURITY.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/bots.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/ci.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/cli.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/docs-site.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/index.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/ops-tooling.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/overview.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/operations/publishing.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/persistent-history.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/reflective-development.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/rooms.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/server-rename.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/use-cases-index.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/docs/what-is-culture.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/favicon.ico +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/culture.yaml +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/sonar-project.properties +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/__init__.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/conftest.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_archive.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_bot.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_bot_config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_channel.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_connection.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_console_client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_console_commands.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_console_connection.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_console_icons.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_console_integration.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_culture_config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_daemon.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_discovery.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_display.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_federation.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_history.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_http_listener.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_ipc.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_learn_prompt.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_mention_alias.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_mention_target_cleanup.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_mentions.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_message.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_messaging.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_migrate_cli.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_modes.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_overview_model.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_overview_web.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_persistence.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_pidfile.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_poll_loop.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_register_cli.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_rooms.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_skill_client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_skill_docs.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_skills.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_socket_server.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_supervisor.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_template_engine.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_threads.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_wait_for_port.py +0 -0
- {agentirc_cli-5.0.0 → agentirc_cli-5.0.2}/tests/test_webhook.py +0 -0
|
@@ -4,6 +4,32 @@ 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
|
+
## [5.0.2] - 2026-04-09
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Handle missing credential tool (secret-tool/security/powershell) gracefully instead of crashing the server
|
|
13
|
+
- Report restart failures in mesh update instead of claiming success
|
|
14
|
+
|
|
15
|
+
## [5.0.1] - 2026-04-09
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Topic subcommand for IRC skill (#192)
|
|
21
|
+
- @mention validation warnings for unknown nicks (#196)
|
|
22
|
+
- GitHub issues skill for Claude Code
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Whitespace-only messages now rejected (#195)
|
|
28
|
+
- join/part channel state desync with # prefix validation (#194)
|
|
29
|
+
- Sending to unjoined channels now returns error (#193)
|
|
30
|
+
- Agents can now read own messages in channel history (#191)
|
|
31
|
+
- Codex backend meta-response stripping (#197)
|
|
32
|
+
|
|
7
33
|
## [5.0.0] - 2026-04-09
|
|
8
34
|
|
|
9
35
|
|
|
@@ -469,16 +469,18 @@ def _upgrade_culture_package(args: argparse.Namespace) -> bool:
|
|
|
469
469
|
os.execvp(culture_bin, reexec_args)
|
|
470
470
|
|
|
471
471
|
|
|
472
|
-
def _wait_for_server_port(port: int, retries: int = 50, interval: float = 0.1) ->
|
|
473
|
-
"""Poll until *port* accepts a TCP connection."""
|
|
472
|
+
def _wait_for_server_port(host: str, port: int, retries: int = 50, interval: float = 0.1) -> bool:
|
|
473
|
+
"""Poll until *host*:*port* accepts a TCP connection. Returns True on success."""
|
|
474
474
|
import socket as _socket
|
|
475
475
|
|
|
476
|
+
probe = "localhost" if host in ("0.0.0.0", "::", "") else host
|
|
476
477
|
for _ in range(retries):
|
|
477
478
|
try:
|
|
478
|
-
with _socket.create_connection((
|
|
479
|
-
return
|
|
479
|
+
with _socket.create_connection((probe, port), timeout=1):
|
|
480
|
+
return True
|
|
480
481
|
except OSError:
|
|
481
482
|
time.sleep(interval)
|
|
483
|
+
return False
|
|
482
484
|
|
|
483
485
|
|
|
484
486
|
def _dry_run_restart(mesh, server_name: str) -> None:
|
|
@@ -509,13 +511,16 @@ def _restart_single_service(svc_name: str, fallback_cmd: list[str], restart_serv
|
|
|
509
511
|
|
|
510
512
|
def _restart_mesh_services(
|
|
511
513
|
mesh, server_name: str, culture_bin: str, config_path: str, dry_run: bool
|
|
512
|
-
) ->
|
|
513
|
-
"""Stop agents and server, regenerate service entries, then restart everything.
|
|
514
|
+
) -> bool:
|
|
515
|
+
"""Stop agents and server, regenerate service entries, then restart everything.
|
|
516
|
+
|
|
517
|
+
Returns True if the server came up successfully, False otherwise.
|
|
518
|
+
"""
|
|
514
519
|
print(f"Restarting mesh node '{server_name}'...")
|
|
515
520
|
|
|
516
521
|
if dry_run:
|
|
517
522
|
_dry_run_restart(mesh, server_name)
|
|
518
|
-
return
|
|
523
|
+
return True
|
|
519
524
|
|
|
520
525
|
for agent in mesh.agents:
|
|
521
526
|
full_nick = f"{server_name}-{agent.nick}"
|
|
@@ -561,7 +566,17 @@ def _restart_mesh_services(
|
|
|
561
566
|
]
|
|
562
567
|
_restart_single_service(server_svc, server_fallback, restart_service)
|
|
563
568
|
|
|
564
|
-
_wait_for_server_port(mesh.server.port)
|
|
569
|
+
if not _wait_for_server_port(mesh.server.host, mesh.server.port):
|
|
570
|
+
if sys.platform == "linux":
|
|
571
|
+
hint = f"journalctl --user -u culture-server-{server_name}"
|
|
572
|
+
else:
|
|
573
|
+
hint = f"~/.culture/logs/server-{server_name}.log"
|
|
574
|
+
print(
|
|
575
|
+
f" Error: server {server_name} did not start on port {mesh.server.port}. "
|
|
576
|
+
f"Check logs: {hint}",
|
|
577
|
+
file=sys.stderr,
|
|
578
|
+
)
|
|
579
|
+
return False
|
|
565
580
|
|
|
566
581
|
for agent in mesh.agents:
|
|
567
582
|
full_nick = f"{server_name}-{agent.nick}"
|
|
@@ -579,6 +594,7 @@ def _restart_mesh_services(
|
|
|
579
594
|
_restart_single_service(agent_svc, agent_fallback, restart_service)
|
|
580
595
|
|
|
581
596
|
print()
|
|
597
|
+
return True
|
|
582
598
|
|
|
583
599
|
|
|
584
600
|
def _resolve_mesh_for_server(server_name: str, config_path: str):
|
|
@@ -630,6 +646,7 @@ def _cmd_update(args: argparse.Namespace) -> None:
|
|
|
630
646
|
culture_bin = shutil.which("culture") or "culture"
|
|
631
647
|
|
|
632
648
|
running = list_servers()
|
|
649
|
+
failed = []
|
|
633
650
|
|
|
634
651
|
if running:
|
|
635
652
|
for srv in running:
|
|
@@ -640,7 +657,10 @@ def _cmd_update(args: argparse.Namespace) -> None:
|
|
|
640
657
|
file=sys.stderr,
|
|
641
658
|
)
|
|
642
659
|
continue
|
|
643
|
-
_restart_mesh_services(
|
|
660
|
+
if not _restart_mesh_services(
|
|
661
|
+
mesh, srv["name"], culture_bin, args.config, args.dry_run
|
|
662
|
+
):
|
|
663
|
+
failed.append(srv["name"])
|
|
644
664
|
else:
|
|
645
665
|
try:
|
|
646
666
|
mesh = load_mesh_config(args.config)
|
|
@@ -648,6 +668,19 @@ def _cmd_update(args: argparse.Namespace) -> None:
|
|
|
648
668
|
mesh = generate_mesh_from_agents(args.config)
|
|
649
669
|
if mesh is None:
|
|
650
670
|
sys.exit(1)
|
|
651
|
-
_restart_mesh_services(
|
|
671
|
+
if not _restart_mesh_services(
|
|
672
|
+
mesh, mesh.server.name, culture_bin, args.config, args.dry_run
|
|
673
|
+
):
|
|
674
|
+
failed.append(mesh.server.name)
|
|
675
|
+
|
|
676
|
+
if failed:
|
|
677
|
+
print(
|
|
678
|
+
f"Update finished with errors. Failed to restart: {', '.join(failed)}",
|
|
679
|
+
file=sys.stderr,
|
|
680
|
+
)
|
|
681
|
+
sys.exit(1)
|
|
652
682
|
|
|
653
|
-
|
|
683
|
+
if args.dry_run:
|
|
684
|
+
print("Dry run complete. No services were restarted.")
|
|
685
|
+
else:
|
|
686
|
+
print("Update complete. All services restarted.")
|
|
@@ -35,6 +35,9 @@ _ERR_MISSING_CHANNEL = "Missing 'channel'"
|
|
|
35
35
|
_ERR_MISSING_CHANNEL_THREAD = "Missing 'channel' or 'thread'"
|
|
36
36
|
_ERR_MISSING_CHANNEL_THREAD_MSG = "Missing 'channel', 'thread', or 'message'"
|
|
37
37
|
|
|
38
|
+
# Regex to extract @mentioned nicks from messages
|
|
39
|
+
_MENTION_RE = re.compile(r"@([\w-]+)")
|
|
40
|
+
|
|
38
41
|
MAX_CRASH_COUNT = 3
|
|
39
42
|
CRASH_WINDOW_SECONDS = 300
|
|
40
43
|
CRASH_RESTART_DELAY = 5
|
|
@@ -103,6 +106,7 @@ class ACPDaemon:
|
|
|
103
106
|
"irc_part": self._ipc_irc_part,
|
|
104
107
|
"irc_channels": self._ipc_irc_channels,
|
|
105
108
|
"irc_who": self._ipc_irc_who,
|
|
109
|
+
"irc_topic": self._ipc_irc_topic,
|
|
106
110
|
"irc_ask": self._ipc_irc_ask,
|
|
107
111
|
"compact": self._ipc_compact,
|
|
108
112
|
"clear": self._ipc_clear,
|
|
@@ -749,16 +753,34 @@ class ACPDaemon:
|
|
|
749
753
|
self._status_query_event = None
|
|
750
754
|
self._status_query_response = ""
|
|
751
755
|
|
|
756
|
+
def _check_mention_warnings(self, text: str) -> list[str]:
|
|
757
|
+
"""Return warnings for @mentioned nicks not seen in any buffer."""
|
|
758
|
+
mentions = _MENTION_RE.findall(text)
|
|
759
|
+
if not mentions or not self._buffer:
|
|
760
|
+
return []
|
|
761
|
+
known_nicks = self._buffer.known_nicks()
|
|
762
|
+
warnings = []
|
|
763
|
+
for nick in mentions:
|
|
764
|
+
if nick not in known_nicks:
|
|
765
|
+
warnings.append(f"Mentioned nick not found: {nick}")
|
|
766
|
+
return warnings
|
|
767
|
+
|
|
752
768
|
async def _ipc_irc_send(self, req_id: str, msg: dict) -> dict:
|
|
753
769
|
channel = msg.get("channel", "")
|
|
754
770
|
text = msg.get("message", "")
|
|
755
771
|
if not channel:
|
|
756
772
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
757
|
-
if not text:
|
|
773
|
+
if not text or not text.strip():
|
|
758
774
|
return make_response(req_id, ok=False, error="Missing 'message'")
|
|
759
775
|
assert self._transport is not None
|
|
776
|
+
if channel.startswith("#") and channel not in self._transport.channels:
|
|
777
|
+
return make_response(req_id, ok=False, error=f"Not joined to {channel}")
|
|
760
778
|
await self._transport.send_privmsg(channel, text)
|
|
761
|
-
|
|
779
|
+
warnings = self._check_mention_warnings(text)
|
|
780
|
+
resp = make_response(req_id, ok=True)
|
|
781
|
+
if warnings:
|
|
782
|
+
resp["warnings"] = warnings
|
|
783
|
+
return resp
|
|
762
784
|
|
|
763
785
|
def _ipc_irc_read(self, req_id: str, msg: dict) -> dict:
|
|
764
786
|
channel = msg.get("channel", "")
|
|
@@ -781,6 +803,8 @@ class ACPDaemon:
|
|
|
781
803
|
channel = msg.get("channel", "")
|
|
782
804
|
if not channel:
|
|
783
805
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
806
|
+
if not channel.startswith("#"):
|
|
807
|
+
return make_response(req_id, ok=False, error="Channel name must start with '#'")
|
|
784
808
|
assert self._transport is not None
|
|
785
809
|
await self._transport.join_channel(channel)
|
|
786
810
|
return make_response(req_id, ok=True)
|
|
@@ -789,6 +813,8 @@ class ACPDaemon:
|
|
|
789
813
|
channel = msg.get("channel", "")
|
|
790
814
|
if not channel:
|
|
791
815
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
816
|
+
if not channel.startswith("#"):
|
|
817
|
+
return make_response(req_id, ok=False, error="Channel name must start with '#'")
|
|
792
818
|
assert self._transport is not None
|
|
793
819
|
await self._transport.part_channel(channel)
|
|
794
820
|
return make_response(req_id, ok=True)
|
|
@@ -862,13 +888,24 @@ class ACPDaemon:
|
|
|
862
888
|
await self._transport.send_who(target)
|
|
863
889
|
return make_response(req_id, ok=True)
|
|
864
890
|
|
|
891
|
+
async def _ipc_irc_topic(self, req_id: str, msg: dict) -> dict:
|
|
892
|
+
channel = msg.get("channel", "")
|
|
893
|
+
if not channel:
|
|
894
|
+
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
895
|
+
if not channel.startswith("#"):
|
|
896
|
+
return make_response(req_id, ok=False, error="Channel name must start with '#'")
|
|
897
|
+
assert self._transport is not None
|
|
898
|
+
topic = msg.get("topic") # None means query, string means set
|
|
899
|
+
await self._transport.send_topic(channel, topic)
|
|
900
|
+
return make_response(req_id, ok=True)
|
|
901
|
+
|
|
865
902
|
async def _ipc_irc_ask(self, req_id: str, msg: dict) -> dict:
|
|
866
903
|
"""Send a PRIVMSG and fire a question webhook. Response matching is TODO."""
|
|
867
904
|
channel = msg.get("channel", "")
|
|
868
905
|
question = msg.get("message", "")
|
|
869
906
|
if not channel:
|
|
870
907
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
871
|
-
if not question:
|
|
908
|
+
if not question or not question.strip():
|
|
872
909
|
return make_response(req_id, ok=False, error="Missing 'message'")
|
|
873
910
|
assert self._transport is not None
|
|
874
911
|
await self._transport.send_privmsg(channel, question)
|
|
@@ -51,6 +51,9 @@ class IRCTransport:
|
|
|
51
51
|
"PRIVMSG": self._on_privmsg,
|
|
52
52
|
"NOTICE": self._on_notice,
|
|
53
53
|
"ROOMINVITE": self._on_roominvite,
|
|
54
|
+
"TOPIC": self._on_topic,
|
|
55
|
+
"331": self._on_numeric_topic,
|
|
56
|
+
"332": self._on_numeric_topic,
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
async def connect(self) -> None:
|
|
@@ -90,6 +93,10 @@ class IRCTransport:
|
|
|
90
93
|
for line in text.splitlines():
|
|
91
94
|
if line:
|
|
92
95
|
await self._send_raw(f"PRIVMSG {target} :{line}")
|
|
96
|
+
if target.startswith("#"):
|
|
97
|
+
self.buffer.add(target, self.nick, line)
|
|
98
|
+
else:
|
|
99
|
+
self.buffer.add(f"DM:{target}", self.nick, line)
|
|
93
100
|
|
|
94
101
|
async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
|
|
95
102
|
lines = [l for l in text.splitlines() if l]
|
|
@@ -112,11 +119,15 @@ class IRCTransport:
|
|
|
112
119
|
await self._send_raw(f"THREADS {channel}")
|
|
113
120
|
|
|
114
121
|
async def join_channel(self, channel: str) -> None:
|
|
122
|
+
if not channel.startswith("#"):
|
|
123
|
+
return
|
|
115
124
|
await self._send_raw(f"JOIN {channel}")
|
|
116
125
|
if channel not in self.channels:
|
|
117
126
|
self.channels.append(channel)
|
|
118
127
|
|
|
119
128
|
async def part_channel(self, channel: str) -> None:
|
|
129
|
+
if not channel.startswith("#"):
|
|
130
|
+
return
|
|
120
131
|
await self._send_raw(f"PART {channel}")
|
|
121
132
|
if channel in self.channels:
|
|
122
133
|
self.channels.remove(channel)
|
|
@@ -124,6 +135,12 @@ class IRCTransport:
|
|
|
124
135
|
async def send_who(self, target: str) -> None:
|
|
125
136
|
await self._send_raw(f"WHO {target}")
|
|
126
137
|
|
|
138
|
+
async def send_topic(self, channel: str, topic: str | None = None) -> None:
|
|
139
|
+
if topic is not None:
|
|
140
|
+
await self._send_raw(f"TOPIC {channel} :{topic}")
|
|
141
|
+
else:
|
|
142
|
+
await self._send_raw(f"TOPIC {channel}")
|
|
143
|
+
|
|
127
144
|
async def send_raw(self, line: str) -> None:
|
|
128
145
|
"""Send a raw IRC line. Public for commands like HISTORY."""
|
|
129
146
|
if self._writer:
|
|
@@ -202,6 +219,28 @@ class IRCTransport:
|
|
|
202
219
|
self._route_to_buffer(target, sender, text)
|
|
203
220
|
self._detect_and_fire_mention(target, sender, text)
|
|
204
221
|
|
|
222
|
+
def _on_topic(self, msg: Message) -> None:
|
|
223
|
+
"""Handle TOPIC broadcasts (someone changed the topic)."""
|
|
224
|
+
if len(msg.params) < 2:
|
|
225
|
+
return
|
|
226
|
+
channel = msg.params[0]
|
|
227
|
+
topic = msg.params[1]
|
|
228
|
+
sender = msg.prefix.split("!")[0] if msg.prefix else "server"
|
|
229
|
+
if channel.startswith("#"):
|
|
230
|
+
self.buffer.add(channel, sender, f"* Topic changed: {topic}")
|
|
231
|
+
|
|
232
|
+
def _on_numeric_topic(self, msg: Message) -> None:
|
|
233
|
+
"""Handle 331 (no topic) and 332 (topic is...) replies."""
|
|
234
|
+
if len(msg.params) < 2:
|
|
235
|
+
return
|
|
236
|
+
channel = msg.params[1]
|
|
237
|
+
if not channel.startswith("#"):
|
|
238
|
+
return
|
|
239
|
+
if msg.command == "331":
|
|
240
|
+
self.buffer.add(channel, "server", "* No topic is set")
|
|
241
|
+
elif msg.command == "332" and len(msg.params) >= 3:
|
|
242
|
+
self.buffer.add(channel, "server", f"* Topic: {msg.params[2]}")
|
|
243
|
+
|
|
205
244
|
def _route_to_buffer(self, target: str, sender: str, text: str) -> None:
|
|
206
245
|
"""Insert the message into the appropriate buffer (channel or DM)."""
|
|
207
246
|
if target.startswith("#"):
|
|
@@ -53,6 +53,14 @@ class MessageBuffer:
|
|
|
53
53
|
self._cursors[channel] = total
|
|
54
54
|
return new_messages
|
|
55
55
|
|
|
56
|
+
def known_nicks(self) -> set[str]:
|
|
57
|
+
"""Return the set of nicks seen across all buffers."""
|
|
58
|
+
nicks: set[str] = set()
|
|
59
|
+
for buf in self._buffers.values():
|
|
60
|
+
for m in buf:
|
|
61
|
+
nicks.add(m.nick)
|
|
62
|
+
return nicks
|
|
63
|
+
|
|
56
64
|
def read_thread(self, channel: str, thread_name: str, limit: int = 50) -> list[BufferedMessage]:
|
|
57
65
|
buf = self._buffers.get(channel)
|
|
58
66
|
if not buf:
|
|
@@ -141,6 +141,26 @@ python3 -m culture.clients.acp.skill.irc_client who <target>
|
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
144
|
+
### topic — get or set a channel topic
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python3 -m culture.clients.acp.skill.irc_client topic <channel> [topic text]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Get current topic:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python3 -m culture.clients.acp.skill.irc_client topic "#general"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Set topic:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
python3 -m culture.clients.acp.skill.irc_client topic "#general" "Welcome to general chat"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
144
164
|
### compact — compact the agent's context window
|
|
145
165
|
|
|
146
166
|
```bash
|
|
@@ -190,6 +210,8 @@ result = await client.irc_join("#ops")
|
|
|
190
210
|
result = await client.irc_part("#ops")
|
|
191
211
|
result = await client.irc_channels()
|
|
192
212
|
result = await client.irc_who("#general")
|
|
213
|
+
result = await client.irc_topic("#general")
|
|
214
|
+
result = await client.irc_topic("#general", "Welcome to general chat")
|
|
193
215
|
result = await client.compact()
|
|
194
216
|
result = await client.clear()
|
|
195
217
|
|
|
@@ -165,6 +165,13 @@ class SkillClient:
|
|
|
165
165
|
"""Send a WHO query for a channel or nick."""
|
|
166
166
|
return await self._request("irc_who", target=target)
|
|
167
167
|
|
|
168
|
+
async def irc_topic(self, channel: str, topic: str | None = None) -> dict[str, Any]:
|
|
169
|
+
"""Get or set a channel topic."""
|
|
170
|
+
params: dict[str, Any] = {"channel": channel}
|
|
171
|
+
if topic is not None:
|
|
172
|
+
params["topic"] = topic
|
|
173
|
+
return await self._request("irc_topic", **params)
|
|
174
|
+
|
|
168
175
|
async def compact(self) -> dict[str, Any]:
|
|
169
176
|
"""Send /compact to the agent runner."""
|
|
170
177
|
return await self._request("compact")
|
|
@@ -248,6 +255,12 @@ async def _cmd_who(client: SkillClient, args: list[str]) -> dict[str, Any]:
|
|
|
248
255
|
return await client.irc_who(args[1])
|
|
249
256
|
|
|
250
257
|
|
|
258
|
+
async def _cmd_topic(client: SkillClient, args: list[str]) -> dict[str, Any]:
|
|
259
|
+
channel = args[1]
|
|
260
|
+
topic = " ".join(args[2:]) if len(args) > 2 else None
|
|
261
|
+
return await client.irc_topic(channel, topic)
|
|
262
|
+
|
|
263
|
+
|
|
251
264
|
async def _cmd_compact(client: SkillClient, args: list[str]) -> dict[str, Any]:
|
|
252
265
|
return await client.compact()
|
|
253
266
|
|
|
@@ -264,6 +277,7 @@ _SUBCOMMANDS: dict[str, Any] = {
|
|
|
264
277
|
"part": _cmd_part,
|
|
265
278
|
"channels": _cmd_channels,
|
|
266
279
|
"who": _cmd_who,
|
|
280
|
+
"topic": _cmd_topic,
|
|
267
281
|
"compact": _cmd_compact,
|
|
268
282
|
"clear": _cmd_clear,
|
|
269
283
|
}
|
|
@@ -274,7 +288,7 @@ async def _main(args: list[str]) -> None:
|
|
|
274
288
|
if not args:
|
|
275
289
|
print(
|
|
276
290
|
"Usage: irc_client.py <subcommand> [args...]\n"
|
|
277
|
-
"Subcommands: send, read, ask, join, part, channels, who, compact, clear",
|
|
291
|
+
"Subcommands: send, read, ask, join, part, channels, who, topic, compact, clear",
|
|
278
292
|
file=sys.stderr,
|
|
279
293
|
)
|
|
280
294
|
sys.exit(1)
|
|
@@ -29,6 +29,9 @@ _ERR_MISSING_CHANNEL = "Missing 'channel'"
|
|
|
29
29
|
_ERR_MISSING_CHANNEL_THREAD = "Missing 'channel' or 'thread'"
|
|
30
30
|
_ERR_MISSING_CHANNEL_THREAD_MSG = "Missing 'channel', 'thread', or 'message'"
|
|
31
31
|
|
|
32
|
+
# Regex to extract @mentioned nicks from messages
|
|
33
|
+
_MENTION_RE = re.compile(r"@([\w-]+)")
|
|
34
|
+
|
|
32
35
|
|
|
33
36
|
class AgentDaemon:
|
|
34
37
|
"""Central orchestrator that ties together the IRC transport, socket server,
|
|
@@ -86,6 +89,7 @@ class AgentDaemon:
|
|
|
86
89
|
"irc_part": self._ipc_irc_part,
|
|
87
90
|
"irc_channels": self._ipc_irc_channels,
|
|
88
91
|
"irc_who": self._ipc_irc_who,
|
|
92
|
+
"irc_topic": self._ipc_irc_topic,
|
|
89
93
|
"irc_ask": self._ipc_irc_ask,
|
|
90
94
|
"compact": self._ipc_compact,
|
|
91
95
|
"clear": self._ipc_clear,
|
|
@@ -639,16 +643,34 @@ class AgentDaemon:
|
|
|
639
643
|
self._status_query_event = None
|
|
640
644
|
self._status_query_response = ""
|
|
641
645
|
|
|
646
|
+
def _check_mention_warnings(self, text: str) -> list[str]:
|
|
647
|
+
"""Return warnings for @mentioned nicks not seen in any buffer."""
|
|
648
|
+
mentions = _MENTION_RE.findall(text)
|
|
649
|
+
if not mentions or not self._buffer:
|
|
650
|
+
return []
|
|
651
|
+
known_nicks = self._buffer.known_nicks()
|
|
652
|
+
warnings = []
|
|
653
|
+
for nick in mentions:
|
|
654
|
+
if nick not in known_nicks:
|
|
655
|
+
warnings.append(f"Mentioned nick not found: {nick}")
|
|
656
|
+
return warnings
|
|
657
|
+
|
|
642
658
|
async def _ipc_irc_send(self, req_id: str, msg: dict) -> dict:
|
|
643
659
|
channel = msg.get("channel", "")
|
|
644
660
|
text = msg.get("message", "")
|
|
645
661
|
if not channel:
|
|
646
662
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
647
|
-
if not text:
|
|
663
|
+
if not text or not text.strip():
|
|
648
664
|
return make_response(req_id, ok=False, error="Missing 'message'")
|
|
649
665
|
assert self._transport is not None
|
|
666
|
+
if channel.startswith("#") and channel not in self._transport.channels:
|
|
667
|
+
return make_response(req_id, ok=False, error=f"Not joined to {channel}")
|
|
650
668
|
await self._transport.send_privmsg(channel, text)
|
|
651
|
-
|
|
669
|
+
warnings = self._check_mention_warnings(text)
|
|
670
|
+
resp = make_response(req_id, ok=True)
|
|
671
|
+
if warnings:
|
|
672
|
+
resp["warnings"] = warnings
|
|
673
|
+
return resp
|
|
652
674
|
|
|
653
675
|
def _ipc_irc_read(self, req_id: str, msg: dict) -> dict:
|
|
654
676
|
channel = msg.get("channel", "")
|
|
@@ -671,6 +693,8 @@ class AgentDaemon:
|
|
|
671
693
|
channel = msg.get("channel", "")
|
|
672
694
|
if not channel:
|
|
673
695
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
696
|
+
if not channel.startswith("#"):
|
|
697
|
+
return make_response(req_id, ok=False, error="Channel name must start with '#'")
|
|
674
698
|
assert self._transport is not None
|
|
675
699
|
await self._transport.join_channel(channel)
|
|
676
700
|
return make_response(req_id, ok=True)
|
|
@@ -679,6 +703,8 @@ class AgentDaemon:
|
|
|
679
703
|
channel = msg.get("channel", "")
|
|
680
704
|
if not channel:
|
|
681
705
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
706
|
+
if not channel.startswith("#"):
|
|
707
|
+
return make_response(req_id, ok=False, error="Channel name must start with '#'")
|
|
682
708
|
assert self._transport is not None
|
|
683
709
|
await self._transport.part_channel(channel)
|
|
684
710
|
return make_response(req_id, ok=True)
|
|
@@ -752,13 +778,24 @@ class AgentDaemon:
|
|
|
752
778
|
await self._transport.send_who(target)
|
|
753
779
|
return make_response(req_id, ok=True)
|
|
754
780
|
|
|
781
|
+
async def _ipc_irc_topic(self, req_id: str, msg: dict) -> dict:
|
|
782
|
+
channel = msg.get("channel", "")
|
|
783
|
+
if not channel:
|
|
784
|
+
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
785
|
+
if not channel.startswith("#"):
|
|
786
|
+
return make_response(req_id, ok=False, error="Channel name must start with '#'")
|
|
787
|
+
assert self._transport is not None
|
|
788
|
+
topic = msg.get("topic") # None means query, string means set
|
|
789
|
+
await self._transport.send_topic(channel, topic)
|
|
790
|
+
return make_response(req_id, ok=True)
|
|
791
|
+
|
|
755
792
|
async def _ipc_irc_ask(self, req_id: str, msg: dict) -> dict:
|
|
756
793
|
"""Send a PRIVMSG and fire a question webhook. Response matching is TODO."""
|
|
757
794
|
channel = msg.get("channel", "")
|
|
758
795
|
question = msg.get("message", "")
|
|
759
796
|
if not channel:
|
|
760
797
|
return make_response(req_id, ok=False, error=_ERR_MISSING_CHANNEL)
|
|
761
|
-
if not question:
|
|
798
|
+
if not question or not question.strip():
|
|
762
799
|
return make_response(req_id, ok=False, error="Missing 'message'")
|
|
763
800
|
assert self._transport is not None
|
|
764
801
|
await self._transport.send_privmsg(channel, question)
|
|
@@ -51,6 +51,9 @@ class IRCTransport:
|
|
|
51
51
|
"PRIVMSG": self._on_privmsg,
|
|
52
52
|
"NOTICE": self._on_notice,
|
|
53
53
|
"ROOMINVITE": self._on_roominvite,
|
|
54
|
+
"TOPIC": self._on_topic,
|
|
55
|
+
"331": self._on_numeric_topic,
|
|
56
|
+
"332": self._on_numeric_topic,
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
async def connect(self) -> None:
|
|
@@ -90,6 +93,10 @@ class IRCTransport:
|
|
|
90
93
|
for line in text.splitlines():
|
|
91
94
|
if line:
|
|
92
95
|
await self._send_raw(f"PRIVMSG {target} :{line}")
|
|
96
|
+
if target.startswith("#"):
|
|
97
|
+
self.buffer.add(target, self.nick, line)
|
|
98
|
+
else:
|
|
99
|
+
self.buffer.add(f"DM:{target}", self.nick, line)
|
|
93
100
|
|
|
94
101
|
async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
|
|
95
102
|
lines = [l for l in text.splitlines() if l]
|
|
@@ -112,11 +119,15 @@ class IRCTransport:
|
|
|
112
119
|
await self._send_raw(f"THREADS {channel}")
|
|
113
120
|
|
|
114
121
|
async def join_channel(self, channel: str) -> None:
|
|
122
|
+
if not channel.startswith("#"):
|
|
123
|
+
return
|
|
115
124
|
await self._send_raw(f"JOIN {channel}")
|
|
116
125
|
if channel not in self.channels:
|
|
117
126
|
self.channels.append(channel)
|
|
118
127
|
|
|
119
128
|
async def part_channel(self, channel: str) -> None:
|
|
129
|
+
if not channel.startswith("#"):
|
|
130
|
+
return
|
|
120
131
|
await self._send_raw(f"PART {channel}")
|
|
121
132
|
if channel in self.channels:
|
|
122
133
|
self.channels.remove(channel)
|
|
@@ -124,6 +135,12 @@ class IRCTransport:
|
|
|
124
135
|
async def send_who(self, target: str) -> None:
|
|
125
136
|
await self._send_raw(f"WHO {target}")
|
|
126
137
|
|
|
138
|
+
async def send_topic(self, channel: str, topic: str | None = None) -> None:
|
|
139
|
+
if topic is not None:
|
|
140
|
+
await self._send_raw(f"TOPIC {channel} :{topic}")
|
|
141
|
+
else:
|
|
142
|
+
await self._send_raw(f"TOPIC {channel}")
|
|
143
|
+
|
|
127
144
|
async def send_raw(self, line: str) -> None:
|
|
128
145
|
"""Send a raw IRC line. Public for commands like HISTORY."""
|
|
129
146
|
if self._writer:
|
|
@@ -202,6 +219,28 @@ class IRCTransport:
|
|
|
202
219
|
self._route_to_buffer(target, sender, text)
|
|
203
220
|
self._detect_and_fire_mention(target, sender, text)
|
|
204
221
|
|
|
222
|
+
def _on_topic(self, msg: Message) -> None:
|
|
223
|
+
"""Handle TOPIC broadcasts (someone changed the topic)."""
|
|
224
|
+
if len(msg.params) < 2:
|
|
225
|
+
return
|
|
226
|
+
channel = msg.params[0]
|
|
227
|
+
topic = msg.params[1]
|
|
228
|
+
sender = msg.prefix.split("!")[0] if msg.prefix else "server"
|
|
229
|
+
if channel.startswith("#"):
|
|
230
|
+
self.buffer.add(channel, sender, f"* Topic changed: {topic}")
|
|
231
|
+
|
|
232
|
+
def _on_numeric_topic(self, msg: Message) -> None:
|
|
233
|
+
"""Handle 331 (no topic) and 332 (topic is...) replies."""
|
|
234
|
+
if len(msg.params) < 2:
|
|
235
|
+
return
|
|
236
|
+
channel = msg.params[1]
|
|
237
|
+
if not channel.startswith("#"):
|
|
238
|
+
return
|
|
239
|
+
if msg.command == "331":
|
|
240
|
+
self.buffer.add(channel, "server", "* No topic is set")
|
|
241
|
+
elif msg.command == "332" and len(msg.params) >= 3:
|
|
242
|
+
self.buffer.add(channel, "server", f"* Topic: {msg.params[2]}")
|
|
243
|
+
|
|
205
244
|
def _route_to_buffer(self, target: str, sender: str, text: str) -> None:
|
|
206
245
|
"""Insert the message into the appropriate buffer (channel or DM)."""
|
|
207
246
|
if target.startswith("#"):
|
|
@@ -53,6 +53,14 @@ class MessageBuffer:
|
|
|
53
53
|
self._cursors[channel] = total
|
|
54
54
|
return new_messages
|
|
55
55
|
|
|
56
|
+
def known_nicks(self) -> set[str]:
|
|
57
|
+
"""Return the set of nicks seen across all buffers."""
|
|
58
|
+
nicks: set[str] = set()
|
|
59
|
+
for buf in self._buffers.values():
|
|
60
|
+
for m in buf:
|
|
61
|
+
nicks.add(m.nick)
|
|
62
|
+
return nicks
|
|
63
|
+
|
|
56
64
|
def read_thread(self, channel: str, thread_name: str, limit: int = 50) -> list[BufferedMessage]:
|
|
57
65
|
buf = self._buffers.get(channel)
|
|
58
66
|
if not buf:
|
|
@@ -134,6 +134,26 @@ python3 -m culture.clients.claude.skill.irc_client who <target>
|
|
|
134
134
|
|
|
135
135
|
---
|
|
136
136
|
|
|
137
|
+
### topic — get or set a channel topic
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
python3 -m culture.clients.claude.skill.irc_client topic <channel> [topic text]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Get current topic:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python3 -m culture.clients.claude.skill.irc_client topic "#general"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Set topic:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python3 -m culture.clients.claude.skill.irc_client topic "#general" "Welcome to general chat"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
137
157
|
### compact — compact the agent's context window
|
|
138
158
|
|
|
139
159
|
```bash
|
|
@@ -183,6 +203,8 @@ result = await client.irc_join("#ops")
|
|
|
183
203
|
result = await client.irc_part("#ops")
|
|
184
204
|
result = await client.irc_channels()
|
|
185
205
|
result = await client.irc_who("#general")
|
|
206
|
+
result = await client.irc_topic("#general")
|
|
207
|
+
result = await client.irc_topic("#general", "Welcome to general chat")
|
|
186
208
|
result = await client.compact()
|
|
187
209
|
result = await client.clear()
|
|
188
210
|
|