aamp-openclaw-plugin 0.1.6

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/dist/index.js ADDED
@@ -0,0 +1,2222 @@
1
+ // src/index.ts
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ // node_modules/aamp-sdk/dist/client.js
7
+ import { EventEmitter as EventEmitter2 } from "events";
8
+
9
+ // node_modules/aamp-sdk/dist/jmap-push.js
10
+ import WebSocket from "ws";
11
+ import { EventEmitter } from "events";
12
+
13
+ // node_modules/aamp-sdk/dist/types.js
14
+ var AAMP_HEADER = {
15
+ INTENT: "X-AAMP-Intent",
16
+ TASK_ID: "X-AAMP-TaskId",
17
+ TIMEOUT: "X-AAMP-Timeout",
18
+ CONTEXT_LINKS: "X-AAMP-ContextLinks",
19
+ STATUS: "X-AAMP-Status",
20
+ OUTPUT: "X-AAMP-Output",
21
+ ERROR_MSG: "X-AAMP-ErrorMsg",
22
+ STRUCTURED_RESULT: "X-AAMP-StructuredResult",
23
+ QUESTION: "X-AAMP-Question",
24
+ BLOCKED_REASON: "X-AAMP-BlockedReason",
25
+ SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
26
+ PARENT_TASK_ID: "X-AAMP-ParentTaskId"
27
+ };
28
+
29
+ // node_modules/aamp-sdk/dist/parser.js
30
+ function normalizeHeaders(headers) {
31
+ return Object.fromEntries(Object.entries(headers).map(([k, v]) => [
32
+ k.toLowerCase(),
33
+ Array.isArray(v) ? v[0] : v
34
+ ]));
35
+ }
36
+ function getAampHeader(headers, headerName) {
37
+ return headers[headerName.toLowerCase()];
38
+ }
39
+ function decodeStructuredResult(value) {
40
+ if (!value)
41
+ return void 0;
42
+ try {
43
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
44
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
45
+ const decoded = Buffer.from(normalized + padding, "base64").toString("utf-8");
46
+ return JSON.parse(decoded);
47
+ } catch {
48
+ return void 0;
49
+ }
50
+ }
51
+ function encodeStructuredResult(value) {
52
+ if (!value)
53
+ return void 0;
54
+ const json = JSON.stringify(value);
55
+ return Buffer.from(json, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
56
+ }
57
+ function parseAampHeaders(meta) {
58
+ const headers = normalizeHeaders(meta.headers);
59
+ const intent = getAampHeader(headers, AAMP_HEADER.INTENT);
60
+ const taskId = getAampHeader(headers, AAMP_HEADER.TASK_ID);
61
+ if (!intent || !taskId)
62
+ return null;
63
+ const from = meta.from.replace(/^<|>$/g, "");
64
+ const to = meta.to.replace(/^<|>$/g, "");
65
+ if (intent === "task.dispatch") {
66
+ const timeoutStr = getAampHeader(headers, AAMP_HEADER.TIMEOUT) ?? "300";
67
+ const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
68
+ const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
69
+ const dispatch = {
70
+ intent: "task.dispatch",
71
+ taskId,
72
+ title: meta.subject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
73
+ timeoutSecs: parseInt(timeoutStr, 10) || 300,
74
+ contextLinks: contextLinksStr ? contextLinksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
75
+ ...parentTaskId ? { parentTaskId } : {},
76
+ from,
77
+ to,
78
+ messageId: meta.messageId,
79
+ subject: meta.subject,
80
+ bodyText: ""
81
+ // filled in by jmap-push.ts after parsing
82
+ };
83
+ return dispatch;
84
+ }
85
+ if (intent === "task.result") {
86
+ const status = getAampHeader(headers, AAMP_HEADER.STATUS) ?? "completed";
87
+ const output = getAampHeader(headers, AAMP_HEADER.OUTPUT) ?? "";
88
+ const errorMsg = getAampHeader(headers, AAMP_HEADER.ERROR_MSG);
89
+ const structuredResult = decodeStructuredResult(getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT));
90
+ const result = {
91
+ intent: "task.result",
92
+ taskId,
93
+ status,
94
+ output,
95
+ errorMsg,
96
+ structuredResult,
97
+ from,
98
+ to
99
+ };
100
+ return result;
101
+ }
102
+ if (intent === "task.help") {
103
+ const question = getAampHeader(headers, AAMP_HEADER.QUESTION) ?? "";
104
+ const blockedReason = getAampHeader(headers, AAMP_HEADER.BLOCKED_REASON) ?? "";
105
+ const suggestedOptionsStr = getAampHeader(headers, AAMP_HEADER.SUGGESTED_OPTIONS) ?? "";
106
+ const help = {
107
+ intent: "task.help",
108
+ taskId,
109
+ question,
110
+ blockedReason,
111
+ suggestedOptions: suggestedOptionsStr ? suggestedOptionsStr.split("|").map((s) => s.trim()).filter(Boolean) : [],
112
+ from,
113
+ to
114
+ };
115
+ return help;
116
+ }
117
+ if (intent === "task.ack") {
118
+ const ack = {
119
+ intent: "task.ack",
120
+ taskId,
121
+ from,
122
+ to
123
+ };
124
+ return ack;
125
+ }
126
+ return null;
127
+ }
128
+ function buildDispatchHeaders(params) {
129
+ const headers = {
130
+ [AAMP_HEADER.INTENT]: "task.dispatch",
131
+ [AAMP_HEADER.TASK_ID]: params.taskId
132
+ };
133
+ if (params.timeoutSecs != null) {
134
+ headers[AAMP_HEADER.TIMEOUT] = String(params.timeoutSecs);
135
+ }
136
+ if (params.contextLinks.length > 0) {
137
+ headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
138
+ }
139
+ if (params.parentTaskId) {
140
+ headers[AAMP_HEADER.PARENT_TASK_ID] = params.parentTaskId;
141
+ }
142
+ return headers;
143
+ }
144
+ function buildAckHeaders(opts) {
145
+ return {
146
+ [AAMP_HEADER.INTENT]: "task.ack",
147
+ [AAMP_HEADER.TASK_ID]: opts.taskId
148
+ };
149
+ }
150
+ function buildResultHeaders(params) {
151
+ const headers = {
152
+ [AAMP_HEADER.INTENT]: "task.result",
153
+ [AAMP_HEADER.TASK_ID]: params.taskId,
154
+ [AAMP_HEADER.STATUS]: params.status,
155
+ [AAMP_HEADER.OUTPUT]: params.output
156
+ };
157
+ if (params.errorMsg) {
158
+ headers[AAMP_HEADER.ERROR_MSG] = params.errorMsg;
159
+ }
160
+ const structuredResult = encodeStructuredResult(params.structuredResult);
161
+ if (structuredResult) {
162
+ headers[AAMP_HEADER.STRUCTURED_RESULT] = structuredResult;
163
+ }
164
+ return headers;
165
+ }
166
+ function buildHelpHeaders(params) {
167
+ return {
168
+ [AAMP_HEADER.INTENT]: "task.help",
169
+ [AAMP_HEADER.TASK_ID]: params.taskId,
170
+ [AAMP_HEADER.QUESTION]: params.question,
171
+ [AAMP_HEADER.BLOCKED_REASON]: params.blockedReason,
172
+ [AAMP_HEADER.SUGGESTED_OPTIONS]: params.suggestedOptions.join("|")
173
+ };
174
+ }
175
+
176
+ // node_modules/aamp-sdk/dist/jmap-push.js
177
+ var JmapPushClient = class extends EventEmitter {
178
+ ws = null;
179
+ session = null;
180
+ reconnectTimer = null;
181
+ pollTimer = null;
182
+ pingTimer = null;
183
+ seenMessageIds = /* @__PURE__ */ new Set();
184
+ connected = false;
185
+ pollingActive = false;
186
+ running = false;
187
+ connecting = false;
188
+ /** JMAP Email state — tracks processed position; null = not yet initialized */
189
+ emailState = null;
190
+ startedAtMs = Date.now();
191
+ email;
192
+ password;
193
+ jmapUrl;
194
+ reconnectInterval;
195
+ rejectUnauthorized;
196
+ pingIntervalMs = 5e3;
197
+ constructor(opts) {
198
+ super();
199
+ this.email = opts.email;
200
+ this.password = opts.password;
201
+ this.jmapUrl = opts.jmapUrl.replace(/\/$/, "");
202
+ this.reconnectInterval = opts.reconnectInterval ?? 5e3;
203
+ this.rejectUnauthorized = opts.rejectUnauthorized ?? true;
204
+ }
205
+ /**
206
+ * Start the JMAP Push listener
207
+ */
208
+ async start() {
209
+ this.running = true;
210
+ await this.connect();
211
+ }
212
+ /**
213
+ * Stop the JMAP Push listener
214
+ */
215
+ stop() {
216
+ this.running = false;
217
+ if (this.reconnectTimer) {
218
+ clearTimeout(this.reconnectTimer);
219
+ this.reconnectTimer = null;
220
+ }
221
+ if (this.pollTimer) {
222
+ clearTimeout(this.pollTimer);
223
+ this.pollTimer = null;
224
+ }
225
+ if (this.pingTimer) {
226
+ clearInterval(this.pingTimer);
227
+ this.pingTimer = null;
228
+ }
229
+ if (this.ws) {
230
+ this.ws.close();
231
+ this.ws = null;
232
+ }
233
+ this.connected = false;
234
+ this.pollingActive = false;
235
+ this.connecting = false;
236
+ }
237
+ getAuthHeader() {
238
+ const creds = `${this.email}:${this.password}`;
239
+ return `Basic ${Buffer.from(creds).toString("base64")}`;
240
+ }
241
+ /**
242
+ * Fetch the JMAP session object
243
+ */
244
+ async fetchSession() {
245
+ const url = `${this.jmapUrl}/.well-known/jmap`;
246
+ const res = await fetch(url, {
247
+ headers: { Authorization: this.getAuthHeader() }
248
+ });
249
+ if (!res.ok) {
250
+ throw new Error(`Failed to fetch JMAP session: ${res.status} ${res.statusText}`);
251
+ }
252
+ return res.json();
253
+ }
254
+ /**
255
+ * Perform a JMAP API call
256
+ */
257
+ async jmapCall(methods) {
258
+ if (!this.session)
259
+ throw new Error("No JMAP session");
260
+ const apiUrl = `${this.jmapUrl}/jmap/`;
261
+ const res = await fetch(apiUrl, {
262
+ method: "POST",
263
+ headers: {
264
+ Authorization: this.getAuthHeader(),
265
+ "Content-Type": "application/json"
266
+ },
267
+ body: JSON.stringify({
268
+ using: [
269
+ "urn:ietf:params:jmap:core",
270
+ "urn:ietf:params:jmap:mail"
271
+ ],
272
+ methodCalls: methods
273
+ })
274
+ });
275
+ if (!res.ok) {
276
+ throw new Error(`JMAP API call failed: ${res.status}`);
277
+ }
278
+ return res.json();
279
+ }
280
+ /**
281
+ * Initialize emailState by fetching the current state without loading any emails.
282
+ * Called on first connect so we only process emails that arrive AFTER this point.
283
+ */
284
+ async initEmailState(accountId) {
285
+ const response = await this.jmapCall([
286
+ ["Email/get", { accountId, ids: [] }, "g0"]
287
+ ]);
288
+ const getResp = response.methodResponses.find(([name]) => name === "Email/get");
289
+ if (getResp) {
290
+ this.emailState = getResp[1].state ?? null;
291
+ }
292
+ }
293
+ /**
294
+ * Fetch only emails created since `sinceState` using Email/changes.
295
+ * Updates `this.emailState` to the new state after fetching.
296
+ * Returns [] and resets state if the server cannot calculate changes (state too old).
297
+ */
298
+ async fetchEmailsSince(accountId, sinceState) {
299
+ const changesResp = await this.jmapCall([
300
+ ["Email/changes", { accountId, sinceState, maxChanges: 50 }, "c1"]
301
+ ]);
302
+ const changesResult = changesResp.methodResponses.find(([name]) => name === "Email/changes");
303
+ if (!changesResult || changesResult[0] === "error") {
304
+ await this.initEmailState(accountId);
305
+ return [];
306
+ }
307
+ const changes = changesResult[1];
308
+ if (changes.newState) {
309
+ this.emailState = changes.newState;
310
+ }
311
+ const newIds = changes.created ?? [];
312
+ if (newIds.length === 0)
313
+ return [];
314
+ const emailResp = await this.jmapCall([
315
+ [
316
+ "Email/get",
317
+ {
318
+ accountId,
319
+ ids: newIds,
320
+ properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
321
+ fetchTextBodyValues: true,
322
+ maxBodyValueBytes: 262144
323
+ },
324
+ "g1"
325
+ ]
326
+ ]);
327
+ const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
328
+ if (!getResult)
329
+ return [];
330
+ const data = getResult[1];
331
+ return data.list ?? [];
332
+ }
333
+ /**
334
+ * Process a received email.
335
+ *
336
+ * Priority:
337
+ * 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.result / task.help)
338
+ * 2. If In-Reply-To is present → emit 'reply' event so the application layer can
339
+ * resolve the thread (inReplyTo → taskId via Redis/DB) and handle human replies.
340
+ * 3. Otherwise → ignore (not an AAMP-related email)
341
+ */
342
+ processEmail(email) {
343
+ const headerMap = {};
344
+ for (const h of email.headers ?? []) {
345
+ headerMap[h.name.toLowerCase()] = h.value.trim();
346
+ }
347
+ const fromAddr = email.from?.[0]?.email ?? "";
348
+ const toAddr = email.to?.[0]?.email ?? "";
349
+ const messageId = email.messageId?.[0] ?? email.id;
350
+ if (this.seenMessageIds.has(messageId))
351
+ return;
352
+ this.seenMessageIds.add(messageId);
353
+ const msg = parseAampHeaders({
354
+ from: fromAddr,
355
+ to: toAddr,
356
+ messageId,
357
+ subject: email.subject ?? "",
358
+ headers: headerMap
359
+ });
360
+ if (msg && "intent" in msg) {
361
+ const aampTextPartId = email.textBody?.[0]?.partId;
362
+ const aampBodyText = aampTextPartId ? (email.bodyValues?.[aampTextPartId]?.value ?? "").trim() : "";
363
+ msg.bodyText = aampBodyText;
364
+ const receivedAttachments = (email.attachments ?? []).map((a) => ({
365
+ filename: a.name ?? "attachment",
366
+ contentType: a.type,
367
+ size: a.size,
368
+ blobId: a.blobId
369
+ }));
370
+ if (receivedAttachments.length > 0) {
371
+ ;
372
+ msg.attachments = receivedAttachments;
373
+ }
374
+ if (msg.intent === "task.dispatch") {
375
+ this.emit("_autoAck", { to: fromAddr, taskId: msg.taskId, messageId });
376
+ }
377
+ this.emit(msg.intent, msg);
378
+ return;
379
+ }
380
+ const rawInReplyTo = headerMap["in-reply-to"] ?? "";
381
+ if (!rawInReplyTo)
382
+ return;
383
+ const rawReferences = headerMap["references"] ?? "";
384
+ const referencesIds = rawReferences.split(/\s+/).map((s) => s.replace(/[<>]/g, "").trim()).filter(Boolean);
385
+ const inReplyTo = rawInReplyTo.replace(/[<>]/g, "").trim();
386
+ const textPartId = email.textBody?.[0]?.partId;
387
+ const bodyText = textPartId ? (email.bodyValues?.[textPartId]?.value ?? "").trim() : "";
388
+ const reply = {
389
+ inReplyTo,
390
+ messageId,
391
+ from: fromAddr,
392
+ to: toAddr,
393
+ subject: email.subject ?? "",
394
+ bodyText
395
+ };
396
+ if (referencesIds.length > 0) {
397
+ Object.assign(reply, { references: referencesIds });
398
+ }
399
+ this.emit("reply", reply);
400
+ }
401
+ async fetchRecentEmails(accountId) {
402
+ const queryResp = await this.jmapCall([
403
+ [
404
+ "Email/query",
405
+ {
406
+ accountId,
407
+ sort: [{ property: "receivedAt", isAscending: false }],
408
+ limit: 20
409
+ },
410
+ "q1"
411
+ ]
412
+ ]);
413
+ const queryResult = queryResp.methodResponses.find(([name]) => name === "Email/query");
414
+ if (!queryResult)
415
+ return [];
416
+ const ids = (queryResult[1].ids ?? []).slice(0, 20);
417
+ if (ids.length === 0)
418
+ return [];
419
+ const emailResp = await this.jmapCall([
420
+ [
421
+ "Email/get",
422
+ {
423
+ accountId,
424
+ ids,
425
+ properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
426
+ fetchTextBodyValues: true,
427
+ maxBodyValueBytes: 262144
428
+ },
429
+ "gRecent"
430
+ ]
431
+ ]);
432
+ const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
433
+ if (!getResult)
434
+ return [];
435
+ return getResult[1].list ?? [];
436
+ }
437
+ shouldProcessBootstrapEmail(email) {
438
+ const receivedAtMs = new Date(email.receivedAt).getTime();
439
+ return Number.isFinite(receivedAtMs) && receivedAtMs >= this.startedAtMs - 15e3;
440
+ }
441
+ /**
442
+ * Connect to JMAP WebSocket
443
+ */
444
+ async connect() {
445
+ if (this.connecting || !this.running)
446
+ return;
447
+ this.connecting = true;
448
+ try {
449
+ this.session = await this.fetchSession();
450
+ } catch (err) {
451
+ this.connecting = false;
452
+ this.emit("error", new Error(`Failed to get JMAP session: ${err.message}`));
453
+ this.startPolling("session fetch failed");
454
+ this.scheduleReconnect();
455
+ return;
456
+ }
457
+ const stalwartWsUrl = `${this.jmapUrl}/_jmap_ws`.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
458
+ this.ws = new WebSocket(stalwartWsUrl, "jmap", {
459
+ headers: {
460
+ Authorization: this.getAuthHeader()
461
+ },
462
+ perMessageDeflate: false,
463
+ rejectUnauthorized: this.rejectUnauthorized
464
+ });
465
+ this.ws.on("unexpected-response", (_req, res) => {
466
+ this.connecting = false;
467
+ const headerSummary = Object.entries(res.headers).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value ?? ""}`).join("; ");
468
+ this.startPolling(`websocket handshake failed: ${res.statusCode ?? "unknown"}`);
469
+ this.emit("error", new Error(`JMAP WebSocket handshake failed: ${res.statusCode ?? "unknown"} ${res.statusMessage ?? ""}${headerSummary ? ` | headers: ${headerSummary}` : ""}`));
470
+ this.scheduleReconnect();
471
+ });
472
+ this.ws.on("open", async () => {
473
+ this.connecting = false;
474
+ this.connected = true;
475
+ this.stopPolling();
476
+ this.startPingHeartbeat();
477
+ const accountId = this.session?.primaryAccounts["urn:ietf:params:jmap:mail"];
478
+ if (accountId && this.emailState === null) {
479
+ await this.initEmailState(accountId);
480
+ }
481
+ this.ws.send(JSON.stringify({
482
+ "@type": "WebSocketPushEnable",
483
+ dataTypes: ["Email"],
484
+ pushState: null
485
+ }));
486
+ this.emit("connected");
487
+ });
488
+ this.ws.on("pong", () => {
489
+ });
490
+ this.ws.on("message", async (rawData) => {
491
+ try {
492
+ const msg = JSON.parse(rawData.toString());
493
+ if (msg["@type"] === "StateChange") {
494
+ await this.handleStateChange(msg);
495
+ }
496
+ } catch (err) {
497
+ this.emit("error", new Error(`Failed to process JMAP push message: ${err.message}`));
498
+ }
499
+ });
500
+ this.ws.on("close", (code, reason) => {
501
+ this.connecting = false;
502
+ this.connected = false;
503
+ this.stopPingHeartbeat();
504
+ const reasonStr = reason?.toString() ?? "connection closed";
505
+ this.startPolling(reasonStr);
506
+ this.emit("disconnected", reasonStr);
507
+ if (this.running) {
508
+ this.scheduleReconnect();
509
+ }
510
+ });
511
+ this.ws.on("error", (err) => {
512
+ this.connecting = false;
513
+ this.stopPingHeartbeat();
514
+ this.startPolling(err.message);
515
+ this.emit("error", err);
516
+ });
517
+ }
518
+ startPingHeartbeat() {
519
+ if (this.pingTimer) {
520
+ clearInterval(this.pingTimer);
521
+ this.pingTimer = null;
522
+ }
523
+ this.pingTimer = setInterval(() => {
524
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
525
+ return;
526
+ try {
527
+ this.ws.ping();
528
+ } catch (err) {
529
+ this.emit("error", new Error(`Failed to send WebSocket ping: ${err.message}`));
530
+ }
531
+ }, this.pingIntervalMs);
532
+ }
533
+ stopPingHeartbeat() {
534
+ if (this.pingTimer) {
535
+ clearInterval(this.pingTimer);
536
+ this.pingTimer = null;
537
+ }
538
+ }
539
+ async handleStateChange(stateChange) {
540
+ if (!this.session)
541
+ return;
542
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"];
543
+ if (!accountId)
544
+ return;
545
+ const changedAccount = stateChange.changed[accountId];
546
+ if (!changedAccount?.Email)
547
+ return;
548
+ try {
549
+ if (this.emailState === null) {
550
+ await this.initEmailState(accountId);
551
+ return;
552
+ }
553
+ const emails = await this.fetchEmailsSince(accountId, this.emailState);
554
+ for (const email of emails) {
555
+ this.processEmail(email);
556
+ }
557
+ } catch (err) {
558
+ this.emit("error", new Error(`Failed to fetch emails: ${err.message}`));
559
+ }
560
+ }
561
+ scheduleReconnect() {
562
+ if (this.reconnectTimer)
563
+ return;
564
+ this.reconnectTimer = setTimeout(async () => {
565
+ this.reconnectTimer = null;
566
+ if (this.running) {
567
+ await this.connect();
568
+ }
569
+ }, this.reconnectInterval);
570
+ }
571
+ isConnected() {
572
+ return this.connected || this.pollingActive;
573
+ }
574
+ isUsingPollingFallback() {
575
+ return this.pollingActive && !this.connected;
576
+ }
577
+ stopPolling() {
578
+ if (this.pollTimer) {
579
+ clearTimeout(this.pollTimer);
580
+ this.pollTimer = null;
581
+ }
582
+ this.pollingActive = false;
583
+ }
584
+ startPolling(reason) {
585
+ if (!this.running || this.pollingActive)
586
+ return;
587
+ this.pollingActive = true;
588
+ this.emit("error", new Error(`JMAP WebSocket unavailable, falling back to polling: ${reason}`));
589
+ this.emit("connected");
590
+ const poll = async () => {
591
+ if (!this.running || this.connected) {
592
+ this.stopPolling();
593
+ return;
594
+ }
595
+ try {
596
+ if (!this.session) {
597
+ this.session = await this.fetchSession();
598
+ }
599
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
600
+ if (!accountId) {
601
+ throw new Error("No mail account available in JMAP session");
602
+ }
603
+ if (this.emailState === null) {
604
+ const recentEmails = await this.fetchRecentEmails(accountId);
605
+ for (const email of recentEmails.sort((a, b) => {
606
+ const aTs = new Date(a.receivedAt).getTime();
607
+ const bTs = new Date(b.receivedAt).getTime();
608
+ return aTs - bTs;
609
+ })) {
610
+ if (!this.shouldProcessBootstrapEmail(email))
611
+ continue;
612
+ this.processEmail(email);
613
+ }
614
+ await this.initEmailState(accountId);
615
+ } else {
616
+ const emails = await this.fetchEmailsSince(accountId, this.emailState);
617
+ for (const email of emails) {
618
+ this.processEmail(email);
619
+ }
620
+ }
621
+ } catch (err) {
622
+ this.emit("error", new Error(`Polling fallback failed: ${err.message}`));
623
+ } finally {
624
+ if (this.running && !this.connected) {
625
+ this.pollTimer = setTimeout(poll, this.reconnectInterval);
626
+ }
627
+ }
628
+ };
629
+ this.pollTimer = setTimeout(poll, 0);
630
+ }
631
+ /**
632
+ * Download a blob (attachment) by its JMAP blobId.
633
+ * Returns the raw binary content as a Buffer.
634
+ */
635
+ async downloadBlob(blobId, filename) {
636
+ if (!this.session) {
637
+ this.session = await this.fetchSession();
638
+ }
639
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
640
+ let downloadUrl = this.session.downloadUrl ?? `${this.jmapUrl}/jmap/download/{accountId}/{blobId}/{name}`;
641
+ try {
642
+ const parsed = new URL(downloadUrl);
643
+ const configured = new URL(this.jmapUrl);
644
+ parsed.protocol = configured.protocol;
645
+ parsed.host = configured.host;
646
+ downloadUrl = parsed.toString();
647
+ } catch {
648
+ }
649
+ const safeFilename = filename ?? "attachment";
650
+ downloadUrl = downloadUrl.replace(/\{accountId\}|%7BaccountId%7D/gi, encodeURIComponent(accountId)).replace(/\{blobId\}|%7BblobId%7D/gi, encodeURIComponent(blobId)).replace(/\{name\}|%7Bname%7D/gi, encodeURIComponent(safeFilename)).replace(/\{type\}|%7Btype%7D/gi, "application/octet-stream");
651
+ const maxAttempts = 8;
652
+ let lastStatus = null;
653
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
654
+ const res = await fetch(downloadUrl, {
655
+ headers: { Authorization: this.getAuthHeader() }
656
+ });
657
+ lastStatus = res.status;
658
+ if (res.ok) {
659
+ const arrayBuffer = await res.arrayBuffer();
660
+ return Buffer.from(arrayBuffer);
661
+ }
662
+ if (attempt < maxAttempts && (res.status === 404 || res.status === 429 || res.status === 503)) {
663
+ console.warn(`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
664
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
665
+ await new Promise((r) => setTimeout(r, delay));
666
+ continue;
667
+ }
668
+ console.error(`[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
669
+ throw new Error(`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
670
+ }
671
+ throw new Error(`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
672
+ }
673
+ /**
674
+ * Actively reconcile recent mailbox contents via JMAP HTTP.
675
+ * Useful as a safety net when the WebSocket stays "connected"
676
+ * but a notification is missed by an intermediate layer.
677
+ */
678
+ async reconcileRecentEmails(limit = 20) {
679
+ if (!this.session) {
680
+ this.session = await this.fetchSession();
681
+ }
682
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
683
+ if (!accountId) {
684
+ throw new Error("No mail account available in JMAP session");
685
+ }
686
+ const queryResp = await this.jmapCall([
687
+ [
688
+ "Email/query",
689
+ {
690
+ accountId,
691
+ sort: [{ property: "receivedAt", isAscending: false }],
692
+ limit
693
+ },
694
+ "qReconcile"
695
+ ]
696
+ ]);
697
+ const queryResult = queryResp.methodResponses.find(([name]) => name === "Email/query");
698
+ if (!queryResult)
699
+ return 0;
700
+ const ids = (queryResult[1].ids ?? []).slice(0, limit);
701
+ if (ids.length === 0)
702
+ return 0;
703
+ const emailResp = await this.jmapCall([
704
+ [
705
+ "Email/get",
706
+ {
707
+ accountId,
708
+ ids,
709
+ properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
710
+ fetchTextBodyValues: true,
711
+ maxBodyValueBytes: 262144
712
+ },
713
+ "gReconcile"
714
+ ]
715
+ ]);
716
+ const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
717
+ if (!getResult)
718
+ return 0;
719
+ const emails = getResult[1].list ?? [];
720
+ for (const email of emails.sort((a, b) => {
721
+ const aTs = new Date(a.receivedAt).getTime();
722
+ const bTs = new Date(b.receivedAt).getTime();
723
+ return aTs - bTs;
724
+ })) {
725
+ if (!this.shouldProcessBootstrapEmail(email))
726
+ continue;
727
+ this.processEmail(email);
728
+ }
729
+ return emails.length;
730
+ }
731
+ };
732
+
733
+ // node_modules/aamp-sdk/dist/smtp-sender.js
734
+ import { createTransport } from "nodemailer";
735
+ import { randomUUID } from "crypto";
736
+ var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
737
+ var SmtpSender = class {
738
+ config;
739
+ transport;
740
+ constructor(config) {
741
+ this.config = config;
742
+ this.transport = createTransport({
743
+ host: config.host,
744
+ port: config.port,
745
+ secure: config.secure ?? false,
746
+ auth: {
747
+ user: config.user,
748
+ pass: config.password
749
+ },
750
+ tls: {
751
+ rejectUnauthorized: config.rejectUnauthorized ?? true
752
+ }
753
+ });
754
+ }
755
+ senderDomain() {
756
+ return this.config.user.split("@")[1]?.toLowerCase() ?? "";
757
+ }
758
+ recipientDomain(email) {
759
+ return email.split("@")[1]?.toLowerCase() ?? "";
760
+ }
761
+ shouldUseHttpFallback(to) {
762
+ return Boolean(this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to));
763
+ }
764
+ async sendViaHttp(opts) {
765
+ const base = this.config.httpBaseUrl?.replace(/\/$/, "");
766
+ if (!base || !this.config.authToken) {
767
+ throw new Error("HTTP send fallback is not configured");
768
+ }
769
+ const res = await fetch(`${base}/api/send`, {
770
+ method: "POST",
771
+ headers: {
772
+ Authorization: `Basic ${this.config.authToken}`,
773
+ "Content-Type": "application/json"
774
+ },
775
+ body: JSON.stringify({
776
+ to: opts.to,
777
+ subject: opts.subject,
778
+ text: opts.text,
779
+ aampHeaders: opts.aampHeaders,
780
+ attachments: opts.attachments?.map((a) => ({
781
+ filename: a.filename,
782
+ contentType: a.contentType,
783
+ content: typeof a.content === "string" ? a.content : a.content.toString("base64")
784
+ }))
785
+ })
786
+ });
787
+ const data = await res.json().catch(() => ({}));
788
+ if (!res.ok) {
789
+ throw new Error(data.details || `HTTP send failed: ${res.status}`);
790
+ }
791
+ return { messageId: data.messageId };
792
+ }
793
+ /**
794
+ * Send a task.dispatch email.
795
+ * Returns both the generated taskId and the SMTP Message-ID so callers can
796
+ * store a reverse-index (messageId → taskId) for In-Reply-To thread routing.
797
+ */
798
+ async sendTask(opts) {
799
+ const taskId = randomUUID();
800
+ const aampHeaders = buildDispatchHeaders({
801
+ taskId,
802
+ timeoutSecs: opts.timeoutSecs,
803
+ contextLinks: opts.contextLinks ?? [],
804
+ parentTaskId: opts.parentTaskId
805
+ });
806
+ const sendMailOpts = {
807
+ from: this.config.user,
808
+ to: opts.to,
809
+ subject: `[AAMP Task] ${sanitize(opts.title)}`,
810
+ text: [
811
+ `Task: ${opts.title}`,
812
+ `Task ID: ${taskId}`,
813
+ opts.timeoutSecs ? `Deadline: ${opts.timeoutSecs}s` : `Deadline: none`,
814
+ opts.contextLinks?.length ? `Context:
815
+ ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
816
+ ``,
817
+ `--- This email was sent by AAMP. Reply directly to submit your result. ---`
818
+ ].filter(Boolean).join("\n"),
819
+ headers: aampHeaders
820
+ };
821
+ if (opts.attachments?.length) {
822
+ sendMailOpts.attachments = opts.attachments.map((a) => ({
823
+ filename: a.filename,
824
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
825
+ contentType: a.contentType
826
+ }));
827
+ }
828
+ if (this.shouldUseHttpFallback(opts.to)) {
829
+ const info2 = await this.sendViaHttp({
830
+ to: opts.to,
831
+ subject: sendMailOpts.subject,
832
+ text: sendMailOpts.text,
833
+ aampHeaders,
834
+ attachments: opts.attachments?.map((a) => ({
835
+ filename: a.filename,
836
+ contentType: a.contentType,
837
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
838
+ }))
839
+ });
840
+ return { taskId, messageId: info2.messageId ?? "" };
841
+ }
842
+ const info = await this.transport.sendMail(sendMailOpts);
843
+ return { taskId, messageId: info.messageId ?? "" };
844
+ }
845
+ /**
846
+ * Send a task.result email back to the dispatcher
847
+ */
848
+ async sendResult(opts) {
849
+ const aampHeaders = buildResultHeaders({
850
+ taskId: opts.taskId,
851
+ status: opts.status,
852
+ output: opts.output,
853
+ errorMsg: opts.errorMsg,
854
+ structuredResult: opts.structuredResult
855
+ });
856
+ const mailOpts = {
857
+ from: this.config.user,
858
+ to: opts.to,
859
+ subject: `[AAMP Result] Task ${opts.taskId} \u2014 ${opts.status}`,
860
+ text: [
861
+ `AAMP Task Result`,
862
+ ``,
863
+ `Task ID: ${opts.taskId}`,
864
+ `Status: ${opts.status}`,
865
+ ``,
866
+ `Output:`,
867
+ opts.output,
868
+ opts.errorMsg ? `
869
+ Error: ${opts.errorMsg}` : ""
870
+ ].filter((s) => s !== "").join("\n"),
871
+ headers: aampHeaders
872
+ };
873
+ if (opts.inReplyTo) {
874
+ mailOpts.inReplyTo = opts.inReplyTo;
875
+ mailOpts.references = opts.inReplyTo;
876
+ }
877
+ if (opts.attachments?.length) {
878
+ mailOpts.attachments = opts.attachments.map((a) => ({
879
+ filename: a.filename,
880
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
881
+ contentType: a.contentType
882
+ }));
883
+ }
884
+ if (this.shouldUseHttpFallback(opts.to)) {
885
+ await this.sendViaHttp({
886
+ to: opts.to,
887
+ subject: mailOpts.subject,
888
+ text: mailOpts.text,
889
+ aampHeaders,
890
+ attachments: opts.attachments?.map((a) => ({
891
+ filename: a.filename,
892
+ contentType: a.contentType,
893
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
894
+ }))
895
+ });
896
+ return;
897
+ }
898
+ await this.transport.sendMail(mailOpts);
899
+ }
900
+ /**
901
+ * Send a task.help email when the agent is blocked
902
+ */
903
+ async sendHelp(opts) {
904
+ const aampHeaders = buildHelpHeaders({
905
+ taskId: opts.taskId,
906
+ question: opts.question,
907
+ blockedReason: opts.blockedReason,
908
+ suggestedOptions: opts.suggestedOptions
909
+ });
910
+ const helpMailOpts = {
911
+ from: this.config.user,
912
+ to: opts.to,
913
+ subject: `[AAMP Help] Task ${opts.taskId} needs assistance`,
914
+ text: [
915
+ `AAMP Task Help Request`,
916
+ ``,
917
+ `Task ID: ${opts.taskId}`,
918
+ ``,
919
+ `Question: ${opts.question}`,
920
+ ``,
921
+ `Blocked reason: ${opts.blockedReason}`,
922
+ ``,
923
+ opts.suggestedOptions.length ? `Suggested options:
924
+ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
925
+ ].filter(Boolean).join("\n"),
926
+ headers: aampHeaders
927
+ };
928
+ if (opts.inReplyTo) {
929
+ helpMailOpts.inReplyTo = opts.inReplyTo;
930
+ helpMailOpts.references = opts.inReplyTo;
931
+ }
932
+ if (opts.attachments?.length) {
933
+ helpMailOpts.attachments = opts.attachments.map((a) => ({
934
+ filename: a.filename,
935
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
936
+ contentType: a.contentType
937
+ }));
938
+ }
939
+ if (this.shouldUseHttpFallback(opts.to)) {
940
+ await this.sendViaHttp({
941
+ to: opts.to,
942
+ subject: helpMailOpts.subject,
943
+ text: helpMailOpts.text,
944
+ aampHeaders,
945
+ attachments: opts.attachments?.map((a) => ({
946
+ filename: a.filename,
947
+ contentType: a.contentType,
948
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
949
+ }))
950
+ });
951
+ return;
952
+ }
953
+ await this.transport.sendMail(helpMailOpts);
954
+ }
955
+ /**
956
+ * Send a task.ack email to confirm receipt of a dispatch
957
+ */
958
+ async sendAck(opts) {
959
+ const aampHeaders = buildAckHeaders({ taskId: opts.taskId });
960
+ const mailOpts = {
961
+ from: this.config.user,
962
+ to: opts.to,
963
+ subject: `[AAMP ACK] Task ${opts.taskId}`,
964
+ text: "",
965
+ headers: aampHeaders
966
+ };
967
+ if (opts.inReplyTo) {
968
+ mailOpts.inReplyTo = opts.inReplyTo;
969
+ mailOpts.references = opts.inReplyTo;
970
+ }
971
+ if (this.shouldUseHttpFallback(opts.to)) {
972
+ await this.sendViaHttp({
973
+ to: opts.to,
974
+ subject: mailOpts.subject,
975
+ text: mailOpts.text,
976
+ aampHeaders
977
+ });
978
+ return;
979
+ }
980
+ await this.transport.sendMail(mailOpts);
981
+ }
982
+ /**
983
+ * Verify SMTP connection
984
+ */
985
+ async verify() {
986
+ try {
987
+ await this.transport.verify();
988
+ return true;
989
+ } catch {
990
+ return false;
991
+ }
992
+ }
993
+ close() {
994
+ this.transport.close();
995
+ }
996
+ };
997
+
998
+ // node_modules/aamp-sdk/dist/client.js
999
+ var AampClient = class extends EventEmitter2 {
1000
+ jmapClient;
1001
+ smtpSender;
1002
+ config;
1003
+ constructor(config) {
1004
+ super();
1005
+ this.config = config;
1006
+ let password;
1007
+ try {
1008
+ const decoded = Buffer.from(config.jmapToken, "base64").toString("utf-8");
1009
+ const colonIdx = decoded.indexOf(":");
1010
+ if (colonIdx < 0)
1011
+ throw new Error("Invalid jmapToken format: expected base64(email:password)");
1012
+ password = decoded.slice(colonIdx + 1);
1013
+ if (!password)
1014
+ throw new Error("Invalid jmapToken: empty password");
1015
+ } catch (err) {
1016
+ if (err instanceof Error && err.message.startsWith("Invalid jmapToken"))
1017
+ throw err;
1018
+ throw new Error(`Failed to decode jmapToken: ${err.message}`);
1019
+ }
1020
+ this.jmapClient = new JmapPushClient({
1021
+ email: config.email,
1022
+ password: password ?? config.smtpPassword,
1023
+ jmapUrl: config.jmapUrl,
1024
+ reconnectInterval: config.reconnectInterval ?? 5e3,
1025
+ rejectUnauthorized: config.rejectUnauthorized
1026
+ });
1027
+ this.smtpSender = new SmtpSender({
1028
+ host: config.smtpHost,
1029
+ port: config.smtpPort ?? 587,
1030
+ user: config.email,
1031
+ password: config.smtpPassword,
1032
+ httpBaseUrl: config.httpSendBaseUrl ?? config.jmapUrl,
1033
+ authToken: config.jmapToken,
1034
+ rejectUnauthorized: config.rejectUnauthorized
1035
+ });
1036
+ this.jmapClient.on("task.dispatch", (task) => {
1037
+ this.emit("task.dispatch", task);
1038
+ });
1039
+ this.jmapClient.on("task.result", (result) => {
1040
+ this.emit("task.result", result);
1041
+ });
1042
+ this.jmapClient.on("task.help", (help) => {
1043
+ this.emit("task.help", help);
1044
+ });
1045
+ this.jmapClient.on("task.ack", (ack) => {
1046
+ this.emit("task.ack", ack);
1047
+ });
1048
+ this.jmapClient.on("_autoAck", async ({ to, taskId, messageId }) => {
1049
+ try {
1050
+ await this.smtpSender.sendAck({ to, taskId, inReplyTo: messageId });
1051
+ } catch (err) {
1052
+ console.warn(`[AAMP] Failed to send ACK for task ${taskId}: ${err.message}`);
1053
+ }
1054
+ });
1055
+ this.jmapClient.on("reply", (reply) => {
1056
+ this.emit("reply", reply);
1057
+ });
1058
+ this.jmapClient.on("connected", () => {
1059
+ this.emit("connected");
1060
+ });
1061
+ this.jmapClient.on("disconnected", (reason) => {
1062
+ this.emit("disconnected", reason);
1063
+ });
1064
+ this.jmapClient.on("error", (err) => {
1065
+ this.emit("error", err);
1066
+ });
1067
+ }
1068
+ // =====================================================
1069
+ // Type-safe event emitter methods
1070
+ // =====================================================
1071
+ on(event, listener) {
1072
+ return super.on(event, listener);
1073
+ }
1074
+ once(event, listener) {
1075
+ return super.once(event, listener);
1076
+ }
1077
+ off(event, listener) {
1078
+ return super.off(event, listener);
1079
+ }
1080
+ // =====================================================
1081
+ // Lifecycle
1082
+ // =====================================================
1083
+ /**
1084
+ * Connect to JMAP and start listening for tasks
1085
+ */
1086
+ async connect() {
1087
+ await this.jmapClient.start();
1088
+ }
1089
+ /**
1090
+ * Disconnect and clean up
1091
+ */
1092
+ disconnect() {
1093
+ this.jmapClient.stop();
1094
+ this.smtpSender.close();
1095
+ }
1096
+ /**
1097
+ * Returns true if the JMAP connection is active
1098
+ */
1099
+ isConnected() {
1100
+ return this.jmapClient.isConnected();
1101
+ }
1102
+ isUsingPollingFallback() {
1103
+ return this.jmapClient.isUsingPollingFallback();
1104
+ }
1105
+ // =====================================================
1106
+ // Sending
1107
+ // =====================================================
1108
+ /**
1109
+ * Send a task.dispatch email to an agent.
1110
+ * Returns the generated taskId and the SMTP Message-ID.
1111
+ * Store messageId → taskId in Redis/DB to support In-Reply-To thread routing
1112
+ * for human replies that arrive without X-AAMP headers.
1113
+ */
1114
+ async sendTask(opts) {
1115
+ return this.smtpSender.sendTask(opts);
1116
+ }
1117
+ /**
1118
+ * Send a task.result email (agent → system/dispatcher)
1119
+ */
1120
+ async sendResult(opts) {
1121
+ return this.smtpSender.sendResult(opts);
1122
+ }
1123
+ /**
1124
+ * Send a task.help email when the agent needs human assistance
1125
+ */
1126
+ async sendHelp(opts) {
1127
+ return this.smtpSender.sendHelp(opts);
1128
+ }
1129
+ /**
1130
+ * Download a blob (attachment) by its JMAP blobId.
1131
+ * Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
1132
+ * Returns the raw binary content as a Buffer.
1133
+ */
1134
+ async downloadBlob(blobId, filename) {
1135
+ return this.jmapClient.downloadBlob(blobId, filename);
1136
+ }
1137
+ /**
1138
+ * Reconcile recent mailbox contents via JMAP HTTP to catch messages missed by
1139
+ * a flaky WebSocket path. Safe to call periodically; duplicate processing is
1140
+ * suppressed by the JMAP push client.
1141
+ */
1142
+ async reconcileRecentEmails(limit) {
1143
+ return this.jmapClient.reconcileRecentEmails(limit);
1144
+ }
1145
+ /**
1146
+ * Verify SMTP connectivity
1147
+ */
1148
+ async verifySmtp() {
1149
+ return this.smtpSender.verify();
1150
+ }
1151
+ get email() {
1152
+ return this.config.email;
1153
+ }
1154
+ };
1155
+
1156
+ // src/index.ts
1157
+ function baseUrl(aampHost) {
1158
+ if (aampHost.startsWith("http://") || aampHost.startsWith("https://")) {
1159
+ return aampHost.replace(/\/$/, "");
1160
+ }
1161
+ return `https://${aampHost}`;
1162
+ }
1163
+ var pendingTasks = /* @__PURE__ */ new Map();
1164
+ var dispatchedSubtasks = /* @__PURE__ */ new Map();
1165
+ var waitingDispatches = /* @__PURE__ */ new Map();
1166
+ var aampClient = null;
1167
+ var agentEmail = "";
1168
+ var lastConnectionError = "";
1169
+ var lastDisconnectReason = "";
1170
+ var lastTransportMode = "disconnected";
1171
+ var reconcileTimer = null;
1172
+ var currentSessionKey = "agent:main:main";
1173
+ var channelRuntime = null;
1174
+ var channelCfg = null;
1175
+ function defaultCredentialsPath() {
1176
+ return join(homedir(), ".openclaw", "extensions", "aamp", ".credentials.json");
1177
+ }
1178
+ function loadCachedIdentity(cfg) {
1179
+ const file = cfg.credentialsFile ?? defaultCredentialsPath();
1180
+ if (!existsSync(file))
1181
+ return null;
1182
+ try {
1183
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
1184
+ if (!parsed.email || !parsed.jmapToken || !parsed.smtpPassword) {
1185
+ return null;
1186
+ }
1187
+ return parsed;
1188
+ } catch {
1189
+ return null;
1190
+ }
1191
+ }
1192
+ function saveCachedIdentity(cfg, identity) {
1193
+ const file = cfg.credentialsFile ?? defaultCredentialsPath();
1194
+ mkdirSync(dirname(file), { recursive: true });
1195
+ writeFileSync(file, JSON.stringify(identity, null, 2), "utf-8");
1196
+ }
1197
+ async function registerNode(cfg) {
1198
+ const slug = (cfg.slug ?? "openclaw-agent").toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "");
1199
+ const base = baseUrl(cfg.aampHost);
1200
+ const res = await fetch(`${base}/api/nodes/self-register`, {
1201
+ method: "POST",
1202
+ headers: { "Content-Type": "application/json" },
1203
+ body: JSON.stringify({ slug, description: "OpenClaw AAMP agent node" })
1204
+ });
1205
+ if (!res.ok) {
1206
+ const err = await res.json().catch(() => ({}));
1207
+ throw new Error(`AAMP registration failed (${res.status}): ${err.error ?? res.statusText}`);
1208
+ }
1209
+ const regData = await res.json();
1210
+ const credRes = await fetch(
1211
+ `${base}/api/nodes/credentials?code=${encodeURIComponent(regData.registrationCode)}`
1212
+ );
1213
+ if (!credRes.ok) {
1214
+ const err = await credRes.json().catch(() => ({}));
1215
+ throw new Error(`AAMP credential exchange failed (${credRes.status}): ${err.error ?? credRes.statusText}`);
1216
+ }
1217
+ const credData = await credRes.json();
1218
+ return {
1219
+ email: credData.email,
1220
+ jmapToken: credData.jmap.token,
1221
+ smtpPassword: credData.smtp.password
1222
+ };
1223
+ }
1224
+ async function resolveIdentity(cfg) {
1225
+ const cached = loadCachedIdentity(cfg);
1226
+ if (cached)
1227
+ return cached;
1228
+ const identity = await registerNode(cfg);
1229
+ saveCachedIdentity(cfg, identity);
1230
+ return identity;
1231
+ }
1232
+ var src_default = {
1233
+ id: "aamp",
1234
+ name: "AAMP Agent Mail Protocol",
1235
+ configSchema: {
1236
+ type: "object",
1237
+ properties: {
1238
+ aampHost: {
1239
+ type: "string",
1240
+ description: "AAMP service host, e.g. https://meshmail.ai"
1241
+ },
1242
+ slug: {
1243
+ type: "string",
1244
+ default: "openclaw-agent",
1245
+ description: "Agent name prefix used in the mailbox address"
1246
+ },
1247
+ credentialsFile: {
1248
+ type: "string",
1249
+ description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp/.credentials.json. Delete this file to force re-registration with a new mailbox."
1250
+ },
1251
+ allowedSenders: {
1252
+ type: "array",
1253
+ items: { type: "string" },
1254
+ description: 'Sender whitelist. Only task.dispatch emails from these addresses are accepted. Matching is case-insensitive and exact. If omitted, all senders are accepted. If set to [], all senders are rejected. Example: ["meego-abc123@aamp.local", "ci-bot-ff09@aamp.local"]'
1255
+ }
1256
+ }
1257
+ },
1258
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1259
+ register(api) {
1260
+ const cfg = api.pluginConfig ?? {};
1261
+ api.registerChannel({
1262
+ id: "aamp",
1263
+ meta: { label: "AAMP" },
1264
+ capabilities: { chatTypes: ["dm"] },
1265
+ config: {
1266
+ listAccountIds: () => cfg.aampHost ? ["default"] : [],
1267
+ resolveAccount: () => ({ aampHost: cfg.aampHost }),
1268
+ isEnabled: () => !!cfg.aampHost,
1269
+ isConfigured: () => !!loadCachedIdentity(cfg)
1270
+ },
1271
+ gateway: {
1272
+ startAccount: async (ctx) => {
1273
+ channelRuntime = ctx.channelRuntime ?? null;
1274
+ channelCfg = ctx.cfg ?? null;
1275
+ api.logger.info(`[AAMP] Channel adapter started \u2014 channelRuntime ${channelRuntime ? "available" : "NOT available"}`);
1276
+ await new Promise((resolve) => {
1277
+ ctx.abortSignal?.addEventListener("abort", () => resolve());
1278
+ });
1279
+ channelRuntime = null;
1280
+ channelCfg = null;
1281
+ },
1282
+ stopAccount: async () => {
1283
+ channelRuntime = null;
1284
+ channelCfg = null;
1285
+ }
1286
+ }
1287
+ });
1288
+ async function doConnect(identity) {
1289
+ if (reconcileTimer) {
1290
+ clearInterval(reconcileTimer);
1291
+ reconcileTimer = null;
1292
+ }
1293
+ agentEmail = identity.email;
1294
+ lastConnectionError = "";
1295
+ lastDisconnectReason = "";
1296
+ const base = baseUrl(cfg.aampHost);
1297
+ aampClient = new AampClient({
1298
+ email: identity.email,
1299
+ jmapToken: identity.jmapToken,
1300
+ jmapUrl: base,
1301
+ smtpHost: new URL(base).hostname,
1302
+ smtpPort: 587,
1303
+ smtpPassword: identity.smtpPassword,
1304
+ // Local/dev: management-service proxy uses plain HTTP, no TLS cert to verify.
1305
+ // Production: set to true when using wss:// with valid certs.
1306
+ rejectUnauthorized: false
1307
+ });
1308
+ aampClient.on("task.dispatch", (task) => {
1309
+ api.logger.info(`[AAMP] \u2190 task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
1310
+ const allowed = cfg.allowedSenders;
1311
+ if (allowed !== void 0) {
1312
+ const fromLower = task.from.toLowerCase();
1313
+ const permitted = allowed.some((e) => e.toLowerCase() === fromLower);
1314
+ if (!permitted) {
1315
+ api.logger.warn(`[AAMP] \u2717 rejected (not in allowedSenders): ${task.from} task=${task.taskId}`);
1316
+ void aampClient.sendResult({
1317
+ to: task.from,
1318
+ taskId: task.taskId,
1319
+ status: "rejected",
1320
+ output: "",
1321
+ errorMsg: `Sender ${task.from} is not in the allowed senders list.`
1322
+ }).catch((err) => {
1323
+ api.logger.error(`[AAMP] Failed to send rejection for task ${task.taskId}: ${err.message}`);
1324
+ });
1325
+ return;
1326
+ }
1327
+ }
1328
+ pendingTasks.set(task.taskId, {
1329
+ taskId: task.taskId,
1330
+ from: task.from,
1331
+ title: task.title,
1332
+ bodyText: task.bodyText ?? "",
1333
+ contextLinks: task.contextLinks,
1334
+ timeoutSecs: task.timeoutSecs,
1335
+ messageId: task.messageId ?? "",
1336
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1337
+ });
1338
+ try {
1339
+ api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: currentSessionKey });
1340
+ api.logger.info(`[AAMP] Heartbeat triggered for session ${currentSessionKey}`);
1341
+ } catch (err) {
1342
+ api.logger.warn(`[AAMP] Could not trigger heartbeat: ${err.message}`);
1343
+ }
1344
+ });
1345
+ aampClient.on("task.result", (result) => {
1346
+ api.logger.info(`[AAMP] \u2190 task.result ${result.taskId} status=${result.status} from=${result.from}`);
1347
+ const sub = dispatchedSubtasks.get(result.taskId);
1348
+ dispatchedSubtasks.delete(result.taskId);
1349
+ const waiter = waitingDispatches.get(result.taskId);
1350
+ if (waiter) {
1351
+ waitingDispatches.delete(result.taskId);
1352
+ api.logger.info(`[AAMP] Resolving sync waiter for sub-task ${result.taskId}`);
1353
+ waiter({ type: "result", data: result });
1354
+ return;
1355
+ }
1356
+ const downloadedFiles = [];
1357
+ const downloadPromise = (async () => {
1358
+ if (!result.attachments?.length)
1359
+ return;
1360
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
1361
+ const dir = "/tmp/aamp-files";
1362
+ mkdirSync2(dir, { recursive: true });
1363
+ for (const att of result.attachments) {
1364
+ try {
1365
+ const buffer = await aampClient.downloadBlob(att.blobId, att.filename);
1366
+ const filepath = `${dir}/${att.filename}`;
1367
+ writeFileSync2(filepath, buffer);
1368
+ downloadedFiles.push({ filename: att.filename, path: filepath, size: buffer.length });
1369
+ api.logger.info(`[AAMP] Pre-downloaded: ${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
1370
+ } catch (dlErr) {
1371
+ api.logger.warn(`[AAMP] Pre-download failed for ${att.filename}: ${dlErr.message}`);
1372
+ }
1373
+ }
1374
+ })();
1375
+ downloadPromise.then(() => {
1376
+ const MAX_OUTPUT_CHARS = 800;
1377
+ const label = result.status === "completed" ? "Sub-task completed" : "Sub-task rejected";
1378
+ const rawOutput = result.output ?? "";
1379
+ const truncatedOutput = rawOutput.length > MAX_OUTPUT_CHARS ? rawOutput.slice(0, MAX_OUTPUT_CHARS) + `
1380
+
1381
+ ... [truncated, ${rawOutput.length} chars total]` : rawOutput;
1382
+ let attachmentInfo = "";
1383
+ if (downloadedFiles.length > 0) {
1384
+ attachmentInfo = `
1385
+
1386
+ Attachments (pre-downloaded to local disk):
1387
+ ${downloadedFiles.map(
1388
+ (f) => `- ${f.filename} (${(f.size / 1024).toFixed(1)} KB) \u2192 ${f.path}`
1389
+ ).join("\n")}
1390
+ Use aamp_send_result with attachments: [${downloadedFiles.map((f) => `{ filename: "${f.filename}", path: "${f.path}" }`).join(", ")}] to forward them.`;
1391
+ } else if (result.attachments?.length) {
1392
+ const files = result.attachments.map(
1393
+ (a) => `${a.filename} (${(a.size / 1024).toFixed(1)} KB, blobId: ${a.blobId})`
1394
+ );
1395
+ attachmentInfo = `
1396
+
1397
+ Attachments (download failed \u2014 use aamp_download_attachment manually):
1398
+ ${files.join("\n")}`;
1399
+ }
1400
+ pendingTasks.set(`result:${result.taskId}`, {
1401
+ taskId: result.taskId,
1402
+ from: result.from,
1403
+ title: `${label}: ${sub?.title ?? result.taskId}`,
1404
+ bodyText: result.status === "completed" ? `Agent ${result.from} completed the sub-task.
1405
+
1406
+ Output:
1407
+ ${truncatedOutput}${attachmentInfo}` : `Agent ${result.from} rejected the sub-task.
1408
+
1409
+ Reason: ${result.errorMsg ?? "unknown"}`,
1410
+ contextLinks: [],
1411
+ timeoutSecs: 0,
1412
+ messageId: "",
1413
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1414
+ });
1415
+ if (channelRuntime && channelCfg) {
1416
+ const notifyBody = pendingTasks.get(`result:${result.taskId}`);
1417
+ const actionableTasks = [...pendingTasks.entries()].filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:")).map(([, t]) => t);
1418
+ const actionSection = actionableTasks.length > 0 ? `
1419
+
1420
+ ### Action Required
1421
+ You MUST call aamp_send_result to complete the pending task(s):
1422
+ ${actionableTasks.map((t) => `- Task ID: ${t.taskId} | From: ${t.from} | Title: "${t.title}"`).join("\n")}` : "";
1423
+ const prompt = `## Sub-task Update
1424
+
1425
+ ${notifyBody?.bodyText ?? "Sub-task completed."}${actionSection}`;
1426
+ channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
1427
+ ctx: {
1428
+ Body: `Sub-task result: ${result.taskId}`,
1429
+ BodyForAgent: prompt,
1430
+ From: result.from,
1431
+ To: agentEmail,
1432
+ SessionKey: `aamp:default:${result.from}`,
1433
+ AccountId: "default",
1434
+ ChatType: "dm",
1435
+ Provider: "aamp",
1436
+ Surface: "aamp",
1437
+ OriginatingChannel: "aamp",
1438
+ OriginatingTo: result.from,
1439
+ MessageSid: result.taskId,
1440
+ Timestamp: Date.now(),
1441
+ SenderName: result.from,
1442
+ SenderId: result.from,
1443
+ CommandAuthorized: true
1444
+ },
1445
+ cfg: channelCfg,
1446
+ dispatcherOptions: {
1447
+ deliver: async () => {
1448
+ },
1449
+ onError: (err) => {
1450
+ api.logger.error(`[AAMP] Channel dispatch error: ${err instanceof Error ? err.message : String(err)}`);
1451
+ }
1452
+ }
1453
+ }).then(() => {
1454
+ api.logger.info(`[AAMP] Channel dispatch completed for sub-task result ${result.taskId}`);
1455
+ pendingTasks.delete(`result:${result.taskId}`);
1456
+ }).catch((err) => {
1457
+ api.logger.error(`[AAMP] Channel dispatch failed: ${err.message}`);
1458
+ });
1459
+ } else {
1460
+ const notifySessionKey = `agent:main:aamp-notify-${Date.now()}`;
1461
+ try {
1462
+ api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: notifySessionKey });
1463
+ api.logger.info(`[AAMP] Heartbeat for sub-task result ${result.taskId}`);
1464
+ } catch (err) {
1465
+ api.logger.warn(`[AAMP] Heartbeat for sub-task result failed: ${err.message}`);
1466
+ }
1467
+ }
1468
+ }).catch((err) => {
1469
+ api.logger.error(`[AAMP] Sub-task result processing failed: ${err.message}`);
1470
+ });
1471
+ });
1472
+ aampClient.on("task.help", (help) => {
1473
+ api.logger.info(`[AAMP] \u2190 task.help ${help.taskId} question="${help.question}" from=${help.from}`);
1474
+ const waiter = waitingDispatches.get(help.taskId);
1475
+ if (waiter) {
1476
+ waitingDispatches.delete(help.taskId);
1477
+ api.logger.info(`[AAMP] Resolving sync waiter for sub-task help ${help.taskId}`);
1478
+ waiter({ type: "help", data: help });
1479
+ return;
1480
+ }
1481
+ const sub = dispatchedSubtasks.get(help.taskId);
1482
+ pendingTasks.set(`help:${help.taskId}`, {
1483
+ taskId: help.taskId,
1484
+ from: help.from,
1485
+ title: `Sub-task needs help: ${sub?.title ?? help.taskId}`,
1486
+ bodyText: `Agent ${help.from} is asking for help on the sub-task.
1487
+
1488
+ Question: ${help.question}
1489
+ Blocked reason: ${help.blockedReason}${help.suggestedOptions?.length ? `
1490
+ Suggested options: ${help.suggestedOptions.join(", ")}` : ""}`,
1491
+ contextLinks: [],
1492
+ timeoutSecs: 0,
1493
+ messageId: "",
1494
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1495
+ });
1496
+ if (channelRuntime && channelCfg) {
1497
+ const notifyBody = pendingTasks.get(`help:${help.taskId}`);
1498
+ const prompt = `## Sub-task Help Request
1499
+
1500
+ ${notifyBody?.bodyText ?? help.question}`;
1501
+ channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
1502
+ ctx: {
1503
+ Body: `Sub-task help: ${help.taskId}`,
1504
+ BodyForAgent: prompt,
1505
+ From: help.from,
1506
+ To: agentEmail,
1507
+ SessionKey: `aamp:default:${help.from}`,
1508
+ AccountId: "default",
1509
+ ChatType: "dm",
1510
+ Provider: "aamp",
1511
+ Surface: "aamp",
1512
+ OriginatingChannel: "aamp",
1513
+ OriginatingTo: help.from,
1514
+ MessageSid: help.taskId,
1515
+ Timestamp: Date.now(),
1516
+ SenderName: help.from,
1517
+ SenderId: help.from,
1518
+ CommandAuthorized: true
1519
+ },
1520
+ cfg: channelCfg,
1521
+ dispatcherOptions: {
1522
+ deliver: async () => {
1523
+ },
1524
+ onError: (err) => {
1525
+ api.logger.error(`[AAMP] Channel dispatch error (help): ${err instanceof Error ? err.message : String(err)}`);
1526
+ }
1527
+ }
1528
+ }).then(() => {
1529
+ pendingTasks.delete(`help:${help.taskId}`);
1530
+ }).catch((err) => {
1531
+ api.logger.error(`[AAMP] Channel dispatch failed for help: ${err.message}`);
1532
+ });
1533
+ } else {
1534
+ const helpSessionKey = `agent:main:aamp-notify-${Date.now()}`;
1535
+ try {
1536
+ api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: helpSessionKey });
1537
+ api.logger.info(`[AAMP] Heartbeat fallback for sub-task help ${help.taskId}`);
1538
+ } catch (err) {
1539
+ api.logger.warn(`[AAMP] Heartbeat for sub-task help failed: ${err.message}`);
1540
+ }
1541
+ }
1542
+ });
1543
+ aampClient.on("connected", () => {
1544
+ lastConnectionError = "";
1545
+ lastDisconnectReason = "";
1546
+ const mode = aampClient?.isUsingPollingFallback() ? "polling" : "websocket";
1547
+ if (mode === lastTransportMode)
1548
+ return;
1549
+ if (mode === "polling") {
1550
+ api.logger.warn(`[AAMP] Connected (polling fallback active) \u2014 listening as ${agentEmail}`);
1551
+ } else if (lastTransportMode === "polling") {
1552
+ api.logger.info(`[AAMP] WebSocket restored \u2014 listening as ${agentEmail}`);
1553
+ } else {
1554
+ api.logger.info(`[AAMP] Connected \u2014 listening as ${agentEmail}`);
1555
+ }
1556
+ lastTransportMode = mode;
1557
+ });
1558
+ aampClient.on("disconnected", (reason) => {
1559
+ lastDisconnectReason = reason;
1560
+ if (lastTransportMode !== "disconnected") {
1561
+ api.logger.warn(`[AAMP] Disconnected: ${reason} (will auto-reconnect)`);
1562
+ lastTransportMode = "disconnected";
1563
+ }
1564
+ });
1565
+ aampClient.on("error", (err) => {
1566
+ lastConnectionError = err.message;
1567
+ api.logger.error(`[AAMP] ${err.message}`);
1568
+ });
1569
+ await aampClient.connect();
1570
+ reconcileTimer = setInterval(() => {
1571
+ if (!aampClient)
1572
+ return;
1573
+ void aampClient.reconcileRecentEmails(20).catch((err) => {
1574
+ lastConnectionError = err.message;
1575
+ api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
1576
+ });
1577
+ }, 15e3);
1578
+ }
1579
+ api.registerTool({
1580
+ name: "aamp_connect",
1581
+ description: "Connect this agent to the AAMP service. Call this once at the start of a session to obtain an email identity and begin receiving AAMP tasks via JMAP WebSocket Push. Credentials are cached to disk so the same mailbox address is reused after a gateway restart. Delete the credentials file (default: ~/.openclaw/extensions/aamp/.credentials.json) to get a fresh identity.",
1582
+ parameters: {
1583
+ type: "object",
1584
+ properties: {
1585
+ slug: {
1586
+ type: "string",
1587
+ description: 'Optional agent name prefix (e.g. "code-reviewer"). Defaults to the slug set in plugin config, or "openclaw-agent".'
1588
+ }
1589
+ }
1590
+ },
1591
+ execute: async (_id, params) => {
1592
+ const p = params;
1593
+ if (aampClient?.isConnected()) {
1594
+ return {
1595
+ content: [{ type: "text", text: `Already connected as ${agentEmail}.` }]
1596
+ };
1597
+ }
1598
+ if (!cfg.aampHost) {
1599
+ return {
1600
+ content: [{
1601
+ type: "text",
1602
+ text: 'Error: aampHost is not configured. Add "aampHost" to the aamp plugin config in openclaw.json.'
1603
+ }]
1604
+ };
1605
+ }
1606
+ try {
1607
+ const effectiveCfg = p.slug ? { ...cfg, slug: p.slug } : cfg;
1608
+ const identity = await resolveIdentity(effectiveCfg);
1609
+ await doConnect(identity);
1610
+ return {
1611
+ content: [{
1612
+ type: "text",
1613
+ text: [
1614
+ `Connected as ${agentEmail}`,
1615
+ `JMAP push active \u2014 incoming task.dispatch emails will appear in aamp_pending_tasks.`
1616
+ ].join("\n")
1617
+ }]
1618
+ };
1619
+ } catch (err) {
1620
+ return {
1621
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1622
+ };
1623
+ }
1624
+ }
1625
+ }, { name: "aamp_connect" });
1626
+ api.registerService({
1627
+ id: "aamp-service",
1628
+ start: async () => {
1629
+ if (!cfg.aampHost) {
1630
+ api.logger.info("[AAMP] aampHost not configured \u2014 skipping auto-connect");
1631
+ return;
1632
+ }
1633
+ const cached = loadCachedIdentity(cfg);
1634
+ if (!cached) {
1635
+ api.logger.info("[AAMP] No cached credentials \u2014 call aamp_connect to register");
1636
+ return;
1637
+ }
1638
+ try {
1639
+ await doConnect(cached);
1640
+ } catch (err) {
1641
+ api.logger.warn(`[AAMP] Service auto-connect failed: ${err.message}`);
1642
+ }
1643
+ },
1644
+ stop: () => {
1645
+ if (reconcileTimer) {
1646
+ clearInterval(reconcileTimer);
1647
+ reconcileTimer = null;
1648
+ }
1649
+ if (aampClient) {
1650
+ try {
1651
+ aampClient.disconnect();
1652
+ api.logger.info("[AAMP] Disconnected on gateway stop");
1653
+ } catch {
1654
+ }
1655
+ }
1656
+ }
1657
+ });
1658
+ api.on("gateway_start", () => {
1659
+ if (pendingTasks.size === 0)
1660
+ return;
1661
+ api.logger.info(`[AAMP] gateway_start: re-triggering heartbeat for ${pendingTasks.size} pending task(s)`);
1662
+ try {
1663
+ api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: currentSessionKey });
1664
+ } catch (err) {
1665
+ api.logger.warn(`[AAMP] gateway_start heartbeat failed: ${err.message}`);
1666
+ }
1667
+ });
1668
+ api.on(
1669
+ "before_prompt_build",
1670
+ (_event, ctx) => {
1671
+ if (ctx?.sessionKey && !String(ctx.sessionKey).startsWith("aamp:")) {
1672
+ currentSessionKey = ctx.sessionKey;
1673
+ }
1674
+ const now = Date.now();
1675
+ for (const [id, t] of pendingTasks) {
1676
+ if (t.timeoutSecs && now - new Date(t.receivedAt).getTime() > t.timeoutSecs * 1e3) {
1677
+ api.logger.warn(`[AAMP] Task ${id} timed out \u2014 removing from queue`);
1678
+ pendingTasks.delete(id);
1679
+ }
1680
+ }
1681
+ if (pendingTasks.size === 0)
1682
+ return {};
1683
+ const allEntries = [...pendingTasks.entries()];
1684
+ const notifications = allEntries.filter(([key]) => key.startsWith("result:") || key.startsWith("help:"));
1685
+ const actionable = allEntries.filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:"));
1686
+ const [taskKey, task] = notifications.length > 0 ? notifications.sort((a, b) => new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime())[0] : actionable.sort((a, b) => new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime())[0];
1687
+ const isNotification = taskKey.startsWith("result:") || taskKey.startsWith("help:");
1688
+ if (isNotification && taskKey) {
1689
+ pendingTasks.delete(taskKey);
1690
+ }
1691
+ const actionableTasks = [...pendingTasks.entries()].filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:")).map(([, t]) => t);
1692
+ const hasAttachmentInfo = isNotification && (task.bodyText?.includes("aamp_download_attachment") ?? false);
1693
+ const actionRequiredSection = isNotification && actionableTasks.length > 0 ? [
1694
+ ``,
1695
+ `### Action Required`,
1696
+ ``,
1697
+ `You still have ${actionableTasks.length} pending task(s) that need a response.`,
1698
+ `Use the sub-task result above to complete them by calling aamp_send_result.`,
1699
+ ``,
1700
+ ...actionableTasks.map(
1701
+ (t) => `- Task ID: ${t.taskId} | From: ${t.from} | Title: "${t.title}"`
1702
+ ),
1703
+ ...hasAttachmentInfo ? [
1704
+ ``,
1705
+ `### Forwarding Attachments`,
1706
+ `The sub-task result includes file attachments. To forward them:`,
1707
+ `1. Call aamp_download_attachment for each blobId listed above`,
1708
+ `2. Include the downloaded files in aamp_send_result via the attachments parameter`,
1709
+ ` Example: attachments: [{ filename: "file.html", path: "/tmp/aamp-files/file.html" }]`
1710
+ ] : []
1711
+ ].join("\n") : "";
1712
+ const lines = isNotification ? [
1713
+ `## Sub-task Update`,
1714
+ ``,
1715
+ `A sub-task you dispatched has returned a result. Review the information below.`,
1716
+ `If the sub-task included attachments, use aamp_download_attachment to fetch them.`,
1717
+ ``,
1718
+ `Task ID: ${task.taskId}`,
1719
+ `From: ${task.from}`,
1720
+ `Title: ${task.title}`,
1721
+ task.bodyText ? `
1722
+ ${task.bodyText}` : "",
1723
+ actionRequiredSection,
1724
+ pendingTasks.size > 1 ? `
1725
+ (+${pendingTasks.size - 1} more items queued)` : ""
1726
+ ] : [
1727
+ `## Pending AAMP Task (action required)`,
1728
+ ``,
1729
+ `You have received a task via AAMP email. You MUST call one of the two tools below`,
1730
+ `BEFORE responding to the user \u2014 do not skip this step.`,
1731
+ ``,
1732
+ `### Tool selection rules (follow strictly):`,
1733
+ ``,
1734
+ `Use aamp_send_result ONLY when ALL of the following are true:`,
1735
+ ` 1. The title contains a clear, specific action verb (e.g. "summarise", "review",`,
1736
+ ` "translate", "generate", "fix", "search", "compare", "list")`,
1737
+ ` 2. You know exactly what input/resource to act on`,
1738
+ ` 3. No ambiguity remains \u2014 you could start work immediately without asking anything`,
1739
+ ``,
1740
+ `Use aamp_send_help in ALL other cases, including:`,
1741
+ ` - Title is a greeting or salutation ("hello", "hi", "hey", "test", "ping", etc.)`,
1742
+ ` - Title is fewer than 4 words and contains no actionable verb`,
1743
+ ` - Title is too vague to act on without guessing (e.g. "help", "task", "question")`,
1744
+ ` - Required context is missing (which file? which URL? which criteria?)`,
1745
+ ` - Multiple interpretations are equally plausible`,
1746
+ ``,
1747
+ `IMPORTANT: Responding to a greeting with a greeting is WRONG. "hello" is not a`,
1748
+ `valid task description \u2014 ask what specific task the dispatcher needs done.`,
1749
+ ``,
1750
+ `### Sub-task dispatch rules:`,
1751
+ `If you delegate work to another agent via aamp_dispatch_task, you MUST pass`,
1752
+ `parentTaskId: "${task.taskId}" to establish the parent-child relationship.`,
1753
+ ``,
1754
+ `Task ID: ${task.taskId}`,
1755
+ `From: ${task.from}`,
1756
+ `Title: ${task.title}`,
1757
+ task.bodyText ? `Description:
1758
+ ${task.bodyText}` : "",
1759
+ task.contextLinks.length ? `Context Links:
1760
+ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
1761
+ task.timeoutSecs ? `Deadline: ${task.timeoutSecs}s from dispatch` : `Deadline: none`,
1762
+ `Received: ${task.receivedAt}`,
1763
+ pendingTasks.size > 1 ? `
1764
+ (+${pendingTasks.size - 1} more tasks queued)` : ""
1765
+ ].filter(Boolean).join("\n");
1766
+ return { prependContext: lines };
1767
+ },
1768
+ { priority: 5 }
1769
+ );
1770
+ api.registerTool({
1771
+ name: "aamp_send_result",
1772
+ description: "Send the result of an AAMP task back to the dispatcher. Call this after you have finished processing the task.",
1773
+ parameters: {
1774
+ type: "object",
1775
+ required: ["taskId", "status", "output"],
1776
+ properties: {
1777
+ taskId: {
1778
+ type: "string",
1779
+ description: "The AAMP task ID to reply to (from the system context)"
1780
+ },
1781
+ status: {
1782
+ type: "string",
1783
+ enum: ["completed", "rejected"],
1784
+ description: '"completed" on success, "rejected" if the task cannot be done'
1785
+ },
1786
+ output: {
1787
+ type: "string",
1788
+ description: "Your result or explanation"
1789
+ },
1790
+ errorMsg: {
1791
+ type: "string",
1792
+ description: "Optional error details (use only when status = rejected)"
1793
+ },
1794
+ attachments: {
1795
+ type: "array",
1796
+ description: "File attachments. Each item: { filename, contentType, path (local file path) }",
1797
+ items: {
1798
+ type: "object",
1799
+ properties: {
1800
+ filename: { type: "string" },
1801
+ contentType: { type: "string" },
1802
+ path: { type: "string", description: "Absolute path to the file on disk" }
1803
+ },
1804
+ required: ["filename", "path"]
1805
+ }
1806
+ },
1807
+ structuredResult: {
1808
+ type: "array",
1809
+ description: "Optional structured Meego field values.",
1810
+ items: {
1811
+ type: "object",
1812
+ required: ["fieldKey", "fieldTypeKey"],
1813
+ properties: {
1814
+ fieldKey: { type: "string" },
1815
+ fieldTypeKey: { type: "string" },
1816
+ fieldAlias: { type: "string" },
1817
+ value: {
1818
+ description: "Field value in the exact format required by Meego for this field type."
1819
+ },
1820
+ index: { type: "string" },
1821
+ attachmentFilenames: {
1822
+ type: "array",
1823
+ items: { type: "string" },
1824
+ description: "For attachment fields, filenames from attachments[] that should be uploaded into this field."
1825
+ }
1826
+ }
1827
+ }
1828
+ }
1829
+ }
1830
+ },
1831
+ execute: async (_id, params) => {
1832
+ const p = params;
1833
+ const task = pendingTasks.get(p.taskId);
1834
+ if (!task) {
1835
+ return {
1836
+ content: [{ type: "text", text: `Error: task ${p.taskId} not found in pending queue.` }]
1837
+ };
1838
+ }
1839
+ if (!aampClient?.isConnected()) {
1840
+ return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
1841
+ }
1842
+ api.logger.info(`[AAMP] aamp_send_result params ${JSON.stringify({
1843
+ taskId: p.taskId,
1844
+ status: p.status,
1845
+ output: p.output,
1846
+ errorMsg: p.errorMsg,
1847
+ attachments: p.attachments?.map((a) => ({
1848
+ filename: a.filename,
1849
+ contentType: a.contentType ?? "application/octet-stream",
1850
+ path: a.path
1851
+ })) ?? [],
1852
+ structuredResult: p.structuredResult?.map((field) => ({
1853
+ fieldKey: field.fieldKey,
1854
+ fieldTypeKey: field.fieldTypeKey,
1855
+ fieldAlias: field.fieldAlias,
1856
+ value: field.value,
1857
+ index: field.index,
1858
+ attachmentFilenames: field.attachmentFilenames ?? []
1859
+ })) ?? []
1860
+ })}`);
1861
+ let attachments;
1862
+ if (p.attachments?.length) {
1863
+ const { readFileSync: readFileSync2 } = await import("node:fs");
1864
+ attachments = p.attachments.map((a) => ({
1865
+ filename: a.filename,
1866
+ contentType: a.contentType ?? "application/octet-stream",
1867
+ content: readFileSync2(a.path)
1868
+ }));
1869
+ }
1870
+ await aampClient.sendResult({
1871
+ to: task.from,
1872
+ taskId: task.taskId,
1873
+ status: p.status,
1874
+ output: p.output,
1875
+ errorMsg: p.errorMsg,
1876
+ structuredResult: p.structuredResult?.length ? p.structuredResult : void 0,
1877
+ inReplyTo: task.messageId || void 0,
1878
+ attachments
1879
+ });
1880
+ pendingTasks.delete(task.taskId);
1881
+ api.logger.info(`[AAMP] \u2192 task.result ${task.taskId} ${p.status}`);
1882
+ if (pendingTasks.size > 0) {
1883
+ try {
1884
+ api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: currentSessionKey });
1885
+ } catch {
1886
+ }
1887
+ }
1888
+ return {
1889
+ content: [
1890
+ {
1891
+ type: "text",
1892
+ text: `Result sent for task ${task.taskId} (status: ${p.status}).`
1893
+ }
1894
+ ]
1895
+ };
1896
+ }
1897
+ }, { name: "aamp_send_result" });
1898
+ api.registerTool({
1899
+ name: "aamp_send_help",
1900
+ description: "Send a help request for an AAMP task when you are blocked or need human clarification before you can proceed.",
1901
+ parameters: {
1902
+ type: "object",
1903
+ required: ["taskId", "question", "blockedReason"],
1904
+ properties: {
1905
+ taskId: {
1906
+ type: "string",
1907
+ description: "The AAMP task ID"
1908
+ },
1909
+ question: {
1910
+ type: "string",
1911
+ description: "Your question for the human dispatcher"
1912
+ },
1913
+ blockedReason: {
1914
+ type: "string",
1915
+ description: "Why you cannot proceed without their input"
1916
+ },
1917
+ suggestedOptions: {
1918
+ type: "array",
1919
+ items: { type: "string" },
1920
+ description: "Optional list of choices for the dispatcher to pick from"
1921
+ }
1922
+ }
1923
+ },
1924
+ execute: async (_id, params) => {
1925
+ const p = params;
1926
+ const task = pendingTasks.get(p.taskId);
1927
+ if (!task) {
1928
+ return {
1929
+ content: [{ type: "text", text: `Error: task ${p.taskId} not found in pending queue.` }]
1930
+ };
1931
+ }
1932
+ if (!aampClient?.isConnected()) {
1933
+ return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
1934
+ }
1935
+ await aampClient.sendHelp({
1936
+ to: task.from,
1937
+ taskId: task.taskId,
1938
+ question: p.question,
1939
+ blockedReason: p.blockedReason,
1940
+ suggestedOptions: p.suggestedOptions ?? [],
1941
+ inReplyTo: task.messageId || void 0
1942
+ });
1943
+ api.logger.info(`[AAMP] \u2192 task.help ${task.taskId}`);
1944
+ return {
1945
+ content: [
1946
+ {
1947
+ type: "text",
1948
+ text: `Help request sent for task ${task.taskId}. The task remains pending until the dispatcher replies.`
1949
+ }
1950
+ ]
1951
+ };
1952
+ }
1953
+ }, { name: "aamp_send_help" });
1954
+ api.registerTool({
1955
+ name: "aamp_pending_tasks",
1956
+ description: "List all AAMP tasks currently waiting to be processed.",
1957
+ parameters: { type: "object", properties: {} },
1958
+ execute: async () => {
1959
+ if (pendingTasks.size === 0) {
1960
+ return { content: [{ type: "text", text: "No pending AAMP tasks." }] };
1961
+ }
1962
+ const lines = [...pendingTasks.values()].sort((a, b) => new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime()).map(
1963
+ (t, i) => `${i + 1}. [${t.taskId}] "${t.title}"${t.bodyText ? `
1964
+ Description: ${t.bodyText}` : ""} \u2014 from ${t.from} (received ${t.receivedAt})`
1965
+ );
1966
+ return {
1967
+ content: [
1968
+ {
1969
+ type: "text",
1970
+ text: `${pendingTasks.size} pending task(s):
1971
+ ${lines.join("\n")}`
1972
+ }
1973
+ ]
1974
+ };
1975
+ }
1976
+ }, { name: "aamp_pending_tasks" });
1977
+ api.registerTool({
1978
+ name: "aamp_dispatch_task",
1979
+ description: "Send a task to another AAMP agent and WAIT for the result. This tool blocks until the sub-agent replies (typically 5-60s). The sub-agent's output and any attachment file paths are returned directly.",
1980
+ parameters: {
1981
+ type: "object",
1982
+ required: ["to", "title"],
1983
+ properties: {
1984
+ to: { type: "string", description: "Target agent AAMP email address" },
1985
+ title: { type: "string", description: "Task title (concise summary)" },
1986
+ bodyText: { type: "string", description: "Detailed task description" },
1987
+ parentTaskId: { type: "string", description: "If you are processing a pending AAMP task, pass its Task ID here to establish parent-child nesting. Omit for top-level tasks." },
1988
+ timeoutSecs: { type: "number", description: "Timeout in seconds (optional)" },
1989
+ contextLinks: {
1990
+ type: "array",
1991
+ items: { type: "string" },
1992
+ description: "URLs providing context (optional)"
1993
+ },
1994
+ attachments: {
1995
+ type: "array",
1996
+ description: "File attachments. Each item: { filename, contentType, path (local file path) }",
1997
+ items: {
1998
+ type: "object",
1999
+ properties: {
2000
+ filename: { type: "string" },
2001
+ contentType: { type: "string" },
2002
+ path: { type: "string", description: "Absolute path to the file on disk" }
2003
+ },
2004
+ required: ["filename", "path"]
2005
+ }
2006
+ }
2007
+ }
2008
+ },
2009
+ execute: async (_id, params) => {
2010
+ if (!aampClient?.isConnected()) {
2011
+ return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
2012
+ }
2013
+ try {
2014
+ let attachments;
2015
+ if (params.attachments?.length) {
2016
+ const { readFileSync: readFileSync2 } = await import("node:fs");
2017
+ attachments = params.attachments.map((a) => ({
2018
+ filename: a.filename,
2019
+ contentType: a.contentType ?? "application/octet-stream",
2020
+ content: readFileSync2(a.path)
2021
+ }));
2022
+ }
2023
+ const result = await aampClient.sendTask({
2024
+ to: params.to,
2025
+ title: params.title,
2026
+ parentTaskId: params.parentTaskId,
2027
+ timeoutSecs: params.timeoutSecs,
2028
+ contextLinks: params.contextLinks,
2029
+ attachments
2030
+ });
2031
+ dispatchedSubtasks.set(result.taskId, {
2032
+ to: params.to,
2033
+ title: params.title,
2034
+ dispatchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2035
+ parentTaskId: params.parentTaskId
2036
+ });
2037
+ api.logger.info(`[AAMP] \u2192 task.dispatch ${result.taskId} to=${params.to} parent=${params.parentTaskId ?? "none"} (waiting for reply\u2026)`);
2038
+ const timeoutMs = (params.timeoutSecs ?? 300) * 1e3;
2039
+ const reply = await new Promise((resolve, reject) => {
2040
+ waitingDispatches.set(result.taskId, resolve);
2041
+ setTimeout(() => {
2042
+ if (waitingDispatches.delete(result.taskId)) {
2043
+ reject(new Error(`Sub-task ${result.taskId} timed out after ${params.timeoutSecs ?? 300}s`));
2044
+ }
2045
+ }, timeoutMs);
2046
+ });
2047
+ api.logger.info(`[AAMP] \u2190 sync reply for ${result.taskId}: type=${reply.type} attachments=${JSON.stringify(reply.data?.attachments?.length ?? 0)}`);
2048
+ if (reply.type === "result") {
2049
+ const r = reply.data;
2050
+ let attachmentLines = "";
2051
+ if (r.attachments?.length) {
2052
+ api.logger.info(`[AAMP] Downloading ${r.attachments.length} attachment(s) from sync reply...`);
2053
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
2054
+ const dir = "/tmp/aamp-files";
2055
+ mkdirSync2(dir, { recursive: true });
2056
+ const downloaded = [];
2057
+ const base = baseUrl(cfg.aampHost);
2058
+ const identity = loadCachedIdentity(cfg);
2059
+ const authHeader = identity ? `Basic ${Buffer.from(identity.email + ":" + identity.smtpPassword).toString("base64")}` : "";
2060
+ for (const att of r.attachments) {
2061
+ try {
2062
+ const dlUrl = `${base}/jmap/download/n/${encodeURIComponent(att.blobId)}/${encodeURIComponent(att.filename)}?accept=application/octet-stream`;
2063
+ api.logger.info(`[AAMP] Fetching ${dlUrl}`);
2064
+ const dlRes = await fetch(dlUrl, { headers: { Authorization: authHeader } });
2065
+ if (!dlRes.ok)
2066
+ throw new Error(`HTTP ${dlRes.status}`);
2067
+ const buffer = Buffer.from(await dlRes.arrayBuffer());
2068
+ const filepath = `${dir}/${att.filename}`;
2069
+ writeFileSync2(filepath, buffer);
2070
+ downloaded.push(`${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
2071
+ api.logger.info(`[AAMP] Downloaded: ${att.filename} (${(buffer.length / 1024).toFixed(1)} KB)`);
2072
+ } catch (dlErr) {
2073
+ api.logger.error(`[AAMP] Download failed for ${att.filename}: ${dlErr.message}`);
2074
+ }
2075
+ }
2076
+ if (downloaded.length) {
2077
+ attachmentLines = `
2078
+
2079
+ Attachments downloaded:
2080
+ ${downloaded.join("\n")}`;
2081
+ }
2082
+ }
2083
+ return {
2084
+ content: [{
2085
+ type: "text",
2086
+ text: [
2087
+ `Sub-task ${r.status}: ${params.title}`,
2088
+ `Agent: ${r.from}`,
2089
+ `Task ID: ${result.taskId}`,
2090
+ r.status === "completed" ? `
2091
+ Output:
2092
+ ${r.output}` : `
2093
+ Error: ${r.errorMsg ?? "rejected"}`,
2094
+ attachmentLines
2095
+ ].filter(Boolean).join("\n")
2096
+ }]
2097
+ };
2098
+ } else {
2099
+ const h = reply.data;
2100
+ return {
2101
+ content: [{
2102
+ type: "text",
2103
+ text: [
2104
+ `Sub-task needs help: ${params.title}`,
2105
+ `Agent: ${h.from}`,
2106
+ `Task ID: ${result.taskId}`,
2107
+ `
2108
+ Question: ${h.question}`,
2109
+ `Blocked reason: ${h.blockedReason}`,
2110
+ h.suggestedOptions?.length ? `Options: ${h.suggestedOptions.join(" | ")}` : ""
2111
+ ].filter(Boolean).join("\n")
2112
+ }]
2113
+ };
2114
+ }
2115
+ } catch (err) {
2116
+ return {
2117
+ content: [{ type: "text", text: `Error dispatching task: ${err.message}` }]
2118
+ };
2119
+ }
2120
+ }
2121
+ }, { name: "aamp_dispatch_task" });
2122
+ api.registerTool({
2123
+ name: "aamp_check_protocol",
2124
+ description: "Check if an email address supports the AAMP protocol. Returns { aamp: true/false } indicating whether the address is an AAMP agent.",
2125
+ parameters: {
2126
+ type: "object",
2127
+ required: ["email"],
2128
+ properties: {
2129
+ email: { type: "string", description: "Email address to check" }
2130
+ }
2131
+ },
2132
+ execute: async (_id, params) => {
2133
+ const base = baseUrl(cfg.aampHost);
2134
+ const email = params?.email ?? "";
2135
+ if (!email) {
2136
+ return { content: [{ type: "text", text: "Error: email parameter is required" }] };
2137
+ }
2138
+ try {
2139
+ const res = await fetch(`${base}/api/aamp-check?email=${encodeURIComponent(email)}`);
2140
+ if (!res.ok)
2141
+ throw new Error(`HTTP ${res.status}`);
2142
+ const data = await res.json();
2143
+ return {
2144
+ content: [{
2145
+ type: "text",
2146
+ text: data.aamp ? `${params.email} supports AAMP protocol (domain: ${data.domain ?? "unknown"})` : `${params.email} does not support AAMP protocol`
2147
+ }]
2148
+ };
2149
+ } catch (err) {
2150
+ return {
2151
+ content: [{ type: "text", text: `Could not check ${params.email}: ${err.message}` }]
2152
+ };
2153
+ }
2154
+ }
2155
+ }, { name: "aamp_check_protocol" });
2156
+ api.registerTool({
2157
+ name: "aamp_download_attachment",
2158
+ description: "Download an AAMP email attachment to local disk by its blobId. Use this to retrieve files received from sub-agent task results.",
2159
+ parameters: {
2160
+ type: "object",
2161
+ required: ["blobId", "filename"],
2162
+ properties: {
2163
+ blobId: { type: "string", description: "The JMAP blobId from the attachment metadata" },
2164
+ filename: { type: "string", description: "Filename to save as" },
2165
+ saveTo: { type: "string", description: "Directory to save to (default: /tmp/aamp-files)" }
2166
+ }
2167
+ },
2168
+ execute: async (_id, params) => {
2169
+ if (!aampClient?.isConnected()) {
2170
+ return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
2171
+ }
2172
+ const dir = params.saveTo ?? "/tmp/aamp-files";
2173
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
2174
+ mkdirSync2(dir, { recursive: true });
2175
+ try {
2176
+ const buffer = await aampClient.downloadBlob(params.blobId, params.filename);
2177
+ const filepath = `${dir}/${params.filename}`;
2178
+ writeFileSync2(filepath, buffer);
2179
+ return {
2180
+ content: [{
2181
+ type: "text",
2182
+ text: `Downloaded ${params.filename} (${(buffer.length / 1024).toFixed(1)} KB) to ${filepath}`
2183
+ }]
2184
+ };
2185
+ } catch (err) {
2186
+ return {
2187
+ content: [{ type: "text", text: `Download failed: ${err.message}` }]
2188
+ };
2189
+ }
2190
+ }
2191
+ }, { name: "aamp_download_attachment" });
2192
+ api.registerCommand({
2193
+ name: "aamp-status",
2194
+ description: "Show AAMP connection status and pending task queue",
2195
+ acceptsArgs: false,
2196
+ requireAuth: false,
2197
+ handler: () => {
2198
+ const isPollingFallback = aampClient?.isUsingPollingFallback?.() ?? false;
2199
+ const connectionLine = aampClient?.isConnected() ? isPollingFallback ? "\u{1F7E1} connected (polling fallback)" : "\u2705 connected" : "\u274C disconnected";
2200
+ return {
2201
+ text: [
2202
+ `**AAMP Plugin Status**`,
2203
+ `Host: ${cfg.aampHost || "(not configured)"}`,
2204
+ `Identity: ${agentEmail || "(not yet registered)"}`,
2205
+ `Connection: ${connectionLine}`,
2206
+ `Cached: ${loadCachedIdentity(cfg) ? "yes" : "no"}`,
2207
+ lastConnectionError ? `Last error: ${lastConnectionError}` : "",
2208
+ lastDisconnectReason ? `Last disconnect: ${lastDisconnectReason}` : "",
2209
+ `Pending: ${pendingTasks.size} task(s)`,
2210
+ ...[...pendingTasks.values()].map(
2211
+ (t) => ` \u2022 ${t.taskId.slice(0, 8)}\u2026 "${t.title}" from ${t.from}`
2212
+ )
2213
+ ].filter(Boolean).join("\n")
2214
+ };
2215
+ }
2216
+ });
2217
+ }
2218
+ };
2219
+ export {
2220
+ src_default as default
2221
+ };
2222
+ //# sourceMappingURL=index.js.map