agentirc-cli 9.0.0__tar.gz → 9.1.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-9.1.0/CHANGELOG.md +57 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/CLAUDE.md +17 -8
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/PKG-INFO +5 -1
- agentirc_cli-9.1.0/agentirc/_internal/aio.py +12 -0
- agentirc_cli-9.1.0/agentirc/_internal/bots/__init__.py +0 -0
- agentirc_cli-9.1.0/agentirc/_internal/bots/bot_manager.py +38 -0
- agentirc_cli-9.1.0/agentirc/_internal/bots/http_listener.py +28 -0
- agentirc_cli-9.1.0/agentirc/_internal/constants.py +17 -0
- agentirc_cli-9.1.0/agentirc/_internal/protocol/__init__.py +0 -0
- agentirc_cli-9.1.0/agentirc/_internal/protocol/message.py +128 -0
- agentirc_cli-9.1.0/agentirc/_internal/protocol/replies.py +52 -0
- agentirc_cli-9.1.0/agentirc/_internal/telemetry/__init__.py +34 -0
- agentirc_cli-9.1.0/agentirc/_internal/telemetry/audit.py +394 -0
- agentirc_cli-9.1.0/agentirc/_internal/telemetry/context.py +121 -0
- agentirc_cli-9.1.0/agentirc/_internal/telemetry/metrics.py +250 -0
- agentirc_cli-9.1.0/agentirc/_internal/telemetry/tracing.py +117 -0
- agentirc_cli-9.1.0/agentirc/_internal/virtual_client.py +231 -0
- agentirc_cli-9.1.0/agentirc/channel.py +80 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/agentirc/cli.py +7 -7
- agentirc_cli-9.1.0/agentirc/config.py +49 -0
- agentirc_cli-9.1.0/agentirc/events.py +117 -0
- agentirc_cli-9.1.0/agentirc/history_store.py +91 -0
- agentirc_cli-9.1.0/agentirc/ircd.py +718 -0
- agentirc_cli-9.1.0/agentirc/remote_client.py +43 -0
- agentirc_cli-9.1.0/agentirc/room_store.py +71 -0
- agentirc_cli-9.1.0/agentirc/rooms_util.py +56 -0
- agentirc_cli-9.1.0/agentirc/server_link.py +1166 -0
- agentirc_cli-9.1.0/agentirc/skill.py +61 -0
- agentirc_cli-9.1.0/agentirc/skills/__init__.py +0 -0
- agentirc_cli-9.1.0/agentirc/skills/history.py +225 -0
- agentirc_cli-9.1.0/agentirc/skills/icon.py +52 -0
- agentirc_cli-9.1.0/agentirc/skills/rooms.py +834 -0
- agentirc_cli-9.1.0/agentirc/skills/threads.py +709 -0
- agentirc_cli-9.1.0/agentirc/thread_store.py +52 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +21 -4
- agentirc_cli-9.1.0/pyproject.toml +248 -0
- agentirc_cli-9.1.0/tests/__init__.py +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/uv.lock +227 -2
- agentirc_cli-9.0.0/pyproject.toml +0 -51
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/workflow.sh +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills.local.yaml.example +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.gitignore +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/LICENSE +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/README.md +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/agentirc/__init__.py +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/agentirc/__main__.py +0 -0
- {agentirc_cli-9.0.0/tests → agentirc_cli-9.1.0/agentirc/_internal}/__init__.py +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/docs/steward/onboarding.md +0 -0
- {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/tests/test_cli.py +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
6
|
+
|
|
7
|
+
## [9.1.0] - 2026-04-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Server-core vendored from `culture` at SHA `df50942`. The `agentirc`
|
|
12
|
+
package now contains the IRCd (`ircd.py`), server-to-server linking
|
|
13
|
+
(`server_link.py`), channel/event/store/skill modules, `remote_client.py`
|
|
14
|
+
(peer-server ghost client), and the four built-in skills
|
|
15
|
+
(`skills/{rooms,threads,history,icon}.py`).
|
|
16
|
+
- Internal vendored support modules under `agentirc/_internal/`:
|
|
17
|
+
- `aio` (`maybe_await`)
|
|
18
|
+
- `constants` (system user/channel constants)
|
|
19
|
+
- `protocol/` (IRC `Message` and numeric `replies`)
|
|
20
|
+
- `telemetry/` (OpenTelemetry audit/tracing/metrics — full subpackage)
|
|
21
|
+
- `virtual_client` (`VirtualClient` for in-process bot integration)
|
|
22
|
+
- `bots/{bot_manager,http_listener}` (no-op stubs; culture replaces
|
|
23
|
+
these at runtime when wrapping an `IRCd`)
|
|
24
|
+
- `[tool.citation]` block in `pyproject.toml` enumerating every vendored
|
|
25
|
+
file with a quote/paraphrase/synthesize status, source URL, and
|
|
26
|
+
sha256, validated by `cite check`.
|
|
27
|
+
- Runtime dependencies: `opentelemetry-api`, `opentelemetry-sdk`,
|
|
28
|
+
`opentelemetry-exporter-otlp-proto-grpc` (all `>=1.22`).
|
|
29
|
+
- Dev dependency: `citation-cli` (provides the `cite` console script).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Bootstrap spec deviation: `remote_client.py` was originally listed as
|
|
34
|
+
"do not copy" but turned out to be server-side (used by `server_link`
|
|
35
|
+
and `virtual_client` for peer-server users in channel member lists).
|
|
36
|
+
Vendored as public `agentirc/remote_client.py`. See commit `8b4a6d8`.
|
|
37
|
+
|
|
38
|
+
### Notes
|
|
39
|
+
|
|
40
|
+
- `agentirc/cli.py` still ships only `version`; the `serve|start|stop|
|
|
41
|
+
restart|status|link|logs` lifecycle verbs remain stubs. The real CLI
|
|
42
|
+
is the next slice (PR-B2).
|
|
43
|
+
- Tests are not migrated yet (PR-B3).
|
|
44
|
+
|
|
45
|
+
## [9.0.0] - 2026-04-30
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Initial bootstrap of `agentirc-cli` as an installable Python package.
|
|
50
|
+
- Skeleton `agentirc/{__init__,__main__,cli}.py` with `version` verb
|
|
51
|
+
wired up and lifecycle verbs (`serve|start|stop|restart|status|link|
|
|
52
|
+
logs`) as stubs.
|
|
53
|
+
- Console scripts: both `agentirc` and `agentirc-cli` map to
|
|
54
|
+
`agentirc.cli:main`.
|
|
55
|
+
- Major version starts at `9.0.0` to leapfrog the
|
|
56
|
+
`agentirc-cli==8.7.X.devN` squat that culture previously published to
|
|
57
|
+
TestPyPI, so dev releases sort as the actual "Latest".
|
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
4
|
|
|
5
|
-
## Current state:
|
|
5
|
+
## Current state: server-core landed (9.1.0), CLI stubbed (PR-B2 next)
|
|
6
6
|
|
|
7
|
-
This repo is
|
|
7
|
+
This repo is the agentirc server-core extraction out of the sibling project [`culture`](https://github.com/OriNachum/culture). As of 9.1.0, the IRCd server core is in place — `agentirc/{ircd,server_link,channel,events,skill,remote_client,…}.py` and `agentirc/skills/{rooms,threads,history,icon}.py` are all present, copied from `culture@df50942` via the `cite-don't-copy` pattern (see `[tool.citation]` in `pyproject.toml`). Internal vendored support modules live under `agentirc/_internal/` (`aio`, `constants`, `protocol/`, `telemetry/`, `virtual_client`, `bots/` stubs).
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
What is **not** done yet:
|
|
10
|
+
- **`agentirc/cli.py`** is still the Shape A stub. All lifecycle verbs (`serve`, `start`, `stop`, `restart`, `status`, `link`, `logs`) print "not yet implemented" and exit 1. The real CLI lands in PR-B2 (extracted from `../culture/culture/cli/server.py`).
|
|
11
|
+
- **`agentirc/protocol.py`** does not exist yet (PR-B2 also).
|
|
12
|
+
- **Test suite migration** is PR-B3.
|
|
13
|
+
|
|
14
|
+
Read the bootstrap spec at `docs/superpowers/specs/2026-04-30-bootstrap-design.md` for the full plan; it is the operative source of truth and is intentionally self-contained. The culture-side counterpart spec is at `../culture/docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md` — not normally needed, but explains *why* if a decision looks arbitrary.
|
|
15
|
+
|
|
16
|
+
### Cite-don't-copy
|
|
17
|
+
|
|
18
|
+
Vendored culture code is tracked under `[tool.citation]` in `pyproject.toml` using the workspace's [`citation-cli`](https://github.com/OriNachum/citation-cli) tool. Each vendored file has a `quote` (verbatim copy), `paraphrase` (copied with import rewrites), or `synthesize` (rewritten as agentirc-native) status. Run `cite check` to verify integrity. When pulling new culture changes, update the citation entries' source URLs and sha256s — the manifest is the provenance ledger.
|
|
10
19
|
|
|
11
20
|
## Three names, one project
|
|
12
21
|
|
|
@@ -22,9 +31,9 @@ There are three different names in play. Don't conflate them:
|
|
|
22
31
|
|
|
23
32
|
## What lives here vs. in culture
|
|
24
33
|
|
|
25
|
-
- **Server-core (here):** `ircd.py`, `server_link.py`, `channel.py`, `config.py`, `events.py`, the stores (`room_store`, `thread_store`, `history_store`), `rooms_util.py`, `skill.py`, and the `skills/` directory (`rooms`, `threads`, `history`, `icon`).
|
|
26
|
-
- **Stays in culture:** `client.py
|
|
27
|
-
- **Newly created here:** `agentirc/cli.py` (
|
|
34
|
+
- **Server-core (here):** `ircd.py`, `server_link.py`, `channel.py`, `config.py`, `events.py`, the stores (`room_store`, `thread_store`, `history_store`), `rooms_util.py`, `skill.py`, `remote_client.py`, and the `skills/` directory (`rooms`, `threads`, `history`, `icon`). Vendored support under `agentirc/_internal/` (`aio`, `constants`, `protocol/`, `telemetry/`, `virtual_client`, `bots/` stubs).
|
|
35
|
+
- **Stays in culture:** `client.py` (full IRC client transport, used by bots) and any test that exercises the IRC *client transport* rather than the server. Note: the bootstrap spec originally also listed `remote_client.py` as "stays in culture", but it turned out to be a 43-line server-side ghost-client stub used by `server_link.py`; it's vendored here. See PR-B1 commit history.
|
|
36
|
+
- **Newly created here:** `agentirc/cli.py` (today: skeleton stub from PR Shape A; PR-B2 extracts the real one from `../culture/culture/cli/server.py`), `agentirc/__main__.py`. **Coming in PR-B2:** `agentirc/protocol.py` (consolidates verb names, numerics, and extension tag names currently inlined as string literals in `ircd.py` / `client.py`).
|
|
28
37
|
|
|
29
38
|
When migrating tests, the rule is: pure server tests come here, transport tests stay in culture, mixed tests stay in culture and get rewritten to drive `agentirc serve` as a subprocess fixture rather than importing `IRCd` directly. When unsure, **prefer copying the test here** — this repo owns the IRCd.
|
|
30
39
|
|
|
@@ -34,7 +43,7 @@ Only three modules are public. Everything else is internal and may be refactored
|
|
|
34
43
|
|
|
35
44
|
| Module | Members |
|
|
36
45
|
|---|---|
|
|
37
|
-
| `agentirc.config` | `ServerConfig`, `LinkConfig`, `
|
|
46
|
+
| `agentirc.config` | `ServerConfig`, `LinkConfig`, `TelemetryConfig` |
|
|
38
47
|
| `agentirc.cli` | `main()`, `dispatch(argv) -> int` |
|
|
39
48
|
| `agentirc.protocol` | verb name constants, numeric reply codes, extension tag names |
|
|
40
49
|
|
|
@@ -51,7 +60,7 @@ Do not rename on-disk artifacts during the bootstrap. That is explicitly out of
|
|
|
51
60
|
## Hard invariants
|
|
52
61
|
|
|
53
62
|
- **No imports back into culture.** After the bootstrap, `git grep -E '^(from|import) culture' agentirc/ tests/` must return nothing. CI should enforce this.
|
|
54
|
-
- **
|
|
63
|
+
- **Cite-don't-copy adaptation only.** Files copy from `../culture/` with import paths rewritten and minimal adaptation where the dependency boundary forces it (e.g. `culture.bots.{bot_manager,http_listener}` are stubbed to no-ops in `agentirc/_internal/bots/`). All adaptations are recorded in `[tool.citation]` with status `paraphrase` or `synthesize`. Improvements beyond what's needed for the dependency boundary ship in follow-up PRs.
|
|
55
64
|
- **Single synthetic first commit.** Message format: `Initial import from culture@<SHA>` where `<SHA>` is the culture commit ID the caller provides. No cherry-picked history.
|
|
56
65
|
- **No backend SDKs, no `culture` console script.** agentirc must not depend on `claude-agent-sdk`, `anthropic`, `agex-cli`, `afi-cli`, `github-copilot-sdk`, or any other agent/backend SDK, and must not declare a `culture` console script. Those are culture concerns — agent backends and the `culture` command live in `../culture` and stay there.
|
|
57
66
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.1.0
|
|
4
4
|
Summary: Agent-friendly IRCd: server core for AI agent meshes
|
|
5
5
|
Project-URL: Homepage, https://github.com/OriNachum/agentirc
|
|
6
6
|
Project-URL: Issues, https://github.com/OriNachum/agentirc/issues
|
|
@@ -37,9 +37,13 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.12
|
|
38
38
|
Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
|
|
39
39
|
Requires-Python: >=3.10
|
|
40
|
+
Requires-Dist: opentelemetry-api>=1.22
|
|
41
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.22
|
|
42
|
+
Requires-Dist: opentelemetry-sdk>=1.22
|
|
40
43
|
Provides-Extra: dev
|
|
41
44
|
Requires-Dist: bandit; extra == 'dev'
|
|
42
45
|
Requires-Dist: black; extra == 'dev'
|
|
46
|
+
Requires-Dist: citation-cli; extra == 'dev'
|
|
43
47
|
Requires-Dist: flake8; extra == 'dev'
|
|
44
48
|
Requires-Dist: isort; extra == 'dev'
|
|
45
49
|
Requires-Dist: pylint; extra == 'dev'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Async utilities for culture."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def maybe_await(result):
|
|
9
|
+
"""Await the result only if it's a coroutine, otherwise return directly."""
|
|
10
|
+
if asyncio.iscoroutine(result):
|
|
11
|
+
return await result
|
|
12
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""No-op ``BotManager`` stub.
|
|
2
|
+
|
|
3
|
+
agentirc is a pure IRCd; bot infrastructure (loading agent backends from
|
|
4
|
+
config, dispatching events to them, graceful shutdown) is a culture concern
|
|
5
|
+
and lives in ``culture.bots.bot_manager``. This stub keeps ``IRCd.start()``
|
|
6
|
+
import-clean for standalone agentirc deployments. Culture's
|
|
7
|
+
:class:`culture.bots.bot_manager.BotManager` is API-compatible and replaces
|
|
8
|
+
this stub when culture wraps an ``IRCd`` (today by subclassing / attribute
|
|
9
|
+
replacement; eventually via a real injection point).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from agentirc.ircd import IRCd
|
|
18
|
+
from agentirc.skill import Event
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BotManager:
|
|
22
|
+
def __init__(self, server: "IRCd") -> None:
|
|
23
|
+
self.server = server
|
|
24
|
+
|
|
25
|
+
async def load_bots(self) -> None: # NOSONAR S7503: stub method must remain async to match the abstract contract real implementations override.
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def load_system_bots(self) -> None:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def get_bot(self, _nick: str):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
async def on_event(self, _event: "Event") -> None: # NOSONAR S7503: stub method must remain async to match the abstract contract real implementations override.
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
async def stop_all(self) -> None: # NOSONAR S7503: stub method must remain async to match the abstract contract real implementations override.
|
|
38
|
+
return None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""No-op ``HttpListener`` stub.
|
|
2
|
+
|
|
3
|
+
Pairs with the no-op :class:`agentirc._internal.bots.bot_manager.BotManager`.
|
|
4
|
+
The real implementation lives in ``culture.bots.http_listener`` and exposes
|
|
5
|
+
a webhook surface for triggering bot events. In a standalone agentirc
|
|
6
|
+
deployment there is nothing to listen for, so ``start()`` and ``stop()``
|
|
7
|
+
are no-ops.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from agentirc._internal.bots.bot_manager import BotManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HttpListener:
|
|
19
|
+
def __init__(self, bot_manager: "BotManager", host: str, port: int) -> None:
|
|
20
|
+
self.bot_manager = bot_manager
|
|
21
|
+
self.host = host
|
|
22
|
+
self.port = port
|
|
23
|
+
|
|
24
|
+
async def start(self) -> None: # NOSONAR S7503: stub method must remain async to match the abstract contract real implementations override.
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
async def stop(self) -> None: # NOSONAR S7503: stub method must remain async to match the abstract contract real implementations override.
|
|
28
|
+
return None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Project-wide constants. Keep strings here, never in source code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# System pseudo-user and channel
|
|
8
|
+
SYSTEM_USER_PREFIX = "system-"
|
|
9
|
+
SYSTEM_CHANNEL = "#system"
|
|
10
|
+
SYSTEM_USER_REALNAME = "Culture system messages"
|
|
11
|
+
|
|
12
|
+
# IRCv3 message-tag keys we emit/consume
|
|
13
|
+
EVENT_TAG_TYPE = "event"
|
|
14
|
+
EVENT_TAG_DATA = "event-data"
|
|
15
|
+
|
|
16
|
+
# Event-type name regex (dotted lowercase, ≥2 segments)
|
|
17
|
+
EVENT_TYPE_RE = re.compile(r"^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$")
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
_TAG_UNESCAPE = {
|
|
4
|
+
"\\:": ";",
|
|
5
|
+
"\\s": " ",
|
|
6
|
+
"\\\\": "\\",
|
|
7
|
+
"\\r": "\r",
|
|
8
|
+
"\\n": "\n",
|
|
9
|
+
}
|
|
10
|
+
_TAG_ESCAPE = {v: k for k, v in _TAG_UNESCAPE.items()}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _unescape_tag_value(value: str) -> str:
|
|
14
|
+
out = []
|
|
15
|
+
i = 0
|
|
16
|
+
while i < len(value):
|
|
17
|
+
if value[i] == "\\" and i + 1 < len(value):
|
|
18
|
+
two = value[i : i + 2]
|
|
19
|
+
# Per IRCv3 spec, unknown escapes drop the backslash (yield only
|
|
20
|
+
# the second char). Known escapes map to their defined character.
|
|
21
|
+
out.append(_TAG_UNESCAPE.get(two, value[i + 1]))
|
|
22
|
+
i += 2
|
|
23
|
+
continue
|
|
24
|
+
out.append(value[i])
|
|
25
|
+
i += 1
|
|
26
|
+
return "".join(out)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _escape_tag_value(value: str) -> str:
|
|
30
|
+
out = []
|
|
31
|
+
for ch in value:
|
|
32
|
+
if ch in _TAG_ESCAPE:
|
|
33
|
+
out.append(_TAG_ESCAPE[ch])
|
|
34
|
+
else:
|
|
35
|
+
out.append(ch)
|
|
36
|
+
return "".join(out)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Message:
|
|
41
|
+
"""An IRC protocol message per RFC 2812 §2.3.1 with IRCv3 message-tags.
|
|
42
|
+
|
|
43
|
+
Wire format: [@tags SPACE] [:prefix SPACE] command [params] CRLF
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
prefix: str | None = None
|
|
47
|
+
command: str = ""
|
|
48
|
+
params: list[str] = field(default_factory=list)
|
|
49
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _parse_tag_block(line: str) -> "tuple[dict[str, str], str]":
|
|
53
|
+
"""Extract leading @tag block from a wire line.
|
|
54
|
+
|
|
55
|
+
Returns (tags_dict, remaining_line). If no tag block, returns ({}, line).
|
|
56
|
+
"""
|
|
57
|
+
if not line.startswith("@"):
|
|
58
|
+
return {}, line
|
|
59
|
+
if " " not in line:
|
|
60
|
+
return {}, "" # malformed — no command after tags
|
|
61
|
+
tag_blob, rest = line[1:].split(" ", 1)
|
|
62
|
+
tags: dict[str, str] = {}
|
|
63
|
+
for piece in tag_blob.split(";"):
|
|
64
|
+
if not piece:
|
|
65
|
+
continue
|
|
66
|
+
if "=" in piece:
|
|
67
|
+
key, value = piece.split("=", 1)
|
|
68
|
+
tags[key] = _unescape_tag_value(value)
|
|
69
|
+
else:
|
|
70
|
+
tags[piece] = ""
|
|
71
|
+
return tags, rest
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def parse(cls, line: str) -> "Message":
|
|
75
|
+
line = line.rstrip("\r\n")
|
|
76
|
+
tags, line = cls._parse_tag_block(line)
|
|
77
|
+
|
|
78
|
+
if not line:
|
|
79
|
+
# malformed @-only input
|
|
80
|
+
return cls(tags=tags, prefix=None, command="", params=[])
|
|
81
|
+
|
|
82
|
+
prefix = None
|
|
83
|
+
if line.startswith(":"):
|
|
84
|
+
if " " not in line:
|
|
85
|
+
return cls(tags=tags, prefix=None, command="", params=[])
|
|
86
|
+
prefix, line = line.split(" ", 1)
|
|
87
|
+
prefix = prefix[1:]
|
|
88
|
+
|
|
89
|
+
trailing = None
|
|
90
|
+
if " :" in line:
|
|
91
|
+
line, trailing = line.split(" :", 1)
|
|
92
|
+
|
|
93
|
+
parts = line.split()
|
|
94
|
+
if not parts:
|
|
95
|
+
return cls(tags=tags, prefix=prefix, command="", params=[])
|
|
96
|
+
command = parts[0].upper()
|
|
97
|
+
params = parts[1:]
|
|
98
|
+
if trailing is not None:
|
|
99
|
+
params.append(trailing)
|
|
100
|
+
|
|
101
|
+
return cls(tags=tags, prefix=prefix, command=command, params=params)
|
|
102
|
+
|
|
103
|
+
def format(self) -> str:
|
|
104
|
+
parts = []
|
|
105
|
+
|
|
106
|
+
if self.tags:
|
|
107
|
+
tag_pieces = []
|
|
108
|
+
for key, value in self.tags.items():
|
|
109
|
+
if value == "":
|
|
110
|
+
tag_pieces.append(key)
|
|
111
|
+
else:
|
|
112
|
+
tag_pieces.append(f"{key}={_escape_tag_value(value)}")
|
|
113
|
+
parts.append("@" + ";".join(tag_pieces))
|
|
114
|
+
|
|
115
|
+
if self.prefix:
|
|
116
|
+
parts.append(f":{self.prefix}")
|
|
117
|
+
parts.append(self.command)
|
|
118
|
+
|
|
119
|
+
if self.params:
|
|
120
|
+
for param in self.params[:-1]:
|
|
121
|
+
parts.append(param)
|
|
122
|
+
last = self.params[-1]
|
|
123
|
+
if " " in last or not last or last.startswith(":"):
|
|
124
|
+
parts.append(f":{last}")
|
|
125
|
+
else:
|
|
126
|
+
parts.append(last)
|
|
127
|
+
|
|
128
|
+
return " ".join(parts) + "\r\n"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""IRC numeric reply codes (RFC 2812 §5)."""
|
|
2
|
+
|
|
3
|
+
# Connection registration
|
|
4
|
+
RPL_WELCOME = "001"
|
|
5
|
+
RPL_YOURHOST = "002"
|
|
6
|
+
RPL_CREATED = "003"
|
|
7
|
+
RPL_MYINFO = "004"
|
|
8
|
+
|
|
9
|
+
# Channel
|
|
10
|
+
RPL_LISTSTART = "321"
|
|
11
|
+
RPL_LIST = "322"
|
|
12
|
+
RPL_LISTEND = "323"
|
|
13
|
+
RPL_TOPIC = "332"
|
|
14
|
+
RPL_NOTOPIC = "331"
|
|
15
|
+
RPL_NAMREPLY = "353"
|
|
16
|
+
RPL_ENDOFNAMES = "366"
|
|
17
|
+
|
|
18
|
+
# Mode
|
|
19
|
+
RPL_UMODEIS = "221"
|
|
20
|
+
RPL_CHANNELMODEIS = "324"
|
|
21
|
+
|
|
22
|
+
# Discovery
|
|
23
|
+
RPL_WHOISUSER = "311"
|
|
24
|
+
RPL_WHOISSERVER = "312"
|
|
25
|
+
RPL_ENDOFWHOIS = "318"
|
|
26
|
+
RPL_WHOISCHANNELS = "319"
|
|
27
|
+
RPL_WHOREPLY = "352"
|
|
28
|
+
RPL_ENDOFWHO = "315"
|
|
29
|
+
|
|
30
|
+
# Errors
|
|
31
|
+
ERR_NOSUCHNICK = "401"
|
|
32
|
+
ERR_NOSUCHSERVER = "402"
|
|
33
|
+
ERR_NOSUCHCHANNEL = "403"
|
|
34
|
+
ERR_CANNOTSENDTOCHAN = "404"
|
|
35
|
+
ERR_UNKNOWNCOMMAND = "421"
|
|
36
|
+
ERR_NONICKNAMEGIVEN = "431"
|
|
37
|
+
ERR_ERRONEUSNICKNAME = "432"
|
|
38
|
+
ERR_NICKNAMEINUSE = "433"
|
|
39
|
+
ERR_USERNOTINCHANNEL = "441"
|
|
40
|
+
ERR_NOTONCHANNEL = "442"
|
|
41
|
+
ERR_NEEDMOREPARAMS = "461"
|
|
42
|
+
ERR_ALREADYREGISTRED = "462"
|
|
43
|
+
ERR_CHANOPRIVSNEEDED = "482"
|
|
44
|
+
ERR_USERSDONTMATCH = "502"
|
|
45
|
+
|
|
46
|
+
# Error/reply message texts
|
|
47
|
+
MSG_NEEDMOREPARAMS = "Not enough parameters"
|
|
48
|
+
MSG_NOSUCHCHANNEL = "No such channel"
|
|
49
|
+
MSG_NOSUCHNICK = "No such nick"
|
|
50
|
+
MSG_NOTONCHANNEL = "You're not on that channel"
|
|
51
|
+
MSG_ENDOFWHO = "End of WHO list"
|
|
52
|
+
MSG_NOSUCHTHREAD = "No such thread"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""OpenTelemetry integration for Culture.
|
|
2
|
+
|
|
3
|
+
Public surface re-exported here; call sites import from `culture.telemetry`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from agentirc._internal.telemetry.audit import AuditSink, build_audit_record, init_audit, utc_iso_timestamp
|
|
7
|
+
from agentirc._internal.telemetry.context import (
|
|
8
|
+
TRACEPARENT_TAG,
|
|
9
|
+
TRACESTATE_TAG,
|
|
10
|
+
ExtractResult,
|
|
11
|
+
context_from_traceparent,
|
|
12
|
+
current_traceparent,
|
|
13
|
+
extract_traceparent_from_tags,
|
|
14
|
+
inject_traceparent,
|
|
15
|
+
)
|
|
16
|
+
from agentirc._internal.telemetry.metrics import MetricsRegistry, init_metrics
|
|
17
|
+
from agentirc._internal.telemetry.tracing import init_telemetry
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AuditSink",
|
|
21
|
+
"ExtractResult",
|
|
22
|
+
"MetricsRegistry",
|
|
23
|
+
"TRACEPARENT_TAG",
|
|
24
|
+
"TRACESTATE_TAG",
|
|
25
|
+
"build_audit_record",
|
|
26
|
+
"context_from_traceparent",
|
|
27
|
+
"current_traceparent",
|
|
28
|
+
"extract_traceparent_from_tags",
|
|
29
|
+
"init_audit",
|
|
30
|
+
"init_metrics",
|
|
31
|
+
"init_telemetry",
|
|
32
|
+
"inject_traceparent",
|
|
33
|
+
"utc_iso_timestamp",
|
|
34
|
+
]
|