aamp-openclaw-plugin 0.1.42 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -910,21 +910,17 @@ var JmapPushClient = class extends TinyEmitter {
910
910
  this.emit("error", new Error(`JMAP WebSocket handshake failed: ${res.statusCode ?? "unknown"} ${res.statusMessage ?? ""}${headerSummary ? ` | headers: ${headerSummary}` : ""}`));
911
911
  this.scheduleReconnect();
912
912
  });
913
- this.ws.on("open", async () => {
914
- this.connecting = false;
915
- this.connected = true;
916
- this.stopPolling();
917
- this.startPingHeartbeat();
918
- const accountId = this.session?.primaryAccounts["urn:ietf:params:jmap:mail"];
919
- if (accountId && this.emailState === null) {
920
- await this.initEmailState(accountId);
921
- }
922
- this.ws.send(JSON.stringify({
923
- "@type": "WebSocketPushEnable",
924
- dataTypes: ["Email"],
925
- pushState: null
926
- }));
927
- this.emit("connected");
913
+ this.ws.on("open", () => {
914
+ void this.handleWebSocketOpen().catch((err) => {
915
+ const reason = `websocket open initialization failed: ${err.message}`;
916
+ this.connecting = false;
917
+ this.connected = false;
918
+ this.stopPingHeartbeat();
919
+ this.startPolling(reason);
920
+ this.emit("error", new Error(`JMAP ${reason}`));
921
+ this.ws?.close();
922
+ this.scheduleReconnect();
923
+ });
928
924
  });
929
925
  this.ws.on("pong", () => {
930
926
  });
@@ -977,6 +973,22 @@ var JmapPushClient = class extends TinyEmitter {
977
973
  this.pingTimer = null;
978
974
  }
979
975
  }
976
+ async handleWebSocketOpen() {
977
+ this.connecting = false;
978
+ this.connected = true;
979
+ this.stopPolling();
980
+ this.startPingHeartbeat();
981
+ const accountId = this.session?.primaryAccounts["urn:ietf:params:jmap:mail"];
982
+ if (accountId && this.emailState === null) {
983
+ await this.initEmailState(accountId);
984
+ }
985
+ this.ws?.send(JSON.stringify({
986
+ "@type": "WebSocketPushEnable",
987
+ dataTypes: ["Email"],
988
+ pushState: null
989
+ }));
990
+ this.emit("connected");
991
+ }
980
992
  startSafetySync() {
981
993
  if (this.safetySyncTimer)
982
994
  return;
@@ -1196,6 +1208,7 @@ var JmapPushClient = class extends TinyEmitter {
1196
1208
 
1197
1209
  // ../sdk/dist/pairing.js
1198
1210
  import { randomBytes } from "node:crypto";
1211
+ var DEFAULT_PAIRING_WEB_URL = "https://meshmail.ai/pair";
1199
1212
  function normalizeMailbox(value) {
1200
1213
  const mailbox = value.trim().toLowerCase();
1201
1214
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mailbox)) {
@@ -1243,6 +1256,23 @@ function buildPairingUrl(payload) {
1243
1256
  }
1244
1257
  return url.toString();
1245
1258
  }
1259
+ function buildPairingWebUrl(payload, baseUrl2 = DEFAULT_PAIRING_WEB_URL) {
1260
+ const mailbox = normalizeMailbox(payload.mailbox);
1261
+ const pairCode = payload.pairCode.trim();
1262
+ if (!pairCode)
1263
+ throw new Error("pairCode cannot be empty");
1264
+ const url = new URL(baseUrl2);
1265
+ url.searchParams.set("mailbox", mailbox);
1266
+ url.searchParams.set("pair_code", pairCode);
1267
+ const rules = normalizeDispatchContextRules(payload.dispatchContextRules);
1268
+ if (rules) {
1269
+ url.searchParams.set("dispatch_context_rules", encodeBase64UrlJson2(rules));
1270
+ }
1271
+ return url.toString();
1272
+ }
1273
+ function pairingUrlToWebUrl(input, baseUrl2 = DEFAULT_PAIRING_WEB_URL) {
1274
+ return buildPairingWebUrl(parsePairingUrl(input), baseUrl2);
1275
+ }
1246
1276
  function createPairingCode(options) {
1247
1277
  const pairCode = options.pairCode?.trim() || randomBytes(6).toString("base64url");
1248
1278
  const dispatchContextRules = normalizeDispatchContextRules(options.dispatchContextRules);
@@ -1265,8 +1295,10 @@ function parsePairingUrl(input) {
1265
1295
  } catch {
1266
1296
  throw new Error("Invalid pairing URL");
1267
1297
  }
1268
- if (url.protocol !== "aamp:" || url.hostname !== "connect") {
1269
- throw new Error("Pairing URL must start with aamp://connect");
1298
+ const isDeepLink = url.protocol === "aamp:" && url.hostname === "connect";
1299
+ const isWebLink = (url.protocol === "https:" || url.protocol === "http:") && url.hostname === "meshmail.ai" && url.pathname === "/pair";
1300
+ if (!isDeepLink && !isWebLink) {
1301
+ throw new Error("Pairing URL must start with aamp://connect or https://meshmail.ai/pair");
1270
1302
  }
1271
1303
  const mailbox = url.searchParams.get("mailbox") ?? "";
1272
1304
  const pairCode = url.searchParams.get("pair_code") ?? "";
@@ -1585,37 +1617,102 @@ var SmtpSender = class _SmtpSender {
1585
1617
  const data = await res.json();
1586
1618
  return data.methodResponses ?? [];
1587
1619
  }
1588
- async uploadSentAttachment(attachment) {
1620
+ async uploadJmapBlob(params) {
1589
1621
  const session = await this.resolveJmapSession();
1590
- const content = typeof attachment.content === "string" ? Buffer.from(attachment.content, "base64") : attachment.content;
1591
1622
  const uploadUrl = session.uploadUrl.replace(/\{accountId\}|%7BaccountId%7D/gi, encodeURIComponent(session.accountId));
1592
1623
  const res = await this.fetch(uploadUrl, {
1593
1624
  method: "POST",
1594
1625
  headers: {
1595
1626
  Authorization: this.getJmapAuthHeader(),
1596
- "Content-Type": attachment.contentType
1627
+ "Content-Type": params.contentType
1597
1628
  },
1598
- body: content
1629
+ body: params.content
1599
1630
  });
1600
1631
  const bodyText = await res.text();
1601
1632
  if (!res.ok) {
1602
- throw new Error(`JMAP attachment upload failed: ${res.status} ${bodyText}`);
1633
+ throw new Error(`JMAP blob upload failed: ${res.status} ${bodyText}`);
1603
1634
  }
1604
1635
  let data;
1605
1636
  try {
1606
1637
  data = JSON.parse(bodyText);
1607
1638
  } catch {
1608
- throw new Error("JMAP attachment upload returned invalid JSON");
1639
+ throw new Error("JMAP blob upload returned invalid JSON");
1609
1640
  }
1610
1641
  if (!data.blobId) {
1611
- throw new Error("JMAP attachment upload did not return blobId");
1642
+ throw new Error("JMAP blob upload did not return blobId");
1612
1643
  }
1613
1644
  return {
1614
1645
  blobId: data.blobId,
1615
- type: data.type ?? attachment.contentType,
1616
- size: data.size ?? content.byteLength,
1617
- name: attachment.filename
1646
+ type: data.type ?? params.contentType,
1647
+ size: data.size ?? params.content.byteLength
1648
+ };
1649
+ }
1650
+ async buildRawSentMessage(params) {
1651
+ const rawTransport = createTransport({
1652
+ streamTransport: true,
1653
+ buffer: true,
1654
+ newline: "unix"
1655
+ });
1656
+ const mailOptions = {
1657
+ from: params.from,
1658
+ to: params.to,
1659
+ subject: params.subject,
1660
+ text: params.text,
1661
+ headers: params.aampHeaders
1618
1662
  };
1663
+ if (params.messageId)
1664
+ mailOptions.messageId = sanitize(params.messageId);
1665
+ if (params.inReplyTo)
1666
+ mailOptions.inReplyTo = params.inReplyTo;
1667
+ if (params.references)
1668
+ mailOptions.references = params.references;
1669
+ if (params.attachments?.length) {
1670
+ mailOptions.attachments = params.attachments.map((attachment) => ({
1671
+ filename: attachment.filename,
1672
+ contentType: attachment.contentType,
1673
+ content: typeof attachment.content === "string" ? Buffer.from(attachment.content, "base64") : attachment.content
1674
+ }));
1675
+ }
1676
+ const info = await rawTransport.sendMail(mailOptions);
1677
+ const rawMessage = info.message;
1678
+ if (Buffer.isBuffer(rawMessage))
1679
+ return rawMessage;
1680
+ if (rawMessage instanceof Uint8Array)
1681
+ return Buffer.from(rawMessage);
1682
+ if (typeof rawMessage === "string")
1683
+ return Buffer.from(rawMessage);
1684
+ throw new Error("Raw message generation did not return a Buffer");
1685
+ }
1686
+ async importRawSentMessage(params) {
1687
+ if (!this.canPersistSentCopy())
1688
+ return;
1689
+ const sentMailboxId = await this.getSentMailboxId();
1690
+ if (!sentMailboxId)
1691
+ return;
1692
+ const rawMessage = await this.buildRawSentMessage(params);
1693
+ const uploadedMessage = await this.uploadJmapBlob({
1694
+ content: rawMessage,
1695
+ contentType: "message/rfc822"
1696
+ });
1697
+ const responses = await this.jmapCall([
1698
+ [
1699
+ "Email/import",
1700
+ {
1701
+ emails: {
1702
+ sent1: {
1703
+ blobId: uploadedMessage.blobId,
1704
+ mailboxIds: { [sentMailboxId]: true },
1705
+ keywords: { "$seen": true }
1706
+ }
1707
+ }
1708
+ },
1709
+ "import1"
1710
+ ]
1711
+ ]);
1712
+ const result = responses.find(([name]) => name === "Email/import")?.[1];
1713
+ if (result?.notImported?.sent1) {
1714
+ throw new Error(`JMAP sent message import failed: ${JSON.stringify(result.notImported.sent1)}`);
1715
+ }
1619
1716
  }
1620
1717
  async getSentMailboxId() {
1621
1718
  if (!this.sentMailboxIdPromise) {
@@ -1638,10 +1735,13 @@ var SmtpSender = class _SmtpSender {
1638
1735
  async saveToSent(params) {
1639
1736
  if (!this.canPersistSentCopy())
1640
1737
  return;
1738
+ if (params.attachments?.length) {
1739
+ await this.importRawSentMessage(params);
1740
+ return;
1741
+ }
1641
1742
  const sentMailboxId = await this.getSentMailboxId();
1642
1743
  if (!sentMailboxId)
1643
1744
  return;
1644
- const uploadedAttachments = params.attachments?.length ? await Promise.all(params.attachments.map((attachment) => this.uploadSentAttachment(attachment))) : [];
1645
1745
  const emailCreate = {
1646
1746
  mailboxIds: { [sentMailboxId]: true },
1647
1747
  from: [{ email: params.from }],
@@ -1649,35 +1749,13 @@ var SmtpSender = class _SmtpSender {
1649
1749
  subject: params.subject,
1650
1750
  keywords: { "$seen": true }
1651
1751
  };
1652
- if (uploadedAttachments.length) {
1653
- emailCreate.bodyStructure = {
1654
- type: "multipart/mixed",
1655
- subParts: [
1656
- { partId: "body", type: "text/plain" },
1657
- ...uploadedAttachments.map((attachment) => ({
1658
- blobId: attachment.blobId,
1659
- type: attachment.type,
1660
- size: attachment.size,
1661
- name: attachment.name,
1662
- disposition: "attachment"
1663
- }))
1664
- ]
1665
- };
1666
- emailCreate.bodyValues = {
1667
- body: {
1668
- value: params.text,
1669
- isTruncated: false
1670
- }
1671
- };
1672
- } else {
1673
- emailCreate.bodyValues = {
1674
- body: {
1675
- value: params.text,
1676
- charset: "utf-8"
1677
- }
1678
- };
1679
- emailCreate.textBody = [{ partId: "body", type: "text/plain" }];
1680
- }
1752
+ emailCreate.bodyValues = {
1753
+ body: {
1754
+ value: params.text,
1755
+ charset: "utf-8"
1756
+ }
1757
+ };
1758
+ emailCreate.textBody = [{ partId: "body", type: "text/plain" }];
1681
1759
  if (params.inReplyTo) {
1682
1760
  emailCreate["header:In-Reply-To:asText"] = ` ${sanitize(params.inReplyTo)}`;
1683
1761
  }
@@ -2350,6 +2428,25 @@ function buildRegisteredCommandDispatchPayload(opts) {
2350
2428
  };
2351
2429
  }
2352
2430
  var DEFAULT_TASK_DISPATCH_CONCURRENCY = 10;
2431
+ function asStreamRecord(value) {
2432
+ if (!value || typeof value !== "object" || Array.isArray(value))
2433
+ return void 0;
2434
+ return value;
2435
+ }
2436
+ function normalizeSseStreamEvent(params) {
2437
+ const record = asStreamRecord(params.data) ?? {};
2438
+ const payload = asStreamRecord(record.payload) ?? record;
2439
+ const seqValue = Number(record.seq);
2440
+ return {
2441
+ id: typeof record.id === "string" && record.id ? record.id : params.eventId,
2442
+ streamId: String(record.streamId ?? params.streamId),
2443
+ taskId: String(record.taskId ?? ""),
2444
+ seq: Number.isFinite(seqValue) ? seqValue : params.seq,
2445
+ timestamp: String(record.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()),
2446
+ type: String(record.type ?? params.eventType ?? "message"),
2447
+ payload
2448
+ };
2449
+ }
2353
2450
  function normalizeTaskDispatchConcurrency(value) {
2354
2451
  if (value == null)
2355
2452
  return DEFAULT_TASK_DISPATCH_CONCURRENCY;
@@ -2365,6 +2462,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2365
2462
  taskDispatchConcurrency;
2366
2463
  pendingTaskDispatches = [];
2367
2464
  activeTaskDispatchCount = 0;
2465
+ discoveryPromise;
2368
2466
  streamAppendQueues = /* @__PURE__ */ new Map();
2369
2467
  constructor(config) {
2370
2468
  super();
@@ -2476,6 +2574,8 @@ var AampClient = class _AampClient extends TinyEmitter {
2476
2574
  }
2477
2575
  static createPairingCode = createPairingCode;
2478
2576
  static buildPairingUrl = buildPairingUrl;
2577
+ static buildPairingWebUrl = buildPairingWebUrl;
2578
+ static pairingUrlToWebUrl = pairingUrlToWebUrl;
2479
2579
  static parsePairingUrl = parsePairingUrl;
2480
2580
  static isPairingUrl = isPairingUrl;
2481
2581
  static consumePairingCode = consumePairingCode;
@@ -2513,6 +2613,38 @@ var AampClient = class _AampClient extends TinyEmitter {
2513
2613
  ...opts.body ? { body: JSON.stringify(opts.body) } : {}
2514
2614
  });
2515
2615
  }
2616
+ discoverCachedAampService() {
2617
+ if (!this.discoveryPromise) {
2618
+ const discoveryPromise = _AampClient.discoverAampService(this.config.baseUrl, this.config.fetch);
2619
+ this.discoveryPromise = discoveryPromise;
2620
+ void discoveryPromise.catch(() => {
2621
+ if (this.discoveryPromise === discoveryPromise) {
2622
+ this.discoveryPromise = void 0;
2623
+ }
2624
+ });
2625
+ }
2626
+ return this.discoveryPromise;
2627
+ }
2628
+ async callAampApi(opts) {
2629
+ const fetchImpl = this.config.fetch ?? fetch;
2630
+ const discovery = await this.discoverCachedAampService();
2631
+ const base = this.config.baseUrl.replace(/\/$/, "");
2632
+ const apiUrl = new URL(discovery.api.url, `${base}/`);
2633
+ apiUrl.searchParams.set("action", opts.action);
2634
+ for (const [key, value] of Object.entries(opts.query ?? {})) {
2635
+ if (value == null)
2636
+ continue;
2637
+ apiUrl.searchParams.set(key, String(value));
2638
+ }
2639
+ return fetchImpl(apiUrl, {
2640
+ method: opts.method ?? "GET",
2641
+ headers: {
2642
+ ...opts.authToken ? { Authorization: `Basic ${opts.authToken}` } : {},
2643
+ ...opts.body ? { "Content-Type": "application/json" } : {}
2644
+ },
2645
+ body: opts.body ? JSON.stringify(opts.body) : void 0
2646
+ });
2647
+ }
2516
2648
  static async registerMailbox(opts) {
2517
2649
  const base = opts.aampHost.replace(/\/$/, "");
2518
2650
  const registerRes = await _AampClient.callDiscoveredApi(base, {
@@ -2738,7 +2870,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2738
2870
  };
2739
2871
  }
2740
2872
  async resolveStreamCapability() {
2741
- const discovery = await _AampClient.discoverAampService(this.config.baseUrl, this.config.fetch);
2873
+ const discovery = await this.discoverCachedAampService();
2742
2874
  const stream = discovery.capabilities?.stream;
2743
2875
  if (!stream?.transport) {
2744
2876
  throw new Error("AAMP stream capability is not available on this service");
@@ -2747,11 +2879,10 @@ var AampClient = class _AampClient extends TinyEmitter {
2747
2879
  }
2748
2880
  async createStream(opts) {
2749
2881
  const stream = await this.resolveStreamCapability();
2750
- const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2882
+ const res = await this.callAampApi({
2751
2883
  action: stream.createAction ?? "aamp.stream.create",
2752
2884
  method: "POST",
2753
2885
  authToken: this.config.mailboxToken,
2754
- fetch: this.config.fetch,
2755
2886
  body: opts
2756
2887
  });
2757
2888
  if (!res.ok) {
@@ -2794,11 +2925,10 @@ var AampClient = class _AampClient extends TinyEmitter {
2794
2925
  }
2795
2926
  async dispatchStreamAppend(opts) {
2796
2927
  const stream = await this.resolveStreamCapability();
2797
- const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2928
+ const res = await this.callAampApi({
2798
2929
  action: stream.appendAction ?? "aamp.stream.append",
2799
2930
  method: "POST",
2800
2931
  authToken: this.config.mailboxToken,
2801
- fetch: this.config.fetch,
2802
2932
  body: opts
2803
2933
  });
2804
2934
  if (!res.ok) {
@@ -2900,11 +3030,10 @@ var AampClient = class _AampClient extends TinyEmitter {
2900
3030
  async closeStream(opts) {
2901
3031
  await this.flushStreamAppendQueue(opts.streamId);
2902
3032
  const stream = await this.resolveStreamCapability();
2903
- const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
3033
+ const res = await this.callAampApi({
2904
3034
  action: stream.closeAction ?? "aamp.stream.close",
2905
3035
  method: "POST",
2906
3036
  authToken: this.config.mailboxToken,
2907
- fetch: this.config.fetch,
2908
3037
  body: opts
2909
3038
  });
2910
3039
  if (!res.ok) {
@@ -2915,10 +3044,9 @@ var AampClient = class _AampClient extends TinyEmitter {
2915
3044
  }
2916
3045
  async getTaskStream(opts) {
2917
3046
  const stream = await this.resolveStreamCapability();
2918
- const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
3047
+ const res = await this.callAampApi({
2919
3048
  action: stream.getAction ?? "aamp.stream.get",
2920
3049
  authToken: this.config.mailboxToken,
2921
- fetch: this.config.fetch,
2922
3050
  query: {
2923
3051
  ...opts.taskId ? { taskId: opts.taskId } : {},
2924
3052
  ...opts.streamId ? { streamId: opts.streamId } : {}
@@ -2949,7 +3077,8 @@ var AampClient = class _AampClient extends TinyEmitter {
2949
3077
  const res = await fetchImpl(url, {
2950
3078
  headers: {
2951
3079
  Authorization: `Basic ${this.config.mailboxToken}`,
2952
- Accept: "text/event-stream"
3080
+ Accept: "text/event-stream",
3081
+ ...opts.lastEventId ? { "Last-Event-ID": opts.lastEventId } : {}
2953
3082
  },
2954
3083
  signal: controller.signal
2955
3084
  });
@@ -2963,16 +3092,19 @@ var AampClient = class _AampClient extends TinyEmitter {
2963
3092
  let currentEvent = "message";
2964
3093
  let currentId = "";
2965
3094
  let currentData = [];
3095
+ let fallbackSeq = 0;
2966
3096
  const flush = () => {
2967
3097
  if (!currentData.length)
2968
3098
  return;
2969
3099
  try {
2970
- const parsed = JSON.parse(currentData.join("\n"));
2971
- handlers.onEvent({
2972
- ...parsed,
2973
- ...currentId ? { id: currentId } : {},
2974
- type: parsed.type ?? currentEvent
2975
- });
3100
+ fallbackSeq += 1;
3101
+ handlers.onEvent(normalizeSseStreamEvent({
3102
+ data: JSON.parse(currentData.join("\n")),
3103
+ streamId,
3104
+ eventType: currentEvent,
3105
+ eventId: currentId || void 0,
3106
+ seq: fallbackSeq
3107
+ }));
2976
3108
  } catch (err) {
2977
3109
  handlers.onError?.(err);
2978
3110
  } finally {
@@ -3058,6 +3190,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3058
3190
  import { dirname, join } from "node:path";
3059
3191
  import { homedir } from "node:os";
3060
3192
  import { randomBytes as randomBytes2 } from "node:crypto";
3193
+ var DEFAULT_PAIRING_WEB_URL2 = "https://meshmail.ai/pair";
3061
3194
  function defaultCredentialsPath() {
3062
3195
  return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".credentials.json");
3063
3196
  }
@@ -3131,6 +3264,14 @@ function createPairingCode2(params) {
3131
3264
  writeFileSync(resolved, JSON.stringify(state, null, 2), "utf-8");
3132
3265
  return state;
3133
3266
  }
3267
+ function pairingUrlToWebUrl2(connectUrl) {
3268
+ const parsed = new URL(connectUrl);
3269
+ const url = new URL(DEFAULT_PAIRING_WEB_URL2);
3270
+ for (const [key, value] of parsed.searchParams) {
3271
+ url.searchParams.set(key, value);
3272
+ }
3273
+ return url.toString();
3274
+ }
3134
3275
  function consumePairingCode2(params) {
3135
3276
  const resolved = params.file ?? defaultPairingPath();
3136
3277
  if (!existsSync(resolved))
@@ -3317,8 +3458,11 @@ async function ensureTaskStream(task) {
3317
3458
  });
3318
3459
  await aampClient.appendStreamEvent({
3319
3460
  streamId: created.streamId,
3320
- type: "status",
3321
- payload: { state: "running", label: "Task queued in OpenClaw" }
3461
+ type: "todo",
3462
+ payload: {
3463
+ items: [{ id: "openclaw-queued", content: "Task queued in OpenClaw", status: "in_progress" }],
3464
+ summary: "Task queued in OpenClaw"
3465
+ }
3322
3466
  });
3323
3467
  activeTaskStreams.set(task.taskId, created.streamId);
3324
3468
  return created.streamId;
@@ -3750,8 +3894,10 @@ var src_default = {
3750
3894
  mailbox: email,
3751
3895
  file: cfg.pairingFile ?? defaultPairingPath()
3752
3896
  });
3753
- const qr = await renderTerminalQr(pairing.connectUrl);
3897
+ const webUrl = pairingUrlToWebUrl2(pairing.connectUrl);
3898
+ const qr = await renderTerminalQr(webUrl);
3754
3899
  api.logger.info(`[AAMP] Pair with AAMP App before ${pairing.expiresAt}: ${pairing.connectUrl}`);
3900
+ api.logger.info(`[AAMP] Web pairing link: ${webUrl}`);
3755
3901
  if (qr)
3756
3902
  api.logger.info(`
3757
3903
  ${qr}`);
@@ -3762,7 +3908,8 @@ ${qr}`);
3762
3908
  Scan this QR code:
3763
3909
  ${qr}` : "\nCould not render a terminal QR code.",
3764
3910
  `
3765
- Pairing URL: ${pairing.connectUrl}`
3911
+ Pairing link: ${webUrl}`,
3912
+ `Pairing URL: ${pairing.connectUrl}`
3766
3913
  ].join("\n");
3767
3914
  }
3768
3915
  function wakeAgentForPendingTask(task) {
@@ -3854,7 +4001,7 @@ Pairing URL: ${pairing.connectUrl}`
3854
4001
  file: cfg.pairingFile ?? defaultPairingPath()
3855
4002
  });
3856
4003
  api.logger.info(`[AAMP] Pair with AAMP App before ${pairing.expiresAt}: ${pairing.connectUrl}`);
3857
- const qr = await renderTerminalQr(pairing.connectUrl);
4004
+ const qr = await renderTerminalQr(pairingUrlToWebUrl2(pairing.connectUrl));
3858
4005
  if (qr)
3859
4006
  api.logger.info(`
3860
4007
  ${qr}`);
@@ -4590,9 +4737,9 @@ ${task.bodyText}` : "",
4590
4737
  content: readBinaryFile(a.path)
4591
4738
  }));
4592
4739
  }
4593
- await appendTaskStream(task.taskId, "status", {
4594
- state: "completing",
4595
- label: `Sending ${p.status} result`
4740
+ await appendTaskStream(task.taskId, "todo", {
4741
+ items: [{ id: "openclaw-result", content: `Sending ${p.status} result`, status: "completed" }],
4742
+ summary: `Sending ${p.status} result`
4596
4743
  });
4597
4744
  if (p.output) {
4598
4745
  await appendTaskStream(task.taskId, "text.delta", { text: p.output });
@@ -4671,9 +4818,9 @@ ${task.bodyText}` : "",
4671
4818
  if (!aampClient?.isConnected()) {
4672
4819
  return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
4673
4820
  }
4674
- await appendTaskStream(task.taskId, "status", {
4675
- state: "help_needed",
4676
- label: p.blockedReason
4821
+ await appendTaskStream(task.taskId, "todo", {
4822
+ items: [{ id: "openclaw-help", content: p.blockedReason, status: "completed" }],
4823
+ summary: p.blockedReason
4677
4824
  });
4678
4825
  try {
4679
4826
  await aampClient.sendHelp({
@@ -4686,12 +4833,9 @@ ${task.bodyText}` : "",
4686
4833
  });
4687
4834
  } catch (err) {
4688
4835
  const message = err instanceof Error ? err.message : String(err);
4689
- await appendTaskStream(task.taskId, "error", {
4690
- message: `Failed to send help request: ${message}`
4691
- });
4692
- await appendTaskStream(task.taskId, "status", {
4693
- state: "running",
4694
- label: "Help request failed; task still needs a reply"
4836
+ await appendTaskStream(task.taskId, "todo", {
4837
+ items: [{ id: "openclaw-help", content: "Help request failed; task still needs a reply", status: "in_progress" }],
4838
+ summary: `Failed to send help request: ${message}`
4695
4839
  });
4696
4840
  api.logger.error(`[AAMP] aamp_send_help failed for ${task.taskId}: ${message}`);
4697
4841
  return {
@@ -4751,7 +4895,7 @@ ${lines.join("\n")}`
4751
4895
  }, { name: "aamp_pending_tasks" });
4752
4896
  api.registerTool({
4753
4897
  name: "aamp_pairing_code",
4754
- description: "Generate a fresh five-minute AAMP pairing code for this OpenClaw agent and show a QR code. Use this when the user asks to pair AAMP App or another AAMP runtime with this agent.",
4898
+ description: 'Generate a fresh five-minute AAMP pairing code for this OpenClaw agent and show a QR code. Use this immediately when the user asks for a pairing code, connect code, QR code, pairing link, invite link, or Chinese requests such as "\u53D1\u5BF9\u63A5\u7801", "\u751F\u6210\u914D\u5BF9\u7801", "\u5F39\u51FA\u4E8C\u7EF4\u7801", or "\u7ED9\u6211\u8FDE\u63A5\u4E8C\u7EF4\u7801".',
4755
4899
  parameters: { type: "object", properties: {} },
4756
4900
  execute: async () => ({
4757
4901
  content: [{ type: "text", text: await renderPairingCodeForCurrentAgent() }]
@@ -5020,7 +5164,7 @@ Question: ${h.question}`,
5020
5164
  });
5021
5165
  api.registerCommand({
5022
5166
  name: "aamp-pair",
5023
- description: "Show a fresh AAMP pairing QR code for this OpenClaw agent",
5167
+ description: "Show a fresh AAMP pairing QR code, web pairing link, and aamp://connect URL for this OpenClaw agent",
5024
5168
  acceptsArgs: false,
5025
5169
  requireAuth: false,
5026
5170
  handler: async () => ({