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/bridge.mjs
CHANGED
|
@@ -6,9 +6,15 @@ import {
|
|
|
6
6
|
getStateDir,
|
|
7
7
|
getConfigDir,
|
|
8
8
|
findAmqRoot,
|
|
9
|
+
getRepoRootFromAmq,
|
|
9
10
|
getAgentHandles,
|
|
10
11
|
execCmd,
|
|
11
12
|
} from "./config.mjs";
|
|
13
|
+
import { listBacklogTasks, getAgentTaskStats } from "./board.mjs";
|
|
14
|
+
import { loadLocalTemplate, renderTemplate } from "./templates.mjs";
|
|
15
|
+
import { isSafeMailIdentifier, listMaildirMessageFiles, readMaildirMessageFile, writeBoundedFileAtomic } from "./protocol.mjs";
|
|
16
|
+
|
|
17
|
+
const MAX_DOORBELL_PROMPT_BYTES = 64 * 1024;
|
|
12
18
|
|
|
13
19
|
function getPidFile() {
|
|
14
20
|
return path.join(getStateDir(), "bridge.pid");
|
|
@@ -120,12 +126,13 @@ function healAgentName(handle, dryRun = false) {
|
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
function promptAgent(handle, text, dryRun = false) {
|
|
129
|
+
const boundedText = boundDoorbellPrompt(text);
|
|
123
130
|
if (dryRun || process.env.HERDR_DISABLE_PROMPT === "1" || process.env.NODE_ENV === "test") {
|
|
124
|
-
console.log(`[bridge] DRY: would prompt ${handle}: ${
|
|
131
|
+
console.log(`[bridge] DRY: would prompt ${handle}: ${boundedText.slice(0, 60)}...`);
|
|
125
132
|
return true;
|
|
126
133
|
}
|
|
127
134
|
try {
|
|
128
|
-
runHerdr(["agent", "prompt", handle,
|
|
135
|
+
runHerdr(["agent", "prompt", handle, boundedText]);
|
|
129
136
|
return true;
|
|
130
137
|
} catch (err) {
|
|
131
138
|
console.warn(`[bridge] Warning: prompt ${handle} failed: ${err.message}`);
|
|
@@ -149,12 +156,50 @@ function recordAlert(handle, count, from, dryRun = false) {
|
|
|
149
156
|
} catch {}
|
|
150
157
|
}
|
|
151
158
|
|
|
159
|
+
function safeStateText(value, maxLength = 512) {
|
|
160
|
+
if (typeof value !== "string") return "";
|
|
161
|
+
return value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sanitizeDeliveredState(value) {
|
|
165
|
+
const delivered = {};
|
|
166
|
+
const deliveredTasks = {};
|
|
167
|
+
if (!value || typeof value !== "object") return { delivered, deliveredTasks, recoveryRequired: true };
|
|
168
|
+
|
|
169
|
+
for (const [id, entry] of Object.entries(value.delivered || {})) {
|
|
170
|
+
if (!isSafeMailIdentifier(id) || !entry || typeof entry !== "object") continue;
|
|
171
|
+
if (!isSafeMailIdentifier(entry.to, 128) || !isSafeMailIdentifier(entry.from, 128)) continue;
|
|
172
|
+
if (typeof entry.at !== "string") continue;
|
|
173
|
+
delivered[id] = {
|
|
174
|
+
at: entry.at,
|
|
175
|
+
to: entry.to,
|
|
176
|
+
from: entry.from,
|
|
177
|
+
attempts: Number.isFinite(entry.attempts) ? Math.max(1, Math.trunc(entry.attempts)) : 1,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const [id, entry] of Object.entries(value.deliveredTasks || {})) {
|
|
182
|
+
if (!isSafeMailIdentifier(id) || !entry || typeof entry !== "object") continue;
|
|
183
|
+
if (!isSafeMailIdentifier(entry.to, 128) || typeof entry.at !== "string") continue;
|
|
184
|
+
deliveredTasks[id] = {
|
|
185
|
+
at: entry.at,
|
|
186
|
+
to: entry.to,
|
|
187
|
+
title: safeStateText(entry.title),
|
|
188
|
+
attempts: Number.isFinite(entry.attempts) ? Math.max(1, Math.trunc(entry.attempts)) : 1,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { delivered, deliveredTasks, recoveryRequired: false };
|
|
193
|
+
}
|
|
194
|
+
|
|
152
195
|
function loadDeliveredState() {
|
|
153
196
|
const stateFile = getStateFile();
|
|
154
197
|
try {
|
|
155
|
-
|
|
198
|
+
const content = readMaildirMessageFile(stateFile, 2 * 1024 * 1024);
|
|
199
|
+
if (content === null) return { delivered: {}, deliveredTasks: {}, recoveryRequired: true };
|
|
200
|
+
return sanitizeDeliveredState(JSON.parse(content));
|
|
156
201
|
} catch {
|
|
157
|
-
return { delivered: {} };
|
|
202
|
+
return { delivered: {}, deliveredTasks: {}, recoveryRequired: true };
|
|
158
203
|
}
|
|
159
204
|
}
|
|
160
205
|
|
|
@@ -167,7 +212,14 @@ function saveDeliveredState(state) {
|
|
|
167
212
|
delete state.delivered[id];
|
|
168
213
|
}
|
|
169
214
|
}
|
|
170
|
-
|
|
215
|
+
const taskIds = Object.keys(state.deliveredTasks || {});
|
|
216
|
+
if (taskIds.length > 2000) {
|
|
217
|
+
taskIds.sort();
|
|
218
|
+
for (const id of taskIds.slice(0, taskIds.length - 1500)) {
|
|
219
|
+
delete state.deliveredTasks[id];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
writeBoundedFileAtomic(stateFile, JSON.stringify(state, null, 1), 2 * 1024 * 1024);
|
|
171
223
|
}
|
|
172
224
|
|
|
173
225
|
export function listInbox(amqRoot, handle) {
|
|
@@ -190,11 +242,12 @@ export function listInbox(amqRoot, handle) {
|
|
|
190
242
|
try {
|
|
191
243
|
const newDir = path.join(amqRoot, "agents", handle, "inbox", "new");
|
|
192
244
|
if (!fs.existsSync(newDir)) return [];
|
|
193
|
-
const files =
|
|
245
|
+
const files = listMaildirMessageFiles(newDir).filter((f) => !f.startsWith("."));
|
|
194
246
|
const msgs = [];
|
|
195
247
|
for (const f of files) {
|
|
196
248
|
const fullPath = path.join(newDir, f);
|
|
197
|
-
const content =
|
|
249
|
+
const content = readMaildirMessageFile(fullPath);
|
|
250
|
+
if (content === null) continue;
|
|
198
251
|
const jsonMatch = content.match(/^---json\r?\n([\s\S]*?)\r?\n---/);
|
|
199
252
|
if (jsonMatch) {
|
|
200
253
|
try {
|
|
@@ -239,16 +292,115 @@ export function listInbox(amqRoot, handle) {
|
|
|
239
292
|
}
|
|
240
293
|
}
|
|
241
294
|
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
function boundDoorbellPrompt(text, maxBytes = MAX_DOORBELL_PROMPT_BYTES) {
|
|
296
|
+
const value = typeof text === "string" ? text : String(text ?? "");
|
|
297
|
+
const bytes = Buffer.from(value, "utf8");
|
|
298
|
+
return bytes.length <= maxBytes
|
|
299
|
+
? value
|
|
300
|
+
: bytes.subarray(0, Math.max(0, maxBytes)).toString("utf8");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function safeCount(value) {
|
|
304
|
+
const count = Number(value);
|
|
305
|
+
return Number.isFinite(count) ? Math.max(0, Math.trunc(count)) : 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function safeSender(value) {
|
|
309
|
+
const sender = typeof value === "string" ? value.trim() : "";
|
|
310
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(sender) ? sender : "unknown";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeDoorbellStats(taskStatsOrBacklog) {
|
|
314
|
+
const raw = Array.isArray(taskStatsOrBacklog)
|
|
315
|
+
? { backlog: taskStatsOrBacklog.length, blocked: 0, doing: 0, done: 0 }
|
|
316
|
+
: taskStatsOrBacklog && typeof taskStatsOrBacklog === "object"
|
|
317
|
+
? taskStatsOrBacklog
|
|
318
|
+
: { backlog: 0, blocked: 0, doing: 0, done: 0 };
|
|
319
|
+
const stats = {
|
|
320
|
+
backlog: safeCount(raw.backlog),
|
|
321
|
+
blocked: safeCount(raw.blocked),
|
|
322
|
+
doing: safeCount(raw.doing),
|
|
323
|
+
done: safeCount(raw.done),
|
|
324
|
+
};
|
|
325
|
+
stats.total = safeCount(raw.total ?? Object.values(stats).reduce((sum, value) => sum + value, 0));
|
|
326
|
+
return stats;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function buildDoorbellContext(handle, msgs, stats) {
|
|
330
|
+
return {
|
|
331
|
+
agent: { handle },
|
|
332
|
+
mail: {
|
|
333
|
+
count: safeCount(msgs.length),
|
|
334
|
+
senders: [...new Set(msgs.map((message) => safeSender(message.from)))].slice(0, 16).join(", "),
|
|
335
|
+
},
|
|
336
|
+
board: stats,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildRequiredDoorbellActions(handle, context) {
|
|
341
|
+
const actions = [];
|
|
342
|
+
if (context.mail.count > 0) {
|
|
343
|
+
actions.push(`Run: herdr-amq mail drain --me ${handle} --include-body.`);
|
|
344
|
+
}
|
|
345
|
+
if (context.board.backlog > 0) {
|
|
346
|
+
actions.push(`Run: herdr-amq task drain --me ${handle}; claim with herdr-amq task next --me ${handle}.`);
|
|
347
|
+
}
|
|
348
|
+
if (context.mail.count > 0) {
|
|
349
|
+
actions.push("Reply only when a message explicitly requests action or asks a question.");
|
|
350
|
+
}
|
|
351
|
+
return actions.join(" ");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildDefaultDoorbellPrompt(handle, msgs, stats) {
|
|
355
|
+
const context = buildDoorbellContext(handle, msgs, stats);
|
|
356
|
+
const taskDetails = [];
|
|
357
|
+
if (context.board.blocked > 0) taskDetails.push(`${context.board.blocked} blocked`);
|
|
358
|
+
if (context.board.doing > 0) taskDetails.push(`${context.board.doing} in progress`);
|
|
359
|
+
if (context.board.done > 0) taskDetails.push(`${context.board.done} done`);
|
|
360
|
+
const details = taskDetails.length ? ` (${taskDetails.join(", ")})` : "";
|
|
361
|
+
let summary;
|
|
362
|
+
if (context.mail.count > 0 && context.board.backlog > 0) {
|
|
363
|
+
summary = `AMQ & Task doorbell: ${context.mail.count} new message(s) from ${context.mail.senders}. You have ${context.board.backlog} task(s) in backlog${details}.`;
|
|
364
|
+
} else if (context.board.backlog > 0) {
|
|
365
|
+
summary = `Task doorbell: You have ${context.board.backlog} task(s) in backlog${details}.`;
|
|
366
|
+
} else {
|
|
367
|
+
summary = `AMQ doorbell: ${context.mail.count} new message(s) in your inbox from ${context.mail.senders}${details}.`;
|
|
368
|
+
}
|
|
369
|
+
return boundDoorbellPrompt(`${summary} ${buildRequiredDoorbellActions(handle, context)}`.trim());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export const buildDoorbellPrompt = (handle, msgs = [], taskStatsOrBacklog = null, templateSource = null) => {
|
|
373
|
+
const stats = normalizeDoorbellStats(taskStatsOrBacklog);
|
|
374
|
+
const context = buildDoorbellContext(handle, msgs, stats);
|
|
375
|
+
const fallback = buildDefaultDoorbellPrompt(handle, msgs, stats);
|
|
376
|
+
if (!templateSource) return fallback;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const custom = renderTemplate(templateSource, context).trim();
|
|
380
|
+
if (!custom) return fallback;
|
|
381
|
+
const requiredActions = `Required actions: ${buildRequiredDoorbellActions(handle, context)}`;
|
|
382
|
+
const bodyLimit = Math.max(0, MAX_DOORBELL_PROMPT_BYTES - Buffer.byteLength(requiredActions, "utf8") - 2);
|
|
383
|
+
return `${boundDoorbellPrompt(custom, bodyLimit)}\n\n${requiredActions}`;
|
|
384
|
+
} catch {
|
|
385
|
+
return fallback;
|
|
386
|
+
}
|
|
250
387
|
};
|
|
251
388
|
|
|
389
|
+
export const DEFAULT_DOORBELL_COOLDOWN_MS = 45000; // 45s cooldown window
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Determines whether an item was recently doorbelled and is still in-flight.
|
|
393
|
+
* If the item has not been drained after the cooldown window, it no longer
|
|
394
|
+
* counts as delivered and will be re-doorbelled.
|
|
395
|
+
*/
|
|
396
|
+
export function isItemPendingDrain(deliveryEntry, cooldownMs = DEFAULT_DOORBELL_COOLDOWN_MS, force = false) {
|
|
397
|
+
if (force) return false;
|
|
398
|
+
if (!deliveryEntry || !deliveryEntry.at) return false;
|
|
399
|
+
const deliveredAt = new Date(deliveryEntry.at).getTime();
|
|
400
|
+
if (isNaN(deliveredAt)) return false;
|
|
401
|
+
return Date.now() - deliveredAt < cooldownMs;
|
|
402
|
+
}
|
|
403
|
+
|
|
252
404
|
// ─── Doorbell Pass ────────────────────────────────────────────────────────────
|
|
253
405
|
|
|
254
406
|
export function runDoorbellPass({
|
|
@@ -256,6 +408,14 @@ export function runDoorbellPass({
|
|
|
256
408
|
handles = null,
|
|
257
409
|
targetHandle = null,
|
|
258
410
|
dryRun = false,
|
|
411
|
+
force = false,
|
|
412
|
+
allowPrompt = false,
|
|
413
|
+
persistState = false,
|
|
414
|
+
state: injectedState = null,
|
|
415
|
+
getStatus = getAgentStatus,
|
|
416
|
+
healName = healAgentName,
|
|
417
|
+
prompt = promptAgent,
|
|
418
|
+
cooldownMs = parseInt(process.env.HERDR_DOORBELL_COOLDOWN_MS || String(DEFAULT_DOORBELL_COOLDOWN_MS), 10),
|
|
259
419
|
} = {}) {
|
|
260
420
|
if (!amqRoot) {
|
|
261
421
|
return { ok: false, error: "No .agent-mail queue found." };
|
|
@@ -266,59 +426,113 @@ export function runDoorbellPass({
|
|
|
266
426
|
: (handles && handles.length > 0 ? handles : getAgentHandles(amqRoot));
|
|
267
427
|
|
|
268
428
|
if (!agentList.length) {
|
|
269
|
-
return { ok: true, checked: 0, doorbelled: 0, message: "No registered agents found." };
|
|
429
|
+
return { ok: true, checked: 0, doorbelled: 0, doorbelledTasks: 0, message: "No registered agents found." };
|
|
270
430
|
}
|
|
271
431
|
|
|
272
|
-
const
|
|
432
|
+
const repoRoot = getRepoRootFromAmq(amqRoot);
|
|
433
|
+
const doorbellTemplate = loadLocalTemplate(amqRoot, "doorbell");
|
|
434
|
+
const state = injectedState || loadDeliveredState();
|
|
273
435
|
state.delivered = state.delivered || {};
|
|
436
|
+
state.deliveredTasks = state.deliveredTasks || {};
|
|
274
437
|
|
|
275
438
|
let doorbelledCount = 0;
|
|
439
|
+
let doorbelledTasksCount = 0;
|
|
276
440
|
const results = [];
|
|
277
441
|
|
|
278
442
|
for (const handle of agentList) {
|
|
443
|
+
if (typeof handle !== "string" || !/^[a-z0-9_-]{1,128}$/.test(handle)) {
|
|
444
|
+
results.push({ handle: String(handle), status: "invalid", count: 0, tasksCount: 0, action: "invalid_handle" });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
279
447
|
const rawMsgs = listInbox(amqRoot, handle);
|
|
280
448
|
// Ignore self-messages
|
|
281
449
|
const msgs = rawMsgs.filter((m) => m.from !== handle);
|
|
450
|
+
// If message is still in inbox/new after cooldown, the agent did NOT drain it!
|
|
451
|
+
const undeliveredMsgs = msgs.filter((m) => !isItemPendingDrain(state.delivered[m.id], cooldownMs, force));
|
|
282
452
|
|
|
283
|
-
|
|
453
|
+
const backlogTasks = listBacklogTasks(repoRoot, amqRoot, handle);
|
|
454
|
+
// If task is still in backlog after cooldown, the agent did NOT claim/drain it!
|
|
455
|
+
const undeliveredTasks = backlogTasks.filter((t) => !isItemPendingDrain(state.deliveredTasks?.[t.id], cooldownMs, force));
|
|
284
456
|
|
|
285
|
-
|
|
286
|
-
const undelivered = msgs.filter((m) => !state.delivered[m.id]);
|
|
287
|
-
if (!undelivered.length) continue;
|
|
457
|
+
if (!undeliveredMsgs.length && !undeliveredTasks.length) continue;
|
|
288
458
|
|
|
289
|
-
let status =
|
|
459
|
+
let status = getStatus(handle);
|
|
290
460
|
if (status === "missing") {
|
|
291
|
-
const healed =
|
|
461
|
+
const healed = healName(
|
|
462
|
+
handle,
|
|
463
|
+
dryRun || !allowPrompt || process.env.HERDR_DISABLE_PROMPT === "1"
|
|
464
|
+
);
|
|
292
465
|
if (healed) status = getAgentStatus(handle);
|
|
293
466
|
}
|
|
294
467
|
|
|
468
|
+
const taskStats = getAgentTaskStats(repoRoot, amqRoot, handle);
|
|
469
|
+
taskStats.backlog = undeliveredTasks.length > 0 ? undeliveredTasks.length : backlogTasks.length;
|
|
470
|
+
|
|
295
471
|
if (status === "idle" || status === "done") {
|
|
296
|
-
const text = buildDoorbellPrompt(handle,
|
|
297
|
-
const ok =
|
|
472
|
+
const text = buildDoorbellPrompt(handle, undeliveredMsgs, taskStats, doorbellTemplate?.source || null);
|
|
473
|
+
const ok = prompt(handle, text, dryRun || !allowPrompt);
|
|
298
474
|
if (ok) {
|
|
299
|
-
doorbelledCount +=
|
|
475
|
+
doorbelledCount += undeliveredMsgs.length;
|
|
476
|
+
doorbelledTasksCount += undeliveredTasks.length;
|
|
300
477
|
if (!dryRun) {
|
|
301
|
-
for (const m of
|
|
478
|
+
for (const m of undeliveredMsgs) {
|
|
479
|
+
const prev = state.delivered[m.id];
|
|
302
480
|
state.delivered[m.id] = {
|
|
303
481
|
at: new Date().toISOString(),
|
|
304
482
|
to: handle,
|
|
305
483
|
from: m.from,
|
|
484
|
+
attempts: (prev?.attempts || 0) + 1,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
for (const t of undeliveredTasks) {
|
|
488
|
+
const prev = state.deliveredTasks[t.id];
|
|
489
|
+
state.deliveredTasks[t.id] = {
|
|
490
|
+
at: new Date().toISOString(),
|
|
491
|
+
to: handle,
|
|
492
|
+
title: t.title,
|
|
493
|
+
attempts: (prev?.attempts || 0) + 1,
|
|
306
494
|
};
|
|
307
495
|
}
|
|
308
496
|
}
|
|
309
|
-
results.push({
|
|
497
|
+
results.push({
|
|
498
|
+
handle,
|
|
499
|
+
status,
|
|
500
|
+
count: undeliveredMsgs.length,
|
|
501
|
+
tasksCount: undeliveredTasks.length,
|
|
502
|
+
action: allowPrompt && !dryRun ? "prompted" : "simulated",
|
|
503
|
+
});
|
|
310
504
|
}
|
|
311
505
|
} else if (status === "working") {
|
|
312
|
-
results.push({
|
|
506
|
+
results.push({
|
|
507
|
+
handle,
|
|
508
|
+
status,
|
|
509
|
+
count: undeliveredMsgs.length,
|
|
510
|
+
tasksCount: undeliveredTasks.length,
|
|
511
|
+
action: "working_wait",
|
|
512
|
+
});
|
|
313
513
|
} else if (status === "blocked") {
|
|
314
|
-
|
|
315
|
-
|
|
514
|
+
if (undeliveredMsgs.length) {
|
|
515
|
+
recordAlert(handle, undeliveredMsgs.length, undeliveredMsgs[0]?.from, dryRun || !allowPrompt);
|
|
516
|
+
}
|
|
517
|
+
results.push({
|
|
518
|
+
handle,
|
|
519
|
+
status,
|
|
520
|
+
count: undeliveredMsgs.length,
|
|
521
|
+
tasksCount: undeliveredTasks.length,
|
|
522
|
+
action: "alert_blocked",
|
|
523
|
+
});
|
|
316
524
|
} else {
|
|
317
|
-
results.push({
|
|
525
|
+
results.push({
|
|
526
|
+
handle,
|
|
527
|
+
status,
|
|
528
|
+
count: undeliveredMsgs.length,
|
|
529
|
+
tasksCount: undeliveredTasks.length,
|
|
530
|
+
action: "unknown_state",
|
|
531
|
+
});
|
|
318
532
|
}
|
|
319
533
|
}
|
|
320
534
|
|
|
321
|
-
if (!dryRun && doorbelledCount > 0) {
|
|
535
|
+
if (allowPrompt && persistState && !dryRun && (doorbelledCount > 0 || doorbelledTasksCount > 0)) {
|
|
322
536
|
saveDeliveredState(state);
|
|
323
537
|
}
|
|
324
538
|
|
|
@@ -327,6 +541,7 @@ export function runDoorbellPass({
|
|
|
327
541
|
amqRoot,
|
|
328
542
|
agentsChecked: agentList.length,
|
|
329
543
|
doorbelled: doorbelledCount,
|
|
544
|
+
doorbelledTasks: doorbelledTasksCount,
|
|
330
545
|
results,
|
|
331
546
|
};
|
|
332
547
|
}
|
|
@@ -359,7 +574,7 @@ export function startDaemonLoop({ interval = 3000, dryRun = false } = {}) {
|
|
|
359
574
|
const tick = () => {
|
|
360
575
|
try {
|
|
361
576
|
const handles = getAgentHandles(amqRoot);
|
|
362
|
-
const res = runDoorbellPass({ amqRoot, handles, dryRun });
|
|
577
|
+
const res = runDoorbellPass({ amqRoot, handles, dryRun, allowPrompt: true, persistState: true });
|
|
363
578
|
if (res.doorbelled > 0) {
|
|
364
579
|
console.log(`[bridge] Doorbelled ${res.doorbelled} message(s)`);
|
|
365
580
|
}
|
package/src/config.mjs
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function getPluginVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const pkgPath = path.resolve(__dirname, "../package.json");
|
|
11
|
+
if (fs.existsSync(pkgPath)) {
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
13
|
+
return pkg.version || "unknown";
|
|
14
|
+
}
|
|
15
|
+
} catch {}
|
|
16
|
+
return "unknown";
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
export function getHerdrBin() {
|
|
6
20
|
return process.env.HERDR_BIN_PATH || "herdr";
|
|
7
21
|
}
|
|
8
22
|
|
|
9
23
|
export function getStateDir() {
|
|
10
|
-
|
|
24
|
+
if (process.env.HERDR_PLUGIN_STATE_DIR) {
|
|
25
|
+
const dir = process.env.HERDR_PLUGIN_STATE_DIR;
|
|
26
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
const legacyDir = path.join(process.env.HOME || "/tmp", ".herdr-amq-state");
|
|
30
|
+
if (fs.existsSync(legacyDir)) {
|
|
31
|
+
return legacyDir;
|
|
32
|
+
}
|
|
33
|
+
const base = process.env.XDG_STATE_HOME || path.join(process.env.HOME || "/tmp", ".local", "state");
|
|
34
|
+
const dir = path.join(base, "herdr-amq");
|
|
11
35
|
if (!fs.existsSync(dir)) {
|
|
12
36
|
fs.mkdirSync(dir, { recursive: true });
|
|
13
37
|
}
|
|
@@ -22,7 +46,17 @@ export function getRepoRootFromAmq(amqRoot) {
|
|
|
22
46
|
}
|
|
23
47
|
|
|
24
48
|
export function getConfigDir() {
|
|
25
|
-
|
|
49
|
+
if (process.env.HERDR_PLUGIN_CONFIG_DIR) {
|
|
50
|
+
const dir = process.env.HERDR_PLUGIN_CONFIG_DIR;
|
|
51
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
const legacyDir = path.join(process.env.HOME || "/tmp", ".herdr-amq-config");
|
|
55
|
+
if (fs.existsSync(legacyDir)) {
|
|
56
|
+
return legacyDir;
|
|
57
|
+
}
|
|
58
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || "/tmp", ".config");
|
|
59
|
+
const dir = path.join(base, "herdr-amq");
|
|
26
60
|
if (!fs.existsSync(dir)) {
|
|
27
61
|
fs.mkdirSync(dir, { recursive: true });
|
|
28
62
|
}
|