@marshell/cli 0.7.9 → 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 +185 -56
- 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,57 +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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|
|
284
311
|
}
|
|
285
|
-
|
|
286
|
-
|
|
312
|
+
if (response.status === 429 && i < attempts - 1) {
|
|
313
|
+
await sleep(parseRetryAfterSeconds(response.headers.get("Retry-After")) * 1000);
|
|
314
|
+
continue;
|
|
287
315
|
}
|
|
316
|
+
break;
|
|
288
317
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
if (result.status === 402) {
|
|
318
|
+
const wallet = parseWallet(lastData.wallet);
|
|
319
|
+
if (lastStatus === 402) {
|
|
292
320
|
return {
|
|
293
321
|
kind: "error",
|
|
294
322
|
status: 402,
|
|
295
|
-
code:
|
|
296
|
-
message:
|
|
323
|
+
code: lastData.code ?? "payment_required",
|
|
324
|
+
message: lastData.error ?? "No message credits remaining.",
|
|
297
325
|
wallet,
|
|
298
|
-
action:
|
|
326
|
+
action: lastData.action ??
|
|
299
327
|
"Ask the subnet owner to top up at https://console.marshell.dev/dashboard/billing",
|
|
300
328
|
};
|
|
301
329
|
}
|
|
302
|
-
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) {
|
|
303
340
|
if (!wallet) {
|
|
304
341
|
return {
|
|
305
342
|
kind: "error",
|
|
306
343
|
message: "Send succeeded but wallet balance was missing from response.",
|
|
307
|
-
status:
|
|
344
|
+
status: lastStatus,
|
|
308
345
|
};
|
|
309
346
|
}
|
|
310
347
|
return {
|
|
311
348
|
kind: "sent",
|
|
312
|
-
id:
|
|
313
|
-
status:
|
|
349
|
+
id: lastData.id,
|
|
350
|
+
status: lastData.status ?? "delivered",
|
|
314
351
|
wallet,
|
|
352
|
+
poll_status: lastData.poll_status ??
|
|
353
|
+
`/v1/messages/status?ids=${lastData.id}`,
|
|
354
|
+
correlation_id: lastData.correlation_id,
|
|
315
355
|
};
|
|
316
356
|
}
|
|
317
357
|
return {
|
|
318
358
|
kind: "error",
|
|
319
|
-
status:
|
|
320
|
-
message:
|
|
359
|
+
status: lastStatus,
|
|
360
|
+
message: lastData.error ?? `Send failed with HTTP ${lastStatus}.`,
|
|
321
361
|
wallet,
|
|
322
362
|
};
|
|
323
363
|
}
|
|
@@ -383,6 +423,19 @@ async function ackMessages(baseUrl, ids) {
|
|
|
383
423
|
async function waitForInbox(baseUrl, options) {
|
|
384
424
|
const deadline = Date.now() + options.waitSeconds * 1000;
|
|
385
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
|
+
}
|
|
386
439
|
while (Date.now() < deadline) {
|
|
387
440
|
const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
|
|
388
441
|
const result = await fetchInbox(baseUrl, {
|
|
@@ -393,7 +446,15 @@ async function waitForInbox(baseUrl, options) {
|
|
|
393
446
|
return result;
|
|
394
447
|
}
|
|
395
448
|
const messages = from
|
|
396
|
-
? 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
|
+
})
|
|
397
458
|
: result.messages;
|
|
398
459
|
if (messages.length > 0) {
|
|
399
460
|
await ackMessages(baseUrl, messages.map((m) => m.id));
|
|
@@ -402,6 +463,49 @@ async function waitForInbox(baseUrl, options) {
|
|
|
402
463
|
}
|
|
403
464
|
return { kind: "timeout" };
|
|
404
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
|
+
}
|
|
405
509
|
async function fetchHistory(baseUrl, options) {
|
|
406
510
|
try {
|
|
407
511
|
const headers = await authHeaders("agent");
|
|
@@ -432,8 +536,10 @@ async function fetchHistory(baseUrl, options) {
|
|
|
432
536
|
return { kind: "error", message: error.message };
|
|
433
537
|
}
|
|
434
538
|
}
|
|
435
|
-
async function askAgent(baseUrl, to, text,
|
|
436
|
-
|
|
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;
|
|
437
543
|
const stale = await fetchInbox(baseUrl, { peek: true });
|
|
438
544
|
if (stale.kind === "ok") {
|
|
439
545
|
const fromPeer = stale.messages.filter((m) => m.from.toLowerCase() === to.toLowerCase());
|
|
@@ -441,15 +547,31 @@ async function askAgent(baseUrl, to, text, waitSeconds) {
|
|
|
441
547
|
await ackMessages(baseUrl, fromPeer.map((m) => m.id));
|
|
442
548
|
}
|
|
443
549
|
}
|
|
444
|
-
const
|
|
445
|
-
|
|
550
|
+
const sent = await sendMessage(baseUrl, to, text, {
|
|
551
|
+
correlationId: opts.correlationId,
|
|
552
|
+
});
|
|
446
553
|
if (sent.kind !== "sent") {
|
|
447
554
|
return { kind: "error", message: sent.message };
|
|
448
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;
|
|
449
564
|
const deadline = Date.now() + waitSeconds * 1000;
|
|
450
565
|
const peer = to.toLowerCase();
|
|
566
|
+
let deliveryStatus = sent.status;
|
|
451
567
|
while (Date.now() < deadline) {
|
|
452
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
|
+
}
|
|
453
575
|
const result = await fetchInbox(baseUrl, {
|
|
454
576
|
peek: true,
|
|
455
577
|
waitSeconds: remaining,
|
|
@@ -469,10 +591,17 @@ async function askAgent(baseUrl, to, text, waitSeconds) {
|
|
|
469
591
|
kind: "ok",
|
|
470
592
|
reply: messages.map((m) => m.text.trim()).join("\n\n"),
|
|
471
593
|
message_id: messages[0]?.id ?? sent.id,
|
|
594
|
+
sent_id: sent.id,
|
|
595
|
+
delivery_status: deliveryStatus,
|
|
472
596
|
};
|
|
473
597
|
}
|
|
474
598
|
}
|
|
475
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
kind: "timeout",
|
|
601
|
+
sent_id: sent.id,
|
|
602
|
+
delivery_status: deliveryStatus,
|
|
603
|
+
tracked: opts.trackOnTimeout,
|
|
604
|
+
};
|
|
476
605
|
}
|
|
477
606
|
function toWsUrl(httpBase) {
|
|
478
607
|
const url = new URL(httpBase);
|