aamp-openclaw-plugin 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1199 +1,31 @@
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
1
+ import { AampClient } from "aamp-sdk";
2
+ import {
3
+ defaultCredentialsPath,
4
+ ensureDir,
5
+ loadCachedIdentity,
6
+ readBinaryFile,
7
+ saveCachedIdentity,
8
+ writeBinaryFile
9
+ } from "./file-store.js";
1157
10
  function baseUrl(aampHost) {
1158
11
  if (aampHost.startsWith("http://") || aampHost.startsWith("https://")) {
1159
12
  return aampHost.replace(/\/$/, "");
1160
13
  }
1161
14
  return `https://${aampHost}`;
1162
15
  }
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
- }
16
+ const pendingTasks = /* @__PURE__ */ new Map();
17
+ const dispatchedSubtasks = /* @__PURE__ */ new Map();
18
+ const shownNotifications = /* @__PURE__ */ new Set();
19
+ const waitingDispatches = /* @__PURE__ */ new Map();
20
+ let aampClient = null;
21
+ let agentEmail = "";
22
+ let lastConnectionError = "";
23
+ let lastDisconnectReason = "";
24
+ let lastTransportMode = "disconnected";
25
+ let reconcileTimer = null;
26
+ let currentSessionKey = "agent:main:main";
27
+ let channelRuntime = null;
28
+ let channelCfg = null;
1197
29
  async function registerNode(cfg) {
1198
30
  const slug = (cfg.slug ?? "openclaw-agent").toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "");
1199
31
  const base = baseUrl(cfg.aampHost);
@@ -1222,15 +54,15 @@ async function registerNode(cfg) {
1222
54
  };
1223
55
  }
1224
56
  async function resolveIdentity(cfg) {
1225
- const cached = loadCachedIdentity(cfg);
57
+ const cached = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
1226
58
  if (cached)
1227
59
  return cached;
1228
60
  const identity = await registerNode(cfg);
1229
- saveCachedIdentity(cfg, identity);
61
+ saveCachedIdentity(identity, cfg.credentialsFile ?? defaultCredentialsPath());
1230
62
  return identity;
1231
63
  }
1232
64
  var src_default = {
1233
- id: "aamp",
65
+ id: "aamp-openclaw-plugin",
1234
66
  name: "AAMP Agent Mail Protocol",
1235
67
  configSchema: {
1236
68
  type: "object",
@@ -1246,7 +78,7 @@ var src_default = {
1246
78
  },
1247
79
  credentialsFile: {
1248
80
  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."
81
+ description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json. Delete this file to force re-registration with a new mailbox."
1250
82
  },
1251
83
  allowedSenders: {
1252
84
  type: "array",
@@ -1266,7 +98,7 @@ var src_default = {
1266
98
  listAccountIds: () => cfg.aampHost ? ["default"] : [],
1267
99
  resolveAccount: () => ({ aampHost: cfg.aampHost }),
1268
100
  isEnabled: () => !!cfg.aampHost,
1269
- isConfigured: () => !!loadCachedIdentity(cfg)
101
+ isConfigured: () => !!loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath())
1270
102
  },
1271
103
  gateway: {
1272
104
  startAccount: async (ctx) => {
@@ -1357,14 +189,13 @@ var src_default = {
1357
189
  const downloadPromise = (async () => {
1358
190
  if (!result.attachments?.length)
1359
191
  return;
1360
- const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
1361
192
  const dir = "/tmp/aamp-files";
1362
- mkdirSync2(dir, { recursive: true });
193
+ ensureDir(dir);
1363
194
  for (const att of result.attachments) {
1364
195
  try {
1365
196
  const buffer = await aampClient.downloadBlob(att.blobId, att.filename);
1366
197
  const filepath = `${dir}/${att.filename}`;
1367
- writeFileSync2(filepath, buffer);
198
+ writeBinaryFile(filepath, buffer);
1368
199
  downloadedFiles.push({ filename: att.filename, path: filepath, size: buffer.length });
1369
200
  api.logger.info(`[AAMP] Pre-downloaded: ${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
1370
201
  } catch (dlErr) {
@@ -1578,7 +409,7 @@ ${notifyBody?.bodyText ?? help.question}`;
1578
409
  }
1579
410
  api.registerTool({
1580
411
  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.",
412
+ 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-openclaw-plugin/.credentials.json) to get a fresh identity.",
1582
413
  parameters: {
1583
414
  type: "object",
1584
415
  properties: {
@@ -1630,7 +461,7 @@ ${notifyBody?.bodyText ?? help.question}`;
1630
461
  api.logger.info("[AAMP] aampHost not configured \u2014 skipping auto-connect");
1631
462
  return;
1632
463
  }
1633
- const cached = loadCachedIdentity(cfg);
464
+ const cached = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
1634
465
  if (!cached) {
1635
466
  api.logger.info("[AAMP] No cached credentials \u2014 call aamp_connect to register");
1636
467
  return;
@@ -1860,11 +691,10 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
1860
691
  })}`);
1861
692
  let attachments;
1862
693
  if (p.attachments?.length) {
1863
- const { readFileSync: readFileSync2 } = await import("node:fs");
1864
694
  attachments = p.attachments.map((a) => ({
1865
695
  filename: a.filename,
1866
696
  contentType: a.contentType ?? "application/octet-stream",
1867
- content: readFileSync2(a.path)
697
+ content: readBinaryFile(a.path)
1868
698
  }));
1869
699
  }
1870
700
  await aampClient.sendResult({
@@ -2013,11 +843,10 @@ ${lines.join("\n")}`
2013
843
  try {
2014
844
  let attachments;
2015
845
  if (params.attachments?.length) {
2016
- const { readFileSync: readFileSync2 } = await import("node:fs");
2017
846
  attachments = params.attachments.map((a) => ({
2018
847
  filename: a.filename,
2019
848
  contentType: a.contentType ?? "application/octet-stream",
2020
- content: readFileSync2(a.path)
849
+ content: readBinaryFile(a.path)
2021
850
  }));
2022
851
  }
2023
852
  const result = await aampClient.sendTask({
@@ -2050,12 +879,11 @@ ${lines.join("\n")}`
2050
879
  let attachmentLines = "";
2051
880
  if (r.attachments?.length) {
2052
881
  api.logger.info(`[AAMP] Downloading ${r.attachments.length} attachment(s) from sync reply...`);
2053
- const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
2054
882
  const dir = "/tmp/aamp-files";
2055
- mkdirSync2(dir, { recursive: true });
883
+ ensureDir(dir);
2056
884
  const downloaded = [];
2057
885
  const base = baseUrl(cfg.aampHost);
2058
- const identity = loadCachedIdentity(cfg);
886
+ const identity = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
2059
887
  const authHeader = identity ? `Basic ${Buffer.from(identity.email + ":" + identity.smtpPassword).toString("base64")}` : "";
2060
888
  for (const att of r.attachments) {
2061
889
  try {
@@ -2066,7 +894,7 @@ ${lines.join("\n")}`
2066
894
  throw new Error(`HTTP ${dlRes.status}`);
2067
895
  const buffer = Buffer.from(await dlRes.arrayBuffer());
2068
896
  const filepath = `${dir}/${att.filename}`;
2069
- writeFileSync2(filepath, buffer);
897
+ writeBinaryFile(filepath, buffer);
2070
898
  downloaded.push(`${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
2071
899
  api.logger.info(`[AAMP] Downloaded: ${att.filename} (${(buffer.length / 1024).toFixed(1)} KB)`);
2072
900
  } catch (dlErr) {
@@ -2170,12 +998,11 @@ Question: ${h.question}`,
2170
998
  return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
2171
999
  }
2172
1000
  const dir = params.saveTo ?? "/tmp/aamp-files";
2173
- const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
2174
- mkdirSync2(dir, { recursive: true });
1001
+ ensureDir(dir);
2175
1002
  try {
2176
1003
  const buffer = await aampClient.downloadBlob(params.blobId, params.filename);
2177
1004
  const filepath = `${dir}/${params.filename}`;
2178
- writeFileSync2(filepath, buffer);
1005
+ writeBinaryFile(filepath, buffer);
2179
1006
  return {
2180
1007
  content: [{
2181
1008
  type: "text",
@@ -2203,7 +1030,7 @@ Question: ${h.question}`,
2203
1030
  `Host: ${cfg.aampHost || "(not configured)"}`,
2204
1031
  `Identity: ${agentEmail || "(not yet registered)"}`,
2205
1032
  `Connection: ${connectionLine}`,
2206
- `Cached: ${loadCachedIdentity(cfg) ? "yes" : "no"}`,
1033
+ `Cached: ${loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath()) ? "yes" : "no"}`,
2207
1034
  lastConnectionError ? `Last error: ${lastConnectionError}` : "",
2208
1035
  lastDisconnectReason ? `Last disconnect: ${lastDisconnectReason}` : "",
2209
1036
  `Pending: ${pendingTasks.size} task(s)`,