mercury-agent 0.4.7 → 0.4.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/container/Dockerfile.power +1 -1
- package/docs/auth/dashboard.md +28 -28
- package/docs/container-lifecycle.md +4 -4
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +1 -1
- package/package.json +1 -1
- package/resources/templates/mercury.example.yaml +1 -1
- package/src/adapters/whatsapp.ts +635 -632
- package/src/agent/container-runner.ts +3 -1
- package/src/agent/model-capabilities.ts +231 -231
- package/src/bridges/discord.ts +178 -178
- package/src/bridges/slack.ts +179 -179
- package/src/bridges/teams.ts +162 -162
- package/src/bridges/telegram.ts +579 -579
- package/src/cli/mercury.ts +2551 -2536
- package/src/cli/whatsapp-auth.ts +263 -260
- package/src/config.ts +316 -316
- package/src/core/permissions.ts +196 -196
- package/src/core/router.ts +191 -191
- package/src/core/routes/chat.ts +175 -175
- package/src/core/routes/dashboard.ts +2491 -2491
- package/src/core/routes/messages.ts +37 -37
- package/src/core/routes/mutes.ts +95 -95
- package/src/core/routes/roles.ts +135 -135
- package/src/core/runtime.ts +1140 -1140
- package/src/core/task-scheduler.ts +139 -139
- package/src/extensions/catalog.ts +117 -117
- package/src/extensions/hooks.ts +161 -161
- 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 -1625
- package/src/storage/pi-auth.ts +95 -95
- package/src/tts/azure.ts +52 -52
- package/src/tts/synthesize.ts +133 -133
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Extends Base with: python3 + pip + venv, build-essential, jq, ffmpeg, bubblewrap
|
|
4
4
|
# The patch script, mrctl, and entrypoint are all inherited from Base — not duplicated here.
|
|
5
5
|
ARG BASE_TAG
|
|
6
|
-
FROM ghcr.io/
|
|
6
|
+
FROM ghcr.io/avishai-tsabari/mercury-agent:${BASE_TAG}-base
|
|
7
7
|
|
|
8
8
|
USER root
|
|
9
9
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
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.
|
|
@@ -176,8 +176,8 @@ Message received
|
|
|
176
176
|
export MERCURY_CONTAINER_TIMEOUT_MS=600000
|
|
177
177
|
|
|
178
178
|
# Use a preset image from GitHub Container Registry
|
|
179
|
-
export MERCURY_AGENT_IMAGE=ghcr.io/
|
|
180
|
-
export MERCURY_AGENT_IMAGE=ghcr.io/
|
|
179
|
+
export MERCURY_AGENT_IMAGE=ghcr.io/avishai-tsabari/mercury-agent:latest # Full (default)
|
|
180
|
+
export MERCURY_AGENT_IMAGE=ghcr.io/avishai-tsabari/mercury-agent:minimal # Lightweight
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
## Sandboxing (Bubblewrap)
|
|
@@ -204,8 +204,8 @@ Mercury publishes two image presets to GitHub Container Registry:
|
|
|
204
204
|
|
|
205
205
|
| Preset | Size | Contents |
|
|
206
206
|
|--------|------|----------|
|
|
207
|
-
| `ghcr.io/
|
|
208
|
-
| `ghcr.io/
|
|
207
|
+
| `ghcr.io/avishai-tsabari/mercury-agent:latest` | ~2.8GB | Full devcontainer: Bun, Node.js, Python, Go, git, build tools |
|
|
208
|
+
| `ghcr.io/avishai-tsabari/mercury-agent:minimal` | ~1.9GB | Lightweight runtime: Bun + pi + Chromium deps |
|
|
209
209
|
|
|
210
210
|
Images are published on each release. Version-specific tags are also available (e.g., `:0.2.0`, `:0.2.0-minimal`).
|
|
211
211
|
|
|
@@ -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
|
+
}
|
|
@@ -117,7 +117,7 @@ def _run_transformers(audio_path: str, model_id: str) -> str:
|
|
|
117
117
|
model=model_id,
|
|
118
118
|
device=device,
|
|
119
119
|
)
|
|
120
|
-
result = pipe(audio_path)
|
|
120
|
+
result = pipe(audio_path, return_timestamps=True)
|
|
121
121
|
if isinstance(result, dict):
|
|
122
122
|
return (result.get("text") or "").strip()
|
|
123
123
|
return str(result).strip()
|
package/package.json
CHANGED