@openvole/volenet-mcp 0.1.0 → 0.2.1
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 +70 -2
- package/dist/index.d.ts +200 -14
- package/dist/index.js +997 -228
- 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) });
|
|
381
|
+
}
|
|
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
|
+
}
|
|
403
|
+
}
|
|
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");
|
|
77
459
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
};
|
|
84
541
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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);
|
|
101
587
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 {
|
|
113
647
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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";
|
|
@@ -146,16 +718,17 @@ function addArgs(scope, command) {
|
|
|
146
718
|
return ["mcp", "add", SERVER_NAME, "-s", 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
|
-
|
|
150
|
-
|
|
721
|
+
var spawnClaude = (args) => spawnSync("claude", args, {
|
|
722
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
723
|
+
encoding: "utf-8",
|
|
724
|
+
timeout: 3e4
|
|
725
|
+
});
|
|
726
|
+
function install(argv, out = process.stdout, exec = spawnClaude) {
|
|
727
|
+
const scope = argv.includes("--local") ? "local" : "user";
|
|
151
728
|
const command = launchCommand();
|
|
152
729
|
const paste = `claude mcp add ${SERVER_NAME} -s ${scope} -- ${command.join(" ")}`;
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
encoding: "utf-8",
|
|
156
|
-
timeout: 3e4
|
|
157
|
-
});
|
|
158
|
-
const listed = run2(["mcp", "list"]);
|
|
730
|
+
const run3 = exec;
|
|
731
|
+
const listed = run3(["mcp", "list"]);
|
|
159
732
|
if (listed.error) {
|
|
160
733
|
out.write(
|
|
161
734
|
`The \`claude\` CLI is not on PATH. Run this once, in the project you want it in:
|
|
@@ -166,12 +739,20 @@ function install(argv, out = process.stdout) {
|
|
|
166
739
|
);
|
|
167
740
|
return 1;
|
|
168
741
|
}
|
|
169
|
-
|
|
170
|
-
|
|
742
|
+
const existing = listed.stdout?.split("\n").find((l) => l.trimStart().startsWith(`${SERVER_NAME}:`));
|
|
743
|
+
if (existing) {
|
|
744
|
+
if (existing.includes(command.join(" "))) {
|
|
745
|
+
out.write(`${SERVER_NAME} is already registered, unchanged.
|
|
171
746
|
${NEXT_STEPS}`);
|
|
172
|
-
|
|
747
|
+
return 0;
|
|
748
|
+
}
|
|
749
|
+
out.write(`Replacing the existing ${SERVER_NAME} registration:
|
|
750
|
+
was: ${existing.trim()}
|
|
751
|
+
`);
|
|
752
|
+
run3(["mcp", "remove", SERVER_NAME, "-s", scope]);
|
|
753
|
+
run3(["mcp", "remove", SERVER_NAME, "-s", scope === "user" ? "local" : "user"]);
|
|
173
754
|
}
|
|
174
|
-
const added =
|
|
755
|
+
const added = run3(addArgs(scope, command));
|
|
175
756
|
if (added.status !== 0) {
|
|
176
757
|
out.write(
|
|
177
758
|
`Could not register it automatically${added.stderr ? `: ${added.stderr.trim()}` : ""}
|
|
@@ -184,25 +765,30 @@ Run this once instead:
|
|
|
184
765
|
);
|
|
185
766
|
return added.status ?? 1;
|
|
186
767
|
}
|
|
187
|
-
out.write(
|
|
188
|
-
${
|
|
768
|
+
out.write(
|
|
769
|
+
`Registered ${SERVER_NAME} (${scope} scope${scope === "user" ? " \u2014 available in every project" : ", this project only"}).
|
|
770
|
+
${NEXT_STEPS}`
|
|
771
|
+
);
|
|
189
772
|
return 0;
|
|
190
773
|
}
|
|
191
774
|
|
|
192
775
|
// src/cli.ts
|
|
193
776
|
var USAGE = `volenet-mcp \u2014 VoleNet as an MCP server
|
|
194
777
|
|
|
195
|
-
volenet-mcp install [--
|
|
778
|
+
volenet-mcp install [--local] register with Claude Code (default: every project)
|
|
196
779
|
volenet-mcp whoami this machine's identity on the mesh
|
|
780
|
+
volenet-mcp daemon run the node in the foreground (normally started for you)
|
|
197
781
|
volenet-mcp hub [url|--leave] which hub to use; takes effect on the next session
|
|
198
|
-
volenet-mcp inbox [--read]
|
|
782
|
+
volenet-mcp inbox [--read] [--quiet]
|
|
783
|
+
messages waiting. --read marks them seen, --quiet says
|
|
784
|
+
nothing when there are none (for hooks)
|
|
199
785
|
|
|
200
786
|
With no command it runs as the MCP server itself, over stdio, which is how Claude Code starts it.
|
|
201
787
|
Anything needing a live node \u2014 peers, pairing, asking an agent's brain \u2014 is a tool you ask for in
|
|
202
788
|
a session, not a command here.
|
|
203
789
|
`;
|
|
204
790
|
var when = (ts) => new Date(ts).toISOString().replace("T", " ").slice(0, 16);
|
|
205
|
-
async function
|
|
791
|
+
async function run2(argv, out = process.stdout) {
|
|
206
792
|
const [command, ...rest] = argv;
|
|
207
793
|
const dir = defaultDir();
|
|
208
794
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -210,9 +796,15 @@ async function run(argv, out = process.stdout) {
|
|
|
210
796
|
return 0;
|
|
211
797
|
}
|
|
212
798
|
if (command === "install") return install(rest, out);
|
|
799
|
+
if (command === "daemon") {
|
|
800
|
+
const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_node(), node_exports));
|
|
801
|
+
const { resolveSettings: resolveSettings2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
802
|
+
await runDaemon2(await resolveSettings2());
|
|
803
|
+
return 0;
|
|
804
|
+
}
|
|
213
805
|
if (command === "whoami") {
|
|
214
806
|
const stored = await loadStored(dir);
|
|
215
|
-
const keys = await loadKeyPair(
|
|
807
|
+
const keys = await loadKeyPair(path6.join(dir, "net")).catch(() => null);
|
|
216
808
|
if (!keys) {
|
|
217
809
|
out.write(
|
|
218
810
|
`No identity yet at ${dir}.
|
|
@@ -261,11 +853,11 @@ It is joined the next time the server starts \u2014 restart Claude Code, or ask
|
|
|
261
853
|
return 0;
|
|
262
854
|
}
|
|
263
855
|
if (command === "inbox") {
|
|
264
|
-
const inbox = new Inbox(
|
|
856
|
+
const inbox = new Inbox(dir, process.env.VOLENET_MCP_SESSION?.trim() || sessionKey());
|
|
265
857
|
await inbox.load();
|
|
266
858
|
const unread = inbox.unread();
|
|
267
859
|
if (unread.length === 0) {
|
|
268
|
-
out.write("No new messages.\n");
|
|
860
|
+
if (!rest.includes("--quiet")) out.write("No new messages.\n");
|
|
269
861
|
return 0;
|
|
270
862
|
}
|
|
271
863
|
out.write(`${unread.length} new VoleNet message${unread.length === 1 ? "" : "s"}:
|
|
@@ -285,103 +877,149 @@ ${USAGE}`);
|
|
|
285
877
|
return 1;
|
|
286
878
|
}
|
|
287
879
|
|
|
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
|
-
|
|
880
|
+
// src/index.ts
|
|
881
|
+
init_node();
|
|
882
|
+
|
|
883
|
+
// src/prompts.ts
|
|
884
|
+
var PROMPTS = [
|
|
885
|
+
{
|
|
886
|
+
name: "whoami",
|
|
887
|
+
description: "This session\u2019s identity on the VoleNet mesh, and whether it can reach anything.",
|
|
888
|
+
render: () => `Report this session's VoleNet identity.
|
|
889
|
+
|
|
890
|
+
Call \`volenet_whoami\`. Give back the name, the instance id and where it is listening, and say in
|
|
891
|
+
one line whether it is actually reachable \u2014 a hub joined, or peers paired \u2014 rather than leaving an
|
|
892
|
+
empty roster to be read as a failure.
|
|
893
|
+
|
|
894
|
+
The instance id is what someone else needs to grant this session anything: an agent's operator names
|
|
895
|
+
it in \`net.peers\`. Offer it if they look like they need it. The public key is several kilobytes of
|
|
896
|
+
post-quantum key material, so ask for it with \`key: true\` only when a peer actually wants it.
|
|
897
|
+
|
|
898
|
+
If nothing is connected, say so and offer \`setup\`.`
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "peers",
|
|
902
|
+
description: "Who this session can reach right now, and by which route.",
|
|
903
|
+
render: () => `List who this session can reach on VoleNet.
|
|
904
|
+
|
|
905
|
+
Call \`volenet_peers\`. For each one say whether it is online, and whether the link is direct or
|
|
906
|
+
through a hub \u2014 the difference matters: a hub carries chat and consent, a direct link is the only
|
|
907
|
+
route that can ask an agent's brain.
|
|
908
|
+
|
|
909
|
+
Flag anything that needs an action rather than only listing state: a hub member with no consent yet
|
|
910
|
+
cannot be messaged until one side asks (\`volenet_connect\`), and someone offline will receive what
|
|
911
|
+
is sent whenever they return. If the list is empty, say why \u2014 no hub, no pairings \u2014 and offer
|
|
912
|
+
\`setup\`.`
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
name: "rooms",
|
|
916
|
+
description: "Rooms this session is in, and how to say something to one.",
|
|
917
|
+
render: () => `Show the VoleNet rooms this session is in.
|
|
918
|
+
|
|
919
|
+
Call \`volenet_room\` with no arguments. For each, say who is in it \u2014 a room is several people and
|
|
920
|
+
agents in one conversation, so who else is there is the useful part.
|
|
921
|
+
|
|
922
|
+
To say something, \`volenet_room\` with \`post\` and \`room\`. Every member gets their own sealed copy;
|
|
923
|
+
there is no shared key, which is why removing somebody stops them reading immediately. Report what
|
|
924
|
+
came back honestly: some copies may be waiting for members who are away, and some may not have been
|
|
925
|
+
sent at all because that member has not accepted this session \u2014 **a room does not create consent**,
|
|
926
|
+
so say that rather than let it read as a failure.
|
|
927
|
+
|
|
928
|
+
If there are no rooms, offer to make one (\`create\`) or to join one by id (\`join\`). A room lives on
|
|
929
|
+
a hub, so one has to be joined first.`
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "setup",
|
|
933
|
+
description: "Get this session onto the VoleNet mesh \u2014 join a hub, or pair with an agent.",
|
|
934
|
+
render: () => `Get this session onto the VoleNet mesh.
|
|
935
|
+
|
|
936
|
+
1. Call \`volenet_whoami\` first. It reports the identity, whether a hub is set, and how many peers
|
|
937
|
+
are reachable. An identity is generated on first run; there is nothing to create.
|
|
938
|
+
2. If nothing is connected, explain the two routes and ask which is wanted \u2014 do not pick silently:
|
|
939
|
+
- **A hub** (\`volenet_hub\`) makes this session reachable from anywhere, including from a phone,
|
|
940
|
+
and works when neither side can dial the other. A hub carries sealed traffic it cannot read and
|
|
941
|
+
stores no message. It will **not** relay a question to an agent's brain.
|
|
942
|
+
- **A direct pair** (\`volenet_connect\`) with an agent whose address is reachable from here. This
|
|
943
|
+
is the only route that can ask an agent's brain.
|
|
944
|
+
Both can be used at once, and either can be added later.
|
|
945
|
+
3. Carry out whichever they choose. For a hub, the URL is enough. For a pair, follow the two-step
|
|
946
|
+
fingerprint check \u2014 the \`pair\` command covers it.
|
|
947
|
+
4. Finish by calling \`volenet_peers\` and saying plainly who is now reachable, and by which route.
|
|
948
|
+
|
|
949
|
+
Reaching someone also needs consent, which is separate from being connected: on a hub, either side
|
|
950
|
+
asks and the other accepts. Say so, rather than letting an empty roster look like a failure.`
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
name: "catch-up",
|
|
954
|
+
description: "Read what arrived while this session was away, and say what needs answering.",
|
|
955
|
+
render: () => `Catch up on VoleNet.
|
|
956
|
+
|
|
957
|
+
1. Call \`volenet_inbox\`. It returns messages that arrived \u2014 including while no session was running,
|
|
958
|
+
since senders hold what they could not deliver and flush on reconnect \u2014 and who tried to reach
|
|
959
|
+
this session while it was away. Reading marks them seen.
|
|
960
|
+
2. Call \`volenet_peers\` if anything needs context about who a sender is.
|
|
961
|
+
3. Summarise for the person: who wrote, what they want, and what is worth answering. Do not reply on
|
|
962
|
+
their behalf without asking.
|
|
963
|
+
4. If a reply is wanted, \`volenet_send\` says it and \`volenet_wait\` waits for what comes back, so
|
|
964
|
+
an exchange happens in one turn rather than by checking again later.
|
|
965
|
+
|
|
966
|
+
If nothing arrived, say so in one line. This is worth running at the start of a session.`
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: "pair",
|
|
970
|
+
description: "Pair with an agent at a URL, checking the fingerprint before trusting it.",
|
|
971
|
+
arguments: [
|
|
972
|
+
{
|
|
973
|
+
name: "url",
|
|
974
|
+
description: "The agent to pair with, e.g. http://10.0.0.5:9700",
|
|
975
|
+
required: true
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
render: (a) => `Pair this session with the VoleNet node at ${a.url ?? "<url>"}.
|
|
979
|
+
|
|
980
|
+
Pairing is deliberately two calls, because trusting a URL blind is trusting whoever holds it.
|
|
981
|
+
|
|
982
|
+
1. Call \`volenet_connect\` with \`url: "${a.url ?? "<url>"}"\`. It reaches the node and reports the
|
|
983
|
+
fingerprint of whoever answered. It trusts nothing yet.
|
|
984
|
+
2. Show that fingerprint to the person and ask them to check it against what the other side reports
|
|
985
|
+
\u2014 \`vole net show-key\` on an OpenVole agent. **Wait for them.** Do not confirm on their behalf:
|
|
986
|
+
this step exists precisely so a human compares two values.
|
|
987
|
+
3. Ask whether this session should also be able to use that agent's **brain** \u2014 running its model
|
|
988
|
+
to answer questions \u2014 or only chat with whoever runs it.
|
|
989
|
+
4. Once they confirm the fingerprint, call \`volenet_connect\` again with the same \`url\`,
|
|
990
|
+
\`confirm:\` set to that fingerprint, and \`brain: true\` if they said yes. This trusts the node
|
|
991
|
+
and sends a pair request carrying the ask.
|
|
992
|
+
5. Tell them the request now waits for the operator of that node to accept it, and that nothing
|
|
993
|
+
arrives until they do.
|
|
994
|
+
|
|
995
|
+
Being trusted is not the same as being allowed to do anything: the keystore says who may connect,
|
|
996
|
+
\`net.peers\` says what they may then do. Sending the ask with the request is what lets the operator
|
|
997
|
+
settle both while accepting, instead of editing a config file afterwards.`
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: "reach",
|
|
1001
|
+
description: "Message a peer and wait for the reply, rather than checking back later.",
|
|
1002
|
+
arguments: [
|
|
1003
|
+
{ name: "peer", description: "Who to reach \u2014 a name or instance id", required: true },
|
|
1004
|
+
{ name: "message", description: "What to say", required: false }
|
|
1005
|
+
],
|
|
1006
|
+
render: (a) => `Reach ${a.peer ?? "a peer"} over VoleNet${a.message ? ` and say: ${a.message}` : ""}.
|
|
1007
|
+
|
|
1008
|
+
1. \`volenet_peers\` first if unsure the name resolves, or by which route they are reachable.
|
|
1009
|
+
2. \`volenet_send\` to say it. This is chat: it reaches whoever is there and does **not** run their
|
|
1010
|
+
brain. To ask an agent's model instead, use \`volenet_ask\` \u2014 direct links only, and its operator
|
|
1011
|
+
must have granted brain access.
|
|
1012
|
+
3. \`volenet_wait\` for the answer, so the exchange completes in this turn. If nothing comes back in
|
|
1013
|
+
time, say so plainly: the message is not lost, and a reply lands in the inbox whenever it comes.
|
|
1014
|
+
|
|
1015
|
+
If the peer is offline the message waits here and goes out when they return \u2014 report that rather
|
|
1016
|
+
than treating it as a failure.`
|
|
381
1017
|
}
|
|
382
|
-
|
|
1018
|
+
];
|
|
383
1019
|
|
|
384
1020
|
// src/tools.ts
|
|
1021
|
+
init_config();
|
|
1022
|
+
init_node();
|
|
385
1023
|
var obj = (properties, required = []) => ({
|
|
386
1024
|
type: "object",
|
|
387
1025
|
properties,
|
|
@@ -390,18 +1028,15 @@ var obj = (properties, required = []) => ({
|
|
|
390
1028
|
var str = (description) => ({ type: "string", description });
|
|
391
1029
|
var num = (description) => ({ type: "number", description });
|
|
392
1030
|
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) => ({
|
|
1031
|
+
async function peers(node) {
|
|
1032
|
+
const out = (await node.net.instances()).map((i) => ({
|
|
398
1033
|
id: i.id,
|
|
399
1034
|
name: i.name,
|
|
400
1035
|
route: "direct",
|
|
401
|
-
connected:
|
|
1036
|
+
connected: i.connected
|
|
402
1037
|
}));
|
|
403
1038
|
const direct = new Set(out.map((p) => p.id));
|
|
404
|
-
for (const m of node.net.
|
|
1039
|
+
for (const m of await node.net.relayMembers()) {
|
|
405
1040
|
if (direct.has(m.id)) continue;
|
|
406
1041
|
out.push({
|
|
407
1042
|
id: m.id,
|
|
@@ -414,8 +1049,8 @@ function peers(node) {
|
|
|
414
1049
|
}
|
|
415
1050
|
return out;
|
|
416
1051
|
}
|
|
417
|
-
function resolve(node, ref) {
|
|
418
|
-
const all = peers(node);
|
|
1052
|
+
async function resolve(node, ref) {
|
|
1053
|
+
const all = await peers(node);
|
|
419
1054
|
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
1055
|
}
|
|
421
1056
|
var TOOLS = [
|
|
@@ -426,16 +1061,20 @@ var TOOLS = [
|
|
|
426
1061
|
key: { type: "boolean", description: "Include the full public key string" }
|
|
427
1062
|
}),
|
|
428
1063
|
async run(node, args) {
|
|
429
|
-
const key = node.net.
|
|
430
|
-
const online = peers(node).filter((p) => p.connected).length;
|
|
1064
|
+
const key = await node.net.identity();
|
|
1065
|
+
const online = (await peers(node)).filter((p) => p.connected).length;
|
|
431
1066
|
const lines = [
|
|
432
1067
|
`name ${node.options.name}`,
|
|
433
1068
|
`instanceId ${key?.instanceId ?? "(not started)"}`,
|
|
434
1069
|
`hub ${node.hubStatus}`,
|
|
435
1070
|
`connected ${online} peer(s) online`,
|
|
436
1071
|
`listening port ${node.options.port} (reachable only from networks that can dial it)`,
|
|
437
|
-
`store ${node.options.dir}
|
|
1072
|
+
`store ${node.options.dir}`,
|
|
1073
|
+
`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
1074
|
];
|
|
1075
|
+
lines.push(
|
|
1076
|
+
`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"}`
|
|
1077
|
+
);
|
|
439
1078
|
if (args.key) lines.push("", `publicKey ${key?.publicKeyString ?? "-"}`);
|
|
440
1079
|
else
|
|
441
1080
|
lines.push(
|
|
@@ -455,7 +1094,7 @@ var TOOLS = [
|
|
|
455
1094
|
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
1095
|
inputSchema: obj({}),
|
|
457
1096
|
async run(node) {
|
|
458
|
-
const all = peers(node);
|
|
1097
|
+
const all = await peers(node);
|
|
459
1098
|
if (all.length === 0) {
|
|
460
1099
|
return "No peers. Join a hub (VOLENET_MCP_HUB) or pair with a node directly (volenet_pair).";
|
|
461
1100
|
}
|
|
@@ -512,7 +1151,7 @@ var TOOLS = [
|
|
|
512
1151
|
const to = String(args.to ?? "");
|
|
513
1152
|
const text = String(args.text ?? "");
|
|
514
1153
|
if (!text.trim()) return "Nothing to send.";
|
|
515
|
-
const peer = resolve(node, to);
|
|
1154
|
+
const peer = await resolve(node, to);
|
|
516
1155
|
const res = await node.net.sendChat(peer?.id ?? to, text);
|
|
517
1156
|
if (!res.ok) return `Not sent: ${res.error ?? "unknown error"}`;
|
|
518
1157
|
await node.inbox.add({
|
|
@@ -537,7 +1176,7 @@ var TOOLS = [
|
|
|
537
1176
|
limit: num("Messages (default 50)")
|
|
538
1177
|
}),
|
|
539
1178
|
async run(node, args) {
|
|
540
|
-
const peer = resolve(node, String(args.peer ?? ""));
|
|
1179
|
+
const peer = await resolve(node, String(args.peer ?? ""));
|
|
541
1180
|
const id = peer?.id ?? String(args.peer ?? "");
|
|
542
1181
|
const msgs = node.inbox.history(id, Number(args.limit ?? 50));
|
|
543
1182
|
if (msgs.length === 0) return `Nothing recorded with ${peer?.name ?? id}.`;
|
|
@@ -556,16 +1195,15 @@ var TOOLS = [
|
|
|
556
1195
|
["to", "question"]
|
|
557
1196
|
),
|
|
558
1197
|
async run(node, args) {
|
|
559
|
-
const peer = resolve(node, String(args.to ?? ""));
|
|
1198
|
+
const peer = await resolve(node, String(args.to ?? ""));
|
|
560
1199
|
if (!peer) return `No peer found: "${args.to}". Use volenet_peers.`;
|
|
561
1200
|
if (peer.route !== "direct") {
|
|
562
1201
|
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
1202
|
}
|
|
564
|
-
const
|
|
565
|
-
if (!mgr) return "Remote task manager not available.";
|
|
566
|
-
const res = await mgr.delegateTask(
|
|
1203
|
+
const res = await node.net.askBrain(
|
|
567
1204
|
peer.id,
|
|
568
|
-
|
|
1205
|
+
String(args.question ?? ""),
|
|
1206
|
+
node.options.name,
|
|
569
1207
|
Number(args.timeout_ms ?? 12e4)
|
|
570
1208
|
);
|
|
571
1209
|
if (res.status === "completed") return `${peer.name} says:
|
|
@@ -575,7 +1213,7 @@ ${res.result}`;
|
|
|
575
1213
|
if (typeof why === "string" && why.includes("allowBrain")) {
|
|
576
1214
|
return `${peer.name} refused: ${why}
|
|
577
1215
|
|
|
578
|
-
Its operator can allow this session by adding { "id": "${node.net.
|
|
1216
|
+
Its operator can allow this session by adding { "id": "${(await node.net.identity())?.instanceId}", "trust": "read", "allowBrain": true } to net.peers and restarting.`;
|
|
579
1217
|
}
|
|
580
1218
|
return `${peer.name} did not answer: ${why}`;
|
|
581
1219
|
}
|
|
@@ -598,7 +1236,7 @@ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.
|
|
|
598
1236
|
node.requests.splice(node.requests.indexOf(req), 1);
|
|
599
1237
|
return ok.ok ? `${accept ? "Accepted" : "Denied"} ${req.fromName}.` : `Failed: ${"error" in ok ? ok.error : "unknown"}`;
|
|
600
1238
|
}
|
|
601
|
-
const pairs = node.net.listPairRequests();
|
|
1239
|
+
const pairs = await node.net.listPairRequests();
|
|
602
1240
|
for (const p of pairs) {
|
|
603
1241
|
if (!node.requests.some((r) => r.from === p.id)) {
|
|
604
1242
|
node.requests.push({ kind: "pair", from: p.id, fromName: p.name, at: Date.now() });
|
|
@@ -619,7 +1257,7 @@ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.
|
|
|
619
1257
|
}),
|
|
620
1258
|
async run(node, args) {
|
|
621
1259
|
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;
|
|
1260
|
+
const want = args.from ? await resolve(node, String(args.from)) : void 0;
|
|
623
1261
|
const wanted = (m) => !args.from || m.peerId === (want?.id ?? args.from);
|
|
624
1262
|
const already = node.inbox.unread().filter(wanted);
|
|
625
1263
|
if (already.length === 0) {
|
|
@@ -655,7 +1293,7 @@ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.
|
|
|
655
1293
|
if (args.leave) {
|
|
656
1294
|
const hub = node.options.hub;
|
|
657
1295
|
if (!hub) return "Not on a hub.";
|
|
658
|
-
node.net.forgetPeer(hub);
|
|
1296
|
+
await node.net.forgetPeer(hub);
|
|
659
1297
|
node.options.hub = void 0;
|
|
660
1298
|
node.hubStatus = "no hub configured";
|
|
661
1299
|
await saveStored(node.options.dir, { hub: void 0 });
|
|
@@ -674,7 +1312,7 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
674
1312
|
await saveStored(node.options.dir, { hub: url });
|
|
675
1313
|
return `Joined ${url} again \u2014 it was already trusted, so no introduction was needed.`;
|
|
676
1314
|
}
|
|
677
|
-
const res = await node.net.
|
|
1315
|
+
const res = await node.net.joinHub(url);
|
|
678
1316
|
if (!res.ok) return `Could not join ${url}: ${res.error}`;
|
|
679
1317
|
node.options.hub = url;
|
|
680
1318
|
node.hubStatus = res.pending ? `waiting for approval at ${url}` : `joined ${res.hubName ?? url}`;
|
|
@@ -682,12 +1320,84 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
682
1320
|
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
1321
|
}
|
|
684
1322
|
},
|
|
1323
|
+
{
|
|
1324
|
+
name: "volenet_room",
|
|
1325
|
+
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.",
|
|
1326
|
+
inputSchema: obj({
|
|
1327
|
+
room: str("Which room, by id or name (see the list)"),
|
|
1328
|
+
post: str("Say this to the room"),
|
|
1329
|
+
create: str("Make a room with this name"),
|
|
1330
|
+
join: str("Join a room by id"),
|
|
1331
|
+
leave: { type: "boolean", description: "Leave the room named in `room`" },
|
|
1332
|
+
invite: str("Bring this peer (name or id) into the room named in `room`"),
|
|
1333
|
+
hub: str("Which hub holds the room. Only needed with more than one.")
|
|
1334
|
+
}),
|
|
1335
|
+
async run(node, args) {
|
|
1336
|
+
const hubOf = async () => {
|
|
1337
|
+
if (args.hub) return String(args.hub);
|
|
1338
|
+
const hubs = (await node.net.instances()).filter((i) => i.connected);
|
|
1339
|
+
if (hubs.length === 0) return null;
|
|
1340
|
+
return hubs[0].id;
|
|
1341
|
+
};
|
|
1342
|
+
const find = async (ref) => {
|
|
1343
|
+
const all2 = await node.net.rooms();
|
|
1344
|
+
return all2.find((r) => r.room === ref) ?? all2.find((r) => r.name === ref);
|
|
1345
|
+
};
|
|
1346
|
+
if (args.create || args.join || args.leave || args.invite) {
|
|
1347
|
+
const hub = await hubOf();
|
|
1348
|
+
if (!hub)
|
|
1349
|
+
return "No hub connected. A room lives on a hub \u2014 join one first with volenet_hub.";
|
|
1350
|
+
if (args.create) {
|
|
1351
|
+
const res2 = await node.net.roomCommand(hub, "room:create", { name: String(args.create) });
|
|
1352
|
+
return res2.ok ? `Asked for a room called "${args.create}". Call this again in a moment to see it.` : `Could not: ${res2.error}`;
|
|
1353
|
+
}
|
|
1354
|
+
if (args.join) {
|
|
1355
|
+
const res2 = await node.net.roomCommand(hub, "room:join", { room: String(args.join) });
|
|
1356
|
+
return res2.ok ? `Asked to join ${args.join}.` : `Could not: ${res2.error}`;
|
|
1357
|
+
}
|
|
1358
|
+
const room = args.room ? await find(String(args.room)) : void 0;
|
|
1359
|
+
if (!room) return "Name the room with `room` \u2014 see the list.";
|
|
1360
|
+
if (args.leave) {
|
|
1361
|
+
const res2 = await node.net.roomCommand(hub, "room:leave", { room: room.room });
|
|
1362
|
+
return res2.ok ? `Left ${room.name}.` : `Could not: ${res2.error}`;
|
|
1363
|
+
}
|
|
1364
|
+
const peer = await resolve(node, String(args.invite));
|
|
1365
|
+
const res = await node.net.roomCommand(hub, "room:invite", {
|
|
1366
|
+
room: room.room,
|
|
1367
|
+
member: peer?.id ?? String(args.invite)
|
|
1368
|
+
});
|
|
1369
|
+
return res.ok ? `Invited ${peer?.name ?? args.invite} to ${room.name}.` : `Could not: ${res.error}`;
|
|
1370
|
+
}
|
|
1371
|
+
if (args.post) {
|
|
1372
|
+
const room = args.room ? await find(String(args.room)) : (await node.net.rooms())[0];
|
|
1373
|
+
if (!room) return "No room to post to. Create or join one first.";
|
|
1374
|
+
const res = await node.net.postToRoom(room.room, String(args.post));
|
|
1375
|
+
if (!res.ok) return `Not posted: ${res.error}`;
|
|
1376
|
+
const bits = [`Posted to ${room.name}: ${res.sent} delivered`];
|
|
1377
|
+
if (res.held) bits.push(`${res.held} waiting for members who are away`);
|
|
1378
|
+
if (res.skipped)
|
|
1379
|
+
bits.push(`${res.skipped} could not be reached \u2014 they may not have accepted you`);
|
|
1380
|
+
return `${bits.join(", ")}.`;
|
|
1381
|
+
}
|
|
1382
|
+
const all = await node.net.rooms();
|
|
1383
|
+
if (all.length === 0) {
|
|
1384
|
+
return 'Not in any room. Create one with create:"name", or join one you have been given the id for.';
|
|
1385
|
+
}
|
|
1386
|
+
return all.map(
|
|
1387
|
+
(r) => ` ${r.name} ${r.room.substring(0, 8)} ${r.members.length} member(s): ${r.members.map((m) => m.name).join(", ")}`
|
|
1388
|
+
).join("\n");
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
685
1391
|
{
|
|
686
1392
|
name: "volenet_connect",
|
|
687
1393
|
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
1394
|
inputSchema: obj({
|
|
689
1395
|
url: str("Node URL to pair with directly, e.g. http://10.0.0.5:9700"),
|
|
690
1396
|
confirm: str("The fingerprint returned by a first call with url, confirming who answers"),
|
|
1397
|
+
brain: {
|
|
1398
|
+
type: "boolean",
|
|
1399
|
+
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."
|
|
1400
|
+
},
|
|
691
1401
|
member: str("Hub member name or id to ask for chat consent"),
|
|
692
1402
|
note: str("A line saying who you are")
|
|
693
1403
|
}),
|
|
@@ -711,8 +1421,18 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
711
1421
|
if (!probe.fingerprint?.startsWith(confirm)) {
|
|
712
1422
|
return `That fingerprint does not match: it answers with ${probe.fingerprint}. Nothing was trusted.`;
|
|
713
1423
|
}
|
|
714
|
-
const res = await node.net.initiatePair(
|
|
715
|
-
|
|
1424
|
+
const res = await node.net.initiatePair(
|
|
1425
|
+
url,
|
|
1426
|
+
probe.publicKey,
|
|
1427
|
+
note,
|
|
1428
|
+
args.brain ? ["brain"] : void 0
|
|
1429
|
+
);
|
|
1430
|
+
if (!res.ok) return `Could not ask: ${res.error}`;
|
|
1431
|
+
return [
|
|
1432
|
+
`Trusted ${probe.name ?? url} and asked it to trust this session.`,
|
|
1433
|
+
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.",
|
|
1434
|
+
"Nothing arrives until their operator accepts."
|
|
1435
|
+
].join(" ");
|
|
716
1436
|
}
|
|
717
1437
|
if (args.member) {
|
|
718
1438
|
const res = await node.net.requestRelayConnect(String(args.member), note);
|
|
@@ -725,6 +1445,9 @@ Call with leave:true to come off it, or url to move to another.` : "Not on a hub
|
|
|
725
1445
|
];
|
|
726
1446
|
|
|
727
1447
|
// src/index.ts
|
|
1448
|
+
init_inbox();
|
|
1449
|
+
init_config();
|
|
1450
|
+
init_node();
|
|
728
1451
|
function unreadFooter(node, toolName) {
|
|
729
1452
|
if (toolName === "volenet_inbox" || toolName === "volenet_wait") return "";
|
|
730
1453
|
const unread = node.inbox.unread();
|
|
@@ -734,8 +1457,34 @@ function unreadFooter(node, toolName) {
|
|
|
734
1457
|
|
|
735
1458
|
\u2014 ${unread.length} unread message${unread.length === 1 ? "" : "s"} from ${who}. Read them with volenet_inbox.`;
|
|
736
1459
|
}
|
|
737
|
-
function
|
|
738
|
-
const server = new Server(
|
|
1460
|
+
function createServer2(node) {
|
|
1461
|
+
const server = new Server(
|
|
1462
|
+
{ name: "volenet", version: "0.1.0" },
|
|
1463
|
+
{ capabilities: { tools: {}, prompts: {} } }
|
|
1464
|
+
);
|
|
1465
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1466
|
+
prompts: PROMPTS.map((p) => ({
|
|
1467
|
+
name: p.name,
|
|
1468
|
+
description: p.description,
|
|
1469
|
+
...p.arguments ? { arguments: p.arguments } : {}
|
|
1470
|
+
}))
|
|
1471
|
+
}));
|
|
1472
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1473
|
+
const prompt = PROMPTS.find((p) => p.name === request.params.name);
|
|
1474
|
+
if (!prompt) throw new Error(`No such prompt: ${request.params.name}`);
|
|
1475
|
+
return {
|
|
1476
|
+
description: prompt.description,
|
|
1477
|
+
messages: [
|
|
1478
|
+
{
|
|
1479
|
+
role: "user",
|
|
1480
|
+
content: {
|
|
1481
|
+
type: "text",
|
|
1482
|
+
text: prompt.render(request.params.arguments ?? {})
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
]
|
|
1486
|
+
};
|
|
1487
|
+
});
|
|
739
1488
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
740
1489
|
tools: TOOLS.map((t) => ({
|
|
741
1490
|
name: t.name,
|
|
@@ -765,13 +1514,31 @@ function createServer(node) {
|
|
|
765
1514
|
});
|
|
766
1515
|
return server;
|
|
767
1516
|
}
|
|
1517
|
+
async function recordClientCapabilities(dir, caps) {
|
|
1518
|
+
try {
|
|
1519
|
+
const fs4 = await import("fs/promises");
|
|
1520
|
+
const path7 = await import("path");
|
|
1521
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1522
|
+
await fs4.writeFile(
|
|
1523
|
+
path7.join(dir, "client.json"),
|
|
1524
|
+
`${JSON.stringify(caps ?? {}, null, 2)}
|
|
1525
|
+
`,
|
|
1526
|
+
"utf-8"
|
|
1527
|
+
);
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
768
1531
|
async function main() {
|
|
769
1532
|
const options = await resolveSettings();
|
|
770
1533
|
const node = await startNode(options);
|
|
771
|
-
const server =
|
|
1534
|
+
const server = createServer2(node);
|
|
772
1535
|
await server.connect(new StdioServerTransport());
|
|
1536
|
+
const caps = server.getClientCapabilities();
|
|
1537
|
+
node.canSample = Boolean(caps && typeof caps === "object" && "sampling" in caps);
|
|
1538
|
+
await recordClientCapabilities(options.dir, caps);
|
|
1539
|
+
const me = await node.net.identity().catch(() => null);
|
|
773
1540
|
process.stderr.write(
|
|
774
|
-
`volenet-mcp: ${options.name} (${
|
|
1541
|
+
`volenet-mcp: ${options.name} (${me?.instanceId.substring(0, 8) ?? "?"}) ready \u2014 node ${node.where}${options.hub ? `, hub ${options.hub}` : ", no hub configured"}
|
|
775
1542
|
`
|
|
776
1543
|
);
|
|
777
1544
|
let stopping = false;
|
|
@@ -787,7 +1554,7 @@ async function main() {
|
|
|
787
1554
|
}
|
|
788
1555
|
if (process.argv[1]?.includes("volenet-mcp") || process.env.VOLENET_MCP_RUN === "1") {
|
|
789
1556
|
if (process.argv[2] || process.stdin.isTTY) {
|
|
790
|
-
|
|
1557
|
+
run2(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
|
|
791
1558
|
process.stderr.write(`volenet-mcp: ${err instanceof Error ? err.message : String(err)}
|
|
792
1559
|
`);
|
|
793
1560
|
process.exit(1);
|
|
@@ -802,13 +1569,15 @@ if (process.argv[1]?.includes("volenet-mcp") || process.env.VOLENET_MCP_RUN ===
|
|
|
802
1569
|
}
|
|
803
1570
|
export {
|
|
804
1571
|
Inbox,
|
|
1572
|
+
PROMPTS,
|
|
805
1573
|
TOOLS,
|
|
806
|
-
createServer,
|
|
1574
|
+
createServer2 as createServer,
|
|
807
1575
|
defaultDir,
|
|
808
1576
|
defaultName,
|
|
809
1577
|
loadStored,
|
|
1578
|
+
recordClientCapabilities,
|
|
810
1579
|
resolveSettings,
|
|
811
|
-
|
|
1580
|
+
run2 as runCli,
|
|
812
1581
|
saveStored,
|
|
813
1582
|
startNode,
|
|
814
1583
|
unreadFooter
|