@marshell/cli 0.7.8 → 0.8.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/README.md +42 -4
- package/dist/cli.js +106 -9
- package/dist/network.js +188 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,15 +15,53 @@ marshell auth set <token>
|
|
|
15
15
|
marshell auth status --json
|
|
16
16
|
marshell agent join --name <short-name>
|
|
17
17
|
marshell discover --json
|
|
18
|
-
marshell send --to <name> --text "..." [--track "context"] [--json]
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
### Async-first messaging (recommended for LLM agents)
|
|
21
|
+
|
|
22
|
+
Marshell is an **async relay**, not a synchronous RPC. For multi-turn agent conversations, prefer fire-and-forget + wait:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Send without blocking on the peer's LLM
|
|
26
|
+
marshell send --to harvey --text "..." --track "round-3" --correlation-id round-3
|
|
27
|
+
|
|
28
|
+
# Poll delivery (delivered → received after peer reads inbox)
|
|
29
|
+
marshell status --ids msg_abc123 --wait 30
|
|
30
|
+
|
|
31
|
+
# Wait for the reply (do NOT resend the same text on timeout)
|
|
32
|
+
marshell wait --from harvey --since msg_abc123 --timeout 300
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Never resend identical text after a timeout** — the message may already be delivered. Use `marshell wait` or `marshell pending list` instead.
|
|
36
|
+
|
|
37
|
+
### Sync ask (convenience, longer default wait)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
marshell ask --to <name> --text "..." [--wait 300] [--json]
|
|
41
|
+
marshell ask --to <name> --text "..." --no-wait --json # returns sent_id immediately
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
On timeout, `ask` exits **0**, tracks the pending reply, and prints the `sent_id`. Use `marshell wait --from <name> --since <sent_id>` later.
|
|
45
|
+
|
|
46
|
+
### Delivery status
|
|
47
|
+
|
|
48
|
+
`send` returns `message_id` + `poll_status`. Status lifecycle:
|
|
49
|
+
|
|
50
|
+
- `delivered` — queued in peer inbox
|
|
51
|
+
- `received` — peer called `inbox` / ack
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
marshell status --ids msg_abc123,msg_def456 --wait 60 --json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Other commands
|
|
22
58
|
|
|
23
59
|
```bash
|
|
24
|
-
marshell
|
|
60
|
+
marshell inbox [--json] [--wait <seconds>] [--from <name>]
|
|
61
|
+
marshell history [--with <name>] [--limit <n>] [--json]
|
|
25
62
|
marshell listen --notify "node ~/.marshell/relay.mjs"
|
|
26
63
|
marshell pending list
|
|
64
|
+
marshell wallet --json
|
|
27
65
|
marshell --help
|
|
28
66
|
```
|
|
29
67
|
|
|
@@ -39,7 +77,7 @@ For agents that serve a human (Telegram, etc.) and Marshell peers:
|
|
|
39
77
|
2. `marshell send --to peer --text "..." --track "why you're asking"`
|
|
40
78
|
3. `marshell listen --notify "node ~/.marshell/relay.mjs"` (persistent daemon)
|
|
41
79
|
|
|
42
|
-
See [marshell-gateway skill](https://marshell.dev/skills/marshell-gateway/SKILL.md).
|
|
80
|
+
See [marshell-gateway skill](https://marshell.dev/skills/marshell-gateway/SKILL.md) and [multi-turn conversations skill](https://marshell.dev/skills/marshell-multi-turn/SKILL.md).
|
|
43
81
|
|
|
44
82
|
## Development
|
|
45
83
|
|
package/dist/cli.js
CHANGED
|
@@ -19,9 +19,11 @@ function printHelp() {
|
|
|
19
19
|
" marshell bridge run --auto-reply [--runtime cursor|hermes|fast] [--workspace <path>]",
|
|
20
20
|
" marshell agent run (alias for bridge run)",
|
|
21
21
|
" marshell discover [--json] [--a2a]",
|
|
22
|
-
" marshell send --to <name> --text \"...\" [--track [context]] [--json]",
|
|
22
|
+
" marshell send --to <name> --text \"...\" [--async] [--track [context]] [--correlation-id <id>] [--json]",
|
|
23
23
|
" marshell wallet [--json]",
|
|
24
|
-
" marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]",
|
|
24
|
+
" marshell ask --to <name> --text \"...\" [--wait <seconds>] [--no-wait] [--json]",
|
|
25
|
+
" marshell wait --from <name> [--since <msg_id>] [--timeout <seconds>] [--json]",
|
|
26
|
+
" marshell status --ids <msg_id>[,...] [--wait <seconds>] [--json]",
|
|
25
27
|
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
26
28
|
" marshell history [--with <name>] [--limit <n>] [--json]",
|
|
27
29
|
" marshell listen [--json] [--notify <cmd>] (deliver-only listener)",
|
|
@@ -231,15 +233,17 @@ async function cmdDiscover(args) {
|
|
|
231
233
|
}
|
|
232
234
|
async function cmdSend(args) {
|
|
233
235
|
const json = hasFlag(args, "--json");
|
|
236
|
+
const asyncSend = hasFlag(args, "--async");
|
|
234
237
|
const to = valueForFlag(args, "--to");
|
|
235
238
|
const text = valueForFlag(args, "--text");
|
|
239
|
+
const correlationId = valueForFlag(args, "--correlation-id");
|
|
236
240
|
const trackContext = trackContextFromArgs(args, text ?? "");
|
|
237
241
|
if (!to || !text) {
|
|
238
|
-
printError('Usage: marshell send --to <name> --text "..." [--track [context]] [--json]');
|
|
242
|
+
printError('Usage: marshell send --to <name> --text "..." [--async] [--track [context]] [--correlation-id <id>] [--json]');
|
|
239
243
|
}
|
|
240
244
|
const config = await (0, config_1.readConfig)();
|
|
241
245
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
242
|
-
const result = await (0, network_1.sendMessage)(networkUrl, to, text);
|
|
246
|
+
const result = await (0, network_1.sendMessage)(networkUrl, to, text, { correlationId });
|
|
243
247
|
if (result.kind === "sent" && trackContext) {
|
|
244
248
|
await (0, pending_1.trackPending)(to, {
|
|
245
249
|
sentMessageId: result.id,
|
|
@@ -257,6 +261,7 @@ async function cmdSend(args) {
|
|
|
257
261
|
}
|
|
258
262
|
if (result.kind === "sent") {
|
|
259
263
|
process.stdout.write(`Sent message to '${to}' (id: ${result.id}, status: ${result.status}).\n`);
|
|
264
|
+
process.stdout.write(`Poll delivery: marshell status --ids ${result.id} --wait 30\n`);
|
|
260
265
|
process.stdout.write(`${(0, network_1.formatWalletLine)(result.wallet)}\n`);
|
|
261
266
|
if (result.wallet.messages_remaining <= 10) {
|
|
262
267
|
process.stdout.write("Low balance — tell the owner to top up at https://console.marshell.dev/dashboard/billing\n");
|
|
@@ -264,6 +269,9 @@ async function cmdSend(args) {
|
|
|
264
269
|
if (trackContext) {
|
|
265
270
|
process.stdout.write(`Tracking reply from '${to}' (${trackContext}).\n`);
|
|
266
271
|
}
|
|
272
|
+
if (asyncSend) {
|
|
273
|
+
process.stdout.write(`Async mode — wait for reply: marshell wait --from ${to} --since ${result.id}\n`);
|
|
274
|
+
}
|
|
267
275
|
return;
|
|
268
276
|
}
|
|
269
277
|
if (result.wallet) {
|
|
@@ -273,6 +281,10 @@ async function cmdSend(args) {
|
|
|
273
281
|
printError(`${result.message} ${result.action ?? "Top up at console.marshell.dev/dashboard/billing"}`);
|
|
274
282
|
return;
|
|
275
283
|
}
|
|
284
|
+
if (result.status === 429) {
|
|
285
|
+
printError(`${result.message} Wait and retry — do not resend the same text.`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
276
288
|
printError(result.message);
|
|
277
289
|
}
|
|
278
290
|
async function cmdWallet(args) {
|
|
@@ -293,16 +305,23 @@ async function cmdWallet(args) {
|
|
|
293
305
|
}
|
|
294
306
|
async function cmdAsk(args) {
|
|
295
307
|
const json = hasFlag(args, "--json");
|
|
308
|
+
const noWait = hasFlag(args, "--no-wait");
|
|
296
309
|
const to = valueForFlag(args, "--to");
|
|
297
310
|
const text = valueForFlag(args, "--text");
|
|
298
311
|
const waitRaw = valueForFlag(args, "--wait");
|
|
299
|
-
const
|
|
312
|
+
const correlationId = valueForFlag(args, "--correlation-id");
|
|
313
|
+
const waitSeconds = waitRaw ? Number(waitRaw) : 300;
|
|
300
314
|
if (!to || !text) {
|
|
301
|
-
printError(
|
|
315
|
+
printError('Usage: marshell ask --to <name> --text "..." [--wait <seconds>] [--no-wait] [--correlation-id <id>] [--json]');
|
|
302
316
|
}
|
|
303
317
|
const config = await (0, config_1.readConfig)();
|
|
304
318
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
305
|
-
const result = await (0, network_1.askAgent)(networkUrl, to, text,
|
|
319
|
+
const result = await (0, network_1.askAgent)(networkUrl, to, text, {
|
|
320
|
+
waitSeconds,
|
|
321
|
+
noWait,
|
|
322
|
+
correlationId,
|
|
323
|
+
trackOnTimeout: true,
|
|
324
|
+
});
|
|
306
325
|
if (result.kind === "timeout") {
|
|
307
326
|
await (0, pending_1.trackPending)(to, {
|
|
308
327
|
sentMessageId: result.sent_id,
|
|
@@ -319,15 +338,85 @@ async function cmdAsk(args) {
|
|
|
319
338
|
return;
|
|
320
339
|
}
|
|
321
340
|
if (result.kind === "ok") {
|
|
341
|
+
if (result.delivery_status && result.delivery_status !== "received") {
|
|
342
|
+
process.stderr.write(`Note: delivery status is '${result.delivery_status}' (peer may not have read inbox yet).\n`);
|
|
343
|
+
}
|
|
322
344
|
process.stdout.write(`${result.reply}\n`);
|
|
323
345
|
return;
|
|
324
346
|
}
|
|
347
|
+
if (result.kind === "sent") {
|
|
348
|
+
process.stdout.write(`Sent to '${to}' (id: ${result.sent_id}). Wait: marshell wait --from ${to} --since ${result.sent_id}\n`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
325
351
|
if (result.kind === "timeout") {
|
|
326
|
-
process.stderr.write(`No reply from '${to}' within ${waitSeconds}s —
|
|
327
|
-
process.
|
|
352
|
+
process.stderr.write(`No reply from '${to}' within ${waitSeconds}s — tracked (sent id: ${result.sent_id}). Do not resend the same text.\n`);
|
|
353
|
+
process.stderr.write(`Wait later: marshell wait --from ${to} --since ${result.sent_id} --timeout 300\n`);
|
|
354
|
+
return;
|
|
328
355
|
}
|
|
329
356
|
printError(result.message);
|
|
330
357
|
}
|
|
358
|
+
async function cmdWait(args) {
|
|
359
|
+
const json = hasFlag(args, "--json");
|
|
360
|
+
const from = valueForFlag(args, "--from");
|
|
361
|
+
const since = valueForFlag(args, "--since");
|
|
362
|
+
const timeoutRaw = valueForFlag(args, "--timeout");
|
|
363
|
+
const timeoutSeconds = timeoutRaw ? Number(timeoutRaw) : 300;
|
|
364
|
+
if (!from) {
|
|
365
|
+
printError("Usage: marshell wait --from <name> [--since <msg_id>] [--timeout <seconds>] [--json]");
|
|
366
|
+
}
|
|
367
|
+
const config = await (0, config_1.readConfig)();
|
|
368
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
369
|
+
const result = await (0, network_1.waitForInbox)(networkUrl, {
|
|
370
|
+
waitSeconds: timeoutSeconds,
|
|
371
|
+
from,
|
|
372
|
+
sinceMessageId: since,
|
|
373
|
+
});
|
|
374
|
+
if (result.kind === "error") {
|
|
375
|
+
printError(result.message);
|
|
376
|
+
}
|
|
377
|
+
if (result.kind === "timeout") {
|
|
378
|
+
if (json) {
|
|
379
|
+
printJson({ timed_out: true, from, since });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
process.stderr.write(`No reply from '${from}' within ${timeoutSeconds}s. Do not resend — try again later.\n`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (json) {
|
|
386
|
+
printJson({ from, messages: result.messages, agent: result.agent });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const msg of result.messages) {
|
|
390
|
+
process.stdout.write(`${msg.text}\n`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function cmdStatus(args) {
|
|
394
|
+
const json = hasFlag(args, "--json");
|
|
395
|
+
const idsRaw = valueForFlag(args, "--ids");
|
|
396
|
+
const waitRaw = valueForFlag(args, "--wait");
|
|
397
|
+
const waitSeconds = waitRaw ? Number(waitRaw) : 0;
|
|
398
|
+
if (!idsRaw) {
|
|
399
|
+
printError("Usage: marshell status --ids <msg_id>[,...] [--wait <seconds>] [--json]");
|
|
400
|
+
}
|
|
401
|
+
const ids = idsRaw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
402
|
+
const config = await (0, config_1.readConfig)();
|
|
403
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
404
|
+
const result = await (0, network_1.fetchMessageStatus)(networkUrl, ids, waitSeconds);
|
|
405
|
+
if (result.kind === "error") {
|
|
406
|
+
printError(result.message);
|
|
407
|
+
}
|
|
408
|
+
if (json) {
|
|
409
|
+
printJson({ receipts: result.receipts });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (result.receipts.length === 0) {
|
|
413
|
+
process.stdout.write("No receipt yet (still delivering).\n");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const receipt of result.receipts) {
|
|
417
|
+
process.stdout.write(`${receipt.message_id}: ${receipt.status}${receipt.updated_at ? ` (${receipt.updated_at})` : ""}\n`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
331
420
|
async function cmdHistory(args) {
|
|
332
421
|
const json = hasFlag(args, "--json");
|
|
333
422
|
const withPeer = valueForFlag(args, "--with");
|
|
@@ -539,6 +628,14 @@ async function main() {
|
|
|
539
628
|
await cmdAsk(args.slice(1));
|
|
540
629
|
return;
|
|
541
630
|
}
|
|
631
|
+
if (args[0] === "wait") {
|
|
632
|
+
await cmdWait(args.slice(1));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (args[0] === "status") {
|
|
636
|
+
await cmdStatus(args.slice(1));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
542
639
|
if (args[0] === "inbox") {
|
|
543
640
|
await cmdInbox(args.slice(1));
|
|
544
641
|
return;
|
package/dist/network.js
CHANGED
|
@@ -11,11 +11,13 @@ exports.sendMessage = sendMessage;
|
|
|
11
11
|
exports.fetchInbox = fetchInbox;
|
|
12
12
|
exports.ackMessages = ackMessages;
|
|
13
13
|
exports.waitForInbox = waitForInbox;
|
|
14
|
+
exports.fetchMessageStatus = fetchMessageStatus;
|
|
15
|
+
exports.waitForDelivery = waitForDelivery;
|
|
14
16
|
exports.fetchHistory = fetchHistory;
|
|
15
17
|
exports.askAgent = askAgent;
|
|
16
18
|
exports.toWsUrl = toWsUrl;
|
|
17
19
|
const config_1 = require("./config");
|
|
18
|
-
exports.MARSHELL_CLI_VERSION = "0.
|
|
20
|
+
exports.MARSHELL_CLI_VERSION = "0.8.0";
|
|
19
21
|
function formatWalletLine(wallet) {
|
|
20
22
|
const parts = [];
|
|
21
23
|
if (wallet.free_remaining > 0) {
|
|
@@ -42,6 +44,33 @@ function parseWallet(raw) {
|
|
|
42
44
|
messages_remaining: w.messages_remaining,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
47
|
+
async function getJson(url, headers) {
|
|
48
|
+
const attempts = 3;
|
|
49
|
+
for (let i = 0; i < attempts; i++) {
|
|
50
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
let data = {};
|
|
53
|
+
if (text.trim().length > 0) {
|
|
54
|
+
try {
|
|
55
|
+
data = JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
data = { raw: text };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const result = { status: response.status, data: data };
|
|
62
|
+
if (isRetryableStatus(response.status) &&
|
|
63
|
+
i < attempts - 1) {
|
|
64
|
+
const waitMs = response.status === 429
|
|
65
|
+
? parseRetryAfterSeconds(response.headers.get("Retry-After")) * 1000
|
|
66
|
+
: 250 * (i + 1);
|
|
67
|
+
await sleep(waitMs);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
return { status: 0, data: {} };
|
|
73
|
+
}
|
|
45
74
|
function normalizeBaseUrl(raw) {
|
|
46
75
|
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
47
76
|
}
|
|
@@ -145,29 +174,13 @@ async function postJson(url, body, headers) {
|
|
|
145
174
|
shouldRetry: (result) => isRetryableStatus(result.status),
|
|
146
175
|
});
|
|
147
176
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
let data = {};
|
|
156
|
-
if (text.trim().length > 0) {
|
|
157
|
-
try {
|
|
158
|
-
data = JSON.parse(text);
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
data = { raw: text };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
status: response.status,
|
|
166
|
-
data: data,
|
|
167
|
-
};
|
|
168
|
-
}, {
|
|
169
|
-
shouldRetry: (result) => isRetryableStatus(result.status),
|
|
170
|
-
});
|
|
177
|
+
function parseRetryAfterSeconds(raw) {
|
|
178
|
+
if (!raw)
|
|
179
|
+
return 60;
|
|
180
|
+
const n = Number(raw.trim());
|
|
181
|
+
if (Number.isFinite(n) && n > 0)
|
|
182
|
+
return Math.min(120, Math.ceil(n));
|
|
183
|
+
return 60;
|
|
171
184
|
}
|
|
172
185
|
async function joinAgent(baseUrl, name, options = {}) {
|
|
173
186
|
const config = await (0, config_1.readConfig)();
|
|
@@ -267,41 +280,84 @@ async function fetchWallet(baseUrl) {
|
|
|
267
280
|
return { kind: "error", message: error.message };
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
|
-
async function sendMessage(baseUrl, to, text) {
|
|
283
|
+
async function sendMessage(baseUrl, to, text, options = {}) {
|
|
271
284
|
try {
|
|
272
285
|
const headers = await authHeaders("agent");
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
286
|
+
if (options.correlationId?.trim()) {
|
|
287
|
+
headers["x-correlation-id"] = options.correlationId.trim();
|
|
288
|
+
}
|
|
289
|
+
const body = { to, text };
|
|
290
|
+
if (options.correlationId?.trim()) {
|
|
291
|
+
body.correlation_id = options.correlationId.trim();
|
|
292
|
+
}
|
|
293
|
+
const attempts = 2;
|
|
294
|
+
let lastStatus = 0;
|
|
295
|
+
let lastData = {};
|
|
296
|
+
for (let i = 0; i < attempts; i++) {
|
|
297
|
+
const response = await fetch(withPath(baseUrl, "/v1/messages/send"), {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers,
|
|
300
|
+
body: JSON.stringify(body),
|
|
301
|
+
});
|
|
302
|
+
lastStatus = response.status;
|
|
303
|
+
const raw = await response.text();
|
|
304
|
+
if (raw.trim().length > 0) {
|
|
305
|
+
try {
|
|
306
|
+
lastData = JSON.parse(raw);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
lastData = { error: raw };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (response.status === 429 && i < attempts - 1) {
|
|
313
|
+
await sleep(parseRetryAfterSeconds(response.headers.get("Retry-After")) * 1000);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
const wallet = parseWallet(lastData.wallet);
|
|
319
|
+
if (lastStatus === 402) {
|
|
276
320
|
return {
|
|
277
321
|
kind: "error",
|
|
278
322
|
status: 402,
|
|
279
|
-
code:
|
|
280
|
-
message:
|
|
323
|
+
code: lastData.code ?? "payment_required",
|
|
324
|
+
message: lastData.error ?? "No message credits remaining.",
|
|
281
325
|
wallet,
|
|
282
|
-
action:
|
|
326
|
+
action: lastData.action ??
|
|
283
327
|
"Ask the subnet owner to top up at https://console.marshell.dev/dashboard/billing",
|
|
284
328
|
};
|
|
285
329
|
}
|
|
286
|
-
if (
|
|
330
|
+
if (lastStatus === 429) {
|
|
331
|
+
return {
|
|
332
|
+
kind: "error",
|
|
333
|
+
status: 429,
|
|
334
|
+
code: "rate_limited",
|
|
335
|
+
message: lastData.error ??
|
|
336
|
+
"Rate limit exceeded. Retry after the Retry-After interval.",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (lastStatus >= 200 && lastStatus < 300 && lastData.id) {
|
|
287
340
|
if (!wallet) {
|
|
288
341
|
return {
|
|
289
342
|
kind: "error",
|
|
290
343
|
message: "Send succeeded but wallet balance was missing from response.",
|
|
291
|
-
status:
|
|
344
|
+
status: lastStatus,
|
|
292
345
|
};
|
|
293
346
|
}
|
|
294
347
|
return {
|
|
295
348
|
kind: "sent",
|
|
296
|
-
id:
|
|
297
|
-
status:
|
|
349
|
+
id: lastData.id,
|
|
350
|
+
status: lastData.status ?? "delivered",
|
|
298
351
|
wallet,
|
|
352
|
+
poll_status: lastData.poll_status ??
|
|
353
|
+
`/v1/messages/status?ids=${lastData.id}`,
|
|
354
|
+
correlation_id: lastData.correlation_id,
|
|
299
355
|
};
|
|
300
356
|
}
|
|
301
357
|
return {
|
|
302
358
|
kind: "error",
|
|
303
|
-
status:
|
|
304
|
-
message:
|
|
359
|
+
status: lastStatus,
|
|
360
|
+
message: lastData.error ?? `Send failed with HTTP ${lastStatus}.`,
|
|
305
361
|
wallet,
|
|
306
362
|
};
|
|
307
363
|
}
|
|
@@ -367,6 +423,19 @@ async function ackMessages(baseUrl, ids) {
|
|
|
367
423
|
async function waitForInbox(baseUrl, options) {
|
|
368
424
|
const deadline = Date.now() + options.waitSeconds * 1000;
|
|
369
425
|
const from = options.from?.toLowerCase();
|
|
426
|
+
let sinceMs = 0;
|
|
427
|
+
if (options.sinceMessageId) {
|
|
428
|
+
const history = await fetchHistory(baseUrl, {
|
|
429
|
+
with: options.from,
|
|
430
|
+
limit: 100,
|
|
431
|
+
});
|
|
432
|
+
if (history.kind === "ok") {
|
|
433
|
+
const sent = history.messages.find((m) => m.id === options.sinceMessageId);
|
|
434
|
+
if (sent) {
|
|
435
|
+
sinceMs = Date.parse(sent.created_at) - 2000;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
370
439
|
while (Date.now() < deadline) {
|
|
371
440
|
const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
|
|
372
441
|
const result = await fetchInbox(baseUrl, {
|
|
@@ -377,7 +446,15 @@ async function waitForInbox(baseUrl, options) {
|
|
|
377
446
|
return result;
|
|
378
447
|
}
|
|
379
448
|
const messages = from
|
|
380
|
-
? result.messages.filter((m) =>
|
|
449
|
+
? result.messages.filter((m) => {
|
|
450
|
+
if (m.from.toLowerCase() !== from)
|
|
451
|
+
return false;
|
|
452
|
+
if (sinceMs > 0) {
|
|
453
|
+
const created = Date.parse(m.created_at);
|
|
454
|
+
return !Number.isNaN(created) && created >= sinceMs;
|
|
455
|
+
}
|
|
456
|
+
return true;
|
|
457
|
+
})
|
|
381
458
|
: result.messages;
|
|
382
459
|
if (messages.length > 0) {
|
|
383
460
|
await ackMessages(baseUrl, messages.map((m) => m.id));
|
|
@@ -386,6 +463,49 @@ async function waitForInbox(baseUrl, options) {
|
|
|
386
463
|
}
|
|
387
464
|
return { kind: "timeout" };
|
|
388
465
|
}
|
|
466
|
+
async function fetchMessageStatus(baseUrl, ids, waitSeconds = 0) {
|
|
467
|
+
if (ids.length === 0) {
|
|
468
|
+
return { kind: "ok", receipts: [] };
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const headers = await authHeaders("agent");
|
|
472
|
+
const params = new URLSearchParams({ ids: ids.join(",") });
|
|
473
|
+
if (waitSeconds > 0) {
|
|
474
|
+
params.set("wait", String(Math.min(120, waitSeconds)));
|
|
475
|
+
}
|
|
476
|
+
const result = await getJson(withPath(baseUrl, `/v1/messages/status?${params}`), headers);
|
|
477
|
+
if (result.status < 200 || result.status >= 300) {
|
|
478
|
+
return {
|
|
479
|
+
kind: "error",
|
|
480
|
+
status: result.status,
|
|
481
|
+
message: result.data.error ?? `Status failed with HTTP ${result.status}.`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return { kind: "ok", receipts: result.data.receipts ?? [] };
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return { kind: "error", message: error.message };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function waitForDelivery(baseUrl, messageId, timeoutSeconds) {
|
|
491
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
492
|
+
let lastStatus;
|
|
493
|
+
while (Date.now() < deadline) {
|
|
494
|
+
const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
|
|
495
|
+
const result = await fetchMessageStatus(baseUrl, [messageId], remaining);
|
|
496
|
+
if (result.kind === "error") {
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
const receipt = result.receipts[0];
|
|
500
|
+
if (receipt) {
|
|
501
|
+
lastStatus = receipt.status;
|
|
502
|
+
if (receipt.status === "received") {
|
|
503
|
+
return { kind: "ok", status: receipt.status, receipt };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return { kind: "timeout", last_status: lastStatus };
|
|
508
|
+
}
|
|
389
509
|
async function fetchHistory(baseUrl, options) {
|
|
390
510
|
try {
|
|
391
511
|
const headers = await authHeaders("agent");
|
|
@@ -416,8 +536,10 @@ async function fetchHistory(baseUrl, options) {
|
|
|
416
536
|
return { kind: "error", message: error.message };
|
|
417
537
|
}
|
|
418
538
|
}
|
|
419
|
-
async function askAgent(baseUrl, to, text,
|
|
420
|
-
|
|
539
|
+
async function askAgent(baseUrl, to, text, options) {
|
|
540
|
+
const opts = typeof options === "number" ? { waitSeconds: options } : options;
|
|
541
|
+
const waitSeconds = opts.waitSeconds;
|
|
542
|
+
const pollDelivery = opts.pollDelivery !== false;
|
|
421
543
|
const stale = await fetchInbox(baseUrl, { peek: true });
|
|
422
544
|
if (stale.kind === "ok") {
|
|
423
545
|
const fromPeer = stale.messages.filter((m) => m.from.toLowerCase() === to.toLowerCase());
|
|
@@ -425,15 +547,31 @@ async function askAgent(baseUrl, to, text, waitSeconds) {
|
|
|
425
547
|
await ackMessages(baseUrl, fromPeer.map((m) => m.id));
|
|
426
548
|
}
|
|
427
549
|
}
|
|
428
|
-
const
|
|
429
|
-
|
|
550
|
+
const sent = await sendMessage(baseUrl, to, text, {
|
|
551
|
+
correlationId: opts.correlationId,
|
|
552
|
+
});
|
|
430
553
|
if (sent.kind !== "sent") {
|
|
431
554
|
return { kind: "error", message: sent.message };
|
|
432
555
|
}
|
|
556
|
+
if (opts.noWait) {
|
|
557
|
+
return {
|
|
558
|
+
kind: "sent",
|
|
559
|
+
sent_id: sent.id,
|
|
560
|
+
poll_status: sent.poll_status,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const sentAt = Date.now() - 2000;
|
|
433
564
|
const deadline = Date.now() + waitSeconds * 1000;
|
|
434
565
|
const peer = to.toLowerCase();
|
|
566
|
+
let deliveryStatus = sent.status;
|
|
435
567
|
while (Date.now() < deadline) {
|
|
436
568
|
const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
|
|
569
|
+
if (pollDelivery && deliveryStatus !== "received") {
|
|
570
|
+
const delivery = await fetchMessageStatus(baseUrl, [sent.id], Math.min(10, remaining));
|
|
571
|
+
if (delivery.kind === "ok" && delivery.receipts[0]) {
|
|
572
|
+
deliveryStatus = delivery.receipts[0].status;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
437
575
|
const result = await fetchInbox(baseUrl, {
|
|
438
576
|
peek: true,
|
|
439
577
|
waitSeconds: remaining,
|
|
@@ -453,10 +591,17 @@ async function askAgent(baseUrl, to, text, waitSeconds) {
|
|
|
453
591
|
kind: "ok",
|
|
454
592
|
reply: messages.map((m) => m.text.trim()).join("\n\n"),
|
|
455
593
|
message_id: messages[0]?.id ?? sent.id,
|
|
594
|
+
sent_id: sent.id,
|
|
595
|
+
delivery_status: deliveryStatus,
|
|
456
596
|
};
|
|
457
597
|
}
|
|
458
598
|
}
|
|
459
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
kind: "timeout",
|
|
601
|
+
sent_id: sent.id,
|
|
602
|
+
delivery_status: deliveryStatus,
|
|
603
|
+
tracked: opts.trackOnTimeout,
|
|
604
|
+
};
|
|
460
605
|
}
|
|
461
606
|
function toWsUrl(httpBase) {
|
|
462
607
|
const url = new URL(httpBase);
|