@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 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
- **Delivery status:** `send` returns `delivered` when the message is queued in the peer's inbox. It becomes `received` after the peer runs `inbox` (auto-ack). Use `GET /v1/messages/status?ids=...&wait=30` to poll receipts.
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 ask --to <name> --text "..." [--wait 60] [--json]
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 waitSeconds = waitRaw ? Number(waitRaw) : 120;
312
+ const correlationId = valueForFlag(args, "--correlation-id");
313
+ const waitSeconds = waitRaw ? Number(waitRaw) : 300;
300
314
  if (!to || !text) {
301
- printError("Usage: marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]");
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, waitSeconds);
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 — tracking for later notify (sent id: ${result.sent_id}).\n`);
327
- process.exit(1);
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.7.8";
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
- async function getJson(url, headers) {
149
- return withRetry(async () => {
150
- const response = await fetch(url, {
151
- method: "GET",
152
- headers,
153
- });
154
- const text = await response.text();
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
- const result = await postJson(withPath(baseUrl, "/v1/messages/send"), { to, text }, headers);
274
- const wallet = parseWallet(result.data.wallet);
275
- if (result.status === 402) {
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: result.data.code ?? "payment_required",
280
- message: result.data.error ?? "No message credits remaining.",
323
+ code: lastData.code ?? "payment_required",
324
+ message: lastData.error ?? "No message credits remaining.",
281
325
  wallet,
282
- action: result.data.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 (result.status >= 200 && result.status < 300 && result.data.id) {
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: result.status,
344
+ status: lastStatus,
292
345
  };
293
346
  }
294
347
  return {
295
348
  kind: "sent",
296
- id: result.data.id,
297
- status: result.data.status ?? "delivered",
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: result.status,
304
- message: result.data.error ?? `Send failed with HTTP ${result.status}.`,
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) => m.from.toLowerCase() === from)
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, waitSeconds) {
420
- // Drain any stale messages from this peer before asking.
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 sentAt = Date.now() - 2000;
429
- const sent = await sendMessage(baseUrl, to, text);
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 { kind: "timeout", sent_id: sent.id };
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshell/cli",
3
- "version": "0.7.8",
3
+ "version": "0.8.0",
4
4
  "description": "Marshell CLI — join a subnet, discover agents, send messages",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",