agentirc-cli 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/CHANGELOG.md +19 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/PKG-INFO +5 -1
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/README.md +2 -0
- agentirc_cli-0.9.0/agentirc/__init__.py +1 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/cli.py +87 -5
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/daemon.py +18 -9
- agentirc_cli-0.9.0/agentirc/clients/copilot/agent_runner.py +165 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/config.py +161 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/daemon.py +463 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/irc_transport.py +146 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/skill/SKILL.md +69 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/skill/irc_client.py +281 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/socket_server.py +106 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/supervisor.py +141 -0
- agentirc_cli-0.9.0/agentirc/clients/copilot/webhook.py +60 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/agent_runner.py +328 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/config.py +161 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/daemon.py +456 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/ipc.py +38 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/irc_transport.py +146 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/message_buffer.py +46 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/skill/SKILL.md +69 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/skill/irc_client.py +281 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/socket_server.py +102 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/supervisor.py +141 -0
- agentirc_cli-0.9.0/agentirc/clients/opencode/webhook.py +59 -0
- agentirc_cli-0.9.0/agentirc/protocol/__init__.py +0 -0
- agentirc_cli-0.9.0/agentirc/server/__init__.py +0 -0
- agentirc_cli-0.9.0/agentirc/server/skills/__init__.py +0 -0
- agentirc_cli-0.9.0/docs/copilot-backend.md +90 -0
- agentirc_cli-0.9.0/docs/opencode-backend.md +82 -0
- agentirc_cli-0.9.0/docs/resources/github-copilot-sdk-instructions.md +762 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/index.md +4 -0
- agentirc_cli-0.9.0/packages/agent-harness/ipc.py +38 -0
- agentirc_cli-0.9.0/packages/agent-harness/message_buffer.py +46 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/pyproject.toml +6 -1
- agentirc_cli-0.9.0/tests/__init__.py +0 -0
- agentirc_cli-0.9.0/tests/test_copilot_daemon.py +138 -0
- agentirc_cli-0.9.0/tests/test_opencode_daemon.py +139 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/uv.lock +46 -1
- agentirc_cli-0.7.0/agentirc/__init__.py +0 -1
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/.gitignore +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/CLAUDE.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/CNAME +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/Gemfile +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/LICENSE +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/_config.yml +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/config.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/daemon.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/config.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/supervisor.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.7.0/agentirc/protocol → agentirc_cli-0.9.0/agentirc/clients/copilot}/__init__.py +0 -0
- {agentirc_cli-0.7.0/packages/agent-harness → agentirc_cli-0.9.0/agentirc/clients/copilot}/ipc.py +0 -0
- {agentirc_cli-0.7.0/packages/agent-harness → agentirc_cli-0.9.0/agentirc/clients/copilot}/message_buffer.py +0 -0
- {agentirc_cli-0.7.0/agentirc/server → agentirc_cli-0.9.0/agentirc/clients/copilot/skill}/__init__.py +0 -0
- {agentirc_cli-0.7.0/agentirc/server/skills → agentirc_cli-0.9.0/agentirc/clients/opencode}/__init__.py +0 -0
- {agentirc_cli-0.7.0/tests → agentirc_cli-0.9.0/agentirc/clients/opencode/skill}/__init__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/channel.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/config.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/ircd.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/server_link.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/skill.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/ci.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/cli.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/design.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/03-research-deep-dive.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/04-agent-delegation.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/05-benchmark-swarm.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/07-knowledge-pipeline.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/08-supervisor-intervention.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases/09-apps-as-agents.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.7.0 → agentirc_cli-0.9.0}/tests/test_webhook.py +0 -0
|
@@ -4,6 +4,25 @@ 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.9.0] - 2026-03-25
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- GitHub Copilot agent harness (Phase 4) using github-copilot-sdk
|
|
13
|
+
|
|
14
|
+
## [0.8.0] - 2026-03-24
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- OpenCode agent harness (Phase 3) — opencode acp over ACP/JSON-RPC/stdio
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- CLI now supports --agent opencode for init, start, and skills install
|
|
25
|
+
|
|
7
26
|
## [0.7.0] - 2026-03-24
|
|
8
27
|
|
|
9
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: IRC protocol chatrooms for AI agents (and humans allowed)
|
|
5
5
|
Project-URL: Homepage, https://github.com/OriNachum/agentirc
|
|
6
6
|
Author: Ori Nachum
|
|
@@ -15,6 +15,8 @@ Requires-Python: >=3.12
|
|
|
15
15
|
Requires-Dist: anthropic>=0.40
|
|
16
16
|
Requires-Dist: claude-agent-sdk>=0.1
|
|
17
17
|
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Provides-Extra: copilot
|
|
19
|
+
Requires-Dist: github-copilot-sdk; extra == 'copilot'
|
|
18
20
|
Description-Content-Type: text/markdown
|
|
19
21
|
|
|
20
22
|
<!-- markdownlint-disable MD033 MD041 -->
|
|
@@ -31,8 +33,10 @@ IRC Protocol ChatRooms for Agents (And humans allowed)
|
|
|
31
33
|
<img src="https://img.shields.io/badge/protocol-IRC_RFC_2812-D97706?style=flat&labelColor=2D2B27" alt="IRC RFC 2812">
|
|
32
34
|
<img src="https://img.shields.io/badge/license-MIT-D97706?style=flat&labelColor=2D2B27" alt="MIT License">
|
|
33
35
|
<a href="https://github.com/OriNachum/agentirc/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/OriNachum/agentirc/tests.yml?style=flat&label=tests&labelColor=2D2B27" alt="Tests"></a>
|
|
36
|
+
<a href="https://github.com/OriNachum/AgentIRC/stargazers"><img src="https://img.shields.io/github/stars/OriNachum/AgentIRC?style=flat&label=%E2%AD%90%20stars&labelColor=2D2B27&color=D97706" alt="GitHub Stars"></a>
|
|
34
37
|
|
|
35
38
|
<br><br>
|
|
39
|
+
<sub>If you find AgentIRC useful, <a href="https://github.com/OriNachum/AgentIRC/stargazers">give it a ⭐</a> — it helps others discover the project.</sub>
|
|
36
40
|
|
|
37
41
|
<img width="800" alt="AgentIRC" src="https://github.com/user-attachments/assets/41401b9d-1da2-483b-b21f-3769d388f74d" />
|
|
38
42
|
|
|
@@ -12,8 +12,10 @@ IRC Protocol ChatRooms for Agents (And humans allowed)
|
|
|
12
12
|
<img src="https://img.shields.io/badge/protocol-IRC_RFC_2812-D97706?style=flat&labelColor=2D2B27" alt="IRC RFC 2812">
|
|
13
13
|
<img src="https://img.shields.io/badge/license-MIT-D97706?style=flat&labelColor=2D2B27" alt="MIT License">
|
|
14
14
|
<a href="https://github.com/OriNachum/agentirc/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/OriNachum/agentirc/tests.yml?style=flat&label=tests&labelColor=2D2B27" alt="Tests"></a>
|
|
15
|
+
<a href="https://github.com/OriNachum/AgentIRC/stargazers"><img src="https://img.shields.io/github/stars/OriNachum/AgentIRC?style=flat&label=%E2%AD%90%20stars&labelColor=2D2B27&color=D97706" alt="GitHub Stars"></a>
|
|
15
16
|
|
|
16
17
|
<br><br>
|
|
18
|
+
<sub>If you find AgentIRC useful, <a href="https://github.com/OriNachum/AgentIRC/stargazers">give it a ⭐</a> — it helps others discover the project.</sub>
|
|
17
19
|
|
|
18
20
|
<img width="800" alt="AgentIRC" src="https://github.com/user-attachments/assets/41401b9d-1da2-483b-b21f-3769d388f74d" />
|
|
19
21
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.0"
|
|
@@ -98,7 +98,7 @@ def main() -> None:
|
|
|
98
98
|
init_parser = sub.add_parser("init", help="Register an agent for the current directory")
|
|
99
99
|
init_parser.add_argument("--server", default=None, help="Server name prefix")
|
|
100
100
|
init_parser.add_argument("--nick", default=None, help="Agent suffix (after server-)")
|
|
101
|
-
init_parser.add_argument("--agent", default="claude", choices=["claude", "codex"], help="Agent backend")
|
|
101
|
+
init_parser.add_argument("--agent", default="claude", choices=["claude", "codex", "opencode", "copilot"], help="Agent backend")
|
|
102
102
|
init_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
103
103
|
|
|
104
104
|
# -- start subcommand --------------------------------------------------
|
|
@@ -137,8 +137,8 @@ def main() -> None:
|
|
|
137
137
|
skills_sub = skills_parser.add_subparsers(dest="skills_command")
|
|
138
138
|
skills_install = skills_sub.add_parser("install", help="Install IRC skill for an agent")
|
|
139
139
|
skills_install.add_argument(
|
|
140
|
-
"target", choices=["claude", "codex", "all"],
|
|
141
|
-
help="Target agent: claude, codex, or all",
|
|
140
|
+
"target", choices=["claude", "codex", "opencode", "copilot", "all"],
|
|
141
|
+
help="Target agent: claude, codex, opencode, copilot, or all",
|
|
142
142
|
)
|
|
143
143
|
|
|
144
144
|
args = parser.parse_args()
|
|
@@ -355,6 +355,22 @@ def _cmd_init(args: argparse.Namespace) -> None:
|
|
|
355
355
|
directory=os.getcwd(),
|
|
356
356
|
channels=["#general"],
|
|
357
357
|
)
|
|
358
|
+
elif args.agent == "opencode":
|
|
359
|
+
from agentirc.clients.opencode.config import AgentConfig as OpenCodeAgentConfig
|
|
360
|
+
agent = OpenCodeAgentConfig(
|
|
361
|
+
nick=full_nick,
|
|
362
|
+
agent="opencode",
|
|
363
|
+
directory=os.getcwd(),
|
|
364
|
+
channels=["#general"],
|
|
365
|
+
)
|
|
366
|
+
elif args.agent == "copilot":
|
|
367
|
+
from agentirc.clients.copilot.config import AgentConfig as CopilotAgentConfig
|
|
368
|
+
agent = CopilotAgentConfig(
|
|
369
|
+
nick=full_nick,
|
|
370
|
+
agent="copilot",
|
|
371
|
+
directory=os.getcwd(),
|
|
372
|
+
channels=["#general"],
|
|
373
|
+
)
|
|
358
374
|
else:
|
|
359
375
|
agent = AgentConfig(
|
|
360
376
|
nick=full_nick,
|
|
@@ -435,6 +451,32 @@ async def _run_single_agent(config: DaemonConfig, agent: AgentConfig) -> None:
|
|
|
435
451
|
agents=config.agents,
|
|
436
452
|
)
|
|
437
453
|
daemon = CodexDaemon(codex_config, agent)
|
|
454
|
+
elif backend == "opencode":
|
|
455
|
+
from agentirc.clients.opencode.daemon import OpenCodeDaemon
|
|
456
|
+
from agentirc.clients.opencode.config import (
|
|
457
|
+
DaemonConfig as OpenCodeDaemonConfig,
|
|
458
|
+
)
|
|
459
|
+
# Re-load config through OpenCode module for correct supervisor defaults
|
|
460
|
+
opencode_config = OpenCodeDaemonConfig(
|
|
461
|
+
server=config.server,
|
|
462
|
+
webhooks=config.webhooks,
|
|
463
|
+
buffer_size=config.buffer_size,
|
|
464
|
+
agents=config.agents,
|
|
465
|
+
)
|
|
466
|
+
daemon = OpenCodeDaemon(opencode_config, agent)
|
|
467
|
+
elif backend == "copilot":
|
|
468
|
+
from agentirc.clients.copilot.daemon import CopilotDaemon
|
|
469
|
+
from agentirc.clients.copilot.config import (
|
|
470
|
+
DaemonConfig as CopilotDaemonConfig,
|
|
471
|
+
)
|
|
472
|
+
# Re-load config through Copilot module for correct supervisor defaults
|
|
473
|
+
copilot_config = CopilotDaemonConfig(
|
|
474
|
+
server=config.server,
|
|
475
|
+
webhooks=config.webhooks,
|
|
476
|
+
buffer_size=config.buffer_size,
|
|
477
|
+
agents=config.agents,
|
|
478
|
+
)
|
|
479
|
+
daemon = CopilotDaemon(copilot_config, agent)
|
|
438
480
|
else:
|
|
439
481
|
from agentirc.clients.claude.daemon import AgentDaemon
|
|
440
482
|
daemon = AgentDaemon(config, agent)
|
|
@@ -737,9 +779,45 @@ def _install_skill_codex() -> None:
|
|
|
737
779
|
print(f"Installed Codex skill: {dest}")
|
|
738
780
|
|
|
739
781
|
|
|
782
|
+
def _get_bundled_opencode_skill_path() -> str:
|
|
783
|
+
"""Return the path to the bundled OpenCode SKILL.md in the installed package."""
|
|
784
|
+
import agentirc
|
|
785
|
+
return os.path.join(os.path.dirname(agentirc.__file__), "clients", "opencode", "skill", "SKILL.md")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _install_skill_opencode() -> None:
|
|
789
|
+
"""Install IRC skill for OpenCode."""
|
|
790
|
+
src = _get_bundled_opencode_skill_path()
|
|
791
|
+
dest_dir = os.path.expanduser("~/.opencode/skills/agentirc-irc")
|
|
792
|
+
dest = os.path.join(dest_dir, "SKILL.md")
|
|
793
|
+
|
|
794
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
795
|
+
import shutil
|
|
796
|
+
shutil.copy2(src, dest)
|
|
797
|
+
print(f"Installed OpenCode skill: {dest}")
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _get_bundled_copilot_skill_path() -> str:
|
|
801
|
+
"""Return the path to the bundled Copilot SKILL.md in the installed package."""
|
|
802
|
+
import agentirc
|
|
803
|
+
return os.path.join(os.path.dirname(agentirc.__file__), "clients", "copilot", "skill", "SKILL.md")
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _install_skill_copilot() -> None:
|
|
807
|
+
"""Install IRC skill for GitHub Copilot."""
|
|
808
|
+
src = _get_bundled_copilot_skill_path()
|
|
809
|
+
dest_dir = os.path.expanduser("~/.copilot_skills/agentirc-irc")
|
|
810
|
+
dest = os.path.join(dest_dir, "SKILL.md")
|
|
811
|
+
|
|
812
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
813
|
+
import shutil
|
|
814
|
+
shutil.copy2(src, dest)
|
|
815
|
+
print(f"Installed Copilot skill: {dest}")
|
|
816
|
+
|
|
817
|
+
|
|
740
818
|
def _cmd_skills(args: argparse.Namespace) -> None:
|
|
741
819
|
if not hasattr(args, "skills_command") or args.skills_command != "install":
|
|
742
|
-
print("Usage: agentirc skills install <claude|codex|all>", file=sys.stderr)
|
|
820
|
+
print("Usage: agentirc skills install <claude|codex|opencode|copilot|all>", file=sys.stderr)
|
|
743
821
|
sys.exit(1)
|
|
744
822
|
|
|
745
823
|
target = args.target
|
|
@@ -748,7 +826,11 @@ def _cmd_skills(args: argparse.Namespace) -> None:
|
|
|
748
826
|
_install_skill_claude()
|
|
749
827
|
if target in ("codex", "all"):
|
|
750
828
|
_install_skill_codex()
|
|
829
|
+
if target in ("opencode", "all"):
|
|
830
|
+
_install_skill_opencode()
|
|
831
|
+
if target in ("copilot", "all"):
|
|
832
|
+
_install_skill_copilot()
|
|
751
833
|
|
|
752
834
|
if target == "all":
|
|
753
|
-
print("\nSkills installed for
|
|
835
|
+
print("\nSkills installed for Claude Code, Codex, OpenCode, and Copilot.")
|
|
754
836
|
print(f"\nSet AGENTIRC_NICK in your shell profile to enable the skill.")
|
|
@@ -10,6 +10,7 @@ import asyncio
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
12
|
import time
|
|
13
|
+
from collections import deque
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
from agentirc.clients.codex.config import DaemonConfig, AgentConfig
|
|
@@ -56,8 +57,10 @@ class CodexDaemon:
|
|
|
56
57
|
self._agent_runner: CodexAgentRunner | None = None
|
|
57
58
|
self._supervisor: CodexSupervisor | None = None
|
|
58
59
|
|
|
59
|
-
#
|
|
60
|
-
|
|
60
|
+
# FIFO queue of relay targets — each @mention enqueues a target,
|
|
61
|
+
# each agent response dequeues one, ensuring correct routing even
|
|
62
|
+
# when multiple mentions arrive while the agent is busy.
|
|
63
|
+
self._mention_targets: deque[str] = deque()
|
|
61
64
|
|
|
62
65
|
# Crash-recovery state
|
|
63
66
|
self._crash_times: list[float] = []
|
|
@@ -177,8 +180,8 @@ class CodexDaemon:
|
|
|
177
180
|
Formats a prompt and enqueues it so the Codex session picks it up.
|
|
178
181
|
"""
|
|
179
182
|
if self._agent_runner and self._agent_runner.is_running():
|
|
180
|
-
#
|
|
181
|
-
self.
|
|
183
|
+
# Enqueue relay target (FIFO matches prompt queue order)
|
|
184
|
+
self._mention_targets.append(target if target.startswith("#") else sender)
|
|
182
185
|
if target.startswith("#"):
|
|
183
186
|
prompt = f"[IRC @mention in {target}] <{sender}> {text}"
|
|
184
187
|
else:
|
|
@@ -187,8 +190,9 @@ class CodexDaemon:
|
|
|
187
190
|
|
|
188
191
|
async def _on_agent_message(self, msg: dict) -> None:
|
|
189
192
|
"""Relay agent text to IRC and feed to supervisor."""
|
|
190
|
-
#
|
|
191
|
-
|
|
193
|
+
# Dequeue the relay target that corresponds to this turn
|
|
194
|
+
relay_target = self._mention_targets.popleft() if self._mention_targets else None
|
|
195
|
+
if self._transport and relay_target:
|
|
192
196
|
content = msg.get("content", [])
|
|
193
197
|
for item in content:
|
|
194
198
|
if item.get("type") == "text":
|
|
@@ -199,7 +203,7 @@ class CodexDaemon:
|
|
|
199
203
|
line = line.strip()
|
|
200
204
|
if line:
|
|
201
205
|
await self._transport.send_privmsg(
|
|
202
|
-
|
|
206
|
+
relay_target, line
|
|
203
207
|
)
|
|
204
208
|
|
|
205
209
|
if self._supervisor:
|
|
@@ -423,14 +427,19 @@ class CodexDaemon:
|
|
|
423
427
|
path = msg.get("path", "")
|
|
424
428
|
if not path:
|
|
425
429
|
return make_response(req_id, ok=False, error="Missing 'path'")
|
|
430
|
+
new_cwd = os.path.abspath(path)
|
|
431
|
+
if not os.path.isdir(new_cwd):
|
|
432
|
+
return make_response(req_id, ok=False, error=f"Not a directory: {new_cwd}")
|
|
433
|
+
# Update the daemon's working directory
|
|
434
|
+
self.agent.directory = new_cwd
|
|
426
435
|
# Check for AGENTS.md (Codex equivalent of CLAUDE.md)
|
|
427
|
-
agents_md = os.path.join(
|
|
436
|
+
agents_md = os.path.join(new_cwd, "AGENTS.md")
|
|
428
437
|
agents_md_content = None
|
|
429
438
|
if os.path.isfile(agents_md):
|
|
430
439
|
with open(agents_md) as f:
|
|
431
440
|
agents_md_content = f.read()
|
|
432
441
|
return make_response(req_id, ok=True, data={
|
|
433
|
-
"directory":
|
|
442
|
+
"directory": new_cwd,
|
|
434
443
|
"agents_md": agents_md_content,
|
|
435
444
|
})
|
|
436
445
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Copilot agent runner — manages a GitHub Copilot SDK session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CopilotAgentRunner:
|
|
13
|
+
"""Manages a GitHub Copilot SDK session for the agentirc daemon."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
model: str,
|
|
18
|
+
directory: str,
|
|
19
|
+
system_prompt: str = "",
|
|
20
|
+
skill_directories: list[str] | None = None,
|
|
21
|
+
on_exit: Callable[[int], Awaitable[None]] | None = None,
|
|
22
|
+
on_message: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.model = model
|
|
25
|
+
self.directory = directory
|
|
26
|
+
self.system_prompt = system_prompt
|
|
27
|
+
self.skill_directories = skill_directories or []
|
|
28
|
+
self.on_exit = on_exit
|
|
29
|
+
self.on_message = on_message
|
|
30
|
+
|
|
31
|
+
self._client: Any = None
|
|
32
|
+
self._session: Any = None
|
|
33
|
+
self._session_id: str | None = None
|
|
34
|
+
self._running = False
|
|
35
|
+
self._stopping = False
|
|
36
|
+
self._prompt_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
37
|
+
self._task: asyncio.Task | None = None
|
|
38
|
+
|
|
39
|
+
def is_running(self) -> bool:
|
|
40
|
+
return self._running
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def session_id(self) -> str | None:
|
|
44
|
+
return self._session_id
|
|
45
|
+
|
|
46
|
+
async def start(self, initial_prompt: str = "") -> None:
|
|
47
|
+
"""Start the Copilot client and create a session."""
|
|
48
|
+
self._stopping = False
|
|
49
|
+
|
|
50
|
+
# Lazy import — github-copilot-sdk is only needed at runtime
|
|
51
|
+
from copilot import CopilotClient, PermissionHandler, SubprocessConfig
|
|
52
|
+
|
|
53
|
+
# Create and start the CopilotClient (spawns copilot CLI process)
|
|
54
|
+
subprocess_config = SubprocessConfig(cwd=self.directory)
|
|
55
|
+
self._client = CopilotClient(config=subprocess_config)
|
|
56
|
+
await self._client.start()
|
|
57
|
+
|
|
58
|
+
# Create a session with model and permissions.
|
|
59
|
+
# Wrap in try/except so a partial start doesn't leak the CLI process.
|
|
60
|
+
try:
|
|
61
|
+
session_kwargs: dict[str, Any] = {
|
|
62
|
+
"on_permission_request": PermissionHandler.approve_all,
|
|
63
|
+
"model": self.model,
|
|
64
|
+
}
|
|
65
|
+
if self.system_prompt:
|
|
66
|
+
session_kwargs["system_message"] = {"content": self.system_prompt}
|
|
67
|
+
if self.skill_directories:
|
|
68
|
+
session_kwargs["skill_directories"] = self.skill_directories
|
|
69
|
+
|
|
70
|
+
self._session = await self._client.create_session(**session_kwargs)
|
|
71
|
+
except Exception:
|
|
72
|
+
await self._client.stop()
|
|
73
|
+
self._client = None
|
|
74
|
+
raise
|
|
75
|
+
self._session_id = getattr(self._session, "id", None)
|
|
76
|
+
self._running = True
|
|
77
|
+
|
|
78
|
+
logger.info(
|
|
79
|
+
"CopilotAgentRunner started (model=%s, session=%s)",
|
|
80
|
+
self.model, self._session_id,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Start the prompt processing loop
|
|
84
|
+
self._task = asyncio.create_task(self._prompt_loop())
|
|
85
|
+
|
|
86
|
+
if initial_prompt:
|
|
87
|
+
await self.send_prompt(initial_prompt)
|
|
88
|
+
|
|
89
|
+
async def stop(self) -> None:
|
|
90
|
+
"""Stop the Copilot session and client."""
|
|
91
|
+
self._stopping = True
|
|
92
|
+
self._running = False
|
|
93
|
+
|
|
94
|
+
if self._task:
|
|
95
|
+
self._task.cancel()
|
|
96
|
+
try:
|
|
97
|
+
await self._task
|
|
98
|
+
except asyncio.CancelledError:
|
|
99
|
+
pass
|
|
100
|
+
self._task = None
|
|
101
|
+
|
|
102
|
+
if self._session:
|
|
103
|
+
try:
|
|
104
|
+
await self._session.destroy()
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.debug("Session destroy error (ignoring)", exc_info=True)
|
|
107
|
+
self._session = None
|
|
108
|
+
|
|
109
|
+
if self._client:
|
|
110
|
+
try:
|
|
111
|
+
await self._client.stop()
|
|
112
|
+
except Exception:
|
|
113
|
+
logger.debug("Client stop error (ignoring)", exc_info=True)
|
|
114
|
+
self._client = None
|
|
115
|
+
|
|
116
|
+
async def send_prompt(self, text: str) -> None:
|
|
117
|
+
"""Queue a prompt for the agent."""
|
|
118
|
+
await self._prompt_queue.put(text)
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Internal: prompt processing loop
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
async def _prompt_loop(self) -> None:
|
|
125
|
+
"""Process queued prompts one at a time using send_and_wait."""
|
|
126
|
+
try:
|
|
127
|
+
while self._running:
|
|
128
|
+
text = await self._prompt_queue.get()
|
|
129
|
+
if not self._running or self._session is None:
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
response = await self._session.send_and_wait(
|
|
134
|
+
text, timeout=120.0
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Extract text from SDK response
|
|
138
|
+
content_text = ""
|
|
139
|
+
if response is not None:
|
|
140
|
+
if hasattr(response, "data") and hasattr(response.data, "content"):
|
|
141
|
+
content_text = response.data.content or ""
|
|
142
|
+
elif isinstance(response, dict):
|
|
143
|
+
content_text = response.get("data", {}).get("content", "")
|
|
144
|
+
|
|
145
|
+
if content_text and self.on_message:
|
|
146
|
+
msg_dict = {
|
|
147
|
+
"type": "assistant",
|
|
148
|
+
"model": self.model,
|
|
149
|
+
"content": [{"type": "text", "text": content_text}],
|
|
150
|
+
}
|
|
151
|
+
await self.on_message(msg_dict)
|
|
152
|
+
|
|
153
|
+
except Exception:
|
|
154
|
+
logger.exception("Copilot session turn error")
|
|
155
|
+
if not self._stopping:
|
|
156
|
+
self._running = False
|
|
157
|
+
if self.on_exit:
|
|
158
|
+
await self.on_exit(1)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
except asyncio.CancelledError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
if not self._stopping and self.on_exit:
|
|
165
|
+
await self.on_exit(0)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ServerConnConfig:
|
|
14
|
+
"""IRC server connection settings."""
|
|
15
|
+
name: str = "agentirc"
|
|
16
|
+
host: str = "localhost"
|
|
17
|
+
port: int = 6667
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SupervisorConfig:
|
|
22
|
+
"""Supervisor sub-agent settings."""
|
|
23
|
+
model: str = "gpt-4.1"
|
|
24
|
+
window_size: int = 20
|
|
25
|
+
eval_interval: int = 5
|
|
26
|
+
escalation_threshold: int = 3
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class WebhookConfig:
|
|
31
|
+
"""Webhook alerting settings."""
|
|
32
|
+
url: str | None = None
|
|
33
|
+
irc_channel: str = "#alerts"
|
|
34
|
+
events: list[str] = field(default_factory=lambda: [
|
|
35
|
+
"agent_spiraling", "agent_error", "agent_question",
|
|
36
|
+
"agent_timeout", "agent_complete",
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AgentConfig:
|
|
42
|
+
"""Per-agent settings."""
|
|
43
|
+
nick: str = ""
|
|
44
|
+
agent: str = "copilot"
|
|
45
|
+
directory: str = "."
|
|
46
|
+
channels: list[str] = field(default_factory=lambda: ["#general"])
|
|
47
|
+
model: str = "gpt-4.1"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class DaemonConfig:
|
|
52
|
+
"""Top-level daemon configuration."""
|
|
53
|
+
server: ServerConnConfig = field(default_factory=ServerConnConfig)
|
|
54
|
+
supervisor: SupervisorConfig = field(default_factory=SupervisorConfig)
|
|
55
|
+
webhooks: WebhookConfig = field(default_factory=WebhookConfig)
|
|
56
|
+
buffer_size: int = 500
|
|
57
|
+
agents: list[AgentConfig] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
def get_agent(self, nick: str) -> AgentConfig | None:
|
|
60
|
+
for agent in self.agents:
|
|
61
|
+
if agent.nick == nick:
|
|
62
|
+
return agent
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_config(path: str | Path) -> DaemonConfig:
|
|
67
|
+
"""Load daemon config from a YAML file."""
|
|
68
|
+
with open(path) as f:
|
|
69
|
+
raw = yaml.safe_load(f) or {}
|
|
70
|
+
|
|
71
|
+
server = ServerConnConfig(**raw.get("server", {}))
|
|
72
|
+
supervisor = SupervisorConfig(**raw.get("supervisor", {}))
|
|
73
|
+
|
|
74
|
+
webhooks = WebhookConfig(**raw.get("webhooks", {}))
|
|
75
|
+
|
|
76
|
+
agents = []
|
|
77
|
+
for agent_raw in raw.get("agents", []):
|
|
78
|
+
agents.append(AgentConfig(**agent_raw))
|
|
79
|
+
|
|
80
|
+
return DaemonConfig(
|
|
81
|
+
server=server,
|
|
82
|
+
supervisor=supervisor,
|
|
83
|
+
webhooks=webhooks,
|
|
84
|
+
buffer_size=raw.get("buffer_size", 500),
|
|
85
|
+
agents=agents,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def sanitize_agent_name(dirname: str) -> str:
|
|
90
|
+
"""Sanitize a directory name into a valid agent/server name.
|
|
91
|
+
|
|
92
|
+
Lowercase, replace non-alphanumeric chars with hyphens, collapse
|
|
93
|
+
multiple hyphens, strip leading/trailing hyphens.
|
|
94
|
+
"""
|
|
95
|
+
name = dirname.lower()
|
|
96
|
+
name = re.sub(r"[^a-z0-9-]", "-", name)
|
|
97
|
+
name = re.sub(r"-+", "-", name)
|
|
98
|
+
name = name.strip("-")
|
|
99
|
+
if not name:
|
|
100
|
+
raise ValueError(f"sanitized name is empty for input: {dirname!r}")
|
|
101
|
+
return name
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_config_or_default(path: str | Path) -> DaemonConfig:
|
|
105
|
+
"""Load config from path, returning a default DaemonConfig if file is missing."""
|
|
106
|
+
path = Path(path)
|
|
107
|
+
if not path.exists():
|
|
108
|
+
return DaemonConfig()
|
|
109
|
+
return load_config(path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def save_config(path: str | Path, config: DaemonConfig) -> None:
|
|
113
|
+
"""Serialize a DaemonConfig to YAML and write atomically."""
|
|
114
|
+
path = Path(path)
|
|
115
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
data = asdict(config)
|
|
118
|
+
yaml_str = yaml.dump(data, default_flow_style=False)
|
|
119
|
+
|
|
120
|
+
# Atomic write: write to temp file in same dir, then rename
|
|
121
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
122
|
+
dir=str(path.parent), suffix=".yaml.tmp",
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
with os.fdopen(fd, "w") as f:
|
|
126
|
+
f.write(yaml_str)
|
|
127
|
+
os.replace(tmp_path, str(path))
|
|
128
|
+
except BaseException:
|
|
129
|
+
# Clean up temp file on failure
|
|
130
|
+
try:
|
|
131
|
+
os.unlink(tmp_path)
|
|
132
|
+
except OSError:
|
|
133
|
+
pass
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def add_agent_to_config(
|
|
138
|
+
path: str | Path,
|
|
139
|
+
agent: AgentConfig,
|
|
140
|
+
server_name: str | None = None,
|
|
141
|
+
) -> DaemonConfig:
|
|
142
|
+
"""Add an agent to a config file, creating it if needed.
|
|
143
|
+
|
|
144
|
+
If server_name is provided, updates config.server.name.
|
|
145
|
+
Raises ValueError if an agent with the same nick already exists.
|
|
146
|
+
"""
|
|
147
|
+
config = load_config_or_default(path)
|
|
148
|
+
|
|
149
|
+
if server_name is not None:
|
|
150
|
+
config.server.name = server_name
|
|
151
|
+
|
|
152
|
+
# Check for nick collision
|
|
153
|
+
for existing in config.agents:
|
|
154
|
+
if existing.nick == agent.nick:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"agent with nick {agent.nick!r} already exists in config"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
config.agents.append(agent)
|
|
160
|
+
save_config(path, config)
|
|
161
|
+
return config
|