@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 +4 -2
- package/dist/cli.js +12 -10
- package/dist/relay-cron.js +38 -5
- package/dist/relay-state.js +50 -3
- package/package.json +1 -1
- package/scripts/relay.mjs +14 -4
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
|
|
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.
|
|
388
|
-
process.stdout.write(`${result.message}\n`);
|
|
388
|
+
if (!result.notify) {
|
|
389
389
|
return;
|
|
390
390
|
}
|
|
391
|
-
if (result.relayed.length
|
|
392
|
-
|
|
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.
|
|
396
|
-
process.stdout.write(`${result.
|
|
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");
|
package/dist/relay-cron.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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:
|
|
98
|
+
relayed: [],
|
|
67
99
|
skipped_relayed: skippedRelayed,
|
|
68
100
|
message,
|
|
101
|
+
notify,
|
|
69
102
|
};
|
|
70
103
|
}
|
|
71
104
|
function buildItem(msg, pending, kind) {
|
package/dist/relay-state.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
package/scripts/relay.mjs
CHANGED
|
@@ -58,11 +58,21 @@ async function main() {
|
|
|
58
58
|
process.exit(0);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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) => {
|