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/herdr.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* herdr.mjs — Herdr Socket API client for AGmail
|
|
3
|
+
*
|
|
4
|
+
* Connects to the running Herdr server via its Unix socket and provides:
|
|
5
|
+
* - getHerdrAgents() – one-shot snapshot of live agent records
|
|
6
|
+
* - subscribeAgentEvents() – long-lived event subscription, calls cb on each state change
|
|
7
|
+
* - getSocketPath() – resolve the Herdr socket path
|
|
8
|
+
*
|
|
9
|
+
* Protocol: JSON-RPC 2.0 over a Unix domain socket (newline-delimited JSON frames).
|
|
10
|
+
* See: https://herdr.dev/docs/socket-api/
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import net from "node:net";
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
// ─── Socket Path Resolution ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export function getSocketPath() {
|
|
21
|
+
if (process.env.HERDR_SOCKET_PATH) return process.env.HERDR_SOCKET_PATH;
|
|
22
|
+
// Default: ~/.config/herdr/herdr.sock
|
|
23
|
+
return path.join(os.homedir(), ".config", "herdr", "herdr.sock");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Low-level request helper (one-shot connect, send, read response) ────────
|
|
27
|
+
|
|
28
|
+
function socketRequest(method, params = {}, timeoutMs = 8000) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const socketPath = getSocketPath();
|
|
31
|
+
const id = `amq_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
32
|
+
const payload = JSON.stringify({ id, method, params }) + "\n";
|
|
33
|
+
|
|
34
|
+
const sock = net.createConnection(socketPath);
|
|
35
|
+
let buf = "";
|
|
36
|
+
let settled = false;
|
|
37
|
+
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
if (!settled) {
|
|
40
|
+
settled = true;
|
|
41
|
+
sock.destroy();
|
|
42
|
+
reject(new Error(`herdr socket request timed out (${method})`));
|
|
43
|
+
}
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
|
|
46
|
+
sock.on("connect", () => {
|
|
47
|
+
sock.write(payload);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
sock.on("data", (chunk) => {
|
|
51
|
+
buf += chunk.toString();
|
|
52
|
+
const lines = buf.split("\n");
|
|
53
|
+
buf = lines.pop(); // keep incomplete last line
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (!line.trim()) continue;
|
|
56
|
+
try {
|
|
57
|
+
const msg = JSON.parse(line);
|
|
58
|
+
if (msg.id === id && !settled) {
|
|
59
|
+
settled = true;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
sock.destroy();
|
|
62
|
+
if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
63
|
+
else resolve(msg.result);
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
sock.on("error", (err) => {
|
|
70
|
+
if (!settled) {
|
|
71
|
+
settled = true;
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
sock.on("close", () => {
|
|
78
|
+
if (!settled) {
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
reject(new Error("herdr socket closed before response"));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the list of agent records from the live Herdr session snapshot.
|
|
91
|
+
* Each record has: { name, agent, agent_status, pane_id, workspace_id, cwd, terminal_title, ... }
|
|
92
|
+
* Returns [] if Herdr is not running or socket unavailable.
|
|
93
|
+
*/
|
|
94
|
+
export async function getHerdrAgents() {
|
|
95
|
+
try {
|
|
96
|
+
const result = await socketRequest("session.snapshot", {});
|
|
97
|
+
const snapshot = result?.snapshot || result || {};
|
|
98
|
+
return Array.isArray(snapshot.agents) ? snapshot.agents : [];
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns a map of handle → { agent_status, pane_id, workspace_id, terminal_title }
|
|
106
|
+
* by matching `name` field from Herdr agent records to AMQ handles.
|
|
107
|
+
* Handles without a `name` in Herdr are skipped.
|
|
108
|
+
*/
|
|
109
|
+
export async function getHerdrStatusMap() {
|
|
110
|
+
const agents = await getHerdrAgents();
|
|
111
|
+
const map = new Map();
|
|
112
|
+
for (const a of agents) {
|
|
113
|
+
const handle = a.name; // e.g. "ballistics", "testkit"
|
|
114
|
+
if (handle) {
|
|
115
|
+
map.set(handle, {
|
|
116
|
+
herdrStatus: a.agent_status || "unknown",
|
|
117
|
+
herdrPaneId: a.pane_id,
|
|
118
|
+
herdrWorkspaceId: a.workspace_id,
|
|
119
|
+
herdrTabId: a.tab_id,
|
|
120
|
+
herdrTitle: a.terminal_title_stripped || a.terminal_title,
|
|
121
|
+
interactiveReady: a.interactive_ready || false,
|
|
122
|
+
agentType: a.agent,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return map;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Long-lived event subscription ──────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Opens a persistent connection to Herdr's events.subscribe endpoint.
|
|
133
|
+
* Calls onEvent(event) for each incoming event object.
|
|
134
|
+
* Calls onDisconnect() when the socket closes.
|
|
135
|
+
* Returns a { close() } handle.
|
|
136
|
+
*
|
|
137
|
+
* Relevant event types:
|
|
138
|
+
* agent.state_changed — { pane_id, name, agent_status, ... }
|
|
139
|
+
* pane.created / pane.closed
|
|
140
|
+
* workspace.created / workspace.closed
|
|
141
|
+
*/
|
|
142
|
+
export function subscribeHerdrEvents({ onEvent, onDisconnect, onConnect } = {}) {
|
|
143
|
+
const socketPath = getSocketPath();
|
|
144
|
+
const subId = `amq_sub_${Date.now()}`;
|
|
145
|
+
const payload =
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
id: subId,
|
|
148
|
+
method: "events.subscribe",
|
|
149
|
+
params: {},
|
|
150
|
+
}) + "\n";
|
|
151
|
+
|
|
152
|
+
let sock = null;
|
|
153
|
+
let closed = false;
|
|
154
|
+
let buf = "";
|
|
155
|
+
|
|
156
|
+
function connect() {
|
|
157
|
+
if (closed) return;
|
|
158
|
+
try {
|
|
159
|
+
sock = net.createConnection(socketPath);
|
|
160
|
+
|
|
161
|
+
sock.on("connect", () => {
|
|
162
|
+
buf = "";
|
|
163
|
+
sock.write(payload);
|
|
164
|
+
if (onConnect) onConnect();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
sock.on("data", (chunk) => {
|
|
168
|
+
buf += chunk.toString();
|
|
169
|
+
const lines = buf.split("\n");
|
|
170
|
+
buf = lines.pop();
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
if (!line.trim()) continue;
|
|
173
|
+
try {
|
|
174
|
+
const msg = JSON.parse(line);
|
|
175
|
+
// Skip the ack for the subscribe call itself
|
|
176
|
+
if (msg.id === subId && msg.result) continue;
|
|
177
|
+
if (msg.method && msg.params && onEvent) {
|
|
178
|
+
onEvent({ type: msg.method, ...msg.params });
|
|
179
|
+
}
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
sock.on("error", () => {});
|
|
185
|
+
sock.on("close", () => {
|
|
186
|
+
if (!closed && onDisconnect) onDisconnect();
|
|
187
|
+
});
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
connect();
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
close() {
|
|
195
|
+
closed = true;
|
|
196
|
+
if (sock) {
|
|
197
|
+
try { sock.destroy(); } catch {}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Herdr availability check ────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns true if the Herdr socket file exists and is reachable.
|
|
207
|
+
*/
|
|
208
|
+
export async function isHerdrAvailable() {
|
|
209
|
+
try {
|
|
210
|
+
await socketRequest("session.snapshot", {}, 2000);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/index.mjs
ADDED
package/src/markdown.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export function escapeHtml(str) {
|
|
2
|
+
return String(str || "")
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """)
|
|
7
|
+
.replace(/'/g, "'");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function renderMarkdown(md) {
|
|
11
|
+
if (!md) return "";
|
|
12
|
+
|
|
13
|
+
// 1. Normalize line endings to LF
|
|
14
|
+
const text = String(md).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
15
|
+
|
|
16
|
+
// 2. Extract fenced code blocks line-by-line so comments (# ...) are never treated as headings
|
|
17
|
+
const lines = text.split("\n");
|
|
18
|
+
let inCodeBlock = false;
|
|
19
|
+
let codeFence = "";
|
|
20
|
+
let codeLang = "";
|
|
21
|
+
let codeLines = [];
|
|
22
|
+
const proseLines = [];
|
|
23
|
+
const codeBlocks = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const line = lines[i];
|
|
27
|
+
if (!inCodeBlock) {
|
|
28
|
+
const fenceMatch = line.match(/^[ \t]*(`{3,}|~{3,})([a-zA-Z0-9_+#.-]*)[^\n]*$/);
|
|
29
|
+
if (fenceMatch) {
|
|
30
|
+
inCodeBlock = true;
|
|
31
|
+
codeFence = fenceMatch[1];
|
|
32
|
+
codeLang = (fenceMatch[2] || "code").trim();
|
|
33
|
+
codeLines = [];
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
proseLines.push(line);
|
|
37
|
+
} else {
|
|
38
|
+
const closeMatch = line.match(/^[ \t]*(`{3,}|~{3,})[ \t]*$/);
|
|
39
|
+
if (closeMatch && closeMatch[1][0] === codeFence[0] && closeMatch[1].length >= codeFence.length) {
|
|
40
|
+
inCodeBlock = false;
|
|
41
|
+
const cleanCode = codeLines.join("\n");
|
|
42
|
+
const escapedCode = escapeHtml(cleanCode);
|
|
43
|
+
const blockHtml = `<div class="code-block-wrapper">
|
|
44
|
+
<div class="code-block-header">
|
|
45
|
+
<span class="code-lang">${escapeHtml(codeLang || "code")}</span>
|
|
46
|
+
<button class="copy-code-btn" onclick="copyCode(this)">Copy</button>
|
|
47
|
+
</div>
|
|
48
|
+
<pre><code>${escapedCode}</code></pre>
|
|
49
|
+
</div>`;
|
|
50
|
+
const idx = codeBlocks.length;
|
|
51
|
+
codeBlocks.push(blockHtml);
|
|
52
|
+
proseLines.push(`\x00AMQ_BLOCK_${idx}_\x00`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
codeLines.push(line);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Handle unclosed fenced code block at end of input
|
|
60
|
+
if (inCodeBlock) {
|
|
61
|
+
const cleanCode = codeLines.join("\n");
|
|
62
|
+
const escapedCode = escapeHtml(cleanCode);
|
|
63
|
+
const blockHtml = `<div class="code-block-wrapper">
|
|
64
|
+
<div class="code-block-header">
|
|
65
|
+
<span class="code-lang">${escapeHtml(codeLang || "code")}</span>
|
|
66
|
+
<button class="copy-code-btn" onclick="copyCode(this)">Copy</button>
|
|
67
|
+
</div>
|
|
68
|
+
<pre><code>${escapedCode}</code></pre>
|
|
69
|
+
</div>`;
|
|
70
|
+
const idx = codeBlocks.length;
|
|
71
|
+
codeBlocks.push(blockHtml);
|
|
72
|
+
proseLines.push(`\x00AMQ_BLOCK_${idx}_\x00`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let prose = proseLines.join("\n");
|
|
76
|
+
|
|
77
|
+
// 3. Extract inline code so inline snippets are not affected by prose formatting
|
|
78
|
+
const inlineCodes = [];
|
|
79
|
+
prose = prose.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (match, fence, code) => {
|
|
80
|
+
const escaped = escapeHtml(code);
|
|
81
|
+
const idx = inlineCodes.length;
|
|
82
|
+
inlineCodes.push(`<code class="inline-code">${escaped}</code>`);
|
|
83
|
+
return `\x00AMQ_INLINE_${idx}_\x00`;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 4. Escape remaining HTML in prose for injection protection
|
|
87
|
+
let html = escapeHtml(prose);
|
|
88
|
+
|
|
89
|
+
// 5. Blockquotes (handling both escaped > and unescaped >)
|
|
90
|
+
html = html.replace(/^(?:>|>)[ \t]?(.*$)/gm, '<blockquote class="md-quote">$1</blockquote>');
|
|
91
|
+
html = html.replace(/<\/blockquote>\n<blockquote class="md-quote">/g, "<br>");
|
|
92
|
+
|
|
93
|
+
// 6. Headings in prose (requiring whitespace after # to avoid false matches on tags or includes)
|
|
94
|
+
html = html.replace(/^######[ \t]+(.*$)/gm, '<h6 class="md-h6">$1</h6>');
|
|
95
|
+
html = html.replace(/^#####[ \t]+(.*$)/gm, '<h5 class="md-h5">$1</h5>');
|
|
96
|
+
html = html.replace(/^####[ \t]+(.*$)/gm, '<h4 class="md-h4">$1</h4>');
|
|
97
|
+
html = html.replace(/^###[ \t]+(.*$)/gm, '<h3 class="md-h3">$1</h3>');
|
|
98
|
+
html = html.replace(/^##[ \t]+(.*$)/gm, '<h2 class="md-h2">$1</h2>');
|
|
99
|
+
html = html.replace(/^#[ \t]+(.*$)/gm, '<h1 class="md-h1">$1</h1>');
|
|
100
|
+
|
|
101
|
+
// 7. Markdown tables in prose
|
|
102
|
+
const tableRegex = /((?:^[ \t]*\|?[^\n\r|]+(?:\|[^\n\r|]+)+\|?[ \t]*\n)(?:^[ \t]*\|?(?:[ \t]*:?-+:?[ \t]*\|)+(?:[ \t]*:?-+:?[ \t]*)\|?[ \t]*\n)(?:^[ \t]*\|?[^\n\r|]+(?:\|[^\n\r|]+)+\|?[ \t]*(?:\n|$))+)/gm;
|
|
103
|
+
html = html.replace(tableRegex, (match) => {
|
|
104
|
+
const tableLines = match.trim().split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
105
|
+
if (tableLines.length < 2) return match;
|
|
106
|
+
const parseRow = (line) => {
|
|
107
|
+
let clean = line;
|
|
108
|
+
if (clean.startsWith("|")) clean = clean.slice(1);
|
|
109
|
+
if (clean.endsWith("|")) clean = clean.slice(0, -1);
|
|
110
|
+
return clean.split("|").map((c) => c.trim());
|
|
111
|
+
};
|
|
112
|
+
const headerCols = parseRow(tableLines[0]);
|
|
113
|
+
const alignLine = parseRow(tableLines[1]);
|
|
114
|
+
const aligns = alignLine.map((col) => {
|
|
115
|
+
const left = col.startsWith(":");
|
|
116
|
+
const right = col.endsWith(":");
|
|
117
|
+
if (left && right) return "center";
|
|
118
|
+
if (right) return "right";
|
|
119
|
+
return "left";
|
|
120
|
+
});
|
|
121
|
+
let tableHtml = '<div class="table-container"><table class="md-table"><thead><tr>';
|
|
122
|
+
headerCols.forEach((col, idx) => {
|
|
123
|
+
const align = aligns[idx] || "left";
|
|
124
|
+
tableHtml += `<th style="text-align: ${align}">${col}</th>`;
|
|
125
|
+
});
|
|
126
|
+
tableHtml += "</tr></thead><tbody>";
|
|
127
|
+
for (let j = 2; j < tableLines.length; j++) {
|
|
128
|
+
const rowCols = parseRow(tableLines[j]);
|
|
129
|
+
tableHtml += "<tr>";
|
|
130
|
+
headerCols.forEach((_, idx) => {
|
|
131
|
+
const cell = rowCols[idx] !== undefined ? rowCols[idx] : "";
|
|
132
|
+
const align = aligns[idx] || "left";
|
|
133
|
+
tableHtml += `<td style="text-align: ${align}">${cell}</td>`;
|
|
134
|
+
});
|
|
135
|
+
tableHtml += "</tr>";
|
|
136
|
+
}
|
|
137
|
+
tableHtml += "</tbody></table></div>\n";
|
|
138
|
+
return tableHtml;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 8. Bold, Italic, Strikethrough in prose
|
|
142
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
143
|
+
html = html.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, "$1<em>$2</em>");
|
|
144
|
+
html = html.replace(/~~([^~]+)~~/g, "<del>$1</del>");
|
|
145
|
+
|
|
146
|
+
// 9. Lists in prose
|
|
147
|
+
html = html.replace(/^([0-9]+\.|\([0-9]+\))[ \t]+(.*$)/gm, '<div class="md-list-item"><span class="md-list-num">$1</span> <span>$2</span></div>');
|
|
148
|
+
html = html.replace(/^[-*+][ \t]+(.*$)/gm, '<div class="md-bullet-item">• $1</div>');
|
|
149
|
+
|
|
150
|
+
// 10. Links in prose: [text](url)
|
|
151
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" class="md-link">$1</a>');
|
|
152
|
+
|
|
153
|
+
// 11. Paragraph breaks in prose
|
|
154
|
+
html = html.replace(/\n{2,}/g, '<div class="md-para-break"></div>');
|
|
155
|
+
|
|
156
|
+
// 12. Restore inline code
|
|
157
|
+
for (let k = 0; k < inlineCodes.length; k++) {
|
|
158
|
+
html = html.replace(`\x00AMQ_INLINE_${k}_\x00`, () => inlineCodes[k]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 13. Restore code blocks
|
|
162
|
+
for (let b = 0; b < codeBlocks.length; b++) {
|
|
163
|
+
html = html.replace(`\x00AMQ_BLOCK_${b}_\x00`, () => codeBlocks[b]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return html;
|
|
167
|
+
}
|
package/src/panes.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { findAmqRoot, getAgentHandles, execCmd } from "./config.mjs";
|
|
5
|
+
import { listInbox } from "./bridge.mjs";
|
|
6
|
+
|
|
7
|
+
import { startWebServer } from "./server.mjs";
|
|
8
|
+
|
|
9
|
+
export function launchDashboardPane() {
|
|
10
|
+
const amqRoot = findAmqRoot();
|
|
11
|
+
console.log("\x1b[1m✉️ AGmail Dashboard (Pure JS)\x1b[0m\n");
|
|
12
|
+
|
|
13
|
+
if (!amqRoot) {
|
|
14
|
+
console.error("❌ No .agent-mail directory found in current workspace.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const port = parseInt(process.env.AGMAIL_PORT || "8505", 10);
|
|
19
|
+
const server = startWebServer({ port, amqRoot });
|
|
20
|
+
|
|
21
|
+
// Open browser in background if xdg-open exists
|
|
22
|
+
try {
|
|
23
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
24
|
+
spawn(opener, [`http://localhost:${port}`], { stdio: "ignore", detached: true }).unref();
|
|
25
|
+
} catch {}
|
|
26
|
+
|
|
27
|
+
console.log("\nPress Ctrl+C to stop the dashboard server.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function launchInboxPeekPane() {
|
|
31
|
+
const amqRoot = findAmqRoot();
|
|
32
|
+
if (!amqRoot) {
|
|
33
|
+
console.log("❌ No .agent-mail directory found.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
renderInboxSummary(amqRoot);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderInboxSummary(amqRoot) {
|
|
41
|
+
const handles = getAgentHandles(amqRoot);
|
|
42
|
+
console.log(`\x1b[1m📫 AMQ Mailbox Overview\x1b[0m: \x1b[36m${amqRoot}\x1b[0m\n`);
|
|
43
|
+
|
|
44
|
+
let totalMsgs = 0;
|
|
45
|
+
for (const h of handles) {
|
|
46
|
+
const msgs = listInbox(amqRoot, h);
|
|
47
|
+
if (!msgs.length) continue;
|
|
48
|
+
totalMsgs += msgs.length;
|
|
49
|
+
|
|
50
|
+
console.log(`\x1b[1m\x1b[33m📥 ${h}\x1b[0m (\x1b[32m${msgs.length} unread\x1b[0m)`);
|
|
51
|
+
for (const m of msgs.slice(0, 5)) {
|
|
52
|
+
const from = m.from || "unknown";
|
|
53
|
+
const subject = m.subject || "(no subject)";
|
|
54
|
+
const id = m.id ? `[${m.id.slice(0, 8)}]` : "";
|
|
55
|
+
console.log(` • \x1b[90m${id}\x1b[0m \x1b[1m${from}\x1b[0m: ${subject}`);
|
|
56
|
+
}
|
|
57
|
+
if (msgs.length > 5) {
|
|
58
|
+
console.log(` \x1b[90m... and ${msgs.length - 5} more\x1b[0m`);
|
|
59
|
+
}
|
|
60
|
+
console.log("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (totalMsgs === 0) {
|
|
64
|
+
console.log("✨ All inboxes are clear! No unread transmissions.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log("\x1b[90mPress Enter or Escape to close.\x1b[0m");
|
|
68
|
+
}
|