mercury-agent 0.5.18-beta.3 → 0.5.19
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 +15 -0
- package/package.json +1 -1
- package/src/core/api-types.ts +3 -0
- package/src/core/api.ts +2 -0
- package/src/core/routes/broadcast.ts +78 -0
- package/src/core/routes/index.ts +1 -0
- package/src/core/runtime.ts +58 -0
- package/src/server.ts +1 -0
package/README.md
CHANGED
|
@@ -445,6 +445,21 @@ runtime:
|
|
|
445
445
|
|
|
446
446
|
Auto-created spaces are seeded with `trigger.match=always`, `context.mode=context`, and the configured member permissions. Changing `rate_limit_daily_member` in `mercury.yaml` propagates to all auto-created spaces immediately — no per-space update needed. Explicit per-space overrides (set via dashboard or API) still take precedence.
|
|
447
447
|
|
|
448
|
+
#### Broadcast
|
|
449
|
+
|
|
450
|
+
Send a message to all auto-created DM spaces at once (e.g. maintenance notices, promotions):
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
curl -X POST http://localhost:8787/api/broadcast \
|
|
454
|
+
-H "Authorization: Bearer $API_SECRET" \
|
|
455
|
+
-H "X-Mercury-Caller: <your-admin-id>" \
|
|
456
|
+
-H "X-Mercury-Space: main" \
|
|
457
|
+
-H "Content-Type: application/json" \
|
|
458
|
+
-d '{"text": "Maintenance tonight 10pm-2am."}'
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Returns `{ total, delivered, failed, errors }`. Only global admins (`admins` or `dm_auto_space.admin_ids`) may broadcast. Messages are sent as literal text — no LLM processing.
|
|
462
|
+
|
|
448
463
|
### Per-space Config
|
|
449
464
|
|
|
450
465
|
Conversations are discovered from incoming traffic. Unlinked conversations stay idle until you attach them to a space via `mercury link <conversation-id> <space-id>` or the dashboard.
|
package/package.json
CHANGED
package/src/core/api-types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { logger } from "../logger.js";
|
|
|
7
7
|
import type { Db } from "../storage/db.js";
|
|
8
8
|
import type { TradeStationFetch } from "../tradestation/host-api.js";
|
|
9
9
|
import { hasPermission } from "./permissions.js";
|
|
10
|
+
import type { MercuryCoreRuntime } from "./runtime.js";
|
|
10
11
|
import type { SpaceQueue } from "./space-queue.js";
|
|
11
12
|
import type { TaskScheduler } from "./task-scheduler.js";
|
|
12
13
|
|
|
@@ -22,6 +23,8 @@ export interface ApiContext {
|
|
|
22
23
|
configRegistry: ConfigRegistry;
|
|
23
24
|
/** Optional mock for TradeStation HTTP in tests */
|
|
24
25
|
tradeStationFetch?: TradeStationFetch;
|
|
26
|
+
/** Runtime reference for cross-space operations (broadcast) */
|
|
27
|
+
runtime?: MercuryCoreRuntime;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface AuthContext {
|
package/src/core/api.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ApiContext, AuthContext, Env } from "./api-types.js";
|
|
|
4
4
|
import { verifyCallerToken } from "./caller-token.js";
|
|
5
5
|
import { resolveRole } from "./permissions.js";
|
|
6
6
|
import {
|
|
7
|
+
broadcast,
|
|
7
8
|
capability,
|
|
8
9
|
config,
|
|
9
10
|
connections,
|
|
@@ -111,6 +112,7 @@ export function createApiApp(apiCtx: ApiContext): Hono<Env> {
|
|
|
111
112
|
app.route("/tradestation", tradestation);
|
|
112
113
|
app.route("/tts", tts);
|
|
113
114
|
app.route("/capability", capability);
|
|
115
|
+
app.route("/broadcast", broadcast);
|
|
114
116
|
|
|
115
117
|
// ─── Fallback ───────────────────────────────────────────────────────────
|
|
116
118
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { logger } from "../../logger.js";
|
|
3
|
+
import { type Env, getApiCtx, getAuth } from "../api-types.js";
|
|
4
|
+
|
|
5
|
+
export const broadcast = new Hono<Env>();
|
|
6
|
+
|
|
7
|
+
broadcast.post("/", async (c) => {
|
|
8
|
+
const { callerId } = getAuth(c);
|
|
9
|
+
const { config, runtime } = getApiCtx(c);
|
|
10
|
+
|
|
11
|
+
if (!config.dmAutoSpaceEnabled) {
|
|
12
|
+
return c.json({ error: "dm_auto_space is not enabled" }, 503);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const globalAdmins = [
|
|
16
|
+
...(config.admins
|
|
17
|
+
? config.admins
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((s) => s.trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
: []),
|
|
22
|
+
...(config.dmAutoSpaceAdminIds
|
|
23
|
+
? config.dmAutoSpaceAdminIds
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
: []),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const normalize = (s: string) =>
|
|
31
|
+
s
|
|
32
|
+
.replace(/^[^:]+:/, "")
|
|
33
|
+
.replace(/^[+]+/, "")
|
|
34
|
+
.replace(/@.*$/, "");
|
|
35
|
+
|
|
36
|
+
const callerNormalized = normalize(callerId);
|
|
37
|
+
|
|
38
|
+
const isAdmin = globalAdmins.some(
|
|
39
|
+
(id) => id === callerId || normalize(id) === callerNormalized,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!isAdmin) {
|
|
43
|
+
logger.warn("Broadcast denied — caller is not a global admin", {
|
|
44
|
+
callerId,
|
|
45
|
+
});
|
|
46
|
+
return c.json({ error: "Forbidden: requires global admin" }, 403);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!runtime) {
|
|
50
|
+
return c.json({ error: "Runtime not available" }, 503);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const body = (await c.req.json().catch(() => ({}))) as { text?: string };
|
|
54
|
+
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
55
|
+
|
|
56
|
+
if (!text) {
|
|
57
|
+
return c.json({ error: "Missing or empty 'text' field" }, 400);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (text.length > 4096) {
|
|
61
|
+
return c.json({ error: "Text exceeds 4096 character limit" }, 400);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await runtime.broadcastToAutoSpaces(text);
|
|
66
|
+
return c.json(result);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
69
|
+
if (message === "Broadcast already in progress") {
|
|
70
|
+
return c.json({ error: message }, 409);
|
|
71
|
+
}
|
|
72
|
+
if (message === "MessageSender not initialized") {
|
|
73
|
+
return c.json({ error: "Message sender not ready" }, 503);
|
|
74
|
+
}
|
|
75
|
+
logger.error("Broadcast failed", { error: message });
|
|
76
|
+
return c.json({ error: "Broadcast failed" }, 500);
|
|
77
|
+
}
|
|
78
|
+
});
|
package/src/core/routes/index.ts
CHANGED
package/src/core/runtime.ts
CHANGED
|
@@ -72,6 +72,7 @@ export class MercuryCoreRuntime {
|
|
|
72
72
|
private readonly shutdownHooks: ShutdownHook[] = [];
|
|
73
73
|
private readonly pauseTimers = new Map<string, NodeJS.Timeout>();
|
|
74
74
|
private messageSender: MessageSender | undefined;
|
|
75
|
+
private broadcastInProgress = false;
|
|
75
76
|
private shuttingDown = false;
|
|
76
77
|
private signalHandlersInstalled = false;
|
|
77
78
|
|
|
@@ -293,6 +294,63 @@ export class MercuryCoreRuntime {
|
|
|
293
294
|
});
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
getMessageSender(): MessageSender | undefined {
|
|
298
|
+
return this.messageSender;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async broadcastToAutoSpaces(text: string): Promise<{
|
|
302
|
+
total: number;
|
|
303
|
+
delivered: number;
|
|
304
|
+
failed: number;
|
|
305
|
+
errors: Array<{ spaceId: string; error: string }>;
|
|
306
|
+
}> {
|
|
307
|
+
if (!this.messageSender) {
|
|
308
|
+
throw new Error("MessageSender not initialized");
|
|
309
|
+
}
|
|
310
|
+
if (this.broadcastInProgress) {
|
|
311
|
+
throw new Error("Broadcast already in progress");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.broadcastInProgress = true;
|
|
315
|
+
try {
|
|
316
|
+
const spaces = this.db.listSpaces().filter((s) => s.id.startsWith("dm-"));
|
|
317
|
+
|
|
318
|
+
const errors: Array<{ spaceId: string; error: string }> = [];
|
|
319
|
+
let delivered = 0;
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
322
|
+
const space = spaces[i];
|
|
323
|
+
try {
|
|
324
|
+
await this.messageSender.send(space.id, text);
|
|
325
|
+
delivered++;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
errors.push({
|
|
328
|
+
spaceId: space.id,
|
|
329
|
+
error: err instanceof Error ? err.message : String(err),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (i < spaces.length - 1) {
|
|
333
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
logger.info("Broadcast complete", {
|
|
338
|
+
total: spaces.length,
|
|
339
|
+
delivered,
|
|
340
|
+
failed: errors.length,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
total: spaces.length,
|
|
345
|
+
delivered,
|
|
346
|
+
failed: errors.length,
|
|
347
|
+
errors,
|
|
348
|
+
};
|
|
349
|
+
} finally {
|
|
350
|
+
this.broadcastInProgress = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
296
354
|
private deliverTaskOutput(spaceId: string, text: string): void {
|
|
297
355
|
const consoleUrl = process.env.MERCURY_CONSOLE_URL;
|
|
298
356
|
const secret = process.env.MERCURY_CONSOLE_INTERNAL_SECRET;
|