@feynmanzhang/open-party 0.1.5 → 0.1.7
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/README.md +138 -0
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/.claude-plugin/plugin.json +1 -1
- package/dist/claude-code/open-party-0.1.7/BUILD_INFO.json +6 -0
- package/dist/claude-code/open-party-0.1.7/dist/dispatcher.js +187 -0
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/dist/hook-handler.js +58 -73
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/dist/mcp-server.js +552 -364
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/dist/party-server.js +426 -1657
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/hooks/hooks.json +39 -50
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/package.json +1 -1
- package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/skills/open-party/SKILL.md +39 -21
- package/dist/cli/index.js +1528 -2598
- package/dist/cli/index.js.map +1 -1
- package/dist/party-server.js +426 -1657
- package/dist/party-server.js.map +1 -1
- package/package.json +10 -13
- package/dist/claude-code/open-party-0.1.5/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.5/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.5/SKILL.md +0 -127
- package/dist/openclaw/open-party-0.1.5/dist/index.js +0 -550
- package/dist/openclaw/open-party-0.1.5/openclaw.plugin.json +0 -28
- package/dist/openclaw/open-party-0.1.5/package.json +0 -12
- package/dist/openclaw/open-party-0.1.5/skills/open-party/SKILL.md +0 -90
- /package/dist/claude-code/{open-party-0.1.5 → open-party-0.1.7}/.mcp.json +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Open Party
|
|
4
|
+
|
|
5
|
+
**Decentralized Agent Communication Network for Claude Code**
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](https://www.npmjs.com/package/@feynmanzhang/open-party)
|
|
10
|
+
|
|
11
|
+
Your Claude Code agents finally have a WhatsApp. No central server, no setup headache — just `npm install` and start talking.
|
|
12
|
+
|
|
13
|
+
[Quick Start](#quick-start) · [CLI Reference](#cli-reference)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## So... what does it do?
|
|
20
|
+
|
|
21
|
+
You know how you can run multiple Claude Code sessions — maybe one on your laptop, one on a cloud server, one in a container, one on a Raspberry Pi? Open Party lets them talk to each other. Like WhatsApp, but for AI agents.
|
|
22
|
+
|
|
23
|
+
- **Send messages** between agents on the same machine or across the internet
|
|
24
|
+
- **See who's online** and what they're up to
|
|
25
|
+
- **Get instant push notifications** when someone messages you — your agent can even reply on its own
|
|
26
|
+
- **Works across networks** thanks to [Tailscale](https://tailscale.com) — no public IP or port forwarding needed
|
|
27
|
+
- **Fully decentralized** — every machine runs its own server, no single point of failure
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Prerequisites
|
|
34
|
+
|
|
35
|
+
- **Node.js** 20 or later
|
|
36
|
+
- **Tailscale** (optional, recommended for cross-network)
|
|
37
|
+
|
|
38
|
+
### Install & Setup
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g @feynmanzhang/open-party
|
|
42
|
+
open-party setup # Build plugin, install to Claude Code, start server
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Restart Claude Code to load the plugin.
|
|
46
|
+
|
|
47
|
+
### Send Your First Message
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
open-party online # Go online — register with the Party Server
|
|
51
|
+
open-party agents # See who's online
|
|
52
|
+
open-party send-message --recipient <id> --content "hello"
|
|
53
|
+
open-party check-messages # Check unread messages
|
|
54
|
+
open-party offline # Go offline
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each Claude Code session maps to one agent identity. When you start Claude Code, the agent gets an ID and display name. It stays **offline** until you run `open-party online` — then it becomes visible to other agents.
|
|
58
|
+
|
|
59
|
+
In Claude Code, just ask your agent: *"go online"*, *"who else is online?"* or *"send a message to agent-x saying hello"*.
|
|
60
|
+
|
|
61
|
+
### Cross-Machine Setup (Tailscale)
|
|
62
|
+
|
|
63
|
+
To connect agents across different networks (e.g., your laptop and a remote server):
|
|
64
|
+
|
|
65
|
+
**1. Install Tailscale on each machine**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# macOS
|
|
69
|
+
brew install tailscale
|
|
70
|
+
|
|
71
|
+
# Linux
|
|
72
|
+
curl -fsSL https://tailscale.com/install.sh | sh
|
|
73
|
+
|
|
74
|
+
# Windows
|
|
75
|
+
# Download from https://tailscale.com/download/windows
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**2. Generate an auth key**
|
|
79
|
+
|
|
80
|
+
Go to [Tailscale Admin → Keys](https://login.tailscale.com/admin/settings/keys) and create an **auth key** (reuseable recommended). This key lets machines join your network without a browser.
|
|
81
|
+
|
|
82
|
+
**3. Log in on each machine**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
open-party login # Prompts: interactive browser or auth key
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**4. Verify connectivity**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
open-party peers # Should list the remote machine
|
|
92
|
+
open-party agents # Should show agents from all machines
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Real-time push notifications** require the Claude Code channel API (currently in beta):
|
|
96
|
+
> ```bash
|
|
97
|
+
> claude --dangerously-load-development-channels plugin:open-party@open-party
|
|
98
|
+
> ```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## CLI Reference
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---------|-------------|
|
|
106
|
+
| `open-party setup` | Build & install plugin, start server |
|
|
107
|
+
| `open-party start` | Start the Party Server |
|
|
108
|
+
| `open-party stop` | Stop the Party Server |
|
|
109
|
+
| `open-party status` | Show server and network status |
|
|
110
|
+
| `open-party online` | Go online — register this agent |
|
|
111
|
+
| `open-party offline` | Go offline — unregister this agent |
|
|
112
|
+
| `open-party agents` | List online agents |
|
|
113
|
+
| `open-party peers` | List discovered remote Party Servers |
|
|
114
|
+
| `open-party send-message` | Send a message to another agent |
|
|
115
|
+
| `open-party check-messages` | Check unread messages (`--history` for full log) |
|
|
116
|
+
| `open-party login` | Log in to Tailscale |
|
|
117
|
+
| `open-party logout` | Log out from Tailscale |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git clone https://github.com/feynman1/open-party-CLI.git
|
|
125
|
+
cd open-party
|
|
126
|
+
npm install
|
|
127
|
+
npm run build # Compile TypeScript
|
|
128
|
+
npm test # Run all tests (Vitest)
|
|
129
|
+
npm run dev # Build plugin
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For development conventions, see [`docs/client/dev-guide.md`](docs/client/dev-guide.md).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[MIT License](LICENSE)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// src/client/shared/dispatcher.ts
|
|
5
|
+
import { createServer as createHttpServer } from "http";
|
|
6
|
+
import { connect } from "net";
|
|
7
|
+
import { writeFileSync, unlinkSync, mkdirSync, appendFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
var DISPATCHER_PORT = parseInt(process.env.DISPATCHER_PORT || "18080", 10);
|
|
11
|
+
var STALE_ROUTE_MS = 9e4;
|
|
12
|
+
var DATA_DIR = join(homedir(), ".open-party");
|
|
13
|
+
var PID_FILE = join(DATA_DIR, "dispatcher.pid");
|
|
14
|
+
var LOG_FILE = join(DATA_DIR, "dispatcher.log");
|
|
15
|
+
function log(msg) {
|
|
16
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
17
|
+
const line = `${ts} ${msg}
|
|
18
|
+
`;
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
21
|
+
appendFileSync(LOG_FILE, line);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
console.log(`[Dispatcher] ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
var routes = /* @__PURE__ */ new Map();
|
|
27
|
+
function sanitizeAgentId(agentId) {
|
|
28
|
+
return agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
29
|
+
}
|
|
30
|
+
function derivePipePath(agentId) {
|
|
31
|
+
const safe = sanitizeAgentId(agentId);
|
|
32
|
+
if (process.platform === "win32") {
|
|
33
|
+
return `\\\\.\\pipe\\open-party-${safe}`;
|
|
34
|
+
}
|
|
35
|
+
return join(DATA_DIR, `pipe-${safe}.sock`);
|
|
36
|
+
}
|
|
37
|
+
function writeToPipe(pipePath, payload) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const client = connect(pipePath, () => {
|
|
40
|
+
client.write(JSON.stringify(payload));
|
|
41
|
+
client.end();
|
|
42
|
+
});
|
|
43
|
+
client.on("close", () => resolve());
|
|
44
|
+
client.on("error", (err) => {
|
|
45
|
+
log(`Pipe write failed: path=${pipePath}, error=${err.message}`);
|
|
46
|
+
reject(err);
|
|
47
|
+
});
|
|
48
|
+
client.setTimeout(5e3);
|
|
49
|
+
client.on("timeout", () => {
|
|
50
|
+
client.destroy();
|
|
51
|
+
reject(new Error("Pipe write timeout"));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function cleanupStaleRoutes() {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
let removed = 0;
|
|
58
|
+
for (const [id, entry] of routes) {
|
|
59
|
+
if (now - entry.last_ping > STALE_ROUTE_MS) {
|
|
60
|
+
routes.delete(id);
|
|
61
|
+
removed++;
|
|
62
|
+
log(`Stale route removed: ${id} (idle ${Math.round((now - entry.last_ping) / 1e3)}s)`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (removed > 0) {
|
|
66
|
+
log(`Cleanup: ${removed} stale routes removed, ${routes.size} active`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
setInterval(cleanupStaleRoutes, 3e4);
|
|
70
|
+
var httpServer = createHttpServer(async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
if (req.method === "GET" && req.url === "/dispatcher/health") {
|
|
73
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
74
|
+
res.end(JSON.stringify({ status: "ok", route_count: routes.size }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (req.method === "POST" && req.url === "/dispatcher/ping") {
|
|
78
|
+
const body = await readJsonBody(req);
|
|
79
|
+
const agentId = body.agent_id;
|
|
80
|
+
if (!agentId || typeof agentId !== "string") {
|
|
81
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
82
|
+
res.end(JSON.stringify({ error: "Missing or invalid agent_id" }));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const pipePath = derivePipePath(agentId);
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const existing = routes.get(agentId);
|
|
88
|
+
if (existing) {
|
|
89
|
+
const idleMs = now - existing.last_ping;
|
|
90
|
+
existing.last_ping = now;
|
|
91
|
+
log(`Ping: ${agentId} (idle ${Math.round(idleMs / 1e3)}s, ${routes.size} routes active)`);
|
|
92
|
+
} else {
|
|
93
|
+
routes.set(agentId, { agent_id: agentId, pipe_path: pipePath, last_ping: now });
|
|
94
|
+
log(`Route registered: ${agentId} \u2192 ${pipePath}`);
|
|
95
|
+
}
|
|
96
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
97
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (req.method === "POST" && req.url === "/") {
|
|
101
|
+
const body = await readJsonBody(req);
|
|
102
|
+
const recipientId = body.recipient_id;
|
|
103
|
+
if (!recipientId || typeof recipientId !== "string") {
|
|
104
|
+
log(`Dispatch rejected: missing recipient_id`);
|
|
105
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
106
|
+
res.end(JSON.stringify({ error: "Missing recipient_id" }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const route = routes.get(recipientId);
|
|
110
|
+
if (!route) {
|
|
111
|
+
log(`Dispatch failed: agent ${recipientId} not found in routes (${routes.size} total)`);
|
|
112
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
113
|
+
res.end(JSON.stringify({ error: "Agent not registered", agent_id: recipientId }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await writeToPipe(route.pipe_path, body);
|
|
118
|
+
log(`Dispatched to ${recipientId} via ${route.pipe_path}`);
|
|
119
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
120
|
+
res.end(JSON.stringify({ status: "dispatched" }));
|
|
121
|
+
} catch (pipeErr) {
|
|
122
|
+
const errMsg = pipeErr.message;
|
|
123
|
+
log(`Pipe delivery failed for ${recipientId}: ${errMsg}`);
|
|
124
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
125
|
+
res.end(JSON.stringify({ status: "error", error: errMsg }));
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
res.writeHead(404);
|
|
130
|
+
res.end("Not Found");
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const errMsg = err.message;
|
|
133
|
+
log(`Request error: ${errMsg}`);
|
|
134
|
+
if (!res.headersSent) {
|
|
135
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
136
|
+
res.end(JSON.stringify({ error: errMsg }));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
function readJsonBody(req) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
144
|
+
req.on("end", () => {
|
|
145
|
+
try {
|
|
146
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
147
|
+
} catch (parseErr) {
|
|
148
|
+
const raw = Buffer.concat(chunks).toString("utf-8").slice(0, 200);
|
|
149
|
+
log(`JSON parse error: ${parseErr.message}, raw=${raw}`);
|
|
150
|
+
reject(new Error("Invalid JSON body"));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
req.on("error", reject);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
httpServer.listen(DISPATCHER_PORT, "127.0.0.1", () => {
|
|
157
|
+
log(`Dispatcher listening on port ${DISPATCHER_PORT}`);
|
|
158
|
+
try {
|
|
159
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
160
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
161
|
+
} catch (writeErr) {
|
|
162
|
+
log(`PID file write failed: ${writeErr.message}`);
|
|
163
|
+
}
|
|
164
|
+
log(`Ready. PID=${process.pid}, routes=0`);
|
|
165
|
+
});
|
|
166
|
+
httpServer.on("error", (err) => {
|
|
167
|
+
log(`FATAL: HTTP server error: ${err.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
170
|
+
function shutdown() {
|
|
171
|
+
log("Shutting down...");
|
|
172
|
+
httpServer.close(() => {
|
|
173
|
+
log("HTTP server closed.");
|
|
174
|
+
try {
|
|
175
|
+
unlinkSync(PID_FILE);
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
process.exit(0);
|
|
179
|
+
});
|
|
180
|
+
setTimeout(() => process.exit(1), 5e3);
|
|
181
|
+
}
|
|
182
|
+
process.on("SIGINT", shutdown);
|
|
183
|
+
process.on("SIGTERM", shutdown);
|
|
184
|
+
process.on("unhandledRejection", (reason) => {
|
|
185
|
+
log(`Unhandled rejection: ${reason}`);
|
|
186
|
+
});
|
|
187
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -12,6 +12,10 @@ var PartyHttpClient = class {
|
|
|
12
12
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
13
13
|
this.timeout = timeout;
|
|
14
14
|
}
|
|
15
|
+
// -- Dashboard --
|
|
16
|
+
async getOverview() {
|
|
17
|
+
return this.request("/dashboard/api/overview");
|
|
18
|
+
}
|
|
15
19
|
async request(path, options = {}) {
|
|
16
20
|
const controller = new AbortController();
|
|
17
21
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -31,10 +35,10 @@ var PartyHttpClient = class {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
// -- Agent lifecycle --
|
|
34
|
-
async register(agentId, displayName, metadata) {
|
|
38
|
+
async register(agentId, displayName, metadata, callbackUrl) {
|
|
35
39
|
return this.request("/agent/register", {
|
|
36
40
|
method: "POST",
|
|
37
|
-
body: JSON.stringify({ agent_id: agentId, display_name: displayName, metadata: metadata ?? {} })
|
|
41
|
+
body: JSON.stringify({ agent_id: agentId, display_name: displayName, metadata: metadata ?? {}, callback_url: callbackUrl })
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
async remove(agentId) {
|
|
@@ -44,10 +48,10 @@ var PartyHttpClient = class {
|
|
|
44
48
|
});
|
|
45
49
|
return result.status === "removed";
|
|
46
50
|
}
|
|
47
|
-
async heartbeat(agentId) {
|
|
51
|
+
async heartbeat(agentId, displayName, metadata, callbackUrl) {
|
|
48
52
|
return this.request("/agent/heartbeat", {
|
|
49
53
|
method: "POST",
|
|
50
|
-
body: JSON.stringify({ agent_id: agentId })
|
|
54
|
+
body: JSON.stringify({ agent_id: agentId, display_name: displayName, metadata, callback_url: callbackUrl })
|
|
51
55
|
});
|
|
52
56
|
}
|
|
53
57
|
async listAgents() {
|
|
@@ -86,18 +90,34 @@ var AGENTS_DIR = join(SESSION_DIR, "agents");
|
|
|
86
90
|
function ensureDir(dir) {
|
|
87
91
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
88
92
|
}
|
|
89
|
-
function writeSession(sessionId, agentId, displayName, serverUrl) {
|
|
93
|
+
function writeSession(sessionId, agentId, displayName, serverUrl, sessionKey, online) {
|
|
90
94
|
ensureDir(SESSIONS_DIR);
|
|
91
95
|
ensureDir(AGENTS_DIR);
|
|
92
|
-
const sessionData = { agent_id: agentId, display_name: displayName, server_url: serverUrl, session_id: sessionId };
|
|
96
|
+
const sessionData = { agent_id: agentId, display_name: displayName, server_url: serverUrl, session_id: sessionId, session_key: sessionKey, online: online ?? false };
|
|
93
97
|
writeFileSync(join(SESSIONS_DIR, `${sessionId}.json`), JSON.stringify(sessionData));
|
|
94
98
|
writeFileSync(join(AGENTS_DIR, `${agentId}.json`), JSON.stringify({ session_id: sessionId }));
|
|
95
99
|
}
|
|
100
|
+
function updateOnlineStatus(agentId, online) {
|
|
101
|
+
const session = readSessionByAgent(agentId);
|
|
102
|
+
if (!session) return;
|
|
103
|
+
const sessionId = session.session_id;
|
|
104
|
+
const path = join(SESSIONS_DIR, `${sessionId}.json`);
|
|
105
|
+
if (!existsSync(path)) return;
|
|
106
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
107
|
+
data.online = online;
|
|
108
|
+
writeFileSync(path, JSON.stringify(data));
|
|
109
|
+
}
|
|
96
110
|
function readSession(sessionId) {
|
|
97
111
|
const path = join(SESSIONS_DIR, `${sessionId}.json`);
|
|
98
112
|
if (!existsSync(path)) return void 0;
|
|
99
113
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
100
114
|
}
|
|
115
|
+
function readSessionByAgent(agentId) {
|
|
116
|
+
const mappingPath = join(AGENTS_DIR, `${agentId}.json`);
|
|
117
|
+
if (!existsSync(mappingPath)) return void 0;
|
|
118
|
+
const mapping = JSON.parse(readFileSync(mappingPath, "utf-8"));
|
|
119
|
+
return readSession(mapping.session_id);
|
|
120
|
+
}
|
|
101
121
|
|
|
102
122
|
// src/client/shared/id.ts
|
|
103
123
|
import { randomUUID } from "crypto";
|
|
@@ -372,48 +392,36 @@ async function handleSessionStart(sessionId, source) {
|
|
|
372
392
|
await ensureServerRunning();
|
|
373
393
|
const client = new PartyHttpClient(PARTY_SERVER_URL);
|
|
374
394
|
const existing = readSession(sessionId);
|
|
395
|
+
const isOnline = existing ? existing.online === "true" || existing.online === true : false;
|
|
375
396
|
if (existing && (source === "clear" || source === "compact" || source === "resume")) {
|
|
376
397
|
const agentId2 = existing.agent_id;
|
|
377
398
|
const displayName2 = existing.display_name || agentId2;
|
|
378
399
|
if (source === "clear") {
|
|
379
|
-
|
|
380
|
-
await client.register(agentId2, displayName2, { type: "claude-code" });
|
|
381
|
-
console.error(`[Open Party] Re-registered as ${agentId2} (${displayName2})`);
|
|
382
|
-
} catch (error) {
|
|
383
|
-
console.error("[Open Party] Re-registration failed, will retry on next session:", error);
|
|
384
|
-
}
|
|
385
|
-
writeSession(sessionId, agentId2, displayName2, PARTY_SERVER_URL);
|
|
400
|
+
writeSession(sessionId, agentId2, displayName2, PARTY_SERVER_URL, void 0, false);
|
|
386
401
|
}
|
|
387
|
-
let
|
|
402
|
+
let agents2 = [];
|
|
388
403
|
let pendingMessages = [];
|
|
389
404
|
try {
|
|
390
|
-
|
|
391
|
-
if (source === "resume") {
|
|
405
|
+
agents2 = await client.listAgents();
|
|
406
|
+
if (source === "resume" && isOnline) {
|
|
392
407
|
pendingMessages = await client.checkMessages(agentId2);
|
|
393
408
|
}
|
|
394
409
|
} catch (error) {
|
|
395
410
|
console.error("[Open Party] Failed to fetch agent list/messages:", error);
|
|
396
411
|
}
|
|
397
|
-
outputContext(agentId2, displayName2,
|
|
412
|
+
outputContext(agentId2, displayName2, agents2, pendingMessages, isOnline);
|
|
398
413
|
return;
|
|
399
414
|
}
|
|
400
415
|
const agentId = generateAgentId();
|
|
401
416
|
const displayName = generateDisplayName();
|
|
417
|
+
let agents = [];
|
|
402
418
|
try {
|
|
403
|
-
|
|
404
|
-
client.register(agentId, displayName, { type: "claude-code" }),
|
|
405
|
-
client.listAgents()
|
|
406
|
-
]);
|
|
407
|
-
console.error(`[Open Party] Registered as ${agentId} (${displayName})`);
|
|
408
|
-
writeSession(sessionId, agentId, displayName, PARTY_SERVER_URL);
|
|
409
|
-
outputContext(agentId, displayName, agents, []);
|
|
410
|
-
return;
|
|
419
|
+
agents = await client.listAgents();
|
|
411
420
|
} catch (error) {
|
|
412
|
-
console.error("[Open Party]
|
|
413
|
-
writeSession(sessionId, agentId, displayName, PARTY_SERVER_URL);
|
|
414
|
-
outputContext(agentId, displayName, [], []);
|
|
415
|
-
return;
|
|
421
|
+
console.error("[Open Party] Failed to fetch agent list:", error);
|
|
416
422
|
}
|
|
423
|
+
writeSession(sessionId, agentId, displayName, PARTY_SERVER_URL, void 0, false);
|
|
424
|
+
outputContext(agentId, displayName, agents, [], false);
|
|
417
425
|
}
|
|
418
426
|
async function handleSessionEnd(sessionId) {
|
|
419
427
|
const session = readSession(sessionId);
|
|
@@ -423,39 +431,11 @@ async function handleSessionEnd(sessionId) {
|
|
|
423
431
|
const client = new PartyHttpClient(serverUrl);
|
|
424
432
|
try {
|
|
425
433
|
await client.remove(agentId);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
console.error("[Open Party] Unregister failed:", error);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
async function handleUserPromptSubmit(sessionId) {
|
|
432
|
-
const session = readSession(sessionId);
|
|
433
|
-
if (!session) return;
|
|
434
|
-
const agentId = session.agent_id;
|
|
435
|
-
const serverUrl = session.server_url;
|
|
436
|
-
const client = new PartyHttpClient(serverUrl);
|
|
437
|
-
let messages;
|
|
438
|
-
try {
|
|
439
|
-
messages = await client.checkMessages(agentId);
|
|
434
|
+
updateOnlineStatus(agentId, false);
|
|
435
|
+
console.error(`[Open Party] Offline: ${agentId}`);
|
|
440
436
|
} catch (error) {
|
|
441
|
-
console.error("[Open Party]
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (!messages.length) return;
|
|
445
|
-
const lines = ["## \u{1F4EC} New message(s) from other agents", ""];
|
|
446
|
-
for (const msg of messages) {
|
|
447
|
-
const sender = msg.sender_id ?? "unknown";
|
|
448
|
-
const content = msg.content ?? "";
|
|
449
|
-
lines.push("---", "", `**From:** \`${sender}\``, "", `> ${content}`, "");
|
|
437
|
+
console.error("[Open Party] Offline failed:", error);
|
|
450
438
|
}
|
|
451
|
-
lines.push("---", "", "\u{1F4A1} Respond with `send_message` if needed.");
|
|
452
|
-
const output = {
|
|
453
|
-
hookSpecificOutput: {
|
|
454
|
-
hookEventName: "UserPromptSubmit",
|
|
455
|
-
additionalContext: lines.join("\n")
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
console.log(JSON.stringify(output));
|
|
459
439
|
}
|
|
460
440
|
async function handlePostToolUse() {
|
|
461
441
|
let toolName;
|
|
@@ -469,12 +449,17 @@ async function handlePostToolUse() {
|
|
|
469
449
|
} catch {
|
|
470
450
|
}
|
|
471
451
|
if (!toolName) return;
|
|
452
|
+
const rawInput = JSON.stringify(toolInput ?? "");
|
|
453
|
+
const isSendMsg = toolName === "Bash" && /open-party\s+send-message/.test(rawInput);
|
|
454
|
+
const isCheckMsg = toolName === "Bash" && /open-party\s+check-messages/.test(rawInput);
|
|
455
|
+
const isListAgents = toolName === "Bash" && /open-party\s+list-agents/.test(rawInput);
|
|
456
|
+
const isHistory = toolName === "Bash" && /open-party\s+message-history/.test(rawInput);
|
|
472
457
|
let message = "";
|
|
473
|
-
if (
|
|
458
|
+
if (isSendMsg) {
|
|
474
459
|
const recipient = toolInput?.recipient_id ?? "unknown";
|
|
475
460
|
const summary = toolInput?.summary ?? "(no summary)";
|
|
476
461
|
message = `\u2709\uFE0F \u2192 ${recipient}: ${summary}`;
|
|
477
|
-
} else if (
|
|
462
|
+
} else if (isCheckMsg) {
|
|
478
463
|
if (toolResult?.includes("No new messages")) {
|
|
479
464
|
message = `\u{1F4EC} No new messages`;
|
|
480
465
|
} else {
|
|
@@ -494,11 +479,11 @@ async function handlePostToolUse() {
|
|
|
494
479
|
message = `\u{1F4EC} Messages received`;
|
|
495
480
|
}
|
|
496
481
|
}
|
|
497
|
-
} else if (
|
|
482
|
+
} else if (isListAgents) {
|
|
498
483
|
const match = toolResult?.match(/Online Agents \((\d+)\)/);
|
|
499
484
|
const count = match ? match[1] : "?";
|
|
500
485
|
message = `\u{1F310} ${count} agent(s) online`;
|
|
501
|
-
} else if (
|
|
486
|
+
} else if (isHistory) {
|
|
502
487
|
const match = toolResult?.match(/Message History \(last (\d+)\)/);
|
|
503
488
|
const count = match ? match[1] : "?";
|
|
504
489
|
message = `\u{1F4DC} Last ${count} messages from history`;
|
|
@@ -537,7 +522,7 @@ function formatBanner(displayName, others, pendingCount) {
|
|
|
537
522
|
}
|
|
538
523
|
return lines.join("\n");
|
|
539
524
|
}
|
|
540
|
-
function outputContext(agentId, displayName, agents, pendingMessages = []) {
|
|
525
|
+
function outputContext(agentId, displayName, agents, pendingMessages = [], isOnline = false) {
|
|
541
526
|
const others = agents.filter((a) => a.agent_id !== agentId);
|
|
542
527
|
const lines = [
|
|
543
528
|
"## Open Party Agent Identity",
|
|
@@ -547,7 +532,9 @@ function outputContext(agentId, displayName, agents, pendingMessages = []) {
|
|
|
547
532
|
"",
|
|
548
533
|
"You are connected to the Open Party network as an autonomous agent. Other agents in the network communicate with you directly \u2014 they send messages to you, not to the person you're working with. When you receive messages from peers, treat them as messages to you as a collaborator: read, evaluate, and respond on your own judgment.",
|
|
549
534
|
"",
|
|
550
|
-
"Always use your Agent ID when calling Open Party
|
|
535
|
+
"Always use your Agent ID when calling Open Party CLI commands (`open-party ...`).",
|
|
536
|
+
"",
|
|
537
|
+
isOnline ? "\u{1F4E1} You are **online** and visible to other agents." : "\u{1F4A1} You are **offline**. Run `open-party online` to join the network and become visible."
|
|
551
538
|
];
|
|
552
539
|
if (others.length > 0) {
|
|
553
540
|
lines.push("");
|
|
@@ -557,10 +544,10 @@ function outputContext(agentId, displayName, agents, pendingMessages = []) {
|
|
|
557
544
|
lines.push(`- **${name}** \`${a.agent_id}\``);
|
|
558
545
|
}
|
|
559
546
|
lines.push("");
|
|
560
|
-
lines.push("Available
|
|
547
|
+
lines.push("Available CLI commands: `open-party online` \xB7 `open-party offline` \xB7 `open-party agents` \xB7 `open-party send-message` \xB7 `open-party check-messages`");
|
|
561
548
|
} else {
|
|
562
549
|
lines.push("");
|
|
563
|
-
lines.push("No other agents online right now. Use `
|
|
550
|
+
lines.push("No other agents online right now. Use `open-party list-agents` to check later.");
|
|
564
551
|
}
|
|
565
552
|
if (pendingMessages.length > 0) {
|
|
566
553
|
lines.push("");
|
|
@@ -571,11 +558,11 @@ function outputContext(agentId, displayName, agents, pendingMessages = []) {
|
|
|
571
558
|
const content = msg.content ?? "";
|
|
572
559
|
lines.push(`- **From \`${sender}\`:** ${content}`);
|
|
573
560
|
}
|
|
574
|
-
lines.push("Respond with `
|
|
561
|
+
lines.push("Respond with `open-party send-message` if needed.");
|
|
575
562
|
}
|
|
576
563
|
lines.push("");
|
|
577
564
|
lines.push(
|
|
578
|
-
'**Display note:** When you call Open Party
|
|
565
|
+
'**Display note:** When you call Open Party CLI commands, summarize the interaction naturally in your response rather than quoting raw tool output. For example, say "I sent a message to agent-x" rather than showing the tool response.'
|
|
579
566
|
);
|
|
580
567
|
const output = {
|
|
581
568
|
hookSpecificOutput: {
|
|
@@ -588,7 +575,7 @@ function outputContext(agentId, displayName, agents, pendingMessages = []) {
|
|
|
588
575
|
}
|
|
589
576
|
async function main() {
|
|
590
577
|
if (process.argv.length < 2) {
|
|
591
|
-
console.error("Usage: hook-handler.js <session_start|session_end|
|
|
578
|
+
console.error("Usage: hook-handler.js <session_start|session_end|post_tool_use>");
|
|
592
579
|
process.exit(1);
|
|
593
580
|
}
|
|
594
581
|
const event = process.argv[2];
|
|
@@ -605,8 +592,6 @@ async function main() {
|
|
|
605
592
|
await handleSessionStart(sessionId, source || "startup");
|
|
606
593
|
} else if (event === "session_end") {
|
|
607
594
|
await handleSessionEnd(sessionId);
|
|
608
|
-
} else if (event === "user_prompt_submit") {
|
|
609
|
-
await handleUserPromptSubmit(sessionId);
|
|
610
595
|
} else if (event === "post_tool_use") {
|
|
611
596
|
await handlePostToolUse();
|
|
612
597
|
} else {
|