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