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/routes/chat.ts
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Hono } from "hono";
|
|
5
|
-
import { logger } from "../../logger.js";
|
|
6
|
-
import { ensureSpaceWorkspace } from "../../storage/memory.js";
|
|
7
|
-
import type { IngressMessage, MessageAttachment } from "../../types.js";
|
|
8
|
-
import { extToMime, mimeToMediaType } from "../media.js";
|
|
9
|
-
import type { MercuryCoreRuntime } from "../runtime.js";
|
|
10
|
-
import { isOverQuota } from "../storage-guard.js";
|
|
11
|
-
|
|
12
|
-
interface ChatFileInput {
|
|
13
|
-
name: string;
|
|
14
|
-
data: string; // base64
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ChatFileOutput {
|
|
18
|
-
filename: string;
|
|
19
|
-
mimeType: string;
|
|
20
|
-
sizeBytes: number;
|
|
21
|
-
data: string; // base64
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function createChatRoute(core: MercuryCoreRuntime): Hono {
|
|
25
|
-
const app = new Hono();
|
|
26
|
-
|
|
27
|
-
app.post("/", async (c) => {
|
|
28
|
-
// Validate chat API key when configured
|
|
29
|
-
const chatApiKey = core.config.chatApiKey;
|
|
30
|
-
let authenticated = false;
|
|
31
|
-
if (chatApiKey) {
|
|
32
|
-
const authHeader = c.req.header("authorization");
|
|
33
|
-
const token = authHeader?.startsWith("Bearer ")
|
|
34
|
-
? authHeader.slice(7)
|
|
35
|
-
: undefined;
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
!token ||
|
|
39
|
-
token.length !== chatApiKey.length ||
|
|
40
|
-
!timingSafeEqual(Buffer.from(token), Buffer.from(chatApiKey))
|
|
41
|
-
) {
|
|
42
|
-
return c.json({ error: "Unauthorized" }, 401);
|
|
43
|
-
}
|
|
44
|
-
authenticated = true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const body = await c.req.json().catch(() => null);
|
|
48
|
-
if (!body || typeof body.text !== "string" || !body.text.trim()) {
|
|
49
|
-
return c.json({ error: "Missing or empty 'text' field" }, 400);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const callerId =
|
|
53
|
-
typeof body.callerId === "string" && body.callerId.trim()
|
|
54
|
-
? body.callerId.trim()
|
|
55
|
-
: "api:anonymous";
|
|
56
|
-
|
|
57
|
-
const spaceId =
|
|
58
|
-
typeof body.spaceId === "string" && body.spaceId.trim()
|
|
59
|
-
? body.spaceId.trim()
|
|
60
|
-
: "main";
|
|
61
|
-
|
|
62
|
-
const authorName =
|
|
63
|
-
typeof body.authorName === "string" ? body.authorName.trim() : undefined;
|
|
64
|
-
|
|
65
|
-
if (!core.db.getSpace(spaceId)) {
|
|
66
|
-
return c.json({ error: "Space not found" }, 404);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Save incoming files to inbox/
|
|
70
|
-
const attachments: MessageAttachment[] = [];
|
|
71
|
-
if (Array.isArray(body.files)) {
|
|
72
|
-
if (body.files.length > 20) {
|
|
73
|
-
return c.json({ error: "Too many files (max 20)" }, 400);
|
|
74
|
-
}
|
|
75
|
-
if (await isOverQuota(core.config)) {
|
|
76
|
-
return c.json({ error: "Storage quota exceeded" }, 413);
|
|
77
|
-
}
|
|
78
|
-
const workspace = ensureSpaceWorkspace(core.config.spacesDir, spaceId);
|
|
79
|
-
const inboxDir = path.join(workspace, "inbox");
|
|
80
|
-
fs.mkdirSync(inboxDir, { recursive: true });
|
|
81
|
-
|
|
82
|
-
for (const file of body.files as ChatFileInput[]) {
|
|
83
|
-
if (!file.name || !file.data) continue;
|
|
84
|
-
try {
|
|
85
|
-
const buffer = Buffer.from(file.data, "base64");
|
|
86
|
-
const safeName = path
|
|
87
|
-
.basename(file.name)
|
|
88
|
-
.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
89
|
-
const filename = `${Date.now()}-${safeName || "file"}`;
|
|
90
|
-
const filePath = path.join(inboxDir, filename);
|
|
91
|
-
fs.writeFileSync(filePath, buffer);
|
|
92
|
-
|
|
93
|
-
const mimeType = extToMime(file.name);
|
|
94
|
-
attachments.push({
|
|
95
|
-
path: filePath,
|
|
96
|
-
type: mimeToMediaType(mimeType),
|
|
97
|
-
mimeType,
|
|
98
|
-
filename: file.name,
|
|
99
|
-
sizeBytes: buffer.length,
|
|
100
|
-
});
|
|
101
|
-
} catch (err) {
|
|
102
|
-
logger.warn("Failed to save chat file", {
|
|
103
|
-
name: file.name,
|
|
104
|
-
error: err instanceof Error ? err.message : String(err),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (authenticated) {
|
|
111
|
-
core.db.seedAdmins(spaceId, [callerId]);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const ingress: IngressMessage = {
|
|
115
|
-
platform: "api",
|
|
116
|
-
spaceId,
|
|
117
|
-
conversationExternalId: `api:${callerId}`,
|
|
118
|
-
callerId,
|
|
119
|
-
authorName,
|
|
120
|
-
text: body.text.trim(),
|
|
121
|
-
isDM: true,
|
|
122
|
-
isReplyToBot: false,
|
|
123
|
-
attachments,
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
logger.info("API chat inbound", {
|
|
127
|
-
callerId,
|
|
128
|
-
spaceId,
|
|
129
|
-
preview: ingress.text.slice(0, 80),
|
|
130
|
-
fileCount: attachments.length,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const result = await core.handleRawInput(ingress, "cli");
|
|
134
|
-
|
|
135
|
-
if (result.type === "ignore") {
|
|
136
|
-
return c.json({ reply: "", files: [] });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (result.type === "denied") {
|
|
140
|
-
return c.json({ error: result.reason }, 403);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const reply = result.result?.reply ?? "";
|
|
144
|
-
const egressFiles = result.result?.files ?? [];
|
|
145
|
-
|
|
146
|
-
// Encode outbox files as base64
|
|
147
|
-
const outputFiles: ChatFileOutput[] = [];
|
|
148
|
-
for (const f of egressFiles) {
|
|
149
|
-
try {
|
|
150
|
-
const buffer = fs.readFileSync(f.path);
|
|
151
|
-
outputFiles.push({
|
|
152
|
-
filename: f.filename,
|
|
153
|
-
mimeType: f.mimeType,
|
|
154
|
-
sizeBytes: f.sizeBytes,
|
|
155
|
-
data: buffer.toString("base64"),
|
|
156
|
-
});
|
|
157
|
-
} catch (err) {
|
|
158
|
-
logger.warn("Failed to read outbox file for chat response", {
|
|
159
|
-
path: f.path,
|
|
160
|
-
error: err instanceof Error ? err.message : String(err),
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
logger.info("API chat outbound", {
|
|
166
|
-
spaceId,
|
|
167
|
-
preview: reply.slice(0, 80),
|
|
168
|
-
fileCount: outputFiles.length,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return c.json({ reply, files: outputFiles });
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
return app;
|
|
175
|
-
}
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { logger } from "../../logger.js";
|
|
6
|
+
import { ensureSpaceWorkspace } from "../../storage/memory.js";
|
|
7
|
+
import type { IngressMessage, MessageAttachment } from "../../types.js";
|
|
8
|
+
import { extToMime, mimeToMediaType } from "../media.js";
|
|
9
|
+
import type { MercuryCoreRuntime } from "../runtime.js";
|
|
10
|
+
import { isOverQuota } from "../storage-guard.js";
|
|
11
|
+
|
|
12
|
+
interface ChatFileInput {
|
|
13
|
+
name: string;
|
|
14
|
+
data: string; // base64
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ChatFileOutput {
|
|
18
|
+
filename: string;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
sizeBytes: number;
|
|
21
|
+
data: string; // base64
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createChatRoute(core: MercuryCoreRuntime): Hono {
|
|
25
|
+
const app = new Hono();
|
|
26
|
+
|
|
27
|
+
app.post("/", async (c) => {
|
|
28
|
+
// Validate chat API key when configured
|
|
29
|
+
const chatApiKey = core.config.chatApiKey;
|
|
30
|
+
let authenticated = false;
|
|
31
|
+
if (chatApiKey) {
|
|
32
|
+
const authHeader = c.req.header("authorization");
|
|
33
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
34
|
+
? authHeader.slice(7)
|
|
35
|
+
: undefined;
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!token ||
|
|
39
|
+
token.length !== chatApiKey.length ||
|
|
40
|
+
!timingSafeEqual(Buffer.from(token), Buffer.from(chatApiKey))
|
|
41
|
+
) {
|
|
42
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
43
|
+
}
|
|
44
|
+
authenticated = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const body = await c.req.json().catch(() => null);
|
|
48
|
+
if (!body || typeof body.text !== "string" || !body.text.trim()) {
|
|
49
|
+
return c.json({ error: "Missing or empty 'text' field" }, 400);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const callerId =
|
|
53
|
+
typeof body.callerId === "string" && body.callerId.trim()
|
|
54
|
+
? body.callerId.trim()
|
|
55
|
+
: "api:anonymous";
|
|
56
|
+
|
|
57
|
+
const spaceId =
|
|
58
|
+
typeof body.spaceId === "string" && body.spaceId.trim()
|
|
59
|
+
? body.spaceId.trim()
|
|
60
|
+
: "main";
|
|
61
|
+
|
|
62
|
+
const authorName =
|
|
63
|
+
typeof body.authorName === "string" ? body.authorName.trim() : undefined;
|
|
64
|
+
|
|
65
|
+
if (!core.db.getSpace(spaceId)) {
|
|
66
|
+
return c.json({ error: "Space not found" }, 404);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Save incoming files to inbox/
|
|
70
|
+
const attachments: MessageAttachment[] = [];
|
|
71
|
+
if (Array.isArray(body.files)) {
|
|
72
|
+
if (body.files.length > 20) {
|
|
73
|
+
return c.json({ error: "Too many files (max 20)" }, 400);
|
|
74
|
+
}
|
|
75
|
+
if (await isOverQuota(core.config)) {
|
|
76
|
+
return c.json({ error: "Storage quota exceeded" }, 413);
|
|
77
|
+
}
|
|
78
|
+
const workspace = ensureSpaceWorkspace(core.config.spacesDir, spaceId);
|
|
79
|
+
const inboxDir = path.join(workspace, "inbox");
|
|
80
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
for (const file of body.files as ChatFileInput[]) {
|
|
83
|
+
if (!file.name || !file.data) continue;
|
|
84
|
+
try {
|
|
85
|
+
const buffer = Buffer.from(file.data, "base64");
|
|
86
|
+
const safeName = path
|
|
87
|
+
.basename(file.name)
|
|
88
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
89
|
+
const filename = `${Date.now()}-${safeName || "file"}`;
|
|
90
|
+
const filePath = path.join(inboxDir, filename);
|
|
91
|
+
fs.writeFileSync(filePath, buffer);
|
|
92
|
+
|
|
93
|
+
const mimeType = extToMime(file.name);
|
|
94
|
+
attachments.push({
|
|
95
|
+
path: filePath,
|
|
96
|
+
type: mimeToMediaType(mimeType),
|
|
97
|
+
mimeType,
|
|
98
|
+
filename: file.name,
|
|
99
|
+
sizeBytes: buffer.length,
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.warn("Failed to save chat file", {
|
|
103
|
+
name: file.name,
|
|
104
|
+
error: err instanceof Error ? err.message : String(err),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (authenticated) {
|
|
111
|
+
core.db.seedAdmins(spaceId, [callerId]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ingress: IngressMessage = {
|
|
115
|
+
platform: "api",
|
|
116
|
+
spaceId,
|
|
117
|
+
conversationExternalId: `api:${callerId}`,
|
|
118
|
+
callerId,
|
|
119
|
+
authorName,
|
|
120
|
+
text: body.text.trim(),
|
|
121
|
+
isDM: true,
|
|
122
|
+
isReplyToBot: false,
|
|
123
|
+
attachments,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
logger.info("API chat inbound", {
|
|
127
|
+
callerId,
|
|
128
|
+
spaceId,
|
|
129
|
+
preview: ingress.text.slice(0, 80),
|
|
130
|
+
fileCount: attachments.length,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await core.handleRawInput(ingress, "cli");
|
|
134
|
+
|
|
135
|
+
if (result.type === "ignore") {
|
|
136
|
+
return c.json({ reply: "", files: [] });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.type === "denied") {
|
|
140
|
+
return c.json({ error: result.reason }, 403);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const reply = result.result?.reply ?? "";
|
|
144
|
+
const egressFiles = result.result?.files ?? [];
|
|
145
|
+
|
|
146
|
+
// Encode outbox files as base64
|
|
147
|
+
const outputFiles: ChatFileOutput[] = [];
|
|
148
|
+
for (const f of egressFiles) {
|
|
149
|
+
try {
|
|
150
|
+
const buffer = fs.readFileSync(f.path);
|
|
151
|
+
outputFiles.push({
|
|
152
|
+
filename: f.filename,
|
|
153
|
+
mimeType: f.mimeType,
|
|
154
|
+
sizeBytes: f.sizeBytes,
|
|
155
|
+
data: buffer.toString("base64"),
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logger.warn("Failed to read outbox file for chat response", {
|
|
159
|
+
path: f.path,
|
|
160
|
+
error: err instanceof Error ? err.message : String(err),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
logger.info("API chat outbound", {
|
|
166
|
+
spaceId,
|
|
167
|
+
preview: reply.slice(0, 80),
|
|
168
|
+
fileCount: outputFiles.length,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return c.json({ reply, files: outputFiles });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return app;
|
|
175
|
+
}
|