aamp-openclaw-plugin 0.1.39 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,10 @@ OpenClaw plugin that gives an OpenClaw agent an AAMP mailbox identity.
4
4
 
5
5
  ## Install
6
6
 
7
+ Requires OpenClaw `>=2026.3.22`. Both `openclaw plugins install` and
8
+ `npx aamp-openclaw-plugin init` stop before installation when the detected
9
+ OpenClaw version is older.
10
+
7
11
  ```bash
8
12
  npm install aamp-openclaw-plugin
9
13
  ```
@@ -15,6 +15,8 @@ const DEFAULT_AAMP_HOST = 'https://meshmail.ai'
15
15
  const DEFAULT_CREDENTIALS_FILE = '~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json'
16
16
  const DEFAULT_PAIRING_FILE = '~/.openclaw/extensions/aamp-openclaw-plugin/.pairing.json'
17
17
  const DEFAULT_SENDER_POLICIES_FILE = '~/.openclaw/extensions/aamp-openclaw-plugin/.sender-policies.json'
18
+ export const MIN_OPENCLAW_VERSION = '2026.3.22'
19
+ export const MIN_OPENCLAW_VERSION_RANGE = `>=${MIN_OPENCLAW_VERSION}`
18
20
  const CODING_TOOL_ALLOWLIST = [
19
21
  'read',
20
22
  'write',
@@ -200,6 +202,108 @@ export function normalizeBaseUrl(url) {
200
202
  return `https://${url.replace(/\/$/, '')}`
201
203
  }
202
204
 
205
+ export function parseOpenClawVersion(outputText) {
206
+ const match = String(outputText ?? '').match(/\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/)
207
+ return match?.[1] ?? null
208
+ }
209
+
210
+ function parseVersionParts(version) {
211
+ const [withoutBuild] = String(version ?? '').split('+', 1)
212
+ const prereleaseIndex = withoutBuild.indexOf('-')
213
+ const core = prereleaseIndex === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseIndex)
214
+ const prereleaseText = prereleaseIndex === -1 ? '' : withoutBuild.slice(prereleaseIndex + 1)
215
+ const numbers = core.split('.').map((part) => Number(part))
216
+ if (numbers.length !== 3 || numbers.some((part) => !Number.isInteger(part) || part < 0)) {
217
+ return null
218
+ }
219
+ const prerelease = prereleaseText ? prereleaseText.split('.') : []
220
+ return { numbers, prerelease }
221
+ }
222
+
223
+ function comparePrereleaseIdentifier(left, right) {
224
+ const leftNumeric = /^\d+$/.test(left)
225
+ const rightNumeric = /^\d+$/.test(right)
226
+ if (leftNumeric && rightNumeric) return Number(left) - Number(right)
227
+ if (leftNumeric) return -1
228
+ if (rightNumeric) return 1
229
+ return left.localeCompare(right)
230
+ }
231
+
232
+ export function compareOpenClawVersions(leftVersion, rightVersion) {
233
+ const left = parseVersionParts(leftVersion)
234
+ const right = parseVersionParts(rightVersion)
235
+ if (!left || !right) {
236
+ throw new Error(`Invalid OpenClaw version comparison: ${leftVersion} vs ${rightVersion}`)
237
+ }
238
+
239
+ for (let idx = 0; idx < 3; idx += 1) {
240
+ const diff = left.numbers[idx] - right.numbers[idx]
241
+ if (diff !== 0) return diff
242
+ }
243
+
244
+ if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0
245
+ if (left.prerelease.length === 0) return 1
246
+ if (right.prerelease.length === 0) return -1
247
+
248
+ const maxLength = Math.max(left.prerelease.length, right.prerelease.length)
249
+ for (let idx = 0; idx < maxLength; idx += 1) {
250
+ const leftPart = left.prerelease[idx]
251
+ const rightPart = right.prerelease[idx]
252
+ if (leftPart === undefined) return -1
253
+ if (rightPart === undefined) return 1
254
+ const diff = comparePrereleaseIdentifier(leftPart, rightPart)
255
+ if (diff !== 0) return diff
256
+ }
257
+ return 0
258
+ }
259
+
260
+ export function checkOpenClawVersion(currentVersion, minimumVersion = MIN_OPENCLAW_VERSION) {
261
+ const comparison = compareOpenClawVersions(currentVersion, minimumVersion)
262
+ return {
263
+ ok: comparison >= 0,
264
+ currentVersion,
265
+ minimumVersion,
266
+ range: `>=${minimumVersion}`,
267
+ }
268
+ }
269
+
270
+ export function getCurrentOpenClawVersion() {
271
+ const result = spawnSync('openclaw', ['--version'], {
272
+ encoding: 'utf-8',
273
+ })
274
+
275
+ if (result.error) {
276
+ throw new Error(
277
+ `OpenClaw CLI was not found. Install OpenClaw ${MIN_OPENCLAW_VERSION_RANGE} or newer before installing ${PLUGIN_ID}.`,
278
+ )
279
+ }
280
+
281
+ if (result.status !== 0) {
282
+ const reason = (result.stderr || result.stdout || `exit code ${result.status}`).trim()
283
+ throw new Error(`Could not detect OpenClaw version: ${reason}`)
284
+ }
285
+
286
+ const version = parseOpenClawVersion(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)
287
+ if (!version) {
288
+ throw new Error(`Could not parse OpenClaw version from: ${(result.stdout || result.stderr || '').trim()}`)
289
+ }
290
+ return version
291
+ }
292
+
293
+ export function assertOpenClawVersionSupported(currentVersion = getCurrentOpenClawVersion()) {
294
+ const check = checkOpenClawVersion(currentVersion)
295
+ if (check.ok) return check
296
+
297
+ throw new Error(
298
+ [
299
+ `${PLUGIN_ID} requires OpenClaw ${check.range}.`,
300
+ `Detected OpenClaw ${check.currentVersion}.`,
301
+ 'Please upgrade OpenClaw, then rerun this installer.',
302
+ 'Example: npm install -g openclaw@latest',
303
+ ].join('\n'),
304
+ )
305
+ }
306
+
203
307
  export function ensurePluginConfig(config, pluginConfig, options = {}) {
204
308
  const next = config && typeof config === 'object' ? structuredClone(config) : {}
205
309
  if (!next.plugins || typeof next.plugins !== 'object') next.plugins = {}
@@ -508,6 +612,7 @@ function printHelp() {
508
612
  }
509
613
 
510
614
  export async function runInit() {
615
+ const openClawVersionCheck = assertOpenClawVersionSupported()
511
616
  const configPath = resolveOpenClawConfigPath()
512
617
  const existing = readJsonFile(configPath)
513
618
  const previousEntry = existing?.plugins?.entries?.[PLUGIN_ID] ?? existing?.plugins?.entries?.aamp
@@ -530,6 +635,7 @@ export async function runInit() {
530
635
  const rl = createInterface({ input, output })
531
636
  try {
532
637
  output.write('AAMP OpenClaw Plugin Setup\n\n')
638
+ output.write(`Detected OpenClaw ${openClawVersionCheck.currentVersion} (${MIN_OPENCLAW_VERSION_RANGE} required)\n\n`)
533
639
 
534
640
  if (previousConfig) {
535
641
  output.write(
package/dist/index.js CHANGED
@@ -1499,12 +1499,34 @@ var SmtpSender = class _SmtpSender {
1499
1499
  return false;
1500
1500
  return Boolean(this.config.httpBaseUrl && this.config.authToken);
1501
1501
  }
1502
+ normalizeAttachments(attachments) {
1503
+ if (!attachments?.length)
1504
+ return void 0;
1505
+ return attachments.map((a) => ({
1506
+ filename: a.filename,
1507
+ contentType: a.contentType,
1508
+ content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1509
+ }));
1510
+ }
1502
1511
  getJmapAuthHeader() {
1503
1512
  if (!this.config.authToken) {
1504
1513
  throw new Error("JMAP auth token is not configured");
1505
1514
  }
1506
1515
  return `Basic ${this.config.authToken}`;
1507
1516
  }
1517
+ rewriteUrlToConfiguredOrigin(rawUrl) {
1518
+ const base = this.config.httpBaseUrl?.replace(/\/$/, "");
1519
+ if (!base)
1520
+ return rawUrl;
1521
+ const parsed = new URL(rawUrl, `${base}/`);
1522
+ const configured = new URL(base);
1523
+ parsed.protocol = configured.protocol;
1524
+ parsed.username = configured.username;
1525
+ parsed.password = configured.password;
1526
+ parsed.hostname = configured.hostname;
1527
+ parsed.port = configured.port;
1528
+ return parsed.toString();
1529
+ }
1508
1530
  async resolveJmapSession() {
1509
1531
  const base = this.config.httpBaseUrl?.replace(/\/$/, "");
1510
1532
  if (!base) {
@@ -1525,7 +1547,8 @@ var SmtpSender = class _SmtpSender {
1525
1547
  }
1526
1548
  return {
1527
1549
  accountId,
1528
- apiUrl: `${base}/jmap/`
1550
+ apiUrl: `${base}/jmap/`,
1551
+ uploadUrl: this.rewriteUrlToConfiguredOrigin(session.uploadUrl ?? `${base}/jmap/upload/{accountId}/`)
1529
1552
  };
1530
1553
  })();
1531
1554
  }
@@ -1562,6 +1585,38 @@ var SmtpSender = class _SmtpSender {
1562
1585
  const data = await res.json();
1563
1586
  return data.methodResponses ?? [];
1564
1587
  }
1588
+ async uploadSentAttachment(attachment) {
1589
+ const session = await this.resolveJmapSession();
1590
+ const content = typeof attachment.content === "string" ? Buffer.from(attachment.content, "base64") : attachment.content;
1591
+ const uploadUrl = session.uploadUrl.replace(/\{accountId\}|%7BaccountId%7D/gi, encodeURIComponent(session.accountId));
1592
+ const res = await this.fetch(uploadUrl, {
1593
+ method: "POST",
1594
+ headers: {
1595
+ Authorization: this.getJmapAuthHeader(),
1596
+ "Content-Type": attachment.contentType
1597
+ },
1598
+ body: content
1599
+ });
1600
+ const bodyText = await res.text();
1601
+ if (!res.ok) {
1602
+ throw new Error(`JMAP attachment upload failed: ${res.status} ${bodyText}`);
1603
+ }
1604
+ let data;
1605
+ try {
1606
+ data = JSON.parse(bodyText);
1607
+ } catch {
1608
+ throw new Error("JMAP attachment upload returned invalid JSON");
1609
+ }
1610
+ if (!data.blobId) {
1611
+ throw new Error("JMAP attachment upload did not return blobId");
1612
+ }
1613
+ return {
1614
+ blobId: data.blobId,
1615
+ type: data.type ?? attachment.contentType,
1616
+ size: data.size ?? content.byteLength,
1617
+ name: attachment.filename
1618
+ };
1619
+ }
1565
1620
  async getSentMailboxId() {
1566
1621
  if (!this.sentMailboxIdPromise) {
1567
1622
  this.sentMailboxIdPromise = (async () => {
@@ -1586,20 +1641,43 @@ var SmtpSender = class _SmtpSender {
1586
1641
  const sentMailboxId = await this.getSentMailboxId();
1587
1642
  if (!sentMailboxId)
1588
1643
  return;
1644
+ const uploadedAttachments = params.attachments?.length ? await Promise.all(params.attachments.map((attachment) => this.uploadSentAttachment(attachment))) : [];
1589
1645
  const emailCreate = {
1590
1646
  mailboxIds: { [sentMailboxId]: true },
1591
1647
  from: [{ email: params.from }],
1592
1648
  to: [{ email: params.to }],
1593
1649
  subject: params.subject,
1594
- bodyValues: {
1650
+ keywords: { "$seen": true }
1651
+ };
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 = {
1595
1674
  body: {
1596
1675
  value: params.text,
1597
1676
  charset: "utf-8"
1598
1677
  }
1599
- },
1600
- textBody: [{ partId: "body", type: "text/plain" }],
1601
- keywords: { "$seen": true }
1602
- };
1678
+ };
1679
+ emailCreate.textBody = [{ partId: "body", type: "text/plain" }];
1680
+ }
1603
1681
  if (params.inReplyTo) {
1604
1682
  emailCreate["header:In-Reply-To:asText"] = ` ${sanitize(params.inReplyTo)}`;
1605
1683
  }
@@ -1622,6 +1700,12 @@ var SmtpSender = class _SmtpSender {
1622
1700
  try {
1623
1701
  await this.saveToSent(params);
1624
1702
  } catch {
1703
+ if (params.attachments?.length) {
1704
+ try {
1705
+ await this.saveToSent({ ...params, attachments: void 0 });
1706
+ } catch {
1707
+ }
1708
+ }
1625
1709
  }
1626
1710
  }
1627
1711
  /**
@@ -1631,6 +1715,7 @@ var SmtpSender = class _SmtpSender {
1631
1715
  */
1632
1716
  async sendTask(opts) {
1633
1717
  const taskId = opts.taskId ?? randomUUID();
1718
+ const attachments = this.normalizeAttachments(opts.attachments);
1634
1719
  const aampHeaders = buildDispatchHeaders({
1635
1720
  taskId,
1636
1721
  priority: opts.priority,
@@ -1653,12 +1738,8 @@ var SmtpSender = class _SmtpSender {
1653
1738
  ].filter(Boolean).join("\n"),
1654
1739
  headers: aampHeaders
1655
1740
  };
1656
- if (opts.attachments?.length) {
1657
- sendMailOpts.attachments = opts.attachments.map((a) => ({
1658
- filename: a.filename,
1659
- content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
1660
- contentType: a.contentType
1661
- }));
1741
+ if (attachments) {
1742
+ sendMailOpts.attachments = attachments;
1662
1743
  }
1663
1744
  if (this.shouldUseHttpFallback(opts.to)) {
1664
1745
  const info2 = await this.sendViaHttp({
@@ -1666,11 +1747,7 @@ var SmtpSender = class _SmtpSender {
1666
1747
  subject: sendMailOpts.subject,
1667
1748
  text: sendMailOpts.text,
1668
1749
  aampHeaders,
1669
- attachments: opts.attachments?.map((a) => ({
1670
- filename: a.filename,
1671
- contentType: a.contentType,
1672
- content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1673
- }))
1750
+ attachments
1674
1751
  });
1675
1752
  await this.saveToSentBestEffort({
1676
1753
  from: this.config.user,
@@ -1678,7 +1755,8 @@ var SmtpSender = class _SmtpSender {
1678
1755
  subject: sendMailOpts.subject,
1679
1756
  text: sendMailOpts.text,
1680
1757
  aampHeaders,
1681
- messageId: info2.messageId
1758
+ messageId: info2.messageId,
1759
+ attachments
1682
1760
  });
1683
1761
  return { taskId, messageId: info2.messageId ?? "" };
1684
1762
  }
@@ -1689,7 +1767,8 @@ var SmtpSender = class _SmtpSender {
1689
1767
  subject: sendMailOpts.subject,
1690
1768
  text: sendMailOpts.text,
1691
1769
  aampHeaders,
1692
- messageId: info.messageId
1770
+ messageId: info.messageId,
1771
+ attachments
1693
1772
  });
1694
1773
  return { taskId, messageId: info.messageId ?? "" };
1695
1774
  }
@@ -1697,6 +1776,7 @@ var SmtpSender = class _SmtpSender {
1697
1776
  * Send a task.result email back to the dispatcher
1698
1777
  */
1699
1778
  async sendResult(opts) {
1779
+ const attachments = this.normalizeAttachments(opts.attachments);
1700
1780
  const aampHeaders = buildResultHeaders({
1701
1781
  taskId: opts.taskId,
1702
1782
  status: opts.status,
@@ -1725,12 +1805,8 @@ Error: ${opts.errorMsg}` : ""
1725
1805
  mailOpts.inReplyTo = opts.inReplyTo;
1726
1806
  mailOpts.references = opts.inReplyTo;
1727
1807
  }
1728
- if (opts.attachments?.length) {
1729
- mailOpts.attachments = opts.attachments.map((a) => ({
1730
- filename: a.filename,
1731
- content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
1732
- contentType: a.contentType
1733
- }));
1808
+ if (attachments) {
1809
+ mailOpts.attachments = attachments;
1734
1810
  }
1735
1811
  if (this.shouldUseHttpFallback(opts.to)) {
1736
1812
  const info2 = await this.sendViaHttp({
@@ -1738,11 +1814,7 @@ Error: ${opts.errorMsg}` : ""
1738
1814
  subject: mailOpts.subject,
1739
1815
  text: mailOpts.text,
1740
1816
  aampHeaders,
1741
- attachments: opts.attachments?.map((a) => ({
1742
- filename: a.filename,
1743
- contentType: a.contentType,
1744
- content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1745
- }))
1817
+ attachments
1746
1818
  });
1747
1819
  await this.saveToSentBestEffort({
1748
1820
  from: this.config.user,
@@ -1752,7 +1824,8 @@ Error: ${opts.errorMsg}` : ""
1752
1824
  aampHeaders,
1753
1825
  messageId: info2.messageId,
1754
1826
  inReplyTo: opts.inReplyTo,
1755
- references: opts.inReplyTo
1827
+ references: opts.inReplyTo,
1828
+ attachments
1756
1829
  });
1757
1830
  return;
1758
1831
  }
@@ -1765,13 +1838,15 @@ Error: ${opts.errorMsg}` : ""
1765
1838
  aampHeaders,
1766
1839
  messageId: info.messageId,
1767
1840
  inReplyTo: opts.inReplyTo,
1768
- references: opts.inReplyTo
1841
+ references: opts.inReplyTo,
1842
+ attachments
1769
1843
  });
1770
1844
  }
1771
1845
  /**
1772
1846
  * Send a task.help_needed email when the agent is blocked
1773
1847
  */
1774
1848
  async sendHelp(opts) {
1849
+ const attachments = this.normalizeAttachments(opts.attachments);
1775
1850
  const aampHeaders = buildHelpHeaders({
1776
1851
  taskId: opts.taskId,
1777
1852
  question: opts.question,
@@ -1800,12 +1875,8 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1800
1875
  helpMailOpts.inReplyTo = opts.inReplyTo;
1801
1876
  helpMailOpts.references = opts.inReplyTo;
1802
1877
  }
1803
- if (opts.attachments?.length) {
1804
- helpMailOpts.attachments = opts.attachments.map((a) => ({
1805
- filename: a.filename,
1806
- content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
1807
- contentType: a.contentType
1808
- }));
1878
+ if (attachments) {
1879
+ helpMailOpts.attachments = attachments;
1809
1880
  }
1810
1881
  if (this.shouldUseHttpFallback(opts.to)) {
1811
1882
  const info2 = await this.sendViaHttp({
@@ -1813,11 +1884,7 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1813
1884
  subject: helpMailOpts.subject,
1814
1885
  text: helpMailOpts.text,
1815
1886
  aampHeaders,
1816
- attachments: opts.attachments?.map((a) => ({
1817
- filename: a.filename,
1818
- contentType: a.contentType,
1819
- content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
1820
- }))
1887
+ attachments
1821
1888
  });
1822
1889
  await this.saveToSentBestEffort({
1823
1890
  from: this.config.user,
@@ -1827,7 +1894,8 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1827
1894
  aampHeaders,
1828
1895
  messageId: info2.messageId,
1829
1896
  inReplyTo: opts.inReplyTo,
1830
- references: opts.inReplyTo
1897
+ references: opts.inReplyTo,
1898
+ attachments
1831
1899
  });
1832
1900
  return;
1833
1901
  }
@@ -1840,7 +1908,8 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1840
1908
  aampHeaders,
1841
1909
  messageId: info.messageId,
1842
1910
  inReplyTo: opts.inReplyTo,
1843
- references: opts.inReplyTo
1911
+ references: opts.inReplyTo,
1912
+ attachments
1844
1913
  });
1845
1914
  }
1846
1915
  /**
@@ -3652,24 +3721,24 @@ var src_default = {
3652
3721
  return;
3653
3722
  if (request.to.trim().toLowerCase() !== agentEmail.trim().toLowerCase())
3654
3723
  return;
3724
+ const senderPoliciesFile = cfg.senderPoliciesFile ?? defaultSenderPoliciesPath();
3655
3725
  const consumed = consumePairingCode2({
3656
3726
  file: cfg.pairingFile ?? defaultPairingPath(),
3657
3727
  mailbox: agentEmail,
3658
3728
  pairCode: request.pairCode
3659
3729
  });
3660
- if (!consumed) {
3661
- const reason = "invalid or expired pair code";
3662
- api.logger.warn(`[AAMP] Rejected pair.request from ${request.from}: ${reason}`);
3663
- await sendPairResponse(request, false, reason);
3664
- return;
3730
+ const pairResponse = consumed ? { success: true } : { success: false, reason: "invalid or expired pair code" };
3731
+ if (pairResponse.success) {
3732
+ pairedSenderPolicies = addPairedSenderPolicy(senderPoliciesFile, {
3733
+ sender: request.from.trim().toLowerCase(),
3734
+ dispatchContextRules: request.dispatchContextRules ?? {},
3735
+ pairedAt: (/* @__PURE__ */ new Date()).toISOString()
3736
+ });
3737
+ api.logger.info(`[AAMP] Paired sender ${request.from}; sender policy saved to ${senderPoliciesFile}`);
3738
+ } else {
3739
+ api.logger.warn(`[AAMP] Rejected pair.request from ${request.from}: ${pairResponse.reason}`);
3665
3740
  }
3666
- pairedSenderPolicies = addPairedSenderPolicy(cfg.senderPoliciesFile ?? defaultSenderPoliciesPath(), {
3667
- sender: request.from.trim().toLowerCase(),
3668
- dispatchContextRules: request.dispatchContextRules ?? {},
3669
- pairedAt: (/* @__PURE__ */ new Date()).toISOString()
3670
- });
3671
- api.logger.info(`[AAMP] Paired sender ${request.from}; sender policy saved to ${cfg.senderPoliciesFile ?? defaultSenderPoliciesPath()}`);
3672
- await sendPairResponse(request, true);
3741
+ await sendPairResponse(request, pairResponse.success, pairResponse.reason);
3673
3742
  }
3674
3743
  async function renderPairingCodeForCurrentAgent() {
3675
3744
  const identity = agentEmail ? { email: agentEmail } : loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());