mercury-agent 0.4.6 → 0.4.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/docs/auth/dashboard.md +28 -28
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/package.json +1 -1
- package/src/adapters/whatsapp.ts +635 -629
- package/src/agent/model-capabilities.ts +231 -231
- package/src/bridges/discord.ts +178 -171
- package/src/bridges/slack.ts +179 -177
- package/src/bridges/teams.ts +162 -160
- package/src/bridges/telegram.ts +579 -571
- package/src/cli/mercury.ts +2536 -2531
- package/src/cli/whatsapp-auth.ts +263 -260
- package/src/config.ts +316 -316
- package/src/core/permissions.ts +196 -192
- package/src/core/router.ts +191 -191
- package/src/core/routes/chat.ts +175 -172
- package/src/core/routes/dashboard.ts +2491 -2491
- package/src/core/routes/messages.ts +37 -37
- package/src/core/routes/mutes.ts +95 -89
- package/src/core/routes/roles.ts +135 -125
- package/src/core/runtime.ts +1140 -1140
- package/src/core/task-scheduler.ts +139 -132
- package/src/extensions/catalog.ts +117 -117
- package/src/extensions/hooks.ts +161 -156
- package/src/extensions/installer.ts +306 -306
- package/src/extensions/loader.ts +271 -271
- package/src/extensions/permission-guard.ts +52 -52
- package/src/server.ts +391 -391
- package/src/storage/db.ts +1625 -1624
- package/src/storage/pi-auth.ts +95 -95
- package/src/tts/azure.ts +52 -52
- package/src/tts/synthesize.ts +133 -133
package/docs/auth/dashboard.md
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
# Dashboard Authentication
|
|
2
|
-
|
|
3
|
-
The Mercury dashboard (`/dashboard/*`) is protected by `MERCURY_API_SECRET`. All htmx partial requests and the SSE events stream require a valid `mercury_token` session cookie.
|
|
4
|
-
|
|
5
|
-
## How it works
|
|
6
|
-
|
|
7
|
-
### Login route
|
|
8
|
-
|
|
9
|
-
`GET /dashboard/login?token=<secret>` — the entry point for browser sessions.
|
|
10
|
-
|
|
11
|
-
- Validates `token` against `MERCURY_API_SECRET` using `timingSafeEqual`
|
|
12
|
-
- On success: sets `mercury_token` HttpOnly cookie and redirects to `/dashboard`
|
|
13
|
-
- On failure: returns `401 Invalid or missing token`
|
|
14
|
-
- Not protected by the dashboard auth middleware (it's the login endpoint itself)
|
|
15
|
-
|
|
16
|
-
## Auth flow
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
User navigates to https://<host>:<port>/dashboard/login?token=<MERCURY_API_SECRET>
|
|
20
|
-
→ agent sets mercury_token HttpOnly cookie
|
|
21
|
-
→ 302 /dashboard
|
|
22
|
-
→ dashboard loads, htmx requests include cookie ✅
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Key constraints
|
|
26
|
-
|
|
27
|
-
- `MERCURY_API_SECRET` should be set to a long random string. `mercury setup` auto-generates one.
|
|
28
|
-
- The cookie is HttpOnly — not accessible from JavaScript.
|
|
1
|
+
# Dashboard Authentication
|
|
2
|
+
|
|
3
|
+
The Mercury dashboard (`/dashboard/*`) is protected by `MERCURY_API_SECRET`. All htmx partial requests and the SSE events stream require a valid `mercury_token` session cookie.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
### Login route
|
|
8
|
+
|
|
9
|
+
`GET /dashboard/login?token=<secret>` — the entry point for browser sessions.
|
|
10
|
+
|
|
11
|
+
- Validates `token` against `MERCURY_API_SECRET` using `timingSafeEqual`
|
|
12
|
+
- On success: sets `mercury_token` HttpOnly cookie and redirects to `/dashboard`
|
|
13
|
+
- On failure: returns `401 Invalid or missing token`
|
|
14
|
+
- Not protected by the dashboard auth middleware (it's the login endpoint itself)
|
|
15
|
+
|
|
16
|
+
## Auth flow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
User navigates to https://<host>:<port>/dashboard/login?token=<MERCURY_API_SECRET>
|
|
20
|
+
→ agent sets mercury_token HttpOnly cookie
|
|
21
|
+
→ 302 /dashboard
|
|
22
|
+
→ dashboard loads, htmx requests include cookie ✅
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Key constraints
|
|
26
|
+
|
|
27
|
+
- `MERCURY_API_SECRET` should be set to a long random string. `mercury setup` auto-generates one.
|
|
28
|
+
- The cookie is HttpOnly — not accessible from JavaScript.
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type {
|
|
4
|
-
MercuryExtensionAPI,
|
|
5
|
-
MercuryExtensionContext,
|
|
6
|
-
} from "mercury-agent/extensions/types";
|
|
7
|
-
import type { MercuryTtsConfig } from "mercury-agent/tts";
|
|
8
|
-
import { synthesizeSpeech } from "mercury-agent/tts";
|
|
9
|
-
import type { EgressFile } from "mercury-agent/types";
|
|
10
|
-
|
|
11
|
-
const EXT = "voice-synth";
|
|
12
|
-
|
|
13
|
-
/** Prefer `voice-synth.mode`; fall back to legacy `voice-synth.auto`. */
|
|
14
|
-
function readVoiceSynthMode(
|
|
15
|
-
ctx: MercuryExtensionContext,
|
|
16
|
-
spaceId: string,
|
|
17
|
-
): "on_demand" | "auto" {
|
|
18
|
-
const mode = ctx.db.getSpaceConfig(spaceId, `${EXT}.mode`);
|
|
19
|
-
if (mode === "auto" || mode === "on_demand") return mode;
|
|
20
|
-
const legacy = ctx.db.getSpaceConfig(spaceId, `${EXT}.auto`);
|
|
21
|
-
return legacy === "true" ? "auto" : "on_demand";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function toTtsConfig(ctx: MercuryExtensionContext): MercuryTtsConfig {
|
|
25
|
-
const c = ctx.config;
|
|
26
|
-
return {
|
|
27
|
-
ttsProvider: c.ttsProvider,
|
|
28
|
-
azureSpeechKey: c.azureSpeechKey,
|
|
29
|
-
azureSpeechRegion: c.azureSpeechRegion,
|
|
30
|
-
googleApplicationCredentials: c.googleApplicationCredentials,
|
|
31
|
-
ttsMaxChars: c.ttsMaxChars,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default function setup(mercury: MercuryExtensionAPI) {
|
|
36
|
-
mercury.config("mode", {
|
|
37
|
-
description:
|
|
38
|
-
"on_demand: TTS only when the agent runs `mrctl tts synthesize` (default). auto: attach a TTS MP3 to every assistant reply.",
|
|
39
|
-
default: "on_demand",
|
|
40
|
-
validate: (v) => v === "on_demand" || v === "auto",
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
mercury.config("auto", {
|
|
44
|
-
description:
|
|
45
|
-
"Legacy: prefer voice-synth.mode. Used only when mode is unset; true equals mode=auto.",
|
|
46
|
-
default: "false",
|
|
47
|
-
validate: (v) => v === "true" || v === "false",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
mercury.skill("./skill");
|
|
51
|
-
|
|
52
|
-
mercury.on("after_container", async (event, ctx) => {
|
|
53
|
-
if (readVoiceSynthMode(ctx, event.spaceId) !== "auto") return undefined;
|
|
54
|
-
if (!event.reply?.trim()) return undefined;
|
|
55
|
-
if (event.error) return undefined;
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
!ctx.hasCallerPermission(event.spaceId, event.callerId, "tts.synthesize")
|
|
59
|
-
) {
|
|
60
|
-
ctx.log.warn("voice-synth.auto skipped: caller lacks tts.synthesize", {
|
|
61
|
-
extension: EXT,
|
|
62
|
-
spaceId: event.spaceId,
|
|
63
|
-
});
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const outDir = path.join(event.workspace, "outbox");
|
|
68
|
-
mkdirSync(outDir, { recursive: true });
|
|
69
|
-
const filename = `tts-${Date.now()}.mp3`;
|
|
70
|
-
const absPath = path.join(outDir, filename);
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const { buffer } = await synthesizeSpeech(toTtsConfig(ctx), {
|
|
74
|
-
text: event.reply,
|
|
75
|
-
language: "auto",
|
|
76
|
-
});
|
|
77
|
-
writeFileSync(absPath, buffer);
|
|
78
|
-
const st = statSync(absPath);
|
|
79
|
-
const file: EgressFile = {
|
|
80
|
-
path: absPath,
|
|
81
|
-
filename,
|
|
82
|
-
mimeType: "audio/mpeg",
|
|
83
|
-
sizeBytes: st.size,
|
|
84
|
-
};
|
|
85
|
-
return { files: [file] };
|
|
86
|
-
} catch (e) {
|
|
87
|
-
ctx.log.error("voice-synth TTS failed", {
|
|
88
|
-
extension: EXT,
|
|
89
|
-
error: e instanceof Error ? e.message : String(e),
|
|
90
|
-
});
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
1
|
+
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
MercuryExtensionAPI,
|
|
5
|
+
MercuryExtensionContext,
|
|
6
|
+
} from "mercury-agent/extensions/types";
|
|
7
|
+
import type { MercuryTtsConfig } from "mercury-agent/tts";
|
|
8
|
+
import { synthesizeSpeech } from "mercury-agent/tts";
|
|
9
|
+
import type { EgressFile } from "mercury-agent/types";
|
|
10
|
+
|
|
11
|
+
const EXT = "voice-synth";
|
|
12
|
+
|
|
13
|
+
/** Prefer `voice-synth.mode`; fall back to legacy `voice-synth.auto`. */
|
|
14
|
+
function readVoiceSynthMode(
|
|
15
|
+
ctx: MercuryExtensionContext,
|
|
16
|
+
spaceId: string,
|
|
17
|
+
): "on_demand" | "auto" {
|
|
18
|
+
const mode = ctx.db.getSpaceConfig(spaceId, `${EXT}.mode`);
|
|
19
|
+
if (mode === "auto" || mode === "on_demand") return mode;
|
|
20
|
+
const legacy = ctx.db.getSpaceConfig(spaceId, `${EXT}.auto`);
|
|
21
|
+
return legacy === "true" ? "auto" : "on_demand";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toTtsConfig(ctx: MercuryExtensionContext): MercuryTtsConfig {
|
|
25
|
+
const c = ctx.config;
|
|
26
|
+
return {
|
|
27
|
+
ttsProvider: c.ttsProvider,
|
|
28
|
+
azureSpeechKey: c.azureSpeechKey,
|
|
29
|
+
azureSpeechRegion: c.azureSpeechRegion,
|
|
30
|
+
googleApplicationCredentials: c.googleApplicationCredentials,
|
|
31
|
+
ttsMaxChars: c.ttsMaxChars,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function setup(mercury: MercuryExtensionAPI) {
|
|
36
|
+
mercury.config("mode", {
|
|
37
|
+
description:
|
|
38
|
+
"on_demand: TTS only when the agent runs `mrctl tts synthesize` (default). auto: attach a TTS MP3 to every assistant reply.",
|
|
39
|
+
default: "on_demand",
|
|
40
|
+
validate: (v) => v === "on_demand" || v === "auto",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
mercury.config("auto", {
|
|
44
|
+
description:
|
|
45
|
+
"Legacy: prefer voice-synth.mode. Used only when mode is unset; true equals mode=auto.",
|
|
46
|
+
default: "false",
|
|
47
|
+
validate: (v) => v === "true" || v === "false",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mercury.skill("./skill");
|
|
51
|
+
|
|
52
|
+
mercury.on("after_container", async (event, ctx) => {
|
|
53
|
+
if (readVoiceSynthMode(ctx, event.spaceId) !== "auto") return undefined;
|
|
54
|
+
if (!event.reply?.trim()) return undefined;
|
|
55
|
+
if (event.error) return undefined;
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
!ctx.hasCallerPermission(event.spaceId, event.callerId, "tts.synthesize")
|
|
59
|
+
) {
|
|
60
|
+
ctx.log.warn("voice-synth.auto skipped: caller lacks tts.synthesize", {
|
|
61
|
+
extension: EXT,
|
|
62
|
+
spaceId: event.spaceId,
|
|
63
|
+
});
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const outDir = path.join(event.workspace, "outbox");
|
|
68
|
+
mkdirSync(outDir, { recursive: true });
|
|
69
|
+
const filename = `tts-${Date.now()}.mp3`;
|
|
70
|
+
const absPath = path.join(outDir, filename);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const { buffer } = await synthesizeSpeech(toTtsConfig(ctx), {
|
|
74
|
+
text: event.reply,
|
|
75
|
+
language: "auto",
|
|
76
|
+
});
|
|
77
|
+
writeFileSync(absPath, buffer);
|
|
78
|
+
const st = statSync(absPath);
|
|
79
|
+
const file: EgressFile = {
|
|
80
|
+
path: absPath,
|
|
81
|
+
filename,
|
|
82
|
+
mimeType: "audio/mpeg",
|
|
83
|
+
sizeBytes: st.size,
|
|
84
|
+
};
|
|
85
|
+
return { files: [file] };
|
|
86
|
+
} catch (e) {
|
|
87
|
+
ctx.log.error("voice-synth TTS failed", {
|
|
88
|
+
extension: EXT,
|
|
89
|
+
error: e instanceof Error ? e.message : String(e),
|
|
90
|
+
});
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|