@holon-run/agentinbox 1.5.2 → 1.7.0
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 +16 -0
- package/dist/src/adapters.js +13 -0
- package/dist/src/cli.js +36 -0
- package/dist/src/service.js +17 -1
- package/dist/src/source_hosts.js +16 -0
- package/dist/src/source_schema.js +51 -0
- package/dist/src/sources/email.js +532 -0
- package/dist/src/sources/remote.js +1 -0
- package/dist/src/sources/remote_modules.js +76 -2
- package/dist/src/store.js +130 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -299,6 +299,22 @@ that database next to the DB path (for example,
|
|
|
299
299
|
`~/.agentinbox/agentinbox.sqlite.pre-v1.<timestamp>.bak`) and starts with a
|
|
300
300
|
fresh v1 database; pre-v1 local data is not imported.
|
|
301
301
|
|
|
302
|
+
## Database Backups
|
|
303
|
+
|
|
304
|
+
Startup backups are event-driven, not unconditional:
|
|
305
|
+
|
|
306
|
+
- before pending schema migrations run, a full backup is written to
|
|
307
|
+
`<db>.pre-migrate-v<N>.bak` (bounded by `AGENTINBOX_MIGRATION_BACKUP_KEEP`,
|
|
308
|
+
default 5; `0` keeps all)
|
|
309
|
+
- `agentinbox backup [--home DIR] [--state PATH]` writes a manual snapshot to
|
|
310
|
+
`<db>.bak` on demand, for scheduled or pre-upgrade snapshots
|
|
311
|
+
- regular opens verify the database with `PRAGMA quick_check` and escalate to
|
|
312
|
+
a full `integrity_check` only when the quick pass fails; recovery considers
|
|
313
|
+
manual and pre-migration backups, most recent first
|
|
314
|
+
|
|
315
|
+
Setting `AGENTINBOX_STARTUP_BACKUP=1` restores the legacy behavior of backing
|
|
316
|
+
up (and fully checking) the database on every open.
|
|
317
|
+
|
|
302
318
|
## License
|
|
303
319
|
|
|
304
320
|
Apache-2.0
|
package/dist/src/adapters.js
CHANGED
|
@@ -5,6 +5,7 @@ const paths_1 = require("./paths");
|
|
|
5
5
|
const feishu_1 = require("./sources/feishu");
|
|
6
6
|
const github_1 = require("./sources/github");
|
|
7
7
|
const telegram_1 = require("./sources/telegram");
|
|
8
|
+
const email_1 = require("./sources/email");
|
|
8
9
|
const remote_1 = require("./sources/remote");
|
|
9
10
|
const remote_modules_1 = require("./sources/remote_modules");
|
|
10
11
|
const source_resolution_1 = require("./source_resolution");
|
|
@@ -42,6 +43,7 @@ class AdapterRegistry {
|
|
|
42
43
|
feishuDelivery;
|
|
43
44
|
githubDelivery = new github_1.GithubDeliveryAdapter();
|
|
44
45
|
telegramDelivery;
|
|
46
|
+
emailDelivery;
|
|
45
47
|
homeDir;
|
|
46
48
|
remoteModuleRegistry;
|
|
47
49
|
constructor(store, appendSourceEvent, options) {
|
|
@@ -51,6 +53,7 @@ class AdapterRegistry {
|
|
|
51
53
|
});
|
|
52
54
|
this.feishuDelivery = new feishu_1.FeishuDeliveryAdapter(options?.feishuClient);
|
|
53
55
|
this.telegramDelivery = new telegram_1.TelegramDeliveryAdapter(options?.telegramClient);
|
|
56
|
+
this.emailDelivery = new email_1.EmailDeliveryAdapter(options?.emailClient);
|
|
54
57
|
this.remoteSource = new remote_1.RemoteSourceRuntime(store, appendSourceEvent, {
|
|
55
58
|
homeDir: this.homeDir,
|
|
56
59
|
client: options?.remoteSourceClient,
|
|
@@ -76,6 +79,9 @@ class AdapterRegistry {
|
|
|
76
79
|
if (type === "telegram_bot") {
|
|
77
80
|
return this.remoteSource;
|
|
78
81
|
}
|
|
82
|
+
if (type === "email_mailbox") {
|
|
83
|
+
return this.remoteSource;
|
|
84
|
+
}
|
|
79
85
|
return this.localEventSource;
|
|
80
86
|
}
|
|
81
87
|
deliveryAdapterFor(provider) {
|
|
@@ -88,6 +94,9 @@ class AdapterRegistry {
|
|
|
88
94
|
if (provider === "telegram") {
|
|
89
95
|
return this.telegramDelivery;
|
|
90
96
|
}
|
|
97
|
+
if (provider === "email") {
|
|
98
|
+
return this.emailDelivery;
|
|
99
|
+
}
|
|
91
100
|
return this.defaultDelivery;
|
|
92
101
|
}
|
|
93
102
|
async start() {
|
|
@@ -251,6 +260,9 @@ class AdapterRegistry {
|
|
|
251
260
|
if (handle.provider === "telegram") {
|
|
252
261
|
return this.remoteModuleRegistry.resolve(syntheticBuiltinSource("telegram_bot"), this.homeDir);
|
|
253
262
|
}
|
|
263
|
+
if (handle.provider === "email") {
|
|
264
|
+
return this.remoteModuleRegistry.resolve(syntheticBuiltinSource("email_mailbox"), this.homeDir);
|
|
265
|
+
}
|
|
254
266
|
return null;
|
|
255
267
|
}
|
|
256
268
|
}
|
|
@@ -262,6 +274,7 @@ function isExplicitFollowPreviewRef(value) {
|
|
|
262
274
|
value === "github_repo_ci" ||
|
|
263
275
|
value === "feishu_bot" ||
|
|
264
276
|
value === "telegram_bot" ||
|
|
277
|
+
value === "email_mailbox" ||
|
|
265
278
|
value.startsWith("remote:"));
|
|
266
279
|
}
|
|
267
280
|
function hasRemoteSourceModulePath(config) {
|
package/dist/src/cli.js
CHANGED
|
@@ -60,6 +60,14 @@ async function main() {
|
|
|
60
60
|
await runDaemon(normalized.slice(1));
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
+
if (command === "backup") {
|
|
64
|
+
if (hasHelpFlag(normalized.slice(1))) {
|
|
65
|
+
printHelp(["backup"]);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await runBackup(normalized.slice(1));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
63
71
|
if (hasHelpFlag(normalized.slice(1))) {
|
|
64
72
|
printHelp([command]);
|
|
65
73
|
return;
|
|
@@ -971,6 +979,7 @@ const COMMAND_SHELL_SPECS = [
|
|
|
971
979
|
{ name: "subscription", group: true, summary: "Manage source subscriptions." },
|
|
972
980
|
{ name: "inbox", group: true, summary: "Read and manage agent inboxes." },
|
|
973
981
|
{ name: "gc", group: false, summary: "Run garbage collection." },
|
|
982
|
+
{ name: "backup", group: false, summary: "Write a local database backup snapshot." },
|
|
974
983
|
{ name: "deliver", group: true, summary: "Send outbound delivery actions." },
|
|
975
984
|
{ name: "status", group: false, summary: "Show daemon status." },
|
|
976
985
|
{ name: "version", group: false, summary: "Show CLI version." },
|
|
@@ -1006,6 +1015,25 @@ function parseCommanderShell(args) {
|
|
|
1006
1015
|
isBareGroup: Boolean(COMMAND_SHELL_SPECS.find((item) => item.name === spec?.name())?.group && args.length === 1),
|
|
1007
1016
|
};
|
|
1008
1017
|
}
|
|
1018
|
+
async function runBackup(args) {
|
|
1019
|
+
const serveConfig = (0, paths_1.resolveServeConfig)({
|
|
1020
|
+
env: process.env,
|
|
1021
|
+
homeDirOverride: takeFlagValue(args, "--home"),
|
|
1022
|
+
statePathOverride: takeFlagValue(args, "--state"),
|
|
1023
|
+
});
|
|
1024
|
+
const store = await store_1.AgentInboxStore.open(serveConfig.dbPath);
|
|
1025
|
+
try {
|
|
1026
|
+
const backupPath = await store.backupDatabase();
|
|
1027
|
+
console.log((0, util_1.jsonResponse)({
|
|
1028
|
+
ok: true,
|
|
1029
|
+
dbPath: serveConfig.dbPath,
|
|
1030
|
+
backupPath,
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
finally {
|
|
1034
|
+
store.close();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1009
1037
|
async function runServe(args) {
|
|
1010
1038
|
const port = parseOptionalNumber(takeFlagValue(args, "--port"));
|
|
1011
1039
|
const homeOverride = takeFlagValue(args, "--home");
|
|
@@ -1677,6 +1705,14 @@ Usage:
|
|
|
1677
1705
|
agentinbox daemon start [--home ~/.agentinbox] [--socket ~/.agentinbox/agentinbox.sock] [--log-level error|warn|info|debug|trace]
|
|
1678
1706
|
agentinbox daemon stop [--home ~/.agentinbox] [--socket ~/.agentinbox/agentinbox.sock]
|
|
1679
1707
|
agentinbox daemon status [--home ~/.agentinbox] [--socket ~/.agentinbox/agentinbox.sock]
|
|
1708
|
+
`,
|
|
1709
|
+
backup: `agentinbox backup
|
|
1710
|
+
|
|
1711
|
+
Usage:
|
|
1712
|
+
agentinbox backup [--home ~/.agentinbox] [--state ~/.agentinbox/agentinbox.sqlite]
|
|
1713
|
+
|
|
1714
|
+
Writes a verified snapshot to <db>.bak (replaces the previous manual backup).
|
|
1715
|
+
Pre-migration backups (<db>.pre-migrate-v<N>.bak) are created automatically.
|
|
1680
1716
|
`,
|
|
1681
1717
|
host: `agentinbox host
|
|
1682
1718
|
|
package/dist/src/service.js
CHANGED
|
@@ -3313,7 +3313,8 @@ function sourceTypeForPreviewRef(sourceRef) {
|
|
|
3313
3313
|
sourceRef === "github_repo" ||
|
|
3314
3314
|
sourceRef === "github_repo_ci" ||
|
|
3315
3315
|
sourceRef === "feishu_bot" ||
|
|
3316
|
-
sourceRef === "telegram_bot"
|
|
3316
|
+
sourceRef === "telegram_bot" ||
|
|
3317
|
+
sourceRef === "email_mailbox") {
|
|
3317
3318
|
return sourceRef;
|
|
3318
3319
|
}
|
|
3319
3320
|
if (sourceRef.startsWith("remote:")) {
|
|
@@ -3615,6 +3616,9 @@ function providerForSourceType(sourceType) {
|
|
|
3615
3616
|
if (sourceType === "telegram_bot") {
|
|
3616
3617
|
return "telegram";
|
|
3617
3618
|
}
|
|
3619
|
+
if (sourceType === "email_mailbox") {
|
|
3620
|
+
return "email";
|
|
3621
|
+
}
|
|
3618
3622
|
return null;
|
|
3619
3623
|
}
|
|
3620
3624
|
function authHeaders(auth) {
|
|
@@ -4209,6 +4213,8 @@ function listHostStreamKinds(hostType) {
|
|
|
4209
4213
|
return ["message_events"];
|
|
4210
4214
|
case "telegram":
|
|
4211
4215
|
return ["message_updates"];
|
|
4216
|
+
case "email":
|
|
4217
|
+
return ["message_events"];
|
|
4212
4218
|
case "local_event":
|
|
4213
4219
|
return ["events"];
|
|
4214
4220
|
case "remote_source":
|
|
@@ -4218,6 +4224,7 @@ function listHostStreamKinds(hostType) {
|
|
|
4218
4224
|
const KNOWN_DELIVERY_SURFACES = {
|
|
4219
4225
|
feishu: ["message_reply", "chat_message"],
|
|
4220
4226
|
github: ["issue_comment", "pull_request_comment", "review_comment"],
|
|
4227
|
+
email: ["message_reply", "message_send"],
|
|
4221
4228
|
};
|
|
4222
4229
|
function unsupportedDeliverySurfaceMessage(handle) {
|
|
4223
4230
|
const supported = KNOWN_DELIVERY_SURFACES[handle.provider];
|
|
@@ -4298,6 +4305,12 @@ function getHostConfigFields(hostType) {
|
|
|
4298
4305
|
{ name: "tokenEnv", type: "string", description: "Environment variable containing the Telegram Bot API token.", required: false },
|
|
4299
4306
|
{ name: "botUsername", type: "string", description: "Optional bot username used for shared host identity.", required: false },
|
|
4300
4307
|
];
|
|
4308
|
+
case "email":
|
|
4309
|
+
return [
|
|
4310
|
+
{ name: "uxcAuth", type: "string", description: "UXC auth profile holding mailbox credentials.", required: false },
|
|
4311
|
+
{ name: "smtpEndpoint", type: "string", description: "Default smtp:// endpoint for outbound delivery.", required: false },
|
|
4312
|
+
{ name: "fromAddress", type: "string", description: "Default outbound From address.", required: false },
|
|
4313
|
+
];
|
|
4301
4314
|
case "local_event":
|
|
4302
4315
|
return [];
|
|
4303
4316
|
case "remote_source":
|
|
@@ -4317,6 +4330,9 @@ function sourceTypeForStreamRegistration(hostType, streamKind) {
|
|
|
4317
4330
|
if (hostType === "telegram") {
|
|
4318
4331
|
return "telegram_bot";
|
|
4319
4332
|
}
|
|
4333
|
+
if (hostType === "email") {
|
|
4334
|
+
return "email_mailbox";
|
|
4335
|
+
}
|
|
4320
4336
|
if (hostType === "local_event") {
|
|
4321
4337
|
return "local_event";
|
|
4322
4338
|
}
|
package/dist/src/source_hosts.js
CHANGED
|
@@ -60,6 +60,22 @@ function resolveSourceRegistration(input) {
|
|
|
60
60
|
sourceType: input.sourceType,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
if (input.sourceType === "email_mailbox") {
|
|
64
|
+
const provider = stringOrDefault(config.provider, "imap");
|
|
65
|
+
return {
|
|
66
|
+
hostType: "email",
|
|
67
|
+
hostKey: `email:${provider}:${stringOrDefault(config.account, stringOrDefault(config.uxcAuth, input.sourceKey))}`,
|
|
68
|
+
hostConfig: {
|
|
69
|
+
...(valueOrUndefined(config.uxcAuth) ? { uxcAuth: config.uxcAuth } : {}),
|
|
70
|
+
...(valueOrUndefined(config.smtpEndpoint) ? { smtpEndpoint: config.smtpEndpoint } : {}),
|
|
71
|
+
...(valueOrUndefined(config.fromAddress) ? { fromAddress: config.fromAddress } : {}),
|
|
72
|
+
},
|
|
73
|
+
streamKind: "message_events",
|
|
74
|
+
streamKey: input.sourceKey,
|
|
75
|
+
streamConfig: config,
|
|
76
|
+
sourceType: input.sourceType,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
63
79
|
if (input.sourceType === "local_event") {
|
|
64
80
|
return {
|
|
65
81
|
hostType: "local_event",
|
|
@@ -189,6 +189,57 @@ const SOURCE_SCHEMAS = {
|
|
|
189
189
|
{ name: "endpoint", type: "string", required: false, description: "Optional Telegram Bot API endpoint override." },
|
|
190
190
|
],
|
|
191
191
|
},
|
|
192
|
+
email_mailbox: {
|
|
193
|
+
sourceType: "email_mailbox",
|
|
194
|
+
metadataFields: [
|
|
195
|
+
{ name: "provider", type: "string", description: "Email provider: imap, gmail, graph, or jmap." },
|
|
196
|
+
{ name: "account", type: "string", description: "Mailbox account alias." },
|
|
197
|
+
{ name: "mailbox", type: "string", description: "Mailbox name such as INBOX." },
|
|
198
|
+
{ name: "messageId", type: "string|null", description: "RFC Message-ID header when present." },
|
|
199
|
+
{ name: "providerMessageId", type: "string|null", description: "Provider-native message id (IMAP UID or provider message id)." },
|
|
200
|
+
{ name: "threadId", type: "string|null", description: "Thread reference derived from References/In-Reply-To headers." },
|
|
201
|
+
{ name: "from", type: "string|null", description: "Normalized sender address." },
|
|
202
|
+
{ name: "fromName", type: "string|null", description: "Sender display name when present." },
|
|
203
|
+
{ name: "to", type: "object[]", description: "Normalized recipients with address and optional name." },
|
|
204
|
+
{ name: "cc", type: "object[]", description: "Normalized cc recipients with address and optional name." },
|
|
205
|
+
{ name: "subject", type: "string|null", description: "Message subject." },
|
|
206
|
+
{ name: "textPreview", type: "string|null", description: "Short body snippet." },
|
|
207
|
+
{ name: "date", type: "string|null", description: "Message date header value." },
|
|
208
|
+
{ name: "hasAttachments", type: "boolean", description: "Whether the message carries attachments." },
|
|
209
|
+
{ name: "attachmentCount", type: "number|null", description: "Attachment count; null when the provider did not expand attachment metadata." },
|
|
210
|
+
{ name: "attachments", type: "object[]", description: "Attachment metadata entries with opaque retrieval handles; binary content is never embedded." },
|
|
211
|
+
],
|
|
212
|
+
payloadExamples: [
|
|
213
|
+
{
|
|
214
|
+
type: "email_event",
|
|
215
|
+
version: "v1",
|
|
216
|
+
provider: "imap",
|
|
217
|
+
account: "user@example.com",
|
|
218
|
+
mailbox: "INBOX",
|
|
219
|
+
event_kind: "message_received",
|
|
220
|
+
message: {
|
|
221
|
+
uid: "42",
|
|
222
|
+
message_id: "<msg@example.com>",
|
|
223
|
+
from: "sender@example.com",
|
|
224
|
+
to: [{ raw: "user@example.com" }],
|
|
225
|
+
subject: "Quarterly report",
|
|
226
|
+
snippet: "Please review...",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
eventVariantExamples: ["email.message.received"],
|
|
231
|
+
configFields: [
|
|
232
|
+
{ name: "provider", type: "string", required: true, description: "Email provider: imap, gmail, graph, or jmap." },
|
|
233
|
+
{ name: "uxcAuth", type: "string", required: true, description: "UXC auth profile name holding mailbox credentials; credentials never inline." },
|
|
234
|
+
{ name: "endpoint", type: "string", required: false, description: "imap:// or imaps:// endpoint for imap; provider API endpoint for gmail/graph/jmap (jmap requires it)." },
|
|
235
|
+
{ name: "account", type: "string", required: false, description: "Account alias; defaults to the auth profile username." },
|
|
236
|
+
{ name: "mailbox", type: "string", required: false, description: "Mailbox to watch; defaults to INBOX." },
|
|
237
|
+
{ name: "pollIntervalSecs", type: "number", required: false, description: "Provider poll interval in seconds (min 15, default 60)." },
|
|
238
|
+
{ name: "smtpEndpoint", type: "string", required: false, description: "Default smtp:// endpoint for outbound reply/send delivery." },
|
|
239
|
+
{ name: "fromAddress", type: "string", required: false, description: "Default outbound From address for delivery." },
|
|
240
|
+
{ name: "addressAllowlist", type: "string[]", required: false, description: "Optional from/to address allowlist applied before inbox routing." },
|
|
241
|
+
],
|
|
242
|
+
},
|
|
192
243
|
};
|
|
193
244
|
function getSourceSchema(sourceType) {
|
|
194
245
|
const schema = SOURCE_SCHEMAS[sourceType];
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EmailDeliveryAdapter = exports.RpcEmailUxcClient = exports.GRAPH_MESSAGES_ENDPOINT = exports.GMAIL_MESSAGES_ENDPOINT = exports.EMAIL_EVENT_VERSION = exports.EMAIL_EVENT_TYPE = exports.EMAIL_MAILBOX_DEFAULT_POLL_INTERVAL_SECS = exports.EMAIL_DEFAULT_MAILBOX = void 0;
|
|
4
|
+
exports.parseEmailMailboxSourceConfig = parseEmailMailboxSourceConfig;
|
|
5
|
+
exports.buildEmailMailboxSourceSpec = buildEmailMailboxSourceSpec;
|
|
6
|
+
exports.normalizeEmailMailboxEvent = normalizeEmailMailboxEvent;
|
|
7
|
+
exports.emailDeliveryOperationsForHandle = emailDeliveryOperationsForHandle;
|
|
8
|
+
exports.invokeEmailDeliveryOperation = invokeEmailDeliveryOperation;
|
|
9
|
+
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
10
|
+
exports.EMAIL_DEFAULT_MAILBOX = "INBOX";
|
|
11
|
+
exports.EMAIL_MAILBOX_DEFAULT_POLL_INTERVAL_SECS = 60;
|
|
12
|
+
exports.EMAIL_EVENT_TYPE = "email_event";
|
|
13
|
+
exports.EMAIL_EVENT_VERSION = "v1";
|
|
14
|
+
exports.GMAIL_MESSAGES_ENDPOINT = "https://gmail.googleapis.com/gmail/v1/users/me/messages";
|
|
15
|
+
exports.GRAPH_MESSAGES_ENDPOINT = "https://graph.microsoft.com/v1.0/me/messages?$expand=attachments";
|
|
16
|
+
const EMAIL_MAILBOX_PROVIDERS = new Set(["imap", "gmail", "graph", "jmap"]);
|
|
17
|
+
const EMAIL_DEFAULT_ENDPOINTS = {
|
|
18
|
+
gmail: exports.GMAIL_MESSAGES_ENDPOINT,
|
|
19
|
+
graph: exports.GRAPH_MESSAGES_ENDPOINT,
|
|
20
|
+
};
|
|
21
|
+
function parseEmailMailboxSourceConfig(source) {
|
|
22
|
+
const config = source.config ?? {};
|
|
23
|
+
const providerRaw = asString(config.provider) ?? asString(config.emailProvider);
|
|
24
|
+
if (!providerRaw) {
|
|
25
|
+
throw new Error("email_mailbox requires config.provider (imap, gmail, graph, or jmap)");
|
|
26
|
+
}
|
|
27
|
+
const provider = providerRaw.trim().toLowerCase();
|
|
28
|
+
if (!EMAIL_MAILBOX_PROVIDERS.has(provider)) {
|
|
29
|
+
throw new Error(`email_mailbox config.provider must be one of imap, gmail, graph, jmap; got ${providerRaw}`);
|
|
30
|
+
}
|
|
31
|
+
const endpointRaw = asString(config.endpoint);
|
|
32
|
+
let endpoint;
|
|
33
|
+
if (provider === "imap") {
|
|
34
|
+
if (!endpointRaw) {
|
|
35
|
+
throw new Error("email_mailbox with provider=imap requires config.endpoint (imap:// or imaps://)");
|
|
36
|
+
}
|
|
37
|
+
if (!/^imaps?:\/\//i.test(endpointRaw)) {
|
|
38
|
+
throw new Error(`email_mailbox imap endpoint must start with imap:// or imaps://; got ${endpointRaw}`);
|
|
39
|
+
}
|
|
40
|
+
endpoint = endpointRaw;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const fallback = EMAIL_DEFAULT_ENDPOINTS[provider];
|
|
44
|
+
if (endpointRaw) {
|
|
45
|
+
if (!/^https?:\/\//i.test(endpointRaw)) {
|
|
46
|
+
throw new Error(`email_mailbox ${provider} endpoint must start with http:// or https://; got ${endpointRaw}`);
|
|
47
|
+
}
|
|
48
|
+
endpoint = endpointRaw;
|
|
49
|
+
}
|
|
50
|
+
else if (fallback) {
|
|
51
|
+
endpoint = fallback;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error(`email_mailbox with provider=${provider} requires config.endpoint (${provider} API URL)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const uxcAuth = asString(config.uxcAuth) ?? asString(config.auth);
|
|
58
|
+
if (!uxcAuth || uxcAuth.trim().length === 0) {
|
|
59
|
+
throw new Error("email_mailbox requires config.uxcAuth (UXC auth profile name; credentials stay in UXC)");
|
|
60
|
+
}
|
|
61
|
+
const pollIntervalRaw = config.pollIntervalSecs;
|
|
62
|
+
let pollIntervalSecs = exports.EMAIL_MAILBOX_DEFAULT_POLL_INTERVAL_SECS;
|
|
63
|
+
if (pollIntervalRaw !== undefined && pollIntervalRaw !== null) {
|
|
64
|
+
const parsed = numberFromUnknown(pollIntervalRaw);
|
|
65
|
+
if (parsed === undefined || !Number.isInteger(parsed) || parsed < 15) {
|
|
66
|
+
throw new Error(`email_mailbox config.pollIntervalSecs must be an integer >= 15; got ${String(pollIntervalRaw)}`);
|
|
67
|
+
}
|
|
68
|
+
pollIntervalSecs = parsed;
|
|
69
|
+
}
|
|
70
|
+
const smtpEndpoint = asString(config.smtpEndpoint);
|
|
71
|
+
if (smtpEndpoint && !/^smtp:\/\/./i.test(smtpEndpoint)) {
|
|
72
|
+
throw new Error(`email_mailbox config.smtpEndpoint must use smtp:// (uxc SMTP surface); got ${smtpEndpoint}`);
|
|
73
|
+
}
|
|
74
|
+
const fromAddress = asString(config.fromAddress);
|
|
75
|
+
if (fromAddress && !fromAddress.includes("@")) {
|
|
76
|
+
throw new Error(`email_mailbox config.fromAddress must be an email address; got ${fromAddress}`);
|
|
77
|
+
}
|
|
78
|
+
const account = asString(config.account) ?? undefined;
|
|
79
|
+
if (account !== undefined && account.trim().length === 0) {
|
|
80
|
+
throw new Error("email_mailbox config.account must be a non-empty string when provided");
|
|
81
|
+
}
|
|
82
|
+
const mailbox = asString(config.mailbox) ?? exports.EMAIL_DEFAULT_MAILBOX;
|
|
83
|
+
if (mailbox.trim().length === 0) {
|
|
84
|
+
throw new Error("email_mailbox config.mailbox must be a non-empty string when provided");
|
|
85
|
+
}
|
|
86
|
+
const addressAllowlist = asStringArray(config.addressAllowlist ?? config.addressAllowList);
|
|
87
|
+
return {
|
|
88
|
+
provider: provider,
|
|
89
|
+
endpoint,
|
|
90
|
+
uxcAuth: uxcAuth.trim(),
|
|
91
|
+
account: account?.trim() || undefined,
|
|
92
|
+
mailbox,
|
|
93
|
+
pollIntervalSecs,
|
|
94
|
+
...(smtpEndpoint ? { smtpEndpoint } : {}),
|
|
95
|
+
...(fromAddress ? { fromAddress } : {}),
|
|
96
|
+
...(addressAllowlist && addressAllowlist.length > 0 ? { addressAllowlist } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function buildEmailMailboxSourceSpec(config) {
|
|
100
|
+
const args = compactRecord({
|
|
101
|
+
mailbox: config.mailbox,
|
|
102
|
+
account: config.account,
|
|
103
|
+
});
|
|
104
|
+
if (config.provider === "imap") {
|
|
105
|
+
return {
|
|
106
|
+
endpoint: config.endpoint,
|
|
107
|
+
mode: "stream",
|
|
108
|
+
transport_hint: "email_imap_idle",
|
|
109
|
+
args,
|
|
110
|
+
options: {
|
|
111
|
+
auth: config.uxcAuth,
|
|
112
|
+
artifact_compaction: false,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
endpoint: config.endpoint,
|
|
118
|
+
mode: "poll",
|
|
119
|
+
transport_hint: "email_provider_poll",
|
|
120
|
+
args: {
|
|
121
|
+
provider: config.provider,
|
|
122
|
+
...args,
|
|
123
|
+
},
|
|
124
|
+
poll_config: {
|
|
125
|
+
interval_secs: config.pollIntervalSecs,
|
|
126
|
+
extract_items_pointer: "/items",
|
|
127
|
+
checkpoint_strategy: {
|
|
128
|
+
type: "item_key",
|
|
129
|
+
item_key_pointer: "/message/uid",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
options: {
|
|
133
|
+
auth: config.uxcAuth,
|
|
134
|
+
artifact_compaction: false,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function emailActorParts(value) {
|
|
139
|
+
if (typeof value === "string") {
|
|
140
|
+
const raw = value.trim();
|
|
141
|
+
if (raw.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const angled = raw.match(/<([^>]+)>\s*$/);
|
|
145
|
+
if (angled) {
|
|
146
|
+
const name = raw.slice(0, raw.lastIndexOf("<")).trim().replace(/^"|"$/g, "").trim();
|
|
147
|
+
return { address: angled[1].trim(), name: name.length > 0 ? name : null, raw };
|
|
148
|
+
}
|
|
149
|
+
return { address: raw, name: null, raw };
|
|
150
|
+
}
|
|
151
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const record = value;
|
|
155
|
+
if (typeof record.raw === "string") {
|
|
156
|
+
return emailActorParts(record.raw);
|
|
157
|
+
}
|
|
158
|
+
const address = asString(record.address) ?? asString(record.email);
|
|
159
|
+
if (address) {
|
|
160
|
+
return { address, name: asString(record.name), raw: address };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function emailActorAddresses(value) {
|
|
165
|
+
if (!Array.isArray(value)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const addresses = [];
|
|
169
|
+
for (const item of value) {
|
|
170
|
+
const parts = emailActorParts(item);
|
|
171
|
+
if (parts?.address) {
|
|
172
|
+
addresses.push(parts.address);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return addresses;
|
|
176
|
+
}
|
|
177
|
+
function emailActorList(value) {
|
|
178
|
+
if (!Array.isArray(value)) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
return value
|
|
182
|
+
.map((item) => emailActorParts(item))
|
|
183
|
+
.filter((parts) => Boolean(parts?.address))
|
|
184
|
+
.map((parts) => ({
|
|
185
|
+
address: parts.address,
|
|
186
|
+
...(parts.name ? { name: parts.name } : {}),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
function normalizeEmailMailboxEvent(source, config, raw) {
|
|
190
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const payload = raw;
|
|
194
|
+
if (payload.type !== exports.EMAIL_EVENT_TYPE) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const version = asString(payload.version);
|
|
198
|
+
if (version !== null && version !== exports.EMAIL_EVENT_VERSION) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (payload.event_kind !== "message_received") {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const message = asRecord(payload.message);
|
|
205
|
+
if (Object.keys(message).length === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const provider = asString(payload.provider) ?? config.provider;
|
|
209
|
+
const account = asString(payload.account) ?? config.account ?? config.uxcAuth;
|
|
210
|
+
const mailbox = asString(payload.mailbox) ?? config.mailbox;
|
|
211
|
+
const uid = stringFromUnknown(message.uid);
|
|
212
|
+
const messageId = asString(message.message_id) ?? uid;
|
|
213
|
+
if (!uid && !messageId) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const fromParts = emailActorParts(message.from);
|
|
217
|
+
const toAddresses = emailActorAddresses(message.to);
|
|
218
|
+
if (config.addressAllowlist &&
|
|
219
|
+
config.addressAllowlist.length > 0 &&
|
|
220
|
+
!addressMatchesAllowlist(config.addressAllowlist, fromParts?.address ?? null, toAddresses)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const attachments = Array.isArray(message.attachments)
|
|
224
|
+
? message.attachments.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item))
|
|
225
|
+
: [];
|
|
226
|
+
const attachmentCountRaw = message.attachment_count;
|
|
227
|
+
const attachmentCount = numberFromUnknown(attachmentCountRaw) ?? null;
|
|
228
|
+
const date = asString(message.date);
|
|
229
|
+
return {
|
|
230
|
+
sourceId: source.sourceId,
|
|
231
|
+
sourceNativeId: `email:${provider}:${account}:${mailbox}:${uid ?? messageId}`,
|
|
232
|
+
eventVariant: "email.message.received",
|
|
233
|
+
occurredAt: occurredAtFromEmailDate(date),
|
|
234
|
+
metadata: {
|
|
235
|
+
provider,
|
|
236
|
+
account,
|
|
237
|
+
mailbox,
|
|
238
|
+
messageId: messageId ?? null,
|
|
239
|
+
providerMessageId: uid ?? null,
|
|
240
|
+
threadId: asString(message.thread_id),
|
|
241
|
+
from: fromParts ? (fromParts.address ?? fromParts.raw) : null,
|
|
242
|
+
fromName: fromParts?.name ?? null,
|
|
243
|
+
to: emailActorList(message.to),
|
|
244
|
+
cc: emailActorList(message.cc),
|
|
245
|
+
subject: asString(message.subject),
|
|
246
|
+
textPreview: asString(message.snippet),
|
|
247
|
+
date,
|
|
248
|
+
hasAttachments: attachments.length > 0 || message.has_attachments === true,
|
|
249
|
+
attachmentCount,
|
|
250
|
+
attachments,
|
|
251
|
+
},
|
|
252
|
+
rawPayload: payload,
|
|
253
|
+
deliveryHandle: {
|
|
254
|
+
provider: "email",
|
|
255
|
+
surface: "message_reply",
|
|
256
|
+
targetRef: fromParts ? (fromParts.address ?? fromParts.raw) : "",
|
|
257
|
+
threadRef: messageId ?? null,
|
|
258
|
+
replyMode: "reply",
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function addressMatchesAllowlist(allowlist, fromAddress, toAddresses) {
|
|
263
|
+
const normalized = allowlist
|
|
264
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
265
|
+
.filter((entry) => entry.length > 0);
|
|
266
|
+
if (normalized.length === 0) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
const lowerFrom = fromAddress?.toLowerCase() ?? null;
|
|
270
|
+
if (lowerFrom && normalized.includes(lowerFrom)) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return toAddresses.some((address) => normalized.includes(address.toLowerCase()));
|
|
274
|
+
}
|
|
275
|
+
function occurredAtFromEmailDate(date) {
|
|
276
|
+
if (date) {
|
|
277
|
+
const parsed = Date.parse(date);
|
|
278
|
+
if (Number.isFinite(parsed)) {
|
|
279
|
+
return new Date(parsed).toISOString();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return new Date().toISOString();
|
|
283
|
+
}
|
|
284
|
+
class RpcEmailUxcClient {
|
|
285
|
+
client;
|
|
286
|
+
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
|
|
287
|
+
this.client = client;
|
|
288
|
+
}
|
|
289
|
+
request(method, params) {
|
|
290
|
+
return this.client.request(method, params);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
exports.RpcEmailUxcClient = RpcEmailUxcClient;
|
|
294
|
+
function emailDeliveryOperationsForHandle(handle) {
|
|
295
|
+
if (handle.provider !== "email") {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
if (handle.surface !== "message_reply" && handle.surface !== "message_send") {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const isReply = handle.surface === "message_reply";
|
|
302
|
+
const commonProperties = {
|
|
303
|
+
text: { type: "string", minLength: 1 },
|
|
304
|
+
subject: { type: "string", minLength: 1 },
|
|
305
|
+
to: { type: "array", items: { type: "string", minLength: 1 } },
|
|
306
|
+
cc: { type: "array", items: { type: "string", minLength: 1 } },
|
|
307
|
+
from: { type: "string", minLength: 1 },
|
|
308
|
+
smtpEndpoint: { type: "string", minLength: 1 },
|
|
309
|
+
uxcAuth: { type: "string", minLength: 1 },
|
|
310
|
+
correlationKey: { type: "string", minLength: 1 },
|
|
311
|
+
};
|
|
312
|
+
const operations = [
|
|
313
|
+
{
|
|
314
|
+
name: "send_text",
|
|
315
|
+
title: isReply ? "Reply With Text" : "Send Text Email",
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: "object",
|
|
318
|
+
additionalProperties: false,
|
|
319
|
+
required: ["text", "subject"],
|
|
320
|
+
properties: commonProperties,
|
|
321
|
+
},
|
|
322
|
+
canonicalTextAlias: true,
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
if (isReply) {
|
|
326
|
+
operations.push({
|
|
327
|
+
name: "reply_text",
|
|
328
|
+
title: "Reply With Text (Threaded)",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: "object",
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
required: ["text"],
|
|
333
|
+
properties: {
|
|
334
|
+
...commonProperties,
|
|
335
|
+
replyAll: { type: "boolean" },
|
|
336
|
+
inReplyTo: { type: "string", minLength: 1 },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return operations;
|
|
342
|
+
}
|
|
343
|
+
async function invokeEmailDeliveryOperation(handle, operation, input, options) {
|
|
344
|
+
if (handle.provider !== "email") {
|
|
345
|
+
throw new Error(`email delivery operation requires an email handle, got ${handle.provider}`);
|
|
346
|
+
}
|
|
347
|
+
if (operation !== "send_text" && operation !== "reply_text") {
|
|
348
|
+
throw new Error(`unknown email delivery operation: ${operation}`);
|
|
349
|
+
}
|
|
350
|
+
const isReply = operation === "reply_text" || handle.surface === "message_reply";
|
|
351
|
+
const text = asString(input.text);
|
|
352
|
+
if (!text || text.trim().length === 0) {
|
|
353
|
+
throw new Error(`${operation} requires input.text`);
|
|
354
|
+
}
|
|
355
|
+
const config = sourceConfigOrNull(options?.source);
|
|
356
|
+
const to = asStringArray(input.to) ?? (handle.targetRef ? [handle.targetRef] : []);
|
|
357
|
+
const cc = asStringArray(input.cc) ?? [];
|
|
358
|
+
if (to.length === 0 && cc.length === 0) {
|
|
359
|
+
throw new Error(`${operation} requires at least one recipient in input.to (or input.cc)`);
|
|
360
|
+
}
|
|
361
|
+
const subject = resolveEmailSubject(operation, input, isReply);
|
|
362
|
+
const smtpEndpoint = asString(input.smtpEndpoint) ?? config?.smtpEndpoint ?? null;
|
|
363
|
+
if (!smtpEndpoint) {
|
|
364
|
+
throw new Error(`${operation} requires input.smtpEndpoint or source config smtpEndpoint (uxc supports smtp://)`);
|
|
365
|
+
}
|
|
366
|
+
const from = asString(input.from) ?? config?.fromAddress ?? addressLikeAccount(config?.account) ?? null;
|
|
367
|
+
if (!from) {
|
|
368
|
+
throw new Error(`${operation} requires input.from, source config fromAddress, or an account-like config.account value`);
|
|
369
|
+
}
|
|
370
|
+
const uxcAuth = asString(input.uxcAuth) ?? config?.uxcAuth ?? null;
|
|
371
|
+
const correlationKey = asString(input.correlationKey);
|
|
372
|
+
const inReplyTo = asString(input.inReplyTo) ?? (isReply ? handle.threadRef ?? null : null);
|
|
373
|
+
const params = compactRecord({
|
|
374
|
+
smtp_url: smtpEndpoint,
|
|
375
|
+
from,
|
|
376
|
+
to,
|
|
377
|
+
cc: cc.length > 0 ? cc : undefined,
|
|
378
|
+
subject,
|
|
379
|
+
text,
|
|
380
|
+
in_reply_to: inReplyTo ?? undefined,
|
|
381
|
+
auth: uxcAuth ?? undefined,
|
|
382
|
+
message_id: correlationKey ? correlationMessageId(correlationKey) : undefined,
|
|
383
|
+
});
|
|
384
|
+
const method = isReply ? "email.reply" : "email.send";
|
|
385
|
+
if (isReply) {
|
|
386
|
+
params.reply_handle = compactRecord({
|
|
387
|
+
message_id: handle.threadRef ?? undefined,
|
|
388
|
+
account: config?.account ?? undefined,
|
|
389
|
+
mailbox: config?.mailbox ?? undefined,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const client = options?.client ?? new RpcEmailUxcClient();
|
|
393
|
+
let result;
|
|
394
|
+
try {
|
|
395
|
+
result = await client.request(method, params);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
throw wrapEmailRpcError(error, method);
|
|
399
|
+
}
|
|
400
|
+
const note = isReply
|
|
401
|
+
? `sent email reply${result.message_id ? ` (message-id ${result.message_id})` : ""}`
|
|
402
|
+
: `sent email${result.message_id ? ` (message-id ${result.message_id})` : ""}`;
|
|
403
|
+
return { status: "sent", note };
|
|
404
|
+
}
|
|
405
|
+
function resolveEmailSubject(operation, input, isReply) {
|
|
406
|
+
const subject = asString(input.subject);
|
|
407
|
+
if (!isReply) {
|
|
408
|
+
if (!subject || subject.trim().length === 0) {
|
|
409
|
+
throw new Error(`${operation} requires input.subject`);
|
|
410
|
+
}
|
|
411
|
+
return subject;
|
|
412
|
+
}
|
|
413
|
+
const base = subject && subject.trim().length > 0 ? subject.trim() : "your message";
|
|
414
|
+
if (/^re:/i.test(base)) {
|
|
415
|
+
return base;
|
|
416
|
+
}
|
|
417
|
+
return `Re: ${base}`;
|
|
418
|
+
}
|
|
419
|
+
function correlationMessageId(correlationKey) {
|
|
420
|
+
const trimmed = correlationKey.trim();
|
|
421
|
+
if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
|
|
422
|
+
return trimmed;
|
|
423
|
+
}
|
|
424
|
+
const safe = trimmed.replace(/[<>\s]/g, "");
|
|
425
|
+
return `<agentinbox-${safe}@localhost>`;
|
|
426
|
+
}
|
|
427
|
+
function sourceConfigOrNull(source) {
|
|
428
|
+
if (!source || source.sourceType !== "email_mailbox") {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
return parseEmailMailboxSourceConfig(source);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function addressLikeAccount(account) {
|
|
439
|
+
if (!account) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return account.includes("@") ? account : null;
|
|
443
|
+
}
|
|
444
|
+
function wrapEmailRpcError(error, method) {
|
|
445
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
446
|
+
const code = error.code;
|
|
447
|
+
if (code === -32601 || code === -32602) {
|
|
448
|
+
return new Error(`connected uxc daemon does not support ${method} (code ${code}); upgrade uxc to a version with email send/reply RPC support`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
452
|
+
}
|
|
453
|
+
class EmailDeliveryAdapter {
|
|
454
|
+
client;
|
|
455
|
+
constructor(client) {
|
|
456
|
+
this.client = client ?? new RpcEmailUxcClient();
|
|
457
|
+
}
|
|
458
|
+
async send(request, attempt) {
|
|
459
|
+
const handle = {
|
|
460
|
+
provider: attempt.provider,
|
|
461
|
+
surface: attempt.surface,
|
|
462
|
+
targetRef: attempt.targetRef,
|
|
463
|
+
threadRef: attempt.threadRef ?? null,
|
|
464
|
+
replyMode: attempt.replyMode ?? null,
|
|
465
|
+
};
|
|
466
|
+
const result = await invokeEmailDeliveryOperation(handle, "send_text", request.payload, {
|
|
467
|
+
client: this.client,
|
|
468
|
+
});
|
|
469
|
+
return { status: "sent", note: result.note };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
exports.EmailDeliveryAdapter = EmailDeliveryAdapter;
|
|
473
|
+
function compactRecord(input) {
|
|
474
|
+
const output = {};
|
|
475
|
+
for (const [key, value] of Object.entries(input)) {
|
|
476
|
+
if (value === undefined || value === null) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
output[key] = value;
|
|
483
|
+
}
|
|
484
|
+
return output;
|
|
485
|
+
}
|
|
486
|
+
function asRecord(value) {
|
|
487
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
488
|
+
return {};
|
|
489
|
+
}
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
function asString(value) {
|
|
493
|
+
return typeof value === "string" ? value : null;
|
|
494
|
+
}
|
|
495
|
+
function asStringArray(value) {
|
|
496
|
+
if (Array.isArray(value)) {
|
|
497
|
+
return value
|
|
498
|
+
.map((item) => stringFromUnknown(item))
|
|
499
|
+
.filter((item) => Boolean(item && item.trim().length > 0));
|
|
500
|
+
}
|
|
501
|
+
if (typeof value === "string") {
|
|
502
|
+
return value
|
|
503
|
+
.split(",")
|
|
504
|
+
.map((item) => item.trim())
|
|
505
|
+
.filter((item) => item.length > 0);
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
function stringFromUnknown(value) {
|
|
510
|
+
if (typeof value === "string") {
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
514
|
+
return String(value);
|
|
515
|
+
}
|
|
516
|
+
if (typeof value === "bigint") {
|
|
517
|
+
return value.toString();
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
function numberFromUnknown(value) {
|
|
522
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
526
|
+
const parsed = Number(value);
|
|
527
|
+
if (Number.isFinite(parsed)) {
|
|
528
|
+
return parsed;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
@@ -15,9 +15,10 @@ const github_1 = require("./github");
|
|
|
15
15
|
const github_ci_1 = require("./github_ci");
|
|
16
16
|
const feishu_1 = require("./feishu");
|
|
17
17
|
const telegram_1 = require("./telegram");
|
|
18
|
+
const email_1 = require("./email");
|
|
18
19
|
const REMOTE_USER_MODULE_ROOT_DIR = "source-modules";
|
|
19
|
-
const BUILTIN_MODULE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot", "builtin.telegram_bot"]);
|
|
20
|
-
const BUILTIN_REMOTE_SOURCE_TYPES = ["github_repo", "github_repo_ci", "feishu_bot", "telegram_bot"];
|
|
20
|
+
const BUILTIN_MODULE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot", "builtin.telegram_bot", "builtin.email_mailbox"]);
|
|
21
|
+
const BUILTIN_REMOTE_SOURCE_TYPES = ["github_repo", "github_repo_ci", "feishu_bot", "telegram_bot", "email_mailbox"];
|
|
21
22
|
class RemoteSourceModuleRegistry {
|
|
22
23
|
moduleCache = new Map();
|
|
23
24
|
githubRepoModule;
|
|
@@ -37,6 +38,9 @@ class RemoteSourceModuleRegistry {
|
|
|
37
38
|
if (source.sourceType === "telegram_bot") {
|
|
38
39
|
return Promise.resolve(TELEGRAM_BOT_MODULE);
|
|
39
40
|
}
|
|
41
|
+
if (source.sourceType === "email_mailbox") {
|
|
42
|
+
return Promise.resolve(EMAIL_MAILBOX_MODULE);
|
|
43
|
+
}
|
|
40
44
|
if (source.sourceType !== "remote_source") {
|
|
41
45
|
throw new Error(`unsupported source type for remote module: ${source.sourceType}`);
|
|
42
46
|
}
|
|
@@ -86,6 +90,9 @@ function builtInModuleIdForSourceType(sourceType) {
|
|
|
86
90
|
if (sourceType === "telegram_bot") {
|
|
87
91
|
return "builtin.telegram_bot";
|
|
88
92
|
}
|
|
93
|
+
if (sourceType === "email_mailbox") {
|
|
94
|
+
return "builtin.email_mailbox";
|
|
95
|
+
}
|
|
89
96
|
return null;
|
|
90
97
|
}
|
|
91
98
|
function builtinRemoteSourceTypes() {
|
|
@@ -631,6 +638,73 @@ const TELEGRAM_BOT_MODULE = {
|
|
|
631
638
|
};
|
|
632
639
|
},
|
|
633
640
|
};
|
|
641
|
+
const EMAIL_MAILBOX_MODULE = {
|
|
642
|
+
id: "builtin.email_mailbox",
|
|
643
|
+
listDeliveryOperations(input) {
|
|
644
|
+
return (0, email_1.emailDeliveryOperationsForHandle)(input.handle);
|
|
645
|
+
},
|
|
646
|
+
async invokeDeliveryOperation(input) {
|
|
647
|
+
return (0, email_1.invokeEmailDeliveryOperation)(input.handle, input.operation, input.input, {
|
|
648
|
+
source: input.source,
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
describeCapabilities() {
|
|
652
|
+
return {
|
|
653
|
+
sourceKind: "email_mailbox",
|
|
654
|
+
aliases: ["email_mailbox", "email"],
|
|
655
|
+
configSchema: [
|
|
656
|
+
{ name: "provider", type: "string", required: true, description: "Email provider: imap, gmail, graph, or jmap." },
|
|
657
|
+
{ name: "uxcAuth", type: "string", required: true, description: "UXC auth profile name holding mailbox credentials." },
|
|
658
|
+
{ name: "endpoint", type: "string", required: false, description: "imap:// or imaps:// endpoint for imap; provider API endpoint otherwise (required for jmap)." },
|
|
659
|
+
{ name: "account", type: "string", required: false, description: "Account alias; defaults to the auth profile username." },
|
|
660
|
+
{ name: "mailbox", type: "string", required: false, description: "Mailbox to watch; defaults to INBOX." },
|
|
661
|
+
{ name: "pollIntervalSecs", type: "number", required: false, description: "Provider poll interval in seconds (min 15, default 60)." },
|
|
662
|
+
{ name: "smtpEndpoint", type: "string", required: false, description: "Default smtp:// endpoint for outbound delivery." },
|
|
663
|
+
{ name: "fromAddress", type: "string", required: false, description: "Default outbound From address." },
|
|
664
|
+
{ name: "addressAllowlist", type: "string[]", required: false, description: "Optional from/to address allowlist." },
|
|
665
|
+
],
|
|
666
|
+
metadataFields: [
|
|
667
|
+
{ name: "provider", type: "string", description: "Email provider." },
|
|
668
|
+
{ name: "account", type: "string", description: "Mailbox account alias." },
|
|
669
|
+
{ name: "mailbox", type: "string", description: "Mailbox name." },
|
|
670
|
+
{ name: "messageId", type: "string|null", description: "RFC Message-ID when present." },
|
|
671
|
+
{ name: "providerMessageId", type: "string|null", description: "Provider-native message id." },
|
|
672
|
+
{ name: "threadId", type: "string|null", description: "Thread reference from References/In-Reply-To headers." },
|
|
673
|
+
{ name: "from", type: "string|null", description: "Normalized sender address." },
|
|
674
|
+
{ name: "fromName", type: "string|null", description: "Sender display name." },
|
|
675
|
+
{ name: "to", type: "object[]", description: "Normalized recipients." },
|
|
676
|
+
{ name: "cc", type: "object[]", description: "Normalized cc recipients." },
|
|
677
|
+
{ name: "subject", type: "string|null", description: "Message subject." },
|
|
678
|
+
{ name: "textPreview", type: "string|null", description: "Body snippet." },
|
|
679
|
+
{ name: "hasAttachments", type: "boolean", description: "Whether the message carries attachments." },
|
|
680
|
+
{ name: "attachmentCount", type: "number|null", description: "Attachment count; null when provider metadata was not expanded." },
|
|
681
|
+
{ name: "attachments", type: "object[]", description: "Attachment metadata with opaque retrieval handles." },
|
|
682
|
+
],
|
|
683
|
+
eventVariantExamples: ["email.message.received"],
|
|
684
|
+
};
|
|
685
|
+
},
|
|
686
|
+
validateConfig(source) {
|
|
687
|
+
(0, email_1.parseEmailMailboxSourceConfig)(source);
|
|
688
|
+
},
|
|
689
|
+
buildManagedSourceSpec(source) {
|
|
690
|
+
return (0, email_1.buildEmailMailboxSourceSpec)((0, email_1.parseEmailMailboxSourceConfig)(source));
|
|
691
|
+
},
|
|
692
|
+
mapRawEvent(rawPayload, source) {
|
|
693
|
+
const config = (0, email_1.parseEmailMailboxSourceConfig)(source);
|
|
694
|
+
const normalized = (0, email_1.normalizeEmailMailboxEvent)(source, config, rawPayload);
|
|
695
|
+
if (!normalized) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
sourceNativeId: normalized.sourceNativeId,
|
|
700
|
+
eventVariant: normalized.eventVariant,
|
|
701
|
+
metadata: normalized.metadata ?? {},
|
|
702
|
+
rawPayload: normalized.rawPayload ?? rawPayload,
|
|
703
|
+
occurredAt: normalized.occurredAt,
|
|
704
|
+
deliveryHandle: normalized.deliveryHandle,
|
|
705
|
+
};
|
|
706
|
+
},
|
|
707
|
+
};
|
|
634
708
|
function moduleConfigForSource(source) {
|
|
635
709
|
const config = asRecord(source.config);
|
|
636
710
|
if (source.sourceType === "remote_source") {
|
package/dist/src/store.js
CHANGED
|
@@ -40,24 +40,29 @@ class AgentInboxStore {
|
|
|
40
40
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dbPath), { recursive: true });
|
|
41
41
|
removeOrphanBackupTmps(dbPath);
|
|
42
42
|
const existedBeforeOpen = node_fs_1.default.existsSync(dbPath);
|
|
43
|
-
|
|
43
|
+
// Legacy AGENTINBOX_STARTUP_BACKUP=1 mode pays for a full check and an
|
|
44
|
+
// unconditional backup on every open, matching v1.5.2 behavior.
|
|
45
|
+
const legacyStartupBackup = legacyStartupBackupEnabled(env);
|
|
46
|
+
let db = await this.openDatabaseWithRecovery(dbPath, legacyStartupBackup);
|
|
44
47
|
let store = new AgentInboxStore(dbPath, db);
|
|
48
|
+
let archivedPreV1 = false;
|
|
45
49
|
if (existedBeforeOpen && store.shouldArchivePreV1Database()) {
|
|
46
50
|
const archivedPath = store.archivePreV1Database();
|
|
47
51
|
console.warn(`[agentinbox] archived pre-v1 local database to ${archivedPath}; starting with a fresh v1 database (no data imported).`);
|
|
48
52
|
db = this.openDatabase(dbPath);
|
|
49
53
|
store = new AgentInboxStore(dbPath, db);
|
|
54
|
+
archivedPreV1 = true;
|
|
50
55
|
}
|
|
51
|
-
else if (existedBeforeOpen &&
|
|
52
|
-
await store.
|
|
56
|
+
else if (existedBeforeOpen && legacyStartupBackup) {
|
|
57
|
+
await store.backupDatabase();
|
|
53
58
|
}
|
|
54
|
-
store.migrate();
|
|
59
|
+
await store.migrate({ existedBeforeOpen: existedBeforeOpen && !archivedPreV1, env });
|
|
55
60
|
store.persist();
|
|
56
61
|
return store;
|
|
57
62
|
}
|
|
58
|
-
static async openDatabaseWithRecovery(dbPath) {
|
|
63
|
+
static async openDatabaseWithRecovery(dbPath, fullCheck) {
|
|
59
64
|
try {
|
|
60
|
-
return this.openDatabase(dbPath);
|
|
65
|
+
return this.openDatabase(dbPath, fullCheck);
|
|
61
66
|
}
|
|
62
67
|
catch (error) {
|
|
63
68
|
if (!node_fs_1.default.existsSync(dbPath)) {
|
|
@@ -71,19 +76,29 @@ class AgentInboxStore {
|
|
|
71
76
|
node_fs_1.default.renameSync(dbPath, corruptPath);
|
|
72
77
|
node_fs_1.default.copyFileSync(backupPath, dbPath);
|
|
73
78
|
console.warn(`[agentinbox] recovered local database from ${backupPath}; archived corrupt database to ${corruptPath}.`);
|
|
74
|
-
return this.openDatabase(dbPath);
|
|
79
|
+
return this.openDatabase(dbPath, fullCheck);
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
|
-
static openDatabase(dbPath) {
|
|
82
|
+
static openDatabase(dbPath, fullCheck = false) {
|
|
78
83
|
const db = new better_sqlite3_1.default(dbPath);
|
|
79
84
|
db.pragma("busy_timeout = 5000");
|
|
80
85
|
db.pragma("journal_mode = WAL");
|
|
81
86
|
db.pragma("synchronous = NORMAL");
|
|
82
87
|
db.pragma("foreign_keys = ON");
|
|
83
|
-
this.assertHealthy(db, dbPath);
|
|
88
|
+
this.assertHealthy(db, dbPath, fullCheck);
|
|
84
89
|
return db;
|
|
85
90
|
}
|
|
86
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Verifies database integrity. Regular opens run `quick_check` and only
|
|
93
|
+
* escalate to a full `integrity_check` when the quick pass fails, so large
|
|
94
|
+
* databases do not pay a full scan on every startup. Conservative callers
|
|
95
|
+
* (legacy `AGENTINBOX_STARTUP_BACKUP=1` mode, recovery-candidate
|
|
96
|
+
* validation) pass `fullCheck`.
|
|
97
|
+
*/
|
|
98
|
+
static assertHealthy(db, dbPath, fullCheck) {
|
|
99
|
+
if (!fullCheck && db.pragma("quick_check", { simple: true }) === "ok") {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
87
102
|
const result = db.pragma("integrity_check", { simple: true });
|
|
88
103
|
if (result !== "ok") {
|
|
89
104
|
db.close();
|
|
@@ -95,7 +110,7 @@ class AgentInboxStore {
|
|
|
95
110
|
for (const candidate of candidates) {
|
|
96
111
|
try {
|
|
97
112
|
const db = new better_sqlite3_1.default(candidate, { readonly: true, fileMustExist: true });
|
|
98
|
-
this.assertHealthy(db, candidate);
|
|
113
|
+
this.assertHealthy(db, candidate, true);
|
|
99
114
|
db.close();
|
|
100
115
|
return candidate;
|
|
101
116
|
}
|
|
@@ -108,14 +123,32 @@ class AgentInboxStore {
|
|
|
108
123
|
static listBackupCandidates(dbPath) {
|
|
109
124
|
const dir = node_path_1.default.dirname(dbPath);
|
|
110
125
|
const baseName = node_path_1.default.basename(dbPath);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const preMigrationPattern = new RegExp(`^${escapeRegExp(baseName)}\\.pre-migrate-v(\\d+)\\.bak$`);
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = node_fs_1.default.readdirSync(dir);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const candidates = [];
|
|
135
|
+
for (const name of entries) {
|
|
136
|
+
const candidatePath = node_path_1.default.join(dir, name);
|
|
137
|
+
if (name === `${baseName}.bak`) {
|
|
138
|
+
candidates.push({ path: candidatePath, version: 0, modifiedAtMs: backupMtimeMs(candidatePath) });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const preMigration = preMigrationPattern.exec(name);
|
|
142
|
+
if (preMigration) {
|
|
143
|
+
candidates.push({
|
|
144
|
+
path: candidatePath,
|
|
145
|
+
version: Number.parseInt(preMigration[1], 10),
|
|
146
|
+
modifiedAtMs: backupMtimeMs(candidatePath),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
candidates.sort((left, right) => right.modifiedAtMs - left.modifiedAtMs || right.version - left.version);
|
|
151
|
+
return candidates.map((candidate) => candidate.path);
|
|
119
152
|
}
|
|
120
153
|
static nextPath(base) {
|
|
121
154
|
if (!node_fs_1.default.existsSync(base)) {
|
|
@@ -135,13 +168,22 @@ class AgentInboxStore {
|
|
|
135
168
|
}
|
|
136
169
|
getDatabaseHealth() {
|
|
137
170
|
return {
|
|
138
|
-
|
|
171
|
+
quickCheck: String(this.db.pragma("quick_check", { simple: true })),
|
|
139
172
|
journalMode: String(this.db.pragma("journal_mode", { simple: true })),
|
|
140
173
|
foreignKeys: Number(this.db.pragma("foreign_keys", { simple: true })) === 1,
|
|
141
174
|
};
|
|
142
175
|
}
|
|
143
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Writes a snapshot to `<db>.bak`, used by `agentinbox backup` and by the
|
|
178
|
+
* opt-in legacy `AGENTINBOX_STARTUP_BACKUP=1` startup backup. The open-time
|
|
179
|
+
* health check has already run against this handle. Returns the backup path.
|
|
180
|
+
*/
|
|
181
|
+
async backupDatabase() {
|
|
144
182
|
const backupPath = `${this.dbPath}.bak`;
|
|
183
|
+
await this.writeBackupTo(backupPath);
|
|
184
|
+
return backupPath;
|
|
185
|
+
}
|
|
186
|
+
async writeBackupTo(backupPath) {
|
|
145
187
|
const tmpPath = `${backupPath}.${process.pid}.tmp`;
|
|
146
188
|
try {
|
|
147
189
|
node_fs_1.default.rmSync(tmpPath, { force: true });
|
|
@@ -152,17 +194,57 @@ class AgentInboxStore {
|
|
|
152
194
|
node_fs_1.default.rmSync(tmpPath, { force: true });
|
|
153
195
|
}
|
|
154
196
|
}
|
|
155
|
-
migrate() {
|
|
197
|
+
async migrate(options) {
|
|
156
198
|
const migrations = this.loadSqlMigrations();
|
|
157
199
|
this.ensureDrizzleMigrationsTable();
|
|
158
200
|
const applied = this.listAppliedMigrationTags();
|
|
159
201
|
const pending = migrations.filter((migration) => !applied.has(migration.tag));
|
|
202
|
+
if (options.existedBeforeOpen && pending.length > 0) {
|
|
203
|
+
const backupPath = await this.backupBeforeMigration(migrations.length);
|
|
204
|
+
console.warn(`[agentinbox] pending schema migrations detected; backed up the local database to ${backupPath} before migrating to schema v${migrations.length}.`);
|
|
205
|
+
this.pruneMigrationBackups(options.env);
|
|
206
|
+
}
|
|
160
207
|
for (const migration of pending) {
|
|
161
208
|
this.applyMigration(migration);
|
|
162
209
|
}
|
|
163
210
|
this.ensureInboxEntryBackfill();
|
|
164
211
|
this.setUserVersion(migrations.length);
|
|
165
212
|
}
|
|
213
|
+
async backupBeforeMigration(targetVersion) {
|
|
214
|
+
const backupPath = `${this.dbPath}.pre-migrate-v${targetVersion}.bak`;
|
|
215
|
+
await this.writeBackupTo(backupPath);
|
|
216
|
+
return backupPath;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Keeps only the newest `AGENTINBOX_MIGRATION_BACKUP_KEEP` pre-migration
|
|
220
|
+
* backups (default 5; values <= 0 keep everything). Migration backups are
|
|
221
|
+
* rare, so pruning only runs right after a new one is written.
|
|
222
|
+
*/
|
|
223
|
+
pruneMigrationBackups(env) {
|
|
224
|
+
const keep = migrationBackupKeep(env);
|
|
225
|
+
if (keep <= 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const dir = node_path_1.default.dirname(this.dbPath);
|
|
229
|
+
const pattern = new RegExp(`^${escapeRegExp(node_path_1.default.basename(this.dbPath))}\\.pre-migrate-v(\\d+)\\.bak$`);
|
|
230
|
+
const entries = [];
|
|
231
|
+
for (const name of node_fs_1.default.readdirSync(dir)) {
|
|
232
|
+
const match = pattern.exec(name);
|
|
233
|
+
if (!match) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
entries.push({ path: node_path_1.default.join(dir, name), version: Number.parseInt(match[1], 10) });
|
|
237
|
+
}
|
|
238
|
+
entries.sort((left, right) => right.version - left.version);
|
|
239
|
+
for (const entry of entries.slice(keep)) {
|
|
240
|
+
try {
|
|
241
|
+
node_fs_1.default.rmSync(entry.path, { force: true });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Best-effort pruning.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
166
248
|
loadSqlMigrations() {
|
|
167
249
|
const migrationsDir = this.resolveMigrationsDir();
|
|
168
250
|
const files = node_fs_1.default
|
|
@@ -2384,16 +2466,37 @@ function uniqueSorted(values) {
|
|
|
2384
2466
|
function summarizeBackfilledItemEntry(item) {
|
|
2385
2467
|
return `${item.eventVariant} from ${item.sourceId}`;
|
|
2386
2468
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2469
|
+
const DEFAULT_MIGRATION_BACKUP_KEEP = 5;
|
|
2470
|
+
/**
|
|
2471
|
+
* Backups are event-driven: a full database backup is taken only before
|
|
2472
|
+
* pending schema migrations run, plus explicit `agentinbox backup` snapshots.
|
|
2473
|
+
* Setting `AGENTINBOX_STARTUP_BACKUP=1` opts back into the legacy v1.5.2
|
|
2474
|
+
* behavior of an unconditional backup and a full `integrity_check` on every
|
|
2475
|
+
* open.
|
|
2476
|
+
*/
|
|
2477
|
+
function legacyStartupBackupEnabled(env) {
|
|
2478
|
+
return (0, util_1.isEnvFlagEnabled)(env.AGENTINBOX_STARTUP_BACKUP);
|
|
2479
|
+
}
|
|
2480
|
+
function migrationBackupKeep(env) {
|
|
2481
|
+
const parsed = Number.parseInt(env.AGENTINBOX_MIGRATION_BACKUP_KEEP ?? "", 10);
|
|
2482
|
+
return Number.isInteger(parsed) ? parsed : DEFAULT_MIGRATION_BACKUP_KEEP;
|
|
2483
|
+
}
|
|
2484
|
+
function backupMtimeMs(candidatePath) {
|
|
2485
|
+
try {
|
|
2486
|
+
return node_fs_1.default.statSync(candidatePath).mtimeMs;
|
|
2487
|
+
}
|
|
2488
|
+
catch {
|
|
2489
|
+
return 0;
|
|
2490
|
+
}
|
|
2389
2491
|
}
|
|
2390
2492
|
/**
|
|
2391
|
-
* Removes leftover `<db
|
|
2392
|
-
*
|
|
2493
|
+
* Removes leftover `<db>.*.bak.<pid>.tmp` files (manual and pre-migration
|
|
2494
|
+
* backup temporaries) whose owning process is dead. Tmp files owned by a live
|
|
2495
|
+
* pid belong to a concurrent open and are preserved.
|
|
2393
2496
|
*/
|
|
2394
2497
|
function removeOrphanBackupTmps(dbPath) {
|
|
2395
2498
|
const dir = node_path_1.default.dirname(dbPath);
|
|
2396
|
-
const pattern = new RegExp(`^${escapeRegExp(node_path_1.default.basename(dbPath))}\\.bak\\.(\\d+)\\.tmp$`);
|
|
2499
|
+
const pattern = new RegExp(`^${escapeRegExp(node_path_1.default.basename(dbPath))}\\.(?:.+\\.)?bak\\.(\\d+)\\.tmp$`);
|
|
2397
2500
|
let entries;
|
|
2398
2501
|
try {
|
|
2399
2502
|
entries = node_fs_1.default.readdirSync(dir);
|