mercury-agent 0.4.5 → 0.4.7
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/agent-package.json +1 -1
- package/docs/container-lifecycle.md +2 -2
- package/examples/extensions/permission-guard/index.ts +1 -1
- package/package.json +4 -4
- package/resources/pi-extensions/subagent/agents.ts +1 -1
- package/resources/pi-extensions/subagent/index.ts +5 -5
- package/src/adapters/whatsapp.ts +5 -2
- package/src/agent/model-capabilities.ts +1 -1
- package/src/bridges/discord.ts +19 -12
- package/src/bridges/slack.ts +2 -0
- package/src/bridges/teams.ts +4 -2
- package/src/bridges/telegram.ts +9 -1
- package/src/cli/mercury.ts +41 -13
- package/src/core/permissions.ts +4 -0
- package/src/core/router.ts +1 -1
- package/src/core/routes/chat.ts +7 -4
- package/src/core/routes/messages.ts +1 -1
- package/src/core/routes/mutes.ts +7 -1
- package/src/core/routes/roles.ts +10 -0
- package/src/core/task-scheduler.ts +11 -4
- package/src/extensions/hooks.ts +5 -0
- package/src/extensions/loader.ts +1 -1
- package/src/extensions/permission-guard.ts +1 -1
- package/src/storage/db.ts +1 -0
- package/src/storage/pi-auth.ts +1 -1
|
@@ -228,7 +228,7 @@ You can use custom Docker images via `MERCURY_AGENT_IMAGE`.
|
|
|
228
228
|
|
|
229
229
|
Your image **must** have:
|
|
230
230
|
- `bun` runtime
|
|
231
|
-
- `pi` CLI (`@
|
|
231
|
+
- `pi` CLI (`@earendil-works/pi-coding-agent`)
|
|
232
232
|
- `bubblewrap` (for agent sandboxing)
|
|
233
233
|
- `mrctl` wrapper (copied during build)
|
|
234
234
|
Extension CLIs (e.g. `pinchtab`, `napkin`, `gws`) are installed in derived images at runtime based on `.mercury/extensions/*` declarations.
|
|
@@ -279,7 +279,7 @@ RUN curl -fsSL https://bun.sh/install | bash
|
|
|
279
279
|
ENV PATH="/home/mercury/.bun/bin:$PATH"
|
|
280
280
|
|
|
281
281
|
# Install required CLIs
|
|
282
|
-
RUN bun add -g @
|
|
282
|
+
RUN bun add -g @earendil-works/pi-coding-agent
|
|
283
283
|
|
|
284
284
|
# Optional: install Playwright/Chromium if your extensions need browser automation
|
|
285
285
|
RUN bunx playwright install chromium
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* directly and blocks them with a message to use `mrctl` instead.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { ExtensionAPI } from "@
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
|
|
22
22
|
export default function (pi: ExtensionAPI) {
|
|
23
23
|
const extClisEnv = process.env.MERCURY_EXT_CLIS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mercury-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Avishai Tsabari",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"@chat-adapter/slack": "^4.14.0",
|
|
65
65
|
"@chat-adapter/teams": "^4.17.0",
|
|
66
66
|
"@chat-adapter/telegram": "^4.26.0",
|
|
67
|
-
"@
|
|
68
|
-
"@
|
|
69
|
-
"@
|
|
67
|
+
"@earendil-works/pi-agent-core": "~0.79.6",
|
|
68
|
+
"@earendil-works/pi-ai": "~0.79.10",
|
|
69
|
+
"@earendil-works/pi-coding-agent": "~0.79.6",
|
|
70
70
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
71
71
|
"axios": "^1.15.1",
|
|
72
72
|
"chat": "^4.14.0",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { getAgentDir, parseFrontmatter } from "@
|
|
7
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
export type AgentScope = "user" | "project" | "both";
|
|
10
10
|
|
|
@@ -16,11 +16,11 @@ import { spawn } from "node:child_process";
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import type { AgentToolResult } from "@
|
|
20
|
-
import type { Message } from "@
|
|
21
|
-
import { StringEnum } from "@
|
|
22
|
-
import { type ExtensionAPI, getMarkdownTheme } from "@
|
|
23
|
-
import { Container, Markdown, Spacer, Text } from "@
|
|
19
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
21
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
22
|
+
import { type ExtensionAPI, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
24
24
|
import { Type } from "@sinclair/typebox";
|
|
25
25
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
26
26
|
|
package/src/adapters/whatsapp.ts
CHANGED
|
@@ -147,7 +147,7 @@ export class WhatsAppBaileysAdapter
|
|
|
147
147
|
private readonly outgoingQueue: Array<{ jid: string; text: string }> = [];
|
|
148
148
|
private flushing = false;
|
|
149
149
|
private connectedAtMs = 0;
|
|
150
|
-
private
|
|
150
|
+
private seenMessageIds = new Set<string>();
|
|
151
151
|
private reconnectAttempt = 0;
|
|
152
152
|
private readonly pushNames = new Map<string, string>();
|
|
153
153
|
private currentQr: string | null = null;
|
|
@@ -474,7 +474,10 @@ export class WhatsAppBaileysAdapter
|
|
|
474
474
|
if (messageId) {
|
|
475
475
|
if (this.seenMessageIds.has(messageId)) return;
|
|
476
476
|
this.seenMessageIds.add(messageId);
|
|
477
|
-
if (this.seenMessageIds.size > 5000)
|
|
477
|
+
if (this.seenMessageIds.size > 5000) {
|
|
478
|
+
const ids = [...this.seenMessageIds];
|
|
479
|
+
this.seenMessageIds = new Set(ids.slice(ids.length - 2500));
|
|
480
|
+
}
|
|
478
481
|
}
|
|
479
482
|
|
|
480
483
|
const tsMs = Number(msg.messageTimestamp ?? 0) * 1000;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { getModels, type KnownProvider } from "@
|
|
7
|
+
import { getModels, type KnownProvider } from "@earendil-works/pi-ai";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import type { ModelLeg } from "../config.js";
|
package/src/bridges/discord.ts
CHANGED
|
@@ -62,18 +62,25 @@ export class DiscordBridge implements PlatformBridge {
|
|
|
62
62
|
const inboxDir = path.join(workspace, "inbox");
|
|
63
63
|
for (const att of rawAttachments) {
|
|
64
64
|
if (!att.url) continue;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
try {
|
|
66
|
+
const type = mimeToMediaType(
|
|
67
|
+
att.mimeType || "application/octet-stream",
|
|
68
|
+
);
|
|
69
|
+
const result = await downloadMediaFromUrl(att.url, {
|
|
70
|
+
type,
|
|
71
|
+
mimeType: att.mimeType || "application/octet-stream",
|
|
72
|
+
filename: att.name,
|
|
73
|
+
expectedSizeBytes: att.size,
|
|
74
|
+
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
75
|
+
outputDir: inboxDir,
|
|
76
|
+
});
|
|
77
|
+
if (result) attachments.push(result);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.warn("Discord attachment download failed", {
|
|
80
|
+
filename: att.name,
|
|
81
|
+
error: err instanceof Error ? err.message : String(err),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
}
|
package/src/bridges/slack.ts
CHANGED
|
@@ -133,12 +133,14 @@ export class SlackBridge implements PlatformBridge {
|
|
|
133
133
|
): Promise<void> {
|
|
134
134
|
const parts = threadId.split(":");
|
|
135
135
|
const channelId = parts.length >= 2 ? parts[1] : threadId;
|
|
136
|
+
const threadTs = parts.length >= 3 ? parts[2] : undefined;
|
|
136
137
|
|
|
137
138
|
for (const file of files) {
|
|
138
139
|
try {
|
|
139
140
|
const buffer = fs.readFileSync(file.path);
|
|
140
141
|
const form = new FormData();
|
|
141
142
|
form.append("channel_id", channelId);
|
|
143
|
+
if (threadTs) form.append("thread_ts", threadTs);
|
|
142
144
|
form.append("filename", file.filename);
|
|
143
145
|
form.append(
|
|
144
146
|
"file",
|
package/src/bridges/teams.ts
CHANGED
|
@@ -73,9 +73,11 @@ export class TeamsBridge implements PlatformBridge {
|
|
|
73
73
|
|
|
74
74
|
const { externalId, isDM } = this.parseThread(threadId);
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// Reply-to-bot detection: in DMs every reply is to the bot;
|
|
77
|
+
// in channels we cannot reliably determine the target, so default to false
|
|
78
|
+
// to avoid responding to conversations not directed at us.
|
|
77
79
|
const raw = msg.raw as { replyToId?: string; id?: string } | undefined;
|
|
78
|
-
const isReplyToBot = Boolean(raw?.replyToId);
|
|
80
|
+
const isReplyToBot = isDM && Boolean(raw?.replyToId);
|
|
79
81
|
|
|
80
82
|
return {
|
|
81
83
|
platform: "teams",
|
package/src/bridges/telegram.ts
CHANGED
|
@@ -479,13 +479,21 @@ export class TelegramBridge implements PlatformBridge {
|
|
|
479
479
|
const { chatId } = this.parseThreadId(threadId);
|
|
480
480
|
const apiUrl = `${TELEGRAM_API_BASE}/bot${this.botToken}/editMessageText`;
|
|
481
481
|
try {
|
|
482
|
+
let formatted: string;
|
|
483
|
+
try {
|
|
484
|
+
formatted = markdownToTelegramHtml(text);
|
|
485
|
+
} catch {
|
|
486
|
+
formatted = escapeHtml(text);
|
|
487
|
+
}
|
|
488
|
+
const truncated = truncateTelegramHtml(formatted, TELEGRAM_MESSAGE_LIMIT);
|
|
482
489
|
const resp = await fetch(apiUrl, {
|
|
483
490
|
method: "POST",
|
|
484
491
|
headers: { "Content-Type": "application/json" },
|
|
485
492
|
body: JSON.stringify({
|
|
486
493
|
chat_id: chatId,
|
|
487
494
|
message_id: Number(messageId),
|
|
488
|
-
text: applyRtlDirection(
|
|
495
|
+
text: applyRtlDirection(truncated),
|
|
496
|
+
parse_mode: "HTML",
|
|
489
497
|
}),
|
|
490
498
|
});
|
|
491
499
|
if (!resp.ok) return false;
|
package/src/cli/mercury.ts
CHANGED
|
@@ -70,7 +70,14 @@ function loadEnvFile(envPath: string): Record<string, string> {
|
|
|
70
70
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
71
71
|
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
72
72
|
if (match) {
|
|
73
|
-
|
|
73
|
+
let value = match[2];
|
|
74
|
+
if (
|
|
75
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
76
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
77
|
+
) {
|
|
78
|
+
value = value.slice(1, -1);
|
|
79
|
+
}
|
|
80
|
+
vars[match[1]] = value;
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
return vars;
|
|
@@ -266,16 +273,14 @@ function statusAction(): void {
|
|
|
266
273
|
`Configuration: ${hasEnv ? "✓ .env exists" : "✗ .env missing (run 'mercury init')"}`,
|
|
267
274
|
);
|
|
268
275
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
},
|
|
275
|
-
);
|
|
276
|
+
const cfg = loadConfig();
|
|
277
|
+
const imageName = cfg.agentContainerImage;
|
|
278
|
+
const imageCheck = spawnSync("docker", ["image", "inspect", imageName], {
|
|
279
|
+
stdio: "pipe",
|
|
280
|
+
});
|
|
276
281
|
const hasImage = imageCheck.status === 0;
|
|
277
282
|
console.log(
|
|
278
|
-
`Container image: ${hasImage ?
|
|
283
|
+
`Container image: ${hasImage ? `✓ ${imageName}` : `✗ not available (run 'mercury build' or pull '${imageName}')`}`,
|
|
279
284
|
);
|
|
280
285
|
|
|
281
286
|
if (hasEnv) {
|
|
@@ -820,7 +825,7 @@ authCommand
|
|
|
820
825
|
)
|
|
821
826
|
.action(async (providerArg?: string) => {
|
|
822
827
|
const { getOAuthProviders, getOAuthProvider } = await import(
|
|
823
|
-
"@
|
|
828
|
+
"@earendil-works/pi-ai/oauth"
|
|
824
829
|
);
|
|
825
830
|
const readline = await import("node:readline");
|
|
826
831
|
const { exec } = await import("node:child_process");
|
|
@@ -913,6 +918,12 @@ authCommand
|
|
|
913
918
|
: "xdg-open";
|
|
914
919
|
exec(`${openCmd} "${info.url}"`);
|
|
915
920
|
},
|
|
921
|
+
onDeviceCode: (info: { userCode: string; verificationUri: string }) => {
|
|
922
|
+
console.log(
|
|
923
|
+
`\nOpen this URL in your browser:\n ${info.verificationUri}`,
|
|
924
|
+
);
|
|
925
|
+
console.log(`Enter code: ${info.userCode}\n`);
|
|
926
|
+
},
|
|
916
927
|
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
917
928
|
const answer = await new Promise<string>((resolve) => {
|
|
918
929
|
rl.question(
|
|
@@ -922,6 +933,23 @@ authCommand
|
|
|
922
933
|
});
|
|
923
934
|
return answer;
|
|
924
935
|
},
|
|
936
|
+
onSelect: async (prompt: {
|
|
937
|
+
message: string;
|
|
938
|
+
options: Array<{ id: string; label: string }>;
|
|
939
|
+
}) => {
|
|
940
|
+
console.log(`\n${prompt.message}`);
|
|
941
|
+
for (let i = 0; i < prompt.options.length; i++) {
|
|
942
|
+
console.log(` ${i + 1}. ${prompt.options[i].label}`);
|
|
943
|
+
}
|
|
944
|
+
const answer = await new Promise<string>((resolve) => {
|
|
945
|
+
rl.question("Choose (number): ", resolve);
|
|
946
|
+
});
|
|
947
|
+
const idx = Number.parseInt(answer, 10) - 1;
|
|
948
|
+
if (idx >= 0 && idx < prompt.options.length) {
|
|
949
|
+
return prompt.options[idx].id;
|
|
950
|
+
}
|
|
951
|
+
return undefined;
|
|
952
|
+
},
|
|
925
953
|
onProgress: (message: string) => {
|
|
926
954
|
console.log(message);
|
|
927
955
|
},
|
|
@@ -999,7 +1027,7 @@ authCommand
|
|
|
999
1027
|
.command("status")
|
|
1000
1028
|
.description("Show authentication status for all providers")
|
|
1001
1029
|
.action(async () => {
|
|
1002
|
-
const { getOAuthProviders } = await import("@
|
|
1030
|
+
const { getOAuthProviders } = await import("@earendil-works/pi-ai/oauth");
|
|
1003
1031
|
|
|
1004
1032
|
const dataDir = getProjectDataDir(CWD);
|
|
1005
1033
|
const authPath = join(CWD, dataDir, "global", "auth.json");
|
|
@@ -1724,7 +1752,7 @@ async function addAction(source: string): Promise<void> {
|
|
|
1724
1752
|
}
|
|
1725
1753
|
|
|
1726
1754
|
console.log("\nRestart mercury to activate:");
|
|
1727
|
-
console.log(" mercury service
|
|
1755
|
+
console.log(" mercury service uninstall && mercury service install");
|
|
1728
1756
|
} finally {
|
|
1729
1757
|
cleanup();
|
|
1730
1758
|
}
|
|
@@ -1739,7 +1767,7 @@ function removeAction(name: string): void {
|
|
|
1739
1767
|
|
|
1740
1768
|
console.log(`✓ Extension "${name}" removed`);
|
|
1741
1769
|
console.log("\nRestart mercury to apply:");
|
|
1742
|
-
console.log(" mercury service
|
|
1770
|
+
console.log(" mercury service uninstall && mercury service install");
|
|
1743
1771
|
}
|
|
1744
1772
|
|
|
1745
1773
|
function extensionsListAction(): void {
|
package/src/core/permissions.ts
CHANGED
|
@@ -30,6 +30,10 @@ const BUILT_IN_PERMISSIONS = new Set([
|
|
|
30
30
|
"media.purge",
|
|
31
31
|
/** Host Text-to-Speech (/api/tts); admin-only by default. */
|
|
32
32
|
"tts.synthesize",
|
|
33
|
+
/** Mute/unmute users and list mutes; admin-only by default. */
|
|
34
|
+
"mutes.list",
|
|
35
|
+
"mutes.mute",
|
|
36
|
+
"mutes.unmute",
|
|
33
37
|
]);
|
|
34
38
|
|
|
35
39
|
// ---------------------------------------------------------------------------
|
package/src/core/router.ts
CHANGED
|
@@ -106,7 +106,7 @@ export function routeInput(input: {
|
|
|
106
106
|
.split(/\s+/);
|
|
107
107
|
const category = rawCategory.toLowerCase();
|
|
108
108
|
const verb = rawVerb?.toLowerCase() || undefined;
|
|
109
|
-
const arg = argParts.join(" ").trim()
|
|
109
|
+
const arg = argParts.join(" ").trim() || undefined;
|
|
110
110
|
if (SLASH_COMMANDS.some((c) => c.name === category)) {
|
|
111
111
|
return gateSlashCommand(
|
|
112
112
|
input.db,
|
package/src/core/routes/chat.ts
CHANGED
|
@@ -62,9 +62,16 @@ export function createChatRoute(core: MercuryCoreRuntime): Hono {
|
|
|
62
62
|
const authorName =
|
|
63
63
|
typeof body.authorName === "string" ? body.authorName.trim() : undefined;
|
|
64
64
|
|
|
65
|
+
if (!core.db.getSpace(spaceId)) {
|
|
66
|
+
return c.json({ error: "Space not found" }, 404);
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
// Save incoming files to inbox/
|
|
66
70
|
const attachments: MessageAttachment[] = [];
|
|
67
71
|
if (Array.isArray(body.files)) {
|
|
72
|
+
if (body.files.length > 20) {
|
|
73
|
+
return c.json({ error: "Too many files (max 20)" }, 400);
|
|
74
|
+
}
|
|
68
75
|
if (await isOverQuota(core.config)) {
|
|
69
76
|
return c.json({ error: "Storage quota exceeded" }, 413);
|
|
70
77
|
}
|
|
@@ -100,10 +107,6 @@ export function createChatRoute(core: MercuryCoreRuntime): Hono {
|
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
if (!core.db.getSpace(spaceId)) {
|
|
104
|
-
return c.json({ error: "Space not found" }, 404);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
110
|
if (authenticated) {
|
|
108
111
|
core.db.seedAdmins(spaceId, [callerId]);
|
|
109
112
|
}
|
package/src/core/routes/mutes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { Env } from "../api-types.js";
|
|
3
|
-
import { getApiCtx, getAuth } from "../api-types.js";
|
|
3
|
+
import { checkPerm, getApiCtx, getAuth } from "../api-types.js";
|
|
4
4
|
import { parseMuteDuration } from "../mute-duration.js";
|
|
5
5
|
|
|
6
6
|
export const mutes = new Hono<Env>();
|
|
@@ -8,6 +8,8 @@ export const mutes = new Hono<Env>();
|
|
|
8
8
|
// ─── List mutes ─────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
mutes.get("/", (c) => {
|
|
11
|
+
const denied = checkPerm(c, "mutes.list");
|
|
12
|
+
if (denied) return denied;
|
|
11
13
|
const { spaceId } = getAuth(c);
|
|
12
14
|
const { db } = getApiCtx(c);
|
|
13
15
|
return c.json({ mutes: db.listMutes(spaceId) });
|
|
@@ -16,6 +18,8 @@ mutes.get("/", (c) => {
|
|
|
16
18
|
// ─── Mute a user ────────────────────────────────────────────────────────
|
|
17
19
|
|
|
18
20
|
mutes.post("/", async (c) => {
|
|
21
|
+
const denied = checkPerm(c, "mutes.mute");
|
|
22
|
+
if (denied) return denied;
|
|
19
23
|
const { spaceId, callerId } = getAuth(c);
|
|
20
24
|
const { db } = getApiCtx(c);
|
|
21
25
|
const body = await c.req.json<{
|
|
@@ -76,6 +80,8 @@ mutes.post("/", async (c) => {
|
|
|
76
80
|
// ─── Unmute a user ──────────────────────────────────────────────────────
|
|
77
81
|
|
|
78
82
|
mutes.delete("/:userId", (c) => {
|
|
83
|
+
const denied = checkPerm(c, "mutes.unmute");
|
|
84
|
+
if (denied) return denied;
|
|
79
85
|
const { spaceId } = getAuth(c);
|
|
80
86
|
const { db } = getApiCtx(c);
|
|
81
87
|
const targetUserId = decodeURIComponent(c.req.param("userId"));
|
package/src/core/routes/roles.ts
CHANGED
|
@@ -33,6 +33,16 @@ roles.post("/", async (c) => {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const targetRole = body.role ?? "admin";
|
|
36
|
+
const VALID_ROLES = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
37
|
+
if (!VALID_ROLES.test(targetRole)) {
|
|
38
|
+
return c.json(
|
|
39
|
+
{
|
|
40
|
+
error:
|
|
41
|
+
"Invalid role name. Use lowercase alphanumeric, hyphens, underscores (max 32 chars).",
|
|
42
|
+
},
|
|
43
|
+
400,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
36
46
|
db.setRole(spaceId, body.platformUserId, targetRole, callerId);
|
|
37
47
|
|
|
38
48
|
return c.json({
|
|
@@ -101,10 +101,17 @@ export class TaskScheduler {
|
|
|
101
101
|
let updated = 0;
|
|
102
102
|
for (const task of tasks) {
|
|
103
103
|
if (!task.active || !task.cron) continue;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
task.timezone ?? undefined
|
|
107
|
-
)
|
|
104
|
+
let correct: number;
|
|
105
|
+
try {
|
|
106
|
+
correct = this.computeNextRun(task.cron, task.timezone ?? undefined);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn("Skipping task with invalid cron expression", {
|
|
109
|
+
taskId: task.id,
|
|
110
|
+
cron: task.cron,
|
|
111
|
+
error: err instanceof Error ? err.message : String(err),
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
108
115
|
if (correct !== task.nextRunAt) {
|
|
109
116
|
this.db.updateTaskNextRun(task.id, correct);
|
|
110
117
|
updated++;
|
package/src/extensions/hooks.ts
CHANGED
|
@@ -86,6 +86,11 @@ export class HookDispatcher {
|
|
|
86
86
|
this.log.error(
|
|
87
87
|
`Hook "before_container" handler failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
88
88
|
);
|
|
89
|
+
return {
|
|
90
|
+
block: {
|
|
91
|
+
reason: `Extension hook failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
package/src/extensions/loader.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Set automatically by Mercury's runtime based on caller permissions.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { ExtensionAPI } from "@
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
|
|
16
16
|
export default function (pi: ExtensionAPI) {
|
|
17
17
|
const deniedEnv = process.env.MERCURY_DENIED_CLIS;
|
package/src/storage/db.ts
CHANGED