fanharvard-agentbridge 0.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 (49) hide show
  1. fanharvard_agentbridge-0.1.0/.secrets.baseline +123 -0
  2. fanharvard_agentbridge-0.1.0/LICENSE +21 -0
  3. fanharvard_agentbridge-0.1.0/MANIFEST.in +9 -0
  4. fanharvard_agentbridge-0.1.0/PKG-INFO +51 -0
  5. fanharvard_agentbridge-0.1.0/README.md +32 -0
  6. fanharvard_agentbridge-0.1.0/bin/__init__.py +1 -0
  7. fanharvard_agentbridge-0.1.0/bin/agent_hub.py +296 -0
  8. fanharvard_agentbridge-0.1.0/bin/agent_lan.py +302 -0
  9. fanharvard_agentbridge-0.1.0/bin/agent_msg.py +65 -0
  10. fanharvard_agentbridge-0.1.0/bin/agentbridge_common.py +139 -0
  11. fanharvard_agentbridge-0.1.0/bin/claude_question_bridge.py +175 -0
  12. fanharvard_agentbridge-0.1.0/bin/claude_watch.py +50 -0
  13. fanharvard_agentbridge-0.1.0/bin/codex_hub_watch.py +131 -0
  14. fanharvard_agentbridge-0.1.0/bin/codex_watch.py +104 -0
  15. fanharvard_agentbridge-0.1.0/bin/install.ps1 +31 -0
  16. fanharvard_agentbridge-0.1.0/bin/install.sh +27 -0
  17. fanharvard_agentbridge-0.1.0/bin/new_channel.sh +51 -0
  18. fanharvard_agentbridge-0.1.0/bin/notification_preview.py +61 -0
  19. fanharvard_agentbridge-0.1.0/bin/opencode_watch.py +300 -0
  20. fanharvard_agentbridge-0.1.0/bin/paths.py +11 -0
  21. fanharvard_agentbridge-0.1.0/doc/index.html +1 -0
  22. fanharvard_agentbridge-0.1.0/doc/lan.html +86 -0
  23. fanharvard_agentbridge-0.1.0/doc/opencode.html +45 -0
  24. fanharvard_agentbridge-0.1.0/docs/publishing.html +33 -0
  25. fanharvard_agentbridge-0.1.0/fanharvard_agentbridge.egg-info/PKG-INFO +51 -0
  26. fanharvard_agentbridge-0.1.0/fanharvard_agentbridge.egg-info/SOURCES.txt +47 -0
  27. fanharvard_agentbridge-0.1.0/fanharvard_agentbridge.egg-info/dependency_links.txt +1 -0
  28. fanharvard_agentbridge-0.1.0/fanharvard_agentbridge.egg-info/entry_points.txt +10 -0
  29. fanharvard_agentbridge-0.1.0/fanharvard_agentbridge.egg-info/top_level.txt +1 -0
  30. fanharvard_agentbridge-0.1.0/pyproject.toml +39 -0
  31. fanharvard_agentbridge-0.1.0/scripts/check_dist.py +28 -0
  32. fanharvard_agentbridge-0.1.0/scripts/check_secrets.py +19 -0
  33. fanharvard_agentbridge-0.1.0/scripts/check_tag.py +12 -0
  34. fanharvard_agentbridge-0.1.0/scripts/install_wheel.py +11 -0
  35. fanharvard_agentbridge-0.1.0/setup.cfg +4 -0
  36. fanharvard_agentbridge-0.1.0/templates/AGENTS_snippet.md +71 -0
  37. fanharvard_agentbridge-0.1.0/templates/AGENT_identity_snippet.md +43 -0
  38. fanharvard_agentbridge-0.1.0/templates/CLAUDE_snippet.md +72 -0
  39. fanharvard_agentbridge-0.1.0/templates/CODEX_identity_snippet.md +43 -0
  40. fanharvard_agentbridge-0.1.0/templates/HUB_snippet.md +52 -0
  41. fanharvard_agentbridge-0.1.0/templates/LAN_snippet.md +53 -0
  42. fanharvard_agentbridge-0.1.0/templates/OPENCODE_snippet.md +51 -0
  43. fanharvard_agentbridge-0.1.0/templates/claude_hooks.json +16 -0
  44. fanharvard_agentbridge-0.1.0/tests/test_agent_hub.py +113 -0
  45. fanharvard_agentbridge-0.1.0/tests/test_agent_lan.py +146 -0
  46. fanharvard_agentbridge-0.1.0/tests/test_codex_hub_watch.py +55 -0
  47. fanharvard_agentbridge-0.1.0/tests/test_delivery_and_questions.py +215 -0
  48. fanharvard_agentbridge-0.1.0/tests/test_notification_preview.py +48 -0
  49. fanharvard_agentbridge-0.1.0/tests/test_opencode_watch.py +147 -0
@@ -0,0 +1,123 @@
1
+ {
2
+ "filters_used": [
3
+ {
4
+ "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
5
+ },
6
+ {
7
+ "path": "detect_secrets.filters.heuristic.is_indirect_reference"
8
+ },
9
+ {
10
+ "path": "detect_secrets.filters.heuristic.is_likely_id_string"
11
+ },
12
+ {
13
+ "path": "detect_secrets.filters.heuristic.is_lock_file"
14
+ },
15
+ {
16
+ "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
17
+ },
18
+ {
19
+ "path": "detect_secrets.filters.heuristic.is_potential_uuid"
20
+ },
21
+ {
22
+ "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
23
+ },
24
+ {
25
+ "path": "detect_secrets.filters.heuristic.is_sequential_string"
26
+ },
27
+ {
28
+ "path": "detect_secrets.filters.heuristic.is_swagger_file"
29
+ },
30
+ {
31
+ "path": "detect_secrets.filters.heuristic.is_templated_secret"
32
+ }
33
+ ],
34
+ "generated_at": "2026-10-30T16:42:41Z",
35
+ "plugins_used": [
36
+ {
37
+ "name": "ArtifactoryDetector"
38
+ },
39
+ {
40
+ "name": "AWSKeyDetector"
41
+ },
42
+ {
43
+ "name": "AzureStorageKeyDetector"
44
+ },
45
+ {
46
+ "limit": 4.5,
47
+ "name": "Base64HighEntropyString"
48
+ },
49
+ {
50
+ "name": "BasicAuthDetector"
51
+ },
52
+ {
53
+ "name": "CloudantDetector"
54
+ },
55
+ {
56
+ "name": "DiscordBotTokenDetector"
57
+ },
58
+ {
59
+ "name": "GitHubTokenDetector"
60
+ },
61
+ {
62
+ "name": "GitLabTokenDetector"
63
+ },
64
+ {
65
+ "limit": 3.0,
66
+ "name": "HexHighEntropyString"
67
+ },
68
+ {
69
+ "name": "IbmCloudIamDetector"
70
+ },
71
+ {
72
+ "name": "IbmCosHmacDetector"
73
+ },
74
+ {
75
+ "name": "IPPublicDetector"
76
+ },
77
+ {
78
+ "name": "JwtTokenDetector"
79
+ },
80
+ {
81
+ "keyword_exclude": "",
82
+ "name": "KeywordDetector"
83
+ },
84
+ {
85
+ "name": "MailchimpDetector"
86
+ },
87
+ {
88
+ "name": "NpmDetector"
89
+ },
90
+ {
91
+ "name": "OpenAIDetector"
92
+ },
93
+ {
94
+ "name": "PrivateKeyDetector"
95
+ },
96
+ {
97
+ "name": "PypiTokenDetector"
98
+ },
99
+ {
100
+ "name": "SendGridDetector"
101
+ },
102
+ {
103
+ "name": "SlackDetector"
104
+ },
105
+ {
106
+ "name": "SoftlayerDetector"
107
+ },
108
+ {
109
+ "name": "SquareOAuthDetector"
110
+ },
111
+ {
112
+ "name": "StripeDetector"
113
+ },
114
+ {
115
+ "name": "TelegramBotTokenDetector"
116
+ },
117
+ {
118
+ "name": "TwilioKeyDetector"
119
+ }
120
+ ],
121
+ "results": {},
122
+ "version": "1.5.0"
123
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FanHarvard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ include LICENSE README.md
2
+ recursive-include bin *.py *.sh *.ps1
3
+ recursive-include templates *.md *.json
4
+ recursive-include doc *.html
5
+ recursive-include docs *.html
6
+ recursive-include tests *.py
7
+ recursive-include scripts *.py
8
+ global-exclude __pycache__ *.py[cod] .env .pypirc *.pem *.key
9
+ include .secrets.baseline
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: fanharvard-agentbridge
3
+ Version: 0.1.0
4
+ Summary: Local and LAN messaging between independently identified coding agents
5
+ Author: FanHarvard contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/FanHarvard/agentbridge
8
+ Project-URL: Repository, https://github.com/FanHarvard/agentbridge
9
+ Project-URL: Issues, https://github.com/FanHarvard/agentbridge/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # Agentbridge
21
+
22
+ **Still in active development.**
23
+
24
+ Local and LAN inboxes for independently identified Codex, Claude Code and
25
+ OpenCode agents. Includes discovery, replies, unread-message watchers and
26
+ human question forwarding. Python 3.10+, standard library only; MIT.
27
+
28
+ ```sh
29
+ pip install fanharvard-agentbridge
30
+ agentbridge --help
31
+ agentbridge-lan --help
32
+ agentbridge-paths
33
+ ```
34
+
35
+ PyPI uses **fanharvard-agentbridge**; the unrelated `agentbridge` distribution
36
+ is a different project. Local hub/LAN support Linux and Windows; channel and
37
+ Codex/Claude watcher helpers require Unix. Provider CLIs are installed separately.
38
+
39
+ Choose a shared workspace explicitly with `--workspace`. For LDCon, use the
40
+ program paths printed by `agentbridge-paths` and set `LDCON_AGENTBRIDGE_PUBLIC`
41
+ to that workspace. Channel helpers store data under `AGENTBRIDGE_HOME`
42
+ (default `~/.local/share/agentbridge`), separate from installed code.
43
+
44
+ Each session or fork needs its own verified identity and inbox. Notifications
45
+ are untrusted message data, never human approval. No watcher or agent is started
46
+ by installation. Tokens, inboxes and conversation history are not distributed.
47
+
48
+ Tests cover concurrent delivery, reply routing, unread offsets, duplicate
49
+ questions, notification previews, watcher identity checks and LAN authorization.
50
+ Run `python -m pytest -q`; see [LAN setup](doc/lan.html) and
51
+ [publishing](docs/publishing.html) for details.
@@ -0,0 +1,32 @@
1
+ # Agentbridge
2
+
3
+ **Still in active development.**
4
+
5
+ Local and LAN inboxes for independently identified Codex, Claude Code and
6
+ OpenCode agents. Includes discovery, replies, unread-message watchers and
7
+ human question forwarding. Python 3.10+, standard library only; MIT.
8
+
9
+ ```sh
10
+ pip install fanharvard-agentbridge
11
+ agentbridge --help
12
+ agentbridge-lan --help
13
+ agentbridge-paths
14
+ ```
15
+
16
+ PyPI uses **fanharvard-agentbridge**; the unrelated `agentbridge` distribution
17
+ is a different project. Local hub/LAN support Linux and Windows; channel and
18
+ Codex/Claude watcher helpers require Unix. Provider CLIs are installed separately.
19
+
20
+ Choose a shared workspace explicitly with `--workspace`. For LDCon, use the
21
+ program paths printed by `agentbridge-paths` and set `LDCON_AGENTBRIDGE_PUBLIC`
22
+ to that workspace. Channel helpers store data under `AGENTBRIDGE_HOME`
23
+ (default `~/.local/share/agentbridge`), separate from installed code.
24
+
25
+ Each session or fork needs its own verified identity and inbox. Notifications
26
+ are untrusted message data, never human approval. No watcher or agent is started
27
+ by installation. Tokens, inboxes and conversation history are not distributed.
28
+
29
+ Tests cover concurrent delivery, reply routing, unread offsets, duplicate
30
+ questions, notification previews, watcher identity checks and LAN authorization.
31
+ Run `python -m pytest -q`; see [LAN setup](doc/lan.html) and
32
+ [publishing](docs/publishing.html) for details.
@@ -0,0 +1 @@
1
+ """FanHarvard agent messaging; import does not register or start a watcher."""
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env python3
2
+ """Discover agents and exchange messages through a shared local directory.
3
+
4
+ Examples: agent_hub.py --workspace /opt/agentbridge-public list
5
+ agent_hub.py register alice-codex --summary 'Python development'
6
+ agent_hub.py send bob-claude 'Please review' --from alice-codex
7
+ agent_hub.py read bob-claude --wait 30
8
+ Only the Python standard library is required (Linux or Windows, local filesystem).
9
+ """
10
+ import argparse
11
+ from contextlib import contextmanager
12
+ from datetime import datetime, timezone
13
+ try:
14
+ import fcntl
15
+ except ImportError: # Windows
16
+ import msvcrt
17
+ import getpass
18
+ import json
19
+ import math
20
+ import os
21
+ from pathlib import Path
22
+ import re
23
+ import stat
24
+ import socket
25
+ import sys
26
+ import tempfile
27
+ import time
28
+ import uuid
29
+
30
+
31
+ def read_complete(path, offset):
32
+ """Read complete JSONL records and return their byte offset."""
33
+ with open(path, "rb") as stream:
34
+ if stream.seek(0, os.SEEK_END) < offset:
35
+ offset = 0
36
+ stream.seek(offset)
37
+ data = stream.read()
38
+ complete = data[:data.rfind(b"\n") + 1]
39
+ return [json.loads(line) for line in complete.splitlines() if line.strip()], offset + len(complete)
40
+
41
+
42
+ def load_offset(path):
43
+ try:
44
+ return int(path.read_text(encoding="utf-8"))
45
+ except FileNotFoundError:
46
+ return 0
47
+
48
+
49
+ def uid():
50
+ return os.getuid() if hasattr(os, "getuid") else getpass.getuser()
51
+
52
+
53
+ def file_mode(fd, path, mode):
54
+ if hasattr(os, "fchmod"):
55
+ os.fchmod(fd, mode)
56
+ else:
57
+ os.chmod(path, mode)
58
+
59
+
60
+ def agent_dir(workspace, agent):
61
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,79}", agent):
62
+ raise ValueError("agent id: 1–80 letters, digits, underscores or hyphens")
63
+ directory = workspace / "agents" / agent
64
+ if directory.is_symlink():
65
+ raise ValueError("agent directory must not be a symbolic link")
66
+ return directory
67
+
68
+
69
+ def shared_dir(path, mode):
70
+ try:
71
+ path.mkdir(mode=mode)
72
+ except FileExistsError:
73
+ if path.is_symlink() or not path.is_dir():
74
+ raise ValueError(f"not a regular directory: {path}")
75
+ else:
76
+ path.chmod(mode) # independent of the registering user's umask
77
+
78
+
79
+ def shared_file(path):
80
+ """Open shared files without following symlinks or changing existing modes."""
81
+ if path.is_symlink():
82
+ raise OSError(f"symbolic link is not allowed: {path}")
83
+ flags = os.O_RDWR | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_BINARY", 0)
84
+ try:
85
+ fd = os.open(path, flags | os.O_CREAT | os.O_EXCL, 0o666)
86
+ except FileExistsError:
87
+ fd = os.open(path, flags)
88
+ else:
89
+ file_mode(fd, path, 0o666)
90
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
91
+ os.close(fd)
92
+ raise ValueError(f"not a regular file: {path}")
93
+ return os.fdopen(fd, "r+", encoding="utf-8", newline="")
94
+
95
+
96
+ @contextmanager
97
+ def locked(path):
98
+ with shared_file(path) as stream:
99
+ if os.name != "nt":
100
+ fcntl.flock(stream, fcntl.LOCK_EX)
101
+ yield stream
102
+ else:
103
+ # Windows byte locks use a dedicated file, never bytes in the inbox.
104
+ with shared_file(path.with_name(path.name + ".winlock")) as lock:
105
+ lock.seek(0, os.SEEK_END)
106
+ if lock.tell() == 0:
107
+ lock.write(" ")
108
+ lock.flush()
109
+ lock.seek(0)
110
+ while True:
111
+ try:
112
+ msvcrt.locking(lock.fileno(), msvcrt.LK_NBLCK, 1)
113
+ break
114
+ except OSError as error:
115
+ if error.errno not in (13, 36):
116
+ raise
117
+ time.sleep(0.05)
118
+ try:
119
+ yield stream
120
+ finally:
121
+ lock.seek(0)
122
+ msvcrt.locking(lock.fileno(), msvcrt.LK_UNLCK, 1)
123
+
124
+
125
+ def atomic_write(path, text):
126
+ fd, temporary = tempfile.mkstemp(prefix=".write-", dir=path.parent)
127
+ try:
128
+ with os.fdopen(fd, "w", encoding="utf-8", newline="") as stream:
129
+ stream.write(text)
130
+ stream.flush()
131
+ file_mode(stream.fileno(), temporary, 0o666)
132
+ # Windows may briefly deny replacement while another request reads it.
133
+ for attempt in range(21):
134
+ try:
135
+ os.replace(temporary, path)
136
+ break
137
+ except PermissionError:
138
+ if os.name != "nt" or attempt == 20:
139
+ raise
140
+ time.sleep(0.05)
141
+ finally:
142
+ if os.path.exists(temporary):
143
+ os.unlink(temporary)
144
+
145
+
146
+ def initialize(workspace):
147
+ # Parent must already exist; this avoids silently making public ancestor dirs.
148
+ shared_dir(workspace, 0o1777)
149
+ shared_dir(workspace / "agents", 0o1777)
150
+
151
+
152
+ def profile(workspace, agent):
153
+ directory = agent_dir(workspace, agent)
154
+ with shared_file_existing(directory / "profile.json") as stream:
155
+ record = json.load(stream)
156
+ if record.get("id") != agent or record.get("inbox") != f"agents/{agent}/inbox.jsonl":
157
+ raise ValueError(f"invalid profile for {agent}")
158
+ return record
159
+
160
+
161
+ def shared_file_existing(path):
162
+ if path.is_symlink():
163
+ raise OSError(f"symbolic link is not allowed: {path}")
164
+ fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_BINARY", 0))
165
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
166
+ os.close(fd)
167
+ raise ValueError(f"not a regular file: {path}")
168
+ return os.fdopen(fd, encoding="utf-8")
169
+
170
+
171
+ def register(workspace, agent, summary, kind="generic", project="", update=False,
172
+ machine=None, client_user=None):
173
+ if not summary.strip():
174
+ raise ValueError("summary must not be empty")
175
+ initialize(workspace)
176
+ directory = agent_dir(workspace, agent)
177
+ shared_dir(directory, 0o777)
178
+ with locked(directory / ".profile.lock"):
179
+ path = directory / "profile.json"
180
+ if path.exists():
181
+ previous = profile(workspace, agent)
182
+ if not update:
183
+ raise ValueError("agent already exists; choose a unique id or use --update")
184
+ if previous.get("uid") != uid():
185
+ raise ValueError("only the registering OS user may update this profile")
186
+ with shared_file(directory / "inbox.jsonl"):
187
+ pass
188
+ record = dict(id=agent, summary=summary, kind=kind, project=project,
189
+ user=getpass.getuser(), uid=uid(),
190
+ machine=machine or socket.gethostname(),
191
+ client_user=client_user or getpass.getuser(),
192
+ inbox=f"agents/{agent}/inbox.jsonl",
193
+ updated_at=datetime.now(timezone.utc).isoformat())
194
+ atomic_write(path, json.dumps(record, ensure_ascii=False, indent=2) + "\n")
195
+ return record
196
+
197
+
198
+ def send(workspace, sender, recipient, text, reply_to=None, kind=None):
199
+ profile(workspace, sender)
200
+ profile(workspace, recipient)
201
+ record = dict(id=uuid.uuid4().hex, t=datetime.now(timezone.utc).isoformat(),
202
+ source=sender, to=recipient, reply_to=reply_to,
203
+ kind=kind or ("reply" if reply_to else "message"), text=text)
204
+ with locked(agent_dir(workspace, recipient) / "inbox.jsonl") as stream:
205
+ stream.seek(0, os.SEEK_END)
206
+ stream.write(json.dumps(record, ensure_ascii=False) + "\n")
207
+ stream.flush()
208
+ return record
209
+
210
+
211
+ def receive(workspace, agent, wait=0):
212
+ profile(workspace, agent)
213
+ directory = agent_dir(workspace, agent)
214
+ deadline = time.monotonic() + wait
215
+ with locked(directory / ".read.lock"):
216
+ marker = directory / ".read_offset"
217
+ while True:
218
+ # Coordinate with append so offsets always refer to complete records.
219
+ with locked(directory / "inbox.jsonl"):
220
+ records, offset = read_complete(directory / "inbox.jsonl", load_offset(marker))
221
+ if records or time.monotonic() >= deadline:
222
+ atomic_write(marker, str(offset))
223
+ return records
224
+ time.sleep(min(0.2, max(0, deadline - time.monotonic())))
225
+
226
+
227
+ def main():
228
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
229
+ parser.add_argument("--workspace", default=os.environ.get("AGENTBRIDGE_WORKSPACE", "/opt/agentbridge-public"))
230
+ sub = parser.add_subparsers(dest="command", required=True)
231
+ sub.add_parser("init")
232
+ reg = sub.add_parser("register")
233
+ reg.add_argument("agent")
234
+ reg.add_argument("--summary", required=True)
235
+ reg.add_argument("--kind", default="generic")
236
+ reg.add_argument("--project", default="")
237
+ reg.add_argument("--update", action="store_true")
238
+ ls = sub.add_parser("list")
239
+ ls.add_argument("--query", default="", help="case-insensitive search across profile fields")
240
+ show = sub.add_parser("show")
241
+ show.add_argument("agent")
242
+ msg = sub.add_parser("send")
243
+ msg.add_argument("to")
244
+ msg.add_argument("text", help="use - to read from stdin")
245
+ msg.add_argument("--from", dest="sender", default=os.environ.get("AGENTBRIDGE_AGENT"))
246
+ msg.add_argument("--reply-to")
247
+ msg.add_argument("--kind")
248
+ read = sub.add_parser("read")
249
+ read.add_argument("agent")
250
+ read.add_argument("--wait", type=float, default=0)
251
+ tail = sub.add_parser("tail")
252
+ tail.add_argument("agent")
253
+ tail.add_argument("-n", type=int, default=10)
254
+ args = parser.parse_args()
255
+ workspace = Path(args.workspace).absolute()
256
+ try:
257
+ if args.command == "init":
258
+ initialize(workspace)
259
+ print(workspace)
260
+ elif args.command == "register":
261
+ print(json.dumps(register(workspace, args.agent, args.summary, args.kind,
262
+ args.project, args.update), ensure_ascii=False))
263
+ elif args.command == "list":
264
+ if not (workspace / "agents").is_dir():
265
+ raise ValueError("workspace is not initialized; run init first")
266
+ for path in sorted((workspace / "agents").iterdir()):
267
+ if path.is_dir() and (path / "profile.json").exists():
268
+ record = profile(workspace, path.name)
269
+ if args.query.casefold() in json.dumps(record, ensure_ascii=False).casefold():
270
+ print(json.dumps(record, ensure_ascii=False))
271
+ elif args.command == "show":
272
+ print(json.dumps(profile(workspace, args.agent), ensure_ascii=False, indent=2))
273
+ elif args.command == "send":
274
+ if not args.sender:
275
+ parser.error("send requires --from ID or AGENTBRIDGE_AGENT")
276
+ text = sys.stdin.read() if args.text == "-" else args.text
277
+ print(send(workspace, args.sender, args.to, text, args.reply_to, args.kind)["id"])
278
+ else:
279
+ if args.command == "read":
280
+ if not math.isfinite(args.wait) or args.wait < 0:
281
+ parser.error("--wait must be finite and non-negative")
282
+ records = receive(workspace, args.agent, args.wait)
283
+ else:
284
+ if args.n < 1:
285
+ parser.error("-n must be positive")
286
+ profile(workspace, args.agent)
287
+ with locked(agent_dir(workspace, args.agent) / "inbox.jsonl"):
288
+ records = read_complete(agent_dir(workspace, args.agent) / "inbox.jsonl", 0)[0][-args.n:]
289
+ for record in records:
290
+ print(json.dumps(record, ensure_ascii=False))
291
+ except (OSError, ValueError) as error:
292
+ parser.exit(1, f"agentbridge: {error}\n")
293
+
294
+
295
+ if __name__ == "__main__":
296
+ main()