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.
Files changed (57) hide show
  1. agentirc_cli-9.1.0/CHANGELOG.md +57 -0
  2. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/CLAUDE.md +17 -8
  3. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/PKG-INFO +5 -1
  4. agentirc_cli-9.1.0/agentirc/_internal/aio.py +12 -0
  5. agentirc_cli-9.1.0/agentirc/_internal/bots/__init__.py +0 -0
  6. agentirc_cli-9.1.0/agentirc/_internal/bots/bot_manager.py +38 -0
  7. agentirc_cli-9.1.0/agentirc/_internal/bots/http_listener.py +28 -0
  8. agentirc_cli-9.1.0/agentirc/_internal/constants.py +17 -0
  9. agentirc_cli-9.1.0/agentirc/_internal/protocol/__init__.py +0 -0
  10. agentirc_cli-9.1.0/agentirc/_internal/protocol/message.py +128 -0
  11. agentirc_cli-9.1.0/agentirc/_internal/protocol/replies.py +52 -0
  12. agentirc_cli-9.1.0/agentirc/_internal/telemetry/__init__.py +34 -0
  13. agentirc_cli-9.1.0/agentirc/_internal/telemetry/audit.py +394 -0
  14. agentirc_cli-9.1.0/agentirc/_internal/telemetry/context.py +121 -0
  15. agentirc_cli-9.1.0/agentirc/_internal/telemetry/metrics.py +250 -0
  16. agentirc_cli-9.1.0/agentirc/_internal/telemetry/tracing.py +117 -0
  17. agentirc_cli-9.1.0/agentirc/_internal/virtual_client.py +231 -0
  18. agentirc_cli-9.1.0/agentirc/channel.py +80 -0
  19. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/agentirc/cli.py +7 -7
  20. agentirc_cli-9.1.0/agentirc/config.py +49 -0
  21. agentirc_cli-9.1.0/agentirc/events.py +117 -0
  22. agentirc_cli-9.1.0/agentirc/history_store.py +91 -0
  23. agentirc_cli-9.1.0/agentirc/ircd.py +718 -0
  24. agentirc_cli-9.1.0/agentirc/remote_client.py +43 -0
  25. agentirc_cli-9.1.0/agentirc/room_store.py +71 -0
  26. agentirc_cli-9.1.0/agentirc/rooms_util.py +56 -0
  27. agentirc_cli-9.1.0/agentirc/server_link.py +1166 -0
  28. agentirc_cli-9.1.0/agentirc/skill.py +61 -0
  29. agentirc_cli-9.1.0/agentirc/skills/__init__.py +0 -0
  30. agentirc_cli-9.1.0/agentirc/skills/history.py +225 -0
  31. agentirc_cli-9.1.0/agentirc/skills/icon.py +52 -0
  32. agentirc_cli-9.1.0/agentirc/skills/rooms.py +834 -0
  33. agentirc_cli-9.1.0/agentirc/skills/threads.py +709 -0
  34. agentirc_cli-9.1.0/agentirc/thread_store.py +52 -0
  35. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +21 -4
  36. agentirc_cli-9.1.0/pyproject.toml +248 -0
  37. agentirc_cli-9.1.0/tests/__init__.py +0 -0
  38. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/uv.lock +227 -2
  39. agentirc_cli-9.0.0/pyproject.toml +0 -51
  40. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/SKILL.md +0 -0
  41. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
  42. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
  43. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
  44. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
  45. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
  46. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills/pr-review/scripts/workflow.sh +0 -0
  47. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.claude/skills.local.yaml.example +0 -0
  48. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.github/workflows/publish.yml +0 -0
  49. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.github/workflows/tests.yml +0 -0
  50. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/.gitignore +0 -0
  51. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/LICENSE +0 -0
  52. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/README.md +0 -0
  53. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/agentirc/__init__.py +0 -0
  54. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/agentirc/__main__.py +0 -0
  55. {agentirc_cli-9.0.0/tests → agentirc_cli-9.1.0/agentirc/_internal}/__init__.py +0 -0
  56. {agentirc_cli-9.0.0 → agentirc_cli-9.1.0}/docs/steward/onboarding.md +0 -0
  57. {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: skeleton package, pre-extraction
5
+ ## Current state: server-core landed (9.1.0), CLI stubbed (PR-B2 next)
6
6
 
7
- This repo is being bootstrapped from a server-core extraction out of the sibling project [`culture`](https://github.com/OriNachum/culture). The package skeleton is in place — `pyproject.toml`, `agentirc/__init__.py`, `agentirc/__main__.py`, `agentirc/cli.py` (stub) and `pip install -e .` produces working `agentirc` and `agentirc-cli` console scripts. The IRCd server core has **not** been copied from culture yet; all lifecycle verbs (`serve`, `start`, `stop`, `restart`, `status`, `link`, `logs`) currently exit non-zero with a "not yet implemented" message. **Read the bootstrap spec before doing anything else**; it is the operative source of truth and is intentionally self-contained.
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
- The culture-side counterpart spec is at `../culture/docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md`. You should not need to open it to act, but it explains the *why* if a decision in the agentirc spec looks arbitrary.
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`, `remote_client.py`, and any test that exercises the IRC *client transport* rather than the server.
27
- - **Newly created here:** `agentirc/cli.py` (extracted from `../culture/culture/cli/server.py`), `agentirc/__main__.py`, and `agentirc/protocol.py` (consolidates verb names, numerics, and extension tag names that today are inlined as string literals in `ircd.py` / `client.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`, `PeerSpec` |
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
- - **No file rewrites in the bootstrap.** Files copy from `../culture/culture/agentirc/` as-is, with import paths rewritten (`from culture.agentirc.X` `from agentirc.X`) and nothing else. Improvements ship in follow-up PRs.
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.0.0
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_-]*)+$")
@@ -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
+ ]