herdr-plugin-amq 0.1.2
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/LICENSE +21 -0
- package/README.md +263 -0
- package/bin/herdr-amq.mjs +74 -0
- package/herdr-plugin.toml +57 -0
- package/package.json +50 -0
- package/skills/herdr-amq/SKILL.md +69 -0
- package/src/actions.mjs +573 -0
- package/src/blobs.mjs +348 -0
- package/src/board.mjs +760 -0
- package/src/bridge.mjs +373 -0
- package/src/briefs.mjs +201 -0
- package/src/config.mjs +146 -0
- package/src/herdr.mjs +215 -0
- package/src/index.mjs +4 -0
- package/src/markdown.mjs +167 -0
- package/src/panes.mjs +68 -0
- package/src/protocol.mjs +347 -0
- package/src/server.mjs +773 -0
- package/src/store.mjs +1063 -0
- package/src/web/app.js +3066 -0
- package/src/web/index.html +727 -0
- package/src/web/style.css +3842 -0
- package/src/worktrees.mjs +217 -0
package/src/actions.mjs
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
findAmqRoot,
|
|
6
|
+
getAgentHandles,
|
|
7
|
+
getStateDir,
|
|
8
|
+
getConfigDir,
|
|
9
|
+
getEventContext,
|
|
10
|
+
} from "./config.mjs";
|
|
11
|
+
import {
|
|
12
|
+
isDaemonRunning,
|
|
13
|
+
startDaemonBackground,
|
|
14
|
+
stopDaemon,
|
|
15
|
+
runDoorbellPass,
|
|
16
|
+
listInbox,
|
|
17
|
+
} from "./bridge.mjs";
|
|
18
|
+
import {
|
|
19
|
+
loadBoard,
|
|
20
|
+
addBoardTask,
|
|
21
|
+
updateBoardTask,
|
|
22
|
+
deleteBoardTask,
|
|
23
|
+
} from "./board.mjs";
|
|
24
|
+
import {
|
|
25
|
+
sendMaildirMessage,
|
|
26
|
+
replyMaildirMessage,
|
|
27
|
+
drainMaildir,
|
|
28
|
+
} from "./protocol.mjs";
|
|
29
|
+
|
|
30
|
+
export function handleStatus() {
|
|
31
|
+
const amqRoot = findAmqRoot();
|
|
32
|
+
const pid = isDaemonRunning();
|
|
33
|
+
const handles = amqRoot ? getAgentHandles(amqRoot) : [];
|
|
34
|
+
|
|
35
|
+
console.log("\n📦 \x1b[1mHerdr AMQ Bridge Status\x1b[0m");
|
|
36
|
+
console.log("──────────────────────────────────────────────");
|
|
37
|
+
console.log(`Daemon: ${pid ? `\x1b[32m● Running\x1b[0m (PID ${pid})` : "\x1b[33m○ Stopped\x1b[0m"}`);
|
|
38
|
+
console.log(`AMQ Root: ${amqRoot ? `\x1b[36m${amqRoot}\x1b[0m` : "\x1b[31mNot found\x1b[0m"}`);
|
|
39
|
+
console.log(`State Dir: ${getStateDir()}`);
|
|
40
|
+
console.log(`Config: ${getConfigDir()}`);
|
|
41
|
+
console.log("──────────────────────────────────────────────");
|
|
42
|
+
|
|
43
|
+
if (!amqRoot) {
|
|
44
|
+
console.log("⚠️ No active .agent-mail directory found in current workspace or path.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`\x1b[1mRegistered Agents (${handles.length}):\x1b[0m`);
|
|
49
|
+
if (!handles.length) {
|
|
50
|
+
console.log(" (no agents registered in config)");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let totalUnread = 0;
|
|
55
|
+
for (const h of handles) {
|
|
56
|
+
const unread = listInbox(amqRoot, h);
|
|
57
|
+
const count = unread.length;
|
|
58
|
+
totalUnread += count;
|
|
59
|
+
|
|
60
|
+
const countBadge = count > 0
|
|
61
|
+
? `\x1b[33m${count} new\x1b[0m`
|
|
62
|
+
: `\x1b[90m0 new\x1b[0m`;
|
|
63
|
+
|
|
64
|
+
const senders = count > 0
|
|
65
|
+
? `(from ${[...new Set(unread.map((m) => m.from))].join(", ")})`
|
|
66
|
+
: "";
|
|
67
|
+
|
|
68
|
+
console.log(` • \x1b[1m${h.padEnd(16)}\x1b[0m ${countBadge} ${senders}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("──────────────────────────────────────────────");
|
|
72
|
+
console.log(`Total Unread: ${totalUnread}`);
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function handleStart() {
|
|
77
|
+
const res = startDaemonBackground();
|
|
78
|
+
if (res.alreadyRunning) {
|
|
79
|
+
console.log(`ℹ️ Bridge daemon is already running (PID ${res.pid}).`);
|
|
80
|
+
} else if (res.ok) {
|
|
81
|
+
console.log(`🚀 Started bridge daemon in background (PID ${res.pid}).`);
|
|
82
|
+
} else {
|
|
83
|
+
console.error(`❌ Failed to start bridge daemon.`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function handleStop() {
|
|
89
|
+
const res = stopDaemon();
|
|
90
|
+
if (res.ok) {
|
|
91
|
+
console.log(`🛑 ${res.message}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`❌ Failed to stop daemon: ${res.error}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function handleDoorbell() {
|
|
99
|
+
const amqRoot = findAmqRoot();
|
|
100
|
+
if (!amqRoot) {
|
|
101
|
+
console.error("❌ No .agent-mail directory found.");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`🔔 Checking AMQ inboxes at ${amqRoot}...`);
|
|
106
|
+
const res = runDoorbellPass({ amqRoot });
|
|
107
|
+
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
console.error(`❌ Doorbell check failed: ${res.error}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`Checked ${res.agentsChecked} agents. Doorbelled: ${res.doorbelled} message(s).`);
|
|
114
|
+
for (const r of res.results || []) {
|
|
115
|
+
console.log(` - ${r.handle} (${r.status}): ${r.count} msg(s) -> ${r.action}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function handleStartup() {
|
|
120
|
+
const amqRoot = findAmqRoot();
|
|
121
|
+
console.log(`[herdr-amq] Startup hook executed. Found AMQ root: ${amqRoot || "none"}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function handleAgentStatusChanged() {
|
|
125
|
+
const evt = getEventContext();
|
|
126
|
+
if (!evt) return;
|
|
127
|
+
|
|
128
|
+
const data = evt.data || evt;
|
|
129
|
+
const status = data.agent_status;
|
|
130
|
+
const handle = data.agent_name || data.handle || data.title;
|
|
131
|
+
|
|
132
|
+
// If an agent transitioned to idle or done, immediately check if it has unread mail!
|
|
133
|
+
if (status === "idle" || status === "done") {
|
|
134
|
+
const amqRoot = findAmqRoot();
|
|
135
|
+
if (amqRoot && handle) {
|
|
136
|
+
runDoorbellPass({ amqRoot, targetHandle: handle });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseTaskArgs(args = []) {
|
|
142
|
+
const flags = {};
|
|
143
|
+
const positional = [];
|
|
144
|
+
for (let i = 0; i < args.length; i++) {
|
|
145
|
+
const arg = args[i];
|
|
146
|
+
if (arg.startsWith("--")) {
|
|
147
|
+
const key = arg.slice(2);
|
|
148
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
149
|
+
flags[key] = args[i + 1];
|
|
150
|
+
i++;
|
|
151
|
+
} else {
|
|
152
|
+
flags[key] = true;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
positional.push(arg);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { flags, positional };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* CLI command handler for AGboard task management:
|
|
163
|
+
* herdr-amq task list [--owner <handle>] [--status <stage>] [--json]
|
|
164
|
+
* herdr-amq task assign --to <handle> --title <title> [--desc <desc>] [--status <stage>]
|
|
165
|
+
* herdr-amq task claim <id> [--me <handle>]
|
|
166
|
+
* herdr-amq task done <id> [--me <handle>] [--proof <proof>]
|
|
167
|
+
* herdr-amq task block <id> [--me <handle>] [--reason <reason>]
|
|
168
|
+
* herdr-amq task show <id>
|
|
169
|
+
*/
|
|
170
|
+
export function handleTaskCommand(subcommand = "list", rawArgs = []) {
|
|
171
|
+
const amqRoot = findAmqRoot();
|
|
172
|
+
if (!amqRoot) {
|
|
173
|
+
console.error("❌ No active .agent-mail directory found.");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const repoRoot = path.resolve(path.dirname(amqRoot));
|
|
178
|
+
const { flags, positional } = parseTaskArgs(rawArgs);
|
|
179
|
+
const me = flags.me || flags.from || process.env.AMQ_ME || "coordinator";
|
|
180
|
+
|
|
181
|
+
switch (subcommand) {
|
|
182
|
+
case "list":
|
|
183
|
+
case "ls": {
|
|
184
|
+
const board = loadBoard(repoRoot, amqRoot);
|
|
185
|
+
let allTasks = [];
|
|
186
|
+
for (const [colName, list] of Object.entries(board.columns)) {
|
|
187
|
+
for (const t of list) {
|
|
188
|
+
allTasks.push({ ...t, column: colName });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (flags.owner) {
|
|
193
|
+
allTasks = allTasks.filter((t) => t.owner?.toLowerCase() === flags.owner.toLowerCase());
|
|
194
|
+
}
|
|
195
|
+
if (flags.status) {
|
|
196
|
+
allTasks = allTasks.filter((t) => t.status === flags.status);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (flags.json) {
|
|
200
|
+
console.log(JSON.stringify(allTasks, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`\n📋 \x1b[1mAGboard Tasks\x1b[0m (${allTasks.length} total)`);
|
|
205
|
+
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
206
|
+
|
|
207
|
+
if (allTasks.length === 0) {
|
|
208
|
+
console.log(" (no matching tasks found)");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const t of allTasks) {
|
|
212
|
+
let statusBadge = `[${t.status}]`;
|
|
213
|
+
if (t.status === "in_progress") statusBadge = `\x1b[33m[in_progress]\x1b[0m`;
|
|
214
|
+
else if (t.status === "blocked") statusBadge = `\x1b[31m[blocked]\x1b[0m`;
|
|
215
|
+
else if (t.status === "done") statusBadge = `\x1b[32m[done]\x1b[0m`;
|
|
216
|
+
else statusBadge = `\x1b[34m[backlog]\x1b[0m`;
|
|
217
|
+
|
|
218
|
+
const idStr = `\x1b[90m${t.id.padEnd(20)}\x1b[0m`;
|
|
219
|
+
const ownerStr = `\x1b[1m${(t.owner || "unassigned").padEnd(14)}\x1b[0m`;
|
|
220
|
+
console.log(` ${statusBadge.padEnd(22)} ${idStr} ${ownerStr} ${t.title}`);
|
|
221
|
+
}
|
|
222
|
+
console.log("────────────────────────────────────────────────────────────────────────────\n");
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case "assign": {
|
|
227
|
+
const to = flags.to || flags.owner;
|
|
228
|
+
const title = flags.title || positional.join(" ");
|
|
229
|
+
const desc = flags.desc || flags.description || "";
|
|
230
|
+
const status = flags.status || "backlog";
|
|
231
|
+
|
|
232
|
+
if (!title || !title.trim()) {
|
|
233
|
+
console.error("❌ Task title is required: --title <title>");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (!to || !to.trim()) {
|
|
237
|
+
console.error("❌ Target owner is required: --to <handle>");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const res = addBoardTask(repoRoot, amqRoot, {
|
|
242
|
+
title,
|
|
243
|
+
owner: to,
|
|
244
|
+
status,
|
|
245
|
+
description: desc,
|
|
246
|
+
from: me,
|
|
247
|
+
notify: flags.notify !== "false",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (res.ok) {
|
|
251
|
+
console.log(`\n✅ \x1b[32mTask created and assigned to ${to}\x1b[0m (ID: ${res.task.id})`);
|
|
252
|
+
console.log(`✉️ Automail notification dispatched via AMQ to ${to}.`);
|
|
253
|
+
console.log(`Title: ${res.task.title}\n`);
|
|
254
|
+
} else {
|
|
255
|
+
console.error(`❌ Failed to create task: ${res.error}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case "claim": {
|
|
262
|
+
const taskId = positional[0] || flags.id;
|
|
263
|
+
if (!taskId) {
|
|
264
|
+
console.error("❌ Task ID is required: herdr-amq task claim <taskId> [--me <handle>]");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const res = updateBoardTask(
|
|
269
|
+
repoRoot,
|
|
270
|
+
amqRoot,
|
|
271
|
+
taskId,
|
|
272
|
+
{ status: "in_progress", owner: me },
|
|
273
|
+
{ from: me, notify: flags.notify !== "false" }
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (res.ok) {
|
|
277
|
+
console.log(`\n🚀 \x1b[33mTask ${taskId} claimed by ${me}\x1b[0m (Status -> in_progress)`);
|
|
278
|
+
console.log(`✉️ Notification dispatched to coordinator via AMQ.\n`);
|
|
279
|
+
} else {
|
|
280
|
+
console.error(`❌ Failed to claim task: ${res.error}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
case "done":
|
|
287
|
+
case "complete": {
|
|
288
|
+
const taskId = positional[0] || flags.id;
|
|
289
|
+
if (!taskId) {
|
|
290
|
+
console.error("❌ Task ID is required: herdr-amq task done <taskId> [--proof <proof>]");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const proof = flags.proof || flags.evidence || positional.slice(1).join(" ") || "";
|
|
295
|
+
const res = updateBoardTask(
|
|
296
|
+
repoRoot,
|
|
297
|
+
amqRoot,
|
|
298
|
+
taskId,
|
|
299
|
+
{ status: "done" },
|
|
300
|
+
{ from: me, proof, notify: flags.notify !== "false" }
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (res.ok) {
|
|
304
|
+
console.log(`\n🎉 \x1b[32mTask ${taskId} marked as DONE\x1b[0m`);
|
|
305
|
+
if (proof) console.log(`Evidence: ${proof}`);
|
|
306
|
+
console.log(`✉️ Completion alert dispatched to coordinator via AMQ.\n`);
|
|
307
|
+
} else {
|
|
308
|
+
console.error(`❌ Failed to complete task: ${res.error}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case "block": {
|
|
315
|
+
const taskId = positional[0] || flags.id;
|
|
316
|
+
if (!taskId) {
|
|
317
|
+
console.error("❌ Task ID is required: herdr-amq task block <taskId> --reason <reason>");
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const reason = flags.reason || flags.desc || positional.slice(1).join(" ") || "Blocked";
|
|
322
|
+
const res = updateBoardTask(
|
|
323
|
+
repoRoot,
|
|
324
|
+
amqRoot,
|
|
325
|
+
taskId,
|
|
326
|
+
{ status: "blocked" },
|
|
327
|
+
{ from: me, reason, notify: flags.notify !== "false" }
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (res.ok) {
|
|
331
|
+
console.log(`\n⚠️ \x1b[31mTask ${taskId} marked as BLOCKED\x1b[0m`);
|
|
332
|
+
console.log(`Reason: ${reason}`);
|
|
333
|
+
console.log(`✉️ Urgent alert dispatched to coordinator via AMQ.\n`);
|
|
334
|
+
} else {
|
|
335
|
+
console.error(`❌ Failed to block task: ${res.error}`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
case "show": {
|
|
342
|
+
const taskId = positional[0] || flags.id;
|
|
343
|
+
if (!taskId) {
|
|
344
|
+
console.error("❌ Task ID is required: herdr-amq task show <taskId>");
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const board = loadBoard(repoRoot, amqRoot);
|
|
349
|
+
let found = null;
|
|
350
|
+
for (const list of Object.values(board.columns)) {
|
|
351
|
+
const m = list.find((t) => t.id === taskId);
|
|
352
|
+
if (m) {
|
|
353
|
+
found = m;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!found) {
|
|
359
|
+
console.error(`❌ Task ${taskId} not found.`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log(`\n📦 \x1b[1mTask Details: ${found.title}\x1b[0m`);
|
|
364
|
+
console.log("──────────────────────────────────────────────");
|
|
365
|
+
console.log(`ID: ${found.id}`);
|
|
366
|
+
console.log(`Owner: ${found.owner}`);
|
|
367
|
+
console.log(`Status: ${found.status}`);
|
|
368
|
+
console.log(`Source: ${found.source || "custom"}`);
|
|
369
|
+
if (found.created) console.log(`Created: ${found.created}`);
|
|
370
|
+
if (found.updated) console.log(`Updated: ${found.updated}`);
|
|
371
|
+
if (found.description) console.log(`Description: ${found.description}`);
|
|
372
|
+
console.log("──────────────────────────────────────────────\n");
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
default:
|
|
377
|
+
console.log(`\n📋 \x1b[1mAGboard Task Coordination CLI\x1b[0m`);
|
|
378
|
+
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
379
|
+
console.log("Usage: herdr-amq task <subcommand> [options]");
|
|
380
|
+
console.log("\nCommands:");
|
|
381
|
+
console.log(" list [--owner <h>] [--status <s>] [--json] List all tasks");
|
|
382
|
+
console.log(" assign --to <h> --title <t> [--desc <d>] Assign a new task to an agent");
|
|
383
|
+
console.log(" claim <id> [--me <h>] Claim an existing task");
|
|
384
|
+
console.log(" done <id> [--me <h>] [--proof <evidence>] Complete a task with proof");
|
|
385
|
+
console.log(" block <id> [--me <h>] [--reason <reason>] Mark task blocked with reason");
|
|
386
|
+
console.log(" show <id> View task details");
|
|
387
|
+
console.log("────────────────────────────────────────────────────────────────────────────\n");
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function handleMailCommand(subcmd, args = []) {
|
|
393
|
+
const action = subcmd || "help";
|
|
394
|
+
|
|
395
|
+
if (action === "help" || action === "--help" || action === "-h" || (args && (args.includes("--help") || args.includes("-h")))) {
|
|
396
|
+
console.log(`\n✉️ \x1b[1mAMQ Maildir Native Engine CLI\x1b[0m`);
|
|
397
|
+
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
398
|
+
console.log("Usage: herdr-amq mail <command> [options]");
|
|
399
|
+
console.log(" herdr-amq send --to <h> --subject <s> --body <b> [--attach <p>]");
|
|
400
|
+
console.log(" herdr-amq reply --id <id> --body <b> [--attach <p>]");
|
|
401
|
+
console.log(" herdr-amq drain --me <handle> [--include-body]");
|
|
402
|
+
console.log("\nCommands:");
|
|
403
|
+
console.log(" send --to <handle> --subject <subj> --body <text|@file> [--from <h>] [--attach <p>]");
|
|
404
|
+
console.log(" reply --id <msg_id> --body <text|@file> [--from <h>] [--attach <p>]");
|
|
405
|
+
console.log(" drain --me <handle> [--include-body]");
|
|
406
|
+
console.log("────────────────────────────────────────────────────────────────────────────\n");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const amqRoot = findAmqRoot();
|
|
411
|
+
if (!amqRoot) {
|
|
412
|
+
console.error("❌ No .agent-mail queue found in workspace or current directory.");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getArg(flag, alias) {
|
|
417
|
+
const idx = args.findIndex((a) => a === flag || (alias && a === alias));
|
|
418
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getMultiArg(flag, alias) {
|
|
422
|
+
const val = getArg(flag, alias);
|
|
423
|
+
return val ? val.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
switch (action) {
|
|
427
|
+
case "send": {
|
|
428
|
+
const from = getArg("--from", "--me") || process.env.AM_ME || "coordinator";
|
|
429
|
+
const to = getMultiArg("--to");
|
|
430
|
+
const subject = getArg("--subject", "-s") || "(no subject)";
|
|
431
|
+
const bodyArg = getArg("--body", "-b");
|
|
432
|
+
let body = bodyArg || "";
|
|
433
|
+
if (bodyArg && bodyArg.startsWith("@")) {
|
|
434
|
+
const filePath = bodyArg.slice(1);
|
|
435
|
+
if (fs.existsSync(filePath)) body = fs.readFileSync(filePath, "utf8");
|
|
436
|
+
}
|
|
437
|
+
const kind = getArg("--kind");
|
|
438
|
+
const priority = getArg("--priority") || "normal";
|
|
439
|
+
const attach = getMultiArg("--attach");
|
|
440
|
+
|
|
441
|
+
if (!to.length) {
|
|
442
|
+
console.error("❌ Missing required --to recipient.");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const res = sendMaildirMessage(amqRoot, {
|
|
448
|
+
from,
|
|
449
|
+
to,
|
|
450
|
+
subject,
|
|
451
|
+
body,
|
|
452
|
+
kind,
|
|
453
|
+
priority,
|
|
454
|
+
attachments: attach,
|
|
455
|
+
});
|
|
456
|
+
console.log(`✉️ Sent ${res.id} to ${to.join(", ")} (from: ${from}) [maildir native]`);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.error(`❌ Send failed: ${err.message}`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case "reply": {
|
|
465
|
+
const from = getArg("--from", "--me") || process.env.AM_ME;
|
|
466
|
+
const id = getArg("--id");
|
|
467
|
+
const bodyArg = getArg("--body", "-b");
|
|
468
|
+
let body = bodyArg || "";
|
|
469
|
+
if (bodyArg && bodyArg.startsWith("@")) {
|
|
470
|
+
const filePath = bodyArg.slice(1);
|
|
471
|
+
if (fs.existsSync(filePath)) body = fs.readFileSync(filePath, "utf8");
|
|
472
|
+
}
|
|
473
|
+
const attach = getMultiArg("--attach");
|
|
474
|
+
|
|
475
|
+
if (!id) {
|
|
476
|
+
console.error("❌ Missing required --id of message to reply to.");
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
if (!from) {
|
|
480
|
+
console.error("❌ Missing required --from / --me handle.");
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const res = replyMaildirMessage(amqRoot, {
|
|
486
|
+
from,
|
|
487
|
+
replyToId: id,
|
|
488
|
+
body,
|
|
489
|
+
attachments: attach,
|
|
490
|
+
});
|
|
491
|
+
console.log(`✉️ Replied ${res.id} to ${res.to.join(", ")} (in-reply-to: ${id}) [maildir native]`);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
console.error(`❌ Reply failed: ${err.message}`);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
case "drain": {
|
|
500
|
+
const me = getArg("--me", "--from") || process.env.AM_ME;
|
|
501
|
+
if (!me) {
|
|
502
|
+
console.error("❌ Missing required --me <handle>.");
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const includeBody = args.includes("--include-body");
|
|
507
|
+
const drained = drainMaildir(amqRoot, me);
|
|
508
|
+
if (!drained.length) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log(`[AMQ] ${drained.length} new message(s) for ${me}:`);
|
|
513
|
+
for (const m of drained) {
|
|
514
|
+
const h = m.header || {};
|
|
515
|
+
console.log(`\n- From: ${h.from}`);
|
|
516
|
+
console.log(` Thread: ${h.thread || ""}`);
|
|
517
|
+
console.log(` ID: ${m.id}`);
|
|
518
|
+
console.log(` Subject: ${h.subject || ""}`);
|
|
519
|
+
console.log(` Priority: ${h.priority || "normal"}`);
|
|
520
|
+
if (h.kind) console.log(` Kind: ${h.kind}`);
|
|
521
|
+
console.log(` Created: ${h.created || ""}`);
|
|
522
|
+
if (includeBody && m.body) {
|
|
523
|
+
console.log(` Body:\n${m.body.trim()}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
console.log("");
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
default:
|
|
531
|
+
console.log(`\n✉️ \x1b[1mAMQ Maildir Native Engine CLI\x1b[0m`);
|
|
532
|
+
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
533
|
+
console.log("Usage: herdr-amq mail <command> [options]");
|
|
534
|
+
console.log("\nCommands:");
|
|
535
|
+
console.log(" send --to <handle> --subject <subj> --body <text|@file> [--from <h>] [--attach <p>]");
|
|
536
|
+
console.log(" reply --id <msg_id> --body <text|@file> [--from <h>] [--attach <p>]");
|
|
537
|
+
console.log(" drain --me <handle> [--include-body]");
|
|
538
|
+
console.log("────────────────────────────────────────────────────────────────────────────\n");
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function handleSkillCommand(args = []) {
|
|
544
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
545
|
+
const skillFile = path.resolve(currentDir, "../skills/herdr-amq/SKILL.md");
|
|
546
|
+
let content = "";
|
|
547
|
+
if (fs.existsSync(skillFile)) {
|
|
548
|
+
content = fs.readFileSync(skillFile, "utf-8");
|
|
549
|
+
} else {
|
|
550
|
+
console.error("❌ Skill file not found.");
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const installIndex = args.findIndex((a) => a === "--install" || a === "install" || a === "-i");
|
|
555
|
+
if (installIndex !== -1) {
|
|
556
|
+
let destDir = args[installIndex + 1];
|
|
557
|
+
if (!destDir || destDir.startsWith("-")) {
|
|
558
|
+
destDir = path.resolve(process.cwd(), ".opencode/skills/herdr-amq");
|
|
559
|
+
} else {
|
|
560
|
+
destDir = path.resolve(process.cwd(), destDir);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
564
|
+
const targetFile = path.join(destDir, "SKILL.md");
|
|
565
|
+
fs.writeFileSync(targetFile, content, "utf-8");
|
|
566
|
+
console.log(`✅ Successfully installed herdr-amq skill to ${targetFile}`);
|
|
567
|
+
return targetFile;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
process.stdout.write(content + (content.endsWith("\n") ? "" : "\n"));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|