mercury-agent 0.4.7 → 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 -632
- 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 +2536 -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
package/src/core/router.ts
CHANGED
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
import type { AppConfig } from "../config.js";
|
|
2
|
-
import type { Db } from "../storage/db.js";
|
|
3
|
-
import type { MessageAttachment } from "../types.js";
|
|
4
|
-
import { SLASH_COMMANDS } from "./commands.js";
|
|
5
|
-
import { hasPermission, resolveRole } from "./permissions.js";
|
|
6
|
-
import { loadTriggerConfig, matchTrigger } from "./trigger.js";
|
|
7
|
-
|
|
8
|
-
export type RouteResult =
|
|
9
|
-
| {
|
|
10
|
-
type: "assistant";
|
|
11
|
-
prompt: string;
|
|
12
|
-
callerId: string;
|
|
13
|
-
role: string;
|
|
14
|
-
isReplyToBot: boolean;
|
|
15
|
-
isDM: boolean;
|
|
16
|
-
}
|
|
17
|
-
| {
|
|
18
|
-
type: "command";
|
|
19
|
-
command: string;
|
|
20
|
-
verb?: string;
|
|
21
|
-
arg?: string;
|
|
22
|
-
callerId: string;
|
|
23
|
-
role: string;
|
|
24
|
-
}
|
|
25
|
-
| { type: "denied"; reason: string }
|
|
26
|
-
| { type: "ignore" };
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Chat-level commands that bypass the LLM.
|
|
30
|
-
* Mapped to the permission required to execute them.
|
|
31
|
-
*/
|
|
32
|
-
const CHAT_COMMANDS: Record<string, string> = {
|
|
33
|
-
stop: "stop",
|
|
34
|
-
compact: "compact",
|
|
35
|
-
clear: "clear",
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export function routeInput(input: {
|
|
39
|
-
text: string;
|
|
40
|
-
spaceId: string;
|
|
41
|
-
callerId: string;
|
|
42
|
-
isDM: boolean;
|
|
43
|
-
isReplyToBot: boolean;
|
|
44
|
-
db: Db;
|
|
45
|
-
config: AppConfig;
|
|
46
|
-
/** Attachments after normalize (saved to inbox). */
|
|
47
|
-
attachments?: MessageAttachment[];
|
|
48
|
-
/**
|
|
49
|
-
* True when the inbound message had attachments or downloadable media before
|
|
50
|
-
* normalize, even if nothing was persisted (routing vs silent drop).
|
|
51
|
-
*/
|
|
52
|
-
hadIncomingAttachments?: boolean;
|
|
53
|
-
/** Display name of the message author (e.g. WhatsApp pushName). */
|
|
54
|
-
authorName?: string | null;
|
|
55
|
-
}): RouteResult {
|
|
56
|
-
const text = input.text.trim();
|
|
57
|
-
const persisted = (input.attachments?.length ?? 0) > 0;
|
|
58
|
-
const hadIncoming = input.hadIncomingAttachments ?? false;
|
|
59
|
-
const hasAttachments = persisted || hadIncoming;
|
|
60
|
-
if (!text && !hasAttachments) return { type: "ignore" };
|
|
61
|
-
|
|
62
|
-
const seededAdmins = input.config.admins
|
|
63
|
-
? input.config.admins
|
|
64
|
-
.split(",")
|
|
65
|
-
.map((s) => s.trim())
|
|
66
|
-
.filter(Boolean)
|
|
67
|
-
: [];
|
|
68
|
-
|
|
69
|
-
input.db.ensureSpace(input.spaceId);
|
|
70
|
-
|
|
71
|
-
// Resolve role (seeds admins + auto-upserts member)
|
|
72
|
-
const role = resolveRole(
|
|
73
|
-
input.db,
|
|
74
|
-
input.spaceId,
|
|
75
|
-
input.callerId,
|
|
76
|
-
seededAdmins,
|
|
77
|
-
input.authorName,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Load trigger config for this group
|
|
81
|
-
const defaultPatterns = input.config.triggerPatterns
|
|
82
|
-
.split(",")
|
|
83
|
-
.map((s) => s.trim())
|
|
84
|
-
.filter(Boolean);
|
|
85
|
-
const triggerConfig = loadTriggerConfig(input.db, input.spaceId, {
|
|
86
|
-
patterns: defaultPatterns,
|
|
87
|
-
match: input.config.triggerMatch,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Match trigger OR reply-to-bot
|
|
91
|
-
const result = matchTrigger(text, triggerConfig, input.isDM, hasAttachments);
|
|
92
|
-
const isReplyTrigger = input.isReplyToBot && !input.isDM;
|
|
93
|
-
if (!result.matched && !isReplyTrigger) return { type: "ignore" };
|
|
94
|
-
|
|
95
|
-
// Use stripped prompt if trigger matched, otherwise full text for replies
|
|
96
|
-
const prompt = result.matched ? result.prompt : text;
|
|
97
|
-
|
|
98
|
-
// Check for slash commands (e.g. "/model list", "/model switch 2")
|
|
99
|
-
// Only parse the first line — appended context (reply quotes etc.) must not
|
|
100
|
-
// bleed into verb/arg tokens.
|
|
101
|
-
if (prompt.startsWith("/")) {
|
|
102
|
-
const firstLine = prompt.split("\n")[0].trim();
|
|
103
|
-
const [rawCategory, rawVerb, ...argParts] = firstLine
|
|
104
|
-
.slice(1)
|
|
105
|
-
.trim()
|
|
106
|
-
.split(/\s+/);
|
|
107
|
-
const category = rawCategory.toLowerCase();
|
|
108
|
-
const verb = rawVerb?.toLowerCase() || undefined;
|
|
109
|
-
const arg = argParts.join(" ").trim() || undefined;
|
|
110
|
-
if (SLASH_COMMANDS.some((c) => c.name === category)) {
|
|
111
|
-
return gateSlashCommand(
|
|
112
|
-
input.db,
|
|
113
|
-
input.spaceId,
|
|
114
|
-
category,
|
|
115
|
-
role,
|
|
116
|
-
input.callerId,
|
|
117
|
-
input.isDM,
|
|
118
|
-
verb,
|
|
119
|
-
arg,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check for commands after trigger (e.g. "@Pi stop", "Pi compact")
|
|
125
|
-
const cmdWord = prompt.toLowerCase().trim();
|
|
126
|
-
if (cmdWord in CHAT_COMMANDS) {
|
|
127
|
-
return gateCommand(input.db, input.spaceId, cmdWord, role, input.callerId);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Check prompt permission
|
|
131
|
-
if (!hasPermission(input.db, input.spaceId, role, "prompt")) {
|
|
132
|
-
return {
|
|
133
|
-
type: "denied",
|
|
134
|
-
reason: "You don't have permission to use the agent in this group.",
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
type: "assistant",
|
|
140
|
-
prompt,
|
|
141
|
-
callerId: input.callerId,
|
|
142
|
-
role,
|
|
143
|
-
isReplyToBot: input.isReplyToBot,
|
|
144
|
-
isDM: input.isDM,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function gateSlashCommand(
|
|
149
|
-
db: Db,
|
|
150
|
-
spaceId: string,
|
|
151
|
-
command: string,
|
|
152
|
-
role: string,
|
|
153
|
-
callerId: string,
|
|
154
|
-
isDM: boolean,
|
|
155
|
-
verb?: string,
|
|
156
|
-
arg?: string,
|
|
157
|
-
): RouteResult {
|
|
158
|
-
if (!isDM && role !== "admin" && role !== "system") {
|
|
159
|
-
return {
|
|
160
|
-
type: "denied",
|
|
161
|
-
reason: "Slash commands are only available to admins in groups.",
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (!hasPermission(db, spaceId, role, "prompt")) {
|
|
165
|
-
return {
|
|
166
|
-
type: "denied",
|
|
167
|
-
reason: `You don't have permission to use '/${command}'.`,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return { type: "command", command, verb, arg, callerId, role };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function gateCommand(
|
|
174
|
-
db: Db,
|
|
175
|
-
spaceId: string,
|
|
176
|
-
command: string,
|
|
177
|
-
role: string,
|
|
178
|
-
callerId: string,
|
|
179
|
-
): RouteResult {
|
|
180
|
-
const permission = CHAT_COMMANDS[command];
|
|
181
|
-
if (!permission) return { type: "ignore" };
|
|
182
|
-
|
|
183
|
-
if (!hasPermission(db, spaceId, role, permission)) {
|
|
184
|
-
return {
|
|
185
|
-
type: "denied",
|
|
186
|
-
reason: `You don't have permission to use '${command}'.`,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return { type: "command", command, callerId, role };
|
|
191
|
-
}
|
|
1
|
+
import type { AppConfig } from "../config.js";
|
|
2
|
+
import type { Db } from "../storage/db.js";
|
|
3
|
+
import type { MessageAttachment } from "../types.js";
|
|
4
|
+
import { SLASH_COMMANDS } from "./commands.js";
|
|
5
|
+
import { hasPermission, resolveRole } from "./permissions.js";
|
|
6
|
+
import { loadTriggerConfig, matchTrigger } from "./trigger.js";
|
|
7
|
+
|
|
8
|
+
export type RouteResult =
|
|
9
|
+
| {
|
|
10
|
+
type: "assistant";
|
|
11
|
+
prompt: string;
|
|
12
|
+
callerId: string;
|
|
13
|
+
role: string;
|
|
14
|
+
isReplyToBot: boolean;
|
|
15
|
+
isDM: boolean;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
type: "command";
|
|
19
|
+
command: string;
|
|
20
|
+
verb?: string;
|
|
21
|
+
arg?: string;
|
|
22
|
+
callerId: string;
|
|
23
|
+
role: string;
|
|
24
|
+
}
|
|
25
|
+
| { type: "denied"; reason: string }
|
|
26
|
+
| { type: "ignore" };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Chat-level commands that bypass the LLM.
|
|
30
|
+
* Mapped to the permission required to execute them.
|
|
31
|
+
*/
|
|
32
|
+
const CHAT_COMMANDS: Record<string, string> = {
|
|
33
|
+
stop: "stop",
|
|
34
|
+
compact: "compact",
|
|
35
|
+
clear: "clear",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function routeInput(input: {
|
|
39
|
+
text: string;
|
|
40
|
+
spaceId: string;
|
|
41
|
+
callerId: string;
|
|
42
|
+
isDM: boolean;
|
|
43
|
+
isReplyToBot: boolean;
|
|
44
|
+
db: Db;
|
|
45
|
+
config: AppConfig;
|
|
46
|
+
/** Attachments after normalize (saved to inbox). */
|
|
47
|
+
attachments?: MessageAttachment[];
|
|
48
|
+
/**
|
|
49
|
+
* True when the inbound message had attachments or downloadable media before
|
|
50
|
+
* normalize, even if nothing was persisted (routing vs silent drop).
|
|
51
|
+
*/
|
|
52
|
+
hadIncomingAttachments?: boolean;
|
|
53
|
+
/** Display name of the message author (e.g. WhatsApp pushName). */
|
|
54
|
+
authorName?: string | null;
|
|
55
|
+
}): RouteResult {
|
|
56
|
+
const text = input.text.trim();
|
|
57
|
+
const persisted = (input.attachments?.length ?? 0) > 0;
|
|
58
|
+
const hadIncoming = input.hadIncomingAttachments ?? false;
|
|
59
|
+
const hasAttachments = persisted || hadIncoming;
|
|
60
|
+
if (!text && !hasAttachments) return { type: "ignore" };
|
|
61
|
+
|
|
62
|
+
const seededAdmins = input.config.admins
|
|
63
|
+
? input.config.admins
|
|
64
|
+
.split(",")
|
|
65
|
+
.map((s) => s.trim())
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
: [];
|
|
68
|
+
|
|
69
|
+
input.db.ensureSpace(input.spaceId);
|
|
70
|
+
|
|
71
|
+
// Resolve role (seeds admins + auto-upserts member)
|
|
72
|
+
const role = resolveRole(
|
|
73
|
+
input.db,
|
|
74
|
+
input.spaceId,
|
|
75
|
+
input.callerId,
|
|
76
|
+
seededAdmins,
|
|
77
|
+
input.authorName,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Load trigger config for this group
|
|
81
|
+
const defaultPatterns = input.config.triggerPatterns
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
const triggerConfig = loadTriggerConfig(input.db, input.spaceId, {
|
|
86
|
+
patterns: defaultPatterns,
|
|
87
|
+
match: input.config.triggerMatch,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Match trigger OR reply-to-bot
|
|
91
|
+
const result = matchTrigger(text, triggerConfig, input.isDM, hasAttachments);
|
|
92
|
+
const isReplyTrigger = input.isReplyToBot && !input.isDM;
|
|
93
|
+
if (!result.matched && !isReplyTrigger) return { type: "ignore" };
|
|
94
|
+
|
|
95
|
+
// Use stripped prompt if trigger matched, otherwise full text for replies
|
|
96
|
+
const prompt = result.matched ? result.prompt : text;
|
|
97
|
+
|
|
98
|
+
// Check for slash commands (e.g. "/model list", "/model switch 2")
|
|
99
|
+
// Only parse the first line — appended context (reply quotes etc.) must not
|
|
100
|
+
// bleed into verb/arg tokens.
|
|
101
|
+
if (prompt.startsWith("/")) {
|
|
102
|
+
const firstLine = prompt.split("\n")[0].trim();
|
|
103
|
+
const [rawCategory, rawVerb, ...argParts] = firstLine
|
|
104
|
+
.slice(1)
|
|
105
|
+
.trim()
|
|
106
|
+
.split(/\s+/);
|
|
107
|
+
const category = rawCategory.toLowerCase();
|
|
108
|
+
const verb = rawVerb?.toLowerCase() || undefined;
|
|
109
|
+
const arg = argParts.join(" ").trim() || undefined;
|
|
110
|
+
if (SLASH_COMMANDS.some((c) => c.name === category)) {
|
|
111
|
+
return gateSlashCommand(
|
|
112
|
+
input.db,
|
|
113
|
+
input.spaceId,
|
|
114
|
+
category,
|
|
115
|
+
role,
|
|
116
|
+
input.callerId,
|
|
117
|
+
input.isDM,
|
|
118
|
+
verb,
|
|
119
|
+
arg,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check for commands after trigger (e.g. "@Pi stop", "Pi compact")
|
|
125
|
+
const cmdWord = prompt.toLowerCase().trim();
|
|
126
|
+
if (cmdWord in CHAT_COMMANDS) {
|
|
127
|
+
return gateCommand(input.db, input.spaceId, cmdWord, role, input.callerId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check prompt permission
|
|
131
|
+
if (!hasPermission(input.db, input.spaceId, role, "prompt")) {
|
|
132
|
+
return {
|
|
133
|
+
type: "denied",
|
|
134
|
+
reason: "You don't have permission to use the agent in this group.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
type: "assistant",
|
|
140
|
+
prompt,
|
|
141
|
+
callerId: input.callerId,
|
|
142
|
+
role,
|
|
143
|
+
isReplyToBot: input.isReplyToBot,
|
|
144
|
+
isDM: input.isDM,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function gateSlashCommand(
|
|
149
|
+
db: Db,
|
|
150
|
+
spaceId: string,
|
|
151
|
+
command: string,
|
|
152
|
+
role: string,
|
|
153
|
+
callerId: string,
|
|
154
|
+
isDM: boolean,
|
|
155
|
+
verb?: string,
|
|
156
|
+
arg?: string,
|
|
157
|
+
): RouteResult {
|
|
158
|
+
if (!isDM && role !== "admin" && role !== "system") {
|
|
159
|
+
return {
|
|
160
|
+
type: "denied",
|
|
161
|
+
reason: "Slash commands are only available to admins in groups.",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (!hasPermission(db, spaceId, role, "prompt")) {
|
|
165
|
+
return {
|
|
166
|
+
type: "denied",
|
|
167
|
+
reason: `You don't have permission to use '/${command}'.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { type: "command", command, verb, arg, callerId, role };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function gateCommand(
|
|
174
|
+
db: Db,
|
|
175
|
+
spaceId: string,
|
|
176
|
+
command: string,
|
|
177
|
+
role: string,
|
|
178
|
+
callerId: string,
|
|
179
|
+
): RouteResult {
|
|
180
|
+
const permission = CHAT_COMMANDS[command];
|
|
181
|
+
if (!permission) return { type: "ignore" };
|
|
182
|
+
|
|
183
|
+
if (!hasPermission(db, spaceId, role, permission)) {
|
|
184
|
+
return {
|
|
185
|
+
type: "denied",
|
|
186
|
+
reason: `You don't have permission to use '${command}'.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { type: "command", command, callerId, role };
|
|
191
|
+
}
|