@marshell/cli 0.7.2 → 0.7.5

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/bridge.js CHANGED
@@ -396,7 +396,7 @@ async function handleInbound(networkUrl, msg, options) {
396
396
  created_at: msg.created_at,
397
397
  pending: pending ?? undefined,
398
398
  });
399
- // Push to human channel (Telegram, webhook, etc.) side effect only.
399
+ // Push to human channel. On failure, leave message in inbox for `relay cron`.
400
400
  if (options.notify && !isEcho(msg.from, msg.text)) {
401
401
  try {
402
402
  await runNotifyCommand(options.notify, msg, pending);
@@ -408,6 +408,7 @@ async function handleInbound(networkUrl, msg, options) {
408
408
  });
409
409
  if (pending) {
410
410
  await (0, pending_1.clearPending)(msg.from);
411
+ await (0, relay_state_1.clearWaitingReported)(msg.from);
411
412
  }
412
413
  await (0, relay_state_1.markRelayed)([msg.id]);
413
414
  }
@@ -418,9 +419,10 @@ async function handleInbound(networkUrl, msg, options) {
418
419
  message: `notify: ${message}`,
419
420
  in_reply_to: msg.id,
420
421
  });
422
+ // Do not ack — cron will retry delivery to the human.
423
+ return;
421
424
  }
422
425
  }
423
- // Always ack so backlog never sticks.
424
426
  await (0, network_1.ackMessages)(networkUrl, [msg.id]);
425
427
  // Drop echoes of our own recent outbound (hi↔hi loops).
426
428
  if (isEcho(msg.from, msg.text)) {
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ function printHelp() {
24
24
  " marshell inbox [--json] [--wait <seconds>] [--from <name>]",
25
25
  " marshell history [--with <name>] [--limit <n>] [--json]",
26
26
  " marshell listen [--json] [--notify <cmd>] (deliver-only listener)",
27
- " marshell relay cron [--include-untracked] [--json]",
27
+ " marshell relay cron [--quiet] [--include-untracked] [--json]",
28
28
  " marshell pending list|clear [--peer <name>] [--json]",
29
29
  " marshell --help",
30
30
  "",
@@ -370,13 +370,14 @@ async function cmdInbox(args) {
370
370
  async function cmdRelay(args) {
371
371
  const sub = args[0];
372
372
  if (sub !== "cron") {
373
- printError("Usage: marshell relay cron [--include-untracked] [--json]");
373
+ printError("Usage: marshell relay cron [--quiet] [--include-untracked] [--json]");
374
374
  }
375
375
  const json = hasFlag(args, "--json");
376
+ const quiet = hasFlag(args, "--quiet");
376
377
  const pendingOnly = !hasFlag(args, "--include-untracked");
377
378
  const config = await (0, config_1.readConfig)();
378
379
  const networkUrl = (0, config_1.getNetworkUrl)(config);
379
- const result = await (0, relay_cron_1.runRelayCron)({ networkUrl, pendingOnly });
380
+ const result = await (0, relay_cron_1.runRelayCron)({ networkUrl, pendingOnly, quiet });
380
381
  if (json) {
381
382
  printJson({
382
383
  ...result,
@@ -384,18 +385,19 @@ async function cmdRelay(args) {
384
385
  });
385
386
  return;
386
387
  }
387
- if (result.message && result.relayed.length === 0) {
388
- process.stdout.write(`${result.message}\n`);
388
+ if (!result.notify) {
389
389
  return;
390
390
  }
391
- if (result.relayed.length === 0) {
392
- process.stdout.write("Nothing new to relay.\n");
391
+ if (result.relayed.length > 0) {
392
+ if (result.pending_count === 0 && !pendingOnly) {
393
+ process.stdout.write(`${result.relayed.length} new inbound message(s):\n\n`);
394
+ }
395
+ process.stdout.write(`${(0, relay_cron_1.formatRelayOutput)(result.relayed)}\n`);
393
396
  return;
394
397
  }
395
- if (result.pending_count === 0 && !pendingOnly) {
396
- process.stdout.write(`${result.relayed.length} new inbound message(s):\n\n`);
398
+ if (result.message) {
399
+ process.stdout.write(`${result.message}\n`);
397
400
  }
398
- process.stdout.write(`${(0, relay_cron_1.formatRelayOutput)(result.relayed)}\n`);
399
401
  }
400
402
  async function cmdPending(args) {
401
403
  const json = hasFlag(args, "--json");
@@ -17,6 +17,7 @@ function formatRelayOutput(items) {
17
17
  return items.map(formatItem).join("\n\n---\n\n");
18
18
  }
19
19
  async function runRelayCron(options) {
20
+ const quiet = options.quiet ?? false;
20
21
  const pending = await (0, pending_1.listPending)();
21
22
  const pendingMap = new Map(pending.map((p) => [p.peer.toLowerCase(), p]));
22
23
  if (options.pendingOnly && pending.length === 0) {
@@ -24,7 +25,10 @@ async function runRelayCron(options) {
24
25
  pending_count: 0,
25
26
  relayed: [],
26
27
  skipped_relayed: 0,
27
- message: "No pending tracked sends — list is empty.",
28
+ message: quiet
29
+ ? undefined
30
+ : "No pending tracked sends — list is empty.",
31
+ notify: !quiet,
28
32
  };
29
33
  }
30
34
  const inbox = await (0, network_1.fetchInbox)(options.networkUrl, { peek: true });
@@ -55,17 +59,46 @@ async function runRelayCron(options) {
55
59
  }
56
60
  for (const peer of peersToClear) {
57
61
  await (0, pending_1.clearPending)(peer);
62
+ await (0, relay_state_1.clearWaitingReported)(peer);
63
+ }
64
+ if (items.length > 0) {
65
+ return {
66
+ pending_count: pending.length,
67
+ relayed: items,
68
+ skipped_relayed: skippedRelayed,
69
+ notify: true,
70
+ };
58
71
  }
59
72
  let message;
60
- if (items.length === 0 && pending.length > 0) {
61
- const names = pending.map((p) => p.peer).join(", ");
62
- message = `Waiting for replies from: ${names}`;
73
+ let notify = !quiet;
74
+ if (pending.length > 0) {
75
+ const waitingPeers = [];
76
+ for (const entry of pending) {
77
+ const shouldReport = await (0, relay_state_1.shouldReportWaiting)(entry.peer, entry.sentAt);
78
+ if (shouldReport) {
79
+ waitingPeers.push(entry.peer);
80
+ await (0, relay_state_1.markWaitingReported)(entry.peer, entry.sentAt);
81
+ }
82
+ }
83
+ if (waitingPeers.length > 0) {
84
+ message = `Waiting for replies from: ${waitingPeers.join(", ")}`;
85
+ notify = true;
86
+ }
87
+ else if (quiet) {
88
+ message = undefined;
89
+ notify = false;
90
+ }
91
+ else {
92
+ message = `Still waiting for: ${pending.map((p) => p.peer).join(", ")}`;
93
+ notify = true;
94
+ }
63
95
  }
64
96
  return {
65
97
  pending_count: pending.length,
66
- relayed: items,
98
+ relayed: [],
67
99
  skipped_relayed: skippedRelayed,
68
100
  message,
101
+ notify,
69
102
  };
70
103
  }
71
104
  function buildItem(msg, pending, kind) {
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.filterUnrelayed = filterUnrelayed;
4
4
  exports.markRelayed = markRelayed;
5
+ exports.shouldReportWaiting = shouldReportWaiting;
6
+ exports.markWaitingReported = markWaitingReported;
7
+ exports.clearWaitingReported = clearWaitingReported;
5
8
  const promises_1 = require("node:fs/promises");
6
9
  const node_path_1 = require("node:path");
7
10
  const node_os_1 = require("node:os");
@@ -9,21 +12,36 @@ const MAX_IDS = 500;
9
12
  function statePath() {
10
13
  return (0, node_path_1.join)((0, node_os_1.homedir)(), ".marshell", "relay-state.json");
11
14
  }
15
+ function emptyState() {
16
+ return {
17
+ relayedIds: [],
18
+ waitingReported: {},
19
+ updatedAt: new Date().toISOString(),
20
+ };
21
+ }
12
22
  async function readState() {
13
23
  try {
14
24
  const raw = await (0, promises_1.readFile)(statePath(), "utf8");
15
- return JSON.parse(raw);
25
+ const parsed = JSON.parse(raw);
26
+ return {
27
+ relayedIds: parsed.relayedIds ?? [],
28
+ waitingReported: parsed.waitingReported ?? {},
29
+ updatedAt: parsed.updatedAt ?? new Date().toISOString(),
30
+ };
16
31
  }
17
32
  catch (error) {
18
33
  const maybeCode = error.code;
19
34
  if (maybeCode === "ENOENT") {
20
- return { relayedIds: [], updatedAt: new Date().toISOString() };
35
+ return emptyState();
21
36
  }
22
37
  throw error;
23
38
  }
24
39
  }
25
40
  async function writeState(state) {
26
41
  await (0, promises_1.mkdir)((0, node_path_1.join)((0, node_os_1.homedir)(), ".marshell"), { recursive: true });
42
+ await writeStateFile(state);
43
+ }
44
+ async function writeStateFile(state) {
27
45
  await (0, promises_1.writeFile)(statePath(), `${JSON.stringify(state, null, 2)}\n`, "utf8");
28
46
  }
29
47
  async function filterUnrelayed(messages) {
@@ -37,8 +55,37 @@ async function markRelayed(ids) {
37
55
  const state = await readState();
38
56
  const merged = [...new Set([...state.relayedIds, ...ids])];
39
57
  const trimmed = merged.length > MAX_IDS ? merged.slice(merged.length - MAX_IDS) : merged;
40
- await writeState({
58
+ await writeStateFile({
59
+ ...state,
41
60
  relayedIds: trimmed,
42
61
  updatedAt: new Date().toISOString(),
43
62
  });
44
63
  }
64
+ /** True if we should tell the human we're waiting on this peer (once per pending). */
65
+ async function shouldReportWaiting(peer, pendingSentAt) {
66
+ const state = await readState();
67
+ const key = peer.toLowerCase();
68
+ return state.waitingReported[key] !== pendingSentAt;
69
+ }
70
+ async function markWaitingReported(peer, pendingSentAt) {
71
+ const state = await readState();
72
+ const key = peer.toLowerCase();
73
+ await writeStateFile({
74
+ ...state,
75
+ waitingReported: { ...state.waitingReported, [key]: pendingSentAt },
76
+ updatedAt: new Date().toISOString(),
77
+ });
78
+ }
79
+ async function clearWaitingReported(peer) {
80
+ const state = await readState();
81
+ const key = peer.toLowerCase();
82
+ if (!(key in state.waitingReported))
83
+ return;
84
+ const next = { ...state.waitingReported };
85
+ delete next[key];
86
+ await writeStateFile({
87
+ ...state,
88
+ waitingReported: next,
89
+ updatedAt: new Date().toISOString(),
90
+ });
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshell/cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.5",
4
4
  "description": "Marshell CLI — join a subnet, discover agents, send messages",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
package/scripts/relay.mjs CHANGED
@@ -58,11 +58,21 @@ async function main() {
58
58
  process.exit(0);
59
59
  }
60
60
 
61
- if (format === "json") {
62
- process.stdout.write(`${JSON.stringify({ ...payload, message })}\n`);
63
- } else {
64
- process.stdout.write(`${message}\n`);
61
+ // Without a webhook, stdout is only a log — not human delivery. Fail so
62
+ // listener leaves inbox intact and `marshell relay cron` can deliver.
63
+ if (process.env.MARSHELL_ALLOW_STDOUT_RELAY === "1") {
64
+ if (format === "json") {
65
+ process.stdout.write(`${JSON.stringify({ ...payload, message })}\n`);
66
+ } else {
67
+ process.stdout.write(`${message}\n`);
68
+ }
69
+ process.exit(0);
65
70
  }
71
+
72
+ process.stderr.write(
73
+ "relay: MARSHELL_NOTIFY_WEBHOOK not set — use relay cron for human delivery\n",
74
+ );
75
+ process.exit(1);
66
76
  }
67
77
 
68
78
  main().catch((err) => {