aamp-wechat-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3541 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import readline2 from "node:readline/promises";
5
+ import { stdin as input2, stdout as output2, argv, exit } from "node:process";
6
+ import qrcodeTerminal from "qrcode-terminal";
7
+
8
+ // src/config.ts
9
+ import { existsSync } from "node:fs";
10
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import readline from "node:readline/promises";
14
+ import { randomUUID as randomUUID2 } from "node:crypto";
15
+ import { stdin as input, stdout as output } from "node:process";
16
+
17
+ // ../sdk/dist/jmap-push.js
18
+ import WebSocket from "ws";
19
+
20
+ // ../sdk/dist/types.js
21
+ var AAMP_PROTOCOL_VERSION = "1.1";
22
+ var AAMP_HEADER = {
23
+ VERSION: "X-AAMP-Version",
24
+ INTENT: "X-AAMP-Intent",
25
+ TASK_ID: "X-AAMP-TaskId",
26
+ SESSION_KEY: "X-AAMP-Session-Key",
27
+ CONTEXT_LINKS: "X-AAMP-ContextLinks",
28
+ DISPATCH_CONTEXT: "X-AAMP-Dispatch-Context",
29
+ PRIORITY: "X-AAMP-Priority",
30
+ EXPIRES_AT: "X-AAMP-Expires-At",
31
+ STATUS: "X-AAMP-Status",
32
+ OUTPUT: "X-AAMP-Output",
33
+ ERROR_MSG: "X-AAMP-ErrorMsg",
34
+ STRUCTURED_RESULT: "X-AAMP-StructuredResult",
35
+ QUESTION: "X-AAMP-Question",
36
+ BLOCKED_REASON: "X-AAMP-BlockedReason",
37
+ SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
38
+ STREAM_ID: "X-AAMP-Stream-Id",
39
+ PARENT_TASK_ID: "X-AAMP-ParentTaskId",
40
+ CARD_SUMMARY: "X-AAMP-Card-Summary"
41
+ };
42
+
43
+ // ../sdk/dist/parser.js
44
+ function normalizeBodyText(value) {
45
+ return value?.replace(/\r\n/g, "\n").trim() ?? "";
46
+ }
47
+ function escapeRegex(value) {
48
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49
+ }
50
+ function extractBodySection(bodyText, label, nextLabels) {
51
+ if (!bodyText)
52
+ return "";
53
+ const nextPattern = nextLabels.length ? `(?=\\n(?:${nextLabels.map(escapeRegex).join("|")}):|$)` : "$";
54
+ const pattern = new RegExp(`(?:^|\\n)${escapeRegex(label)}:\\s*([\\s\\S]*?)${nextPattern}`, "i");
55
+ const match = pattern.exec(bodyText);
56
+ return match?.[1]?.trim() ?? "";
57
+ }
58
+ function parseSuggestedOptionsBlock(block) {
59
+ if (!block.trim())
60
+ return [];
61
+ return block.split("\n").map((line) => line.replace(/^\s*(?:[-*]|\d+\.)\s*/, "").trim()).filter(Boolean);
62
+ }
63
+ function parseTaskResultBody(bodyText) {
64
+ const normalized = normalizeBodyText(bodyText);
65
+ if (!normalized)
66
+ return { output: "" };
67
+ const output3 = extractBodySection(normalized, "Output", ["Error"]);
68
+ const errorMsg = extractBodySection(normalized, "Error", []);
69
+ if (output3 || errorMsg) {
70
+ return { output: output3, ...errorMsg ? { errorMsg } : {} };
71
+ }
72
+ return { output: normalized };
73
+ }
74
+ function parseTaskHelpBody(bodyText) {
75
+ const normalized = normalizeBodyText(bodyText);
76
+ if (!normalized) {
77
+ return { question: "", blockedReason: "", suggestedOptions: [] };
78
+ }
79
+ const question = extractBodySection(normalized, "Question", ["Blocked reason", "Suggested options"]);
80
+ const blockedReason = extractBodySection(normalized, "Blocked reason", ["Suggested options"]);
81
+ const suggestedOptions = parseSuggestedOptionsBlock(extractBodySection(normalized, "Suggested options", []));
82
+ if (question || blockedReason || suggestedOptions.length) {
83
+ return { question, blockedReason, suggestedOptions };
84
+ }
85
+ return { question: normalized, blockedReason: "", suggestedOptions: [] };
86
+ }
87
+ function decodeMimeEncodedWordSegment(segment) {
88
+ const match = /^=\?([^?]+)\?([bBqQ])\?([^?]*)\?=$/.exec(segment);
89
+ if (!match)
90
+ return segment;
91
+ const [, charsetRaw, encodingRaw, body] = match;
92
+ const charset = charsetRaw.toLowerCase();
93
+ const encoding = encodingRaw.toUpperCase();
94
+ try {
95
+ if (encoding === "B") {
96
+ const buf = Buffer.from(body, "base64");
97
+ return buf.toString(charset === "utf-8" || charset === "utf8" ? "utf8" : "utf8");
98
+ }
99
+ const normalized = body.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
100
+ const bytes = Buffer.from(normalized, "binary");
101
+ return bytes.toString(charset === "utf-8" || charset === "utf8" ? "utf8" : "utf8");
102
+ } catch {
103
+ return segment;
104
+ }
105
+ }
106
+ function decodeMimeEncodedWords(value) {
107
+ if (!value || !value.includes("=?"))
108
+ return value ?? "";
109
+ const collapsed = value.replace(/\r?\n[ \t]+/g, " ");
110
+ const decoded = collapsed.replace(/=\?[^?]+\?[bBqQ]\?[^?]*\?=/g, (segment) => decodeMimeEncodedWordSegment(segment));
111
+ return decoded.replace(/\s{2,}/g, " ").trim();
112
+ }
113
+ function normalizeHeaders(headers) {
114
+ return Object.fromEntries(Object.entries(headers).map(([k, v]) => [
115
+ k.toLowerCase(),
116
+ Array.isArray(v) ? v[0] : v
117
+ ]));
118
+ }
119
+ function getAampHeader(headers, headerName) {
120
+ return headers[headerName.toLowerCase()];
121
+ }
122
+ var DISPATCH_CONTEXT_KEY_RE = /^[a-z0-9_-]+$/;
123
+ function parseDispatchContextHeader(value) {
124
+ if (!value)
125
+ return void 0;
126
+ const context = {};
127
+ for (const part of value.split(";")) {
128
+ const segment = part.trim();
129
+ if (!segment)
130
+ continue;
131
+ const eqIdx = segment.indexOf("=");
132
+ if (eqIdx <= 0)
133
+ continue;
134
+ const rawKey = segment.slice(0, eqIdx).trim();
135
+ const rawValue = segment.slice(eqIdx + 1).trim();
136
+ if (!DISPATCH_CONTEXT_KEY_RE.test(rawKey))
137
+ continue;
138
+ try {
139
+ context[rawKey] = decodeURIComponent(rawValue);
140
+ } catch {
141
+ context[rawKey] = rawValue;
142
+ }
143
+ }
144
+ return Object.keys(context).length ? context : void 0;
145
+ }
146
+ function serializeDispatchContextHeader(context) {
147
+ if (!context)
148
+ return void 0;
149
+ const parts = Object.entries(context).flatMap(([key, value]) => {
150
+ const normalizedKey = key.trim().toLowerCase();
151
+ if (!DISPATCH_CONTEXT_KEY_RE.test(normalizedKey))
152
+ return [];
153
+ const normalizedValue = String(value ?? "").trim();
154
+ if (!normalizedValue)
155
+ return [];
156
+ return `${normalizedKey}=${encodeURIComponent(normalizedValue)}`;
157
+ });
158
+ return parts.length ? parts.join("; ") : void 0;
159
+ }
160
+ function decodeStructuredResult(value) {
161
+ if (!value)
162
+ return void 0;
163
+ try {
164
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
165
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
166
+ const decoded = Buffer.from(normalized + padding, "base64").toString("utf-8");
167
+ return JSON.parse(decoded);
168
+ } catch {
169
+ return void 0;
170
+ }
171
+ }
172
+ function encodeStructuredResult(value) {
173
+ if (!value)
174
+ return void 0;
175
+ const json = JSON.stringify(value);
176
+ return Buffer.from(json, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
177
+ }
178
+ function parseAampHeaders(meta) {
179
+ const headers = normalizeHeaders(meta.headers);
180
+ const intent = getAampHeader(headers, AAMP_HEADER.INTENT);
181
+ const taskId = getAampHeader(headers, AAMP_HEADER.TASK_ID);
182
+ const protocolVersion = getAampHeader(headers, AAMP_HEADER.VERSION) ?? AAMP_PROTOCOL_VERSION;
183
+ if (!intent || !taskId)
184
+ return null;
185
+ const from = meta.from.replace(/^<|>$/g, "");
186
+ const to = meta.to.replace(/^<|>$/g, "");
187
+ const decodedSubject = decodeMimeEncodedWords(meta.subject);
188
+ if (intent === "task.dispatch") {
189
+ const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
190
+ const dispatchContext = parseDispatchContextHeader(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT));
191
+ const sessionKey = getAampHeader(headers, AAMP_HEADER.SESSION_KEY);
192
+ const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
193
+ const priority = getAampHeader(headers, AAMP_HEADER.PRIORITY) ?? "normal";
194
+ const expiresAt = getAampHeader(headers, AAMP_HEADER.EXPIRES_AT);
195
+ const dispatch = {
196
+ protocolVersion,
197
+ intent: "task.dispatch",
198
+ taskId,
199
+ ...sessionKey ? { sessionKey } : {},
200
+ title: decodedSubject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
201
+ priority: priority === "urgent" || priority === "high" ? priority : "normal",
202
+ ...expiresAt ? { expiresAt } : {},
203
+ contextLinks: contextLinksStr ? contextLinksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
204
+ ...dispatchContext ? { dispatchContext } : {},
205
+ ...parentTaskId ? { parentTaskId } : {},
206
+ from,
207
+ to,
208
+ messageId: meta.messageId,
209
+ subject: meta.subject,
210
+ bodyText: ""
211
+ // filled in by jmap-push.ts after parsing
212
+ };
213
+ return dispatch;
214
+ }
215
+ if (intent === "task.cancel") {
216
+ const cancel = {
217
+ protocolVersion,
218
+ intent: "task.cancel",
219
+ taskId,
220
+ from,
221
+ to,
222
+ messageId: meta.messageId,
223
+ subject: meta.subject,
224
+ bodyText: ""
225
+ };
226
+ return cancel;
227
+ }
228
+ if (intent === "task.result") {
229
+ const parsedBody = parseTaskResultBody(meta.bodyText);
230
+ const status = getAampHeader(headers, AAMP_HEADER.STATUS) ?? "completed";
231
+ const output3 = getAampHeader(headers, AAMP_HEADER.OUTPUT) ?? parsedBody.output;
232
+ const errorMsg = getAampHeader(headers, AAMP_HEADER.ERROR_MSG) ?? parsedBody.errorMsg;
233
+ const structuredResult = decodeStructuredResult(getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT));
234
+ const result = {
235
+ protocolVersion,
236
+ intent: "task.result",
237
+ taskId,
238
+ status,
239
+ output: decodeMimeEncodedWords(output3),
240
+ errorMsg: errorMsg ? decodeMimeEncodedWords(errorMsg) : errorMsg,
241
+ structuredResult,
242
+ from,
243
+ to,
244
+ messageId: meta.messageId
245
+ };
246
+ return result;
247
+ }
248
+ if (intent === "task.help_needed") {
249
+ const parsedBody = parseTaskHelpBody(meta.bodyText);
250
+ const question = getAampHeader(headers, AAMP_HEADER.QUESTION) ?? parsedBody.question;
251
+ const blockedReason = getAampHeader(headers, AAMP_HEADER.BLOCKED_REASON) ?? parsedBody.blockedReason;
252
+ const suggestedOptionsStr = getAampHeader(headers, AAMP_HEADER.SUGGESTED_OPTIONS) ?? "";
253
+ const help = {
254
+ protocolVersion,
255
+ intent: "task.help_needed",
256
+ taskId,
257
+ question: decodeMimeEncodedWords(question),
258
+ blockedReason: decodeMimeEncodedWords(blockedReason),
259
+ suggestedOptions: suggestedOptionsStr ? suggestedOptionsStr.split("|").map((s) => decodeMimeEncodedWords(s.trim())).filter(Boolean) : parsedBody.suggestedOptions,
260
+ from,
261
+ to,
262
+ messageId: meta.messageId
263
+ };
264
+ return help;
265
+ }
266
+ if (intent === "task.ack") {
267
+ const ack = {
268
+ protocolVersion,
269
+ intent: "task.ack",
270
+ taskId,
271
+ from,
272
+ to,
273
+ messageId: meta.messageId
274
+ };
275
+ return ack;
276
+ }
277
+ if (intent === "task.stream.opened") {
278
+ const streamId = getAampHeader(headers, AAMP_HEADER.STREAM_ID) ?? "";
279
+ if (!streamId)
280
+ return null;
281
+ const streamOpened = {
282
+ protocolVersion,
283
+ intent: "task.stream.opened",
284
+ taskId,
285
+ streamId,
286
+ from,
287
+ to,
288
+ messageId: meta.messageId
289
+ };
290
+ return streamOpened;
291
+ }
292
+ if (intent === "card.query") {
293
+ const cardQuery = {
294
+ protocolVersion,
295
+ intent: "card.query",
296
+ taskId,
297
+ from,
298
+ to,
299
+ messageId: meta.messageId,
300
+ subject: meta.subject,
301
+ bodyText: ""
302
+ };
303
+ return cardQuery;
304
+ }
305
+ if (intent === "card.response") {
306
+ const summary = getAampHeader(headers, AAMP_HEADER.CARD_SUMMARY) ?? "";
307
+ const cardResponse = {
308
+ protocolVersion,
309
+ intent: "card.response",
310
+ taskId,
311
+ summary: decodeMimeEncodedWords(summary) || decodedSubject.replace(/^\[AAMP Card\]\s*/i, "").trim(),
312
+ from,
313
+ to,
314
+ messageId: meta.messageId,
315
+ subject: meta.subject,
316
+ bodyText: ""
317
+ };
318
+ return cardResponse;
319
+ }
320
+ return null;
321
+ }
322
+ function buildDispatchHeaders(params) {
323
+ const headers = {
324
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
325
+ [AAMP_HEADER.INTENT]: "task.dispatch",
326
+ [AAMP_HEADER.TASK_ID]: params.taskId,
327
+ [AAMP_HEADER.PRIORITY]: params.priority ?? "normal"
328
+ };
329
+ if (params.expiresAt) {
330
+ headers[AAMP_HEADER.EXPIRES_AT] = params.expiresAt;
331
+ }
332
+ if (params.sessionKey?.trim()) {
333
+ headers[AAMP_HEADER.SESSION_KEY] = params.sessionKey.trim();
334
+ }
335
+ if (params.contextLinks.length > 0) {
336
+ headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
337
+ }
338
+ const dispatchContext = serializeDispatchContextHeader(params.dispatchContext);
339
+ if (dispatchContext) {
340
+ headers[AAMP_HEADER.DISPATCH_CONTEXT] = dispatchContext;
341
+ }
342
+ if (params.parentTaskId) {
343
+ headers[AAMP_HEADER.PARENT_TASK_ID] = params.parentTaskId;
344
+ }
345
+ return headers;
346
+ }
347
+ function buildCancelHeaders(params) {
348
+ return {
349
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
350
+ [AAMP_HEADER.INTENT]: "task.cancel",
351
+ [AAMP_HEADER.TASK_ID]: params.taskId
352
+ };
353
+ }
354
+ function buildAckHeaders(opts) {
355
+ return {
356
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
357
+ [AAMP_HEADER.INTENT]: "task.ack",
358
+ [AAMP_HEADER.TASK_ID]: opts.taskId
359
+ };
360
+ }
361
+ function buildStreamOpenedHeaders(opts) {
362
+ return {
363
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
364
+ [AAMP_HEADER.INTENT]: "task.stream.opened",
365
+ [AAMP_HEADER.TASK_ID]: opts.taskId,
366
+ [AAMP_HEADER.STREAM_ID]: opts.streamId
367
+ };
368
+ }
369
+ function buildResultHeaders(params) {
370
+ const headers = {
371
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
372
+ [AAMP_HEADER.INTENT]: "task.result",
373
+ [AAMP_HEADER.TASK_ID]: params.taskId,
374
+ [AAMP_HEADER.STATUS]: params.status
375
+ };
376
+ const structuredResult = encodeStructuredResult(params.structuredResult);
377
+ if (structuredResult) {
378
+ headers[AAMP_HEADER.STRUCTURED_RESULT] = structuredResult;
379
+ }
380
+ return headers;
381
+ }
382
+ function buildHelpHeaders(params) {
383
+ return {
384
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
385
+ [AAMP_HEADER.INTENT]: "task.help_needed",
386
+ [AAMP_HEADER.TASK_ID]: params.taskId,
387
+ [AAMP_HEADER.SUGGESTED_OPTIONS]: params.suggestedOptions.join("|")
388
+ };
389
+ }
390
+ function buildCardQueryHeaders(params) {
391
+ return {
392
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
393
+ [AAMP_HEADER.INTENT]: "card.query",
394
+ [AAMP_HEADER.TASK_ID]: params.taskId
395
+ };
396
+ }
397
+ function buildCardResponseHeaders(params) {
398
+ return {
399
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
400
+ [AAMP_HEADER.INTENT]: "card.response",
401
+ [AAMP_HEADER.TASK_ID]: params.taskId,
402
+ [AAMP_HEADER.CARD_SUMMARY]: params.summary
403
+ };
404
+ }
405
+
406
+ // ../sdk/dist/tiny-emitter.js
407
+ var TinyEmitter = class {
408
+ listeners = /* @__PURE__ */ new Map();
409
+ onceWrappers = /* @__PURE__ */ new WeakMap();
410
+ on(event, listener) {
411
+ const bucket = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
412
+ bucket.add(listener);
413
+ this.listeners.set(event, bucket);
414
+ return this;
415
+ }
416
+ once(event, listener) {
417
+ const wrapped = (...args) => {
418
+ this.off(event, listener);
419
+ listener(...args);
420
+ };
421
+ this.onceWrappers.set(listener, wrapped);
422
+ return this.on(event, wrapped);
423
+ }
424
+ off(event, listener) {
425
+ const bucket = this.listeners.get(event);
426
+ if (!bucket)
427
+ return this;
428
+ const original = listener;
429
+ const wrapped = this.onceWrappers.get(original);
430
+ bucket.delete(wrapped ?? original);
431
+ if (wrapped)
432
+ this.onceWrappers.delete(original);
433
+ if (bucket.size === 0)
434
+ this.listeners.delete(event);
435
+ return this;
436
+ }
437
+ emit(event, ...args) {
438
+ const bucket = this.listeners.get(event);
439
+ if (!bucket || bucket.size === 0)
440
+ return false;
441
+ for (const listener of [...bucket]) {
442
+ listener(...args);
443
+ }
444
+ return true;
445
+ }
446
+ async emitAsync(event, ...args) {
447
+ const bucket = this.listeners.get(event);
448
+ if (!bucket || bucket.size === 0)
449
+ return false;
450
+ const settled = await Promise.allSettled([...bucket].map((listener) => Promise.resolve(listener(...args))));
451
+ const rejected = settled.find((result) => result.status === "rejected");
452
+ if (rejected) {
453
+ throw rejected.reason;
454
+ }
455
+ return true;
456
+ }
457
+ };
458
+
459
+ // ../sdk/dist/jmap-push.js
460
+ function describeError(err) {
461
+ if (!(err instanceof Error))
462
+ return String(err);
463
+ const parts = [err.message];
464
+ const details = err;
465
+ if (details.code)
466
+ parts.push(`code=${details.code}`);
467
+ if (details.errno !== void 0)
468
+ parts.push(`errno=${details.errno}`);
469
+ if (details.syscall)
470
+ parts.push(`syscall=${details.syscall}`);
471
+ if (details.address)
472
+ parts.push(`address=${details.address}`);
473
+ if (details.port !== void 0)
474
+ parts.push(`port=${details.port}`);
475
+ if (details.cause instanceof Error) {
476
+ parts.push(`cause=${describeError(details.cause)}`);
477
+ } else if (details.cause !== void 0) {
478
+ parts.push(`cause=${String(details.cause)}`);
479
+ }
480
+ return parts.join(" | ");
481
+ }
482
+ function sleep(ms) {
483
+ return new Promise((resolve) => setTimeout(resolve, ms));
484
+ }
485
+ function shouldRetrySessionFetch(status) {
486
+ return status === 429 || status >= 500;
487
+ }
488
+ function shouldRetryBlobDownload(status) {
489
+ return status === 404 || status === 429 || status === 503;
490
+ }
491
+ function rewriteUrlToConfiguredOrigin(rawUrl, configuredBaseUrl) {
492
+ const parsed = new URL(rawUrl);
493
+ const configured = new URL(configuredBaseUrl);
494
+ parsed.protocol = configured.protocol;
495
+ parsed.username = configured.username;
496
+ parsed.password = configured.password;
497
+ parsed.hostname = configured.hostname;
498
+ parsed.port = configured.port;
499
+ return parsed.toString();
500
+ }
501
+ var SESSION_FETCH_MAX_ATTEMPTS = 3;
502
+ var SESSION_FETCH_RETRY_BASE_DELAY_MS = 250;
503
+ var JmapPushClient = class extends TinyEmitter {
504
+ ws = null;
505
+ session = null;
506
+ reconnectTimer = null;
507
+ pollTimer = null;
508
+ pingTimer = null;
509
+ safetySyncTimer = null;
510
+ seenMessageIds = /* @__PURE__ */ new Set();
511
+ connected = false;
512
+ pollingActive = false;
513
+ running = false;
514
+ connecting = false;
515
+ /** JMAP Email state — tracks processed position; null = not yet initialized */
516
+ emailState = null;
517
+ startedAtMs = Date.now();
518
+ email;
519
+ password;
520
+ jmapUrl;
521
+ reconnectInterval;
522
+ rejectUnauthorized;
523
+ pingIntervalMs = 5e3;
524
+ safetySyncIntervalMs = 5e3;
525
+ constructor(opts) {
526
+ super();
527
+ this.email = opts.email;
528
+ this.password = opts.password;
529
+ this.jmapUrl = opts.jmapUrl.replace(/\/$/, "");
530
+ this.reconnectInterval = opts.reconnectInterval ?? 5e3;
531
+ this.rejectUnauthorized = opts.rejectUnauthorized ?? true;
532
+ }
533
+ /**
534
+ * Start the JMAP Push listener
535
+ */
536
+ async start() {
537
+ this.running = true;
538
+ this.startSafetySync();
539
+ await this.connect();
540
+ }
541
+ /**
542
+ * Stop the JMAP Push listener
543
+ */
544
+ stop() {
545
+ this.running = false;
546
+ if (this.reconnectTimer) {
547
+ clearTimeout(this.reconnectTimer);
548
+ this.reconnectTimer = null;
549
+ }
550
+ if (this.pollTimer) {
551
+ clearTimeout(this.pollTimer);
552
+ this.pollTimer = null;
553
+ }
554
+ if (this.pingTimer) {
555
+ clearInterval(this.pingTimer);
556
+ this.pingTimer = null;
557
+ }
558
+ if (this.safetySyncTimer) {
559
+ clearInterval(this.safetySyncTimer);
560
+ this.safetySyncTimer = null;
561
+ }
562
+ if (this.ws) {
563
+ this.ws.close();
564
+ this.ws = null;
565
+ }
566
+ this.connected = false;
567
+ this.pollingActive = false;
568
+ this.connecting = false;
569
+ }
570
+ getAuthHeader() {
571
+ const creds = `${this.email}:${this.password}`;
572
+ return `Basic ${Buffer.from(creds).toString("base64")}`;
573
+ }
574
+ /**
575
+ * Fetch the JMAP session object
576
+ */
577
+ async fetchSession() {
578
+ const url = `${this.jmapUrl}/.well-known/jmap`;
579
+ let lastError = null;
580
+ for (let attempt = 1; attempt <= SESSION_FETCH_MAX_ATTEMPTS; attempt += 1) {
581
+ let res;
582
+ try {
583
+ res = await fetch(url, {
584
+ headers: { Authorization: this.getAuthHeader() }
585
+ });
586
+ } catch (err) {
587
+ lastError = new Error(`fetchSession ${url} failed: ${describeError(err)}`);
588
+ if (attempt >= SESSION_FETCH_MAX_ATTEMPTS)
589
+ throw lastError;
590
+ await sleep(SESSION_FETCH_RETRY_BASE_DELAY_MS * attempt);
591
+ continue;
592
+ }
593
+ if (res.ok) {
594
+ return res.json();
595
+ }
596
+ lastError = new Error(attempt >= SESSION_FETCH_MAX_ATTEMPTS || !shouldRetrySessionFetch(res.status) ? `Failed to fetch JMAP session: ${res.status} ${res.statusText}` : `Failed to fetch JMAP session after ${attempt} attempt(s): ${res.status} ${res.statusText}`);
597
+ if (attempt >= SESSION_FETCH_MAX_ATTEMPTS || !shouldRetrySessionFetch(res.status)) {
598
+ throw lastError;
599
+ }
600
+ await sleep(SESSION_FETCH_RETRY_BASE_DELAY_MS * attempt);
601
+ }
602
+ throw lastError ?? new Error("Failed to fetch JMAP session");
603
+ }
604
+ /**
605
+ * Perform a JMAP API call
606
+ */
607
+ async jmapCall(methods) {
608
+ if (!this.session)
609
+ throw new Error("No JMAP session");
610
+ const apiUrl = `${this.jmapUrl}/jmap/`;
611
+ let res;
612
+ try {
613
+ res = await fetch(apiUrl, {
614
+ method: "POST",
615
+ headers: {
616
+ Authorization: this.getAuthHeader(),
617
+ "Content-Type": "application/json"
618
+ },
619
+ body: JSON.stringify({
620
+ using: [
621
+ "urn:ietf:params:jmap:core",
622
+ "urn:ietf:params:jmap:mail"
623
+ ],
624
+ methodCalls: methods
625
+ })
626
+ });
627
+ } catch (err) {
628
+ throw new Error(`jmapCall ${apiUrl} failed: ${describeError(err)}`);
629
+ }
630
+ if (!res.ok) {
631
+ throw new Error(`JMAP API call failed: ${res.status}`);
632
+ }
633
+ return res.json();
634
+ }
635
+ /**
636
+ * Initialize emailState by fetching the current state without loading any emails.
637
+ * Called on first connect so we only process emails that arrive AFTER this point.
638
+ */
639
+ async initEmailState(accountId) {
640
+ const response = await this.jmapCall([
641
+ ["Email/get", { accountId, ids: [] }, "g0"]
642
+ ]);
643
+ const getResp = response.methodResponses.find(([name]) => name === "Email/get");
644
+ if (getResp) {
645
+ this.emailState = getResp[1].state ?? null;
646
+ }
647
+ }
648
+ /**
649
+ * Fetch only emails created since `sinceState` using Email/changes.
650
+ * Updates `this.emailState` to the new state after fetching.
651
+ * Returns [] and resets state if the server cannot calculate changes (state too old).
652
+ */
653
+ async fetchEmailsSince(accountId, sinceState) {
654
+ const changesResp = await this.jmapCall([
655
+ ["Email/changes", { accountId, sinceState, maxChanges: 50 }, "c1"]
656
+ ]);
657
+ const changesResult = changesResp.methodResponses.find(([name]) => name === "Email/changes");
658
+ if (!changesResult || changesResult[0] === "error") {
659
+ await this.initEmailState(accountId);
660
+ return [];
661
+ }
662
+ const changes = changesResult[1];
663
+ if (changes.newState) {
664
+ this.emailState = changes.newState;
665
+ }
666
+ const newIds = changes.created ?? [];
667
+ if (newIds.length === 0)
668
+ return [];
669
+ const emailResp = await this.jmapCall([
670
+ [
671
+ "Email/get",
672
+ {
673
+ accountId,
674
+ ids: newIds,
675
+ properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
676
+ fetchTextBodyValues: true,
677
+ maxBodyValueBytes: 262144
678
+ },
679
+ "g1"
680
+ ]
681
+ ]);
682
+ const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
683
+ if (!getResult)
684
+ return [];
685
+ const data = getResult[1];
686
+ return data.list ?? [];
687
+ }
688
+ /**
689
+ * Process a received email.
690
+ *
691
+ * Priority:
692
+ * 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.cancel / task.result / task.help_needed)
693
+ * 2. If In-Reply-To is present → emit 'reply' event so the application layer can
694
+ * resolve the thread (inReplyTo → taskId via Redis/DB) and handle human replies.
695
+ * 3. Otherwise → ignore (not an AAMP-related email)
696
+ */
697
+ processEmail(email) {
698
+ const headerMap = {};
699
+ for (const h of email.headers ?? []) {
700
+ headerMap[h.name.toLowerCase()] = h.value.trim();
701
+ }
702
+ const fromAddr = email.from?.[0]?.email ?? "";
703
+ const toAddr = email.to?.[0]?.email ?? "";
704
+ const messageId = email.messageId?.[0] ?? email.id;
705
+ if (this.seenMessageIds.has(messageId))
706
+ return;
707
+ this.seenMessageIds.add(messageId);
708
+ const aampTextPartId = email.textBody?.[0]?.partId;
709
+ const aampBodyText = aampTextPartId ? (email.bodyValues?.[aampTextPartId]?.value ?? "").trim() : "";
710
+ const msg = parseAampHeaders({
711
+ from: fromAddr,
712
+ to: toAddr,
713
+ messageId,
714
+ subject: email.subject ?? "",
715
+ headers: headerMap,
716
+ bodyText: aampBodyText
717
+ });
718
+ if (msg && "intent" in msg) {
719
+ ;
720
+ msg.bodyText = aampBodyText;
721
+ const receivedAttachments = (email.attachments ?? []).map((a) => ({
722
+ filename: a.name ?? "attachment",
723
+ contentType: a.type,
724
+ size: a.size,
725
+ blobId: a.blobId
726
+ }));
727
+ if (receivedAttachments.length > 0) {
728
+ ;
729
+ msg.attachments = receivedAttachments;
730
+ }
731
+ if (msg.intent === "task.dispatch") {
732
+ this.emit("_autoAck", { to: fromAddr, taskId: msg.taskId, messageId });
733
+ }
734
+ const aampMsg = msg;
735
+ switch (aampMsg.intent) {
736
+ case "task.dispatch":
737
+ this.emit("task.dispatch", aampMsg);
738
+ break;
739
+ case "task.cancel":
740
+ this.emit("task.cancel", aampMsg);
741
+ break;
742
+ case "task.result":
743
+ this.emit("task.result", aampMsg);
744
+ break;
745
+ case "task.help_needed":
746
+ this.emit("task.help_needed", aampMsg);
747
+ break;
748
+ case "task.ack":
749
+ this.emit("task.ack", aampMsg);
750
+ break;
751
+ case "task.stream.opened":
752
+ this.emit("task.stream.opened", aampMsg);
753
+ break;
754
+ case "card.query":
755
+ this.emit("card.query", aampMsg);
756
+ break;
757
+ case "card.response":
758
+ this.emit("card.response", aampMsg);
759
+ break;
760
+ }
761
+ return;
762
+ }
763
+ const rawInReplyTo = headerMap["in-reply-to"] ?? "";
764
+ if (!rawInReplyTo)
765
+ return;
766
+ const rawReferences = headerMap["references"] ?? "";
767
+ const referencesIds = rawReferences.split(/\s+/).map((s) => s.replace(/[<>]/g, "").trim()).filter(Boolean);
768
+ const inReplyTo = rawInReplyTo.replace(/[<>]/g, "").trim();
769
+ const textPartId = email.textBody?.[0]?.partId;
770
+ const bodyText = textPartId ? (email.bodyValues?.[textPartId]?.value ?? "").trim() : "";
771
+ const reply = {
772
+ inReplyTo,
773
+ messageId,
774
+ from: fromAddr,
775
+ to: toAddr,
776
+ subject: email.subject ?? "",
777
+ bodyText
778
+ };
779
+ if (referencesIds.length > 0) {
780
+ Object.assign(reply, { references: referencesIds });
781
+ }
782
+ this.emit("reply", reply);
783
+ }
784
+ async fetchRecentEmails(accountId) {
785
+ const queryResp = await this.jmapCall([
786
+ [
787
+ "Email/query",
788
+ {
789
+ accountId,
790
+ sort: [{ property: "receivedAt", isAscending: false }],
791
+ limit: 20
792
+ },
793
+ "q1"
794
+ ]
795
+ ]);
796
+ const queryResult = queryResp.methodResponses.find(([name]) => name === "Email/query");
797
+ if (!queryResult)
798
+ return [];
799
+ const ids = (queryResult[1].ids ?? []).slice(0, 20);
800
+ if (ids.length === 0)
801
+ return [];
802
+ const emailResp = await this.jmapCall([
803
+ [
804
+ "Email/get",
805
+ {
806
+ accountId,
807
+ ids,
808
+ properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
809
+ fetchTextBodyValues: true,
810
+ maxBodyValueBytes: 262144
811
+ },
812
+ "gRecent"
813
+ ]
814
+ ]);
815
+ const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
816
+ if (!getResult)
817
+ return [];
818
+ return getResult[1].list ?? [];
819
+ }
820
+ shouldProcessBootstrapEmail(email) {
821
+ const receivedAtMs = new Date(email.receivedAt).getTime();
822
+ return Number.isFinite(receivedAtMs) && receivedAtMs >= this.startedAtMs - 15e3;
823
+ }
824
+ /**
825
+ * Connect to JMAP WebSocket
826
+ */
827
+ async connect() {
828
+ if (this.connecting || !this.running)
829
+ return;
830
+ this.connecting = true;
831
+ try {
832
+ this.session = await this.fetchSession();
833
+ } catch (err) {
834
+ this.connecting = false;
835
+ this.emit("error", new Error(`Failed to get JMAP session: ${err.message}`));
836
+ this.startPolling("session fetch failed");
837
+ this.scheduleReconnect();
838
+ return;
839
+ }
840
+ const stalwartWsUrl = `${this.jmapUrl}/jmap/ws`.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
841
+ this.ws = new WebSocket(stalwartWsUrl, "jmap", {
842
+ headers: {
843
+ Authorization: this.getAuthHeader()
844
+ },
845
+ perMessageDeflate: false,
846
+ rejectUnauthorized: this.rejectUnauthorized
847
+ });
848
+ this.ws.on("unexpected-response", (_req, res) => {
849
+ this.connecting = false;
850
+ const headerSummary = Object.entries(res.headers).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value ?? ""}`).join("; ");
851
+ this.startPolling(`websocket handshake failed: ${res.statusCode ?? "unknown"}`);
852
+ this.emit("error", new Error(`JMAP WebSocket handshake failed: ${res.statusCode ?? "unknown"} ${res.statusMessage ?? ""}${headerSummary ? ` | headers: ${headerSummary}` : ""}`));
853
+ this.scheduleReconnect();
854
+ });
855
+ this.ws.on("open", async () => {
856
+ this.connecting = false;
857
+ this.connected = true;
858
+ this.stopPolling();
859
+ this.startPingHeartbeat();
860
+ const accountId = this.session?.primaryAccounts["urn:ietf:params:jmap:mail"];
861
+ if (accountId && this.emailState === null) {
862
+ await this.initEmailState(accountId);
863
+ }
864
+ this.ws.send(JSON.stringify({
865
+ "@type": "WebSocketPushEnable",
866
+ dataTypes: ["Email"],
867
+ pushState: null
868
+ }));
869
+ this.emit("connected");
870
+ });
871
+ this.ws.on("pong", () => {
872
+ });
873
+ this.ws.on("message", async (rawData) => {
874
+ try {
875
+ const msg = JSON.parse(rawData.toString());
876
+ if (msg["@type"] === "StateChange") {
877
+ await this.handleStateChange(msg);
878
+ }
879
+ } catch (err) {
880
+ this.emit("error", new Error(`Failed to process JMAP push message: ${err.message}`));
881
+ }
882
+ });
883
+ this.ws.on("close", (code, reason) => {
884
+ this.connecting = false;
885
+ this.connected = false;
886
+ this.stopPingHeartbeat();
887
+ const reasonStr = reason?.toString() ?? "connection closed";
888
+ this.startPolling(reasonStr);
889
+ this.emit("disconnected", reasonStr);
890
+ if (this.running) {
891
+ this.scheduleReconnect();
892
+ }
893
+ });
894
+ this.ws.on("error", (err) => {
895
+ this.connecting = false;
896
+ this.stopPingHeartbeat();
897
+ this.startPolling(err.message);
898
+ this.emit("error", err);
899
+ });
900
+ }
901
+ startPingHeartbeat() {
902
+ if (this.pingTimer) {
903
+ clearInterval(this.pingTimer);
904
+ this.pingTimer = null;
905
+ }
906
+ this.pingTimer = setInterval(() => {
907
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
908
+ return;
909
+ try {
910
+ this.ws.ping();
911
+ } catch (err) {
912
+ this.emit("error", new Error(`Failed to send WebSocket ping: ${err.message}`));
913
+ }
914
+ }, this.pingIntervalMs);
915
+ }
916
+ stopPingHeartbeat() {
917
+ if (this.pingTimer) {
918
+ clearInterval(this.pingTimer);
919
+ this.pingTimer = null;
920
+ }
921
+ }
922
+ startSafetySync() {
923
+ if (this.safetySyncTimer)
924
+ return;
925
+ this.safetySyncTimer = setInterval(() => {
926
+ if (!this.running)
927
+ return;
928
+ void this.reconcileRecentEmails(20).catch((err) => {
929
+ this.emit("error", new Error(`Safety reconcile failed: ${err.message}`));
930
+ });
931
+ }, this.safetySyncIntervalMs);
932
+ }
933
+ async handleStateChange(stateChange) {
934
+ if (!this.session)
935
+ return;
936
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"];
937
+ if (!accountId)
938
+ return;
939
+ const changedAccount = stateChange.changed[accountId];
940
+ if (!changedAccount?.Email)
941
+ return;
942
+ try {
943
+ if (this.emailState === null) {
944
+ await this.initEmailState(accountId);
945
+ return;
946
+ }
947
+ const emails = await this.fetchEmailsSince(accountId, this.emailState);
948
+ for (const email of emails) {
949
+ this.processEmail(email);
950
+ }
951
+ } catch (err) {
952
+ this.emit("error", new Error(`Failed to fetch emails: ${err.message}`));
953
+ }
954
+ }
955
+ scheduleReconnect() {
956
+ if (this.reconnectTimer)
957
+ return;
958
+ this.reconnectTimer = setTimeout(async () => {
959
+ this.reconnectTimer = null;
960
+ if (this.running) {
961
+ await this.connect();
962
+ }
963
+ }, this.reconnectInterval);
964
+ }
965
+ isConnected() {
966
+ return this.connected || this.pollingActive;
967
+ }
968
+ isUsingPollingFallback() {
969
+ return this.pollingActive && !this.connected;
970
+ }
971
+ stopPolling() {
972
+ if (this.pollTimer) {
973
+ clearTimeout(this.pollTimer);
974
+ this.pollTimer = null;
975
+ }
976
+ this.pollingActive = false;
977
+ }
978
+ startPolling(reason) {
979
+ if (!this.running || this.pollingActive)
980
+ return;
981
+ this.pollingActive = true;
982
+ this.emit("error", new Error(`JMAP WebSocket unavailable, falling back to polling: ${reason}`));
983
+ this.emit("connected");
984
+ const poll = async () => {
985
+ if (!this.running || this.connected) {
986
+ this.stopPolling();
987
+ return;
988
+ }
989
+ try {
990
+ if (!this.session) {
991
+ this.session = await this.fetchSession();
992
+ }
993
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
994
+ if (!accountId) {
995
+ throw new Error("No mail account available in JMAP session");
996
+ }
997
+ if (this.emailState === null) {
998
+ const recentEmails = await this.fetchRecentEmails(accountId);
999
+ for (const email of recentEmails.sort((a, b) => {
1000
+ const aTs = new Date(a.receivedAt).getTime();
1001
+ const bTs = new Date(b.receivedAt).getTime();
1002
+ return aTs - bTs;
1003
+ })) {
1004
+ if (!this.shouldProcessBootstrapEmail(email))
1005
+ continue;
1006
+ this.processEmail(email);
1007
+ }
1008
+ await this.initEmailState(accountId);
1009
+ } else {
1010
+ const emails = await this.fetchEmailsSince(accountId, this.emailState);
1011
+ for (const email of emails) {
1012
+ this.processEmail(email);
1013
+ }
1014
+ }
1015
+ } catch (err) {
1016
+ this.emit("error", new Error(`Polling fallback failed: ${err.message}`));
1017
+ } finally {
1018
+ if (this.running && !this.connected) {
1019
+ this.pollTimer = setTimeout(poll, this.reconnectInterval);
1020
+ }
1021
+ }
1022
+ };
1023
+ this.pollTimer = setTimeout(poll, 0);
1024
+ }
1025
+ /**
1026
+ * Download a blob (attachment) by its JMAP blobId.
1027
+ * Returns the raw binary content as a Buffer.
1028
+ */
1029
+ async downloadBlob(blobId, filename) {
1030
+ if (!this.session) {
1031
+ this.session = await this.fetchSession();
1032
+ }
1033
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
1034
+ let downloadUrl = this.session.downloadUrl ?? `${this.jmapUrl}/jmap/download/{accountId}/{blobId}/{name}`;
1035
+ try {
1036
+ downloadUrl = rewriteUrlToConfiguredOrigin(downloadUrl, this.jmapUrl);
1037
+ } catch {
1038
+ }
1039
+ const safeFilename = filename ?? "attachment";
1040
+ 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");
1041
+ const maxAttempts = 8;
1042
+ let lastStatus = null;
1043
+ let lastError = null;
1044
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1045
+ let res;
1046
+ try {
1047
+ res = await fetch(downloadUrl, {
1048
+ headers: { Authorization: this.getAuthHeader() }
1049
+ });
1050
+ } catch (err) {
1051
+ lastError = new Error(`Blob download fetch failed: attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl} error=${describeError(err)}`);
1052
+ if (attempt < maxAttempts) {
1053
+ console.warn(`[AAMP-SDK] blob download retry fetch-error attempt=${attempt}/${maxAttempts} url=${downloadUrl} error=${describeError(err)}`);
1054
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
1055
+ await new Promise((r) => setTimeout(r, delay));
1056
+ continue;
1057
+ }
1058
+ console.error(`[AAMP-SDK] blob download fetch-error attempt=${attempt}/${maxAttempts} url=${downloadUrl} error=${describeError(err)}`);
1059
+ throw lastError;
1060
+ }
1061
+ lastStatus = res.status;
1062
+ if (res.ok) {
1063
+ const arrayBuffer = await res.arrayBuffer();
1064
+ return Buffer.from(arrayBuffer);
1065
+ }
1066
+ if (attempt < maxAttempts && shouldRetryBlobDownload(res.status)) {
1067
+ console.warn(`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
1068
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
1069
+ await new Promise((r) => setTimeout(r, delay));
1070
+ continue;
1071
+ }
1072
+ console.error(`[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
1073
+ throw new Error(`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
1074
+ }
1075
+ if (lastError)
1076
+ throw lastError;
1077
+ throw new Error(`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
1078
+ }
1079
+ /**
1080
+ * Actively reconcile recent mailbox contents via JMAP HTTP.
1081
+ * Useful as a safety net when the WebSocket stays "connected"
1082
+ * but a notification is missed by an intermediate layer.
1083
+ */
1084
+ async reconcileRecentEmails(limit = 20, opts) {
1085
+ if (!this.session) {
1086
+ this.session = await this.fetchSession();
1087
+ }
1088
+ const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
1089
+ if (!accountId) {
1090
+ throw new Error("No mail account available in JMAP session");
1091
+ }
1092
+ const queryResp = await this.jmapCall([
1093
+ [
1094
+ "Email/query",
1095
+ {
1096
+ accountId,
1097
+ sort: [{ property: "receivedAt", isAscending: false }],
1098
+ limit
1099
+ },
1100
+ "qReconcile"
1101
+ ]
1102
+ ]);
1103
+ const queryResult = queryResp.methodResponses.find(([name]) => name === "Email/query");
1104
+ if (!queryResult)
1105
+ return 0;
1106
+ const ids = (queryResult[1].ids ?? []).slice(0, limit);
1107
+ if (ids.length === 0)
1108
+ return 0;
1109
+ const emailResp = await this.jmapCall([
1110
+ [
1111
+ "Email/get",
1112
+ {
1113
+ accountId,
1114
+ ids,
1115
+ properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
1116
+ fetchTextBodyValues: true,
1117
+ maxBodyValueBytes: 262144
1118
+ },
1119
+ "gReconcile"
1120
+ ]
1121
+ ]);
1122
+ const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
1123
+ if (!getResult)
1124
+ return 0;
1125
+ const emails = getResult[1].list ?? [];
1126
+ for (const email of emails.sort((a, b) => {
1127
+ const aTs = new Date(a.receivedAt).getTime();
1128
+ const bTs = new Date(b.receivedAt).getTime();
1129
+ return aTs - bTs;
1130
+ })) {
1131
+ if (!opts?.includeHistorical && !this.shouldProcessBootstrapEmail(email))
1132
+ continue;
1133
+ this.processEmail(email);
1134
+ }
1135
+ return emails.length;
1136
+ }
1137
+ };
1138
+
1139
+ // ../sdk/dist/smtp-sender.js
1140
+ import { createTransport } from "nodemailer";
1141
+ import { randomUUID } from "crypto";
1142
+ var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
1143
+ function deriveMailboxServiceDefaults(email, baseUrl) {
1144
+ const domain = email.split("@")[1]?.trim();
1145
+ const resolvedBaseUrl = baseUrl?.trim() || (domain ? `https://${domain}` : void 0);
1146
+ const smtpHost = domain || (resolvedBaseUrl ? new URL(resolvedBaseUrl).hostname : "localhost");
1147
+ return {
1148
+ smtpHost,
1149
+ httpBaseUrl: resolvedBaseUrl
1150
+ };
1151
+ }
1152
+ var SmtpSender = class _SmtpSender {
1153
+ config;
1154
+ transport;
1155
+ discoveredApiUrlPromise = null;
1156
+ jmapSessionPromise = null;
1157
+ sentMailboxIdPromise = null;
1158
+ static fromMailboxIdentity(config) {
1159
+ const derived = deriveMailboxServiceDefaults(config.email, config.baseUrl);
1160
+ return new _SmtpSender({
1161
+ host: derived.smtpHost,
1162
+ port: config.smtpPort ?? 587,
1163
+ user: config.email,
1164
+ password: config.password,
1165
+ httpBaseUrl: derived.httpBaseUrl,
1166
+ authToken: Buffer.from(`${config.email}:${config.password}`).toString("base64"),
1167
+ secure: config.secure,
1168
+ rejectUnauthorized: config.rejectUnauthorized
1169
+ });
1170
+ }
1171
+ constructor(config) {
1172
+ this.config = config;
1173
+ this.transport = createTransport({
1174
+ host: config.host,
1175
+ port: config.port,
1176
+ secure: config.secure ?? false,
1177
+ auth: {
1178
+ user: config.user,
1179
+ pass: config.password
1180
+ },
1181
+ tls: {
1182
+ rejectUnauthorized: config.rejectUnauthorized ?? true
1183
+ }
1184
+ });
1185
+ }
1186
+ senderDomain() {
1187
+ return this.config.user.split("@")[1]?.toLowerCase() ?? "";
1188
+ }
1189
+ recipientDomain(email) {
1190
+ return email.split("@")[1]?.toLowerCase() ?? "";
1191
+ }
1192
+ shouldUseHttpFallback(to) {
1193
+ return Boolean(this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to));
1194
+ }
1195
+ async resolveAampApiUrl() {
1196
+ const base = this.config.httpBaseUrl?.replace(/\/$/, "");
1197
+ if (!base) {
1198
+ throw new Error("HTTP send fallback is not configured");
1199
+ }
1200
+ if (!this.discoveredApiUrlPromise) {
1201
+ this.discoveredApiUrlPromise = (async () => {
1202
+ const discoveryRes = await fetch(`${base}/.well-known/aamp`);
1203
+ if (!discoveryRes.ok) {
1204
+ throw new Error(`AAMP discovery failed: ${discoveryRes.status}`);
1205
+ }
1206
+ const discovery = await discoveryRes.json();
1207
+ if (!discovery.api?.url) {
1208
+ throw new Error("AAMP discovery did not return api.url");
1209
+ }
1210
+ return new URL(discovery.api.url, `${base}/`).toString();
1211
+ })();
1212
+ }
1213
+ try {
1214
+ return await this.discoveredApiUrlPromise;
1215
+ } catch (err) {
1216
+ this.discoveredApiUrlPromise = null;
1217
+ throw err;
1218
+ }
1219
+ }
1220
+ async sendViaHttp(opts) {
1221
+ if (!this.config.authToken) {
1222
+ throw new Error("HTTP send fallback is not configured");
1223
+ }
1224
+ const apiUrl = new URL(await this.resolveAampApiUrl());
1225
+ apiUrl.searchParams.set("action", "aamp.mailbox.send");
1226
+ const res = await fetch(apiUrl, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ Authorization: `Basic ${this.config.authToken}`,
1230
+ "Content-Type": "application/json"
1231
+ },
1232
+ body: JSON.stringify({
1233
+ to: opts.to,
1234
+ subject: opts.subject,
1235
+ text: opts.text,
1236
+ aampHeaders: opts.aampHeaders,
1237
+ attachments: opts.attachments?.map((a) => ({
1238
+ filename: a.filename,
1239
+ contentType: a.contentType,
1240
+ content: typeof a.content === "string" ? a.content : a.content.toString("base64")
1241
+ }))
1242
+ })
1243
+ });
1244
+ const data = await res.json().catch(() => ({}));
1245
+ if (!res.ok) {
1246
+ throw new Error(data.details || `HTTP send failed: ${res.status}`);
1247
+ }
1248
+ return { messageId: data.messageId };
1249
+ }
1250
+ canPersistSentCopy() {
1251
+ return Boolean(this.config.httpBaseUrl && this.config.authToken);
1252
+ }
1253
+ getJmapAuthHeader() {
1254
+ if (!this.config.authToken) {
1255
+ throw new Error("JMAP auth token is not configured");
1256
+ }
1257
+ return `Basic ${this.config.authToken}`;
1258
+ }
1259
+ async resolveJmapSession() {
1260
+ const base = this.config.httpBaseUrl?.replace(/\/$/, "");
1261
+ if (!base) {
1262
+ throw new Error("JMAP base URL is not configured");
1263
+ }
1264
+ if (!this.jmapSessionPromise) {
1265
+ this.jmapSessionPromise = (async () => {
1266
+ const res = await fetch(`${base}/.well-known/jmap`, {
1267
+ headers: { Authorization: this.getJmapAuthHeader() }
1268
+ });
1269
+ if (!res.ok) {
1270
+ throw new Error(`JMAP session failed: ${res.status} ${res.statusText}`);
1271
+ }
1272
+ const session = await res.json();
1273
+ const accountId = session.primaryAccounts?.["urn:ietf:params:jmap:mail"] ?? Object.keys(session.accounts ?? {})[0];
1274
+ if (!accountId) {
1275
+ throw new Error("No JMAP mail account available");
1276
+ }
1277
+ return {
1278
+ accountId,
1279
+ apiUrl: `${base}/jmap/`
1280
+ };
1281
+ })();
1282
+ }
1283
+ try {
1284
+ return await this.jmapSessionPromise;
1285
+ } catch (err) {
1286
+ this.jmapSessionPromise = null;
1287
+ throw err;
1288
+ }
1289
+ }
1290
+ async jmapCall(methodCalls) {
1291
+ const session = await this.resolveJmapSession();
1292
+ const res = await fetch(session.apiUrl, {
1293
+ method: "POST",
1294
+ headers: {
1295
+ Authorization: this.getJmapAuthHeader(),
1296
+ "Content-Type": "application/json"
1297
+ },
1298
+ body: JSON.stringify({
1299
+ using: [
1300
+ "urn:ietf:params:jmap:core",
1301
+ "urn:ietf:params:jmap:mail"
1302
+ ],
1303
+ methodCalls: methodCalls.map(([name, args, tag]) => [
1304
+ name,
1305
+ { accountId: session.accountId, ...args },
1306
+ tag
1307
+ ])
1308
+ })
1309
+ });
1310
+ if (!res.ok) {
1311
+ throw new Error(`JMAP API call failed: ${res.status}`);
1312
+ }
1313
+ const data = await res.json();
1314
+ return data.methodResponses ?? [];
1315
+ }
1316
+ async getSentMailboxId() {
1317
+ if (!this.sentMailboxIdPromise) {
1318
+ this.sentMailboxIdPromise = (async () => {
1319
+ const responses = await this.jmapCall([
1320
+ ["Mailbox/get", { ids: null }, "mb1"]
1321
+ ]);
1322
+ const result = responses.find(([name]) => name === "Mailbox/get")?.[1];
1323
+ const mailboxes = result?.list ?? [];
1324
+ return mailboxes.find((mailbox) => mailbox.role === "sent")?.id ?? mailboxes[0]?.id ?? null;
1325
+ })();
1326
+ }
1327
+ try {
1328
+ return await this.sentMailboxIdPromise;
1329
+ } catch (err) {
1330
+ this.sentMailboxIdPromise = null;
1331
+ throw err;
1332
+ }
1333
+ }
1334
+ async saveToSent(params) {
1335
+ if (!this.canPersistSentCopy())
1336
+ return;
1337
+ const sentMailboxId = await this.getSentMailboxId();
1338
+ if (!sentMailboxId)
1339
+ return;
1340
+ const emailCreate = {
1341
+ mailboxIds: { [sentMailboxId]: true },
1342
+ from: [{ email: params.from }],
1343
+ to: [{ email: params.to }],
1344
+ subject: params.subject,
1345
+ bodyValues: {
1346
+ body: {
1347
+ value: params.text,
1348
+ charset: "utf-8"
1349
+ }
1350
+ },
1351
+ textBody: [{ partId: "body", type: "text/plain" }],
1352
+ keywords: { "$seen": true }
1353
+ };
1354
+ if (params.inReplyTo) {
1355
+ emailCreate["header:In-Reply-To:asText"] = ` ${sanitize(params.inReplyTo)}`;
1356
+ }
1357
+ if (params.messageId) {
1358
+ emailCreate["header:Message-ID:asText"] = ` ${sanitize(params.messageId)}`;
1359
+ }
1360
+ if (params.references) {
1361
+ emailCreate["header:References:asText"] = ` ${sanitize(params.references)}`;
1362
+ }
1363
+ for (const [name, value] of Object.entries(params.aampHeaders)) {
1364
+ emailCreate[`header:${name}:asText`] = ` ${value}`;
1365
+ }
1366
+ await this.jmapCall([
1367
+ ["Email/set", { create: { sent1: emailCreate } }, "sent1"]
1368
+ ]);
1369
+ }
1370
+ async saveToSentBestEffort(params) {
1371
+ if (!this.canPersistSentCopy())
1372
+ return;
1373
+ try {
1374
+ await this.saveToSent(params);
1375
+ } catch {
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Send a task.dispatch email.
1380
+ * Returns both the generated taskId and the SMTP Message-ID so callers can
1381
+ * store a reverse-index (messageId → taskId) for In-Reply-To thread routing.
1382
+ */
1383
+ async sendTask(opts) {
1384
+ const taskId = opts.taskId ?? randomUUID();
1385
+ const aampHeaders = buildDispatchHeaders({
1386
+ taskId,
1387
+ priority: opts.priority,
1388
+ expiresAt: opts.expiresAt,
1389
+ contextLinks: opts.contextLinks ?? [],
1390
+ dispatchContext: opts.dispatchContext,
1391
+ parentTaskId: opts.parentTaskId
1392
+ });
1393
+ const sendMailOpts = {
1394
+ from: this.config.user,
1395
+ to: opts.to,
1396
+ subject: `[AAMP Task] ${sanitize(opts.title)}`,
1397
+ text: opts.rawBodyText ?? [
1398
+ `Task: ${opts.title}`,
1399
+ `Task ID: ${taskId}`,
1400
+ `Priority: ${opts.priority ?? "normal"}`,
1401
+ opts.expiresAt ? `Expires At: ${opts.expiresAt}` : `Expires At: none`,
1402
+ opts.contextLinks?.length ? `Context:
1403
+ ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
1404
+ opts.bodyText ?? "",
1405
+ ``,
1406
+ `--- This email was sent by AAMP. Reply directly to submit your result. ---`
1407
+ ].filter(Boolean).join("\n"),
1408
+ headers: aampHeaders
1409
+ };
1410
+ if (opts.attachments?.length) {
1411
+ sendMailOpts.attachments = opts.attachments.map((a) => ({
1412
+ filename: a.filename,
1413
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
1414
+ contentType: a.contentType
1415
+ }));
1416
+ }
1417
+ if (this.shouldUseHttpFallback(opts.to)) {
1418
+ const info2 = await this.sendViaHttp({
1419
+ to: opts.to,
1420
+ subject: sendMailOpts.subject,
1421
+ text: sendMailOpts.text,
1422
+ aampHeaders,
1423
+ attachments: opts.attachments?.map((a) => ({
1424
+ filename: a.filename,
1425
+ contentType: a.contentType,
1426
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1427
+ }))
1428
+ });
1429
+ await this.saveToSentBestEffort({
1430
+ from: this.config.user,
1431
+ to: opts.to,
1432
+ subject: sendMailOpts.subject,
1433
+ text: sendMailOpts.text,
1434
+ aampHeaders,
1435
+ messageId: info2.messageId
1436
+ });
1437
+ return { taskId, messageId: info2.messageId ?? "" };
1438
+ }
1439
+ const info = await this.transport.sendMail(sendMailOpts);
1440
+ await this.saveToSentBestEffort({
1441
+ from: this.config.user,
1442
+ to: opts.to,
1443
+ subject: sendMailOpts.subject,
1444
+ text: sendMailOpts.text,
1445
+ aampHeaders,
1446
+ messageId: info.messageId
1447
+ });
1448
+ return { taskId, messageId: info.messageId ?? "" };
1449
+ }
1450
+ /**
1451
+ * Send a task.result email back to the dispatcher
1452
+ */
1453
+ async sendResult(opts) {
1454
+ const aampHeaders = buildResultHeaders({
1455
+ taskId: opts.taskId,
1456
+ status: opts.status,
1457
+ output: opts.output,
1458
+ errorMsg: opts.errorMsg,
1459
+ structuredResult: opts.structuredResult
1460
+ });
1461
+ const mailOpts = {
1462
+ from: this.config.user,
1463
+ to: opts.to,
1464
+ subject: `[AAMP Result] Task ${opts.taskId} \u2014 ${opts.status}`,
1465
+ text: opts.rawBodyText ?? [
1466
+ `AAMP Task Result`,
1467
+ ``,
1468
+ `Task ID: ${opts.taskId}`,
1469
+ `Status: ${opts.status}`,
1470
+ ``,
1471
+ `Output:`,
1472
+ opts.output,
1473
+ opts.errorMsg ? `
1474
+ Error: ${opts.errorMsg}` : ""
1475
+ ].filter((s) => s !== "").join("\n"),
1476
+ headers: aampHeaders
1477
+ };
1478
+ if (opts.inReplyTo) {
1479
+ mailOpts.inReplyTo = opts.inReplyTo;
1480
+ mailOpts.references = opts.inReplyTo;
1481
+ }
1482
+ if (opts.attachments?.length) {
1483
+ mailOpts.attachments = opts.attachments.map((a) => ({
1484
+ filename: a.filename,
1485
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
1486
+ contentType: a.contentType
1487
+ }));
1488
+ }
1489
+ if (this.shouldUseHttpFallback(opts.to)) {
1490
+ const info2 = await this.sendViaHttp({
1491
+ to: opts.to,
1492
+ subject: mailOpts.subject,
1493
+ text: mailOpts.text,
1494
+ aampHeaders,
1495
+ attachments: opts.attachments?.map((a) => ({
1496
+ filename: a.filename,
1497
+ contentType: a.contentType,
1498
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1499
+ }))
1500
+ });
1501
+ await this.saveToSentBestEffort({
1502
+ from: this.config.user,
1503
+ to: opts.to,
1504
+ subject: mailOpts.subject,
1505
+ text: mailOpts.text,
1506
+ aampHeaders,
1507
+ messageId: info2.messageId,
1508
+ inReplyTo: opts.inReplyTo,
1509
+ references: opts.inReplyTo
1510
+ });
1511
+ return;
1512
+ }
1513
+ const info = await this.transport.sendMail(mailOpts);
1514
+ await this.saveToSentBestEffort({
1515
+ from: this.config.user,
1516
+ to: opts.to,
1517
+ subject: mailOpts.subject,
1518
+ text: mailOpts.text,
1519
+ aampHeaders,
1520
+ messageId: info.messageId,
1521
+ inReplyTo: opts.inReplyTo,
1522
+ references: opts.inReplyTo
1523
+ });
1524
+ }
1525
+ /**
1526
+ * Send a task.help_needed email when the agent is blocked
1527
+ */
1528
+ async sendHelp(opts) {
1529
+ const aampHeaders = buildHelpHeaders({
1530
+ taskId: opts.taskId,
1531
+ question: opts.question,
1532
+ blockedReason: opts.blockedReason,
1533
+ suggestedOptions: opts.suggestedOptions
1534
+ });
1535
+ const helpMailOpts = {
1536
+ from: this.config.user,
1537
+ to: opts.to,
1538
+ subject: `[AAMP Help] Task ${opts.taskId} needs assistance`,
1539
+ text: opts.rawBodyText ?? [
1540
+ `AAMP Task Help Request`,
1541
+ ``,
1542
+ `Task ID: ${opts.taskId}`,
1543
+ ``,
1544
+ `Question: ${opts.question}`,
1545
+ ``,
1546
+ `Blocked reason: ${opts.blockedReason}`,
1547
+ ``,
1548
+ opts.suggestedOptions.length ? `Suggested options:
1549
+ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1550
+ ].filter(Boolean).join("\n"),
1551
+ headers: aampHeaders
1552
+ };
1553
+ if (opts.inReplyTo) {
1554
+ helpMailOpts.inReplyTo = opts.inReplyTo;
1555
+ helpMailOpts.references = opts.inReplyTo;
1556
+ }
1557
+ if (opts.attachments?.length) {
1558
+ helpMailOpts.attachments = opts.attachments.map((a) => ({
1559
+ filename: a.filename,
1560
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
1561
+ contentType: a.contentType
1562
+ }));
1563
+ }
1564
+ if (this.shouldUseHttpFallback(opts.to)) {
1565
+ const info2 = await this.sendViaHttp({
1566
+ to: opts.to,
1567
+ subject: helpMailOpts.subject,
1568
+ text: helpMailOpts.text,
1569
+ aampHeaders,
1570
+ attachments: opts.attachments?.map((a) => ({
1571
+ filename: a.filename,
1572
+ contentType: a.contentType,
1573
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1574
+ }))
1575
+ });
1576
+ await this.saveToSentBestEffort({
1577
+ from: this.config.user,
1578
+ to: opts.to,
1579
+ subject: helpMailOpts.subject,
1580
+ text: helpMailOpts.text,
1581
+ aampHeaders,
1582
+ messageId: info2.messageId,
1583
+ inReplyTo: opts.inReplyTo,
1584
+ references: opts.inReplyTo
1585
+ });
1586
+ return;
1587
+ }
1588
+ const info = await this.transport.sendMail(helpMailOpts);
1589
+ await this.saveToSentBestEffort({
1590
+ from: this.config.user,
1591
+ to: opts.to,
1592
+ subject: helpMailOpts.subject,
1593
+ text: helpMailOpts.text,
1594
+ aampHeaders,
1595
+ messageId: info.messageId,
1596
+ inReplyTo: opts.inReplyTo,
1597
+ references: opts.inReplyTo
1598
+ });
1599
+ }
1600
+ /**
1601
+ * Send a task.cancel email to stop a previously dispatched task.
1602
+ */
1603
+ async sendCancel(opts) {
1604
+ const aampHeaders = buildCancelHeaders({
1605
+ taskId: opts.taskId
1606
+ });
1607
+ const mailOpts = {
1608
+ from: this.config.user,
1609
+ to: opts.to,
1610
+ subject: `[AAMP Cancel] Task ${opts.taskId}`,
1611
+ text: opts.bodyText ?? "The dispatcher cancelled this task.",
1612
+ headers: aampHeaders
1613
+ };
1614
+ if (opts.inReplyTo) {
1615
+ mailOpts.inReplyTo = opts.inReplyTo;
1616
+ mailOpts.references = opts.inReplyTo;
1617
+ }
1618
+ if (this.shouldUseHttpFallback(opts.to)) {
1619
+ const info2 = await this.sendViaHttp({
1620
+ to: opts.to,
1621
+ subject: mailOpts.subject,
1622
+ text: mailOpts.text,
1623
+ aampHeaders
1624
+ });
1625
+ await this.saveToSentBestEffort({
1626
+ from: this.config.user,
1627
+ to: opts.to,
1628
+ subject: mailOpts.subject,
1629
+ text: mailOpts.text,
1630
+ aampHeaders,
1631
+ messageId: info2.messageId,
1632
+ inReplyTo: opts.inReplyTo,
1633
+ references: opts.inReplyTo
1634
+ });
1635
+ return;
1636
+ }
1637
+ const info = await this.transport.sendMail(mailOpts);
1638
+ await this.saveToSentBestEffort({
1639
+ from: this.config.user,
1640
+ to: opts.to,
1641
+ subject: mailOpts.subject,
1642
+ text: mailOpts.text,
1643
+ aampHeaders,
1644
+ messageId: info.messageId,
1645
+ inReplyTo: opts.inReplyTo,
1646
+ references: opts.inReplyTo
1647
+ });
1648
+ }
1649
+ /**
1650
+ * Send a task.ack email to confirm receipt of a dispatch
1651
+ */
1652
+ async sendAck(opts) {
1653
+ const aampHeaders = buildAckHeaders({ taskId: opts.taskId });
1654
+ const mailOpts = {
1655
+ from: this.config.user,
1656
+ to: opts.to,
1657
+ subject: `[AAMP ACK] Task ${opts.taskId}`,
1658
+ text: "",
1659
+ headers: aampHeaders
1660
+ };
1661
+ if (opts.inReplyTo) {
1662
+ mailOpts.inReplyTo = opts.inReplyTo;
1663
+ mailOpts.references = opts.inReplyTo;
1664
+ }
1665
+ if (this.shouldUseHttpFallback(opts.to)) {
1666
+ const info2 = await this.sendViaHttp({
1667
+ to: opts.to,
1668
+ subject: mailOpts.subject,
1669
+ text: mailOpts.text,
1670
+ aampHeaders
1671
+ });
1672
+ await this.saveToSentBestEffort({
1673
+ from: this.config.user,
1674
+ to: opts.to,
1675
+ subject: mailOpts.subject,
1676
+ text: mailOpts.text,
1677
+ aampHeaders,
1678
+ messageId: info2.messageId,
1679
+ inReplyTo: opts.inReplyTo,
1680
+ references: opts.inReplyTo
1681
+ });
1682
+ return;
1683
+ }
1684
+ const info = await this.transport.sendMail(mailOpts);
1685
+ await this.saveToSentBestEffort({
1686
+ from: this.config.user,
1687
+ to: opts.to,
1688
+ subject: mailOpts.subject,
1689
+ text: mailOpts.text,
1690
+ aampHeaders,
1691
+ messageId: info.messageId,
1692
+ inReplyTo: opts.inReplyTo,
1693
+ references: opts.inReplyTo
1694
+ });
1695
+ }
1696
+ async sendStreamOpened(opts) {
1697
+ const aampHeaders = buildStreamOpenedHeaders({
1698
+ taskId: opts.taskId,
1699
+ streamId: opts.streamId
1700
+ });
1701
+ const mailOpts = {
1702
+ from: this.config.user,
1703
+ to: opts.to,
1704
+ subject: `[AAMP Stream] Task ${opts.taskId}`,
1705
+ text: `AAMP task stream is ready.
1706
+
1707
+ Task ID: ${opts.taskId}
1708
+ Stream ID: ${opts.streamId}`,
1709
+ headers: aampHeaders
1710
+ };
1711
+ if (opts.inReplyTo) {
1712
+ mailOpts.inReplyTo = opts.inReplyTo;
1713
+ mailOpts.references = opts.inReplyTo;
1714
+ }
1715
+ if (this.shouldUseHttpFallback(opts.to)) {
1716
+ const info2 = await this.sendViaHttp({
1717
+ to: opts.to,
1718
+ subject: mailOpts.subject,
1719
+ text: mailOpts.text,
1720
+ aampHeaders
1721
+ });
1722
+ await this.saveToSentBestEffort({
1723
+ from: this.config.user,
1724
+ to: opts.to,
1725
+ subject: mailOpts.subject,
1726
+ text: mailOpts.text,
1727
+ aampHeaders,
1728
+ messageId: info2.messageId,
1729
+ inReplyTo: opts.inReplyTo,
1730
+ references: opts.inReplyTo
1731
+ });
1732
+ return;
1733
+ }
1734
+ const info = await this.transport.sendMail(mailOpts);
1735
+ await this.saveToSentBestEffort({
1736
+ from: this.config.user,
1737
+ to: opts.to,
1738
+ subject: mailOpts.subject,
1739
+ text: mailOpts.text,
1740
+ aampHeaders,
1741
+ messageId: info.messageId,
1742
+ inReplyTo: opts.inReplyTo,
1743
+ references: opts.inReplyTo
1744
+ });
1745
+ }
1746
+ async sendCardQuery(opts) {
1747
+ const taskId = opts.taskId ?? randomUUID();
1748
+ const aampHeaders = buildCardQueryHeaders({ taskId });
1749
+ const mailOpts = {
1750
+ from: this.config.user,
1751
+ to: opts.to,
1752
+ subject: `[AAMP Card Query] ${taskId}`,
1753
+ text: opts.bodyText?.trim() || "Please share your agent card and capability details.",
1754
+ headers: aampHeaders
1755
+ };
1756
+ if (opts.inReplyTo) {
1757
+ mailOpts.inReplyTo = opts.inReplyTo;
1758
+ mailOpts.references = opts.inReplyTo;
1759
+ }
1760
+ if (this.shouldUseHttpFallback(opts.to)) {
1761
+ const info2 = await this.sendViaHttp({
1762
+ to: opts.to,
1763
+ subject: mailOpts.subject,
1764
+ text: mailOpts.text,
1765
+ aampHeaders
1766
+ });
1767
+ await this.saveToSentBestEffort({
1768
+ from: this.config.user,
1769
+ to: opts.to,
1770
+ subject: mailOpts.subject,
1771
+ text: mailOpts.text,
1772
+ aampHeaders,
1773
+ messageId: info2.messageId,
1774
+ inReplyTo: opts.inReplyTo,
1775
+ references: opts.inReplyTo
1776
+ });
1777
+ return { taskId, messageId: info2.messageId ?? "" };
1778
+ }
1779
+ const info = await this.transport.sendMail(mailOpts);
1780
+ await this.saveToSentBestEffort({
1781
+ from: this.config.user,
1782
+ to: opts.to,
1783
+ subject: mailOpts.subject,
1784
+ text: mailOpts.text,
1785
+ aampHeaders,
1786
+ messageId: info.messageId,
1787
+ inReplyTo: opts.inReplyTo,
1788
+ references: opts.inReplyTo
1789
+ });
1790
+ return { taskId, messageId: info.messageId ?? "" };
1791
+ }
1792
+ async sendCardResponse(opts) {
1793
+ const aampHeaders = buildCardResponseHeaders({
1794
+ taskId: opts.taskId,
1795
+ summary: opts.summary
1796
+ });
1797
+ const mailOpts = {
1798
+ from: this.config.user,
1799
+ to: opts.to,
1800
+ subject: `[AAMP Card] ${sanitize(opts.summary)}`,
1801
+ text: opts.bodyText,
1802
+ headers: aampHeaders
1803
+ };
1804
+ if (opts.inReplyTo) {
1805
+ mailOpts.inReplyTo = opts.inReplyTo;
1806
+ mailOpts.references = opts.inReplyTo;
1807
+ }
1808
+ if (this.shouldUseHttpFallback(opts.to)) {
1809
+ const info2 = await this.sendViaHttp({
1810
+ to: opts.to,
1811
+ subject: mailOpts.subject,
1812
+ text: mailOpts.text,
1813
+ aampHeaders
1814
+ });
1815
+ await this.saveToSentBestEffort({
1816
+ from: this.config.user,
1817
+ to: opts.to,
1818
+ subject: mailOpts.subject,
1819
+ text: mailOpts.text,
1820
+ aampHeaders,
1821
+ messageId: info2.messageId,
1822
+ inReplyTo: opts.inReplyTo,
1823
+ references: opts.inReplyTo
1824
+ });
1825
+ return;
1826
+ }
1827
+ const info = await this.transport.sendMail(mailOpts);
1828
+ await this.saveToSentBestEffort({
1829
+ from: this.config.user,
1830
+ to: opts.to,
1831
+ subject: mailOpts.subject,
1832
+ text: mailOpts.text,
1833
+ aampHeaders,
1834
+ messageId: info.messageId,
1835
+ inReplyTo: opts.inReplyTo,
1836
+ references: opts.inReplyTo
1837
+ });
1838
+ }
1839
+ /**
1840
+ * Verify SMTP connection
1841
+ */
1842
+ async verify() {
1843
+ try {
1844
+ await this.transport.verify();
1845
+ return true;
1846
+ } catch {
1847
+ return false;
1848
+ }
1849
+ }
1850
+ close() {
1851
+ this.transport.close();
1852
+ }
1853
+ };
1854
+
1855
+ // ../sdk/dist/thread.js
1856
+ function singleLine(value, maxLength = 220) {
1857
+ const normalized = (value ?? "").replace(/\s+/g, " ").trim();
1858
+ if (!normalized)
1859
+ return "";
1860
+ if (normalized.length <= maxLength)
1861
+ return normalized;
1862
+ return `${normalized.slice(0, maxLength - 1)}\u2026`;
1863
+ }
1864
+ function formatTimestamp(value) {
1865
+ const date = new Date(value);
1866
+ if (Number.isNaN(date.getTime()))
1867
+ return value;
1868
+ return date.toISOString().slice(0, 16).replace("T", " ");
1869
+ }
1870
+ function renderEventLine(event) {
1871
+ const from = event.from.split("@")[0] || event.from;
1872
+ const timestamp = formatTimestamp(event.createdAt);
1873
+ if (event.intent === "task.dispatch") {
1874
+ const summary = singleLine(event.bodyText) || singleLine(event.title) || "Task dispatched";
1875
+ return `[${timestamp}] ${from} dispatched: ${summary}`;
1876
+ }
1877
+ if (event.intent === "task.help_needed") {
1878
+ const question = singleLine(event.question) || "Asked for help";
1879
+ const reason = singleLine(event.blockedReason);
1880
+ return `[${timestamp}] ${from} asked for help: ${question}${reason ? ` (reason: ${reason})` : ""}`;
1881
+ }
1882
+ if (event.intent === "task.result") {
1883
+ const output3 = singleLine(event.output) || singleLine(event.bodyText) || "Sent a result";
1884
+ return `[${timestamp}] ${from} replied: ${output3}`;
1885
+ }
1886
+ if (event.intent === "task.cancel") {
1887
+ const body = singleLine(event.bodyText) || "Cancelled the task";
1888
+ return `[${timestamp}] ${from} cancelled the task: ${body}`;
1889
+ }
1890
+ if (event.intent === "task.ack") {
1891
+ return `[${timestamp}] ${from} acknowledged the task`;
1892
+ }
1893
+ return `[${timestamp}] ${from}: ${singleLine(event.bodyText) || event.intent}`;
1894
+ }
1895
+ function renderThreadHistoryForAgent(events, options = {}) {
1896
+ const filtered = events.filter((event) => event.intent !== "task.stream.opened");
1897
+ if (filtered.length === 0)
1898
+ return "";
1899
+ const maxEvents = Math.max(1, options.maxEvents ?? 8);
1900
+ const visible = filtered.slice(-maxEvents);
1901
+ const omitted = filtered.length - visible.length;
1902
+ return [
1903
+ "Prior thread context:",
1904
+ ...omitted > 0 ? [`(${omitted} earlier event(s) omitted)`] : [],
1905
+ ...visible.map((event) => `- ${renderEventLine(event)}`)
1906
+ ].join("\n");
1907
+ }
1908
+
1909
+ // ../sdk/dist/client.js
1910
+ function buildRegisteredCommandDispatchPayload(opts) {
1911
+ const command = opts.command.trim();
1912
+ if (!command) {
1913
+ throw new Error("Registered command name cannot be empty.");
1914
+ }
1915
+ if (opts.args != null && (typeof opts.args !== "object" || Array.isArray(opts.args))) {
1916
+ throw new Error("Registered command args must be an object when provided.");
1917
+ }
1918
+ if (opts.inputs) {
1919
+ for (const input3 of opts.inputs) {
1920
+ if (!input3.slot?.trim() || !input3.attachmentName?.trim()) {
1921
+ throw new Error("Each registered command input must include slot and attachmentName.");
1922
+ }
1923
+ }
1924
+ }
1925
+ return {
1926
+ kind: "registered-command/v1",
1927
+ command,
1928
+ ...opts.args && Object.keys(opts.args).length > 0 ? { args: opts.args } : {},
1929
+ ...opts.inputs?.length ? { inputs: opts.inputs } : {},
1930
+ stream: { mode: opts.streamMode ?? "full" }
1931
+ };
1932
+ }
1933
+ var DEFAULT_TASK_DISPATCH_CONCURRENCY = 10;
1934
+ function normalizeTaskDispatchConcurrency(value) {
1935
+ if (value == null)
1936
+ return DEFAULT_TASK_DISPATCH_CONCURRENCY;
1937
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
1938
+ throw new Error("taskDispatchConcurrency must be a positive integer");
1939
+ }
1940
+ return value;
1941
+ }
1942
+ var AampClient = class _AampClient extends TinyEmitter {
1943
+ jmapClient;
1944
+ smtpSender;
1945
+ config;
1946
+ taskDispatchConcurrency;
1947
+ pendingTaskDispatches = [];
1948
+ activeTaskDispatchCount = 0;
1949
+ streamAppendQueues = /* @__PURE__ */ new Map();
1950
+ constructor(config) {
1951
+ super();
1952
+ this.config = config;
1953
+ this.taskDispatchConcurrency = normalizeTaskDispatchConcurrency(config.taskDispatchConcurrency);
1954
+ const mailboxToken = config.mailboxToken;
1955
+ const resolvedBaseUrl = config.baseUrl;
1956
+ const derived = deriveMailboxServiceDefaults(config.email, resolvedBaseUrl);
1957
+ const smtpHost = config.smtpHost ?? derived.smtpHost;
1958
+ let password;
1959
+ try {
1960
+ const decoded = Buffer.from(mailboxToken, "base64").toString("utf-8");
1961
+ const colonIdx = decoded.indexOf(":");
1962
+ if (colonIdx < 0)
1963
+ throw new Error("Invalid mailboxToken format: expected base64(email:password)");
1964
+ password = decoded.slice(colonIdx + 1);
1965
+ if (!password)
1966
+ throw new Error("Invalid mailboxToken: empty password");
1967
+ } catch (err) {
1968
+ if (err instanceof Error && err.message.startsWith("Invalid mailboxToken"))
1969
+ throw err;
1970
+ throw new Error(`Failed to decode mailboxToken: ${err.message}`);
1971
+ }
1972
+ this.jmapClient = new JmapPushClient({
1973
+ email: config.email,
1974
+ password: password ?? config.smtpPassword,
1975
+ jmapUrl: resolvedBaseUrl,
1976
+ reconnectInterval: config.reconnectInterval ?? 5e3,
1977
+ rejectUnauthorized: config.rejectUnauthorized
1978
+ });
1979
+ this.smtpSender = new SmtpSender({
1980
+ host: smtpHost,
1981
+ port: config.smtpPort ?? 587,
1982
+ user: config.email,
1983
+ password: config.smtpPassword,
1984
+ httpBaseUrl: config.httpSendBaseUrl ?? resolvedBaseUrl,
1985
+ authToken: mailboxToken,
1986
+ rejectUnauthorized: config.rejectUnauthorized
1987
+ });
1988
+ this.jmapClient.on("task.dispatch", (task) => {
1989
+ this.enqueueTaskDispatch(task);
1990
+ });
1991
+ this.jmapClient.on("task.cancel", (task) => {
1992
+ this.emit("task.cancel", task);
1993
+ });
1994
+ this.jmapClient.on("task.result", (result) => {
1995
+ this.emit("task.result", result);
1996
+ });
1997
+ this.jmapClient.on("task.help_needed", (help) => {
1998
+ this.emit("task.help_needed", help);
1999
+ });
2000
+ this.jmapClient.on("task.ack", (ack) => {
2001
+ this.emit("task.ack", ack);
2002
+ });
2003
+ this.jmapClient.on("task.stream.opened", (stream) => {
2004
+ this.emit("task.stream.opened", stream);
2005
+ });
2006
+ this.jmapClient.on("card.query", (query) => {
2007
+ this.emit("card.query", query);
2008
+ });
2009
+ this.jmapClient.on("card.response", (response) => {
2010
+ this.emit("card.response", response);
2011
+ });
2012
+ this.jmapClient.on("_autoAck", async ({ to, taskId, messageId }) => {
2013
+ try {
2014
+ await this.smtpSender.sendAck({ to, taskId, inReplyTo: messageId });
2015
+ } catch (err) {
2016
+ console.warn(`[AAMP] Failed to send ACK for task ${taskId}: ${err.message}`);
2017
+ }
2018
+ });
2019
+ this.jmapClient.on("reply", (reply) => {
2020
+ this.emit("reply", reply);
2021
+ });
2022
+ this.jmapClient.on("connected", () => {
2023
+ this.emit("connected");
2024
+ });
2025
+ this.jmapClient.on("disconnected", (reason) => {
2026
+ this.emit("disconnected", reason);
2027
+ });
2028
+ this.jmapClient.on("error", (err) => {
2029
+ this.emit("error", err);
2030
+ });
2031
+ }
2032
+ static fromMailboxIdentity(config) {
2033
+ const derived = deriveMailboxServiceDefaults(config.email, config.baseUrl);
2034
+ return new _AampClient({
2035
+ email: config.email,
2036
+ mailboxToken: Buffer.from(`${config.email}:${config.smtpPassword}`).toString("base64"),
2037
+ baseUrl: derived.httpBaseUrl ?? `https://${config.email.split("@")[1] ?? "localhost"}`,
2038
+ smtpHost: derived.smtpHost,
2039
+ smtpPort: config.smtpPort ?? 587,
2040
+ smtpPassword: config.smtpPassword,
2041
+ reconnectInterval: config.reconnectInterval,
2042
+ taskDispatchConcurrency: config.taskDispatchConcurrency,
2043
+ rejectUnauthorized: config.rejectUnauthorized
2044
+ });
2045
+ }
2046
+ static async discoverAampService(aampHost) {
2047
+ const base = aampHost.replace(/\/$/, "");
2048
+ const res = await fetch(`${base}/.well-known/aamp`);
2049
+ if (!res.ok) {
2050
+ throw new Error(`AAMP discovery failed: ${res.status} ${res.statusText}`);
2051
+ }
2052
+ const discovery = await res.json();
2053
+ if (!discovery.api?.url) {
2054
+ throw new Error("AAMP discovery did not return api.url");
2055
+ }
2056
+ return discovery;
2057
+ }
2058
+ static async callDiscoveredApi(base, opts) {
2059
+ const discovery = await _AampClient.discoverAampService(base);
2060
+ const apiUrl = new URL(discovery.api.url, `${base}/`);
2061
+ apiUrl.searchParams.set("action", opts.action);
2062
+ for (const [key, value] of Object.entries(opts.query ?? {})) {
2063
+ if (value == null)
2064
+ continue;
2065
+ apiUrl.searchParams.set(key, String(value));
2066
+ }
2067
+ return fetch(apiUrl, {
2068
+ method: opts.method ?? "GET",
2069
+ headers: {
2070
+ ...opts.authToken ? { Authorization: `Basic ${opts.authToken}` } : {},
2071
+ ...opts.body ? { "Content-Type": "application/json" } : {}
2072
+ },
2073
+ ...opts.body ? { body: JSON.stringify(opts.body) } : {}
2074
+ });
2075
+ }
2076
+ static async registerMailbox(opts) {
2077
+ const base = opts.aampHost.replace(/\/$/, "");
2078
+ const registerRes = await _AampClient.callDiscoveredApi(base, {
2079
+ action: "aamp.mailbox.register",
2080
+ method: "POST",
2081
+ body: {
2082
+ slug: opts.slug,
2083
+ description: opts.description
2084
+ }
2085
+ });
2086
+ if (!registerRes.ok) {
2087
+ const body = await registerRes.text().catch(() => "");
2088
+ throw new Error(`Mailbox registration failed: ${registerRes.status} ${body || registerRes.statusText}`);
2089
+ }
2090
+ const registerData = await registerRes.json();
2091
+ if (!registerData.registrationCode) {
2092
+ throw new Error("Mailbox registration succeeded but no registrationCode was returned");
2093
+ }
2094
+ const credsRes = await _AampClient.callDiscoveredApi(base, {
2095
+ action: "aamp.mailbox.credentials",
2096
+ query: { code: registerData.registrationCode }
2097
+ });
2098
+ if (!credsRes.ok) {
2099
+ const body = await credsRes.text().catch(() => "");
2100
+ throw new Error(`Mailbox credential exchange failed: ${credsRes.status} ${body || credsRes.statusText}`);
2101
+ }
2102
+ const creds = await credsRes.json();
2103
+ if (!creds.email || !creds.mailbox?.token || !creds.smtp?.password) {
2104
+ throw new Error("Mailbox credential exchange returned an incomplete identity payload");
2105
+ }
2106
+ return {
2107
+ email: creds.email,
2108
+ mailboxToken: creds.mailbox.token,
2109
+ smtpPassword: creds.smtp.password,
2110
+ baseUrl: base
2111
+ };
2112
+ }
2113
+ static async checkMailbox(opts) {
2114
+ const base = opts.aampHost.replace(/\/$/, "");
2115
+ const res = await _AampClient.callDiscoveredApi(base, {
2116
+ action: "aamp.mailbox.check",
2117
+ query: { email: opts.email }
2118
+ });
2119
+ if (!res.ok) {
2120
+ const body = await res.text().catch(() => "");
2121
+ throw new Error(`Mailbox check failed: ${res.status} ${body || res.statusText}`);
2122
+ }
2123
+ const payload = await res.json();
2124
+ return {
2125
+ aamp: Boolean(payload.aamp),
2126
+ ...payload.domain ? { domain: payload.domain } : {}
2127
+ };
2128
+ }
2129
+ // =====================================================
2130
+ // Lifecycle
2131
+ // =====================================================
2132
+ /**
2133
+ * Connect to JMAP and start listening for tasks
2134
+ */
2135
+ async connect() {
2136
+ await this.jmapClient.start();
2137
+ }
2138
+ /**
2139
+ * Disconnect and clean up
2140
+ */
2141
+ disconnect() {
2142
+ this.jmapClient.stop();
2143
+ this.smtpSender.close();
2144
+ }
2145
+ /**
2146
+ * Returns true if the JMAP connection is active
2147
+ */
2148
+ isConnected() {
2149
+ return this.jmapClient.isConnected();
2150
+ }
2151
+ isUsingPollingFallback() {
2152
+ return this.jmapClient.isUsingPollingFallback();
2153
+ }
2154
+ // =====================================================
2155
+ // Sending
2156
+ // =====================================================
2157
+ /**
2158
+ * Send a task.dispatch email to an agent.
2159
+ * Returns the generated taskId and the SMTP Message-ID.
2160
+ * Store messageId → taskId in Redis/DB to support In-Reply-To thread routing
2161
+ * for human replies that arrive without X-AAMP headers.
2162
+ */
2163
+ async sendTask(opts) {
2164
+ return this.smtpSender.sendTask(opts);
2165
+ }
2166
+ async sendRegisteredCommand(opts) {
2167
+ const payload = buildRegisteredCommandDispatchPayload(opts);
2168
+ return this.smtpSender.sendTask({
2169
+ to: opts.to,
2170
+ taskId: opts.taskId,
2171
+ title: opts.title?.trim() || `Registered command: ${payload.command}`,
2172
+ rawBodyText: JSON.stringify(payload, null, 2),
2173
+ priority: opts.priority,
2174
+ expiresAt: opts.expiresAt,
2175
+ sessionKey: opts.sessionKey,
2176
+ contextLinks: opts.contextLinks,
2177
+ dispatchContext: opts.dispatchContext,
2178
+ parentTaskId: opts.parentTaskId,
2179
+ attachments: opts.attachments
2180
+ });
2181
+ }
2182
+ async sendCancel(opts) {
2183
+ return this.smtpSender.sendCancel(opts);
2184
+ }
2185
+ /**
2186
+ * Send a task.result email (agent → system/dispatcher)
2187
+ */
2188
+ async sendResult(opts) {
2189
+ return this.smtpSender.sendResult(opts);
2190
+ }
2191
+ /**
2192
+ * Send a task.help_needed email when the agent needs human assistance
2193
+ */
2194
+ async sendHelp(opts) {
2195
+ return this.smtpSender.sendHelp(opts);
2196
+ }
2197
+ async sendStreamOpened(opts) {
2198
+ return this.smtpSender.sendStreamOpened(opts);
2199
+ }
2200
+ async sendCardQuery(opts) {
2201
+ return this.smtpSender.sendCardQuery(opts);
2202
+ }
2203
+ async sendCardResponse(opts) {
2204
+ return this.smtpSender.sendCardResponse(opts);
2205
+ }
2206
+ async updateDirectoryProfile(opts) {
2207
+ const base = this.config.baseUrl;
2208
+ const mailboxToken = this.config.mailboxToken;
2209
+ const res = await _AampClient.callDiscoveredApi(base, {
2210
+ action: "aamp.directory.upsert",
2211
+ method: "POST",
2212
+ authToken: mailboxToken,
2213
+ body: opts
2214
+ });
2215
+ if (!res.ok) {
2216
+ const body = await res.text().catch(() => "");
2217
+ throw new Error(`Directory profile update failed: ${res.status} ${body || res.statusText}`);
2218
+ }
2219
+ const data = await res.json();
2220
+ return data.profile;
2221
+ }
2222
+ async listDirectory(opts = {}) {
2223
+ const base = this.config.baseUrl;
2224
+ const mailboxToken = this.config.mailboxToken;
2225
+ const res = await _AampClient.callDiscoveredApi(base, {
2226
+ action: "aamp.directory.list",
2227
+ authToken: mailboxToken,
2228
+ query: {
2229
+ scope: opts.scope,
2230
+ includeSelf: opts.includeSelf,
2231
+ limit: opts.limit
2232
+ }
2233
+ });
2234
+ if (!res.ok) {
2235
+ const body = await res.text().catch(() => "");
2236
+ throw new Error(`Directory list failed: ${res.status} ${body || res.statusText}`);
2237
+ }
2238
+ const data = await res.json();
2239
+ return data.agents;
2240
+ }
2241
+ async searchDirectory(opts) {
2242
+ const base = this.config.baseUrl;
2243
+ const mailboxToken = this.config.mailboxToken;
2244
+ const res = await _AampClient.callDiscoveredApi(base, {
2245
+ action: "aamp.directory.search",
2246
+ authToken: mailboxToken,
2247
+ query: {
2248
+ q: opts.query,
2249
+ scope: opts.scope,
2250
+ includeSelf: opts.includeSelf,
2251
+ limit: opts.limit
2252
+ }
2253
+ });
2254
+ if (!res.ok) {
2255
+ const body = await res.text().catch(() => "");
2256
+ throw new Error(`Directory search failed: ${res.status} ${body || res.statusText}`);
2257
+ }
2258
+ const data = await res.json();
2259
+ return data.agents;
2260
+ }
2261
+ async getThreadHistory(taskId, opts = {}) {
2262
+ const base = this.config.baseUrl;
2263
+ const mailboxToken = this.config.mailboxToken;
2264
+ const res = await _AampClient.callDiscoveredApi(base, {
2265
+ action: "aamp.mailbox.thread",
2266
+ authToken: mailboxToken,
2267
+ query: {
2268
+ taskId,
2269
+ includeStreamOpened: opts.includeStreamOpened
2270
+ }
2271
+ });
2272
+ if (!res.ok) {
2273
+ const body = await res.text().catch(() => "");
2274
+ throw new Error(`Thread history fetch failed: ${res.status} ${body || res.statusText}`);
2275
+ }
2276
+ const data = await res.json();
2277
+ return {
2278
+ taskId: data.taskId,
2279
+ events: Array.isArray(data.events) ? data.events : []
2280
+ };
2281
+ }
2282
+ async hydrateTaskDispatch(task) {
2283
+ const history = await this.getThreadHistory(task.taskId);
2284
+ const priorEvents = history.events.filter((event) => event.messageId !== task.messageId);
2285
+ return {
2286
+ ...task,
2287
+ threadHistory: priorEvents,
2288
+ threadContextText: renderThreadHistoryForAgent(priorEvents)
2289
+ };
2290
+ }
2291
+ async resolveStreamCapability() {
2292
+ const discovery = await _AampClient.discoverAampService(this.config.baseUrl);
2293
+ const stream = discovery.capabilities?.stream;
2294
+ if (!stream?.transport) {
2295
+ throw new Error("AAMP stream capability is not available on this service");
2296
+ }
2297
+ return stream;
2298
+ }
2299
+ async createStream(opts) {
2300
+ const stream = await this.resolveStreamCapability();
2301
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2302
+ action: stream.createAction ?? "aamp.stream.create",
2303
+ method: "POST",
2304
+ authToken: this.config.mailboxToken,
2305
+ body: opts
2306
+ });
2307
+ if (!res.ok) {
2308
+ const body = await res.text().catch(() => "");
2309
+ throw new Error(`AAMP stream create failed: ${res.status} ${body || res.statusText}`);
2310
+ }
2311
+ return res.json();
2312
+ }
2313
+ enqueueTaskDispatch(task) {
2314
+ this.pendingTaskDispatches.push(task);
2315
+ this.drainTaskDispatchQueue();
2316
+ }
2317
+ drainTaskDispatchQueue() {
2318
+ while (this.activeTaskDispatchCount < this.taskDispatchConcurrency && this.pendingTaskDispatches.length > 0) {
2319
+ const nextTask = this.pendingTaskDispatches.shift();
2320
+ if (!nextTask)
2321
+ return;
2322
+ this.activeTaskDispatchCount += 1;
2323
+ void this.runTaskDispatch(nextTask);
2324
+ }
2325
+ }
2326
+ async runTaskDispatch(task) {
2327
+ try {
2328
+ await this.emitAsync("task.dispatch", task);
2329
+ } catch (err) {
2330
+ const error = err instanceof Error ? err : new Error(String(err));
2331
+ this.emit("error", error);
2332
+ } finally {
2333
+ this.activeTaskDispatchCount = Math.max(0, this.activeTaskDispatchCount - 1);
2334
+ this.drainTaskDispatchQueue();
2335
+ }
2336
+ }
2337
+ getStreamAppendQueue(streamId) {
2338
+ let queue = this.streamAppendQueues.get(streamId);
2339
+ if (!queue) {
2340
+ queue = { running: false, operations: [] };
2341
+ this.streamAppendQueues.set(streamId, queue);
2342
+ }
2343
+ return queue;
2344
+ }
2345
+ async dispatchStreamAppend(opts) {
2346
+ const stream = await this.resolveStreamCapability();
2347
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2348
+ action: stream.appendAction ?? "aamp.stream.append",
2349
+ method: "POST",
2350
+ authToken: this.config.mailboxToken,
2351
+ body: opts
2352
+ });
2353
+ if (!res.ok) {
2354
+ const body = await res.text().catch(() => "");
2355
+ throw new Error(`AAMP stream append failed: ${res.status} ${body || res.statusText}`);
2356
+ }
2357
+ return res.json();
2358
+ }
2359
+ enqueueStreamAppend(streamId, operation) {
2360
+ const queue = this.getStreamAppendQueue(streamId);
2361
+ queue.operations.push(operation);
2362
+ void this.drainStreamAppendQueue(streamId);
2363
+ }
2364
+ async drainStreamAppendQueue(streamId) {
2365
+ const queue = this.streamAppendQueues.get(streamId);
2366
+ if (!queue || queue.running)
2367
+ return;
2368
+ queue.running = true;
2369
+ try {
2370
+ while (queue.operations.length) {
2371
+ const operation = queue.operations.shift();
2372
+ if (!operation)
2373
+ continue;
2374
+ if (operation.kind === "text-delta-batch") {
2375
+ try {
2376
+ const event = await this.dispatchStreamAppend({
2377
+ streamId,
2378
+ type: "text.delta",
2379
+ payload: {
2380
+ ...operation.payload,
2381
+ text: operation.text
2382
+ }
2383
+ });
2384
+ for (const resolve of operation.resolvers)
2385
+ resolve(event);
2386
+ } catch (error) {
2387
+ for (const reject of operation.rejecters)
2388
+ reject(error);
2389
+ }
2390
+ continue;
2391
+ }
2392
+ try {
2393
+ const event = await this.dispatchStreamAppend(operation.opts);
2394
+ operation.resolve(event);
2395
+ } catch (error) {
2396
+ operation.reject(error);
2397
+ }
2398
+ }
2399
+ } finally {
2400
+ queue.running = false;
2401
+ if (queue.operations.length === 0) {
2402
+ this.streamAppendQueues.delete(streamId);
2403
+ }
2404
+ }
2405
+ }
2406
+ async flushStreamAppendQueue(streamId) {
2407
+ while (true) {
2408
+ const queue = this.streamAppendQueues.get(streamId);
2409
+ if (!queue)
2410
+ return;
2411
+ if (!queue.running && queue.operations.length === 0) {
2412
+ this.streamAppendQueues.delete(streamId);
2413
+ return;
2414
+ }
2415
+ await new Promise((resolve) => setTimeout(resolve, 0));
2416
+ }
2417
+ }
2418
+ async appendStreamEvent(opts) {
2419
+ if (opts.type === "text.delta" && typeof opts.payload.text === "string") {
2420
+ return await new Promise((resolve, reject) => {
2421
+ const queue = this.getStreamAppendQueue(opts.streamId);
2422
+ const lastOperation = queue.operations.at(-1);
2423
+ if (lastOperation?.kind === "text-delta-batch") {
2424
+ lastOperation.text += String(opts.payload.text ?? "");
2425
+ lastOperation.resolvers.push(resolve);
2426
+ lastOperation.rejecters.push(reject);
2427
+ return;
2428
+ }
2429
+ this.enqueueStreamAppend(opts.streamId, {
2430
+ kind: "text-delta-batch",
2431
+ text: String(opts.payload.text ?? ""),
2432
+ payload: {
2433
+ ...opts.payload
2434
+ },
2435
+ resolvers: [resolve],
2436
+ rejecters: [reject]
2437
+ });
2438
+ });
2439
+ }
2440
+ return await new Promise((resolve, reject) => {
2441
+ this.enqueueStreamAppend(opts.streamId, {
2442
+ kind: "single-event",
2443
+ opts,
2444
+ resolve,
2445
+ reject
2446
+ });
2447
+ });
2448
+ }
2449
+ async closeStream(opts) {
2450
+ await this.flushStreamAppendQueue(opts.streamId);
2451
+ const stream = await this.resolveStreamCapability();
2452
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2453
+ action: stream.closeAction ?? "aamp.stream.close",
2454
+ method: "POST",
2455
+ authToken: this.config.mailboxToken,
2456
+ body: opts
2457
+ });
2458
+ if (!res.ok) {
2459
+ const body = await res.text().catch(() => "");
2460
+ throw new Error(`AAMP stream close failed: ${res.status} ${body || res.statusText}`);
2461
+ }
2462
+ return res.json();
2463
+ }
2464
+ async getTaskStream(opts) {
2465
+ const stream = await this.resolveStreamCapability();
2466
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2467
+ action: stream.getAction ?? "aamp.stream.get",
2468
+ authToken: this.config.mailboxToken,
2469
+ query: {
2470
+ ...opts.taskId ? { taskId: opts.taskId } : {},
2471
+ ...opts.streamId ? { streamId: opts.streamId } : {}
2472
+ }
2473
+ });
2474
+ if (res.status === 404)
2475
+ return null;
2476
+ if (!res.ok) {
2477
+ const body = await res.text().catch(() => "");
2478
+ throw new Error(`AAMP stream get failed: ${res.status} ${body || res.statusText}`);
2479
+ }
2480
+ return res.json();
2481
+ }
2482
+ async subscribeStream(streamId, handlers, opts = {}) {
2483
+ const stream = await this.resolveStreamCapability();
2484
+ const template = stream.subscribeUrlTemplate;
2485
+ if (!template)
2486
+ throw new Error("AAMP stream subscribeUrlTemplate is missing");
2487
+ const url = new URL(template.replace("{streamId}", encodeURIComponent(streamId)), this.config.baseUrl);
2488
+ if (opts.lastEventId) {
2489
+ url.searchParams.set("lastEventId", opts.lastEventId);
2490
+ }
2491
+ const controller = new AbortController();
2492
+ if (opts.signal) {
2493
+ opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
2494
+ }
2495
+ const res = await fetch(url, {
2496
+ headers: {
2497
+ Authorization: `Basic ${this.config.mailboxToken}`,
2498
+ Accept: "text/event-stream"
2499
+ },
2500
+ signal: controller.signal
2501
+ });
2502
+ if (!res.ok || !res.body) {
2503
+ throw new Error(`AAMP stream subscribe failed: ${res.status} ${res.statusText}`);
2504
+ }
2505
+ handlers.onOpen?.();
2506
+ const reader = res.body.getReader();
2507
+ const decoder = new TextDecoder();
2508
+ let buffer = "";
2509
+ let currentEvent = "message";
2510
+ let currentId = "";
2511
+ let currentData = [];
2512
+ const flush = () => {
2513
+ if (!currentData.length)
2514
+ return;
2515
+ try {
2516
+ const parsed = JSON.parse(currentData.join("\n"));
2517
+ handlers.onEvent({
2518
+ ...parsed,
2519
+ ...currentId ? { id: currentId } : {},
2520
+ type: parsed.type ?? currentEvent
2521
+ });
2522
+ } catch (err) {
2523
+ handlers.onError?.(err);
2524
+ } finally {
2525
+ currentEvent = "message";
2526
+ currentId = "";
2527
+ currentData = [];
2528
+ }
2529
+ };
2530
+ void (async () => {
2531
+ try {
2532
+ while (true) {
2533
+ const { value, done } = await reader.read();
2534
+ if (done)
2535
+ break;
2536
+ buffer += decoder.decode(value, { stream: true });
2537
+ let index = buffer.indexOf("\n\n");
2538
+ while (index >= 0) {
2539
+ const frame = buffer.slice(0, index);
2540
+ buffer = buffer.slice(index + 2);
2541
+ for (const rawLine of frame.split("\n")) {
2542
+ const line = rawLine.replace(/\r$/, "");
2543
+ if (!line || line.startsWith(":"))
2544
+ continue;
2545
+ if (line.startsWith("event:")) {
2546
+ currentEvent = line.slice(6).trim();
2547
+ } else if (line.startsWith("id:")) {
2548
+ currentId = line.slice(3).trim();
2549
+ } else if (line.startsWith("data:")) {
2550
+ currentData.push(line.slice(5).trimStart());
2551
+ }
2552
+ }
2553
+ flush();
2554
+ index = buffer.indexOf("\n\n");
2555
+ }
2556
+ }
2557
+ } catch (err) {
2558
+ if (!controller.signal.aborted) {
2559
+ handlers.onError?.(err);
2560
+ }
2561
+ } finally {
2562
+ buffer += decoder.decode();
2563
+ controller.abort();
2564
+ }
2565
+ })();
2566
+ return {
2567
+ close() {
2568
+ controller.abort();
2569
+ }
2570
+ };
2571
+ }
2572
+ /**
2573
+ * Download a blob (attachment) by its JMAP blobId.
2574
+ * Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
2575
+ * Returns the raw binary content as a Buffer.
2576
+ */
2577
+ async downloadBlob(blobId, filename) {
2578
+ return this.jmapClient.downloadBlob(blobId, filename);
2579
+ }
2580
+ /**
2581
+ * Reconcile recent mailbox contents via JMAP HTTP to catch messages missed by
2582
+ * a flaky WebSocket path. Safe to call periodically; duplicate processing is
2583
+ * suppressed by the JMAP push client.
2584
+ */
2585
+ async reconcileRecentEmails(limit, opts) {
2586
+ return this.jmapClient.reconcileRecentEmails(limit, opts);
2587
+ }
2588
+ /**
2589
+ * Verify SMTP connectivity
2590
+ */
2591
+ async verifySmtp() {
2592
+ return this.smtpSender.verify();
2593
+ }
2594
+ get email() {
2595
+ return this.config.email;
2596
+ }
2597
+ };
2598
+
2599
+ // src/config.ts
2600
+ var CONFIG_FILENAME = "config.json";
2601
+ var STATE_FILENAME = "state.json";
2602
+ function getBridgeHomeDir(customDir) {
2603
+ return customDir ? path.resolve(customDir) : path.join(os.homedir(), ".aamp", "wechat-bridge");
2604
+ }
2605
+ function getConfigPath(customDir) {
2606
+ return path.join(getBridgeHomeDir(customDir), CONFIG_FILENAME);
2607
+ }
2608
+ function getStatePath(customDir) {
2609
+ return path.join(getBridgeHomeDir(customDir), STATE_FILENAME);
2610
+ }
2611
+ async function ensureBridgeHomeDir(customDir) {
2612
+ const dir = getBridgeHomeDir(customDir);
2613
+ await mkdir(dir, { recursive: true });
2614
+ return dir;
2615
+ }
2616
+ async function writeJsonAtomic(filePath, value) {
2617
+ const parentDir = path.dirname(filePath);
2618
+ await mkdir(parentDir, { recursive: true });
2619
+ const tempPath = path.join(parentDir, `.${path.basename(filePath)}.${randomUUID2()}.tmp`);
2620
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
2621
+ `, "utf8");
2622
+ await rename(tempPath, filePath);
2623
+ }
2624
+ function createDefaultBridgeState() {
2625
+ return {
2626
+ version: 1,
2627
+ processedMessageIds: [],
2628
+ contextTokens: {},
2629
+ conversations: {},
2630
+ tasks: {}
2631
+ };
2632
+ }
2633
+ async function loadBridgeConfig(customDir) {
2634
+ const filePath = getConfigPath(customDir);
2635
+ if (!existsSync(filePath))
2636
+ return null;
2637
+ const raw = await readFile(filePath, "utf8");
2638
+ return JSON.parse(raw);
2639
+ }
2640
+ async function saveBridgeConfig(config, customDir) {
2641
+ await writeJsonAtomic(getConfigPath(customDir), config);
2642
+ }
2643
+ async function loadBridgeState(customDir) {
2644
+ const filePath = getStatePath(customDir);
2645
+ if (!existsSync(filePath))
2646
+ return createDefaultBridgeState();
2647
+ const raw = await readFile(filePath, "utf8");
2648
+ const parsed = JSON.parse(raw);
2649
+ return {
2650
+ ...createDefaultBridgeState(),
2651
+ ...parsed,
2652
+ processedMessageIds: Array.isArray(parsed.processedMessageIds) ? parsed.processedMessageIds : [],
2653
+ contextTokens: parsed.contextTokens ?? {},
2654
+ conversations: parsed.conversations ?? {},
2655
+ tasks: parsed.tasks ?? {}
2656
+ };
2657
+ }
2658
+ async function saveBridgeState(state, customDir) {
2659
+ await writeJsonAtomic(getStatePath(customDir), state);
2660
+ }
2661
+ function normalizeBaseUrl(url) {
2662
+ if (url.startsWith("http://") || url.startsWith("https://"))
2663
+ return url.replace(/\/$/, "");
2664
+ return `https://${url.replace(/\/$/, "")}`;
2665
+ }
2666
+ function normalizeSlug(rawValue) {
2667
+ return rawValue.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "wechat-bridge";
2668
+ }
2669
+ async function prompt(question, defaultValue = "") {
2670
+ const rl = readline.createInterface({ input, output });
2671
+ try {
2672
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
2673
+ const answer = await rl.question(`${question}${suffix}: `);
2674
+ return answer.trim() || defaultValue;
2675
+ } finally {
2676
+ rl.close();
2677
+ }
2678
+ }
2679
+ function toMailboxIdentity(mailbox) {
2680
+ return {
2681
+ email: mailbox.email,
2682
+ mailboxToken: mailbox.mailboxToken,
2683
+ smtpPassword: mailbox.smtpPassword,
2684
+ baseUrl: mailbox.baseUrl
2685
+ };
2686
+ }
2687
+ async function initializeBridgeConfig(options) {
2688
+ const existing = await loadBridgeConfig(options.configDir);
2689
+ const aampHost = (options.aampHost ?? existing?.aampHost ?? await prompt("AAMP host", "https://meshmail.ai")).trim();
2690
+ const targetAgentEmail = (options.targetAgentEmail ?? existing?.targetAgentEmail ?? await prompt("Target AAMP agent email")).trim();
2691
+ const slug = normalizeSlug(options.slug ?? existing?.slug ?? await prompt("Bridge mailbox slug", "wechat-bridge"));
2692
+ const summary = (options.summary ?? existing?.summary ?? "").trim() || void 0;
2693
+ const botAgent = (options.botAgent ?? existing?.wechat.botAgent ?? await prompt("WeChat bot agent", "AAMP-WeChat-Bridge/0.1.0")).trim();
2694
+ const dispatchTimeoutMs = Math.max(1e3, Math.trunc(options.dispatchTimeoutMs ?? existing?.behavior.dispatchTimeoutMs ?? 18e4));
2695
+ const pollTimeoutMs = Math.max(5e3, Math.trunc(options.pollTimeoutMs ?? existing?.behavior.pollTimeoutMs ?? 35e3));
2696
+ if (!aampHost)
2697
+ throw new Error("AAMP host is required.");
2698
+ if (!targetAgentEmail)
2699
+ throw new Error("Target AAMP agent email is required.");
2700
+ const mailbox = existing?.mailbox ?? toMailboxIdentity(await AampClient.registerMailbox({
2701
+ aampHost,
2702
+ slug,
2703
+ description: `WeChat bridge for ${targetAgentEmail}`
2704
+ }));
2705
+ const config = {
2706
+ version: 1,
2707
+ aampHost: normalizeBaseUrl(aampHost),
2708
+ targetAgentEmail,
2709
+ slug,
2710
+ ...summary ? { summary } : {},
2711
+ mailbox: toMailboxIdentity(mailbox),
2712
+ wechat: {
2713
+ apiBaseUrl: existing?.wechat.apiBaseUrl ?? "https://ilinkai.weixin.qq.com",
2714
+ botType: existing?.wechat.botType ?? "3",
2715
+ botAgent: botAgent || "AAMP-WeChat-Bridge/0.1.0"
2716
+ },
2717
+ behavior: {
2718
+ dispatchTimeoutMs,
2719
+ pollTimeoutMs
2720
+ }
2721
+ };
2722
+ await ensureBridgeHomeDir(options.configDir);
2723
+ await saveBridgeConfig(config, options.configDir);
2724
+ return config;
2725
+ }
2726
+
2727
+ // src/wechat-api.ts
2728
+ import crypto from "node:crypto";
2729
+ var DEFAULT_APP_ID = "bot";
2730
+ var DEFAULT_CHANNEL_VERSION = "0.1.0";
2731
+ function normalizeWechatApiBaseUrl(url) {
2732
+ if (url.startsWith("http://") || url.startsWith("https://"))
2733
+ return url.replace(/\/$/, "");
2734
+ return `https://${url.replace(/\/$/, "")}`;
2735
+ }
2736
+ function buildClientVersion(version) {
2737
+ const parts = version.split(".").map((part) => Number.parseInt(part, 10));
2738
+ const major = parts[0] ?? 0;
2739
+ const minor = parts[1] ?? 0;
2740
+ const patch = parts[2] ?? 0;
2741
+ return (major & 255) << 16 | (minor & 255) << 8 | patch & 255;
2742
+ }
2743
+ function sanitizeBotAgent(raw) {
2744
+ const trimmed = raw.trim();
2745
+ if (!trimmed)
2746
+ return "AAMP-WeChat-Bridge/0.1.0";
2747
+ return trimmed.slice(0, 256);
2748
+ }
2749
+ function buildBaseInfo(botAgent) {
2750
+ return {
2751
+ channel_version: DEFAULT_CHANNEL_VERSION,
2752
+ bot_agent: sanitizeBotAgent(botAgent)
2753
+ };
2754
+ }
2755
+ function buildHeaders(token) {
2756
+ const headers = {
2757
+ "Content-Type": "application/json",
2758
+ AuthorizationType: "ilink_bot_token",
2759
+ "X-WECHAT-UIN": Buffer.from(String(crypto.randomBytes(4).readUInt32BE(0)), "utf8").toString("base64"),
2760
+ "iLink-App-Id": DEFAULT_APP_ID,
2761
+ "iLink-App-ClientVersion": String(buildClientVersion(DEFAULT_CHANNEL_VERSION))
2762
+ };
2763
+ if (token?.trim()) {
2764
+ headers.Authorization = `Bearer ${token.trim()}`;
2765
+ }
2766
+ return headers;
2767
+ }
2768
+ async function postJson(endpoint, body, opts) {
2769
+ const controller = opts.timeoutMs ? new AbortController() : void 0;
2770
+ const timeout = opts.timeoutMs ? setTimeout(() => controller?.abort(), opts.timeoutMs) : void 0;
2771
+ try {
2772
+ const response = await fetch(`${normalizeWechatApiBaseUrl(opts.apiBaseUrl)}/${endpoint}`, {
2773
+ method: "POST",
2774
+ headers: buildHeaders(opts.token),
2775
+ body: JSON.stringify(body),
2776
+ ...controller ? { signal: controller.signal } : {}
2777
+ });
2778
+ const text = await response.text();
2779
+ if (!response.ok) {
2780
+ throw new Error(`${endpoint} ${response.status}: ${text || response.statusText}`);
2781
+ }
2782
+ return JSON.parse(text);
2783
+ } finally {
2784
+ if (timeout)
2785
+ clearTimeout(timeout);
2786
+ }
2787
+ }
2788
+ async function getJson(endpoint, opts) {
2789
+ const controller = opts.timeoutMs ? new AbortController() : void 0;
2790
+ const timeout = opts.timeoutMs ? setTimeout(() => controller?.abort(), opts.timeoutMs) : void 0;
2791
+ try {
2792
+ const response = await fetch(`${normalizeWechatApiBaseUrl(opts.apiBaseUrl)}/${endpoint}`, {
2793
+ method: "GET",
2794
+ headers: buildHeaders(opts.token),
2795
+ ...controller ? { signal: controller.signal } : {}
2796
+ });
2797
+ const text = await response.text();
2798
+ if (!response.ok) {
2799
+ throw new Error(`${endpoint} ${response.status}: ${text || response.statusText}`);
2800
+ }
2801
+ return JSON.parse(text);
2802
+ } finally {
2803
+ if (timeout)
2804
+ clearTimeout(timeout);
2805
+ }
2806
+ }
2807
+ async function startQrLogin(opts) {
2808
+ const response = await postJson(
2809
+ `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(opts.botType)}`,
2810
+ {
2811
+ local_token_list: [],
2812
+ base_info: buildBaseInfo(opts.botAgent)
2813
+ },
2814
+ { apiBaseUrl: opts.apiBaseUrl, botAgent: opts.botAgent, timeoutMs: 15e3 }
2815
+ );
2816
+ if (!response.qrcode || !response.qrcode_img_content) {
2817
+ throw new Error("\u5FAE\u4FE1\u767B\u5F55\u4E8C\u7EF4\u7801\u83B7\u53D6\u5931\u8D25\u3002");
2818
+ }
2819
+ return {
2820
+ qrCode: response.qrcode,
2821
+ qrCodeUrl: response.qrcode_img_content
2822
+ };
2823
+ }
2824
+ async function pollQrStatus(opts) {
2825
+ const query = new URLSearchParams({ qrcode: opts.qrCode });
2826
+ if (opts.verifyCode)
2827
+ query.set("verify_code", opts.verifyCode);
2828
+ const response = await getJson(`ilink/bot/get_qrcode_status?${query.toString()}`, {
2829
+ apiBaseUrl: opts.apiBaseUrl,
2830
+ botAgent: opts.botAgent,
2831
+ timeoutMs: 35e3
2832
+ });
2833
+ return {
2834
+ status: response.status ?? "wait",
2835
+ botToken: response.bot_token,
2836
+ ilinkUserId: response.ilink_user_id,
2837
+ baseUrl: response.baseurl ? normalizeWechatApiBaseUrl(response.baseurl) : void 0,
2838
+ redirectHost: response.redirect_host
2839
+ };
2840
+ }
2841
+ async function getUpdates(opts) {
2842
+ try {
2843
+ return await postJson(
2844
+ "ilink/bot/getupdates",
2845
+ {
2846
+ get_updates_buf: opts.syncCursor ?? "",
2847
+ base_info: buildBaseInfo(opts.botAgent)
2848
+ },
2849
+ opts
2850
+ );
2851
+ } catch (error) {
2852
+ if (error instanceof Error && error.name === "AbortError") {
2853
+ return {
2854
+ ret: 0,
2855
+ msgs: [],
2856
+ get_updates_buf: opts.syncCursor
2857
+ };
2858
+ }
2859
+ throw error;
2860
+ }
2861
+ }
2862
+ async function sendTextMessage(opts) {
2863
+ await postJson(
2864
+ "ilink/bot/sendmessage",
2865
+ {
2866
+ msg: {
2867
+ from_user_id: "",
2868
+ to_user_id: opts.toUserId,
2869
+ client_id: crypto.randomUUID(),
2870
+ message_type: 2,
2871
+ message_state: 2,
2872
+ item_list: opts.text ? [{ type: 1, text_item: { text: opts.text } }] : void 0,
2873
+ context_token: opts.contextToken ?? void 0
2874
+ },
2875
+ base_info: buildBaseInfo(opts.botAgent)
2876
+ },
2877
+ opts
2878
+ );
2879
+ }
2880
+ async function getTypingTicket(opts) {
2881
+ const response = await postJson(
2882
+ "ilink/bot/getconfig",
2883
+ {
2884
+ ilink_user_id: opts.ilinkUserId,
2885
+ context_token: opts.contextToken ?? void 0,
2886
+ base_info: buildBaseInfo(opts.botAgent)
2887
+ },
2888
+ opts
2889
+ );
2890
+ if (response.ret && response.ret !== 0)
2891
+ return void 0;
2892
+ return response.typing_ticket;
2893
+ }
2894
+ async function sendTypingStatus(opts) {
2895
+ await postJson(
2896
+ "ilink/bot/sendtyping",
2897
+ {
2898
+ ilink_user_id: opts.ilinkUserId,
2899
+ typing_ticket: opts.typingTicket,
2900
+ status: opts.status === "typing" ? 1 : 2,
2901
+ base_info: buildBaseInfo(opts.botAgent)
2902
+ },
2903
+ opts
2904
+ );
2905
+ }
2906
+ async function notifyStart(opts) {
2907
+ await postJson(
2908
+ "ilink/bot/msg/notifystart",
2909
+ { base_info: buildBaseInfo(opts.botAgent) },
2910
+ opts
2911
+ );
2912
+ }
2913
+ async function notifyStop(opts) {
2914
+ await postJson(
2915
+ "ilink/bot/msg/notifystop",
2916
+ { base_info: buildBaseInfo(opts.botAgent) },
2917
+ opts
2918
+ );
2919
+ }
2920
+
2921
+ // src/runtime.ts
2922
+ import { randomUUID as randomUUID3 } from "node:crypto";
2923
+ var MAX_PROCESSED_MESSAGE_IDS = 200;
2924
+ var WechatBridgeRuntime = class {
2925
+ aamp;
2926
+ config;
2927
+ configDir;
2928
+ logger;
2929
+ activeStreamSubscriptions = /* @__PURE__ */ new Map();
2930
+ liveTaskIds = /* @__PURE__ */ new Set();
2931
+ typingTickets = /* @__PURE__ */ new Map();
2932
+ state = createDefaultBridgeState();
2933
+ stopping = false;
2934
+ pollLoopPromise;
2935
+ constructor(config, options = {}) {
2936
+ this.config = config;
2937
+ this.configDir = options.configDir;
2938
+ this.logger = options.logger ?? console;
2939
+ this.aamp = new AampClient({
2940
+ email: config.mailbox.email,
2941
+ mailboxToken: config.mailbox.mailboxToken,
2942
+ smtpPassword: config.mailbox.smtpPassword,
2943
+ baseUrl: config.mailbox.baseUrl
2944
+ });
2945
+ }
2946
+ async start() {
2947
+ this.state = await loadBridgeState(this.configDir);
2948
+ if (!this.state.account?.token) {
2949
+ throw new Error("\u5C1A\u672A\u767B\u5F55\u5FAE\u4FE1\uFF0C\u8BF7\u5148\u6267\u884C `aamp-wechat-bridge login`\u3002");
2950
+ }
2951
+ this.state.tasks = {};
2952
+ this.state.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
2953
+ this.state.lastError = void 0;
2954
+ await this.persistState();
2955
+ this.registerAampHandlers();
2956
+ await this.aamp.connect();
2957
+ await notifyStart({
2958
+ apiBaseUrl: this.state.account.baseUrl,
2959
+ token: this.state.account.token,
2960
+ botAgent: this.config.wechat.botAgent,
2961
+ timeoutMs: 1e4
2962
+ }).catch(() => {
2963
+ });
2964
+ this.pollLoopPromise = this.pollLoop();
2965
+ await this.pollLoopPromise;
2966
+ }
2967
+ async stop() {
2968
+ if (this.stopping)
2969
+ return;
2970
+ this.stopping = true;
2971
+ for (const subscription of this.activeStreamSubscriptions.values()) {
2972
+ subscription.close();
2973
+ }
2974
+ this.activeStreamSubscriptions.clear();
2975
+ this.liveTaskIds.clear();
2976
+ if (this.state.account?.token) {
2977
+ await notifyStop({
2978
+ apiBaseUrl: this.state.account.baseUrl,
2979
+ token: this.state.account.token,
2980
+ botAgent: this.config.wechat.botAgent,
2981
+ timeoutMs: 1e4
2982
+ }).catch(() => {
2983
+ });
2984
+ }
2985
+ this.aamp.disconnect();
2986
+ this.state.lastStoppedAt = (/* @__PURE__ */ new Date()).toISOString();
2987
+ await this.persistState();
2988
+ }
2989
+ registerAampHandlers() {
2990
+ this.aamp.on("task.ack", (task) => {
2991
+ void this.handleTaskAck(task);
2992
+ });
2993
+ this.aamp.on("task.stream.opened", (task) => {
2994
+ void this.handleTaskStreamOpened(task);
2995
+ });
2996
+ this.aamp.on("task.result", (task) => {
2997
+ void this.handleTaskResult(task);
2998
+ });
2999
+ this.aamp.on("task.help_needed", (task) => {
3000
+ void this.handleTaskHelp(task);
3001
+ });
3002
+ this.aamp.on("error", (error) => {
3003
+ this.state.lastError = error.message;
3004
+ this.logger.error(`[aamp] ${error.message}`);
3005
+ void this.persistState();
3006
+ });
3007
+ }
3008
+ async pollLoop() {
3009
+ while (!this.stopping) {
3010
+ try {
3011
+ await this.pollOnce();
3012
+ } catch (error) {
3013
+ const message = error instanceof Error ? error.message : String(error);
3014
+ this.state.lastError = message;
3015
+ this.logger.error(`[wechat] ${message}`);
3016
+ await this.persistState();
3017
+ if (message.includes("session timeout") || message.includes("errcode=-14")) {
3018
+ throw error;
3019
+ }
3020
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
3021
+ }
3022
+ }
3023
+ }
3024
+ async pollOnce() {
3025
+ if (!this.state.account?.token)
3026
+ return;
3027
+ const response = await getUpdates({
3028
+ apiBaseUrl: this.state.account.baseUrl,
3029
+ token: this.state.account.token,
3030
+ botAgent: this.config.wechat.botAgent,
3031
+ timeoutMs: this.config.behavior.pollTimeoutMs,
3032
+ syncCursor: this.state.syncCursor
3033
+ });
3034
+ if (response.errcode === -14) {
3035
+ throw new Error("WeChat session timeout (errcode=-14). Please run `aamp-wechat-bridge login` again.");
3036
+ }
3037
+ if (response.ret && response.ret !== 0) {
3038
+ throw new Error(`WeChat getupdates failed: errcode=${response.errcode ?? response.ret} ${response.errmsg ?? ""}`.trim());
3039
+ }
3040
+ if (response.get_updates_buf && response.get_updates_buf !== this.state.syncCursor) {
3041
+ this.state.syncCursor = response.get_updates_buf;
3042
+ await this.persistState();
3043
+ }
3044
+ for (const message of response.msgs ?? []) {
3045
+ await this.handleInboundWechatMessage(message);
3046
+ }
3047
+ }
3048
+ async handleInboundWechatMessage(message) {
3049
+ if (message.group_id)
3050
+ return;
3051
+ if (message.message_type === 2)
3052
+ return;
3053
+ const senderId = message.from_user_id?.trim();
3054
+ if (!senderId)
3055
+ return;
3056
+ const messageKey = String(message.message_id ?? "");
3057
+ if (messageKey && this.state.processedMessageIds.includes(messageKey)) {
3058
+ return;
3059
+ }
3060
+ const text = this.extractMessageText(message.item_list ?? []);
3061
+ const mediaNote = this.extractMediaNote(message.item_list ?? []);
3062
+ const body = [text, mediaNote].filter(Boolean).join("\n\n").trim();
3063
+ if (!body)
3064
+ return;
3065
+ const taskId = randomUUID3();
3066
+ const sessionKey = this.buildSessionKey(senderId);
3067
+ const contextToken = message.context_token?.trim() || void 0;
3068
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3069
+ this.liveTaskIds.add(taskId);
3070
+ this.state.tasks[taskId] = {
3071
+ taskId,
3072
+ senderId,
3073
+ sessionKey,
3074
+ ...contextToken ? { contextToken } : {},
3075
+ status: "received",
3076
+ createdAt: now,
3077
+ updatedAt: now
3078
+ };
3079
+ this.state.conversations[sessionKey] = {
3080
+ senderId,
3081
+ sessionKey,
3082
+ lastTaskId: taskId,
3083
+ ...contextToken ? { lastContextToken: contextToken } : {},
3084
+ updatedAt: now
3085
+ };
3086
+ if (contextToken) {
3087
+ this.state.contextTokens[senderId] = contextToken;
3088
+ }
3089
+ if (messageKey) {
3090
+ this.rememberProcessedMessageId(messageKey);
3091
+ }
3092
+ await this.persistState();
3093
+ await this.aamp.sendTask({
3094
+ to: this.config.targetAgentEmail,
3095
+ taskId,
3096
+ sessionKey,
3097
+ title: `WeChat DM from ${senderId}`,
3098
+ bodyText: body,
3099
+ dispatchContext: {
3100
+ source: "wechat",
3101
+ wechat_account_id: this.state.account?.accountId ?? "default",
3102
+ wechat_sender_id: senderId,
3103
+ ...contextToken ? { wechat_context_token: contextToken } : {},
3104
+ ...message.session_id ? { wechat_session_id: message.session_id } : {},
3105
+ ...message.message_id != null ? { wechat_message_id: String(message.message_id) } : {}
3106
+ }
3107
+ });
3108
+ }
3109
+ extractMessageText(items) {
3110
+ const parts = [];
3111
+ for (const item of items) {
3112
+ const text = item.text_item?.text?.trim();
3113
+ if (text)
3114
+ parts.push(text);
3115
+ const voiceText = item.voice_item?.text?.trim();
3116
+ if (voiceText)
3117
+ parts.push(voiceText);
3118
+ }
3119
+ return parts.join("\n").trim();
3120
+ }
3121
+ extractMediaNote(items) {
3122
+ const labels = [];
3123
+ for (const item of items) {
3124
+ if (item.type === 2)
3125
+ labels.push("image");
3126
+ if (item.type === 3)
3127
+ labels.push("voice");
3128
+ if (item.type === 4)
3129
+ labels.push(`file${item.file_item?.file_name ? ` (${item.file_item.file_name})` : ""}`);
3130
+ if (item.type === 5)
3131
+ labels.push("video");
3132
+ }
3133
+ if (labels.length === 0)
3134
+ return "";
3135
+ return `User also sent WeChat media: ${labels.join(", ")}. Native media relay is not implemented yet, so please answer based on the textual context only.`;
3136
+ }
3137
+ buildSessionKey(senderId) {
3138
+ return `wechat:${this.state.account?.accountId ?? "default"}:${senderId}`;
3139
+ }
3140
+ rememberProcessedMessageId(messageId) {
3141
+ this.state.processedMessageIds.push(messageId);
3142
+ if (this.state.processedMessageIds.length > MAX_PROCESSED_MESSAGE_IDS) {
3143
+ this.state.processedMessageIds.splice(0, this.state.processedMessageIds.length - MAX_PROCESSED_MESSAGE_IDS);
3144
+ }
3145
+ }
3146
+ async handleTaskAck(task) {
3147
+ const state = this.state.tasks[task.taskId];
3148
+ if (!state || !this.liveTaskIds.has(task.taskId))
3149
+ return;
3150
+ state.status = "pending";
3151
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3152
+ await this.ensureTyping(state, "typing");
3153
+ await this.persistState();
3154
+ }
3155
+ async handleTaskStreamOpened(task) {
3156
+ const state = this.state.tasks[task.taskId];
3157
+ if (!state || !this.liveTaskIds.has(task.taskId))
3158
+ return;
3159
+ state.status = "streaming";
3160
+ state.streamId = task.streamId;
3161
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3162
+ await this.subscribeToTaskStream(state);
3163
+ await this.persistState();
3164
+ }
3165
+ async subscribeToTaskStream(task) {
3166
+ if (!task.streamId || this.activeStreamSubscriptions.has(task.taskId))
3167
+ return;
3168
+ const subscription = await this.aamp.subscribeStream(
3169
+ task.streamId,
3170
+ {
3171
+ onEvent: (event) => {
3172
+ void this.handleStreamEvent(task.taskId, event);
3173
+ },
3174
+ onError: (error) => {
3175
+ this.logger.error(`[stream ${task.taskId}] ${error.message}`);
3176
+ }
3177
+ }
3178
+ );
3179
+ this.activeStreamSubscriptions.set(task.taskId, subscription);
3180
+ }
3181
+ async handleStreamEvent(taskId, event) {
3182
+ const state = this.state.tasks[taskId];
3183
+ if (!state || !this.liveTaskIds.has(taskId))
3184
+ return;
3185
+ if (event.type === "text.delta") {
3186
+ state.streamText = (state.streamText ?? "") + String(event.payload.text ?? "");
3187
+ state.status = "streaming";
3188
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3189
+ await this.persistState();
3190
+ return;
3191
+ }
3192
+ if (event.type === "error") {
3193
+ state.resultError = String(event.payload.message ?? event.payload.error ?? "Unknown stream error");
3194
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3195
+ await this.persistState();
3196
+ }
3197
+ }
3198
+ async handleTaskResult(task) {
3199
+ const state = this.state.tasks[task.taskId];
3200
+ if (!state || !this.liveTaskIds.has(task.taskId))
3201
+ return;
3202
+ state.status = task.status === "completed" ? "completed" : "rejected";
3203
+ state.outputText = task.output || state.streamText || state.outputText;
3204
+ state.resultError = task.errorMsg;
3205
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3206
+ this.closeActiveStream(task.taskId);
3207
+ await this.ensureTyping(state, "cancel");
3208
+ await this.replyToWechat(state, this.buildReplyTextFromResult(task, state));
3209
+ this.finishTask(task.taskId);
3210
+ }
3211
+ async handleTaskHelp(task) {
3212
+ const state = this.state.tasks[task.taskId];
3213
+ if (!state || !this.liveTaskIds.has(task.taskId))
3214
+ return;
3215
+ state.status = "help_needed";
3216
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3217
+ this.closeActiveStream(task.taskId);
3218
+ await this.ensureTyping(state, "cancel");
3219
+ await this.replyToWechat(state, this.buildReplyTextFromHelp(task));
3220
+ this.finishTask(task.taskId);
3221
+ }
3222
+ buildReplyTextFromResult(task, state) {
3223
+ const body = (task.output || state.streamText || "").trim();
3224
+ const base = body || (task.status === "rejected" ? "\u5F53\u524D\u8BF7\u6C42\u88AB\u76EE\u6807 Agent \u62D2\u7EDD\u4E86\u3002" : "\u5DF2\u7ECF\u5904\u7406\u5B8C\u6210\u3002");
3225
+ const attachmentLines = (task.attachments ?? []).map((attachment) => `- ${attachment.filename} (${attachment.size} bytes)`);
3226
+ const senderPolicyHint = this.buildSenderPolicyHint(task.errorMsg, state);
3227
+ return [
3228
+ base,
3229
+ ...attachmentLines.length ? ["", "\u8FD4\u56DE\u4E2D\u8FD8\u5305\u542B\u9644\u4EF6\uFF0C\u76EE\u524D\u5FAE\u4FE1 bridge \u5148\u53EA\u63D0\u793A\u6587\u4EF6\u540D\uFF1A", ...attachmentLines] : [],
3230
+ ...senderPolicyHint ? ["", senderPolicyHint] : []
3231
+ ].join("\n");
3232
+ }
3233
+ buildReplyTextFromHelp(task) {
3234
+ return [
3235
+ task.question.trim() || "\u6211\u8FD8\u9700\u8981\u4E00\u4E9B\u4FE1\u606F\u624D\u80FD\u7EE7\u7EED\u3002",
3236
+ ...task.blockedReason?.trim() ? ["", `\u539F\u56E0\uFF1A${task.blockedReason.trim()}`] : [],
3237
+ ...task.suggestedOptions?.length ? ["", "\u4F60\u53EF\u4EE5\u8FD9\u6837\u8865\u5145\uFF1A", ...task.suggestedOptions.map((item) => `- ${item}`)] : []
3238
+ ].join("\n");
3239
+ }
3240
+ buildSenderPolicyHint(errorMsg, state) {
3241
+ if (!errorMsg?.includes("senderPolicies"))
3242
+ return "";
3243
+ return [
3244
+ "\u76EE\u6807 Agent \u5F53\u524D\u6CA1\u6709\u653E\u884C\u8FD9\u4E2A\u5FAE\u4FE1\u6865\u3002",
3245
+ "\u8BF7\u5728 target agent \u7684 senderPolicies \u4E2D\u5141\u8BB8\u5F53\u524D bridge \u90AE\u7BB1\uFF0C\u5E76\u628A `wechat_sender_id` \u4F5C\u4E3A dispatchContext \u767D\u540D\u5355\u6761\u4EF6\u3002",
3246
+ "",
3247
+ "\u793A\u4F8B\uFF1A",
3248
+ "[",
3249
+ ` {"sender":"${this.config.mailbox.email}","dispatchContextRules":{"wechat_sender_id":["${state.senderId}"]}}`,
3250
+ "]"
3251
+ ].join("\n");
3252
+ }
3253
+ async replyToWechat(task, text) {
3254
+ if (!this.state.account?.token)
3255
+ return;
3256
+ await sendTextMessage({
3257
+ apiBaseUrl: this.state.account.baseUrl,
3258
+ token: this.state.account.token,
3259
+ botAgent: this.config.wechat.botAgent,
3260
+ timeoutMs: 15e3,
3261
+ toUserId: task.senderId,
3262
+ text,
3263
+ contextToken: task.contextToken ?? this.state.contextTokens[task.senderId]
3264
+ });
3265
+ }
3266
+ async ensureTyping(task, status) {
3267
+ if (!this.state.account?.token)
3268
+ return;
3269
+ const cachedTicket = this.typingTickets.get(task.senderId);
3270
+ const ticket = cachedTicket || await getTypingTicket({
3271
+ apiBaseUrl: this.state.account.baseUrl,
3272
+ token: this.state.account.token,
3273
+ botAgent: this.config.wechat.botAgent,
3274
+ timeoutMs: 1e4,
3275
+ ilinkUserId: task.senderId,
3276
+ contextToken: task.contextToken ?? this.state.contextTokens[task.senderId]
3277
+ }).catch(() => void 0);
3278
+ if (!ticket)
3279
+ return;
3280
+ this.typingTickets.set(task.senderId, ticket);
3281
+ await sendTypingStatus({
3282
+ apiBaseUrl: this.state.account.baseUrl,
3283
+ token: this.state.account.token,
3284
+ botAgent: this.config.wechat.botAgent,
3285
+ timeoutMs: 1e4,
3286
+ ilinkUserId: task.senderId,
3287
+ typingTicket: ticket,
3288
+ status
3289
+ }).catch(() => {
3290
+ });
3291
+ task.typingActive = status === "typing";
3292
+ }
3293
+ closeActiveStream(taskId) {
3294
+ const subscription = this.activeStreamSubscriptions.get(taskId);
3295
+ if (!subscription)
3296
+ return;
3297
+ subscription.close();
3298
+ this.activeStreamSubscriptions.delete(taskId);
3299
+ }
3300
+ finishTask(taskId) {
3301
+ this.liveTaskIds.delete(taskId);
3302
+ delete this.state.tasks[taskId];
3303
+ void this.persistState();
3304
+ }
3305
+ async persistState() {
3306
+ await saveBridgeState(this.state, this.configDir);
3307
+ }
3308
+ };
3309
+
3310
+ // src/index.ts
3311
+ function printUsage() {
3312
+ console.log([
3313
+ "Usage: aamp-wechat-bridge <command> [options]",
3314
+ "",
3315
+ "Commands:",
3316
+ " init Create or update local bridge config and AAMP mailbox credentials",
3317
+ " login Start QR login and persist the WeChat bot token locally",
3318
+ " run Start the local WeChat bridge daemon",
3319
+ " status Show local config, login state, and target agent information",
3320
+ "",
3321
+ "Options:",
3322
+ " --config-dir <path> Override bridge config directory",
3323
+ "",
3324
+ "Examples:",
3325
+ " aamp-wechat-bridge init",
3326
+ " aamp-wechat-bridge login",
3327
+ " aamp-wechat-bridge run"
3328
+ ].join("\n"));
3329
+ }
3330
+ function parseCliArgs(rawArgs) {
3331
+ let command = "run";
3332
+ let commandAssigned = false;
3333
+ let configDir;
3334
+ const options = {};
3335
+ for (let index = 0; index < rawArgs.length; index += 1) {
3336
+ const token = rawArgs[index];
3337
+ if (!commandAssigned && !token.startsWith("-")) {
3338
+ command = token;
3339
+ commandAssigned = true;
3340
+ continue;
3341
+ }
3342
+ if (token === "--config-dir") {
3343
+ const value = rawArgs[index + 1];
3344
+ if (!value)
3345
+ throw new Error("--config-dir requires a value");
3346
+ configDir = value;
3347
+ index += 1;
3348
+ continue;
3349
+ }
3350
+ if (token.startsWith("--")) {
3351
+ const key = token.slice(2);
3352
+ const next = rawArgs[index + 1];
3353
+ if (!next || next.startsWith("--")) {
3354
+ options[key] = true;
3355
+ continue;
3356
+ }
3357
+ options[key] = next;
3358
+ index += 1;
3359
+ continue;
3360
+ }
3361
+ }
3362
+ return { command, configDir, options };
3363
+ }
3364
+ async function requireConfig(configDir) {
3365
+ const config = await loadBridgeConfig(configDir);
3366
+ if (!config) {
3367
+ throw new Error(`Bridge config not found at ${getBridgeHomeDir(configDir)}/config.json. Run \`aamp-wechat-bridge init\` first.`);
3368
+ }
3369
+ return config;
3370
+ }
3371
+ function printQrCode(url) {
3372
+ qrcodeTerminal.generate(url, { small: true });
3373
+ console.log(`\u626B\u7801\u94FE\u63A5: ${url}`);
3374
+ }
3375
+ async function promptVerifyCode() {
3376
+ const rl = readline2.createInterface({ input: input2, output: output2 });
3377
+ try {
3378
+ const value = await rl.question("\u8BF7\u8F93\u5165\u5FAE\u4FE1\u8FD4\u56DE\u7684\u9A8C\u8BC1\u7801: ");
3379
+ return value.trim();
3380
+ } finally {
3381
+ rl.close();
3382
+ }
3383
+ }
3384
+ async function waitForQrLogin(config, state) {
3385
+ const started = await startQrLogin({
3386
+ apiBaseUrl: config.wechat.apiBaseUrl,
3387
+ botType: config.wechat.botType,
3388
+ botAgent: config.wechat.botAgent
3389
+ });
3390
+ console.log("\u8BF7\u4F7F\u7528\u5FAE\u4FE1\u626B\u7801\u767B\u5F55\u3002");
3391
+ printQrCode(started.qrCodeUrl);
3392
+ let currentBaseUrl = config.wechat.apiBaseUrl;
3393
+ let pendingVerifyCode;
3394
+ for (; ; ) {
3395
+ const status = await pollQrStatus({
3396
+ apiBaseUrl: currentBaseUrl,
3397
+ qrCode: started.qrCode,
3398
+ botAgent: config.wechat.botAgent,
3399
+ ...pendingVerifyCode ? { verifyCode: pendingVerifyCode } : {}
3400
+ });
3401
+ pendingVerifyCode = void 0;
3402
+ if (status.status === "wait") {
3403
+ continue;
3404
+ }
3405
+ if (status.status === "scaned") {
3406
+ console.log("\u4E8C\u7EF4\u7801\u5DF2\u626B\u63CF\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u767B\u5F55\u3002");
3407
+ continue;
3408
+ }
3409
+ if (status.status === "scaned_but_redirect") {
3410
+ if (status.redirectHost) {
3411
+ currentBaseUrl = `https://${status.redirectHost.replace(/^https?:\/\//, "").replace(/\/$/, "")}`;
3412
+ }
3413
+ continue;
3414
+ }
3415
+ if (status.status === "need_verifycode") {
3416
+ pendingVerifyCode = await promptVerifyCode();
3417
+ continue;
3418
+ }
3419
+ if (status.status === "verify_code_blocked") {
3420
+ throw new Error("\u5FAE\u4FE1\u767B\u5F55\u9A8C\u8BC1\u5931\u8D25\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C `aamp-wechat-bridge login`\u3002");
3421
+ }
3422
+ if (status.status === "expired") {
3423
+ throw new Error("\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C `aamp-wechat-bridge login`\u3002");
3424
+ }
3425
+ if (status.status === "binded_redirect") {
3426
+ console.log("\u8BE5\u5FAE\u4FE1\u8D26\u53F7\u5DF2\u7ECF\u7ED1\u5B9A\u5230\u5F53\u524D\u672C\u5730 bridge\uFF0C\u7EE7\u7EED\u590D\u7528\u73B0\u6709\u767B\u5F55\u6001\u3002");
3427
+ return state;
3428
+ }
3429
+ if (status.status === "confirmed") {
3430
+ if (!status.botToken) {
3431
+ throw new Error("\u5FAE\u4FE1\u767B\u5F55\u5DF2\u786E\u8BA4\uFF0C\u4F46\u6CA1\u6709\u8FD4\u56DE bot token\u3002");
3432
+ }
3433
+ const accountId = status.ilinkUserId?.trim() || "default";
3434
+ state.account = {
3435
+ accountId,
3436
+ token: status.botToken,
3437
+ baseUrl: status.baseUrl?.trim() || currentBaseUrl,
3438
+ ilinkUserId: status.ilinkUserId?.trim() || void 0,
3439
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
3440
+ };
3441
+ state.lastLoginAt = (/* @__PURE__ */ new Date()).toISOString();
3442
+ return state;
3443
+ }
3444
+ const exhaustive = status.status;
3445
+ throw new Error(`Unexpected QR login state: ${exhaustive}`);
3446
+ }
3447
+ }
3448
+ async function handleInit(configDir, options = {}) {
3449
+ const config = await initializeBridgeConfig({
3450
+ configDir,
3451
+ aampHost: typeof options.aampHost === "string" ? options.aampHost : void 0,
3452
+ targetAgentEmail: typeof options.targetAgentEmail === "string" ? options.targetAgentEmail : void 0,
3453
+ slug: typeof options.slug === "string" ? options.slug : void 0,
3454
+ summary: typeof options.summary === "string" ? options.summary : void 0,
3455
+ botAgent: typeof options.botAgent === "string" ? options.botAgent : void 0,
3456
+ dispatchTimeoutMs: typeof options.dispatchTimeoutMs === "string" ? Number(options.dispatchTimeoutMs) : void 0,
3457
+ pollTimeoutMs: typeof options.pollTimeoutMs === "string" ? Number(options.pollTimeoutMs) : void 0
3458
+ });
3459
+ console.log(`Bridge config saved to ${getBridgeHomeDir(configDir)}/config.json`);
3460
+ console.log(`Mailbox: ${config.mailbox.email}`);
3461
+ console.log(`Target agent: ${config.targetAgentEmail}`);
3462
+ }
3463
+ async function handleLogin(configDir) {
3464
+ const config = await requireConfig(configDir);
3465
+ const state = await loadBridgeState(configDir);
3466
+ const nextState = await waitForQrLogin(config, state);
3467
+ await saveBridgeState(nextState, configDir);
3468
+ console.log(`\u5FAE\u4FE1\u767B\u5F55\u6210\u529F\uFF0C\u8D26\u53F7\u6807\u8BC6: ${nextState.account?.accountId ?? "default"}`);
3469
+ }
3470
+ async function handleRun(configDir) {
3471
+ const config = await requireConfig(configDir);
3472
+ const state = await loadBridgeState(configDir);
3473
+ if (!state.account?.token) {
3474
+ throw new Error("\u5C1A\u672A\u767B\u5F55\u5FAE\u4FE1\uFF0C\u8BF7\u5148\u6267\u884C `aamp-wechat-bridge login`\u3002");
3475
+ }
3476
+ const runtime = new WechatBridgeRuntime(config, {
3477
+ configDir,
3478
+ logger: console
3479
+ });
3480
+ const shutdown = async () => {
3481
+ await runtime.stop().catch(() => {
3482
+ });
3483
+ exit(0);
3484
+ };
3485
+ process.once("SIGINT", () => {
3486
+ void shutdown();
3487
+ });
3488
+ process.once("SIGTERM", () => {
3489
+ void shutdown();
3490
+ });
3491
+ console.log(`WeChat bridge is running for ${config.targetAgentEmail}`);
3492
+ console.log(`Mailbox: ${config.mailbox.email}`);
3493
+ await runtime.start();
3494
+ }
3495
+ async function handleStatus(configDir) {
3496
+ const config = await requireConfig(configDir);
3497
+ const state = await loadBridgeState(configDir);
3498
+ console.log([
3499
+ `Bridge home: ${getBridgeHomeDir(configDir)}`,
3500
+ `AAMP host: ${config.aampHost}`,
3501
+ `Target agent: ${config.targetAgentEmail}`,
3502
+ `Mailbox: ${config.mailbox.email}`,
3503
+ `WeChat API base: ${config.wechat.apiBaseUrl}`,
3504
+ `Bot agent: ${config.wechat.botAgent}`,
3505
+ `Logged in: ${state.account?.token ? "yes" : "no"}`,
3506
+ ...state.account?.token ? [
3507
+ `WeChat account: ${state.account.accountId}`,
3508
+ `Login base URL: ${state.account.baseUrl}`,
3509
+ `Last login: ${state.lastLoginAt ?? state.account.connectedAt}`
3510
+ ] : []
3511
+ ].join("\n"));
3512
+ }
3513
+ async function main() {
3514
+ const parsed = parseCliArgs(argv.slice(2));
3515
+ switch (parsed.command) {
3516
+ case "help":
3517
+ case "--help":
3518
+ case "-h":
3519
+ printUsage();
3520
+ return;
3521
+ case "init":
3522
+ await handleInit(parsed.configDir, parsed.options);
3523
+ return;
3524
+ case "login":
3525
+ await handleLogin(parsed.configDir);
3526
+ return;
3527
+ case "run":
3528
+ await handleRun(parsed.configDir);
3529
+ return;
3530
+ case "status":
3531
+ await handleStatus(parsed.configDir);
3532
+ return;
3533
+ default:
3534
+ throw new Error(`Unknown command: ${parsed.command}`);
3535
+ }
3536
+ }
3537
+ main().catch((error) => {
3538
+ console.error(error.message);
3539
+ exit(1);
3540
+ });
3541
+ //# sourceMappingURL=index.js.map