herdr-plugin-amq 0.1.7 → 0.1.9
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 +35 -265
- package/bin/herdr-amq.mjs +6 -0
- package/docs/architecture.md +50 -0
- package/docs/cli-and-workflows.md +72 -0
- package/docs/images/agmail-agent-activity.webp +0 -0
- package/docs/images/agmail-mobile-new-task.webp +0 -0
- package/docs/images/agmail-task-drawer.webp +0 -0
- package/docs/installation.md +70 -0
- package/docs/security-and-testing.md +33 -0
- package/docs/ui-screenshots.md +21 -0
- package/herdr-plugin.toml +7 -1
- package/package.json +11 -5
- package/skills/herdr-amq/SKILL.md +12 -6
- package/src/actions.mjs +149 -12
- package/src/board.mjs +144 -6
- package/src/bridge.mjs +249 -34
- package/src/config.mjs +36 -2
- package/src/fleet.mjs +282 -111
- package/src/herdr.mjs +68 -14
- package/src/index.mjs +1 -0
- package/src/protocol.mjs +315 -67
- package/src/runtime-models.mjs +118 -0
- package/src/server.mjs +81 -37
- package/src/store.mjs +219 -34
- package/src/templates.mjs +118 -0
- package/src/web/app.js +487 -105
- package/src/web/index.html +130 -11
- package/src/web/style.css +1022 -39
package/src/fleet.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import { execFileSync
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { scanAgentBriefs } from "./briefs.mjs";
|
|
6
6
|
import { ensureAgentWorktree } from "./worktrees.mjs";
|
|
7
7
|
import { registerAgent, formatAgentTitle } from "./store.mjs";
|
|
@@ -173,145 +173,316 @@ export function buildFleetEnvPath() {
|
|
|
173
173
|
process.env.PATH || "",
|
|
174
174
|
];
|
|
175
175
|
|
|
176
|
-
return Array.from(new Set(paths.filter(Boolean))).join(
|
|
176
|
+
return Array.from(new Set(paths.filter(Boolean))).join(path.delimiter);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
? (Array.isArray(options.agents) ? options.agents : options.agents.split(",")).map((s) => s.trim().toLowerCase())
|
|
189
|
-
: null;
|
|
179
|
+
function filterPersonas(fleet, filter) {
|
|
180
|
+
if (!filter) return fleet;
|
|
181
|
+
const selected = new Set(
|
|
182
|
+
(Array.isArray(filter) ? filter : filter.split(","))
|
|
183
|
+
.map((value) => value.trim().toLowerCase())
|
|
184
|
+
.filter(Boolean),
|
|
185
|
+
);
|
|
186
|
+
return fleet.filter((agent) => selected.has(agent.handle.toLowerCase()));
|
|
187
|
+
}
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
function pathsMatch(left, right) {
|
|
190
|
+
if (!left || !right) return false;
|
|
191
|
+
try {
|
|
192
|
+
return fs.realpathSync(left) === fs.realpathSync(right);
|
|
193
|
+
} catch {
|
|
194
|
+
return path.resolve(left) === path.resolve(right);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
launched: [],
|
|
201
|
-
failed: [],
|
|
202
|
-
dryRun,
|
|
203
|
-
};
|
|
198
|
+
function matchingFleetPanes(agent, liveAgents) {
|
|
199
|
+
return liveAgents.filter((live) => (
|
|
200
|
+
live.name === agent.handle && pathsMatch(live.cwd, agent.worktree)
|
|
201
|
+
));
|
|
202
|
+
}
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
activeHandles = new Set(liveAgents.map((a) => a.name).filter(Boolean));
|
|
210
|
-
} catch {}
|
|
204
|
+
function runHerdr(args, execHerdr) {
|
|
205
|
+
if (execHerdr) return execHerdr(args);
|
|
206
|
+
return execFileSync("herdr", args, { encoding: "utf8" });
|
|
207
|
+
}
|
|
211
208
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
} else {
|
|
216
|
-
result.wouldLaunch.push(agent.handle);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
209
|
+
function waitFor(ms) {
|
|
210
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
211
|
+
}
|
|
219
212
|
|
|
220
|
-
|
|
221
|
-
|
|
213
|
+
export function defaultLaunchArgs(kind, handle) {
|
|
214
|
+
if (kind === "agy") return ["--dangerously-skip-permissions"];
|
|
215
|
+
if (kind === "opencode") return ["--agent", handle, "--auto"];
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resolveExecutable(name, envPath = buildFleetEnvPath()) {
|
|
220
|
+
for (const dir of envPath.split(path.delimiter)) {
|
|
221
|
+
if (!dir) continue;
|
|
222
|
+
const candidate = path.join(dir, name);
|
|
223
|
+
try {
|
|
224
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
225
|
+
if (fs.statSync(candidate).isFile()) return fs.realpathSync(candidate);
|
|
226
|
+
} catch {}
|
|
222
227
|
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
function validModel(model) {
|
|
232
|
+
return typeof model === "string" && /^[^\s/]+\/[^\s/]+/.test(model) ? model : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function readOpencodeModel(worktree) {
|
|
236
|
+
for (const relativePath of ["opencode.json", path.join(".opencode", "opencode.json")]) {
|
|
237
|
+
const configPath = path.join(worktree, relativePath);
|
|
227
238
|
try {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const matched = workspaces.find((w) => w.cwd === repoRoot || w.label === path.basename(repoRoot));
|
|
232
|
-
workspaceId = matched ? matched.workspace_id : (workspaces[0]?.workspace_id || null);
|
|
239
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
240
|
+
const model = validModel(config.model);
|
|
241
|
+
if (model) return model;
|
|
233
242
|
} catch {}
|
|
234
243
|
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
235
246
|
|
|
236
|
-
|
|
247
|
+
function shellQuote(value) {
|
|
248
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function createOpencodeLauncher(handle, model, executable) {
|
|
252
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "herdr-amq-opencode-"));
|
|
253
|
+
const binDir = path.join(root, handle.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
254
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
255
|
+
const agentConfig = { mode: "all" };
|
|
256
|
+
const selectedModel = validModel(model);
|
|
257
|
+
if (selectedModel) agentConfig.model = selectedModel;
|
|
258
|
+
const config = JSON.stringify({ agent: { [handle]: agentConfig } });
|
|
259
|
+
const launcher = path.join(binDir, "opencode");
|
|
260
|
+
fs.writeFileSync(
|
|
261
|
+
launcher,
|
|
262
|
+
`#!/bin/sh\nexport OPENCODE_CONFIG_CONTENT=${shellQuote(config)}\nexec ${shellQuote(executable)} "$@"\n`,
|
|
263
|
+
{ mode: 0o700 },
|
|
264
|
+
);
|
|
265
|
+
return { root, binDir, launcher, config };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function stopFleet(amqRoot, repoRoot, options = {}) {
|
|
269
|
+
const personas = discoverFleetPersonas(repoRoot);
|
|
270
|
+
const fleet = [...personas.values()].map((agent) => ({
|
|
271
|
+
...agent,
|
|
272
|
+
worktree: path.join(repoRoot, ".worktrees", agent.handle),
|
|
273
|
+
}));
|
|
274
|
+
const targetFleet = filterPersonas(fleet, options.agents);
|
|
275
|
+
const kind = options.kind || null;
|
|
276
|
+
const dryRun = Boolean(options.dryRun);
|
|
277
|
+
const getLiveAgents = options.getLiveAgents || getHerdrAgents;
|
|
278
|
+
const execHerdr = options.execHerdr || null;
|
|
279
|
+
const liveAgents = await getLiveAgents();
|
|
280
|
+
const result = {
|
|
281
|
+
total: targetFleet.length,
|
|
282
|
+
stopped: [],
|
|
283
|
+
wouldStop: [],
|
|
284
|
+
skipped: [],
|
|
285
|
+
failed: [],
|
|
286
|
+
dryRun,
|
|
287
|
+
};
|
|
237
288
|
|
|
238
|
-
// 4. Launch each non-active agent into a tab
|
|
239
289
|
for (const agent of targetFleet) {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
290
|
+
const panes = matchingFleetPanes(agent, liveAgents);
|
|
291
|
+
const selected = kind ? panes.filter((pane) => pane.agent === kind) : panes;
|
|
292
|
+
if (selected.length === 0) {
|
|
293
|
+
if (panes.length > 0) {
|
|
294
|
+
result.skipped.push({ handle: agent.handle, reason: `kind is ${panes.map((pane) => pane.agent).join(", ")}` });
|
|
295
|
+
}
|
|
243
296
|
continue;
|
|
244
297
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
252
|
-
agent.
|
|
253
|
-
|
|
254
|
-
handle,
|
|
255
|
-
"--env",
|
|
256
|
-
`PATH=${safePath}`,
|
|
257
|
-
"--no-focus",
|
|
258
|
-
];
|
|
259
|
-
if (workspaceId) {
|
|
260
|
-
tabArgs.push("--workspace", workspaceId);
|
|
298
|
+
if (dryRun) {
|
|
299
|
+
result.wouldStop.push(...selected.map((pane) => ({ handle: agent.handle, paneId: pane.pane_id, kind: pane.agent })));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
for (const pane of selected) {
|
|
303
|
+
try {
|
|
304
|
+
runHerdr(["pane", "close", pane.pane_id], execHerdr);
|
|
305
|
+
result.stopped.push({ handle: agent.handle, paneId: pane.pane_id, kind: pane.agent });
|
|
306
|
+
} catch (error) {
|
|
307
|
+
result.failed.push({ handle: agent.handle, paneId: pane.pane_id, error: error.message });
|
|
261
308
|
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
262
311
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const paneId = tabJson?.result?.root_pane?.pane_id;
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
266
314
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Launch or recover the agent fleet inside Herdr
|
|
317
|
+
*/
|
|
318
|
+
export async function launchFleet(amqRoot, repoRoot, options = {}) {
|
|
319
|
+
const kind = options.kind || "agy";
|
|
320
|
+
const dryRun = Boolean(options.dryRun);
|
|
321
|
+
const timeoutMs = options.timeout || 25000;
|
|
322
|
+
const replace = options.replace !== false;
|
|
323
|
+
const prepopulate = options.prepopulate || prepopulateFleet;
|
|
324
|
+
const getLiveAgents = options.getLiveAgents || getHerdrAgents;
|
|
325
|
+
const execHerdr = options.execHerdr || null;
|
|
326
|
+
const sleep = options.sleep || waitFor;
|
|
327
|
+
const safePath = options.envPath || buildFleetEnvPath();
|
|
328
|
+
const configuredArgs = options.args == null
|
|
329
|
+
? defaultLaunchArgs(kind, "")
|
|
330
|
+
: Array.isArray(options.args) ? options.args : [String(options.args)];
|
|
331
|
+
const fleet = prepopulate(amqRoot, repoRoot);
|
|
332
|
+
const targetFleet = filterPersonas(fleet, options.agents);
|
|
333
|
+
const result = {
|
|
334
|
+
total: targetFleet.length,
|
|
335
|
+
prepopulated: targetFleet.map((agent) => agent.handle),
|
|
336
|
+
alreadyRunning: [],
|
|
337
|
+
replaced: [],
|
|
338
|
+
wouldReplace: [],
|
|
339
|
+
wouldLaunch: [],
|
|
340
|
+
blocked: [],
|
|
341
|
+
launched: [],
|
|
342
|
+
failed: [],
|
|
343
|
+
dryRun,
|
|
344
|
+
};
|
|
345
|
+
const liveAgents = await getLiveAgents();
|
|
346
|
+
const launcherRoots = new Set();
|
|
347
|
+
let workspaceId = process.env.HERDR_WORKSPACE_ID || null;
|
|
348
|
+
let workspaceLookupAttempted = Boolean(workspaceId);
|
|
270
349
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
350
|
+
try {
|
|
351
|
+
for (const agent of targetFleet) {
|
|
352
|
+
const handle = agent.handle;
|
|
353
|
+
const panes = matchingFleetPanes(agent, liveAgents);
|
|
354
|
+
const matchingKind = panes.filter((pane) => pane.agent === kind);
|
|
355
|
+
|
|
356
|
+
if (matchingKind.length > 0) {
|
|
357
|
+
result.alreadyRunning.push(handle);
|
|
358
|
+
for (const duplicate of matchingKind.slice(1)) {
|
|
359
|
+
try {
|
|
360
|
+
runHerdr(["pane", "close", duplicate.pane_id], execHerdr);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
result.failed.push({ handle, error: error.message });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
continue;
|
|
286
366
|
}
|
|
287
367
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
368
|
+
if (panes.length > 0) {
|
|
369
|
+
if (dryRun) {
|
|
370
|
+
result.wouldReplace.push(handle);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!replace) {
|
|
374
|
+
result.blocked.push({ handle, reason: `already running as ${panes.map((pane) => pane.agent).join(", ")}` });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
291
377
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
} else {
|
|
295
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
296
|
-
}
|
|
297
|
-
execFileSync("herdr", startArgs, { encoding: "utf8" });
|
|
298
|
-
started = true;
|
|
299
|
-
result.launched.push({ handle, paneId, kind });
|
|
300
|
-
break;
|
|
301
|
-
} catch (err) {
|
|
302
|
-
lastErr = err;
|
|
303
|
-
if (err.message && err.message.includes("agent_pane_busy")) {
|
|
304
|
-
continue;
|
|
378
|
+
for (const pane of panes) {
|
|
379
|
+
runHerdr(["pane", "close", pane.pane_id], execHerdr);
|
|
305
380
|
}
|
|
306
|
-
|
|
381
|
+
await sleep(300);
|
|
382
|
+
result.replaced.push({
|
|
383
|
+
handle,
|
|
384
|
+
fromKinds: panes.map((pane) => pane.agent),
|
|
385
|
+
paneIds: panes.map((pane) => pane.pane_id),
|
|
386
|
+
});
|
|
387
|
+
} catch (error) {
|
|
388
|
+
result.failed.push({ handle, error: error.message });
|
|
389
|
+
continue;
|
|
307
390
|
}
|
|
308
391
|
}
|
|
309
392
|
|
|
310
|
-
if (
|
|
311
|
-
result.
|
|
393
|
+
if (dryRun) {
|
|
394
|
+
result.wouldLaunch.push(handle);
|
|
395
|
+
continue;
|
|
312
396
|
}
|
|
313
|
-
|
|
314
|
-
|
|
397
|
+
|
|
398
|
+
if (!workspaceLookupAttempted) {
|
|
399
|
+
workspaceLookupAttempted = true;
|
|
400
|
+
try {
|
|
401
|
+
const workspaceOutput = runHerdr(["workspace", "list"], execHerdr);
|
|
402
|
+
const workspaceJson = JSON.parse(workspaceOutput);
|
|
403
|
+
const workspaces = workspaceJson?.result?.workspaces || [];
|
|
404
|
+
const matched = workspaces.find((workspace) => workspace.cwd === repoRoot || workspace.label === path.basename(repoRoot));
|
|
405
|
+
workspaceId = matched ? matched.workspace_id : (workspaces[0]?.workspace_id || null);
|
|
406
|
+
} catch {}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let paneId;
|
|
410
|
+
try {
|
|
411
|
+
let launchPath = safePath;
|
|
412
|
+
if (kind === "opencode") {
|
|
413
|
+
const executable = options.opencodeExecutable || resolveExecutable("opencode", safePath);
|
|
414
|
+
if (!executable) throw new Error("OpenCode executable not found in fleet PATH");
|
|
415
|
+
const createLauncher = options.createOpencodeLauncher || createOpencodeLauncher;
|
|
416
|
+
const launcher = createLauncher(handle, readOpencodeModel(agent.worktree), executable);
|
|
417
|
+
launcherRoots.add(launcher.root);
|
|
418
|
+
launchPath = `${launcher.binDir}${path.delimiter}${safePath}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const tabArgs = [
|
|
422
|
+
"tab",
|
|
423
|
+
"create",
|
|
424
|
+
"--cwd",
|
|
425
|
+
agent.worktree,
|
|
426
|
+
"--label",
|
|
427
|
+
handle,
|
|
428
|
+
"--env",
|
|
429
|
+
`PATH=${launchPath}`,
|
|
430
|
+
"--no-focus",
|
|
431
|
+
];
|
|
432
|
+
if (workspaceId) tabArgs.push("--workspace", workspaceId);
|
|
433
|
+
const tabOutput = runHerdr(tabArgs, execHerdr);
|
|
434
|
+
const tabJson = JSON.parse(tabOutput);
|
|
435
|
+
paneId = tabJson?.result?.root_pane?.pane_id;
|
|
436
|
+
if (!paneId) throw new Error(`Failed to acquire pane_id from herdr tab create: ${tabOutput}`);
|
|
437
|
+
|
|
438
|
+
const startArgs = [
|
|
439
|
+
"agent",
|
|
440
|
+
"start",
|
|
441
|
+
handle,
|
|
442
|
+
"--kind",
|
|
443
|
+
kind,
|
|
444
|
+
"--pane",
|
|
445
|
+
paneId,
|
|
446
|
+
"--timeout",
|
|
447
|
+
String(timeoutMs),
|
|
448
|
+
];
|
|
449
|
+
const launchArgs = kind === "opencode" && options.args == null
|
|
450
|
+
? defaultLaunchArgs(kind, handle)
|
|
451
|
+
: configuredArgs;
|
|
452
|
+
if (launchArgs.length > 0) startArgs.push("--", ...launchArgs);
|
|
453
|
+
|
|
454
|
+
let started = false;
|
|
455
|
+
let lastError = null;
|
|
456
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
457
|
+
try {
|
|
458
|
+
await sleep(attempt === 0 ? 800 : 1200);
|
|
459
|
+
runHerdr(startArgs, execHerdr);
|
|
460
|
+
started = true;
|
|
461
|
+
result.launched.push({ handle, paneId, kind });
|
|
462
|
+
break;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
lastError = error;
|
|
465
|
+
if (error.message && error.message.includes("agent_pane_busy")) continue;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!started) {
|
|
470
|
+
throw lastError || new Error("Failed to start agent");
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
result.failed.push({ handle, error: error.message || String(error) });
|
|
474
|
+
if (paneId) {
|
|
475
|
+
try {
|
|
476
|
+
runHerdr(["pane", "close", paneId], execHerdr);
|
|
477
|
+
} catch {}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} finally {
|
|
482
|
+
for (const root of launcherRoots) {
|
|
483
|
+
try {
|
|
484
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
485
|
+
} catch {}
|
|
315
486
|
}
|
|
316
487
|
}
|
|
317
488
|
|
package/src/herdr.mjs
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import net from "node:net";
|
|
14
|
-
import { execSync } from "node:child_process";
|
|
15
14
|
import os from "node:os";
|
|
16
15
|
import path from "node:path";
|
|
16
|
+
import { getOpenCodeSessionModels, resolveRuntimeModel } from "./runtime-models.mjs";
|
|
17
17
|
|
|
18
18
|
// ─── Socket Path Resolution ──────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -108,24 +108,78 @@ export async function getHerdrAgents() {
|
|
|
108
108
|
*/
|
|
109
109
|
export async function getHerdrStatusMap() {
|
|
110
110
|
const agents = await getHerdrAgents();
|
|
111
|
+
const observedAt = new Date().toISOString();
|
|
112
|
+
const sessionModels = getOpenCodeSessionModels(agents);
|
|
111
113
|
const map = new Map();
|
|
112
|
-
for (const
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
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
|
-
}
|
|
114
|
+
for (const agent of agents) {
|
|
115
|
+
const activity = mapHerdrAgentActivity(agent, observedAt, resolveRuntimeModel(agent, sessionModels));
|
|
116
|
+
if (activity) map.set(activity.herdrHandle, activity);
|
|
125
117
|
}
|
|
126
118
|
return map;
|
|
127
119
|
}
|
|
128
120
|
|
|
121
|
+
export function normalizeHerdrStatus(value) {
|
|
122
|
+
const status = String(value || "").trim().toLowerCase();
|
|
123
|
+
if (status === "online" || status === "active") return "idle";
|
|
124
|
+
if (["idle", "working", "blocked", "done", "error", "unknown"].includes(status)) return status;
|
|
125
|
+
return "unknown";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function herdrText(value) {
|
|
129
|
+
if (typeof value === "string") return value.trim();
|
|
130
|
+
if (!value || typeof value !== "object") return "";
|
|
131
|
+
for (const key of ["text", "label", "title", "value"]) {
|
|
132
|
+
if (typeof value[key] === "string" && value[key].trim()) return value[key].trim();
|
|
133
|
+
}
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function herdrStateLabels(value) {
|
|
138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
139
|
+
const result = {};
|
|
140
|
+
for (const [status, label] of Object.entries(value)) {
|
|
141
|
+
const text = herdrText(label);
|
|
142
|
+
if (text) result[status] = text;
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function herdrTokens(value) {
|
|
148
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
149
|
+
return values.map(herdrText).filter(Boolean);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function mapHerdrAgentActivity(agent, observedAt = new Date().toISOString(), runtime = null) {
|
|
153
|
+
const handle = herdrText(agent?.name);
|
|
154
|
+
if (!handle) return null;
|
|
155
|
+
const status = normalizeHerdrStatus(agent?.agent_status);
|
|
156
|
+
const terminalTitle = herdrText(agent?.terminal_title_stripped) || herdrText(agent?.terminal_title);
|
|
157
|
+
const metadataTitle = herdrText(agent?.title);
|
|
158
|
+
const model = runtime || resolveRuntimeModel(agent);
|
|
159
|
+
return {
|
|
160
|
+
herdrHandle: handle,
|
|
161
|
+
herdrStatus: status,
|
|
162
|
+
herdrPaneId: herdrText(agent?.pane_id),
|
|
163
|
+
herdrWorkspaceId: herdrText(agent?.workspace_id),
|
|
164
|
+
herdrTabId: herdrText(agent?.tab_id),
|
|
165
|
+
herdrTerminalId: herdrText(agent?.terminal_id),
|
|
166
|
+
herdrTitle: terminalTitle || metadataTitle,
|
|
167
|
+
herdrTerminalTitle: terminalTitle,
|
|
168
|
+
herdrMetadataTitle: metadataTitle,
|
|
169
|
+
herdrStateLabels: herdrStateLabels(agent?.state_labels),
|
|
170
|
+
herdrTokens: herdrTokens(agent?.tokens),
|
|
171
|
+
herdrStateChangeSeq: Number.isFinite(agent?.state_change_seq) ? agent.state_change_seq : null,
|
|
172
|
+
interactiveReady: Boolean(agent?.interactive_ready),
|
|
173
|
+
herdrFocused: Boolean(agent?.focused),
|
|
174
|
+
herdrLaunchPending: Boolean(agent?.launch_pending),
|
|
175
|
+
agentType: herdrText(agent?.agent),
|
|
176
|
+
herdrSessionId: model.sessionId || null,
|
|
177
|
+
herdrModel: model.model || null,
|
|
178
|
+
herdrModelSource: model.source || null,
|
|
179
|
+
herdrObservedAt: observedAt,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
129
183
|
// ─── Long-lived event subscription ──────────────────────────────────────────
|
|
130
184
|
|
|
131
185
|
/**
|
package/src/index.mjs
CHANGED