aamp-openclaw-plugin 0.1.33 → 0.1.35
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 +137 -29
- package/dist/index.js.map +2 -2
- package/openclaw.plugin.json +43 -0
- package/package.json +99 -1
- package/skills/SKILL.md +130 -0
package/dist/index.js
CHANGED
|
@@ -446,6 +446,27 @@ function describeError(err) {
|
|
|
446
446
|
}
|
|
447
447
|
return parts.join(" | ");
|
|
448
448
|
}
|
|
449
|
+
function sleep(ms) {
|
|
450
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
451
|
+
}
|
|
452
|
+
function shouldRetrySessionFetch(status) {
|
|
453
|
+
return status === 429 || status >= 500;
|
|
454
|
+
}
|
|
455
|
+
function shouldRetryBlobDownload(status) {
|
|
456
|
+
return status === 404 || status === 429 || status === 503;
|
|
457
|
+
}
|
|
458
|
+
function rewriteUrlToConfiguredOrigin(rawUrl, configuredBaseUrl) {
|
|
459
|
+
const parsed = new URL(rawUrl);
|
|
460
|
+
const configured = new URL(configuredBaseUrl);
|
|
461
|
+
parsed.protocol = configured.protocol;
|
|
462
|
+
parsed.username = configured.username;
|
|
463
|
+
parsed.password = configured.password;
|
|
464
|
+
parsed.hostname = configured.hostname;
|
|
465
|
+
parsed.port = configured.port;
|
|
466
|
+
return parsed.toString();
|
|
467
|
+
}
|
|
468
|
+
var SESSION_FETCH_MAX_ATTEMPTS = 3;
|
|
469
|
+
var SESSION_FETCH_RETRY_BASE_DELAY_MS = 250;
|
|
449
470
|
var JmapPushClient = class extends TinyEmitter {
|
|
450
471
|
ws = null;
|
|
451
472
|
session = null;
|
|
@@ -522,18 +543,30 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
522
543
|
*/
|
|
523
544
|
async fetchSession() {
|
|
524
545
|
const url = `${this.jmapUrl}/.well-known/jmap`;
|
|
525
|
-
let
|
|
526
|
-
|
|
527
|
-
res
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
546
|
+
let lastError = null;
|
|
547
|
+
for (let attempt = 1; attempt <= SESSION_FETCH_MAX_ATTEMPTS; attempt += 1) {
|
|
548
|
+
let res;
|
|
549
|
+
try {
|
|
550
|
+
res = await fetch(url, {
|
|
551
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
lastError = new Error(`fetchSession ${url} failed: ${describeError(err)}`);
|
|
555
|
+
if (attempt >= SESSION_FETCH_MAX_ATTEMPTS)
|
|
556
|
+
throw lastError;
|
|
557
|
+
await sleep(SESSION_FETCH_RETRY_BASE_DELAY_MS * attempt);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (res.ok) {
|
|
561
|
+
return res.json();
|
|
562
|
+
}
|
|
563
|
+
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}`);
|
|
564
|
+
if (attempt >= SESSION_FETCH_MAX_ATTEMPTS || !shouldRetrySessionFetch(res.status)) {
|
|
565
|
+
throw lastError;
|
|
566
|
+
}
|
|
567
|
+
await sleep(SESSION_FETCH_RETRY_BASE_DELAY_MS * attempt);
|
|
535
568
|
}
|
|
536
|
-
|
|
569
|
+
throw lastError ?? new Error("Failed to fetch JMAP session");
|
|
537
570
|
}
|
|
538
571
|
/**
|
|
539
572
|
* Perform a JMAP API call
|
|
@@ -967,27 +1000,37 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
967
1000
|
const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
|
|
968
1001
|
let downloadUrl = this.session.downloadUrl ?? `${this.jmapUrl}/jmap/download/{accountId}/{blobId}/{name}`;
|
|
969
1002
|
try {
|
|
970
|
-
|
|
971
|
-
const configured = new URL(this.jmapUrl);
|
|
972
|
-
parsed.protocol = configured.protocol;
|
|
973
|
-
parsed.host = configured.host;
|
|
974
|
-
downloadUrl = parsed.toString();
|
|
1003
|
+
downloadUrl = rewriteUrlToConfiguredOrigin(downloadUrl, this.jmapUrl);
|
|
975
1004
|
} catch {
|
|
976
1005
|
}
|
|
977
1006
|
const safeFilename = filename ?? "attachment";
|
|
978
1007
|
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");
|
|
979
1008
|
const maxAttempts = 8;
|
|
980
1009
|
let lastStatus = null;
|
|
1010
|
+
let lastError = null;
|
|
981
1011
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1012
|
+
let res;
|
|
1013
|
+
try {
|
|
1014
|
+
res = await fetch(downloadUrl, {
|
|
1015
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
1016
|
+
});
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
lastError = new Error(`Blob download fetch failed: attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl} error=${describeError(err)}`);
|
|
1019
|
+
if (attempt < maxAttempts) {
|
|
1020
|
+
console.warn(`[AAMP-SDK] blob download retry fetch-error attempt=${attempt}/${maxAttempts} url=${downloadUrl} error=${describeError(err)}`);
|
|
1021
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
|
|
1022
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
console.error(`[AAMP-SDK] blob download fetch-error attempt=${attempt}/${maxAttempts} url=${downloadUrl} error=${describeError(err)}`);
|
|
1026
|
+
throw lastError;
|
|
1027
|
+
}
|
|
985
1028
|
lastStatus = res.status;
|
|
986
1029
|
if (res.ok) {
|
|
987
1030
|
const arrayBuffer = await res.arrayBuffer();
|
|
988
1031
|
return Buffer.from(arrayBuffer);
|
|
989
1032
|
}
|
|
990
|
-
if (attempt < maxAttempts && (res.status
|
|
1033
|
+
if (attempt < maxAttempts && shouldRetryBlobDownload(res.status)) {
|
|
991
1034
|
console.warn(`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
|
|
992
1035
|
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
|
|
993
1036
|
await new Promise((r) => setTimeout(r, delay));
|
|
@@ -996,6 +1039,8 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
996
1039
|
console.error(`[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
|
|
997
1040
|
throw new Error(`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
|
|
998
1041
|
}
|
|
1042
|
+
if (lastError)
|
|
1043
|
+
throw lastError;
|
|
999
1044
|
throw new Error(`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
|
|
1000
1045
|
}
|
|
1001
1046
|
/**
|
|
@@ -1829,6 +1874,29 @@ function renderThreadHistoryForAgent(events, options = {}) {
|
|
|
1829
1874
|
}
|
|
1830
1875
|
|
|
1831
1876
|
// ../sdk/src/client.js
|
|
1877
|
+
function buildRegisteredCommandDispatchPayload(opts) {
|
|
1878
|
+
const command = opts.command.trim();
|
|
1879
|
+
if (!command) {
|
|
1880
|
+
throw new Error("Registered command name cannot be empty.");
|
|
1881
|
+
}
|
|
1882
|
+
if (opts.args != null && (typeof opts.args !== "object" || Array.isArray(opts.args))) {
|
|
1883
|
+
throw new Error("Registered command args must be an object when provided.");
|
|
1884
|
+
}
|
|
1885
|
+
if (opts.inputs) {
|
|
1886
|
+
for (const input of opts.inputs) {
|
|
1887
|
+
if (!input.slot?.trim() || !input.attachmentName?.trim()) {
|
|
1888
|
+
throw new Error("Each registered command input must include slot and attachmentName.");
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
kind: "registered-command/v1",
|
|
1894
|
+
command,
|
|
1895
|
+
...opts.args && Object.keys(opts.args).length > 0 ? { args: opts.args } : {},
|
|
1896
|
+
...opts.inputs?.length ? { inputs: opts.inputs } : {},
|
|
1897
|
+
stream: { mode: opts.streamMode ?? "full" }
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1832
1900
|
var AampClient = class _AampClient extends TinyEmitter {
|
|
1833
1901
|
jmapClient;
|
|
1834
1902
|
smtpSender;
|
|
@@ -2032,6 +2100,21 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
2032
2100
|
async sendTask(opts) {
|
|
2033
2101
|
return this.smtpSender.sendTask(opts);
|
|
2034
2102
|
}
|
|
2103
|
+
async sendRegisteredCommand(opts) {
|
|
2104
|
+
const payload = buildRegisteredCommandDispatchPayload(opts);
|
|
2105
|
+
return this.smtpSender.sendTask({
|
|
2106
|
+
to: opts.to,
|
|
2107
|
+
taskId: opts.taskId,
|
|
2108
|
+
title: opts.title?.trim() || `Registered command: ${payload.command}`,
|
|
2109
|
+
rawBodyText: JSON.stringify(payload, null, 2),
|
|
2110
|
+
priority: opts.priority,
|
|
2111
|
+
expiresAt: opts.expiresAt,
|
|
2112
|
+
contextLinks: opts.contextLinks,
|
|
2113
|
+
dispatchContext: opts.dispatchContext,
|
|
2114
|
+
parentTaskId: opts.parentTaskId,
|
|
2115
|
+
attachments: opts.attachments
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2035
2118
|
async sendCancel(opts) {
|
|
2036
2119
|
return this.smtpSender.sendCancel(opts);
|
|
2037
2120
|
}
|
|
@@ -2880,7 +2963,12 @@ var src_default = {
|
|
|
2880
2963
|
const cfg = api.config?.channels?.aamp ?? api.pluginConfig ?? {};
|
|
2881
2964
|
api.registerChannel({
|
|
2882
2965
|
id: "aamp",
|
|
2883
|
-
meta: {
|
|
2966
|
+
meta: {
|
|
2967
|
+
label: "AAMP",
|
|
2968
|
+
selectionLabel: "AAMP",
|
|
2969
|
+
docsPath: "/channels/aamp",
|
|
2970
|
+
blurb: "AAMP mailbox channel for receiving and replying to tasks over email."
|
|
2971
|
+
},
|
|
2884
2972
|
capabilities: { chatTypes: ["dm"] },
|
|
2885
2973
|
config: {
|
|
2886
2974
|
listAccountIds: () => cfg.aampHost ? ["default"] : [],
|
|
@@ -3787,17 +3875,37 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
3787
3875
|
state: "help_needed",
|
|
3788
3876
|
label: p.blockedReason
|
|
3789
3877
|
});
|
|
3878
|
+
try {
|
|
3879
|
+
await aampClient.sendHelp({
|
|
3880
|
+
to: task.from,
|
|
3881
|
+
taskId: task.taskId,
|
|
3882
|
+
question: p.question,
|
|
3883
|
+
blockedReason: p.blockedReason,
|
|
3884
|
+
suggestedOptions: p.suggestedOptions ?? [],
|
|
3885
|
+
inReplyTo: task.messageId || void 0
|
|
3886
|
+
});
|
|
3887
|
+
} catch (err) {
|
|
3888
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3889
|
+
await appendTaskStream(task.taskId, "error", {
|
|
3890
|
+
message: `Failed to send help request: ${message}`
|
|
3891
|
+
});
|
|
3892
|
+
await appendTaskStream(task.taskId, "status", {
|
|
3893
|
+
state: "running",
|
|
3894
|
+
label: "Help request failed; task still needs a reply"
|
|
3895
|
+
});
|
|
3896
|
+
api.logger.error(`[AAMP] aamp_send_help failed for ${task.taskId}: ${message}`);
|
|
3897
|
+
return {
|
|
3898
|
+
content: [
|
|
3899
|
+
{
|
|
3900
|
+
type: "text",
|
|
3901
|
+
text: `Error: failed to send help request for task ${task.taskId}: ${message}`
|
|
3902
|
+
}
|
|
3903
|
+
]
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3790
3906
|
await closeTaskStream(task.taskId, {
|
|
3791
3907
|
reason: "task.help_needed"
|
|
3792
3908
|
});
|
|
3793
|
-
await aampClient.sendHelp({
|
|
3794
|
-
to: task.from,
|
|
3795
|
-
taskId: task.taskId,
|
|
3796
|
-
question: p.question,
|
|
3797
|
-
blockedReason: p.blockedReason,
|
|
3798
|
-
suggestedOptions: p.suggestedOptions ?? [],
|
|
3799
|
-
inReplyTo: task.messageId || void 0
|
|
3800
|
-
});
|
|
3801
3909
|
pendingTasks.set(task.taskId, {
|
|
3802
3910
|
...task,
|
|
3803
3911
|
awaitingHelpReply: true
|