agent-coord-mcp 0.3.5 → 0.3.8
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/hooks/peek-coord.mjs +44 -6
- package/package.json +1 -1
- package/scripts/coord-chat.mjs +228 -30
package/hooks/peek-coord.mjs
CHANGED
|
@@ -10,21 +10,31 @@
|
|
|
10
10
|
// Optional env: AGENT_COORD_DIR (default ~/agent-coord)
|
|
11
11
|
// Optional env: AGENT_COORD_INCLUDE_ROOM=1 to also drain the shared room
|
|
12
12
|
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
|
|
17
17
|
const MODE = (process.argv.find((a) => a.startsWith("--mode="))?.slice(7)) ?? "user-prompt";
|
|
18
|
-
const AGENT_ID = process.env.AGENT_COORD_ID;
|
|
19
|
-
if (!AGENT_ID) {
|
|
20
|
-
// No agent id configured — silently no-op so the hook never blocks the user.
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
18
|
|
|
24
19
|
const ROOT =
|
|
25
20
|
process.env.AGENT_COORD_DIR ??
|
|
26
21
|
process.env.CLAUDE_COORD_DIR ??
|
|
27
22
|
path.join(homedir(), "agent-coord");
|
|
23
|
+
|
|
24
|
+
// Prefer the agentId that is actually attached to this tmux pane (via the
|
|
25
|
+
// transports/ registry) over the env-provided one. This is what lets the
|
|
26
|
+
// generic settings.json hook command — which derives AGENT_COORD_ID from the
|
|
27
|
+
// cwd basename — still resolve to the agent that registered with a custom id
|
|
28
|
+
// (e.g. "claude-david" rather than "linkaroo.io"). Without this, peek reads
|
|
29
|
+
// the wrong cursor + inbox files and the m.from === AGENT_ID self-filter
|
|
30
|
+
// below never matches the agent's own room posts.
|
|
31
|
+
const AGENT_ID = resolveAgentId();
|
|
32
|
+
if (!AGENT_ID) {
|
|
33
|
+
// No agent id configured and no tmux transport match — silently no-op so
|
|
34
|
+
// the hook never blocks the user.
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
const INBOX = path.join(ROOT, "inbox", `${sanitize(AGENT_ID)}.jsonl`);
|
|
29
39
|
const ROOM = path.join(ROOT, "room.jsonl");
|
|
30
40
|
const CURSOR = path.join(ROOT, "cursors", `${sanitize(AGENT_ID)}.json`);
|
|
@@ -107,3 +117,31 @@ function writeJsonAtomic(file, data) {
|
|
|
107
117
|
function sanitize(id) {
|
|
108
118
|
return id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
109
119
|
}
|
|
120
|
+
|
|
121
|
+
function resolveAgentId() {
|
|
122
|
+
const pane = process.env.TMUX_PANE;
|
|
123
|
+
if (pane) {
|
|
124
|
+
const fromTransport = lookupAgentByPane(pane);
|
|
125
|
+
if (fromTransport) return fromTransport;
|
|
126
|
+
}
|
|
127
|
+
return process.env.AGENT_COORD_ID || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function lookupAgentByPane(pane) {
|
|
131
|
+
const dir = path.join(ROOT, "transports");
|
|
132
|
+
if (!existsSync(dir)) return null;
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = readdirSync(dir);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
for (const name of entries) {
|
|
140
|
+
if (!name.endsWith(".json")) continue;
|
|
141
|
+
const data = readJson(path.join(dir, name), null);
|
|
142
|
+
if (data && data.tmuxTarget === pane && typeof data.agentId === "string") {
|
|
143
|
+
return data.agentId;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -53,29 +53,97 @@ const CURSOR_FILE = path.join(CURSOR_DIR, `${sanitize(ID)}.json`);
|
|
|
53
53
|
mkdirSync(INBOX_DIR, { recursive: true });
|
|
54
54
|
mkdirSync(CURSOR_DIR, { recursive: true });
|
|
55
55
|
|
|
56
|
+
// ---------- ANSI helpers ----------
|
|
57
|
+
|
|
58
|
+
const A = {
|
|
59
|
+
reset: "\x1b[0m",
|
|
60
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
61
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
62
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
63
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
64
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
65
|
+
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
66
|
+
magenta: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
67
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Stable per-agent color from agentId hash. Skip red (reserved for errors)
|
|
71
|
+
// and white/black; cycle the remaining 5 bright colors.
|
|
72
|
+
const AGENT_COLORS = [A.green, A.yellow, A.blue, A.magenta, A.cyan];
|
|
73
|
+
function agentColor(id) {
|
|
74
|
+
let h = 0;
|
|
75
|
+
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
|
76
|
+
return AGENT_COLORS[h % AGENT_COLORS.length];
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
// ---------- register and start UI ----------
|
|
57
80
|
|
|
58
81
|
await register();
|
|
59
82
|
|
|
83
|
+
// Visual separator above the prompt — delimits "typing area starts here."
|
|
84
|
+
// Embedded in the prompt string itself so readline owns the redraw; no
|
|
85
|
+
// scroll-region gymnastics needed (those don't survive tmux cleanly).
|
|
86
|
+
const TTY = !!process.stdout.isTTY;
|
|
87
|
+
let COLS = process.stdout.columns || 80;
|
|
88
|
+
const sepLine = () => A.dim("─".repeat(Math.max(10, COLS)));
|
|
89
|
+
|
|
90
|
+
// inputAreaReady flips to true after the banner + initial drain finish and
|
|
91
|
+
// we lay down the first separator. From that point say() treats the line
|
|
92
|
+
// above the prompt as a separator slot it owns.
|
|
93
|
+
let inputAreaReady = false;
|
|
94
|
+
|
|
95
|
+
if (TTY) {
|
|
96
|
+
process.stdout.on("resize", () => {
|
|
97
|
+
COLS = process.stdout.columns || 80;
|
|
98
|
+
if (typeof rl !== "undefined") {
|
|
99
|
+
rl.setPrompt(makePrompt());
|
|
100
|
+
rl.prompt(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SLASH_COMMANDS = [
|
|
106
|
+
"/dm", "/list", "/who", "/whoami", "/last", "/clear", "/cls",
|
|
107
|
+
"/me", "/help", "/?", "/quit", "/exit",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
function completer(line) {
|
|
111
|
+
// Tab-complete slash commands and DM targets.
|
|
112
|
+
if (line.startsWith("/dm ")) {
|
|
113
|
+
const partial = line.slice(4);
|
|
114
|
+
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
115
|
+
const ids = Object.keys(reg).filter((id) => id !== ID && id.startsWith(partial));
|
|
116
|
+
const hits = ids.map((id) => `/dm ${id} `);
|
|
117
|
+
return [hits, line];
|
|
118
|
+
}
|
|
119
|
+
if (line.startsWith("/")) {
|
|
120
|
+
const hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
121
|
+
return [hits, line];
|
|
122
|
+
}
|
|
123
|
+
return [[], line];
|
|
124
|
+
}
|
|
125
|
+
|
|
60
126
|
const rl = readline.createInterface({
|
|
61
127
|
input: process.stdin,
|
|
62
128
|
output: process.stdout,
|
|
63
|
-
prompt:
|
|
129
|
+
prompt: makePrompt(),
|
|
130
|
+
completer,
|
|
64
131
|
});
|
|
65
132
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
69
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
70
|
-
|
|
71
|
-
say(dim(`coord-chat — agentId=${ID} dir=${ROOT}`));
|
|
72
|
-
say(dim("commands: <text>=room /dm <id> <text> /list /help /quit"));
|
|
73
|
-
|
|
133
|
+
// Banner — printed once on launch. Keep it tight; this is a CLI, not a poster.
|
|
134
|
+
printBanner();
|
|
74
135
|
await drainAndPrint();
|
|
75
136
|
|
|
137
|
+
// Lay down the first separator, then activate input-area mode so subsequent
|
|
138
|
+
// say() calls maintain a sep line directly above the prompt.
|
|
139
|
+
process.stdout.write(sepLine() + "\n");
|
|
140
|
+
inputAreaReady = true;
|
|
141
|
+
|
|
76
142
|
try { watch(INBOX_FILE, () => void drainAndPrint()); } catch {}
|
|
77
143
|
try { watch(ROOM_FILE, () => void drainAndPrint()); } catch {}
|
|
144
|
+
try { watch(AGENTS_FILE, () => refreshPrompt()); } catch {}
|
|
78
145
|
setInterval(() => void drainAndPrint(), 1000);
|
|
146
|
+
setInterval(refreshPrompt, 5000);
|
|
79
147
|
|
|
80
148
|
rl.prompt();
|
|
81
149
|
|
|
@@ -85,34 +153,56 @@ rl.on("line", async (line) => {
|
|
|
85
153
|
try {
|
|
86
154
|
if (text === "/quit" || text === "/exit") {
|
|
87
155
|
await unregister();
|
|
88
|
-
|
|
156
|
+
teardownFooter();
|
|
157
|
+
process.stdout.write(A.dim("bye.\n"));
|
|
89
158
|
process.exit(0);
|
|
90
|
-
} else if (text === "/help") {
|
|
91
|
-
|
|
92
|
-
} else if (text === "/list") {
|
|
159
|
+
} else if (text === "/help" || text === "/?") {
|
|
160
|
+
printHelp();
|
|
161
|
+
} else if (text === "/list" || text === "/who") {
|
|
93
162
|
await printAgents();
|
|
163
|
+
} else if (text === "/whoami") {
|
|
164
|
+
await printWhoami();
|
|
165
|
+
} else if (text === "/clear" || text === "/cls") {
|
|
166
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
167
|
+
printBanner();
|
|
168
|
+
} else if (text.startsWith("/last")) {
|
|
169
|
+
const m = text.match(/^\/last(?:\s+(\d+))?$/);
|
|
170
|
+
const n = m && m[1] ? parseInt(m[1], 10) : 20;
|
|
171
|
+
await printRecent(n);
|
|
172
|
+
} else if (text.startsWith("/me ")) {
|
|
173
|
+
const action = text.slice(4).trim();
|
|
174
|
+
if (!action) say(A.red("usage: /me <action>"));
|
|
175
|
+
else await sendRoom(`* ${ID} ${action}`);
|
|
94
176
|
} else if (text.startsWith("/dm ")) {
|
|
95
177
|
const m = text.match(/^\/dm\s+(\S+)\s+([\s\S]+)$/);
|
|
96
|
-
if (!m) say(red("usage: /dm <agentId> <text>"));
|
|
178
|
+
if (!m) say(A.red("usage: /dm <agentId> <text>"));
|
|
97
179
|
else await sendDm(m[1], m[2]);
|
|
98
180
|
} else if (text.startsWith("/")) {
|
|
99
|
-
say(red(`unknown command: ${text.split(" ")[0]}`));
|
|
181
|
+
say(A.red(`unknown command: ${text.split(" ")[0]}`) + A.dim(" (try /help)"));
|
|
100
182
|
} else {
|
|
101
183
|
await sendRoom(text);
|
|
102
184
|
}
|
|
103
185
|
} catch (e) {
|
|
104
|
-
say(red(`error: ${e?.message ?? e}`));
|
|
186
|
+
say(A.red(`error: ${e?.message ?? e}`));
|
|
105
187
|
}
|
|
188
|
+
// After Enter, terminal advanced to a new line. Print a fresh separator
|
|
189
|
+
// there so the next prompt sits below a sep line, maintaining the layout.
|
|
190
|
+
process.stdout.write(sepLine() + "\n");
|
|
106
191
|
rl.prompt();
|
|
107
192
|
});
|
|
108
193
|
|
|
109
194
|
process.on("SIGINT", async () => {
|
|
110
195
|
try { await unregister(); } catch {}
|
|
111
|
-
|
|
112
|
-
|
|
196
|
+
teardownFooter();
|
|
197
|
+
process.stdout.write("\n" + A.dim("bye.\n"));
|
|
113
198
|
process.exit(0);
|
|
114
199
|
});
|
|
115
200
|
|
|
201
|
+
process.on("exit", () => {
|
|
202
|
+
// Final safety net — restore terminal state if we exit via any path.
|
|
203
|
+
teardownFooter();
|
|
204
|
+
});
|
|
205
|
+
|
|
116
206
|
// ---------- helpers ----------
|
|
117
207
|
|
|
118
208
|
function parseArgs(argv) {
|
|
@@ -134,14 +224,97 @@ function sanitize(s) {
|
|
|
134
224
|
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
135
225
|
}
|
|
136
226
|
|
|
137
|
-
// Write output without clobbering whatever the user is typing.
|
|
138
227
|
function say(line) {
|
|
139
|
-
|
|
140
|
-
|
|
228
|
+
if (!inputAreaReady) {
|
|
229
|
+
process.stdout.write(line + "\n");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// We're on the prompt line. The line above is the current separator.
|
|
233
|
+
// Move up to it, clear it, drop our message there, then a fresh separator,
|
|
234
|
+
// then re-render the prompt on the next line.
|
|
235
|
+
process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
141
236
|
process.stdout.write(line + "\n");
|
|
237
|
+
process.stdout.write(sepLine() + "\n");
|
|
142
238
|
if (typeof rl !== "undefined") rl.prompt(true);
|
|
143
239
|
}
|
|
144
240
|
|
|
241
|
+
function makePrompt() {
|
|
242
|
+
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
const STALE = 5 * 60 * 1000;
|
|
245
|
+
let online = 0;
|
|
246
|
+
for (const id of Object.keys(reg)) {
|
|
247
|
+
if (id === ID) continue;
|
|
248
|
+
const a = reg[id];
|
|
249
|
+
const marker = readJsonSafe(path.join(TRANSPORT_DIR, `${sanitize(id)}.json`), null);
|
|
250
|
+
const live = marker && marker.pid && pidAlive(marker.pid);
|
|
251
|
+
if (live || now - a.lastHeartbeat < STALE) online++;
|
|
252
|
+
}
|
|
253
|
+
const peers = online === 1 ? "1 peer" : `${online} peers`;
|
|
254
|
+
return `${agentColor(ID)(ID)} ${A.dim(`(${peers})`)}${A.dim(">")} `;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// No-op stubs kept so the exit paths don't reference deleted functions.
|
|
258
|
+
function teardownFooter() {}
|
|
259
|
+
|
|
260
|
+
function refreshPrompt() {
|
|
261
|
+
if (typeof rl === "undefined") return;
|
|
262
|
+
rl.setPrompt(makePrompt());
|
|
263
|
+
rl.prompt(true);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function printBanner() {
|
|
267
|
+
// Compact banner — three lines plus a separator. ASCII so it renders
|
|
268
|
+
// anywhere; no UTF-8 box-drawing surprises.
|
|
269
|
+
const lines = [
|
|
270
|
+
A.bold(A.cyan(" agent-coord ")) + A.dim("— shared chat for agents and humans"),
|
|
271
|
+
A.dim(` agentId=${A.reset}${agentColor(ID)(ID)}${A.dim(" dir=" + ROOT)}`),
|
|
272
|
+
A.dim(" type /help for commands · /quit to leave"),
|
|
273
|
+
];
|
|
274
|
+
for (const l of lines) say(l);
|
|
275
|
+
say(A.dim(" " + "─".repeat(60)));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function printHelp() {
|
|
279
|
+
const rows = [
|
|
280
|
+
["<text>", "post to the shared room"],
|
|
281
|
+
["/dm <agent> <text>", "send a direct message"],
|
|
282
|
+
["/me <action>", "post an IRC-style action (* you wave)"],
|
|
283
|
+
["/list, /who", "show registered agents + transports"],
|
|
284
|
+
["/whoami", "show your registration + transport"],
|
|
285
|
+
["/last [n]", "show last n messages (default 20)"],
|
|
286
|
+
["/clear", "clear the screen"],
|
|
287
|
+
["/help, /?", "this list"],
|
|
288
|
+
["/quit, /exit", "unregister and leave"],
|
|
289
|
+
];
|
|
290
|
+
say(A.bold("commands:"));
|
|
291
|
+
for (const [cmd, desc] of rows) {
|
|
292
|
+
say(` ${A.cyan(cmd.padEnd(22))} ${A.dim(desc)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function printWhoami() {
|
|
297
|
+
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
298
|
+
const a = reg[ID];
|
|
299
|
+
const marker = readJsonSafe(path.join(TRANSPORT_DIR, `${sanitize(ID)}.json`), null);
|
|
300
|
+
const live = marker && marker.pid && pidAlive(marker.pid);
|
|
301
|
+
say(A.bold("you:"));
|
|
302
|
+
say(` ${A.cyan("id")} ${agentColor(ID)(ID)}`);
|
|
303
|
+
say(` ${A.cyan("role")} ${a?.role ?? "-"}`);
|
|
304
|
+
say(` ${A.cyan("dir")} ${A.dim(ROOT)}`);
|
|
305
|
+
say(` ${A.cyan("transport")} ${live ? A.green(marker.transport) : A.dim("none")}`);
|
|
306
|
+
say(` ${A.cyan("registered")} ${a ? A.green("yes") : A.red("no")}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function printRecent(n) {
|
|
310
|
+
const inbox = readJsonl(INBOX_FILE).slice(-n).map((m) => ({ ...m, _kind: "DM" }));
|
|
311
|
+
const room = readJsonl(ROOM_FILE).slice(-n).map((m) => ({ ...m, _kind: "room" }));
|
|
312
|
+
const all = [...inbox, ...room].sort((a, b) => a.ts - b.ts).slice(-n);
|
|
313
|
+
if (!all.length) return say(A.dim("(no history)"));
|
|
314
|
+
say(A.bold(`last ${all.length} message(s):`));
|
|
315
|
+
for (const m of all) printMsg(m._kind, m, { history: true });
|
|
316
|
+
}
|
|
317
|
+
|
|
145
318
|
async function withLock(file, fn) {
|
|
146
319
|
await ensureFile(file);
|
|
147
320
|
const release = await lockfile.lock(file, {
|
|
@@ -184,7 +357,7 @@ async function unregister() {
|
|
|
184
357
|
async function sendDm(to, text) {
|
|
185
358
|
const target = path.join(INBOX_DIR, `${sanitize(to)}.jsonl`);
|
|
186
359
|
await appendMessage(target, { from: ID, to, text });
|
|
187
|
-
say(dim(`→ DM sent to ${to}`));
|
|
360
|
+
say(A.dim(`→ DM sent to ${to}`));
|
|
188
361
|
}
|
|
189
362
|
|
|
190
363
|
async function sendRoom(text) {
|
|
@@ -226,10 +399,21 @@ async function drainAndPrint() {
|
|
|
226
399
|
if (changed) writeJsonAtomic(CURSOR_FILE, cursor);
|
|
227
400
|
}
|
|
228
401
|
|
|
229
|
-
function printMsg(kind, m) {
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
402
|
+
function printMsg(kind, m, opts = {}) {
|
|
403
|
+
const d = new Date(m.ts);
|
|
404
|
+
const t = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
405
|
+
const who = m.from ?? "?";
|
|
406
|
+
const tag = kind === "DM" ? A.bold(A.cyan("DM")) : A.dim("room");
|
|
407
|
+
const meta = `${A.dim(t)} ${tag} ${agentColor(who)(who)}`;
|
|
408
|
+
// Multi-line bodies: first line on the meta row, subsequent lines indented
|
|
409
|
+
// to the body column for readability.
|
|
410
|
+
const body = (m.text ?? "").split("\n");
|
|
411
|
+
const indent = " ";
|
|
412
|
+
const first = body[0] ?? "";
|
|
413
|
+
const rest = body.slice(1).map((l) => indent + A.dim("│ ") + l);
|
|
414
|
+
const prefix = opts.history ? A.dim(" ") : "";
|
|
415
|
+
say(`${prefix}${meta} ${first}`);
|
|
416
|
+
for (const line of rest) say(`${prefix}${line}`);
|
|
233
417
|
}
|
|
234
418
|
|
|
235
419
|
async function printAgents() {
|
|
@@ -237,14 +421,28 @@ async function printAgents() {
|
|
|
237
421
|
const now = Date.now();
|
|
238
422
|
const STALE = 5 * 60 * 1000;
|
|
239
423
|
const ids = Object.keys(reg).sort();
|
|
240
|
-
if (!ids.length) return say(dim("(no agents)"));
|
|
424
|
+
if (!ids.length) return say(A.dim("(no agents)"));
|
|
425
|
+
// Compute column widths from data so things line up.
|
|
426
|
+
const idW = Math.max(8, ...ids.map((i) => i.length));
|
|
427
|
+
const roleW = Math.max(4, ...ids.map((i) => (reg[i].role ?? "-").length));
|
|
428
|
+
say(A.bold(`agents (${ids.length}):`));
|
|
429
|
+
say(
|
|
430
|
+
" " +
|
|
431
|
+
A.dim(
|
|
432
|
+
`${"id".padEnd(idW)} ${"status".padEnd(7)} ${"role".padEnd(roleW)} transport`,
|
|
433
|
+
),
|
|
434
|
+
);
|
|
241
435
|
for (const id of ids) {
|
|
242
436
|
const a = reg[id];
|
|
243
437
|
const marker = readJsonSafe(path.join(TRANSPORT_DIR, `${sanitize(id)}.json`), null);
|
|
244
438
|
const live = marker && marker.pid && pidAlive(marker.pid);
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
439
|
+
const onlineNow = live || now - a.lastHeartbeat < STALE;
|
|
440
|
+
const dot = onlineNow ? A.green("●") : A.dim("○");
|
|
441
|
+
const status = onlineNow ? "online " : "offline";
|
|
442
|
+
const role = (a.role ?? "-").padEnd(roleW);
|
|
443
|
+
const trans = live ? A.green(marker.transport) : A.dim("none");
|
|
444
|
+
const me = id === ID ? A.dim(" (you)") : "";
|
|
445
|
+
say(` ${dot} ${agentColor(id)(id.padEnd(idW))} ${A.dim(status)} ${role} ${trans}${me}`);
|
|
248
446
|
}
|
|
249
447
|
}
|
|
250
448
|
|