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 +123 -0
- agent_bus/__init__.py +5 -0
- agent_bus/__main__.py +9 -0
- agent_bus/adapters/__init__.py +17 -0
- agent_bus/adapters/claude.py +66 -0
- agent_bus/adapters/codex.py +48 -0
- agent_bus/adapters/grok.py +48 -0
- agent_bus/adapters/omp.py +69 -0
- agent_bus/cli.py +276 -0
- agent_bus/protocol.py +119 -0
- agent_bus/skills/SKILL.md +103 -0
- agent_bus/skills/references/UDS-protocol.md +208 -0
- agent_bus/store.py +444 -0
- agent_bus/uds.py +480 -0
- agent_bus_team-0.1.2.dist-info/METADATA +136 -0
- agent_bus_team-0.1.2.dist-info/RECORD +19 -0
- agent_bus_team-0.1.2.dist-info/WHEEL +4 -0
- agent_bus_team-0.1.2.dist-info/entry_points.txt +2 -0
- agent_bus_team-0.1.2.dist-info/licenses/LICENSE +7 -0
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
agent_bus/__main__.py
ADDED
|
@@ -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())
|