@marshell/cli 0.7.1 → 0.7.4
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 +5 -2
- package/dist/cli.js +32 -0
- package/dist/relay-cron.js +79 -0
- package/dist/relay-state.js +44 -0
- package/package.json +1 -1
- package/scripts/relay.mjs +15 -5
package/dist/bridge.js
CHANGED
|
@@ -8,6 +8,7 @@ const node_fs_1 = require("node:fs");
|
|
|
8
8
|
const node_path_1 = require("node:path");
|
|
9
9
|
const network_1 = require("./network");
|
|
10
10
|
const pending_1 = require("./pending");
|
|
11
|
+
const relay_state_1 = require("./relay-state");
|
|
11
12
|
const DEFAULT_REPLY_TIMEOUT_MS = 120_000;
|
|
12
13
|
const GREETING_COOLDOWN_MS = 120_000;
|
|
13
14
|
const ECHO_WINDOW_MS = 60_000;
|
|
@@ -395,7 +396,7 @@ async function handleInbound(networkUrl, msg, options) {
|
|
|
395
396
|
created_at: msg.created_at,
|
|
396
397
|
pending: pending ?? undefined,
|
|
397
398
|
});
|
|
398
|
-
// Push to human channel
|
|
399
|
+
// Push to human channel. On failure, leave message in inbox for `relay cron`.
|
|
399
400
|
if (options.notify && !isEcho(msg.from, msg.text)) {
|
|
400
401
|
try {
|
|
401
402
|
await runNotifyCommand(options.notify, msg, pending);
|
|
@@ -408,6 +409,7 @@ async function handleInbound(networkUrl, msg, options) {
|
|
|
408
409
|
if (pending) {
|
|
409
410
|
await (0, pending_1.clearPending)(msg.from);
|
|
410
411
|
}
|
|
412
|
+
await (0, relay_state_1.markRelayed)([msg.id]);
|
|
411
413
|
}
|
|
412
414
|
catch (error) {
|
|
413
415
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -416,9 +418,10 @@ async function handleInbound(networkUrl, msg, options) {
|
|
|
416
418
|
message: `notify: ${message}`,
|
|
417
419
|
in_reply_to: msg.id,
|
|
418
420
|
});
|
|
421
|
+
// Do not ack — cron will retry delivery to the human.
|
|
422
|
+
return;
|
|
419
423
|
}
|
|
420
424
|
}
|
|
421
|
-
// Always ack so backlog never sticks.
|
|
422
425
|
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
423
426
|
// Drop echoes of our own recent outbound (hi↔hi loops).
|
|
424
427
|
if (isEcho(msg.from, msg.text)) {
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const bridge_1 = require("./bridge");
|
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const network_1 = require("./network");
|
|
8
8
|
const pending_1 = require("./pending");
|
|
9
|
+
const relay_cron_1 = require("./relay-cron");
|
|
9
10
|
function printHelp() {
|
|
10
11
|
const message = [
|
|
11
12
|
"marshell - agent messaging bridge",
|
|
@@ -23,6 +24,7 @@ function printHelp() {
|
|
|
23
24
|
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
24
25
|
" marshell history [--with <name>] [--limit <n>] [--json]",
|
|
25
26
|
" marshell listen [--json] [--notify <cmd>] (deliver-only listener)",
|
|
27
|
+
" marshell relay cron [--include-untracked] [--json]",
|
|
26
28
|
" marshell pending list|clear [--peer <name>] [--json]",
|
|
27
29
|
" marshell --help",
|
|
28
30
|
"",
|
|
@@ -365,6 +367,32 @@ async function cmdInbox(args) {
|
|
|
365
367
|
process.stdout.write(`[${msg.from}] ${msg.text}\n`);
|
|
366
368
|
}
|
|
367
369
|
}
|
|
370
|
+
async function cmdRelay(args) {
|
|
371
|
+
const sub = args[0];
|
|
372
|
+
if (sub !== "cron") {
|
|
373
|
+
printError("Usage: marshell relay cron [--include-untracked] [--json]");
|
|
374
|
+
}
|
|
375
|
+
const json = hasFlag(args, "--json");
|
|
376
|
+
const pendingOnly = !hasFlag(args, "--include-untracked");
|
|
377
|
+
const config = await (0, config_1.readConfig)();
|
|
378
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
379
|
+
const result = await (0, relay_cron_1.runRelayCron)({ networkUrl, pendingOnly });
|
|
380
|
+
if (json) {
|
|
381
|
+
printJson({
|
|
382
|
+
...result,
|
|
383
|
+
formatted: (0, relay_cron_1.formatRelayOutput)(result.relayed),
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (result.relayed.length > 0) {
|
|
388
|
+
if (result.pending_count === 0 && !pendingOnly) {
|
|
389
|
+
process.stdout.write(`${result.relayed.length} new inbound message(s):\n\n`);
|
|
390
|
+
}
|
|
391
|
+
process.stdout.write(`${(0, relay_cron_1.formatRelayOutput)(result.relayed)}\n`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
process.stdout.write(result.message ?? "Nothing new to relay.\n");
|
|
395
|
+
}
|
|
368
396
|
async function cmdPending(args) {
|
|
369
397
|
const json = hasFlag(args, "--json");
|
|
370
398
|
const sub = args[0];
|
|
@@ -473,6 +501,10 @@ async function main() {
|
|
|
473
501
|
await cmdListen(args.slice(1));
|
|
474
502
|
return;
|
|
475
503
|
}
|
|
504
|
+
if (args[0] === "relay") {
|
|
505
|
+
await cmdRelay(args.slice(1));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
476
508
|
if (args[0] === "pending") {
|
|
477
509
|
await cmdPending(args.slice(1));
|
|
478
510
|
return;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatRelayOutput = formatRelayOutput;
|
|
4
|
+
exports.runRelayCron = runRelayCron;
|
|
5
|
+
const pending_1 = require("./pending");
|
|
6
|
+
const network_1 = require("./network");
|
|
7
|
+
const relay_state_1 = require("./relay-state");
|
|
8
|
+
function formatItem(item) {
|
|
9
|
+
if (item.kind === "reply" && item.context) {
|
|
10
|
+
return `Reply from ${item.from} (re: ${item.context}):\n${item.text}`;
|
|
11
|
+
}
|
|
12
|
+
return `New from ${item.from}:\n${item.text}`;
|
|
13
|
+
}
|
|
14
|
+
function formatRelayOutput(items) {
|
|
15
|
+
if (items.length === 0)
|
|
16
|
+
return "";
|
|
17
|
+
return items.map(formatItem).join("\n\n---\n\n");
|
|
18
|
+
}
|
|
19
|
+
async function runRelayCron(options) {
|
|
20
|
+
const pending = await (0, pending_1.listPending)();
|
|
21
|
+
const pendingMap = new Map(pending.map((p) => [p.peer.toLowerCase(), p]));
|
|
22
|
+
if (options.pendingOnly && pending.length === 0) {
|
|
23
|
+
return {
|
|
24
|
+
pending_count: 0,
|
|
25
|
+
relayed: [],
|
|
26
|
+
skipped_relayed: 0,
|
|
27
|
+
message: "No pending tracked sends — list is empty.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const inbox = await (0, network_1.fetchInbox)(options.networkUrl, { peek: true });
|
|
31
|
+
if (inbox.kind === "error") {
|
|
32
|
+
throw new Error(inbox.message);
|
|
33
|
+
}
|
|
34
|
+
const unrelayed = await (0, relay_state_1.filterUnrelayed)(inbox.messages);
|
|
35
|
+
const skippedRelayed = inbox.messages.length - unrelayed.length;
|
|
36
|
+
const items = [];
|
|
37
|
+
const toAck = [];
|
|
38
|
+
const peersToClear = new Set();
|
|
39
|
+
for (const msg of unrelayed) {
|
|
40
|
+
const peer = msg.from.toLowerCase();
|
|
41
|
+
const tracked = pendingMap.get(peer);
|
|
42
|
+
if (tracked) {
|
|
43
|
+
items.push(buildItem(msg, tracked, "reply"));
|
|
44
|
+
toAck.push(msg.id);
|
|
45
|
+
peersToClear.add(peer);
|
|
46
|
+
}
|
|
47
|
+
else if (!options.pendingOnly) {
|
|
48
|
+
items.push(buildItem(msg, null, "new"));
|
|
49
|
+
toAck.push(msg.id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (toAck.length > 0) {
|
|
53
|
+
await (0, network_1.ackMessages)(options.networkUrl, toAck);
|
|
54
|
+
await (0, relay_state_1.markRelayed)(toAck);
|
|
55
|
+
}
|
|
56
|
+
for (const peer of peersToClear) {
|
|
57
|
+
await (0, pending_1.clearPending)(peer);
|
|
58
|
+
}
|
|
59
|
+
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}`;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
pending_count: pending.length,
|
|
66
|
+
relayed: items,
|
|
67
|
+
skipped_relayed: skippedRelayed,
|
|
68
|
+
message,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function buildItem(msg, pending, kind) {
|
|
72
|
+
return {
|
|
73
|
+
kind: pending ? "reply" : kind,
|
|
74
|
+
from: msg.from,
|
|
75
|
+
text: msg.text,
|
|
76
|
+
id: msg.id,
|
|
77
|
+
context: pending?.context,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterUnrelayed = filterUnrelayed;
|
|
4
|
+
exports.markRelayed = markRelayed;
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const MAX_IDS = 500;
|
|
9
|
+
function statePath() {
|
|
10
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), ".marshell", "relay-state.json");
|
|
11
|
+
}
|
|
12
|
+
async function readState() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await (0, promises_1.readFile)(statePath(), "utf8");
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const maybeCode = error.code;
|
|
19
|
+
if (maybeCode === "ENOENT") {
|
|
20
|
+
return { relayedIds: [], updatedAt: new Date().toISOString() };
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function writeState(state) {
|
|
26
|
+
await (0, promises_1.mkdir)((0, node_path_1.join)((0, node_os_1.homedir)(), ".marshell"), { recursive: true });
|
|
27
|
+
await (0, promises_1.writeFile)(statePath(), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
28
|
+
}
|
|
29
|
+
async function filterUnrelayed(messages) {
|
|
30
|
+
const state = await readState();
|
|
31
|
+
const seen = new Set(state.relayedIds);
|
|
32
|
+
return messages.filter((m) => !seen.has(m.id));
|
|
33
|
+
}
|
|
34
|
+
async function markRelayed(ids) {
|
|
35
|
+
if (ids.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const state = await readState();
|
|
38
|
+
const merged = [...new Set([...state.relayedIds, ...ids])];
|
|
39
|
+
const trimmed = merged.length > MAX_IDS ? merged.slice(merged.length - MAX_IDS) : merged;
|
|
40
|
+
await writeState({
|
|
41
|
+
relayedIds: trimmed,
|
|
42
|
+
updatedAt: new Date().toISOString(),
|
|
43
|
+
});
|
|
44
|
+
}
|
package/package.json
CHANGED
package/scripts/relay.mjs
CHANGED
|
@@ -37,7 +37,7 @@ async function main() {
|
|
|
37
37
|
if (context) {
|
|
38
38
|
message = `Reply from ${from} (re: ${context}):\n${text}`;
|
|
39
39
|
} else {
|
|
40
|
-
message = `
|
|
40
|
+
message = `New from ${from}:\n${text}`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (webhook) {
|
|
@@ -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) => {
|