mercury-agent 0.5.12 → 0.5.13
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 +22 -3
- package/docs/authoring-profiles.md +25 -25
- package/docs/dashboard.md +81 -0
- package/docs/web-search.md +28 -35
- package/package.json +1 -1
- package/resources/templates/mercury.example.yaml +3 -3
- package/src/core/routes/capability.ts +1 -1
- package/src/core/routes/dashboard.ts +265 -1
- package/src/extensions/catalog.ts +0 -1
- package/src/extensions/types.ts +2 -2
package/README.md
CHANGED
|
@@ -154,8 +154,26 @@ Each space is a user-defined memory boundary with its own workspace and pi sessi
|
|
|
154
154
|
| **Permissions** | Role-based access control | [docs/permissions.md](docs/permissions.md) |
|
|
155
155
|
| **Media** | Images, documents, voice notes | [docs/media/overview.md](docs/media/overview.md) |
|
|
156
156
|
| **KB Distillation** | Extract lasting knowledge from chats | [docs/kb-distillation.md](docs/kb-distillation.md) |
|
|
157
|
+
| **Dashboard** | Web UI for managing spaces, tasks, permissions, extensions | [docs/dashboard.md](docs/dashboard.md) |
|
|
157
158
|
| **Extensions** | TypeScript plugins for CLIs, skills, jobs, hooks | [docs/extensions.md](docs/extensions.md) |
|
|
158
|
-
| **DM Auto-Space** | Auto-create isolated spaces per
|
|
159
|
+
| **DM Auto-Space** | Auto-create isolated spaces per user DM | [docs/rate-limiting.md](docs/rate-limiting.md) |
|
|
160
|
+
|
|
161
|
+
### Default capabilities vs. extensions
|
|
162
|
+
|
|
163
|
+
Out of the box, Mercury is a capable chat assistant with conversation context, scheduled tasks, role-based permissions, multi-space management, message recall, and sub-agent delegation. It does **not** include web search, web browsing, a knowledge vault, voice, or third-party service integrations — those are added via extensions.
|
|
164
|
+
|
|
165
|
+
Install extensions from the dashboard (**Features** page), the CLI, or during setup with a profile:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# From the CLI
|
|
169
|
+
mercury add web-browser # Web browsing & search (uses Brave Search, no API key needed)
|
|
170
|
+
mercury add napkin # Obsidian-style knowledge vault
|
|
171
|
+
|
|
172
|
+
# Or pick a profile that bundles what you need
|
|
173
|
+
mercury setup --profile research
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Run `mercury extensions list` or visit the dashboard's Features page to see what's available.
|
|
159
177
|
|
|
160
178
|
---
|
|
161
179
|
|
|
@@ -404,7 +422,7 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
|
|
|
404
422
|
|
|
405
423
|
### DM Auto-Space
|
|
406
424
|
|
|
407
|
-
Auto-create isolated spaces for each
|
|
425
|
+
Auto-create isolated spaces for each user who DMs your bot. Each user gets their own space with persistent context, configurable permissions, and rate limits.
|
|
408
426
|
|
|
409
427
|
```yaml
|
|
410
428
|
# mercury.yaml
|
|
@@ -413,7 +431,7 @@ dm_auto_space:
|
|
|
413
431
|
admin_ids: # platform IDs — auto-link to "main" space
|
|
414
432
|
- "24417056866472" # WhatsApp LID, Telegram user ID, etc.
|
|
415
433
|
default_system_prompt: "You are a helpful assistant."
|
|
416
|
-
default_member_permissions: "prompt,prefs.get" # restrict
|
|
434
|
+
default_member_permissions: "prompt,prefs.get" # restrict users to chat only
|
|
417
435
|
|
|
418
436
|
runtime:
|
|
419
437
|
rate_limit_daily_member: 20 # global daily cap for members (0 = unlimited)
|
|
@@ -449,6 +467,7 @@ mrctl config set trigger_patterns "@Bot,Bot"
|
|
|
449
467
|
- [Container lifecycle](docs/container-lifecycle.md)
|
|
450
468
|
- [Graceful shutdown](docs/graceful-shutdown.md)
|
|
451
469
|
- [Rate limiting](docs/rate-limiting.md)
|
|
470
|
+
- [Dashboard](docs/dashboard.md)
|
|
452
471
|
- [Extensions](docs/extensions.md)
|
|
453
472
|
|
|
454
473
|
---
|
|
@@ -13,8 +13,8 @@ only on this contract.
|
|
|
13
13
|
## 1. Profile manifest — `mercury-profile.yaml`
|
|
14
14
|
|
|
15
15
|
```yaml
|
|
16
|
-
name:
|
|
17
|
-
description:
|
|
16
|
+
name: room-booking # lowercase-kebab; required
|
|
17
|
+
description: Meeting room booking assistant
|
|
18
18
|
version: 0.1.0
|
|
19
19
|
|
|
20
20
|
agents_md: ./AGENTS.md # optional; copied to the global AGENTS.md
|
|
@@ -26,8 +26,8 @@ capabilities:
|
|
|
26
26
|
|
|
27
27
|
# Extensions this profile ships (copied into .mercury/extensions).
|
|
28
28
|
extensions:
|
|
29
|
-
- name:
|
|
30
|
-
source: ./extensions/
|
|
29
|
+
- name: rooms
|
|
30
|
+
source: ./extensions/rooms
|
|
31
31
|
|
|
32
32
|
# EXHAUSTIVE member permission set while active. When present it REPLACES the
|
|
33
33
|
# default member permissions — nothing is merged in — so raw capabilities stay
|
|
@@ -35,18 +35,18 @@ extensions:
|
|
|
35
35
|
member_permissions:
|
|
36
36
|
- prompt
|
|
37
37
|
- prefs.get
|
|
38
|
-
-
|
|
38
|
+
- rooms # the profile's own capability, NOT gws
|
|
39
39
|
|
|
40
40
|
# Optional env the profile needs (host-side credentials for capabilities).
|
|
41
41
|
env:
|
|
42
|
-
- key:
|
|
43
|
-
description: "
|
|
42
|
+
- key: MERCURY_ROOMS_OFFICE_HOURS
|
|
43
|
+
description: "Office hours"
|
|
44
44
|
default: "09:00-18:00"
|
|
45
45
|
|
|
46
46
|
# Project-wide agent persona, injected into every container's system prompt.
|
|
47
47
|
system_prompt: |
|
|
48
|
-
You are a
|
|
49
|
-
ONLY their own
|
|
48
|
+
You are a meeting room booking assistant. Help each user book, view, and
|
|
49
|
+
cancel ONLY their own reservations. Never reveal other users' bookings.
|
|
50
50
|
|
|
51
51
|
defaults:
|
|
52
52
|
trigger_patterns: always
|
|
@@ -59,7 +59,7 @@ capabilities, copies extensions/AGENTS.md, and persists activation to
|
|
|
59
59
|
## 2. The capability broker (where the business logic runs)
|
|
60
60
|
|
|
61
61
|
A profile's privileged work runs **on the host**, so credentials never enter the
|
|
62
|
-
|
|
62
|
+
user-controlled agent container. The profile's extension registers a
|
|
63
63
|
handler; the agent invokes it through a thin CLI.
|
|
64
64
|
|
|
65
65
|
### Register a handler (in the extension's `index.ts`)
|
|
@@ -72,18 +72,18 @@ const setup: ExtensionSetupFn = (mercury) => {
|
|
|
72
72
|
mercury.permission({ defaultRoles: [] });
|
|
73
73
|
|
|
74
74
|
// Host-side credentials for the wrapped capability (never sent to the container).
|
|
75
|
-
mercury.env({ from: "
|
|
75
|
+
mercury.env({ from: "MERCURY_ROOMS_GOOGLE_CREDENTIALS" });
|
|
76
76
|
|
|
77
|
-
mercury.capability("
|
|
77
|
+
mercury.capability("rooms", async (req, ctx) => {
|
|
78
78
|
// req.callerId is the TOKEN-DERIVED, unspoofable caller — safe for ownership.
|
|
79
79
|
switch (req.action) {
|
|
80
80
|
case "availability":
|
|
81
|
-
return { data: await
|
|
81
|
+
return { data: await listOpenRooms(req.body, ctx) };
|
|
82
82
|
case "book":
|
|
83
|
-
return { data: await
|
|
83
|
+
return { data: await bookRoom(req.callerId, req.body, ctx) };
|
|
84
84
|
case "cancel":
|
|
85
85
|
return { data: await cancelOwn(req.callerId, req.body, ctx) };
|
|
86
|
-
case "my-
|
|
86
|
+
case "my-bookings":
|
|
87
87
|
return { data: await listOwn(req.callerId, ctx) };
|
|
88
88
|
default:
|
|
89
89
|
return { status: 400, data: { error: `unknown action: ${req.action}` } };
|
|
@@ -125,21 +125,21 @@ interface CapabilityResult {
|
|
|
125
125
|
Inside the container the agent runs:
|
|
126
126
|
|
|
127
127
|
```
|
|
128
|
-
mrctl capability
|
|
128
|
+
mrctl capability rooms book '{"room":"A3","slot":"2026-07-02T15:00"}'
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
→ `POST /api/capability/
|
|
131
|
+
→ `POST /api/capability/rooms/book`. Document these commands for the agent in
|
|
132
132
|
the extension's `SKILL.md` or the profile's `AGENTS.md`.
|
|
133
133
|
|
|
134
134
|
## 3. Permission & security model (non-negotiable)
|
|
135
135
|
|
|
136
136
|
- **`member_permissions` is exhaustive.** List every permission a member may
|
|
137
|
-
hold, including the capability name (e.g. `
|
|
137
|
+
hold, including the capability name (e.g. `rooms`). Anything not listed —
|
|
138
138
|
including raw capabilities like `gws` — is unavailable to members.
|
|
139
139
|
- **Authorization = permission named after the capability.** The broker route
|
|
140
140
|
requires the caller to hold the `<name>` permission; the same grant gates both
|
|
141
|
-
the `mrctl capability <name> …` CLI and the route. Keep capability name
|
|
142
|
-
permission name
|
|
141
|
+
the `mrctl capability <name> …` CLI and the route. Keep capability name =
|
|
142
|
+
permission name = the entry in `member_permissions`.
|
|
143
143
|
- **Credentials stay on the host.** Put capability credentials in host env
|
|
144
144
|
(`mercury.env`) / `mercury.store` and use them only inside the handler. Never
|
|
145
145
|
expose a raw capability CLI (e.g. `gws`) to members — its secret would be
|
|
@@ -147,17 +147,17 @@ the extension's `SKILL.md` or the profile's `AGENTS.md`.
|
|
|
147
147
|
- **Enforce ownership with `req.callerId`.** It is token-derived and cannot be
|
|
148
148
|
spoofed by the container. Never trust an id passed in `req.body`.
|
|
149
149
|
- **`callerId === "system"`** for scheduled/system runs — handle it explicitly
|
|
150
|
-
(e.g. skip per-
|
|
150
|
+
(e.g. skip per-user ownership).
|
|
151
151
|
|
|
152
152
|
## 4. Suggested profile repo layout
|
|
153
153
|
|
|
154
154
|
```
|
|
155
155
|
<profile-repo>/
|
|
156
|
-
|
|
156
|
+
room-booking/ # one folder per profile
|
|
157
157
|
mercury-profile.yaml
|
|
158
158
|
AGENTS.md
|
|
159
159
|
extensions/
|
|
160
|
-
|
|
160
|
+
rooms/
|
|
161
161
|
index.ts # registers permission + capability
|
|
162
162
|
package.json
|
|
163
163
|
```
|
|
@@ -168,7 +168,7 @@ Depend on this repo (`mercury-agent`) for the types
|
|
|
168
168
|
|
|
169
169
|
## 5. Local test loop
|
|
170
170
|
|
|
171
|
-
1. `mercury setup --profile ./
|
|
171
|
+
1. `mercury setup --profile ./room-booking`
|
|
172
172
|
2. `mercury service install` (builds the derived image with the extension CLI)
|
|
173
173
|
3. DM the bot as a non-admin number; confirm you can `book`/`cancel` only your
|
|
174
|
-
own
|
|
174
|
+
own reservations and cannot reach `gws`/Gmail.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Dashboard
|
|
2
|
+
|
|
3
|
+
Mercury ships with a web dashboard at `http://localhost:8787/dashboard` (or whichever port `MERCURY_PORT` is set to). It gives you a real-time view of your instance and lets you manage spaces, conversations, tasks, permissions, extensions, and API keys — all without touching the CLI.
|
|
4
|
+
|
|
5
|
+
## Access & Authentication
|
|
6
|
+
|
|
7
|
+
When `MERCURY_API_SECRET` is set (auto-generated by `mercury setup`), the dashboard requires authentication. Open the login URL printed at startup or navigate to:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
http://localhost:8787/dashboard/login?token=<your-api-secret>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This sets a session cookie so subsequent visits go straight through. If no secret is configured, the dashboard is open.
|
|
14
|
+
|
|
15
|
+
## Pages
|
|
16
|
+
|
|
17
|
+
### Overview
|
|
18
|
+
|
|
19
|
+
System-at-a-glance: adapter connection status (WhatsApp, Slack, Discord, Telegram), active container runs, recent activity across spaces, upcoming scheduled tasks, space count, queue depth, uptime, and the configured model chain. Extension-provided widgets also appear here.
|
|
20
|
+
|
|
21
|
+
### Spaces
|
|
22
|
+
|
|
23
|
+
Lists all memory spaces with platform badges, conversation count, message count, and last-active time. Click a space to open its detail page.
|
|
24
|
+
|
|
25
|
+
**Space detail** is the most feature-rich page. From here you can:
|
|
26
|
+
|
|
27
|
+
| Section | What you can do |
|
|
28
|
+
|---------|-----------------|
|
|
29
|
+
| **Conversations** | View linked conversations, unlink them |
|
|
30
|
+
| **Roles** | Promote members to admin, demote admins |
|
|
31
|
+
| **Mutes** | Mute/unmute users with optional duration and reason |
|
|
32
|
+
| **Triggers & Ambient** | Set `trigger.match` (mention/prefix/always), patterns, case sensitivity, media-in-groups, and ambient mode |
|
|
33
|
+
| **Context** | Choose context mode (clear/context), window size, reply-chain depth |
|
|
34
|
+
| **Rate limits** | Set burst-per-minute and daily caps for members and admins |
|
|
35
|
+
| **Voice** | Configure per-space STT (transcription preset or custom model) and TTS (on-demand vs auto) |
|
|
36
|
+
| **Preferences** | Add/edit/delete arbitrary key-value preferences |
|
|
37
|
+
| **Recent messages** | View the last 50 messages with role coloring and token usage |
|
|
38
|
+
|
|
39
|
+
### Conversations
|
|
40
|
+
|
|
41
|
+
All discovered platform conversations across every adapter. Shows platform, title, kind (group/DM), linked space, and last seen. You can link unlinked conversations to a space or unlink existing ones — the same operation as `mercury link` from the CLI.
|
|
42
|
+
|
|
43
|
+
### Tasks
|
|
44
|
+
|
|
45
|
+
All scheduled tasks (cron and one-shot). For each task you can see the cron expression, timezone, prompt, next run time, and whether it runs silently or posts to chat. Actions: **run now**, **pause**, **resume**, **delete**. Filterable by space.
|
|
46
|
+
|
|
47
|
+
### Permissions
|
|
48
|
+
|
|
49
|
+
Cross-space view of every role assignment. Shows platform, space, user (with display name), and role. You can promote or demote users directly.
|
|
50
|
+
|
|
51
|
+
### Logs
|
|
52
|
+
|
|
53
|
+
Aggregated recent messages across all spaces, displayed as a log stream with timestamps, level, source platform, and message preview. Filterable by log level.
|
|
54
|
+
|
|
55
|
+
### Usage
|
|
56
|
+
|
|
57
|
+
Token usage statistics: aggregate totals (input tokens, output tokens, estimated cost, run count) and a per-space breakdown table.
|
|
58
|
+
|
|
59
|
+
### API Keys
|
|
60
|
+
|
|
61
|
+
Manage API keys for supported model providers (Anthropic, OpenAI, Google Gemini, Groq, Mistral, OpenRouter). Shows current status (SET / NOT SET). Saving or clearing a key writes to `.env` and triggers a restart.
|
|
62
|
+
|
|
63
|
+
### Features
|
|
64
|
+
|
|
65
|
+
Extension management through the UI. Shows installed extensions with their capabilities (CLI / skill / widget) and a remove button. Lists available catalog extensions with a one-click install button. Installing or removing an extension triggers a container image rebuild and restart.
|
|
66
|
+
|
|
67
|
+
## Real-time Updates
|
|
68
|
+
|
|
69
|
+
The dashboard uses Server-Sent Events (SSE) for live updates:
|
|
70
|
+
|
|
71
|
+
- **Health banner** — shows healthy / degraded / critical status with uptime
|
|
72
|
+
- **Active runs badge** — pulses when agent containers are running
|
|
73
|
+
- **Connection indicator** — shows live / reconnecting / disconnected state
|
|
74
|
+
|
|
75
|
+
## Keyboard Shortcuts
|
|
76
|
+
|
|
77
|
+
| Key | Action |
|
|
78
|
+
|-----|--------|
|
|
79
|
+
| `1`–`8` | Switch between tabs |
|
|
80
|
+
| `/` | Focus the search input |
|
|
81
|
+
| `?` | Show shortcut help |
|
package/docs/web-search.md
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
# Web Search
|
|
1
|
+
# Web Search & Browsing
|
|
2
2
|
|
|
3
|
-
Web search is **extension-based** in Mercury.
|
|
3
|
+
Web search is **extension-based** in Mercury. The base agent does not include a browser — install the `web-browser` extension to add search and browsing capabilities.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
```bash
|
|
8
|
+
mercury add web-browser
|
|
9
|
+
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Or install from the dashboard's **Features** page (one click).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
No API key is required. The extension uses [pinchtab](https://www.npmjs.com/package/pinchtab) to control a local headless Chromium and navigates to the public [Brave Search](https://search.brave.com) website — the same way a human would.
|
|
14
14
|
|
|
15
|
-
It
|
|
15
|
+
## What It Provides
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
- `before_container` hook for runtime env injection
|
|
19
|
-
- system-prompt guidance for consistent search behavior
|
|
17
|
+
The `web-browser` extension installs pinchtab and Playwright/Chromium into the agent container. The agent can:
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
- **Search the web** — navigate to Brave Search, read results
|
|
20
|
+
- **Browse any URL** — open pages, follow links
|
|
21
|
+
- **Extract text** — pull readable content from any page
|
|
22
|
+
- **Take snapshots** — capture the accessibility tree for structured interaction
|
|
23
|
+
- **Fill forms** — click elements, type into fields
|
|
24
|
+
- **Authenticated sessions** — log into sites using injected cookies/localStorage (configured via `MERCURY_BROWSER_SESSIONS`)
|
|
22
25
|
|
|
23
26
|
## Typical Flow
|
|
24
27
|
|
|
25
|
-
1. Start browser
|
|
26
|
-
2. Navigate to search
|
|
27
|
-
3. Extract text
|
|
28
|
-
4. Summarize and cite
|
|
29
|
-
|
|
30
|
-
Example command pattern used by agents (see `examples/extensions/pinchtab/` for `pinchtab_ensure` — wait until the bridge listens on `:9867` before calling the CLI; otherwise you get `connection refused`):
|
|
28
|
+
1. Start the browser daemon (`pinchtab_ensure`)
|
|
29
|
+
2. Navigate to a search URL or any page
|
|
30
|
+
3. Extract text or snapshot content
|
|
31
|
+
4. Summarize and cite findings
|
|
31
32
|
|
|
32
33
|
```bash
|
|
33
34
|
pinchtab_ensure || exit 1
|
|
@@ -36,27 +37,19 @@ sleep 3
|
|
|
36
37
|
pinchtab text
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
---
|
|
40
|
-
|
|
41
40
|
## Why Extension-Based
|
|
42
41
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
---
|
|
42
|
+
- Keeps Mercury core lean
|
|
43
|
+
- Lets each deployment pick its own browser/search stack
|
|
44
|
+
- Allows per-space RBAC for web tooling (`pinchtab` permission)
|
|
45
|
+
- Avoids locking users into one provider/tool
|
|
49
46
|
|
|
50
47
|
## Security & RBAC
|
|
51
48
|
|
|
52
|
-
Extension CLIs are called directly in bash, with RBAC enforced by Mercury's in-container permission guard.
|
|
53
|
-
|
|
54
|
-
If a caller lacks permission for a web extension CLI, execution is blocked.
|
|
55
|
-
|
|
56
|
-
---
|
|
49
|
+
Extension CLIs are called directly in bash, with RBAC enforced by Mercury's in-container permission guard. If a caller lacks the `pinchtab` permission, execution is blocked.
|
|
57
50
|
|
|
58
51
|
## Related Docs
|
|
59
52
|
|
|
60
|
-
- [
|
|
61
|
-
- [
|
|
62
|
-
- [
|
|
53
|
+
- [Extensions](./extensions.md)
|
|
54
|
+
- [Pipeline](./pipeline.md)
|
|
55
|
+
- [Container lifecycle](./container-lifecycle.md)
|
package/package.json
CHANGED
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
# enabled: false
|
|
81
81
|
# admin_ids: # auto-link to "main" space
|
|
82
82
|
# - "972501234567"
|
|
83
|
-
# default_system_prompt: "" # seeded into auto-created
|
|
84
|
-
# default_member_permissions: "prompt,prefs.get" # restrict
|
|
83
|
+
# default_system_prompt: "" # seeded into auto-created user spaces
|
|
84
|
+
# default_member_permissions: "prompt,prefs.get" # restrict users to chat only
|
|
85
85
|
|
|
86
86
|
# ─── Applicative profile ────────────────────────────────────────────────────
|
|
87
87
|
# A profile wraps raw capabilities with deterministic business logic and scopes
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
# credentials never enter the agent container.
|
|
93
93
|
#
|
|
94
94
|
# Env-only override of the active profile name (informational):
|
|
95
|
-
# MERCURY_PROFILE=
|
|
95
|
+
# MERCURY_PROFILE=room-booking
|
|
96
96
|
#
|
|
97
97
|
# Host-only signing key for per-turn caller tokens (auto-generated if unset;
|
|
98
98
|
# only set when the API and container-runner run in separate processes):
|
|
@@ -41,7 +41,7 @@ capability.post("/:name/:action", async (c) => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
// Authorization reuses the permission whose name equals the capability, so a
|
|
44
|
-
// single grant (e.g. "
|
|
44
|
+
// single grant (e.g. "rooms" in member_permissions) gates both the CLI and
|
|
45
45
|
// this route.
|
|
46
46
|
const denied = checkPerm(c, name);
|
|
47
47
|
if (denied) return denied;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { getModels, type KnownProvider } from "@earendil-works/pi-ai";
|
|
3
4
|
import { Hono } from "hono";
|
|
4
5
|
import { html, raw } from "hono/html";
|
|
5
6
|
import { streamSSE } from "hono/streaming";
|
|
@@ -115,42 +116,140 @@ export function createDashboardRoutes(ctx: DashboardContext) {
|
|
|
115
116
|
label: string;
|
|
116
117
|
envVar: string;
|
|
117
118
|
placeholder: string;
|
|
119
|
+
defaultModel: string;
|
|
118
120
|
}[] = [
|
|
119
121
|
{
|
|
120
122
|
id: "anthropic",
|
|
121
123
|
label: "Anthropic",
|
|
122
124
|
envVar: "MERCURY_ANTHROPIC_API_KEY",
|
|
123
125
|
placeholder: "sk-ant-...",
|
|
126
|
+
defaultModel: "claude-sonnet-4-6",
|
|
124
127
|
},
|
|
125
128
|
{
|
|
126
129
|
id: "openai",
|
|
127
130
|
label: "OpenAI",
|
|
128
131
|
envVar: "MERCURY_OPENAI_API_KEY",
|
|
129
132
|
placeholder: "sk-...",
|
|
133
|
+
defaultModel: "gpt-4o",
|
|
130
134
|
},
|
|
131
135
|
{
|
|
132
136
|
id: "google",
|
|
133
137
|
label: "Google Gemini",
|
|
134
138
|
envVar: "MERCURY_GEMINI_API_KEY",
|
|
135
139
|
placeholder: "AIza...",
|
|
140
|
+
defaultModel: "gemini-2.5-flash",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "deepseek",
|
|
144
|
+
label: "DeepSeek",
|
|
145
|
+
envVar: "MERCURY_DEEPSEEK_API_KEY",
|
|
146
|
+
placeholder: "sk-...",
|
|
147
|
+
defaultModel: "deepseek-v4-flash",
|
|
136
148
|
},
|
|
137
149
|
{
|
|
138
150
|
id: "groq",
|
|
139
151
|
label: "Groq",
|
|
140
152
|
envVar: "MERCURY_GROQ_API_KEY",
|
|
141
153
|
placeholder: "gsk_...",
|
|
154
|
+
defaultModel: "llama-3.3-70b-versatile",
|
|
142
155
|
},
|
|
143
156
|
{
|
|
144
157
|
id: "mistral",
|
|
145
158
|
label: "Mistral",
|
|
146
159
|
envVar: "MERCURY_MISTRAL_API_KEY",
|
|
147
160
|
placeholder: "...",
|
|
161
|
+
defaultModel: "mistral-large-latest",
|
|
148
162
|
},
|
|
149
163
|
{
|
|
150
164
|
id: "openrouter",
|
|
151
165
|
label: "OpenRouter",
|
|
152
166
|
envVar: "MERCURY_OPENROUTER_API_KEY",
|
|
153
167
|
placeholder: "sk-or-...",
|
|
168
|
+
defaultModel: "meta-llama/llama-3.3-70b-instruct",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "xai",
|
|
172
|
+
label: "xAI (Grok)",
|
|
173
|
+
envVar: "MERCURY_XAI_API_KEY",
|
|
174
|
+
placeholder: "xai-...",
|
|
175
|
+
defaultModel: "grok-2-latest",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "cerebras",
|
|
179
|
+
label: "Cerebras",
|
|
180
|
+
envVar: "MERCURY_CEREBRAS_API_KEY",
|
|
181
|
+
placeholder: "csk-...",
|
|
182
|
+
defaultModel: "llama3.1-8b",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "google-vertex",
|
|
186
|
+
label: "Google Vertex AI",
|
|
187
|
+
envVar: "MERCURY_GOOGLE_CLOUD_API_KEY",
|
|
188
|
+
placeholder: "AIza...",
|
|
189
|
+
defaultModel: "gemini-2.0-flash",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "amazon-bedrock",
|
|
193
|
+
label: "Amazon Bedrock",
|
|
194
|
+
envVar: "MERCURY_AWS_BEARER_TOKEN_BEDROCK",
|
|
195
|
+
placeholder: "...",
|
|
196
|
+
defaultModel: "amazon.nova-lite-v1:0",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "huggingface",
|
|
200
|
+
label: "HuggingFace",
|
|
201
|
+
envVar: "MERCURY_HF_TOKEN",
|
|
202
|
+
placeholder: "hf_...",
|
|
203
|
+
defaultModel: "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "azure-openai-responses",
|
|
207
|
+
label: "Azure OpenAI",
|
|
208
|
+
envVar: "MERCURY_AZURE_OPENAI_API_KEY",
|
|
209
|
+
placeholder: "...",
|
|
210
|
+
defaultModel: "gpt-4o",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "vercel-ai-gateway",
|
|
214
|
+
label: "Vercel AI Gateway",
|
|
215
|
+
envVar: "MERCURY_AI_GATEWAY_API_KEY",
|
|
216
|
+
placeholder: "...",
|
|
217
|
+
defaultModel: "alibaba/qwen-3-235b",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "minimax",
|
|
221
|
+
label: "MiniMax",
|
|
222
|
+
envVar: "MERCURY_MINIMAX_API_KEY",
|
|
223
|
+
placeholder: "...",
|
|
224
|
+
defaultModel: "MiniMax-M2.7",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "minimax-cn",
|
|
228
|
+
label: "MiniMax (China)",
|
|
229
|
+
envVar: "MERCURY_MINIMAX_CN_API_KEY",
|
|
230
|
+
placeholder: "...",
|
|
231
|
+
defaultModel: "MiniMax-M2.7",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "zai",
|
|
235
|
+
label: "ZAI",
|
|
236
|
+
envVar: "MERCURY_ZAI_API_KEY",
|
|
237
|
+
placeholder: "...",
|
|
238
|
+
defaultModel: "glm-4.5",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "kimi-coding",
|
|
242
|
+
label: "Kimi (Moonshot)",
|
|
243
|
+
envVar: "MERCURY_KIMI_API_KEY",
|
|
244
|
+
placeholder: "...",
|
|
245
|
+
defaultModel: "k2p5",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "github-copilot",
|
|
249
|
+
label: "GitHub Copilot",
|
|
250
|
+
envVar: "MERCURY_GITHUB_COPILOT_OAUTH_TOKEN",
|
|
251
|
+
placeholder: "",
|
|
252
|
+
defaultModel: "claude-sonnet-4-6",
|
|
154
253
|
},
|
|
155
254
|
];
|
|
156
255
|
|
|
@@ -1861,7 +1960,85 @@ export function createDashboardRoutes(ctx: DashboardContext) {
|
|
|
1861
1960
|
`);
|
|
1862
1961
|
}
|
|
1863
1962
|
|
|
1864
|
-
const
|
|
1963
|
+
const cfg = core.config;
|
|
1964
|
+
const isMultiLeg = cfg.resolvedModelChain.length > 1;
|
|
1965
|
+
const currentProvider = cfg.resolvedModelChain[0]?.provider ?? "";
|
|
1966
|
+
const currentModel = cfg.resolvedModelChain[0]?.model ?? "";
|
|
1967
|
+
const currentProviderMeta = MODEL_PROVIDERS.find(
|
|
1968
|
+
(p) => p.id === currentProvider,
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1971
|
+
const modelSelectorPanel = (() => {
|
|
1972
|
+
if (isMultiLeg) {
|
|
1973
|
+
return `
|
|
1974
|
+
<div class="panel" style="margin-bottom:16px">
|
|
1975
|
+
<div class="panel-header" style="font-weight:600;font-size:13px">Active model</div>
|
|
1976
|
+
<div class="panel-body">
|
|
1977
|
+
${renderModelBlock(cfg)}
|
|
1978
|
+
<p class="muted" style="font-size:12px;margin:8px 0 0">
|
|
1979
|
+
Model chain is configured via <span class="mono">mercury.yaml</span> or
|
|
1980
|
+
<span class="mono">MERCURY_MODEL_CHAIN</span> — edit those to change.
|
|
1981
|
+
</p>
|
|
1982
|
+
</div>
|
|
1983
|
+
</div>`;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const providerOptions = MODEL_PROVIDERS.map((p) => {
|
|
1987
|
+
const selected = p.id === currentProvider ? " selected" : "";
|
|
1988
|
+
const keySet = !!process.env[p.envVar];
|
|
1989
|
+
const suffix = keySet ? "" : " (no key)";
|
|
1990
|
+
return `<option value="${escapeHtml(p.id)}"${selected}>${escapeHtml(p.label)}${suffix}</option>`;
|
|
1991
|
+
}).join("");
|
|
1992
|
+
|
|
1993
|
+
const currentInRegistry = getModels(
|
|
1994
|
+
currentProvider as KnownProvider,
|
|
1995
|
+
).some((m) => m.id === currentModel);
|
|
1996
|
+
|
|
1997
|
+
const modelOptions = getModels(currentProvider as KnownProvider);
|
|
1998
|
+
let modelOptionsHtml: string;
|
|
1999
|
+
if (modelOptions.length === 0) {
|
|
2000
|
+
modelOptionsHtml = `<input type="text" name="model" value="${escapeHtml(currentModel)}" required
|
|
2001
|
+
style="flex:1;min-width:200px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:var(--radius,4px);font-size:13px;font-family:monospace"
|
|
2002
|
+
placeholder="Enter model ID" />`;
|
|
2003
|
+
} else {
|
|
2004
|
+
const opts = modelOptions
|
|
2005
|
+
.map(
|
|
2006
|
+
(m) =>
|
|
2007
|
+
`<option value="${escapeHtml(m.id)}"${m.id === currentModel ? " selected" : ""}>${escapeHtml(m.name)}</option>`,
|
|
2008
|
+
)
|
|
2009
|
+
.join("");
|
|
2010
|
+
const customOpt =
|
|
2011
|
+
!currentInRegistry && currentModel
|
|
2012
|
+
? `<option value="${escapeHtml(currentModel)}" selected>${escapeHtml(currentModel)} (not in registry)</option>`
|
|
2013
|
+
: "";
|
|
2014
|
+
modelOptionsHtml = `<select name="model" class="select" style="flex:1;min-width:200px">${customOpt}${opts}</select>`;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
return `
|
|
2018
|
+
<div class="panel" style="margin-bottom:16px">
|
|
2019
|
+
<div class="panel-header" style="font-weight:600;font-size:13px">Active model</div>
|
|
2020
|
+
<div class="panel-body">
|
|
2021
|
+
<p class="muted" style="font-size:12px;margin:0 0 10px">
|
|
2022
|
+
Current: <span class="mono">${escapeHtml(currentProvider)}</span> / <span class="mono">${escapeHtml(currentModel)}</span>
|
|
2023
|
+
</p>
|
|
2024
|
+
<form hx-post="/dashboard/api/model/set" hx-target="#keys-feedback" hx-swap="innerHTML"
|
|
2025
|
+
style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
2026
|
+
<select name="provider" class="select" style="min-width:160px"
|
|
2027
|
+
hx-get="/dashboard/api/models" hx-trigger="change"
|
|
2028
|
+
hx-target="#model-select-container" hx-swap="innerHTML">
|
|
2029
|
+
${providerOptions}
|
|
2030
|
+
</select>
|
|
2031
|
+
<span id="model-select-container" style="display:flex;flex:1;min-width:200px">
|
|
2032
|
+
${modelOptionsHtml}
|
|
2033
|
+
</span>
|
|
2034
|
+
<button class="btn btn-sm" type="submit">Save</button>
|
|
2035
|
+
</form>
|
|
2036
|
+
<p id="model-key-warning" class="muted" style="font-size:12px;margin:8px 0 0;${currentProviderMeta && process.env[currentProviderMeta.envVar] ? "display:none" : ""}">
|
|
2037
|
+
No API key set for this provider. Set one below or via <span class="mono">mercury auth login</span>.
|
|
2038
|
+
</p>
|
|
2039
|
+
</div>
|
|
2040
|
+
</div>`;
|
|
2041
|
+
})();
|
|
1865
2042
|
|
|
1866
2043
|
const providerRows = MODEL_PROVIDERS.map((p) => {
|
|
1867
2044
|
const isSet = !!process.env[p.envVar];
|
|
@@ -1900,6 +2077,8 @@ export function createDashboardRoutes(ctx: DashboardContext) {
|
|
|
1900
2077
|
|
|
1901
2078
|
<div id="keys-feedback" style="min-height:4px"></div>
|
|
1902
2079
|
|
|
2080
|
+
${raw(modelSelectorPanel)}
|
|
2081
|
+
|
|
1903
2082
|
<div class="panel" style="margin-bottom:16px">
|
|
1904
2083
|
<div class="panel-header" style="font-weight:600;font-size:13px">Model providers</div>
|
|
1905
2084
|
<div class="panel-body" style="padding:0 16px">
|
|
@@ -1995,6 +2174,91 @@ export function createDashboardRoutes(ctx: DashboardContext) {
|
|
|
1995
2174
|
);
|
|
1996
2175
|
});
|
|
1997
2176
|
|
|
2177
|
+
// ─── Model Selector ─────────────────────────────────────────────────────
|
|
2178
|
+
|
|
2179
|
+
app.get("/api/models", (c) => {
|
|
2180
|
+
const provider = c.req.query("provider") ?? "";
|
|
2181
|
+
const meta = MODEL_PROVIDERS.find((p) => p.id === provider);
|
|
2182
|
+
const hasKey = meta ? !!process.env[meta.envVar] : false;
|
|
2183
|
+
const keyWarning = `<p id="model-key-warning" hx-swap-oob="true" class="muted" style="font-size:12px;margin:8px 0 0;${hasKey ? "display:none" : ""}">
|
|
2184
|
+
No API key set for this provider. Set one below or via <span class="mono">mercury auth login</span>.
|
|
2185
|
+
</p>`;
|
|
2186
|
+
|
|
2187
|
+
const models = getModels(provider as KnownProvider);
|
|
2188
|
+
if (models.length === 0) {
|
|
2189
|
+
const placeholder = meta?.defaultModel ?? "model-id";
|
|
2190
|
+
return c.html(
|
|
2191
|
+
`<input type="text" name="model" required placeholder="${escapeHtml(placeholder)}"
|
|
2192
|
+
style="flex:1;min-width:200px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:var(--radius,4px);font-size:13px;font-family:monospace" />${keyWarning}`,
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
const options = models
|
|
2196
|
+
.map(
|
|
2197
|
+
(m) =>
|
|
2198
|
+
`<option value="${escapeHtml(m.id)}">${escapeHtml(m.name)}</option>`,
|
|
2199
|
+
)
|
|
2200
|
+
.join("");
|
|
2201
|
+
return c.html(
|
|
2202
|
+
`<select name="model" class="select" style="flex:1;min-width:200px">${options}</select>${keyWarning}`,
|
|
2203
|
+
);
|
|
2204
|
+
});
|
|
2205
|
+
|
|
2206
|
+
app.post("/api/model/set", async (c) => {
|
|
2207
|
+
if (core.config.consoleUrl) {
|
|
2208
|
+
return c.html(
|
|
2209
|
+
renderFeaturesToast(
|
|
2210
|
+
"error",
|
|
2211
|
+
"Model configuration is managed from the Mercury Console.",
|
|
2212
|
+
),
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if (core.config.resolvedModelChain.length > 1) {
|
|
2217
|
+
return c.html(
|
|
2218
|
+
renderFeaturesToast(
|
|
2219
|
+
"error",
|
|
2220
|
+
"Model chain is configured — edit mercury.yaml or MERCURY_MODEL_CHAIN directly.",
|
|
2221
|
+
),
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
const form = await c.req.parseBody();
|
|
2226
|
+
const provider =
|
|
2227
|
+
typeof form.provider === "string" ? form.provider.trim() : "";
|
|
2228
|
+
const model = typeof form.model === "string" ? form.model.trim() : "";
|
|
2229
|
+
|
|
2230
|
+
if (!provider) {
|
|
2231
|
+
return c.html(renderFeaturesToast("error", "Provider is required."));
|
|
2232
|
+
}
|
|
2233
|
+
if (!model) {
|
|
2234
|
+
return c.html(renderFeaturesToast("error", "Model is required."));
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
const meta = MODEL_PROVIDERS.find((p) => p.id === provider);
|
|
2238
|
+
const providerLabel = meta?.label ?? provider;
|
|
2239
|
+
|
|
2240
|
+
try {
|
|
2241
|
+
const envPath = path.join(projectRoot, ".env");
|
|
2242
|
+
updateDotEnv(envPath, {
|
|
2243
|
+
MERCURY_MODEL_PROVIDER: provider,
|
|
2244
|
+
MERCURY_MODEL: model,
|
|
2245
|
+
});
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2248
|
+
return c.html(
|
|
2249
|
+
renderFeaturesToast("error", `Failed to write .env: ${msg}`),
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
setTimeout(() => process.kill(process.pid, "SIGTERM"), 500);
|
|
2254
|
+
return c.html(
|
|
2255
|
+
renderFeaturesToast(
|
|
2256
|
+
"success",
|
|
2257
|
+
`Model set to ${providerLabel} / ${model} — restarting…`,
|
|
2258
|
+
),
|
|
2259
|
+
);
|
|
2260
|
+
});
|
|
2261
|
+
|
|
1998
2262
|
// ─── SSE Stream ─────────────────────────────────────────────────────────
|
|
1999
2263
|
|
|
2000
2264
|
app.get("/events", (c) => {
|
|
@@ -30,7 +30,6 @@ export const EXTENSION_CATALOG: ExtensionCatalogEntry[] = [
|
|
|
30
30
|
"Web search (Brave) and interactive browser (pinchtab) for sites like Gmail, banks, and forms.",
|
|
31
31
|
category: "automation",
|
|
32
32
|
sourceDir: "pinchtab",
|
|
33
|
-
requiredEnvVars: ["MERCURY_BRAVE_API_KEY"],
|
|
34
33
|
requiresRestart: true,
|
|
35
34
|
},
|
|
36
35
|
{
|
package/src/extensions/types.ts
CHANGED
|
@@ -356,8 +356,8 @@ export interface MercuryExtensionAPI {
|
|
|
356
356
|
* handler stay on the host and never enter the agent container.
|
|
357
357
|
*
|
|
358
358
|
* @example
|
|
359
|
-
* mercury.capability("
|
|
360
|
-
* if (req.action === "book") return { data: await
|
|
359
|
+
* mercury.capability("rooms", async (req, ctx) => {
|
|
360
|
+
* if (req.action === "book") return { data: await bookRoom(req.callerId, req.body) };
|
|
361
361
|
* return { status: 400, data: { error: "unknown action" } };
|
|
362
362
|
* });
|
|
363
363
|
*/
|