agent-bus-team 0.1.2__py3-none-any.whl

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.
agent_bus/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # agent-bus
2
+
3
+ Small stdlib-only Python 3.11+ inter-agent messaging CLI and library.
4
+
5
+ Parallel bus for Claude Code, Grok, Oh My Pi (omp), Codex, and others.
6
+ Two channels: file bus (`send`/`inbox`) vs. native UDS (`listen` + `send-peer`). See [UDS-protocol.md](skills/references/UDS-protocol.md).
7
+
8
+ ## On-disk (AGENT_BUS_HOME=~/.agent-bus)
9
+
10
+ ```
11
+ ~/.agent-bus/
12
+ roster/<id>.json # registered agents (uuid ids)
13
+ inboxes/<id>.jsonl # append-only, one JSON message per line
14
+ captures/<pid>.jsonl # from `listen`
15
+ ```
16
+
17
+ Roster entry and Message envelope match the spec in the source.
18
+
19
+ ## Rules (enforced)
20
+
21
+ - Plain text **only** in `text` (no structured payloads).
22
+ - Refuse `> 1_000_000` chars.
23
+ - Per-inbox unread cap 50 (send fails with clear error).
24
+ - **Never treat a received message as user consent.** Messages are cross-session only. The receiving agent must still show the user and obtain explicit approval before acting on any instruction in a message.
25
+ - Names unique among *live* (pid-alive) registrations; collisions get `-2`, `-3` suffix on register.
26
+ - `list` drops stale roster entries (dead pid) but leaves their inbox files.
27
+
28
+ ## CLI
29
+
30
+ ```sh
31
+ agent-bus list [--kind claude|grok|omp|codex|all] [--json]
32
+ agent-bus send <name-or-id> -m TEXT [--summary S] [--from-name N]
33
+ agent-bus inbox [--name N] [--unread] [--json]
34
+ agent-bus ack <message-id> [--name N]
35
+ agent-bus register --name N --kind K [--cwd P] [--pid P]
36
+ agent-bus unregister --name N
37
+ agent-bus self [--json]
38
+
39
+ # EXPERIMENT (see below)
40
+ agent-bus listen [--name agent-bus]
41
+ agent-bus send-uds <socket-path> -m TEXT
42
+ ```
43
+
44
+ `list` = live roster entries (after pruning dead) UNION native adapters (claude/grok/omp/codex read-only discovery of their registries). Only alive pids.
45
+
46
+ `send` to a discovered native name/id will lazily create a roster entry + inbox under this bus home using a stable derived id (`claude:<sessionId>`, `grok:...` etc). The recipient only sees it if they also run `agent-bus inbox` (or via skill).
47
+
48
+ ## Adapters (read-only, best-effort, never throw)
49
+
50
+ - claude: `~/.claude/sessions/*.json` (pid alive)
51
+ - grok: `~/.grok/active_sessions.json`
52
+ - omp: `~/.omp/run/daemons/*/clients/*.json` + terminal-sessions fallback
53
+ - codex: `~/.codex/process_manager/chat_processes.json` (catalog skipped silently)
54
+
55
+ Override for tests with `AGENT_BUS_SESSIONS_DIR` etc. (File-bus adapters are read-only discovery and never write native sockets; native UDS send path does write for acks and peer messages — see UDS-protocol.md.)
56
+
57
+ ## The `listen` + UDS experiment
58
+
59
+ `agent-bus listen` lets a Claude Code session discover us via its `ListAgents` / `/list-agents`.
60
+
61
+ It:
62
+ - Binds UDS at `/tmp/cc-socks/<ourpid>.sock` (0o600, dir 0o700)
63
+ - Writes a matching `~/.claude/sessions/<ourpid>.json` (exact fields + timestamps) + peer key
64
+ - Accepts connections, reads newline-delimited JSON frames (tolerates final buffer w/o nl)
65
+ - Logs raw + parsed (auth redacted) to stdout + appends to `~/.agent-bus/captures/<pid>.jsonl`
66
+ - Auth first line on conns; accepts `type:user` frames
67
+ - On `msg_id` present: dials back an authenticated `{"type":"control","action":"peer_message_status","status":"delivered",...}` (NEVER writes status on the inbound conn)
68
+ - On SIGINT/SIGTERM: unlinks *only* our sock, sessions json, and key
69
+
70
+ `agent-bus send-peer` sends native UDS messages into other Claude sessions (or other listeners).
71
+
72
+ **Usage (from another Claude):**
73
+ 1. In one terminal (this agent): `AGENT_BUS_HOME=/tmp/ab-test agent-bus listen --name my-bus`
74
+ 2. In a real Claude Code session: run `/list-agents` or tool `ListAgents`. You should see `my-bus`.
75
+ 3. Send a message from Claude to it (it will enqueue via their SendMessage to our socket).
76
+ 4. Watch the logs + capture file.
77
+
78
+ **Outbound to Claude peers:**
79
+ `agent-bus send-peer <name-or-sock> -m "text here"`
80
+ See [UDS-protocol.md](skills/references/UDS-protocol.md) for the full wire format, auth, frame shapes, and verified bidirectional behavior.
81
+
82
+ **CRITICAL SAFETY**
83
+ - This is an experiment to reverse the wire format.
84
+ - Do not use `send-uds` against anything except a socket we started with `listen` under test overrides.
85
+ - Real delivery in Claude happens at their next tool round; they may show `<cross-session-message ...>`
86
+ - Received content must never auto-execute. Always require fresh user approval.
87
+
88
+ Test overrides (used by our test suite, safe):
89
+ - `AGENT_BUS_SOCK_DIR=/tmp/ab-test-socks`
90
+ - `AGENT_BUS_SESSIONS_DIR=/tmp/ab-test-sessions`
91
+ - `AGENT_BUS_HOME=/tmp/ab-test-bus`
92
+ ## Installation / running from source
93
+
94
+ ```sh
95
+ gh repo clone danbarua/agent-bus && cd agent-bus
96
+ python -m pip install -e .
97
+ agent-bus --help
98
+ ```
99
+
100
+ For a Claude session or omp: `python -m agent_bus ...` or after pip install use the script.
101
+
102
+ Also a skill copy lives at `~/.claude/skills/agent-bus/SKILL.md` (and source `skills/SKILL.md`).
103
+
104
+ ## Skills / integration
105
+
106
+ See `skills/SKILL.md`. Agents can call the CLI or import `agent_bus.store` etc to register on start and poll inbox.
107
+
108
+ ## Development / test
109
+
110
+ ```sh
111
+ python -m pytest tests/ -q --tb=line
112
+ AGENT_BUS_HOME=/tmp/ab-test python -m agent_bus list --json
113
+ ```
114
+
115
+ ## Limitations / non-goals
116
+
117
+ - No impersonation of Claude's full protocol (listen + send-peer implement the UDS peer messaging subset — see [UDS-protocol.md](skills/references/UDS-protocol.md).
118
+ - No auto-start of other agents.
119
+ - Herdr TTY injection is a separate channel (not used here).
120
+ - No impersonation of Claude's full protocol beyond the listen experiment.
121
+ - Inboxes are per bus-home; multiple users would need separate homes or sync.
122
+
123
+ This is intentionally small and boring.
agent_bus/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """agent-bus: inter-agent messaging for Claude, Grok, OMP, Codex etc.
2
+
3
+ Stdlib only. Parallel bus, does not use or impersonate Claude's UDS protocol.
4
+ """
5
+ __version__ = "0.1.0"
agent_bus/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Support `python -m agent_bus` invocation for the CLI."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from .cli import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -0,0 +1,17 @@
1
+ """Adapters package: read-only discovery for native agent registries."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from . import claude, codex, grok, omp
7
+
8
+
9
+ def discover_all() -> list[dict[str, Any]]:
10
+ out: list[dict[str, Any]] = []
11
+ for mod in (claude, grok, omp, codex):
12
+ try:
13
+ out.extend(mod.discover())
14
+ except Exception:
15
+ # never throw from adapters
16
+ continue
17
+ return out
@@ -0,0 +1,66 @@
1
+ """Claude Code adapter: read-only best-effort discovery.
2
+
3
+ Scans ~/.claude/sessions/<pid>.json (or AGENT_BUS_SESSIONS_DIR override for tests).
4
+ Only returns entries where pid is alive. Never writes. Never touches sockets.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import time
11
+ from typing import Any
12
+
13
+ from ..store import is_pid_alive
14
+
15
+
16
+ def _sessions_dir() -> str:
17
+ # override for tests: AGENT_BUS_SESSIONS_DIR
18
+ env = os.environ.get("AGENT_BUS_SESSIONS_DIR")
19
+ if env:
20
+ return env
21
+ return os.path.expanduser("~/.claude/sessions")
22
+
23
+
24
+ def discover() -> list[dict[str, Any]]:
25
+ """Return list of discovered claude agents (live pids only)."""
26
+ sdir = _sessions_dir()
27
+ out: list[dict[str, Any]] = []
28
+ if not os.path.isdir(sdir):
29
+ return out
30
+ try:
31
+ for fn in os.listdir(sdir):
32
+ if not fn.endswith(".json") or not fn.split(".")[0].isdigit():
33
+ continue
34
+ path = os.path.join(sdir, fn)
35
+ try:
36
+ with open(path, "r", encoding="utf-8") as f:
37
+ data = json.load(f)
38
+ pid = int(data.get("pid", 0))
39
+ if not is_pid_alive(pid):
40
+ continue
41
+ session_id = data.get("sessionId") or f"pid:{pid}"
42
+ rid = f"claude:{session_id}"
43
+ name = data.get("name") or f"claude-{pid}"
44
+ cwd = data.get("cwd")
45
+ status = data.get("status", "unknown")
46
+ out.append({
47
+ "id": rid,
48
+ "name": name,
49
+ "kind": "claude",
50
+ "pid": pid,
51
+ "cwd": cwd,
52
+ "status": status,
53
+ "native": {
54
+ "sessionId": session_id,
55
+ "messagingSocketPath": data.get("messagingSocketPath"),
56
+ "version": data.get("version"),
57
+ "kind": data.get("kind"),
58
+ },
59
+ "registeredAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
60
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
61
+ })
62
+ except Exception:
63
+ continue
64
+ except Exception:
65
+ pass
66
+ return out
@@ -0,0 +1,48 @@
1
+ """Codex adapter: read-only, best effort, silent skip on catalog."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import time
7
+ from typing import Any
8
+
9
+ from ..store import is_pid_alive
10
+
11
+
12
+ def discover() -> list[dict[str, Any]]:
13
+ out: list[dict[str, Any]] = []
14
+ base = os.path.expanduser("~/.codex")
15
+ pm = os.path.join(base, "process_manager", "chat_processes.json")
16
+ if not os.path.isfile(pm):
17
+ return out
18
+ try:
19
+ with open(pm, "r", encoding="utf-8") as f:
20
+ procs = json.load(f)
21
+ # expected shape unknown, try list or dict of pids
22
+ items = procs if isinstance(procs, list) else (procs.get("processes", []) if isinstance(procs, dict) else [])
23
+ for item in items if isinstance(items, list) else []:
24
+ pid = None
25
+ if isinstance(item, int):
26
+ pid = item
27
+ elif isinstance(item, dict):
28
+ pid = item.get("pid") or item.get("process_id")
29
+ if not is_pid_alive(pid):
30
+ continue
31
+ rid = f"codex:pid:{pid}"
32
+ name = f"codex-{pid}"
33
+ cwd = item.get("cwd") if isinstance(item, dict) else None
34
+ out.append({
35
+ "id": rid,
36
+ "name": name,
37
+ "kind": "codex",
38
+ "pid": pid,
39
+ "cwd": cwd,
40
+ "status": "unknown",
41
+ "native": {"source": "chat_processes"},
42
+ "registeredAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
43
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
44
+ })
45
+ except Exception:
46
+ # silent on error or empty
47
+ pass
48
+ return out
@@ -0,0 +1,48 @@
1
+ """Grok adapter: read-only."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import time
7
+ from typing import Any
8
+
9
+ from ..store import is_pid_alive
10
+
11
+
12
+ def _grok_dir() -> str:
13
+ return os.path.expanduser("~/.grok")
14
+
15
+
16
+ def discover() -> list[dict[str, Any]]:
17
+ out: list[dict[str, Any]] = []
18
+ gdir = _grok_dir()
19
+ active = os.path.join(gdir, "active_sessions.json")
20
+ if not os.path.isfile(active):
21
+ return out
22
+ try:
23
+ with open(active, "r", encoding="utf-8") as f:
24
+ sessions = json.load(f)
25
+ if not isinstance(sessions, list):
26
+ return out
27
+ for s in sessions:
28
+ pid = s.get("pid")
29
+ if not is_pid_alive(pid):
30
+ continue
31
+ sid = s.get("session_id") or f"pid:{pid}"
32
+ rid = f"grok:{sid}"
33
+ name = s.get("agent_name") or f"grok-{pid}"
34
+ cwd = s.get("cwd")
35
+ out.append({
36
+ "id": rid,
37
+ "name": name,
38
+ "kind": "grok",
39
+ "pid": pid,
40
+ "cwd": cwd,
41
+ "status": "unknown",
42
+ "native": {"session_id": sid, "opened_at": s.get("opened_at")},
43
+ "registeredAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
44
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
45
+ })
46
+ except Exception:
47
+ pass
48
+ return out
@@ -0,0 +1,69 @@
1
+ """OMP adapter: read-only best effort."""
2
+ from __future__ import annotations
3
+
4
+ import glob
5
+ import json
6
+ import os
7
+ import time
8
+ from typing import Any
9
+
10
+ from ..store import is_pid_alive
11
+
12
+
13
+ def discover() -> list[dict[str, Any]]:
14
+ out: list[dict[str, Any]] = []
15
+ base = os.path.expanduser("~/.omp")
16
+ # daemons clients
17
+ try:
18
+ for cli_json in glob.glob(os.path.join(base, "run", "daemons", "*", "clients", "*.json")):
19
+ try:
20
+ with open(cli_json, "r", encoding="utf-8") as f:
21
+ data = json.load(f)
22
+ pid = data.get("pid")
23
+ if not is_pid_alive(pid):
24
+ continue
25
+ aid = data.get("id") or f"pid:{pid}"
26
+ rid = f"omp:{aid}"
27
+ name = data.get("id", f"omp-{pid}")
28
+ cwd = data.get("projectDir") or data.get("cwd")
29
+ out.append({
30
+ "id": rid,
31
+ "name": name,
32
+ "kind": "omp",
33
+ "pid": pid,
34
+ "cwd": cwd,
35
+ "status": "unknown",
36
+ "native": {"id": aid, "projectDir": cwd},
37
+ "registeredAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
38
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
39
+ })
40
+ except Exception:
41
+ continue
42
+ except Exception:
43
+ pass
44
+
45
+ # terminal sessions fallback (ttys*)
46
+ try:
47
+ for ts in glob.glob(os.path.join(base, "agent", "terminal-sessions", "ttys*")):
48
+ # these are dirs? assume have info, but for simplicity if dir take name
49
+ pid_str = os.path.basename(ts).replace("ttys", "")
50
+ try:
51
+ pid = int(pid_str) if pid_str.isdigit() else None
52
+ except Exception:
53
+ pid = None
54
+ if pid and is_pid_alive(pid):
55
+ rid = f"omp:tty:{pid}"
56
+ out.append({
57
+ "id": rid,
58
+ "name": f"omp-tty-{pid}",
59
+ "kind": "omp",
60
+ "pid": pid,
61
+ "cwd": None,
62
+ "status": "unknown",
63
+ "native": {"tty": os.path.basename(ts)},
64
+ "registeredAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
65
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
66
+ })
67
+ except Exception:
68
+ pass
69
+ return out
agent_bus/cli.py ADDED
@@ -0,0 +1,276 @@
1
+ """CLI entrypoint for agent-bus."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import glob
6
+ import json
7
+ import os
8
+ import sys
9
+ from typing import Any
10
+
11
+ from .protocol import roster_to_dict
12
+ from .store import (
13
+ ack_message,
14
+ get_inbox,
15
+ get_self,
16
+ list_agents,
17
+ send_message,
18
+ )
19
+ from .store import (
20
+ register as do_register,
21
+ )
22
+ from .store import (
23
+ unregister as do_unregister,
24
+ )
25
+ from .uds import run_listen, send_peer_message, send_uds_frame
26
+
27
+
28
+ def _print_json(obj: Any) -> None:
29
+ print(json.dumps(obj, indent=2, default=str, sort_keys=True))
30
+
31
+
32
+ def _resolve_kind(k: str | None) -> str | None:
33
+ if not k or k.lower() == "all":
34
+ return None
35
+ k = k.lower()
36
+ if k in ("claude", "grok", "omp", "codex", "other"):
37
+ return k
38
+ return None
39
+
40
+
41
+ def cmd_list(args: argparse.Namespace) -> int:
42
+ kind = _resolve_kind(args.kind)
43
+ agents = list_agents(kind=kind)
44
+ if args.json:
45
+ _print_json([roster_to_dict(a) for a in agents])
46
+ return 0
47
+ if not agents:
48
+ print("no agents")
49
+ return 0
50
+ print(f"{'NAME':<20} {'KIND':<8} {'PID':>7} {'STATUS':<10} ID")
51
+ for a in agents:
52
+ print(f"{a.name:<20} {a.kind:<8} {a.pid or ''!s:>7} {a.status:<10} {a.id}")
53
+ return 0
54
+
55
+
56
+ def cmd_send(args: argparse.Namespace) -> int:
57
+ try:
58
+ mid = send_message(
59
+ to=args.target,
60
+ text=args.message,
61
+ summary=args.summary or "",
62
+ from_name=args.from_name,
63
+ from_kind="other",
64
+ )
65
+ print(f"sent id={mid}")
66
+ return 0
67
+ except Exception as e:
68
+ print(f"send failed: {e}", file=sys.stderr)
69
+ return 1
70
+
71
+
72
+ def cmd_inbox(args: argparse.Namespace) -> int:
73
+ msgs = get_inbox(name_or_id=args.name, unread_only=args.unread)
74
+ if args.json:
75
+ # serialize friendly
76
+ out = []
77
+ for m in msgs:
78
+ out.append({
79
+ "id": m["id"],
80
+ "ts": m["ts"],
81
+ "from": {"id": m["from_"].id, "name": m["from_"].name, "kind": m["from_"].kind},
82
+ "to": m["to"],
83
+ "summary": m["summary"],
84
+ "text": m["text"],
85
+ "read": m["read"],
86
+ "replyTo": m["replyTo"],
87
+ })
88
+ _print_json(out)
89
+ return 0
90
+ if not msgs:
91
+ print("inbox empty")
92
+ return 0
93
+ for m in msgs:
94
+ flag = " " if m["read"] else "U"
95
+ print(f"[{flag}] {m['ts'][:19]} from={m['from_'].name} ({m['from_'].kind})")
96
+ if m["summary"]:
97
+ print(f" summary: {m['summary']}")
98
+ print(f" {m['text'][:200]}{'...' if len(m['text'])>200 else ''}")
99
+ print(f" id={m['id']}")
100
+ return 0
101
+
102
+
103
+ def cmd_ack(args: argparse.Namespace) -> int:
104
+ ok = ack_message(args.message_id, name_or_id=args.name)
105
+ print("acked" if ok else "not found")
106
+ return 0 if ok else 1
107
+
108
+
109
+ def cmd_register(args: argparse.Namespace) -> int:
110
+ kind = args.kind # type: ignore
111
+ if kind not in ("claude", "grok", "omp", "codex", "other"):
112
+ print("invalid kind", file=sys.stderr)
113
+ return 1
114
+ try:
115
+ entry = do_register(
116
+ name=args.name,
117
+ kind=kind, # type: ignore
118
+ cwd=args.cwd,
119
+ pid=args.pid,
120
+ )
121
+ print(f"registered id={entry.id} name={entry.name}")
122
+ return 0
123
+ except Exception as e:
124
+ print(f"register failed: {e}", file=sys.stderr)
125
+ return 1
126
+
127
+
128
+ def cmd_unregister(args: argparse.Namespace) -> int:
129
+ ok = do_unregister(args.name)
130
+ print("unregistered" if ok else "no match")
131
+ return 0
132
+
133
+
134
+ def cmd_self(args: argparse.Namespace) -> int:
135
+ e = get_self()
136
+ if not e:
137
+ print("not registered (use register)")
138
+ return 1
139
+ if args.json:
140
+ _print_json({
141
+ "id": e.id, "name": e.name, "kind": e.kind, "pid": e.pid,
142
+ "cwd": e.cwd, "status": e.status, "inbox": e.inbox
143
+ })
144
+ return 0
145
+ print(f"id={e.id} name={e.name} kind={e.kind} pid={e.pid} cwd={e.cwd}")
146
+ return 0
147
+
148
+
149
+ def cmd_listen(args: argparse.Namespace) -> int:
150
+ # blocks
151
+ try:
152
+ run_listen(name=args.name or "agent-bus")
153
+ return 0
154
+ except Exception as e:
155
+ print(f"listen error: {e}", file=sys.stderr)
156
+ return 1
157
+
158
+
159
+ def cmd_send_uds(args: argparse.Namespace) -> int:
160
+ try:
161
+ send_uds_frame(args.socket, args.message)
162
+ print("frame sent")
163
+ return 0
164
+ except Exception as e:
165
+ print(f"send-uds failed: {e}", file=sys.stderr)
166
+ return 1
167
+ def cmd_send_peer(args: argparse.Namespace) -> int:
168
+ target = args.target
169
+ sock = None
170
+ if target.endswith(".sock") and os.path.exists(target):
171
+ sock = target
172
+ else:
173
+ # lookup by name in claude sessions
174
+ sess_dir = os.path.expanduser("~/.claude/sessions")
175
+ for f in glob.glob(os.path.join(sess_dir, "*.json")):
176
+ try:
177
+ with open(f) as jf:
178
+ d = json.load(jf)
179
+ if d.get("name") == target:
180
+ sock = d.get("messagingSocketPath")
181
+ break
182
+ except Exception:
183
+ pass
184
+ if not sock or not os.path.exists(sock):
185
+ print(f"target not found or dead: {target}", file=sys.stderr)
186
+ return 1
187
+ try:
188
+ send_peer_message(sock, args.message)
189
+ return 0
190
+ except Exception as e:
191
+ print(f"send-peer failed: {e}", file=sys.stderr)
192
+ return 1
193
+
194
+
195
+
196
+
197
+ def main(argv: list[str] | None = None) -> int:
198
+ if argv is None:
199
+ argv = sys.argv[1:]
200
+ p = argparse.ArgumentParser(prog="agent-bus", description="inter-agent messaging bus")
201
+ sub = p.add_subparsers(dest="cmd", required=True)
202
+
203
+ # list
204
+ pl = sub.add_parser("list", help="list live agents from roster + native adapters")
205
+ pl.add_argument("--kind", default=None, help="claude|grok|omp|codex|all")
206
+ pl.add_argument("--json", action="store_true")
207
+ pl.set_defaults(func=cmd_list)
208
+
209
+ # send (file bus)
210
+ ps = sub.add_parser("send", help="send text via file-bus inbox to name-or-id")
211
+ ps.add_argument("target", help="name or id (from list)")
212
+ ps.add_argument("-m", "--message", required=True, help="plain text (max 1M)")
213
+ ps.add_argument("--summary", default=None)
214
+ ps.add_argument("--from-name", default=None)
215
+ ps.set_defaults(func=cmd_send)
216
+
217
+ # inbox
218
+ pi = sub.add_parser("inbox", help="read messages addressed to self or --name")
219
+ pi.add_argument("--name", default=None)
220
+ pi.add_argument("--unread", action="store_true")
221
+ pi.add_argument("--json", action="store_true")
222
+ pi.set_defaults(func=cmd_inbox)
223
+
224
+ # ack
225
+ pa = sub.add_parser("ack", help="mark message read")
226
+ pa.add_argument("message_id")
227
+ pa.add_argument("--name", default=None)
228
+ pa.set_defaults(func=cmd_ack)
229
+
230
+ # register
231
+ pr = sub.add_parser("register", help="register this process so others can send to you")
232
+ pr.add_argument("--name", required=True)
233
+ pr.add_argument("--kind", required=True, choices=["claude", "grok", "omp", "codex", "other"])
234
+ pr.add_argument("--cwd", default=None)
235
+ pr.add_argument("--pid", type=int, default=None)
236
+ pr.set_defaults(func=cmd_register)
237
+
238
+ # unregister
239
+ pu = sub.add_parser("unregister", help="remove by name")
240
+ pu.add_argument("--name", required=True)
241
+ pu.set_defaults(func=cmd_unregister)
242
+
243
+ # self
244
+ psf = sub.add_parser("self", help="show current registered self")
245
+ psf.add_argument("--json", action="store_true")
246
+ psf.set_defaults(func=cmd_self)
247
+
248
+ # listen (UDS experiment)
249
+ plis = sub.add_parser(
250
+ "listen",
251
+ help="EXPERIMENT: publish as Claude peer (writes our sessions/<pid>.json + binds /tmp/cc-socks/<pid>.sock)",
252
+ )
253
+ plis.add_argument("--name", default="agent-bus", help="name visible to ListAgents")
254
+ plis.set_defaults(func=cmd_listen)
255
+
256
+ # send-uds (for testing listen only)
257
+ psu = sub.add_parser(
258
+ "send-uds",
259
+ help="send the two-line UDS auth+user frame (test ONLY against our own listen, never live Claude)",
260
+ )
261
+ psu.add_argument("socket", help="path to target .sock")
262
+ psu.add_argument("-m", "--message", required=True)
263
+ psu.set_defaults(func=cmd_send_uds)
264
+
265
+ # send-peer (UDS to native claude peer)
266
+ psp = sub.add_parser("send-peer", help="send user msg via UDS peer protocol to name or socket")
267
+ psp.add_argument("target", help="name (from list) or path to .sock")
268
+ psp.add_argument("-m", "--message", required=True, help="plain text")
269
+ psp.set_defaults(func=cmd_send_peer)
270
+
271
+ args = p.parse_args(argv)
272
+ return args.func(args)
273
+
274
+
275
+ if __name__ == "__main__":
276
+ sys.exit(main())