@openvole/volenet-mcp 0.1.0 → 0.2.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/README.md +66 -2
- package/dist/index.d.ts +200 -14
- package/dist/index.js +980 -220
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
// src/config.ts
|
|
18
|
+
var config_exports = {};
|
|
19
|
+
__export(config_exports, {
|
|
20
|
+
defaultDir: () => defaultDir,
|
|
21
|
+
defaultName: () => defaultName,
|
|
22
|
+
loadStored: () => loadStored,
|
|
23
|
+
resolveSettings: () => resolveSettings,
|
|
24
|
+
saveStored: () => saveStored,
|
|
25
|
+
sessionKey: () => sessionKey
|
|
26
|
+
});
|
|
27
|
+
import * as crypto from "crypto";
|
|
13
28
|
import * as fs from "fs/promises";
|
|
14
29
|
import * as os from "os";
|
|
15
30
|
import * as path from "path";
|
|
31
|
+
function sessionKey(cwd = process.cwd()) {
|
|
32
|
+
const hash = crypto.createHash("sha256").update(cwd).digest("hex").slice(0, 8);
|
|
33
|
+
const base = (cwd.split("/").filter(Boolean).pop() ?? "session").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").slice(0, 40);
|
|
34
|
+
return `${base}-${hash}`;
|
|
35
|
+
}
|
|
16
36
|
function defaultDir() {
|
|
17
37
|
return process.env.VOLENET_MCP_DIR?.trim() || path.join(os.homedir(), ".openvole", "volenet-mcp");
|
|
18
38
|
}
|
|
19
39
|
function defaultName() {
|
|
20
40
|
return `claude-${os.hostname().split(".")[0].toLowerCase()}`;
|
|
21
41
|
}
|
|
22
|
-
var file = (dir) => path.join(dir, "config.json");
|
|
23
42
|
async function loadStored(dir) {
|
|
24
43
|
try {
|
|
25
44
|
const raw = JSON.parse(await fs.readFile(file(dir), "utf-8"));
|
|
@@ -46,92 +65,645 @@ async function resolveSettings() {
|
|
|
46
65
|
name: process.env.VOLENET_MCP_NAME?.trim() || stored.name || defaultName(),
|
|
47
66
|
hub: process.env.VOLENET_MCP_HUB?.trim() || stored.hub || void 0,
|
|
48
67
|
dir,
|
|
49
|
-
port: (Number.isFinite(envPort) && envPort > 0 ? envPort : stored.port) || 9750
|
|
68
|
+
port: (Number.isFinite(envPort) && envPort > 0 ? envPort : stored.port) || 9750,
|
|
69
|
+
session: process.env.VOLENET_MCP_SESSION?.trim() || sessionKey()
|
|
50
70
|
};
|
|
51
71
|
}
|
|
72
|
+
var file;
|
|
73
|
+
var init_config = __esm({
|
|
74
|
+
"src/config.ts"() {
|
|
75
|
+
"use strict";
|
|
76
|
+
file = (dir) => path.join(dir, "config.json");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
52
79
|
|
|
53
80
|
// src/inbox.ts
|
|
54
81
|
import * as fs2 from "fs/promises";
|
|
55
82
|
import * as path2 from "path";
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
async load() {
|
|
83
|
+
async function readLog(file2) {
|
|
84
|
+
let body;
|
|
85
|
+
try {
|
|
86
|
+
body = await fs2.readFile(file2, "utf-8");
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const line of body.split("\n")) {
|
|
92
|
+
if (!line.trim()) continue;
|
|
67
93
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
const m = JSON.parse(line);
|
|
95
|
+
if (m && typeof m.peerId === "string" && typeof m.text === "string") out.push(m);
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
var MAX_MESSAGES, Inbox;
|
|
102
|
+
var init_inbox = __esm({
|
|
103
|
+
"src/inbox.ts"() {
|
|
104
|
+
"use strict";
|
|
105
|
+
MAX_MESSAGES = 2e3;
|
|
106
|
+
Inbox = class {
|
|
107
|
+
/**
|
|
108
|
+
* @param dir where the shared log and the cursors live
|
|
109
|
+
* @param session which read state is ours. Sessions in different projects are different
|
|
110
|
+
* readers; the same project reopened is the same reader, so restarting does
|
|
111
|
+
* not replay everything already seen.
|
|
112
|
+
*/
|
|
113
|
+
constructor(dir, session = "default") {
|
|
114
|
+
this.dir = dir;
|
|
115
|
+
this.session = session;
|
|
116
|
+
}
|
|
117
|
+
dir;
|
|
118
|
+
session;
|
|
119
|
+
messages = [];
|
|
120
|
+
/** Per peer, the timestamp up to which *this* session has been shown its messages. */
|
|
121
|
+
readAt = /* @__PURE__ */ new Map();
|
|
122
|
+
writing = Promise.resolve();
|
|
123
|
+
get log() {
|
|
124
|
+
return path2.join(this.dir, "messages.jsonl");
|
|
125
|
+
}
|
|
126
|
+
get cursor() {
|
|
127
|
+
return path2.join(this.dir, "cursors", `${this.session}.json`);
|
|
128
|
+
}
|
|
129
|
+
async load() {
|
|
130
|
+
await this.adoptLegacy();
|
|
131
|
+
this.messages = await readLog(this.log);
|
|
132
|
+
try {
|
|
133
|
+
const raw = JSON.parse(await fs2.readFile(this.cursor, "utf-8"));
|
|
134
|
+
this.readAt = new Map(Object.entries(raw ?? {}));
|
|
135
|
+
} catch {
|
|
136
|
+
this.readAt = /* @__PURE__ */ new Map();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Re-read what other sessions have appended since we loaded. */
|
|
140
|
+
async refresh() {
|
|
141
|
+
this.messages = await readLog(this.log);
|
|
142
|
+
}
|
|
143
|
+
/** Record a message. Returns false when this id was already recorded. */
|
|
144
|
+
async add(m) {
|
|
145
|
+
await this.refresh();
|
|
146
|
+
if (this.messages.some((x) => x.id === m.id)) return false;
|
|
147
|
+
this.messages.push(m);
|
|
148
|
+
await this.append(m);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/** Everything with one peer, oldest first. */
|
|
152
|
+
history(peerId, limit = 50) {
|
|
153
|
+
return this.messages.filter((m) => m.peerId === peerId).slice(-limit);
|
|
154
|
+
}
|
|
155
|
+
/** Inbound messages this session has not been shown yet, oldest first. */
|
|
156
|
+
unread() {
|
|
157
|
+
return this.messages.filter((m) => m.dir === "in" && m.ts > (this.readAt.get(m.peerId) ?? 0));
|
|
158
|
+
}
|
|
159
|
+
/** Mark everything currently unread as seen — for this session, and nobody else. */
|
|
160
|
+
async markRead() {
|
|
161
|
+
for (const m of this.unread()) {
|
|
162
|
+
const at = this.readAt.get(m.peerId) ?? 0;
|
|
163
|
+
if (m.ts > at) this.readAt.set(m.peerId, m.ts);
|
|
164
|
+
}
|
|
165
|
+
await this.persistCursor();
|
|
166
|
+
}
|
|
167
|
+
/** Every peer we have said anything to or heard anything from, most recent first. */
|
|
168
|
+
peers() {
|
|
169
|
+
const by = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const m of this.messages) {
|
|
171
|
+
const e = by.get(m.peerId) ?? { peerId: m.peerId, peerName: m.peerName, last: 0, unread: 0 };
|
|
172
|
+
if (m.peerName) e.peerName = m.peerName;
|
|
173
|
+
e.last = Math.max(e.last, m.ts);
|
|
174
|
+
if (m.dir === "in" && m.ts > (this.readAt.get(m.peerId) ?? 0)) e.unread++;
|
|
175
|
+
by.set(m.peerId, e);
|
|
176
|
+
}
|
|
177
|
+
return [...by.values()].sort((a, b) => b.last - a.last);
|
|
178
|
+
}
|
|
179
|
+
get size() {
|
|
180
|
+
return this.messages.length;
|
|
181
|
+
}
|
|
182
|
+
/** One line, one write — an append no other session can lose. */
|
|
183
|
+
append(m) {
|
|
184
|
+
this.writing = this.writing.then(async () => {
|
|
185
|
+
await fs2.mkdir(this.dir, { recursive: true });
|
|
186
|
+
await fs2.appendFile(this.log, `${JSON.stringify(m)}
|
|
187
|
+
`, "utf-8");
|
|
188
|
+
if (this.messages.length > MAX_MESSAGES) await this.compact();
|
|
189
|
+
});
|
|
190
|
+
return this.writing;
|
|
191
|
+
}
|
|
192
|
+
/** Rewrite the log with the newest MAX_MESSAGES. Rare, and atomic via rename. */
|
|
193
|
+
async compact() {
|
|
194
|
+
const keep = this.messages.slice(-MAX_MESSAGES);
|
|
195
|
+
const tmp = `${this.log}.${process.pid}.tmp`;
|
|
196
|
+
await fs2.writeFile(tmp, keep.map((m) => `${JSON.stringify(m)}
|
|
197
|
+
`).join(""), "utf-8");
|
|
198
|
+
await fs2.rename(tmp, this.log);
|
|
199
|
+
this.messages = keep;
|
|
200
|
+
}
|
|
201
|
+
async persistCursor() {
|
|
202
|
+
await fs2.mkdir(path2.dirname(this.cursor), { recursive: true });
|
|
203
|
+
const tmp = `${this.cursor}.tmp`;
|
|
204
|
+
await fs2.writeFile(tmp, JSON.stringify(Object.fromEntries(this.readAt), null, 2), "utf-8");
|
|
205
|
+
await fs2.rename(tmp, this.cursor);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Carry over messages written before the log existed.
|
|
209
|
+
*
|
|
210
|
+
* Earlier versions kept one `inbox.json` holding both the messages and a single read state. The
|
|
211
|
+
* messages are still someone's; dropping them on upgrade would lose real conversations. The old
|
|
212
|
+
* read state is deliberately *not* carried over — it was one cursor for every session, so honouring
|
|
213
|
+
* it would mark messages seen for sessions that never saw them. Unread is the safe direction.
|
|
214
|
+
*/
|
|
215
|
+
async adoptLegacy() {
|
|
216
|
+
const legacy = path2.join(this.dir, "inbox.json");
|
|
217
|
+
try {
|
|
218
|
+
await fs2.access(this.log);
|
|
219
|
+
return;
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
let raw;
|
|
223
|
+
try {
|
|
224
|
+
raw = JSON.parse(await fs2.readFile(legacy, "utf-8"));
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const messages = (raw.messages ?? []).filter(
|
|
229
|
+
(m) => m && typeof m.peerId === "string" && typeof m.text === "string"
|
|
230
|
+
);
|
|
231
|
+
if (messages.length === 0) return;
|
|
232
|
+
await fs2.mkdir(this.dir, { recursive: true });
|
|
233
|
+
await fs2.writeFile(this.log, messages.map((m) => `${JSON.stringify(m)}
|
|
234
|
+
`).join(""), "utf-8");
|
|
235
|
+
await fs2.rename(legacy, `${legacy}.migrated`);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// src/net-api.ts
|
|
242
|
+
function localNet(m) {
|
|
243
|
+
return {
|
|
244
|
+
async identity() {
|
|
245
|
+
const k = m.getKeyPair();
|
|
246
|
+
return k ? { instanceId: k.instanceId, publicKeyString: k.publicKeyString } : null;
|
|
247
|
+
},
|
|
248
|
+
async instances() {
|
|
249
|
+
const live = new Set(
|
|
250
|
+
(m.getTransport()?.getPeers() ?? []).filter((p) => p.connected).map((p) => p.peerId)
|
|
71
251
|
);
|
|
72
|
-
|
|
252
|
+
return m.getInstances().map((i) => ({ id: i.id, name: i.name, connected: live.has(i.id) }));
|
|
253
|
+
},
|
|
254
|
+
async relayMembers() {
|
|
255
|
+
return m.getRelayMembers().map((r) => ({
|
|
256
|
+
id: r.id,
|
|
257
|
+
name: r.name,
|
|
258
|
+
viaHubName: r.viaHubName,
|
|
259
|
+
connected: r.connected,
|
|
260
|
+
accepted: r.accepted,
|
|
261
|
+
incoming: r.incoming,
|
|
262
|
+
awaiting: r.awaiting
|
|
263
|
+
}));
|
|
264
|
+
},
|
|
265
|
+
sendChat: (to, text) => m.sendChat(to, text),
|
|
266
|
+
async askBrain(to, input, fromName, timeoutMs) {
|
|
267
|
+
const mgr = m.getRemoteTaskManager();
|
|
268
|
+
if (!mgr) return { status: "failed", error: "remote task manager not available" };
|
|
269
|
+
const r = await mgr.delegateTask(to, { taskId: "", input, fromName }, timeoutMs);
|
|
270
|
+
return { status: r.status, result: r.result, error: r.error };
|
|
271
|
+
},
|
|
272
|
+
joinHub: (url) => m.initiateJoin(url),
|
|
273
|
+
addPeer: (url) => m.addPeer(url),
|
|
274
|
+
async forgetPeer(url) {
|
|
275
|
+
return m.forgetPeer(url);
|
|
276
|
+
},
|
|
277
|
+
probePair: (url) => m.probePair(url),
|
|
278
|
+
initiatePair: (url, publicKey, note, wants) => m.initiatePair(url, publicKey, note, wants),
|
|
279
|
+
requestRelayConnect: (ref, note) => m.requestRelayConnect(ref, note),
|
|
280
|
+
approveRelayConnect: (ref) => m.approveRelayConnect(ref),
|
|
281
|
+
denyRelayConnect: (ref) => m.denyRelayConnect(ref),
|
|
282
|
+
async listPairRequests() {
|
|
283
|
+
return m.listPairRequests().map((r) => ({
|
|
284
|
+
id: r.id,
|
|
285
|
+
name: r.name,
|
|
286
|
+
note: r.note,
|
|
287
|
+
wants: r.wants
|
|
288
|
+
}));
|
|
289
|
+
},
|
|
290
|
+
async rooms() {
|
|
291
|
+
return m.getRooms().map((r) => ({
|
|
292
|
+
room: r.room,
|
|
293
|
+
name: r.name,
|
|
294
|
+
topic: r.topic,
|
|
295
|
+
members: r.members.map((x) => ({ instanceId: x.instanceId, name: x.name }))
|
|
296
|
+
}));
|
|
297
|
+
},
|
|
298
|
+
roomCommand: (hub, type, payload) => m.roomCommand(hub, type, payload),
|
|
299
|
+
postToRoom: (room, text) => m.postToRoom(room, text),
|
|
300
|
+
acceptPair: (ref, grant) => m.acceptPair(ref, grant),
|
|
301
|
+
denyPair: (ref) => m.denyPair(ref)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
var NET_METHODS;
|
|
305
|
+
var init_net_api = __esm({
|
|
306
|
+
"src/net-api.ts"() {
|
|
307
|
+
"use strict";
|
|
308
|
+
NET_METHODS = [
|
|
309
|
+
"identity",
|
|
310
|
+
"instances",
|
|
311
|
+
"relayMembers",
|
|
312
|
+
"sendChat",
|
|
313
|
+
"askBrain",
|
|
314
|
+
"joinHub",
|
|
315
|
+
"addPeer",
|
|
316
|
+
"forgetPeer",
|
|
317
|
+
"probePair",
|
|
318
|
+
"initiatePair",
|
|
319
|
+
"requestRelayConnect",
|
|
320
|
+
"approveRelayConnect",
|
|
321
|
+
"denyRelayConnect",
|
|
322
|
+
"rooms",
|
|
323
|
+
"roomCommand",
|
|
324
|
+
"postToRoom",
|
|
325
|
+
"listPairRequests",
|
|
326
|
+
"acceptPair",
|
|
327
|
+
"denyPair"
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// src/daemon.ts
|
|
333
|
+
import { spawn } from "child_process";
|
|
334
|
+
import * as fs3 from "fs/promises";
|
|
335
|
+
import * as net from "net";
|
|
336
|
+
import * as path4 from "path";
|
|
337
|
+
async function serve(dir, node) {
|
|
338
|
+
const sock = socketPath(dir);
|
|
339
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
340
|
+
await fs3.rm(sock, { force: true });
|
|
341
|
+
const server = net.createServer((conn) => {
|
|
342
|
+
let buffer = "";
|
|
343
|
+
conn.setEncoding("utf-8");
|
|
344
|
+
conn.on("data", (chunk) => {
|
|
345
|
+
buffer += chunk;
|
|
346
|
+
for (let nl = buffer.indexOf("\n"); nl >= 0; nl = buffer.indexOf("\n")) {
|
|
347
|
+
const line = buffer.slice(0, nl);
|
|
348
|
+
buffer = buffer.slice(nl + 1);
|
|
349
|
+
if (line.trim()) void handle(line, conn, node);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
conn.on("error", () => void 0);
|
|
353
|
+
});
|
|
354
|
+
server.on("error", () => void 0);
|
|
355
|
+
await new Promise((done) => server.listen(sock, done));
|
|
356
|
+
return server;
|
|
357
|
+
}
|
|
358
|
+
async function handle(line, conn, node) {
|
|
359
|
+
let req;
|
|
360
|
+
try {
|
|
361
|
+
req = JSON.parse(line);
|
|
362
|
+
} catch {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const reply = (r) => {
|
|
366
|
+
try {
|
|
367
|
+
conn.write(`${JSON.stringify({ id: req.id, ...r })}
|
|
368
|
+
`);
|
|
73
369
|
} catch {
|
|
74
|
-
this.messages = [];
|
|
75
|
-
this.readAt = /* @__PURE__ */ new Map();
|
|
76
370
|
}
|
|
371
|
+
};
|
|
372
|
+
if (!NET_METHODS.includes(req.method)) {
|
|
373
|
+
reply({ ok: false, error: `unknown method: ${req.method}` });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const fn = node[req.method];
|
|
378
|
+
reply({ ok: true, result: await fn(...req.args ?? []) });
|
|
379
|
+
} catch (err) {
|
|
380
|
+
reply({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
77
381
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
382
|
+
}
|
|
383
|
+
function remoteNet(conn) {
|
|
384
|
+
let next = 1;
|
|
385
|
+
const pending = /* @__PURE__ */ new Map();
|
|
386
|
+
let buffer = "";
|
|
387
|
+
conn.setEncoding("utf-8");
|
|
388
|
+
conn.on("data", (chunk) => {
|
|
389
|
+
buffer += chunk;
|
|
390
|
+
for (let nl = buffer.indexOf("\n"); nl >= 0; nl = buffer.indexOf("\n")) {
|
|
391
|
+
const line = buffer.slice(0, nl);
|
|
392
|
+
buffer = buffer.slice(nl + 1);
|
|
393
|
+
if (!line.trim()) continue;
|
|
394
|
+
try {
|
|
395
|
+
const res = JSON.parse(line);
|
|
396
|
+
const waiter = pending.get(res.id);
|
|
397
|
+
if (!waiter) continue;
|
|
398
|
+
pending.delete(res.id);
|
|
399
|
+
if (res.ok) waiter.resolve(res.result);
|
|
400
|
+
else waiter.reject(new Error(res.error ?? "daemon error"));
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
84
403
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
404
|
+
});
|
|
405
|
+
const fail = (why) => {
|
|
406
|
+
for (const [, w] of pending) w.reject(new Error(why));
|
|
407
|
+
pending.clear();
|
|
408
|
+
};
|
|
409
|
+
conn.on("close", () => fail("the volenet daemon closed the connection"));
|
|
410
|
+
conn.on("error", (e) => fail(e.message));
|
|
411
|
+
const call = (method, ...args) => new Promise((resolve2, reject) => {
|
|
412
|
+
const id = next++;
|
|
413
|
+
pending.set(id, { resolve: resolve2, reject });
|
|
414
|
+
conn.write(`${JSON.stringify({ id, method, args })}
|
|
415
|
+
`);
|
|
416
|
+
});
|
|
417
|
+
return Object.fromEntries(
|
|
418
|
+
NET_METHODS.map((m) => [m, (...args) => call(m, ...args)])
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
async function connect(dir) {
|
|
422
|
+
return new Promise((resolve2) => {
|
|
423
|
+
const conn = net.createConnection(socketPath(dir));
|
|
424
|
+
const give = (ok) => {
|
|
425
|
+
conn.removeAllListeners("connect");
|
|
426
|
+
conn.removeAllListeners("error");
|
|
427
|
+
if (ok) resolve2(conn);
|
|
428
|
+
else {
|
|
429
|
+
conn.destroy();
|
|
430
|
+
resolve2(null);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
conn.once("connect", () => give(true));
|
|
434
|
+
conn.once("error", () => give(false));
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async function spawnDaemon(dir, env = {}) {
|
|
438
|
+
const entry = process.argv[1];
|
|
439
|
+
if (!entry) return null;
|
|
440
|
+
const child = spawn(process.execPath, [entry, "daemon"], {
|
|
441
|
+
detached: true,
|
|
442
|
+
stdio: "ignore",
|
|
443
|
+
env: { ...process.env, ...env, VOLENET_MCP_DIR: dir }
|
|
444
|
+
});
|
|
445
|
+
child.unref();
|
|
446
|
+
for (let i = 0; i < 40; i++) {
|
|
447
|
+
const conn = await connect(dir);
|
|
448
|
+
if (conn) return conn;
|
|
449
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
var socketPath;
|
|
454
|
+
var init_daemon = __esm({
|
|
455
|
+
"src/daemon.ts"() {
|
|
456
|
+
"use strict";
|
|
457
|
+
init_net_api();
|
|
458
|
+
socketPath = (dir) => path4.join(dir, "daemon.sock");
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// src/notify.ts
|
|
463
|
+
import { spawn as spawn2 } from "child_process";
|
|
464
|
+
function run(command, args) {
|
|
465
|
+
try {
|
|
466
|
+
const child = spawn2(command, args, { stdio: "ignore", detached: true });
|
|
467
|
+
child.on("error", () => void 0);
|
|
468
|
+
child.unref();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function notifier(platform = process.platform) {
|
|
473
|
+
const setting = process.env.VOLENET_MCP_NOTIFY?.trim();
|
|
474
|
+
if (setting === "off") return () => void 0;
|
|
475
|
+
if (setting) return (title, body) => run(setting, [title, body]);
|
|
476
|
+
if (platform === "darwin") {
|
|
477
|
+
return (title, body) => run("osascript", [
|
|
478
|
+
"-e",
|
|
479
|
+
`display notification "${applescript(body)}" with title "${applescript(title)}"`
|
|
480
|
+
]);
|
|
481
|
+
}
|
|
482
|
+
if (platform === "linux") return (title, body) => run("notify-send", [title, body]);
|
|
483
|
+
if (platform === "win32") {
|
|
484
|
+
return (title, body) => run("powershell", [
|
|
485
|
+
"-NoProfile",
|
|
486
|
+
"-Command",
|
|
487
|
+
`[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms');$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;$n.ShowBalloonTip(5000,'${title.replace(/'/g, "''")}','${body.replace(/'/g, "''")}',0)`
|
|
488
|
+
]);
|
|
489
|
+
}
|
|
490
|
+
return () => void 0;
|
|
491
|
+
}
|
|
492
|
+
function preview(text, limit = 140) {
|
|
493
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
494
|
+
return flat.length > limit ? `${flat.slice(0, limit - 1)}\u2026` : flat;
|
|
495
|
+
}
|
|
496
|
+
var applescript;
|
|
497
|
+
var init_notify = __esm({
|
|
498
|
+
"src/notify.ts"() {
|
|
499
|
+
"use strict";
|
|
500
|
+
applescript = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// src/node.ts
|
|
505
|
+
var node_exports = {};
|
|
506
|
+
__export(node_exports, {
|
|
507
|
+
alreadyTrusts: () => alreadyTrusts,
|
|
508
|
+
resolveSettings: () => resolveSettings,
|
|
509
|
+
runDaemon: () => runDaemon,
|
|
510
|
+
startLocal: () => startLocal,
|
|
511
|
+
startNode: () => startNode
|
|
512
|
+
});
|
|
513
|
+
import * as fsSync from "fs";
|
|
514
|
+
import * as path5 from "path";
|
|
515
|
+
import {
|
|
516
|
+
VoleNetManager,
|
|
517
|
+
createEventBus,
|
|
518
|
+
loadAuthorizedVoles,
|
|
519
|
+
parsePublicKey
|
|
520
|
+
} from "@openvole/volenet";
|
|
521
|
+
async function startNode(options) {
|
|
522
|
+
const inbox = new Inbox(options.dir, options.session);
|
|
523
|
+
await inbox.load();
|
|
524
|
+
if (process.env.VOLENET_MCP_NO_DAEMON !== "1") {
|
|
525
|
+
const conn = await connect(options.dir) ?? await spawnDaemon(options.dir);
|
|
526
|
+
if (conn) {
|
|
527
|
+
return {
|
|
528
|
+
net: remoteNet(conn),
|
|
529
|
+
inbox,
|
|
530
|
+
requests: [],
|
|
531
|
+
notices: [],
|
|
532
|
+
options,
|
|
533
|
+
hubStatus: options.hub ? `joined ${options.hub}` : "no hub configured",
|
|
534
|
+
canSample: false,
|
|
535
|
+
where: "daemon",
|
|
536
|
+
onMessage: watchLog(options.dir, inbox),
|
|
537
|
+
stop: async () => {
|
|
538
|
+
conn.destroy();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
101
541
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
542
|
+
}
|
|
543
|
+
const local = await startLocal(options, inbox);
|
|
544
|
+
return { ...local, where: "in-process" };
|
|
545
|
+
}
|
|
546
|
+
async function startLocal(options, inbox, notify) {
|
|
547
|
+
const bus = createEventBus();
|
|
548
|
+
const requests = [];
|
|
549
|
+
const notices = [];
|
|
550
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
551
|
+
const port = await isFree(options.port) ? options.port : 0;
|
|
552
|
+
const manager = new VoleNetManager(
|
|
553
|
+
{
|
|
554
|
+
enabled: true,
|
|
555
|
+
instanceName: options.name,
|
|
556
|
+
role: "peer",
|
|
557
|
+
port,
|
|
558
|
+
keyPath: path5.join(options.dir, "net", "vole_key"),
|
|
559
|
+
// A hub is a peer we dial. 'read' rather than 'full': a hub carries our sealed traffic,
|
|
560
|
+
// it has no business acting on this node.
|
|
561
|
+
peers: options.hub ? [{ url: options.hub, trust: "read" }] : []
|
|
562
|
+
},
|
|
563
|
+
options.dir
|
|
564
|
+
);
|
|
565
|
+
bus.on("volenet:chat", (d) => {
|
|
566
|
+
const m = d;
|
|
567
|
+
const message = {
|
|
568
|
+
peerId: m.from,
|
|
569
|
+
peerName: m.fromName,
|
|
570
|
+
dir: "in",
|
|
571
|
+
text: m.text,
|
|
572
|
+
ts: m.timestamp,
|
|
573
|
+
id: m.messageId
|
|
574
|
+
};
|
|
575
|
+
void inbox.add(message).then((added) => {
|
|
576
|
+
if (!added) return;
|
|
577
|
+
for (const fn of listeners) fn(message);
|
|
578
|
+
notify?.(`${message.peerName} on VoleNet`, preview(message.text));
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
bus.on("volenet:chat:pending", (d) => {
|
|
582
|
+
const p = d;
|
|
583
|
+
for (const n of p.from ?? []) {
|
|
584
|
+
const at = notices.findIndex((x) => x.from === n.from);
|
|
585
|
+
if (at >= 0) notices[at] = n;
|
|
586
|
+
else notices.push(n);
|
|
113
587
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
588
|
+
});
|
|
589
|
+
const remember = (kind) => (d) => {
|
|
590
|
+
const r = d;
|
|
591
|
+
if (requests.some((x) => x.from === r.from && x.kind === kind)) return;
|
|
592
|
+
requests.push({ kind, from: r.from, fromName: r.fromName, note: r.note, at: Date.now() });
|
|
593
|
+
};
|
|
594
|
+
bus.on("volenet:pair:request", remember("pair"));
|
|
595
|
+
bus.on("volenet:relay:request", remember("relay"));
|
|
596
|
+
await manager.start(void 0, bus);
|
|
597
|
+
const bound = manager.getTransport()?.getPort?.() ?? port;
|
|
598
|
+
const settings = { ...options, port: bound || options.port };
|
|
599
|
+
let hubStatus = "no hub configured";
|
|
600
|
+
if (options.hub) {
|
|
601
|
+
hubStatus = await alreadyTrusts(options.dir, options.hub) ? `joined ${options.hub}` : await join5(manager, options.hub);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
net: localNet(manager),
|
|
605
|
+
inbox,
|
|
606
|
+
requests,
|
|
607
|
+
notices,
|
|
608
|
+
options: settings,
|
|
609
|
+
hubStatus,
|
|
610
|
+
canSample: false,
|
|
611
|
+
onMessage: (fn) => {
|
|
612
|
+
listeners.add(fn);
|
|
613
|
+
return () => listeners.delete(fn);
|
|
614
|
+
},
|
|
615
|
+
stop: () => manager.stop()
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
async function runDaemon(options) {
|
|
619
|
+
const inbox = new Inbox(options.dir, "daemon");
|
|
620
|
+
await inbox.load();
|
|
621
|
+
const node = await startLocal(options, inbox, notifier());
|
|
622
|
+
await serve(options.dir, node.net);
|
|
623
|
+
await new Promise(() => void 0);
|
|
624
|
+
}
|
|
625
|
+
function watchLog(dir, inbox) {
|
|
626
|
+
return (fn) => {
|
|
627
|
+
const file2 = path5.join(dir, "messages.jsonl");
|
|
628
|
+
const seen = new Set(inbox.history("", 0).map((m) => m.id));
|
|
629
|
+
let closed = false;
|
|
630
|
+
const check = async () => {
|
|
631
|
+
if (closed) return;
|
|
632
|
+
const before = new Set(inbox.unread().map((m) => m.id));
|
|
633
|
+
await inbox.refresh();
|
|
634
|
+
for (const m of inbox.unread()) {
|
|
635
|
+
if (!before.has(m.id) && !seen.has(m.id)) {
|
|
636
|
+
seen.add(m.id);
|
|
637
|
+
fn(m);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
let watcher;
|
|
642
|
+
try {
|
|
643
|
+
watcher = fsSync.watch(path5.dirname(file2), (_e, name) => {
|
|
644
|
+
if (name === "messages.jsonl") void check();
|
|
645
|
+
});
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
const timer = setInterval(() => void check(), 1e3);
|
|
649
|
+
return () => {
|
|
650
|
+
closed = true;
|
|
651
|
+
clearInterval(timer);
|
|
652
|
+
watcher?.close();
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async function join5(node, hub) {
|
|
657
|
+
const res = await node.initiateJoin(hub);
|
|
658
|
+
if (!res.ok) return `could not join ${hub}: ${res.error}`;
|
|
659
|
+
if (res.pending) return `waiting for approval at ${hub}`;
|
|
660
|
+
return `joined ${res.hubName ?? hub}`;
|
|
661
|
+
}
|
|
662
|
+
async function alreadyTrusts(dir, hub) {
|
|
663
|
+
try {
|
|
664
|
+
const r = await fetch(`${hub.replace(/\/$/, "")}/volenet/info`, {
|
|
665
|
+
signal: AbortSignal.timeout(8e3)
|
|
131
666
|
});
|
|
132
|
-
|
|
667
|
+
const info = await r.json();
|
|
668
|
+
const parsed = info.publicKey ? parsePublicKey(info.publicKey) : null;
|
|
669
|
+
if (!parsed) return false;
|
|
670
|
+
return (await loadAuthorizedVoles(path5.join(dir, "net"))).has(parsed.instanceId);
|
|
671
|
+
} catch {
|
|
672
|
+
return false;
|
|
133
673
|
}
|
|
134
|
-
}
|
|
674
|
+
}
|
|
675
|
+
async function isFree(port) {
|
|
676
|
+
const netmod = await import("net");
|
|
677
|
+
return new Promise((resolve2) => {
|
|
678
|
+
const probe = netmod.createServer().once("error", () => resolve2(false)).once("listening", () => probe.close(() => resolve2(true))).listen(port, "0.0.0.0");
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
var init_node = __esm({
|
|
682
|
+
"src/node.ts"() {
|
|
683
|
+
"use strict";
|
|
684
|
+
init_config();
|
|
685
|
+
init_daemon();
|
|
686
|
+
init_inbox();
|
|
687
|
+
init_net_api();
|
|
688
|
+
init_notify();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// src/index.ts
|
|
693
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
694
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
695
|
+
import {
|
|
696
|
+
CallToolRequestSchema,
|
|
697
|
+
GetPromptRequestSchema,
|
|
698
|
+
ListPromptsRequestSchema,
|
|
699
|
+
ListToolsRequestSchema
|
|
700
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
701
|
+
|
|
702
|
+
// src/cli.ts
|
|
703
|
+
init_config();
|
|
704
|
+
init_inbox();
|
|
705
|
+
import * as path6 from "path";
|
|
706
|
+
import { loadKeyPair } from "@openvole/volenet";
|
|
135
707
|
|
|
136
708
|
// src/install.ts
|
|
137
709
|
import { spawnSync } from "child_process";
|
|
@@ -147,15 +719,15 @@ function addArgs(scope, command) {
|
|
|
147
719
|
}
|
|
148
720
|
var NEXT_STEPS = '\nRestart Claude Code \u2014 MCP servers load at startup \u2014 then:\n\n volenet_whoami who you are on the mesh (an identity is made on first run)\n volenet_hub url:"..." join a hub, to be reachable from anywhere\n volenet_connect url:"..." or pair directly with an agent you can dial\n\nNothing else needs configuring.\n';
|
|
149
721
|
function install(argv, out = process.stdout) {
|
|
150
|
-
const scope = argv.includes("--
|
|
722
|
+
const scope = argv.includes("--local") ? "local" : "user";
|
|
151
723
|
const command = launchCommand();
|
|
152
724
|
const paste = `claude mcp add ${SERVER_NAME} -s ${scope} -- ${command.join(" ")}`;
|
|
153
|
-
const
|
|
725
|
+
const run3 = (args) => spawnSync("claude", args, {
|
|
154
726
|
stdio: ["ignore", "pipe", "pipe"],
|
|
155
727
|
encoding: "utf-8",
|
|
156
728
|
timeout: 3e4
|
|
157
729
|
});
|
|
158
|
-
const listed =
|
|
730
|
+
const listed = run3(["mcp", "list"]);
|
|
159
731
|
if (listed.error) {
|
|
160
732
|
out.write(
|
|
161
733
|
`The \`claude\` CLI is not on PATH. Run this once, in the project you want it in:
|
|
@@ -171,7 +743,7 @@ function install(argv, out = process.stdout) {
|
|
|
171
743
|
${NEXT_STEPS}`);
|
|
172
744
|
return 0;
|
|
173
745
|
}
|
|
174
|
-
const added =
|
|
746
|
+
const added = run3(addArgs(scope, command));
|
|
175
747
|
if (added.status !== 0) {
|
|
176
748
|
out.write(
|
|
177
749
|
`Could not register it automatically${added.stderr ? `: ${added.stderr.trim()}` : ""}
|
|
@@ -184,25 +756,30 @@ Run this once instead:
|
|
|
184
756
|
);
|
|
185
757
|
return added.status ?? 1;
|
|
186
758
|
}
|
|
187
|
-
out.write(
|
|
188
|
-
${
|
|
759
|
+
out.write(
|
|
760
|
+
`Registered ${SERVER_NAME} (${scope} scope${scope === "user" ? " \u2014 available in every project" : ", this project only"}).
|
|
761
|
+
${NEXT_STEPS}`
|
|
762
|
+
);
|
|
189
763
|
return 0;
|
|
190
764
|
}
|
|
191
765
|
|
|
192
766
|
// src/cli.ts
|
|
193
767
|
var USAGE = `volenet-mcp \u2014 VoleNet as an MCP server
|
|
194
768
|
|
|
195
|
-
volenet-mcp install [--
|
|
769
|
+
volenet-mcp install [--local] register with Claude Code (default: every project)
|
|
196
770
|
volenet-mcp whoami this machine's identity on the mesh
|
|
771
|
+
volenet-mcp daemon run the node in the foreground (normally started for you)
|
|
197
772
|
volenet-mcp hub [url|--leave] which hub to use; takes effect on the next session
|
|
198
|
-
volenet-mcp inbox [--read]
|
|
773
|
+
volenet-mcp inbox [--read] [--quiet]
|
|
774
|
+
messages waiting. --read marks them seen, --quiet says
|
|
775
|
+
nothing when there are none (for hooks)
|
|
199
776
|
|
|
200
777
|
With no command it runs as the MCP server itself, over stdio, which is how Claude Code starts it.
|
|
201
778
|
Anything needing a live node \u2014 peers, pairing, asking an agent's brain \u2014 is a tool you ask for in
|
|
202
779
|
a session, not a command here.
|
|
203
780
|
`;
|
|
204
781
|
var when = (ts) => new Date(ts).toISOString().replace("T", " ").slice(0, 16);
|
|
205
|
-
async function
|
|
782
|
+
async function run2(argv, out = process.stdout) {
|
|
206
783
|
const [command, ...rest] = argv;
|
|
207
784
|
const dir = defaultDir();
|
|
208
785
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -210,9 +787,15 @@ async function run(argv, out = process.stdout) {
|
|
|
210
787
|
return 0;
|
|
211
788
|
}
|
|
212
789
|
if (command === "install") return install(rest, out);
|
|
790
|
+
if (command === "daemon") {
|
|
791
|
+
const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_node(), node_exports));
|
|
792
|
+
const { resolveSettings: resolveSettings2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
793
|
+
await runDaemon2(await resolveSettings2());
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
213
796
|
if (command === "whoami") {
|
|
214
797
|
const stored = await loadStored(dir);
|
|
215
|
-
const keys = await loadKeyPair(
|
|
798
|
+
const keys = await loadKeyPair(path6.join(dir, "net")).catch(() => null);
|
|
216
799
|
if (!keys) {
|
|
217
800
|
out.write(
|
|
218
801
|
`No identity yet at ${dir}.
|
|
@@ -261,11 +844,11 @@ It is joined the next time the server starts \u2014 restart Claude Code, or ask
|
|
|
261
844
|
return 0;
|
|
262
845
|
}
|
|
263
846
|
if (command === "inbox") {
|
|
264
|
-
const inbox = new Inbox(
|
|
847
|
+
const inbox = new Inbox(dir, process.env.VOLENET_MCP_SESSION?.trim() || sessionKey());
|
|
265
848
|
await inbox.load();
|
|
266
849
|
const unread = inbox.unread();
|
|
267
850
|
if (unread.length === 0) {
|
|
268
|
-
out.write("No new messages.\n");
|
|
851
|
+
if (!rest.includes("--quiet")) out.write("No new messages.\n");
|
|
269
852
|
return 0;
|
|
270
853
|
}
|
|
271
854
|
out.write(`${unread.length} new VoleNet message${unread.length === 1 ? "" : "s"}:
|
|
@@ -285,103 +868,149 @@ ${USAGE}`);
|
|
|
285
868
|
return 1;
|
|
286
869
|
}
|
|
287
870
|
|
|
288
|
-
// src/
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
871
|
+
// src/index.ts
|
|
872
|
+
init_node();
|
|
873
|
+
|
|
874
|
+
// src/prompts.ts
|
|
875
|
+
var PROMPTS = [
|
|
876
|
+
{
|
|
877
|
+
name: "whoami",
|
|
878
|
+
description: "This session\u2019s identity on the VoleNet mesh, and whether it can reach anything.",
|
|
879
|
+
render: () => `Report this session's VoleNet identity.
|
|
880
|
+
|
|
881
|
+
Call \`volenet_whoami\`. Give back the name, the instance id and where it is listening, and say in
|
|
882
|
+
one line whether it is actually reachable \u2014 a hub joined, or peers paired \u2014 rather than leaving an
|
|
883
|
+
empty roster to be read as a failure.
|
|
884
|
+
|
|
885
|
+
The instance id is what someone else needs to grant this session anything: an agent's operator names
|
|
886
|
+
it in \`net.peers\`. Offer it if they look like they need it. The public key is several kilobytes of
|
|
887
|
+
post-quantum key material, so ask for it with \`key: true\` only when a peer actually wants it.
|
|
888
|
+
|
|
889
|
+
If nothing is connected, say so and offer \`setup\`.`
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
name: "peers",
|
|
893
|
+
description: "Who this session can reach right now, and by which route.",
|
|
894
|
+
render: () => `List who this session can reach on VoleNet.
|
|
895
|
+
|
|
896
|
+
Call \`volenet_peers\`. For each one say whether it is online, and whether the link is direct or
|
|
897
|
+
through a hub \u2014 the difference matters: a hub carries chat and consent, a direct link is the only
|
|
898
|
+
route that can ask an agent's brain.
|
|
899
|
+
|
|
900
|
+
Flag anything that needs an action rather than only listing state: a hub member with no consent yet
|
|
901
|
+
cannot be messaged until one side asks (\`volenet_connect\`), and someone offline will receive what
|
|
902
|
+
is sent whenever they return. If the list is empty, say why \u2014 no hub, no pairings \u2014 and offer
|
|
903
|
+
\`setup\`.`
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: "rooms",
|
|
907
|
+
description: "Rooms this session is in, and how to say something to one.",
|
|
908
|
+
render: () => `Show the VoleNet rooms this session is in.
|
|
909
|
+
|
|
910
|
+
Call \`volenet_room\` with no arguments. For each, say who is in it \u2014 a room is several people and
|
|
911
|
+
agents in one conversation, so who else is there is the useful part.
|
|
912
|
+
|
|
913
|
+
To say something, \`volenet_room\` with \`post\` and \`room\`. Every member gets their own sealed copy;
|
|
914
|
+
there is no shared key, which is why removing somebody stops them reading immediately. Report what
|
|
915
|
+
came back honestly: some copies may be waiting for members who are away, and some may not have been
|
|
916
|
+
sent at all because that member has not accepted this session \u2014 **a room does not create consent**,
|
|
917
|
+
so say that rather than let it read as a failure.
|
|
918
|
+
|
|
919
|
+
If there are no rooms, offer to make one (\`create\`) or to join one by id (\`join\`). A room lives on
|
|
920
|
+
a hub, so one has to be joined first.`
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: "setup",
|
|
924
|
+
description: "Get this session onto the VoleNet mesh \u2014 join a hub, or pair with an agent.",
|
|
925
|
+
render: () => `Get this session onto the VoleNet mesh.
|
|
926
|
+
|
|
927
|
+
1. Call \`volenet_whoami\` first. It reports the identity, whether a hub is set, and how many peers
|
|
928
|
+
are reachable. An identity is generated on first run; there is nothing to create.
|
|
929
|
+
2. If nothing is connected, explain the two routes and ask which is wanted \u2014 do not pick silently:
|
|
930
|
+
- **A hub** (\`volenet_hub\`) makes this session reachable from anywhere, including from a phone,
|
|
931
|
+
and works when neither side can dial the other. A hub carries sealed traffic it cannot read and
|
|
932
|
+
stores no message. It will **not** relay a question to an agent's brain.
|
|
933
|
+
- **A direct pair** (\`volenet_connect\`) with an agent whose address is reachable from here. This
|
|
934
|
+
is the only route that can ask an agent's brain.
|
|
935
|
+
Both can be used at once, and either can be added later.
|
|
936
|
+
3. Carry out whichever they choose. For a hub, the URL is enough. For a pair, follow the two-step
|
|
937
|
+
fingerprint check \u2014 the \`pair\` command covers it.
|
|
938
|
+
4. Finish by calling \`volenet_peers\` and saying plainly who is now reachable, and by which route.
|
|
939
|
+
|
|
940
|
+
Reaching someone also needs consent, which is separate from being connected: on a hub, either side
|
|
941
|
+
asks and the other accepts. Say so, rather than letting an empty roster look like a failure.`
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "catch-up",
|
|
945
|
+
description: "Read what arrived while this session was away, and say what needs answering.",
|
|
946
|
+
render: () => `Catch up on VoleNet.
|
|
947
|
+
|
|
948
|
+
1. Call \`volenet_inbox\`. It returns messages that arrived \u2014 including while no session was running,
|
|
949
|
+
since senders hold what they could not deliver and flush on reconnect \u2014 and who tried to reach
|
|
950
|
+
this session while it was away. Reading marks them seen.
|
|
951
|
+
2. Call \`volenet_peers\` if anything needs context about who a sender is.
|
|
952
|
+
3. Summarise for the person: who wrote, what they want, and what is worth answering. Do not reply on
|
|
953
|
+
their behalf without asking.
|
|
954
|
+
4. If a reply is wanted, \`volenet_send\` says it and \`volenet_wait\` waits for what comes back, so
|
|
955
|
+
an exchange happens in one turn rather than by checking again later.
|
|
956
|
+
|
|
957
|
+
If nothing arrived, say so in one line. This is worth running at the start of a session.`
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
name: "pair",
|
|
961
|
+
description: "Pair with an agent at a URL, checking the fingerprint before trusting it.",
|
|
962
|
+
arguments: [
|
|
963
|
+
{
|
|
964
|
+
name: "url",
|
|
965
|
+
description: "The agent to pair with, e.g. http://10.0.0.5:9700",
|
|
966
|
+
required: true
|
|
967
|
+
}
|
|
968
|
+
],
|
|
969
|
+
render: (a) => `Pair this session with the VoleNet node at ${a.url ?? "<url>"}.
|
|
970
|
+
|
|
971
|
+
Pairing is deliberately two calls, because trusting a URL blind is trusting whoever holds it.
|
|
972
|
+
|
|
973
|
+
1. Call \`volenet_connect\` with \`url: "${a.url ?? "<url>"}"\`. It reaches the node and reports the
|
|
974
|
+
fingerprint of whoever answered. It trusts nothing yet.
|
|
975
|
+
2. Show that fingerprint to the person and ask them to check it against what the other side reports
|
|
976
|
+
\u2014 \`vole net show-key\` on an OpenVole agent. **Wait for them.** Do not confirm on their behalf:
|
|
977
|
+
this step exists precisely so a human compares two values.
|
|
978
|
+
3. Ask whether this session should also be able to use that agent's **brain** \u2014 running its model
|
|
979
|
+
to answer questions \u2014 or only chat with whoever runs it.
|
|
980
|
+
4. Once they confirm the fingerprint, call \`volenet_connect\` again with the same \`url\`,
|
|
981
|
+
\`confirm:\` set to that fingerprint, and \`brain: true\` if they said yes. This trusts the node
|
|
982
|
+
and sends a pair request carrying the ask.
|
|
983
|
+
5. Tell them the request now waits for the operator of that node to accept it, and that nothing
|
|
984
|
+
arrives until they do.
|
|
985
|
+
|
|
986
|
+
Being trusted is not the same as being allowed to do anything: the keystore says who may connect,
|
|
987
|
+
\`net.peers\` says what they may then do. Sending the ask with the request is what lets the operator
|
|
988
|
+
settle both while accepting, instead of editing a config file afterwards.`
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
name: "reach",
|
|
992
|
+
description: "Message a peer and wait for the reply, rather than checking back later.",
|
|
993
|
+
arguments: [
|
|
994
|
+
{ name: "peer", description: "Who to reach \u2014 a name or instance id", required: true },
|
|
995
|
+
{ name: "message", description: "What to say", required: false }
|
|
996
|
+
],
|
|
997
|
+
render: (a) => `Reach ${a.peer ?? "a peer"} over VoleNet${a.message ? ` and say: ${a.message}` : ""}.
|
|
998
|
+
|
|
999
|
+
1. \`volenet_peers\` first if unsure the name resolves, or by which route they are reachable.
|
|
1000
|
+
2. \`volenet_send\` to say it. This is chat: it reaches whoever is there and does **not** run their
|
|
1001
|
+
brain. To ask an agent's model instead, use \`volenet_ask\` \u2014 direct links only, and its operator
|
|
1002
|
+
must have granted brain access.
|
|
1003
|
+
3. \`volenet_wait\` for the answer, so the exchange completes in this turn. If nothing comes back in
|
|
1004
|
+
time, say so plainly: the message is not lost, and a reply lands in the inbox whenever it comes.
|
|
1005
|
+
|
|
1006
|
+
If the peer is offline the message waits here and goes out when they return \u2014 report that rather
|
|
1007
|
+
than treating it as a failure.`
|
|
381
1008
|
}
|
|
382
|
-
|
|
1009
|
+
];
|
|
383
1010
|
|
|
384
1011
|
// src/tools.ts
|
|
1012
|
+
init_config();
|
|
1013
|
+
init_node();
|
|
385
1014
|
var obj = (properties, required = []) => ({
|
|
386
1015
|
type: "object",
|
|
387
1016
|
properties,
|
|
@@ -390,18 +1019,15 @@ var obj = (properties, required = []) => ({
|
|
|
390
1019
|
var str = (description) => ({ type: "string", description });
|
|
391
1020
|
var num = (description) => ({ type: "number", description });
|
|
392
1021
|
var when2 = (ts) => ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : "-";
|
|
393
|
-
function peers(node) {
|
|
394
|
-
const
|
|
395
|
-
(node.net.getTransport()?.getPeers() ?? []).filter((p) => p.connected).map((p) => p.peerId)
|
|
396
|
-
);
|
|
397
|
-
const out = node.net.getInstances().map((i) => ({
|
|
1022
|
+
async function peers(node) {
|
|
1023
|
+
const out = (await node.net.instances()).map((i) => ({
|
|
398
1024
|
id: i.id,
|
|
399
1025
|
name: i.name,
|
|
400
1026
|
route: "direct",
|
|
401
|
-
connected:
|
|
1027
|
+
connected: i.connected
|
|
402
1028
|
}));
|
|
403
1029
|
const direct = new Set(out.map((p) => p.id));
|
|
404
|
-
for (const m of node.net.
|
|
1030
|
+
for (const m of await node.net.relayMembers()) {
|
|
405
1031
|
if (direct.has(m.id)) continue;
|
|
406
1032
|
out.push({
|
|
407
1033
|
id: m.id,
|
|
@@ -414,8 +1040,8 @@ function peers(node) {
|
|
|
414
1040
|
}
|
|
415
1041
|
return out;
|
|
416
1042
|
}
|
|
417
|
-
function resolve(node, ref) {
|
|
418
|
-
const all = peers(node);
|
|
1043
|
+
async function resolve(node, ref) {
|
|
1044
|
+
const all = await peers(node);
|
|
419
1045
|
return all.find((p) => p.id === ref) ?? all.find((p) => p.name === ref) ?? all.find((p) => p.id.startsWith(ref)) ?? all.find((p) => p.name.toLowerCase() === ref.toLowerCase());
|
|
420
1046
|
}
|
|
421
1047
|
var TOOLS = [
|
|
@@ -426,16 +1052,20 @@ var TOOLS = [
|
|
|
426
1052
|
key: { type: "boolean", description: "Include the full public key string" }
|
|
427
1053
|
}),
|
|
428
1054
|
async run(node, args) {
|
|
429
|
-
const key = node.net.
|
|
430
|
-
const online = peers(node).filter((p) => p.connected).length;
|
|
1055
|
+
const key = await node.net.identity();
|
|
1056
|
+
const online = (await peers(node)).filter((p) => p.connected).length;
|
|
431
1057
|
const lines = [
|
|
432
1058
|
`name ${node.options.name}`,
|
|
433
1059
|
`instanceId ${key?.instanceId ?? "(not started)"}`,
|
|
434
1060
|
`hub ${node.hubStatus}`,
|
|
435
1061
|
`connected ${online} peer(s) online`,
|
|
436
1062
|
`listening port ${node.options.port} (reachable only from networks that can dial it)`,
|
|
437
|
-
`store ${node.options.dir}
|
|
1063
|
+
`store ${node.options.dir}`,
|
|
1064
|
+
`node ${node.where === "daemon" ? "a daemon, so this identity stays reachable when no session is open" : "in this session, so it is only reachable while this session is"}`
|
|
438
1065
|
];
|
|
1066
|
+
lines.push(
|
|
1067
|
+
`replies ${node.canSample ? "this client can be asked to answer on its own" : "only when you ask \u2014 this client cannot be woken by a message"}`
|
|
1068
|
+
);
|
|
439
1069
|
if (args.key) lines.push("", `publicKey ${key?.publicKeyString ?? "-"}`);
|
|
440
1070
|
else
|
|
441
1071
|
lines.push(
|
|
@@ -455,7 +1085,7 @@ var TOOLS = [
|
|
|
455
1085
|
description: "Everyone this session can reach: agents and people, whether the link is direct or through a hub, and whether they are online right now.",
|
|
456
1086
|
inputSchema: obj({}),
|
|
457
1087
|
async run(node) {
|
|
458
|
-
const all = peers(node);
|
|
1088
|
+
const all = await peers(node);
|
|
459
1089
|
if (all.length === 0) {
|
|
460
1090
|
return "No peers. Join a hub (VOLENET_MCP_HUB) or pair with a node directly (volenet_pair).";
|
|
461
1091
|
}
|
|
@@ -512,7 +1142,7 @@ var TOOLS = [
|
|
|
512
1142
|
const to = String(args.to ?? "");
|
|
513
1143
|
const text = String(args.text ?? "");
|
|
514
1144
|
if (!text.trim()) return "Nothing to send.";
|
|
515
|
-
const peer = resolve(node, to);
|
|
1145
|
+
const peer = await resolve(node, to);
|
|
516
1146
|
const res = await node.net.sendChat(peer?.id ?? to, text);
|
|
517
1147
|
if (!res.ok) return `Not sent: ${res.error ?? "unknown error"}`;
|
|
518
1148
|
await node.inbox.add({
|
|
@@ -537,7 +1167,7 @@ var TOOLS = [
|
|
|
537
1167
|
limit: num("Messages (default 50)")
|
|
538
1168
|
}),
|
|
539
1169
|
async run(node, args) {
|
|
540
|
-
const peer = resolve(node, String(args.peer ?? ""));
|
|
1170
|
+
const peer = await resolve(node, String(args.peer ?? ""));
|
|
541
1171
|
const id = peer?.id ?? String(args.peer ?? "");
|
|
542
1172
|
const msgs = node.inbox.history(id, Number(args.limit ?? 50));
|
|
543
1173
|
if (msgs.length === 0) return `Nothing recorded with ${peer?.name ?? id}.`;
|
|
@@ -556,16 +1186,15 @@ var TOOLS = [
|
|
|
556
1186
|
["to", "question"]
|
|
557
1187
|
),
|
|
558
1188
|
async run(node, args) {
|
|
559
|
-
const peer = resolve(node, String(args.to ?? ""));
|
|
1189
|
+
const peer = await resolve(node, String(args.to ?? ""));
|
|
560
1190
|
if (!peer) return `No peer found: "${args.to}". Use volenet_peers.`;
|
|
561
1191
|
if (peer.route !== "direct") {
|
|
562
1192
|
return `${peer.name} is only reachable through a hub, and a hub will not relay a question to an agent's brain \u2014 it carries chat and consent only. Use volenet_send, or pair directly with volenet_pair.`;
|
|
563
1193
|
}
|
|
564
|
-
const
|
|
565
|
-
if (!mgr) return "Remote task manager not available.";
|
|
566
|
-
const res = await mgr.delegateTask(
|
|
1194
|
+
const res = await node.net.askBrain(
|
|
567
1195
|
peer.id,
|
|
568
|
-
|
|
1196
|
+
String(args.question ?? ""),
|
|
1197
|
+
node.options.name,
|
|
569
1198
|
Number(args.timeout_ms ?? 12e4)
|
|
570
1199
|
);
|
|
571
1200
|
if (res.status === "completed") return `${peer.name} says:
|
|
@@ -575,7 +1204,7 @@ ${res.result}`;
|
|
|
575
1204
|
if (typeof why === "string" && why.includes("allowBrain")) {
|
|
576
1205
|
return `${peer.name} refused: ${why}
|
|
577
1206
|
|
|
578
|
-
Its operator can allow this session by adding { "id": "${node.net.
|
|
1207
|
+
Its operator can allow this session by adding { "id": "${(await node.net.identity())?.instanceId}", "trust": "read", "allowBrain": true } to net.peers and restarting.`;
|
|
579
1208
|
}
|
|
580
1209
|
return `${peer.name} did not answer: ${why}`;
|
|
581
1210
|
}
|
|
@@ -598,7 +1227,7 @@ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.
|
|
|
598
1227
|
node.requests.splice(node.requests.indexOf(req), 1);
|
|
599
1228
|
return ok.ok ? `${accept ? "Accepted" : "Denied"} ${req.fromName}.` : `Failed: ${"error" in ok ? ok.error : "unknown"}`;
|
|
600
1229
|
}
|
|
601
|
-
const pairs = node.net.listPairRequests();
|
|
1230
|
+
const pairs = await node.net.listPairRequests();
|
|
602
1231
|
for (const p of pairs) {
|
|
603
1232
|
if (!node.requests.some((r) => r.from === p.id)) {
|
|
604
1233
|
node.requests.push({ kind: "pair", from: p.id, fromName: p.name, at: Date.now() });
|
|
@@ -619,7 +1248,7 @@ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.
|
|
|
619
1248
|
}),
|
|
620
1249
|
async run(node, args) {
|
|
621
1250
|
const limit = Math.min(Math.max(Number(args.timeout_ms ?? 6e4), 1e3), 3e5);
|
|
622
|
-
const want = args.from ? resolve(node, String(args.from)) : void 0;
|
|
1251
|
+
const want = args.from ? await resolve(node, String(args.from)) : void 0;
|
|
623
1252
|
const wanted = (m) => !args.from || m.peerId === (want?.id ?? args.from);
|
|
624
1253
|
const already = node.inbox.unread().filter(wanted);
|
|
625
1254
|
if (already.length === 0) {
|
|
@@ -655,7 +1284,7 @@ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.
|
|
|
655
1284
|
if (args.leave) {
|
|
656
1285
|
const hub = node.options.hub;
|
|
657
1286
|
if (!hub) return "Not on a hub.";
|
|
658
|
-
node.net.forgetPeer(hub);
|
|
1287
|
+
await node.net.forgetPeer(hub);
|
|
659
1288
|
node.options.hub = void 0;
|
|
660
1289
|
node.hubStatus = "no hub configured";
|
|
661
1290
|
await saveStored(node.options.dir, { hub: void 0 });
|
|
@@ -674,7 +1303,7 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
674
1303
|
await saveStored(node.options.dir, { hub: url });
|
|
675
1304
|
return `Joined ${url} again \u2014 it was already trusted, so no introduction was needed.`;
|
|
676
1305
|
}
|
|
677
|
-
const res = await node.net.
|
|
1306
|
+
const res = await node.net.joinHub(url);
|
|
678
1307
|
if (!res.ok) return `Could not join ${url}: ${res.error}`;
|
|
679
1308
|
node.options.hub = url;
|
|
680
1309
|
node.hubStatus = res.pending ? `waiting for approval at ${url}` : `joined ${res.hubName ?? url}`;
|
|
@@ -682,12 +1311,84 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
682
1311
|
return res.pending ? `Asked to join ${url}. Its operator has to approve before you appear in the roster.` : `Joined ${res.hubName ?? url}. Remembered, so the next session starts here. Call volenet_peers to see who is around.`;
|
|
683
1312
|
}
|
|
684
1313
|
},
|
|
1314
|
+
{
|
|
1315
|
+
name: "volenet_room",
|
|
1316
|
+
description: "Rooms: several people and agents in one conversation. With no arguments it lists the rooms this session is in. Give `post` to say something to a room \u2014 every member gets their own sealed copy, so there is no shared key and removing someone stops them reading immediately. Give `create`, `join`, `leave` or `invite` to change membership, which the hub keeps. A room does not create consent: a member who has not accepted you will not receive your posts.",
|
|
1317
|
+
inputSchema: obj({
|
|
1318
|
+
room: str("Which room, by id or name (see the list)"),
|
|
1319
|
+
post: str("Say this to the room"),
|
|
1320
|
+
create: str("Make a room with this name"),
|
|
1321
|
+
join: str("Join a room by id"),
|
|
1322
|
+
leave: { type: "boolean", description: "Leave the room named in `room`" },
|
|
1323
|
+
invite: str("Bring this peer (name or id) into the room named in `room`"),
|
|
1324
|
+
hub: str("Which hub holds the room. Only needed with more than one.")
|
|
1325
|
+
}),
|
|
1326
|
+
async run(node, args) {
|
|
1327
|
+
const hubOf = async () => {
|
|
1328
|
+
if (args.hub) return String(args.hub);
|
|
1329
|
+
const hubs = (await node.net.instances()).filter((i) => i.connected);
|
|
1330
|
+
if (hubs.length === 0) return null;
|
|
1331
|
+
return hubs[0].id;
|
|
1332
|
+
};
|
|
1333
|
+
const find = async (ref) => {
|
|
1334
|
+
const all2 = await node.net.rooms();
|
|
1335
|
+
return all2.find((r) => r.room === ref) ?? all2.find((r) => r.name === ref);
|
|
1336
|
+
};
|
|
1337
|
+
if (args.create || args.join || args.leave || args.invite) {
|
|
1338
|
+
const hub = await hubOf();
|
|
1339
|
+
if (!hub)
|
|
1340
|
+
return "No hub connected. A room lives on a hub \u2014 join one first with volenet_hub.";
|
|
1341
|
+
if (args.create) {
|
|
1342
|
+
const res2 = await node.net.roomCommand(hub, "room:create", { name: String(args.create) });
|
|
1343
|
+
return res2.ok ? `Asked for a room called "${args.create}". Call this again in a moment to see it.` : `Could not: ${res2.error}`;
|
|
1344
|
+
}
|
|
1345
|
+
if (args.join) {
|
|
1346
|
+
const res2 = await node.net.roomCommand(hub, "room:join", { room: String(args.join) });
|
|
1347
|
+
return res2.ok ? `Asked to join ${args.join}.` : `Could not: ${res2.error}`;
|
|
1348
|
+
}
|
|
1349
|
+
const room = args.room ? await find(String(args.room)) : void 0;
|
|
1350
|
+
if (!room) return "Name the room with `room` \u2014 see the list.";
|
|
1351
|
+
if (args.leave) {
|
|
1352
|
+
const res2 = await node.net.roomCommand(hub, "room:leave", { room: room.room });
|
|
1353
|
+
return res2.ok ? `Left ${room.name}.` : `Could not: ${res2.error}`;
|
|
1354
|
+
}
|
|
1355
|
+
const peer = await resolve(node, String(args.invite));
|
|
1356
|
+
const res = await node.net.roomCommand(hub, "room:invite", {
|
|
1357
|
+
room: room.room,
|
|
1358
|
+
member: peer?.id ?? String(args.invite)
|
|
1359
|
+
});
|
|
1360
|
+
return res.ok ? `Invited ${peer?.name ?? args.invite} to ${room.name}.` : `Could not: ${res.error}`;
|
|
1361
|
+
}
|
|
1362
|
+
if (args.post) {
|
|
1363
|
+
const room = args.room ? await find(String(args.room)) : (await node.net.rooms())[0];
|
|
1364
|
+
if (!room) return "No room to post to. Create or join one first.";
|
|
1365
|
+
const res = await node.net.postToRoom(room.room, String(args.post));
|
|
1366
|
+
if (!res.ok) return `Not posted: ${res.error}`;
|
|
1367
|
+
const bits = [`Posted to ${room.name}: ${res.sent} delivered`];
|
|
1368
|
+
if (res.held) bits.push(`${res.held} waiting for members who are away`);
|
|
1369
|
+
if (res.skipped)
|
|
1370
|
+
bits.push(`${res.skipped} could not be reached \u2014 they may not have accepted you`);
|
|
1371
|
+
return `${bits.join(", ")}.`;
|
|
1372
|
+
}
|
|
1373
|
+
const all = await node.net.rooms();
|
|
1374
|
+
if (all.length === 0) {
|
|
1375
|
+
return 'Not in any room. Create one with create:"name", or join one you have been given the id for.';
|
|
1376
|
+
}
|
|
1377
|
+
return all.map(
|
|
1378
|
+
(r) => ` ${r.name} ${r.room.substring(0, 8)} ${r.members.length} member(s): ${r.members.map((m) => m.name).join(", ")}`
|
|
1379
|
+
).join("\n");
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
685
1382
|
{
|
|
686
1383
|
name: "volenet_connect",
|
|
687
1384
|
description: "Reach out to someone new: pair directly with a node at a URL, or ask a hub member for consent to chat. Pairing is two calls \u2014 the first reports the fingerprint of whoever answers, the second confirms it \u2014 because trusting a URL blind is trusting whoever holds it. Neither side trusts you until they accept.",
|
|
688
1385
|
inputSchema: obj({
|
|
689
1386
|
url: str("Node URL to pair with directly, e.g. http://10.0.0.5:9700"),
|
|
690
1387
|
confirm: str("The fingerprint returned by a first call with url, confirming who answers"),
|
|
1388
|
+
brain: {
|
|
1389
|
+
type: "boolean",
|
|
1390
|
+
description: "Also ask for permission to use that agent's brain. The operator sees it as part of the same decision and can grant it while accepting; without it, being trusted allows chat only."
|
|
1391
|
+
},
|
|
691
1392
|
member: str("Hub member name or id to ask for chat consent"),
|
|
692
1393
|
note: str("A line saying who you are")
|
|
693
1394
|
}),
|
|
@@ -711,8 +1412,18 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
711
1412
|
if (!probe.fingerprint?.startsWith(confirm)) {
|
|
712
1413
|
return `That fingerprint does not match: it answers with ${probe.fingerprint}. Nothing was trusted.`;
|
|
713
1414
|
}
|
|
714
|
-
const res = await node.net.initiatePair(
|
|
715
|
-
|
|
1415
|
+
const res = await node.net.initiatePair(
|
|
1416
|
+
url,
|
|
1417
|
+
probe.publicKey,
|
|
1418
|
+
note,
|
|
1419
|
+
args.brain ? ["brain"] : void 0
|
|
1420
|
+
);
|
|
1421
|
+
if (!res.ok) return `Could not ask: ${res.error}`;
|
|
1422
|
+
return [
|
|
1423
|
+
`Trusted ${probe.name ?? url} and asked it to trust this session.`,
|
|
1424
|
+
args.brain ? "The request also asks to use its brain, so its operator can grant that while accepting \u2014 no config editing, no restart." : "It asks for trust only. Pass brain:true to also ask for brain access.",
|
|
1425
|
+
"Nothing arrives until their operator accepts."
|
|
1426
|
+
].join(" ");
|
|
716
1427
|
}
|
|
717
1428
|
if (args.member) {
|
|
718
1429
|
const res = await node.net.requestRelayConnect(String(args.member), note);
|
|
@@ -725,6 +1436,9 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
725
1436
|
];
|
|
726
1437
|
|
|
727
1438
|
// src/index.ts
|
|
1439
|
+
init_inbox();
|
|
1440
|
+
init_config();
|
|
1441
|
+
init_node();
|
|
728
1442
|
function unreadFooter(node, toolName) {
|
|
729
1443
|
if (toolName === "volenet_inbox" || toolName === "volenet_wait") return "";
|
|
730
1444
|
const unread = node.inbox.unread();
|
|
@@ -734,8 +1448,34 @@ function unreadFooter(node, toolName) {
|
|
|
734
1448
|
|
|
735
1449
|
\u2014 ${unread.length} unread message${unread.length === 1 ? "" : "s"} from ${who}. Read them with volenet_inbox.`;
|
|
736
1450
|
}
|
|
737
|
-
function
|
|
738
|
-
const server = new Server(
|
|
1451
|
+
function createServer2(node) {
|
|
1452
|
+
const server = new Server(
|
|
1453
|
+
{ name: "volenet", version: "0.1.0" },
|
|
1454
|
+
{ capabilities: { tools: {}, prompts: {} } }
|
|
1455
|
+
);
|
|
1456
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1457
|
+
prompts: PROMPTS.map((p) => ({
|
|
1458
|
+
name: p.name,
|
|
1459
|
+
description: p.description,
|
|
1460
|
+
...p.arguments ? { arguments: p.arguments } : {}
|
|
1461
|
+
}))
|
|
1462
|
+
}));
|
|
1463
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1464
|
+
const prompt = PROMPTS.find((p) => p.name === request.params.name);
|
|
1465
|
+
if (!prompt) throw new Error(`No such prompt: ${request.params.name}`);
|
|
1466
|
+
return {
|
|
1467
|
+
description: prompt.description,
|
|
1468
|
+
messages: [
|
|
1469
|
+
{
|
|
1470
|
+
role: "user",
|
|
1471
|
+
content: {
|
|
1472
|
+
type: "text",
|
|
1473
|
+
text: prompt.render(request.params.arguments ?? {})
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
]
|
|
1477
|
+
};
|
|
1478
|
+
});
|
|
739
1479
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
740
1480
|
tools: TOOLS.map((t) => ({
|
|
741
1481
|
name: t.name,
|
|
@@ -765,13 +1505,31 @@ function createServer(node) {
|
|
|
765
1505
|
});
|
|
766
1506
|
return server;
|
|
767
1507
|
}
|
|
1508
|
+
async function recordClientCapabilities(dir, caps) {
|
|
1509
|
+
try {
|
|
1510
|
+
const fs4 = await import("fs/promises");
|
|
1511
|
+
const path7 = await import("path");
|
|
1512
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1513
|
+
await fs4.writeFile(
|
|
1514
|
+
path7.join(dir, "client.json"),
|
|
1515
|
+
`${JSON.stringify(caps ?? {}, null, 2)}
|
|
1516
|
+
`,
|
|
1517
|
+
"utf-8"
|
|
1518
|
+
);
|
|
1519
|
+
} catch {
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
768
1522
|
async function main() {
|
|
769
1523
|
const options = await resolveSettings();
|
|
770
1524
|
const node = await startNode(options);
|
|
771
|
-
const server =
|
|
1525
|
+
const server = createServer2(node);
|
|
772
1526
|
await server.connect(new StdioServerTransport());
|
|
1527
|
+
const caps = server.getClientCapabilities();
|
|
1528
|
+
node.canSample = Boolean(caps && typeof caps === "object" && "sampling" in caps);
|
|
1529
|
+
await recordClientCapabilities(options.dir, caps);
|
|
1530
|
+
const me = await node.net.identity().catch(() => null);
|
|
773
1531
|
process.stderr.write(
|
|
774
|
-
`volenet-mcp: ${options.name} (${
|
|
1532
|
+
`volenet-mcp: ${options.name} (${me?.instanceId.substring(0, 8) ?? "?"}) ready \u2014 node ${node.where}${options.hub ? `, hub ${options.hub}` : ", no hub configured"}
|
|
775
1533
|
`
|
|
776
1534
|
);
|
|
777
1535
|
let stopping = false;
|
|
@@ -787,7 +1545,7 @@ async function main() {
|
|
|
787
1545
|
}
|
|
788
1546
|
if (process.argv[1]?.includes("volenet-mcp") || process.env.VOLENET_MCP_RUN === "1") {
|
|
789
1547
|
if (process.argv[2] || process.stdin.isTTY) {
|
|
790
|
-
|
|
1548
|
+
run2(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
|
|
791
1549
|
process.stderr.write(`volenet-mcp: ${err instanceof Error ? err.message : String(err)}
|
|
792
1550
|
`);
|
|
793
1551
|
process.exit(1);
|
|
@@ -802,13 +1560,15 @@ if (process.argv[1]?.includes("volenet-mcp") || process.env.VOLENET_MCP_RUN ===
|
|
|
802
1560
|
}
|
|
803
1561
|
export {
|
|
804
1562
|
Inbox,
|
|
1563
|
+
PROMPTS,
|
|
805
1564
|
TOOLS,
|
|
806
|
-
createServer,
|
|
1565
|
+
createServer2 as createServer,
|
|
807
1566
|
defaultDir,
|
|
808
1567
|
defaultName,
|
|
809
1568
|
loadStored,
|
|
1569
|
+
recordClientCapabilities,
|
|
810
1570
|
resolveSettings,
|
|
811
|
-
|
|
1571
|
+
run2 as runCli,
|
|
812
1572
|
saveStored,
|
|
813
1573
|
startNode,
|
|
814
1574
|
unreadFooter
|