@eamonpluto/agentboard 2.3.0 → 2.4.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/AGENTBOARD.html +207 -0
- package/AGENTS.template.md +21 -11
- package/CHANGELOG.md +40 -0
- package/README.md +51 -10
- package/bin/agentboard-hook.js +13 -5
- package/bin/agentboard-mcp.js +103 -20
- package/bin/agentboard.js +423 -68
- package/opencode/plugins/dm-watch.js +69 -10
- package/opencode/tools/dm-send.js +132 -35
- package/package.json +2 -1
package/bin/agentboard-mcp.js
CHANGED
|
@@ -27,9 +27,10 @@ import os from "node:os";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import crypto from "node:crypto";
|
|
29
29
|
import readline from "node:readline";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
30
31
|
|
|
31
32
|
const MAX_BODY_CHARS = 8000;
|
|
32
|
-
const SERVER_VERSION = "2.
|
|
33
|
+
const SERVER_VERSION = "2.2.0";
|
|
33
34
|
const KNOWN_PROTOCOL_VERSIONS = new Set(["2024-11-05", "2025-03-26", "2025-06-18"]);
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -82,9 +83,49 @@ function readJson(p) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function writeJson(p, obj) {
|
|
85
|
-
const tmp = p + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
86
|
+
const tmp = p + "." + process.pid + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
86
87
|
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + "\n");
|
|
87
|
-
|
|
88
|
+
try {
|
|
89
|
+
fs.renameSync(tmp, p);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
while (Date.now() - start < 50) { /* brief spin for Windows AV holds */ }
|
|
93
|
+
try {
|
|
94
|
+
fs.renameSync(tmp, p);
|
|
95
|
+
} catch (e2) {
|
|
96
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
97
|
+
throw e2;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Best-effort git rev of the project containing the board. Never throws.
|
|
103
|
+
function gitRev(root) {
|
|
104
|
+
try {
|
|
105
|
+
const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
106
|
+
cwd: path.dirname(root),
|
|
107
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
108
|
+
timeout: 3000,
|
|
109
|
+
});
|
|
110
|
+
return String(out).trim().slice(0, 40) || undefined;
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseRecipients(raw) {
|
|
117
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") {
|
|
118
|
+
throw new Error("missing to (recipient); or comma-separate for broadcast: alice,bob,carol");
|
|
119
|
+
}
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const part of String(raw).split(",")) {
|
|
122
|
+
if (part.trim() === "") continue;
|
|
123
|
+
const c = cleanName(part, "to");
|
|
124
|
+
if (!out.includes(c)) out.push(c);
|
|
125
|
+
}
|
|
126
|
+
if (out.length === 0) throw new Error("missing to (recipient)");
|
|
127
|
+
if (out.length > 20) throw new Error(`too many recipients (max 20, got ${out.length})`);
|
|
128
|
+
return out;
|
|
88
129
|
}
|
|
89
130
|
|
|
90
131
|
function cleanName(name, what) {
|
|
@@ -105,7 +146,7 @@ function newId(prefix) {
|
|
|
105
146
|
String(t.getUTCHours()).padStart(2, "0") +
|
|
106
147
|
String(t.getUTCMinutes()).padStart(2, "0") +
|
|
107
148
|
String(t.getUTCSeconds()).padStart(2, "0");
|
|
108
|
-
return `${prefix}-${stamp}-${crypto.randomBytes(
|
|
149
|
+
return `${prefix}-${stamp}-${crypto.randomBytes(4).toString("hex")}`;
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
function listDMs(d, recipient) {
|
|
@@ -150,13 +191,15 @@ const TOOLS = [
|
|
|
150
191
|
{
|
|
151
192
|
name: "dm_send",
|
|
152
193
|
description:
|
|
153
|
-
"Send a direct message to another AI agent via agent-board. Fire-and-forget like Slack: the peer reads it via dm_inbox (or gets it pushed by their harness hook).
|
|
194
|
+
"Send a direct message to another AI agent via agent-board. Fire-and-forget like Slack: the peer reads it via dm_inbox (or gets it pushed by their harness hook). `to` accepts a comma list for broadcast (one copy each, shared batch id) to fan work out to N agents — the DM is the task. Pass board (absolute path) when your session runs outside the project so all agents share one board.",
|
|
154
195
|
inputSchema: {
|
|
155
196
|
type: "object",
|
|
156
197
|
properties: {
|
|
157
198
|
from: { type: "string", description: "Your stable agent name, e.g. alice. Keep it constant for the session." },
|
|
158
|
-
to: { type: "string", description: "Recipient agent name, e.g. bob." },
|
|
199
|
+
to: { type: "string", description: "Recipient agent name, e.g. bob — or comma list for broadcast: alice,bob,carol." },
|
|
159
200
|
body: { type: "string", description: "Message text, 1..8000 chars." },
|
|
201
|
+
subject: { type: "string", description: "Optional mission line, e.g. 'brief: borderless cards'. Shown above the body." },
|
|
202
|
+
replyTo: { type: "string", description: "Optional message id you are answering (threads the reply)." },
|
|
160
203
|
board: { type: "string", description: "Optional absolute board path, e.g. C:/proj/.agentboard. Overrides AGENTBOARD_DIR and auto-detection." },
|
|
161
204
|
},
|
|
162
205
|
required: ["from", "to", "body"],
|
|
@@ -215,31 +258,69 @@ function isDriveRootMissing(root, explicit) {
|
|
|
215
258
|
return path.dirname(root) === path.parse(root).root;
|
|
216
259
|
}
|
|
217
260
|
|
|
261
|
+
// Read-side tools must never plant a board: without board.json, report the
|
|
262
|
+
// resolved path so the caller spots the split-board instead of an empty room.
|
|
263
|
+
function requireBoard(root) {
|
|
264
|
+
let meta = null;
|
|
265
|
+
try {
|
|
266
|
+
meta = readJson(path.join(root, "board.json"));
|
|
267
|
+
} catch {}
|
|
268
|
+
if (!meta || meta.version !== 2) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`no board at ${root} (cwd "${process.cwd()}"). Run from your project, pass board (absolute path to .agentboard), or set AGENTBOARD_DIR.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return dirs(root);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatInbox(m) {
|
|
277
|
+
const bits = [`from ${m.from}`, `@ ${m.at || "unknown time"}`];
|
|
278
|
+
if (m.rev) bits.push(`rev ${m.rev}`);
|
|
279
|
+
if (m.replyTo) bits.push(`re: ${m.replyTo}`);
|
|
280
|
+
if (m.batch) bits.push(`batch ${m.batch}`);
|
|
281
|
+
return `[${m.id}] ${bits.join(" ")}${m.subject ? `\nsubj: ${m.subject}` : ""}\n${m.body}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
218
284
|
function callTool(name, args) {
|
|
219
285
|
const a = args && typeof args === "object" ? args : {};
|
|
220
286
|
const boardArg = a.board === undefined || a.board === null || String(a.board).trim() === "" ? undefined : String(a.board);
|
|
221
287
|
const root = boardRoot(boardArg);
|
|
222
288
|
if (isDriveRootMissing(root, boardArg || process.env.AGENTBOARD_DIR)) {
|
|
223
289
|
throw new Error(
|
|
224
|
-
`refusing to create a board at drive root ${root} —
|
|
290
|
+
`refusing to create a board at drive root ${root} (cwd "${process.cwd()}") — no project board found above cwd. Pass board (absolute path to .agentboard) or set AGENTBOARD_DIR.`
|
|
225
291
|
);
|
|
226
292
|
}
|
|
227
|
-
const d = ensureBoard(root);
|
|
228
293
|
switch (name) {
|
|
229
294
|
case "dm_send": {
|
|
295
|
+
const d = ensureBoard(root);
|
|
230
296
|
const from = cleanName(a.from, "from");
|
|
231
|
-
const
|
|
297
|
+
const recipients = parseRecipients(a.to);
|
|
232
298
|
const body = String(a.body ?? "").trim();
|
|
233
299
|
if (!body) throw new Error("empty body");
|
|
234
300
|
if (body.length > MAX_BODY_CHARS) throw new Error(`body too large (max ${MAX_BODY_CHARS} chars)`);
|
|
301
|
+
const subject = a.subject === undefined || a.subject === null || String(a.subject).trim() === "" ? undefined : String(a.subject).trim().slice(0, 120);
|
|
302
|
+
const replyTo = a.replyTo === undefined || a.replyTo === null || String(a.replyTo).trim() === "" ? undefined : String(a.replyTo).trim().slice(0, 80);
|
|
235
303
|
touchAgent(d, from);
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
304
|
+
const rev = gitRev(root);
|
|
305
|
+
const at = new Date().toISOString();
|
|
306
|
+
const batch = recipients.length > 1 ? newId("batch") : undefined;
|
|
307
|
+
const sent = [];
|
|
308
|
+
for (const to of recipients) {
|
|
309
|
+
const id = newId("msg");
|
|
310
|
+
const msg = { id, from, to, body, at };
|
|
311
|
+
if (subject) msg.subject = subject;
|
|
312
|
+
if (replyTo) msg.replyTo = replyTo;
|
|
313
|
+
if (batch) msg.batch = batch;
|
|
314
|
+
if (rev) msg.rev = rev;
|
|
315
|
+
fs.mkdirSync(path.join(d.dm, to), { recursive: true });
|
|
316
|
+
writeJson(path.join(d.dm, to, `${id}.json`), msg);
|
|
317
|
+
sent.push(`${id} -> ${to}`);
|
|
318
|
+
}
|
|
319
|
+
if (sent.length === 1) return toolResult(`sent ${sent[0]} [board ${d.root}]`);
|
|
320
|
+
return toolResult(`sent ${sent.length} messages [board ${d.root}]: ${sent.join(", ")}`);
|
|
241
321
|
}
|
|
242
322
|
case "dm_inbox": {
|
|
323
|
+
const d = requireBoard(root);
|
|
243
324
|
const agent = cleanName(a.agent, "agent");
|
|
244
325
|
let items = listDMs(d, agent);
|
|
245
326
|
if (a.after !== undefined && a.after !== null && String(a.after) !== "") {
|
|
@@ -249,12 +330,13 @@ function callTool(name, args) {
|
|
|
249
330
|
const limit = a.limit === undefined || a.limit === null ? 20 : Number(a.limit);
|
|
250
331
|
if (!(limit >= 0)) throw new Error("limit must be a non-negative number");
|
|
251
332
|
items = items.slice(-limit);
|
|
252
|
-
if (items.length === 0) return toolResult(`no messages for ${agent}`);
|
|
253
|
-
return toolResult(items.map(
|
|
333
|
+
if (items.length === 0) return toolResult(`no messages for ${agent} [board ${d.root}]`);
|
|
334
|
+
return toolResult(items.map(formatInbox).join("\n\n"));
|
|
254
335
|
}
|
|
255
336
|
case "dm_agents": {
|
|
337
|
+
const d = requireBoard(root);
|
|
256
338
|
const dir = d.agents;
|
|
257
|
-
if (!fs.existsSync(dir)) return toolResult(
|
|
339
|
+
if (!fs.existsSync(dir)) return toolResult(`no agents registered [board ${d.root}]`);
|
|
258
340
|
const names = fs
|
|
259
341
|
.readdirSync(dir)
|
|
260
342
|
.filter((f) => f.endsWith(".json"))
|
|
@@ -267,14 +349,15 @@ function callTool(name, args) {
|
|
|
267
349
|
})
|
|
268
350
|
.filter(Boolean)
|
|
269
351
|
.sort();
|
|
270
|
-
if (names.length === 0) return toolResult(
|
|
271
|
-
return toolResult(names.join("\n"));
|
|
352
|
+
if (names.length === 0) return toolResult(`no agents registered (dm_register -- your name) [board ${d.root}]`);
|
|
353
|
+
return toolResult(names.join("\n") + `\n[board ${d.root}]`);
|
|
272
354
|
}
|
|
273
355
|
case "dm_register": {
|
|
356
|
+
const d = ensureBoard(root);
|
|
274
357
|
const agent = cleanName(a.agent, "agent");
|
|
275
358
|
const session = a.session === undefined || a.session === null ? undefined : String(a.session);
|
|
276
359
|
touchAgent(d, agent, session || undefined);
|
|
277
|
-
return toolResult(`registered ${agent}${session ? ` (session ${session})` : ""}`);
|
|
360
|
+
return toolResult(`registered ${agent}${session ? ` (session ${session})` : ""} [board ${d.root}]`);
|
|
278
361
|
}
|
|
279
362
|
default:
|
|
280
363
|
throw new Error(`unknown tool "${name}"`);
|