agentirc-cli 9.1.0__tar.gz → 9.2.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 (64) hide show
  1. agentirc_cli-9.2.0/CHANGELOG.md +118 -0
  2. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/CLAUDE.md +34 -17
  3. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/PKG-INFO +1 -1
  4. agentirc_cli-9.2.0/agentirc/_internal/cli_shared/constants.py +56 -0
  5. agentirc_cli-9.2.0/agentirc/_internal/cli_shared/mesh.py +39 -0
  6. agentirc_cli-9.2.0/agentirc/_internal/pidfile.py +238 -0
  7. agentirc_cli-9.2.0/agentirc/cli.py +659 -0
  8. agentirc_cli-9.2.0/agentirc/client.py +1070 -0
  9. agentirc_cli-9.2.0/agentirc/protocol.py +259 -0
  10. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +19 -22
  11. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/pyproject.toml +57 -1
  12. agentirc_cli-9.2.0/tests/__init__.py +0 -0
  13. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/tests/test_cli.py +35 -6
  14. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/uv.lock +1 -1
  15. agentirc_cli-9.1.0/CHANGELOG.md +0 -57
  16. agentirc_cli-9.1.0/agentirc/cli.py +0 -104
  17. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/SKILL.md +0 -0
  18. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
  19. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
  20. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
  21. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
  22. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
  23. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills/pr-review/scripts/workflow.sh +0 -0
  24. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.claude/skills.local.yaml.example +0 -0
  25. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.github/workflows/publish.yml +0 -0
  26. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.github/workflows/tests.yml +0 -0
  27. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/.gitignore +0 -0
  28. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/LICENSE +0 -0
  29. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/README.md +0 -0
  30. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/__init__.py +0 -0
  31. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/__main__.py +0 -0
  32. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/__init__.py +0 -0
  33. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/aio.py +0 -0
  34. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/bots/__init__.py +0 -0
  35. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/bots/bot_manager.py +0 -0
  36. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/bots/http_listener.py +0 -0
  37. {agentirc_cli-9.1.0/agentirc/_internal/protocol → agentirc_cli-9.2.0/agentirc/_internal/cli_shared}/__init__.py +0 -0
  38. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/constants.py +0 -0
  39. {agentirc_cli-9.1.0/agentirc/skills → agentirc_cli-9.2.0/agentirc/_internal/protocol}/__init__.py +0 -0
  40. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/protocol/message.py +0 -0
  41. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/protocol/replies.py +0 -0
  42. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/telemetry/__init__.py +0 -0
  43. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/telemetry/audit.py +0 -0
  44. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/telemetry/context.py +0 -0
  45. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/telemetry/metrics.py +0 -0
  46. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/telemetry/tracing.py +0 -0
  47. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/_internal/virtual_client.py +0 -0
  48. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/channel.py +0 -0
  49. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/config.py +0 -0
  50. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/events.py +0 -0
  51. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/history_store.py +0 -0
  52. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/ircd.py +0 -0
  53. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/remote_client.py +0 -0
  54. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/room_store.py +0 -0
  55. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/rooms_util.py +0 -0
  56. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/server_link.py +0 -0
  57. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/skill.py +0 -0
  58. {agentirc_cli-9.1.0/tests → agentirc_cli-9.2.0/agentirc/skills}/__init__.py +0 -0
  59. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/skills/history.py +0 -0
  60. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/skills/icon.py +0 -0
  61. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/skills/rooms.py +0 -0
  62. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/skills/threads.py +0 -0
  63. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/agentirc/thread_store.py +0 -0
  64. {agentirc_cli-9.1.0 → agentirc_cli-9.2.0}/docs/steward/onboarding.md +0 -0
@@ -0,0 +1,118 @@
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.2.0] - 2026-05-01
8
+
9
+ ### Added
10
+
11
+ - `agentirc/protocol.py` — public, semver-tracked module consolidating
12
+ IRC verb names, numeric reply codes (re-exported from
13
+ `_internal.protocol.replies`), and IRCv3 / agentirc tag names. Wire-
14
+ format quirks (`ROOMETAEND`, `ROOMETASET` typos; `ERR_NOSUCHCHANNEL`
15
+ semantic misuse; `STHREAD` verb collapse) are preserved verbatim with
16
+ explanatory comments — they require coordinated cross-repo bumps to
17
+ fix.
18
+ - `agentirc/client.py` — IRC client transport vendored from
19
+ `culture/agentirc/client.py` at SHA `df50942`. Body unchanged; only
20
+ imports rewritten. The bootstrap spec originally said this would
21
+ "stay in culture" but its dependency surface (already-vendored
22
+ `_internal` support modules + opentelemetry) made vendoring the
23
+ cleaner path. Without it, `agentirc/ircd.py:580`'s runtime
24
+ `from agentirc.client import Client` raised `ImportError` on the
25
+ first TCP IRC connection.
26
+ - Real `agentirc/cli.py` — verb dispatch extracted from
27
+ `culture/cli/server.py`. New verbs: `serve` (foreground, no PID;
28
+ for systemd `Type=simple` and containers), `restart`, `link`
29
+ (peer-spec validator), `logs` (cat / tail of `~/.culture/logs/server-
30
+ <name>.log`). Existing verbs `start`/`stop`/`status` reuse culture's
31
+ proven daemonize / `_wait_for_port` / `_wait_for_graceful_stop` /
32
+ `_force_kill` helpers.
33
+ - Internal support modules:
34
+ - `agentirc/_internal/pidfile.py` — PID/port file management.
35
+ `is_managed_process()` recognizes both `culture` and
36
+ `agentirc`/`agentirc-cli` argv tokens; `is_culture_process` is
37
+ preserved as a thin alias.
38
+ - `agentirc/_internal/cli_shared/{constants,mesh}.py` — minimal
39
+ subset of `culture/cli/shared`. Keeps `DEFAULT_CONFIG`, `LOG_DIR`,
40
+ `culture_runtime_dir()`, `parse_link()`. Drops everything that
41
+ touched `culture.bots.config`, `culture.credentials`,
42
+ `culture.mesh_config`.
43
+ - Citations recorded in `[tool.citation]`: `culture-pidfile`,
44
+ `culture-cli-shared`, `culture-client`, `culture-cli-server`.
45
+
46
+ ### Changed
47
+
48
+ - Default server name changed from `culture` to `agentirc` in
49
+ `agentirc.cli`. PID/port files at `~/.culture/pids/server-<name>.{pid,
50
+ port}` keep their existing layout per the "Defaults preserve culture
51
+ continuity" rule, but the default fallback name no longer collides
52
+ with culture's daemon when both run on the same host.
53
+ - Dropped culture-only verbs (`default`, `rename`, `archive`,
54
+ `unarchive`) from agentirc's CLI surface — they manage culture's
55
+ agent manifest, which agentirc does not own.
56
+ - Dropped `--mesh-config` from `agentirc start` — depends on
57
+ `culture.credentials` / `culture.mesh_config` (out of scope).
58
+
59
+ ### Notes
60
+
61
+ - End-to-end smoke verified: `agentirc start --port 16667` boots,
62
+ TCP NICK/USER handshake returns `001 RPL_WELCOME` from a real
63
+ `IRCd`, `agentirc stop` shuts cleanly. `agentirc serve` is now
64
+ byte-indistinguishable from `culture server start` for the lifecycle
65
+ contract culture's shim relies on.
66
+ - Test suite migration (PR-B3) is the only remaining bootstrap slice.
67
+
68
+ ## [9.1.0] - 2026-04-30
69
+
70
+ ### Added
71
+
72
+ - Server-core vendored from `culture` at SHA `df50942`. The `agentirc`
73
+ package now contains the IRCd (`ircd.py`), server-to-server linking
74
+ (`server_link.py`), channel/event/store/skill modules, `remote_client.py`
75
+ (peer-server ghost client), and the four built-in skills
76
+ (`skills/{rooms,threads,history,icon}.py`).
77
+ - Internal vendored support modules under `agentirc/_internal/`:
78
+ - `aio` (`maybe_await`)
79
+ - `constants` (system user/channel constants)
80
+ - `protocol/` (IRC `Message` and numeric `replies`)
81
+ - `telemetry/` (OpenTelemetry audit/tracing/metrics — full subpackage)
82
+ - `virtual_client` (`VirtualClient` for in-process bot integration)
83
+ - `bots/{bot_manager,http_listener}` (no-op stubs; culture replaces
84
+ these at runtime when wrapping an `IRCd`)
85
+ - `[tool.citation]` block in `pyproject.toml` enumerating every vendored
86
+ file with a quote/paraphrase/synthesize status, source URL, and
87
+ sha256, validated by `cite check`.
88
+ - Runtime dependencies: `opentelemetry-api`, `opentelemetry-sdk`,
89
+ `opentelemetry-exporter-otlp-proto-grpc` (all `>=1.22`).
90
+ - Dev dependency: `citation-cli` (provides the `cite` console script).
91
+
92
+ ### Changed
93
+
94
+ - Bootstrap spec deviation: `remote_client.py` was originally listed as
95
+ "do not copy" but turned out to be server-side (used by `server_link`
96
+ and `virtual_client` for peer-server users in channel member lists).
97
+ Vendored as public `agentirc/remote_client.py`. See commit `8b4a6d8`.
98
+
99
+ ### Notes
100
+
101
+ - `agentirc/cli.py` still ships only `version`; the `serve|start|stop|
102
+ restart|status|link|logs` lifecycle verbs remain stubs. The real CLI
103
+ is the next slice (PR-B2).
104
+ - Tests are not migrated yet (PR-B3).
105
+
106
+ ## [9.0.0] - 2026-04-30
107
+
108
+ ### Added
109
+
110
+ - Initial bootstrap of `agentirc-cli` as an installable Python package.
111
+ - Skeleton `agentirc/{__init__,__main__,cli}.py` with `version` verb
112
+ wired up and lifecycle verbs (`serve|start|stop|restart|status|link|
113
+ logs`) as stubs.
114
+ - Console scripts: both `agentirc` and `agentirc-cli` map to
115
+ `agentirc.cli:main`.
116
+ - Major version starts at `9.0.0` to leapfrog the
117
+ `agentirc-cli==8.7.X.devN` squat that culture previously published to
118
+ TestPyPI, so dev releases sort as the actual "Latest".
@@ -2,14 +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: server-core landed (9.1.0), CLI stubbed (PR-B2 next)
5
+ ## Current state: server-core + real CLI + protocol module landed (9.2.0)
6
6
 
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).
7
+ This repo is the agentirc server-core extraction out of the sibling project [`culture`](https://github.com/OriNachum/culture). As of 9.2.0:
8
+
9
+ - **Server-core** (`agentirc/{ircd,server_link,channel,events,skill,remote_client,…}.py`, `agentirc/skills/{rooms,threads,history,icon}.py`) — vendored from `culture@df50942` via the `cite-don't-copy` pattern (see `[tool.citation]` in `pyproject.toml`).
10
+ - **Client transport** (`agentirc/client.py`) — vendored from `culture/agentirc/client.py` in PR-B2. The bootstrap spec originally said this would "stay in culture", but the dependency-boundary analysis after PR-B1 showed `client.py` only imports already-vendored support modules plus opentelemetry. Without it, `agentirc/ircd.py:580`'s runtime `from agentirc.client import Client` raised `ImportError` on the first TCP IRC connection.
11
+ - **Public CLI** (`agentirc/cli.py`) — real verb dispatch extracted from `culture/cli/server.py`. Verbs: `serve` (foreground, no PID; for systemd `Type=simple` and containers), `start`/`stop`/`status` (lifecycle), `restart`, `link` (peer-spec validator), `logs` (cat / tail of `~/.culture/logs/server-<name>.log`), `version`.
12
+ - **Public protocol** (`agentirc/protocol.py`) — verb name constants, numerics, IRCv3 tag names. Wire-format quirks (`ROOMETAEND`, `ROOMETASET` typos, `ERR_NOSUCHCHANNEL` semantic misuse, `STHREAD` verb collapse) preserved verbatim — they need coordinated cross-repo bumps to fix.
13
+ - **Internal support** (`agentirc/_internal/`) — `aio`, `constants`, `protocol/`, `telemetry/`, `virtual_client`, `pidfile` (PR-B2), `cli_shared/` (PR-B2), `bots/` stubs.
14
+
15
+ End-to-end verified: `agentirc start --port <p>` boots a real IRCd, TCP NICK/USER handshake returns `001 RPL_WELCOME`, `agentirc stop` shuts cleanly. `agentirc serve` is byte-indistinguishable from `culture server start` for the lifecycle contract culture's shim relies on.
8
16
 
9
17
  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.
18
+ - **Test suite migration** is PR-B3 only remaining bootstrap slice.
13
19
 
14
20
  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
21
 
@@ -31,11 +37,12 @@ There are three different names in play. Don't conflate them:
31
37
 
32
38
  ## What lives here vs. in culture
33
39
 
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`).
40
+ - **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`).
41
+ - **Client transport (here, since 9.2.0):** `client.py` vendored in PR-B2. Pre-9.2 the bootstrap spec said "stays in culture"; that assumption broke down once we found `client.py` only imports already-vendored modules and that the IRCd needs it at runtime to accept TCP clients.
42
+ - **Internal support (here):** `agentirc/_internal/{aio, constants, protocol/, telemetry/, virtual_client, pidfile, cli_shared/}` plus `bots/` no-op stubs (the real `culture.bots.*` depends on backend SDKs forbidden by agentirc's dependency boundary; culture replaces the stubs at runtime when wrapping an IRCd).
43
+ - **Stays in culture:** `culture.bots.*` (the real bot manager), `culture.config` / `culture.bots.config` (agent-manifest concerns), `culture.cli.shared.{ipc,display,formatting,process}` (CLI ergonomics agentirc doesn't need), `culture.credentials` / `culture.mesh_config` (OS-keyring + mesh.yaml).
37
44
 
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.
45
+ When migrating tests, the rule is: pure server tests come here, transport tests **also** come here now that we own `client.py`, 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 and the client transport.
39
46
 
40
47
  ## Public API contract (semver-tracked)
41
48
 
@@ -49,6 +56,11 @@ Only three modules are public. Everything else is internal and may be refactored
49
56
 
50
57
  `agentirc.cli.dispatch(argv)` is the function `culture`'s `culture server` shim calls — it must accept the exact same flag set, exit codes, and stderr formatting that `culture server` produces today. Do not "improve" CLI ergonomics during the bootstrap; that breaks the transparency contract culture relies on. `dispatch()` returns `int` on successful command dispatch and lets argparse's `SystemExit` propagate on `--help`/`--version`/parse-errors per Python convention; in-process callers (i.e. culture's shim) must catch `SystemExit` themselves or use `subprocess`.
51
58
 
59
+ Two intentional, additive deltas vs. culture's CLI:
60
+
61
+ - `agentirc status` prints `Server 'X': running (PID N, port P)` when a port file is present — culture only prints `(PID N)`. Strictly a superset; culture's shim relies on exit codes, not output parsing.
62
+ - `agentirc start` no longer accepts `--mesh-config` (depends on `culture.credentials` and `culture.mesh_config`, out of agentirc's scope). Use `--link name:host:port:password[:trust]` flags instead.
63
+
52
64
  ## Defaults preserve culture continuity
53
65
 
54
66
  - Default `--config` path: `~/.culture/server.yaml` (yes, `.culture/`, not `.agentirc/`).
@@ -76,14 +88,19 @@ pytest -n auto
76
88
  # Run a single test
77
89
  pytest tests/path/to/test_file.py::test_name -v
78
90
 
79
- # CLI smoke (works today against the skeleton)
91
+ # CLI smoke
80
92
  agentirc --help
81
93
  agentirc-cli --help # alias of agentirc
82
- agentirc version # prints "agentirc 9.0.0"
94
+ agentirc version # prints "agentirc 9.2.0"
83
95
  python -m agentirc version # equivalent
84
96
 
85
- # Lifecycle verbs (stubs in 9.0.0; real impls land with the IRCd extraction)
86
- agentirc serve --config ~/.culture/server.yaml
97
+ # Lifecycle (functional since 9.2.0)
98
+ agentirc serve --config ~/.culture/server.yaml # foreground, no PID
99
+ agentirc start --name spark --host 127.0.0.1 --port 6667 # daemonize
100
+ agentirc status --name spark
101
+ agentirc stop --name spark
102
+ agentirc logs --name spark -f # tail -f the daemon log
103
+ agentirc link 'peer1:host:6667:secret:full' # parse + validate spec
87
104
  ```
88
105
 
89
106
  CLI verbs: `serve`, `start`, `stop`, `restart`, `status`, `link`, `logs`, `version`. Of these, only `start`, `stop`, `status` have a `culture server …` analogue today; the rest are agentirc-only additions. Culture's pure-passthrough shim only ever emits its existing verbs, so the additions don't break it.
@@ -129,7 +146,7 @@ Per-machine paths for these skills go in `.claude/skills.local.yaml` (gitignored
129
146
 
130
147
  The full list lives in §"Acceptance criteria" of the bootstrap spec. The non-obvious ones:
131
148
 
132
- - `pip install agentirc-cli==9.0.0` on a clean venv produces working `agentirc` *and* `agentirc-cli` binaries (both pointing at `agentirc.cli:main`).
133
- - `agentirc serve` is byte-indistinguishable from `culture server start` (same socket, same logs, same systemd integration).
134
- - `agentirc.config.LinkConfig`, `agentirc.config.PeerSpec`, `agentirc.cli.dispatch`, `agentirc.protocol.*` all import from a clean Python session.
135
- - `docs/api-stability.md` names the three public modules.
149
+ - `pip install agentirc-cli==9.2.0` on a clean venv produces working `agentirc` *and* `agentirc-cli` binaries (both pointing at `agentirc.cli:main`). ✅ since 9.0.0.
150
+ - `agentirc serve` is byte-indistinguishable from `culture server start` (same socket, same logs, same systemd integration). ✅ since 9.2.0.
151
+ - `agentirc.config.{ServerConfig, LinkConfig, TelemetryConfig}`, `agentirc.cli.{main, dispatch}`, `agentirc.protocol.*` all import from a clean Python session. ✅ since 9.2.0.
152
+ - `docs/api-stability.md` names the three public modules. ⏳ pending.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 9.1.0
3
+ Version: 9.2.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
@@ -0,0 +1,56 @@
1
+ """Shared constants for the agentirc CLI.
2
+
3
+ Vendored from culture@df50942 (`culture/cli/shared/constants.py`) and
4
+ trimmed to the subset agentirc actually consumes. Bot-related constants
5
+ (``BOT_CONFIG_FILE``, ``LEGACY_CONFIG``, ``AGENTS_YAML``, etc.) are
6
+ dropped; agentirc has no bot configuration concept.
7
+
8
+ Default paths (``~/.culture/server.yaml``, ``~/.culture/logs``) are kept
9
+ intact per the "Defaults preserve culture continuity" rule in
10
+ CLAUDE.md, so agentirc and culture daemons share state directories on a
11
+ host without separate config trees.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import stat
18
+
19
+ DEFAULT_CONFIG = os.path.expanduser("~/.culture/server.yaml")
20
+ LOG_DIR = os.path.expanduser("~/.culture/logs")
21
+
22
+ _CONFIG_HELP = "Config file path"
23
+ _SERVER_NAME_HELP = "Server name"
24
+
25
+
26
+ def culture_runtime_dir() -> str:
27
+ """Return a user-private directory for daemon sockets.
28
+
29
+ Resolution order:
30
+
31
+ 1. ``$XDG_RUNTIME_DIR`` when set (Linux/systemd default — already
32
+ user-private at ``/run/user/<uid>``).
33
+ 2. ``~/.culture/run/`` otherwise (typical macOS path), created mode
34
+ 0700 if missing and re-tightened to 0700 on every call so a
35
+ hand-created or pre-existing dir cannot leak sockets.
36
+
37
+ Raises ``RuntimeError`` when neither ``XDG_RUNTIME_DIR`` nor a
38
+ resolvable home directory is available — silently writing a literal
39
+ ``~/.culture/run`` directory in CWD would surprise callers and the
40
+ daemons (which now route through this resolver) would fail at
41
+ socket-bind time anyway.
42
+ """
43
+ xdg = os.environ.get("XDG_RUNTIME_DIR")
44
+ if xdg:
45
+ return xdg
46
+ home = os.path.expanduser("~")
47
+ if not home or home == "~" or not os.path.isabs(home):
48
+ raise RuntimeError(
49
+ "culture_runtime_dir(): cannot resolve a home directory "
50
+ "(os.path.expanduser('~') returned %r). Set $HOME or "
51
+ "$XDG_RUNTIME_DIR before running agentirc commands." % home
52
+ )
53
+ fallback = os.path.join(home, ".culture", "run")
54
+ os.makedirs(fallback, mode=0o700, exist_ok=True)
55
+ os.chmod(fallback, stat.S_IRWXU)
56
+ return fallback
@@ -0,0 +1,39 @@
1
+ """Mesh and link helpers for the agentirc CLI.
2
+
3
+ Vendored from culture@df50942 (`culture/cli/shared/mesh.py`), reduced
4
+ to the single helper agentirc needs: ``parse_link``. The upstream
5
+ helpers ``resolve_links_from_mesh`` and ``generate_mesh_from_agents``
6
+ depend on culture's ``mesh_config`` / ``credentials`` modules, which
7
+ manage agent mesh and OS-keyring credential lookup — concepts that do
8
+ not belong in agentirc's dependency-bounded surface.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+
15
+
16
+ def parse_link(value: str):
17
+ """Parse a link spec: ``name:host:port:password[:trust]``.
18
+
19
+ Trust is extracted from the end if it matches a known value. This
20
+ allows passwords containing colons to round-trip through argparse
21
+ without escaping.
22
+ """
23
+ from agentirc.config import LinkConfig
24
+
25
+ trust = "full"
26
+ if value.endswith(":full") or value.endswith(":restricted"):
27
+ value, trust = value.rsplit(":", 1)
28
+
29
+ parts = value.split(":", 3)
30
+ if len(parts) != 4:
31
+ raise argparse.ArgumentTypeError(
32
+ f"Link must be name:host:port:password[:trust], got: {value}"
33
+ )
34
+ name, host, port_str, password = parts
35
+ try:
36
+ port = int(port_str)
37
+ except ValueError as exc:
38
+ raise argparse.ArgumentTypeError(f"Invalid port: {port_str}") from exc
39
+ return LinkConfig(name=name, host=host, port=port, password=password, trust=trust)
@@ -0,0 +1,238 @@
1
+ """PID file management for agentirc daemon instances.
2
+
3
+ Vendored from culture@df50942 (`culture/pidfile.py`) and adapted for
4
+ agentirc per the cite-don't-copy convention. The on-disk layout
5
+ (`~/.culture/pids/<name>.{pid,port}`) is preserved unchanged so culture
6
+ and agentirc daemons can coexist on a host without separate state
7
+ directories.
8
+
9
+ Adaptation versus the upstream copy: `is_managed_process()` accepts
10
+ both ``culture`` and ``agentirc`` argv tokens. ``is_culture_process``
11
+ remains as a thin alias so call sites that haven't migrated keep
12
+ working.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import re
19
+ from pathlib import Path
20
+
21
+ PID_DIR = os.path.expanduser("~/.culture/pids")
22
+
23
+ _MANAGED_PROCESS_TOKENS = ("culture", "agentirc", "agentirc-cli")
24
+
25
+
26
+ def _safe_name(name: str) -> str:
27
+ """Sanitize a daemon name to prevent path traversal."""
28
+ return re.sub(r"[^a-zA-Z0-9._-]", "_", Path(name).name)
29
+
30
+
31
+ def write_pid(name: str, pid: int) -> Path:
32
+ """Write a PID file for the named daemon. Creates the directory if needed."""
33
+ pid_dir = Path(PID_DIR)
34
+ pid_dir.mkdir(parents=True, exist_ok=True)
35
+ pid_path = pid_dir / f"{_safe_name(name)}.pid"
36
+ pid_path.write_text(str(pid))
37
+ return pid_path
38
+
39
+
40
+ def read_pid(name: str) -> int | None:
41
+ """Read the PID for the named daemon. Returns None if file is missing."""
42
+ pid_path = Path(PID_DIR) / f"{_safe_name(name)}.pid"
43
+ if not pid_path.exists():
44
+ return None
45
+ try:
46
+ return int(pid_path.read_text().strip())
47
+ except (ValueError, OSError):
48
+ return None
49
+
50
+
51
+ def remove_pid(name: str) -> None:
52
+ """Remove the PID file for the named daemon if it exists."""
53
+ pid_path = Path(PID_DIR) / f"{_safe_name(name)}.pid"
54
+ try:
55
+ pid_path.unlink()
56
+ except FileNotFoundError:
57
+ pass
58
+
59
+
60
+ def write_port(name: str, port: int) -> Path:
61
+ """Write a port file for the named daemon. Creates the directory if needed."""
62
+ pid_dir = Path(PID_DIR)
63
+ pid_dir.mkdir(parents=True, exist_ok=True)
64
+ port_path = pid_dir / f"{_safe_name(name)}.port"
65
+ port_path.write_text(str(port))
66
+ return port_path
67
+
68
+
69
+ def read_port(name: str) -> int | None:
70
+ """Read the port for the named daemon. Returns None if file is missing."""
71
+ port_path = Path(PID_DIR) / f"{_safe_name(name)}.port"
72
+ if not port_path.exists():
73
+ return None
74
+ try:
75
+ return int(port_path.read_text().strip())
76
+ except (ValueError, OSError):
77
+ return None
78
+
79
+
80
+ def remove_port(name: str) -> None:
81
+ """Remove the port file for the named daemon if it exists."""
82
+ port_path = Path(PID_DIR) / f"{_safe_name(name)}.port"
83
+ try:
84
+ port_path.unlink()
85
+ except FileNotFoundError:
86
+ pass
87
+
88
+
89
+ def is_managed_process(pid: int) -> bool:
90
+ """Check whether the given PID belongs to a culture or agentirc process.
91
+
92
+ Reads /proc/<pid>/cmdline on Linux and checks NUL-separated argv
93
+ tokens for an exact match against ``culture``, ``agentirc``, or
94
+ ``agentirc-cli`` (e.g. argv[0] basename or a ``-m culture``
95
+ argument). On platforms without /proc (macOS, Windows) or when
96
+ /proc parsing fails, falls back to ``ps -p <pid> -o command=``
97
+ when available; if neither identification path succeeds, returns
98
+ False (fail closed) so a stale PID file can never SIGTERM an
99
+ unrelated reused-PID process. Upstream culture treated the
100
+ macOS/Windows case as "assume valid"; PR #4 review flagged that
101
+ as exploitable when PIDs are reused.
102
+ """
103
+ if os.path.isdir("/proc"):
104
+ try:
105
+ raw = Path(f"/proc/{pid}/cmdline").read_bytes()
106
+ tokens = [t for t in raw.decode(errors="replace").split("\x00") if t]
107
+ return any(
108
+ os.path.basename(t) in _MANAGED_PROCESS_TOKENS
109
+ or t in _MANAGED_PROCESS_TOKENS
110
+ for t in tokens
111
+ )
112
+ except OSError:
113
+ return False
114
+ return _is_managed_via_ps(pid)
115
+
116
+
117
+ def _is_managed_via_ps(pid: int) -> bool:
118
+ """Fallback identity check using ``ps`` for non-/proc platforms.
119
+
120
+ Uses ``ps -p <pid> -o command=`` (POSIX) to recover the command
121
+ line and tests for a managed-process token. Any failure (no
122
+ ``ps``, non-zero exit, parse error) returns False so we fail
123
+ closed rather than signalling an unknown process.
124
+ """
125
+ import shutil
126
+ import subprocess
127
+
128
+ ps = shutil.which("ps")
129
+ if ps is None:
130
+ return False
131
+ try:
132
+ # argv is a fixed token list with the pid coerced to int — no shell
133
+ # interpretation, no path that traverses untrusted input.
134
+ result = subprocess.run(
135
+ [ps, "-p", str(int(pid)), "-o", "command="],
136
+ capture_output=True,
137
+ text=True,
138
+ timeout=2,
139
+ check=False,
140
+ )
141
+ except (OSError, subprocess.TimeoutExpired):
142
+ return False
143
+ if result.returncode != 0:
144
+ return False
145
+ cmdline = result.stdout.strip()
146
+ if not cmdline:
147
+ return False
148
+ tokens = cmdline.split()
149
+ return any(
150
+ os.path.basename(t) in _MANAGED_PROCESS_TOKENS or t in _MANAGED_PROCESS_TOKENS
151
+ for t in tokens
152
+ )
153
+
154
+
155
+ def is_culture_process(pid: int) -> bool:
156
+ """Backwards-compatible alias for :func:`is_managed_process`.
157
+
158
+ The upstream culture API used this name; preserved so existing call
159
+ sites in vendored code keep working without churn.
160
+ """
161
+ return is_managed_process(pid)
162
+
163
+
164
+ def is_process_alive(pid: int) -> bool:
165
+ """Check whether a process with the given PID is alive.
166
+
167
+ Uses signal 0 (existence test, no signal delivered) per POSIX. The
168
+ ``# NOSONAR S4828`` marker is intentional: signal 0 cannot deliver
169
+ a signal to the target — it only reports whether the kernel can
170
+ locate the PID. Sonar's "sending signals is safe here" rule is a
171
+ false positive for this idiom.
172
+ """
173
+ try:
174
+ os.kill(pid, 0) # NOSONAR S4828 — sig=0 is existence test, no delivery
175
+ return True
176
+ except ProcessLookupError:
177
+ return False
178
+ except PermissionError:
179
+ return True
180
+
181
+
182
+ def list_servers() -> list[dict]:
183
+ """List running culture/agentirc servers.
184
+
185
+ Returns list of dicts with keys: name, pid, port.
186
+ """
187
+ pid_dir = Path(PID_DIR)
188
+ if not pid_dir.exists():
189
+ return []
190
+ servers = []
191
+ prefix = "server-"
192
+ for pid_path in sorted(pid_dir.glob(f"{prefix}*.pid")):
193
+ pid_name = pid_path.stem
194
+ name = pid_name[len(prefix) :]
195
+ pid = read_pid(pid_name)
196
+ if pid is None or not is_process_alive(pid) or not is_managed_process(pid):
197
+ continue
198
+ port = read_port(pid_name) or 6667
199
+ servers.append({"name": name, "pid": pid, "port": port})
200
+ return servers
201
+
202
+
203
+ def read_default_server() -> str | None:
204
+ """Read the default server name. Returns None if unset."""
205
+ default_path = Path(PID_DIR) / "default_server"
206
+ if not default_path.exists():
207
+ return None
208
+ try:
209
+ return default_path.read_text().strip() or None
210
+ except OSError:
211
+ return None
212
+
213
+
214
+ def write_default_server(name: str) -> None:
215
+ """Set the default server name."""
216
+ pid_dir = Path(PID_DIR)
217
+ pid_dir.mkdir(parents=True, exist_ok=True)
218
+ (pid_dir / "default_server").write_text(name)
219
+
220
+
221
+ def rename_pid(old_name: str, new_name: str) -> bool:
222
+ """Rename a PID file and its associated port file.
223
+
224
+ Best-effort: returns True if at least one file was renamed.
225
+ Failures are silently ignored to avoid raising during cleanup paths.
226
+ """
227
+ pid_dir = Path(PID_DIR)
228
+ renamed = False
229
+ for suffix in (".pid", ".port"):
230
+ old_path = pid_dir / f"{_safe_name(old_name)}{suffix}"
231
+ new_path = pid_dir / f"{_safe_name(new_name)}{suffix}"
232
+ if old_path.exists():
233
+ try:
234
+ old_path.rename(new_path)
235
+ renamed = True
236
+ except OSError:
237
+ pass
238
+ return renamed