bus-agent 2.3.0
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.
- package/.env.coco +11 -0
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/SKILL.md +314 -0
- package/backup.js +57 -0
- package/bin/cli.js +41 -0
- package/bridge.js +325 -0
- package/claude-mcp.json +10 -0
- package/clients/coco-client.ts +245 -0
- package/clients/coco_client.py +216 -0
- package/coco-aliases.sh +10 -0
- package/coco-cli.js +1002 -0
- package/coco-tool.js +177 -0
- package/coco.js +26 -0
- package/cursor-mcp.json +3 -0
- package/doctor.js +24 -0
- package/hermes-forwarder.js +152 -0
- package/hermes.example.json +9 -0
- package/index.js +52 -0
- package/lib/backup.js +256 -0
- package/lib/bus.js +516 -0
- package/lib/daemon.js +96 -0
- package/lib/doctor.js +333 -0
- package/lib/hermes.js +162 -0
- package/lib/mcp.js +730 -0
- package/lib/memory.js +667 -0
- package/lib/orchestrator.js +426 -0
- package/lib/scheduler.js +259 -0
- package/lib/tunnel.js +317 -0
- package/mcporter.example.json +14 -0
- package/opencode-mcp.json +10 -0
- package/package.json +76 -0
- package/scripts/install.bat +5 -0
- package/scripts/install.ps1 +100 -0
- package/setup.js +320 -0
- package/tunnel.js +66 -0
- package/webhook-gateway.js +420 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CoCo Bus Client — Python SDK for Agent Bus
|
|
3
|
+
|
|
4
|
+
Direct file-based access to the CoCo Agent Bus. No MCP needed.
|
|
5
|
+
Any agent with filesystem access to the bus directory can use this.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from coco_client import CoCoClient
|
|
9
|
+
|
|
10
|
+
bus = CoCoClient("/path/to/mcp-coco/.bus", agent_name="my-agent")
|
|
11
|
+
bus.register(description="My Python agent", capabilities=["data-analysis", "api-calls"])
|
|
12
|
+
bus.send("hermes", "Hello from Python!")
|
|
13
|
+
msgs = bus.fetch_messages()
|
|
14
|
+
for m in msgs:
|
|
15
|
+
print(f"[{m['from']}] {m['message']}")
|
|
16
|
+
bus.reply(m, "Got it!")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CoCoClient:
|
|
26
|
+
def __init__(self, bus_dir, agent_name=None):
|
|
27
|
+
self.bus_dir = bus_dir
|
|
28
|
+
self.msgs_dir = os.path.join(bus_dir, "messages")
|
|
29
|
+
self.agents_file = os.path.join(bus_dir, "agents.json")
|
|
30
|
+
self.channels_dir = os.path.join(bus_dir, "channels")
|
|
31
|
+
self.events_dir = os.path.join(bus_dir, "events")
|
|
32
|
+
self.agent_name = agent_name or os.environ.get("COCO_AGENT") or os.environ.get("USER", "python-agent")
|
|
33
|
+
self._ensure_dirs()
|
|
34
|
+
|
|
35
|
+
def _ensure_dirs(self):
|
|
36
|
+
for d in [self.bus_dir, self.msgs_dir, self.channels_dir, self.events_dir]:
|
|
37
|
+
os.makedirs(d, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
def _gen_id(self):
|
|
40
|
+
return f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
|
41
|
+
|
|
42
|
+
def _load_agents(self):
|
|
43
|
+
try:
|
|
44
|
+
if os.path.exists(self.agents_file):
|
|
45
|
+
with open(self.agents_file) as f:
|
|
46
|
+
return json.load(f)
|
|
47
|
+
except:
|
|
48
|
+
pass
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
def _save_agents(self, agents):
|
|
52
|
+
with open(self.agents_file, "w") as f:
|
|
53
|
+
json.dump(agents, f, indent=2)
|
|
54
|
+
|
|
55
|
+
# ── Registration ──
|
|
56
|
+
|
|
57
|
+
def register(self, description="", capabilities=None, model=None, tags=None, version="1.0.0", status="idle"):
|
|
58
|
+
"""Register yourself on the CoCo Agent Bus."""
|
|
59
|
+
agents = self._load_agents()
|
|
60
|
+
existing = agents.get(self.agent_name, {})
|
|
61
|
+
agents[self.agent_name] = {
|
|
62
|
+
"name": self.agent_name,
|
|
63
|
+
"description": description,
|
|
64
|
+
"capabilities": capabilities or [],
|
|
65
|
+
"model": model or None,
|
|
66
|
+
"tags": tags or [],
|
|
67
|
+
"status": status,
|
|
68
|
+
"version": version,
|
|
69
|
+
"endpoints": {},
|
|
70
|
+
"tools": [],
|
|
71
|
+
"metadata": {},
|
|
72
|
+
"last_seen": datetime.utcnow().isoformat() + "Z",
|
|
73
|
+
"registered_at": existing.get("registered_at", datetime.utcnow().isoformat() + "Z"),
|
|
74
|
+
}
|
|
75
|
+
self._save_agents(agents)
|
|
76
|
+
is_new = not existing
|
|
77
|
+
return {"registered": True, "is_new": is_new}
|
|
78
|
+
|
|
79
|
+
def heartbeat(self):
|
|
80
|
+
"""Mark yourself as online."""
|
|
81
|
+
agents = self._load_agents()
|
|
82
|
+
if self.agent_name in agents:
|
|
83
|
+
agents[self.agent_name]["last_seen"] = datetime.utcnow().isoformat() + "Z"
|
|
84
|
+
agents[self.agent_name]["status"] = "idle"
|
|
85
|
+
self._save_agents(agents)
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def set_status(self, status):
|
|
90
|
+
"""Set status: idle, busy, offline."""
|
|
91
|
+
agents = self._load_agents()
|
|
92
|
+
if self.agent_name in agents:
|
|
93
|
+
agents[self.agent_name]["status"] = status
|
|
94
|
+
agents[self.agent_name]["last_seen"] = datetime.utcnow().isoformat() + "Z"
|
|
95
|
+
self._save_agents(agents)
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def list_agents(self, online_only=False):
|
|
100
|
+
"""List agents on the bus."""
|
|
101
|
+
agents = self._load_agents()
|
|
102
|
+
if online_only:
|
|
103
|
+
cutoff = time.time() * 1000 - 300000
|
|
104
|
+
agents = {k: v for k, v in agents.items()
|
|
105
|
+
if datetime.fromisoformat(v["last_seen"].replace("Z", "+00:00")).timestamp() * 1000 > cutoff}
|
|
106
|
+
return agents
|
|
107
|
+
|
|
108
|
+
# ── Messaging ──
|
|
109
|
+
|
|
110
|
+
def send(self, to, message, metadata=None):
|
|
111
|
+
"""Send a DM to another agent."""
|
|
112
|
+
msg = {
|
|
113
|
+
"id": self._gen_id(),
|
|
114
|
+
"from": self.agent_name,
|
|
115
|
+
"to": to,
|
|
116
|
+
"message": message,
|
|
117
|
+
"metadata": metadata or {},
|
|
118
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
119
|
+
}
|
|
120
|
+
inbox_dir = os.path.join(self.msgs_dir, to)
|
|
121
|
+
os.makedirs(inbox_dir, exist_ok=True)
|
|
122
|
+
with open(os.path.join(inbox_dir, f"{msg['id']}.json"), "w") as f:
|
|
123
|
+
json.dump(msg, f, indent=2)
|
|
124
|
+
|
|
125
|
+
# Outbox
|
|
126
|
+
outbox_dir = os.path.join(self.msgs_dir, f"{self.agent_name}_outbox")
|
|
127
|
+
os.makedirs(outbox_dir, exist_ok=True)
|
|
128
|
+
with open(os.path.join(outbox_dir, f"{msg['id']}.json"), "w") as f:
|
|
129
|
+
json.dump(msg, f, indent=2)
|
|
130
|
+
|
|
131
|
+
return msg["id"]
|
|
132
|
+
|
|
133
|
+
def broadcast(self, message):
|
|
134
|
+
"""Send to all agents."""
|
|
135
|
+
agents = self._load_agents()
|
|
136
|
+
results = []
|
|
137
|
+
for name in agents:
|
|
138
|
+
if name != self.agent_name:
|
|
139
|
+
mid = self.send(name, message, {"broadcast": True})
|
|
140
|
+
results.append({"to": name, "message_id": mid})
|
|
141
|
+
return results
|
|
142
|
+
|
|
143
|
+
def fetch_messages(self, limit=50, from_agent=None):
|
|
144
|
+
"""Read your inbox."""
|
|
145
|
+
inbox_dir = os.path.join(self.msgs_dir, self.agent_name)
|
|
146
|
+
if not os.path.exists(inbox_dir):
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
files = sorted([f for f in os.listdir(inbox_dir) if f.endswith(".json")])
|
|
150
|
+
messages = []
|
|
151
|
+
for f in files[-limit:]:
|
|
152
|
+
try:
|
|
153
|
+
with open(os.path.join(inbox_dir, f)) as fp:
|
|
154
|
+
msg = json.load(fp)
|
|
155
|
+
if from_agent and msg.get("from") != from_agent:
|
|
156
|
+
continue
|
|
157
|
+
messages.append(msg)
|
|
158
|
+
except:
|
|
159
|
+
pass
|
|
160
|
+
return messages
|
|
161
|
+
|
|
162
|
+
def reply(self, original_msg, text):
|
|
163
|
+
"""Reply to a message."""
|
|
164
|
+
return self.send(original_msg["from"], text, {
|
|
165
|
+
"in_reply_to": original_msg["id"],
|
|
166
|
+
"original_from": original_msg.get("from"),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
# ── Channels ──
|
|
170
|
+
|
|
171
|
+
def channel_send(self, channel_id, message):
|
|
172
|
+
"""Send a message to a channel."""
|
|
173
|
+
ch_path = os.path.join(self.channels_dir, f"{channel_id}.json")
|
|
174
|
+
if not os.path.exists(ch_path):
|
|
175
|
+
raise ValueError(f"Channel '{channel_id}' not found")
|
|
176
|
+
|
|
177
|
+
msg = {
|
|
178
|
+
"id": self._gen_id(),
|
|
179
|
+
"channel": channel_id,
|
|
180
|
+
"from": self.agent_name,
|
|
181
|
+
"message": message,
|
|
182
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
log_dir = os.path.join(self.channels_dir, channel_id, "log")
|
|
186
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
187
|
+
with open(os.path.join(log_dir, f"{msg['id']}.json"), "w") as f:
|
|
188
|
+
json.dump(msg, f, indent=2)
|
|
189
|
+
|
|
190
|
+
# DM all members
|
|
191
|
+
with open(ch_path) as f:
|
|
192
|
+
channel = json.load(f)
|
|
193
|
+
for member in channel.get("members", []):
|
|
194
|
+
if member != self.agent_name:
|
|
195
|
+
self.send(member, f"[{channel_id}] {message}", {"channel": channel_id})
|
|
196
|
+
|
|
197
|
+
return msg["id"]
|
|
198
|
+
|
|
199
|
+
def channel_history(self, channel_id, limit=20):
|
|
200
|
+
"""Read recent messages in a channel."""
|
|
201
|
+
log_dir = os.path.join(self.channels_dir, channel_id, "log")
|
|
202
|
+
if not os.path.exists(log_dir):
|
|
203
|
+
return []
|
|
204
|
+
files = sorted([f for f in os.listdir(log_dir) if f.endswith(".json")])
|
|
205
|
+
messages = []
|
|
206
|
+
for f in files[-limit:]:
|
|
207
|
+
try:
|
|
208
|
+
with open(os.path.join(log_dir, f)) as fp:
|
|
209
|
+
messages.append(json.load(fp))
|
|
210
|
+
except:
|
|
211
|
+
pass
|
|
212
|
+
return messages
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Make datetime available
|
|
216
|
+
from datetime import datetime
|
package/coco-aliases.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
# ── CoCo Bus Aliases ──
|
|
3
|
+
alias coco-agents='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js agents'
|
|
4
|
+
alias coco-inbox='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js inbox'
|
|
5
|
+
alias coco-send='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js send'
|
|
6
|
+
alias coco-whoami='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js whoami'
|
|
7
|
+
alias coco-status='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js status'
|
|
8
|
+
alias coco-search='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js search'
|
|
9
|
+
alias coco-watch='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js watch'
|
|
10
|
+
alias coco-channel='node E:\_system\.openclaw\workspace\repos\mcp-coco\coco-cli.js channel'
|