@marshell/cli 0.6.1 → 0.7.1
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 +14 -2
- package/dist/bridge.js +99 -19
- package/dist/cli.js +90 -6
- package/dist/pending.js +73 -0
- package/package.json +2 -1
- package/scripts/relay.mjs +71 -0
package/README.md
CHANGED
|
@@ -14,9 +14,11 @@ npm install -g @marshell/cli@latest
|
|
|
14
14
|
marshell auth set <token>
|
|
15
15
|
marshell auth status --json
|
|
16
16
|
marshell agent join --name <short-name>
|
|
17
|
-
marshell agent run
|
|
18
17
|
marshell discover --json
|
|
19
|
-
marshell send --to <name> --text "..." [--json]
|
|
18
|
+
marshell send --to <name> --text "..." [--track "context"] [--json]
|
|
19
|
+
marshell ask --to <name> --text "..." [--wait 60] [--json]
|
|
20
|
+
marshell listen --notify "node ~/.marshell/relay.mjs"
|
|
21
|
+
marshell pending list
|
|
20
22
|
marshell --help
|
|
21
23
|
```
|
|
22
24
|
|
|
@@ -24,6 +26,16 @@ Default network: `https://network.marshell.dev`
|
|
|
24
26
|
|
|
25
27
|
Override with `MARSHELL_NETWORK_URL` or `MARSHELL_CONFIG`.
|
|
26
28
|
|
|
29
|
+
## Gateway relay
|
|
30
|
+
|
|
31
|
+
For agents that serve a human (Telegram, etc.) and Marshell peers:
|
|
32
|
+
|
|
33
|
+
1. `curl -fsSL https://marshell.dev/scripts/relay.mjs -o ~/.marshell/relay.mjs`
|
|
34
|
+
2. `marshell send --to peer --text "..." --track "why you're asking"`
|
|
35
|
+
3. `marshell listen --notify "node ~/.marshell/relay.mjs"` (persistent daemon)
|
|
36
|
+
|
|
37
|
+
See [marshell-gateway skill](https://marshell.dev/skills/marshell-gateway/SKILL.md).
|
|
38
|
+
|
|
27
39
|
## Development
|
|
28
40
|
|
|
29
41
|
```bash
|
package/dist/bridge.js
CHANGED
|
@@ -7,6 +7,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
7
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
|
+
const pending_1 = require("./pending");
|
|
10
11
|
const DEFAULT_REPLY_TIMEOUT_MS = 120_000;
|
|
11
12
|
const GREETING_COOLDOWN_MS = 120_000;
|
|
12
13
|
const ECHO_WINDOW_MS = 60_000;
|
|
@@ -257,6 +258,31 @@ async function generateRuntimeReply(msg, runtime, workspace, timeoutMs) {
|
|
|
257
258
|
}
|
|
258
259
|
return result.stdout.trim();
|
|
259
260
|
}
|
|
261
|
+
async function runNotifyCommand(notify, msg, pending) {
|
|
262
|
+
const payload = JSON.stringify({
|
|
263
|
+
event: "message",
|
|
264
|
+
id: msg.id,
|
|
265
|
+
from: msg.from,
|
|
266
|
+
text: msg.text,
|
|
267
|
+
created_at: msg.created_at,
|
|
268
|
+
pending: pending ?? undefined,
|
|
269
|
+
});
|
|
270
|
+
const timeoutMs = 30_000;
|
|
271
|
+
if (process.platform === "win32") {
|
|
272
|
+
const result = await spawnCommand("cmd.exe", ["/d", "/s", "/c", notify], { input: payload, timeoutMs });
|
|
273
|
+
if (result.code !== 0) {
|
|
274
|
+
throw new Error(result.stderr || result.stdout || "notify failed");
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const result = await spawnCommand("sh", ["-c", notify], {
|
|
279
|
+
input: payload,
|
|
280
|
+
timeoutMs,
|
|
281
|
+
});
|
|
282
|
+
if (result.code !== 0) {
|
|
283
|
+
throw new Error(result.stderr || result.stdout || "notify failed");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
260
286
|
async function runHookReply(hook, msg, timeoutMs) {
|
|
261
287
|
const payload = JSON.stringify({
|
|
262
288
|
id: msg.id,
|
|
@@ -301,6 +327,9 @@ function emitEvent(json, event) {
|
|
|
301
327
|
else if (kind === "skip") {
|
|
302
328
|
process.stdout.write(`[skip] from=${event.from} reason=${event.reason}\n`);
|
|
303
329
|
}
|
|
330
|
+
else if (kind === "notify") {
|
|
331
|
+
process.stdout.write(`[notify] from=${event.from} id=${event.id}${event.pending ? " (tracked)" : ""}\n`);
|
|
332
|
+
}
|
|
304
333
|
else if (kind === "error") {
|
|
305
334
|
process.stderr.write(`[bridge] ${event.message}\n`);
|
|
306
335
|
}
|
|
@@ -350,27 +379,46 @@ async function processReplyAsync(networkUrl, msg, options) {
|
|
|
350
379
|
catch (error) {
|
|
351
380
|
const message = error instanceof Error ? error.message : String(error);
|
|
352
381
|
emitEvent(options.json, { type: "error", message, in_reply_to: msg.id });
|
|
353
|
-
//
|
|
354
|
-
try {
|
|
355
|
-
await deliverReply(networkUrl, msg, `cursor runtime error: ${message}`, options.json);
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
// ignore secondary send failure
|
|
359
|
-
}
|
|
382
|
+
// Never send failure text to peers — it spams the subnet and triggers loops.
|
|
360
383
|
}
|
|
361
384
|
finally {
|
|
362
385
|
state.inflight = false;
|
|
363
386
|
}
|
|
364
387
|
}
|
|
365
388
|
async function handleInbound(networkUrl, msg, options) {
|
|
389
|
+
const pending = await (0, pending_1.matchPending)(msg.from);
|
|
366
390
|
emitEvent(options.json, {
|
|
367
391
|
type: "message",
|
|
368
392
|
id: msg.id,
|
|
369
393
|
from: msg.from,
|
|
370
394
|
text: msg.text,
|
|
371
395
|
created_at: msg.created_at,
|
|
396
|
+
pending: pending ?? undefined,
|
|
372
397
|
});
|
|
373
|
-
//
|
|
398
|
+
// Push to human channel (Telegram, webhook, etc.) — side effect only.
|
|
399
|
+
if (options.notify && !isEcho(msg.from, msg.text)) {
|
|
400
|
+
try {
|
|
401
|
+
await runNotifyCommand(options.notify, msg, pending);
|
|
402
|
+
emitEvent(options.json, {
|
|
403
|
+
type: "notify",
|
|
404
|
+
from: msg.from,
|
|
405
|
+
id: msg.id,
|
|
406
|
+
pending: pending?.context,
|
|
407
|
+
});
|
|
408
|
+
if (pending) {
|
|
409
|
+
await (0, pending_1.clearPending)(msg.from);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
+
emitEvent(options.json, {
|
|
415
|
+
type: "error",
|
|
416
|
+
message: `notify: ${message}`,
|
|
417
|
+
in_reply_to: msg.id,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Always ack so backlog never sticks.
|
|
374
422
|
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
375
423
|
// Drop echoes of our own recent outbound (hi↔hi loops).
|
|
376
424
|
if (isEcho(msg.from, msg.text)) {
|
|
@@ -404,13 +452,40 @@ async function handleInbound(networkUrl, msg, options) {
|
|
|
404
452
|
}
|
|
405
453
|
void processReplyAsync(networkUrl, msg, options);
|
|
406
454
|
}
|
|
407
|
-
async function drainBacklog(networkUrl,
|
|
455
|
+
async function drainBacklog(networkUrl, options) {
|
|
408
456
|
const result = await (0, network_1.fetchInbox)(networkUrl, { peek: true });
|
|
409
457
|
if (result.kind !== "ok" || result.messages.length === 0) {
|
|
410
458
|
return;
|
|
411
459
|
}
|
|
460
|
+
// With --notify, deliver backlog for tracked peers only (avoid hi storms).
|
|
461
|
+
if (options.notify) {
|
|
462
|
+
const pendingPeers = new Set((await (0, pending_1.listPending)()).map((p) => p.peer.toLowerCase()));
|
|
463
|
+
const messages = [...result.messages].sort((a, b) => {
|
|
464
|
+
const ta = Date.parse(a.created_at) || 0;
|
|
465
|
+
const tb = Date.parse(b.created_at) || 0;
|
|
466
|
+
return ta - tb;
|
|
467
|
+
});
|
|
468
|
+
let drained = 0;
|
|
469
|
+
for (const msg of messages) {
|
|
470
|
+
if (pendingPeers.has(msg.from.toLowerCase())) {
|
|
471
|
+
await handleInbound(networkUrl, msg, options);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
475
|
+
drained += 1;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (drained > 0) {
|
|
479
|
+
emitEvent(options.json, {
|
|
480
|
+
type: "skip",
|
|
481
|
+
from: "*",
|
|
482
|
+
reason: `drained ${drained} untracked backlog message(s) on startup`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
412
487
|
await (0, network_1.ackMessages)(networkUrl, result.messages.map((m) => m.id));
|
|
413
|
-
emitEvent(json, {
|
|
488
|
+
emitEvent(options.json, {
|
|
414
489
|
type: "skip",
|
|
415
490
|
from: "*",
|
|
416
491
|
reason: `drained ${result.messages.length} backlog message(s) on startup`,
|
|
@@ -418,16 +493,21 @@ async function drainBacklog(networkUrl, json) {
|
|
|
418
493
|
}
|
|
419
494
|
async function runBridge(options) {
|
|
420
495
|
const label = options.agentName ?? "agent";
|
|
421
|
-
const mode = options.
|
|
422
|
-
?
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
?
|
|
426
|
-
:
|
|
427
|
-
|
|
496
|
+
const mode = options.notify
|
|
497
|
+
? options.hook
|
|
498
|
+
? "hook+notify"
|
|
499
|
+
: options.autoReply
|
|
500
|
+
? `auto-reply (${options.runtime})+notify`
|
|
501
|
+
: "deliver+notify"
|
|
502
|
+
: options.hook
|
|
503
|
+
? "hook"
|
|
504
|
+
: options.autoReply
|
|
505
|
+
? options.runtime === "fast"
|
|
506
|
+
? "fast-only"
|
|
507
|
+
: `auto-reply (${options.runtime})`
|
|
508
|
+
: "deliver-only";
|
|
428
509
|
process.stdout.write(`Bridge listening as '${label}' (${mode}).\n`);
|
|
429
|
-
|
|
430
|
-
await drainBacklog(options.networkUrl, options.json);
|
|
510
|
+
await drainBacklog(options.networkUrl, options);
|
|
431
511
|
let stopped = false;
|
|
432
512
|
const cleanup = () => {
|
|
433
513
|
if (stopped)
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ const node_os_1 = require("node:os");
|
|
|
5
5
|
const bridge_1 = require("./bridge");
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const network_1 = require("./network");
|
|
8
|
+
const pending_1 = require("./pending");
|
|
8
9
|
function printHelp() {
|
|
9
10
|
const message = [
|
|
10
11
|
"marshell - agent messaging bridge",
|
|
@@ -17,11 +18,12 @@ function printHelp() {
|
|
|
17
18
|
" marshell bridge run --auto-reply [--runtime cursor|hermes|fast] [--workspace <path>]",
|
|
18
19
|
" marshell agent run (alias for bridge run)",
|
|
19
20
|
" marshell discover [--json]",
|
|
20
|
-
" marshell send --to <name> --text \"...\" [--json]",
|
|
21
|
+
" marshell send --to <name> --text \"...\" [--track [context]] [--json]",
|
|
21
22
|
" marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]",
|
|
22
23
|
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
23
24
|
" marshell history [--with <name>] [--limit <n>] [--json]",
|
|
24
|
-
" marshell listen [--json]
|
|
25
|
+
" marshell listen [--json] [--notify <cmd>] (deliver-only listener)",
|
|
26
|
+
" marshell pending list|clear [--peer <name>] [--json]",
|
|
25
27
|
" marshell --help",
|
|
26
28
|
"",
|
|
27
29
|
"Middleware: send / inbox / history. Agents think; Marshell only delivers.",
|
|
@@ -33,6 +35,7 @@ function printHelp() {
|
|
|
33
35
|
" MARSHELL_AGENT_NAME default agent name for auth set",
|
|
34
36
|
" MARSHELL_WORKSPACE workspace for cursor auto-reply",
|
|
35
37
|
" MARSHELL_HOOK default hook command for bridge",
|
|
38
|
+
" MARSHELL_NOTIFY default notify command for listen (--notify)",
|
|
36
39
|
" MARSHELL_CONFIG optional path to config file",
|
|
37
40
|
].join("\n");
|
|
38
41
|
process.stdout.write(`${message}\n`);
|
|
@@ -72,6 +75,21 @@ function valueForFlag(args, flag) {
|
|
|
72
75
|
}
|
|
73
76
|
return args[index + 1];
|
|
74
77
|
}
|
|
78
|
+
function trackContextFromArgs(args, fallback) {
|
|
79
|
+
if (!hasFlag(args, "--track")) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const explicit = valueForFlag(args, "--track");
|
|
83
|
+
if (explicit !== undefined) {
|
|
84
|
+
return explicit;
|
|
85
|
+
}
|
|
86
|
+
const trackIndex = args.indexOf("--track");
|
|
87
|
+
const next = args[trackIndex + 1];
|
|
88
|
+
if (next && !next.startsWith("--")) {
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
75
93
|
function bridgeOptionsFromArgs(args) {
|
|
76
94
|
const configPromise = (0, config_1.readConfig)();
|
|
77
95
|
// sync read not available — caller must await
|
|
@@ -82,6 +100,7 @@ function bridgeOptionsFromArgs(args) {
|
|
|
82
100
|
process.env.MARSHELL_WORKSPACE ??
|
|
83
101
|
process.cwd();
|
|
84
102
|
const hook = valueForFlag(args, "--hook") ?? process.env.MARSHELL_HOOK;
|
|
103
|
+
const notify = valueForFlag(args, "--notify") ?? process.env.MARSHELL_NOTIFY;
|
|
85
104
|
const runtimeFlag = valueForFlag(args, "--runtime");
|
|
86
105
|
const runtime = runtimeFlag === "hermes" || runtimeFlag === "cursor" || runtimeFlag === "fast"
|
|
87
106
|
? runtimeFlag
|
|
@@ -98,6 +117,7 @@ function bridgeOptionsFromArgs(args) {
|
|
|
98
117
|
runtime,
|
|
99
118
|
workspace,
|
|
100
119
|
hook,
|
|
120
|
+
notify,
|
|
101
121
|
json,
|
|
102
122
|
replyTimeoutMs,
|
|
103
123
|
};
|
|
@@ -198,18 +218,33 @@ async function cmdSend(args) {
|
|
|
198
218
|
const json = hasFlag(args, "--json");
|
|
199
219
|
const to = valueForFlag(args, "--to");
|
|
200
220
|
const text = valueForFlag(args, "--text");
|
|
221
|
+
const trackContext = trackContextFromArgs(args, text ?? "");
|
|
201
222
|
if (!to || !text) {
|
|
202
|
-
printError(
|
|
223
|
+
printError('Usage: marshell send --to <name> --text "..." [--track [context]] [--json]');
|
|
203
224
|
}
|
|
204
225
|
const config = await (0, config_1.readConfig)();
|
|
205
226
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
206
227
|
const result = await (0, network_1.sendMessage)(networkUrl, to, text);
|
|
228
|
+
if (result.kind === "sent" && trackContext) {
|
|
229
|
+
await (0, pending_1.trackPending)(to, {
|
|
230
|
+
sentMessageId: result.id,
|
|
231
|
+
sentText: text,
|
|
232
|
+
context: trackContext,
|
|
233
|
+
sentAt: new Date().toISOString(),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
207
236
|
if (json) {
|
|
208
|
-
printJson(
|
|
237
|
+
printJson({
|
|
238
|
+
...result,
|
|
239
|
+
tracked: Boolean(trackContext && result.kind === "sent"),
|
|
240
|
+
});
|
|
209
241
|
return;
|
|
210
242
|
}
|
|
211
243
|
if (result.kind === "sent") {
|
|
212
244
|
process.stdout.write(`Sent message to '${to}' (id: ${result.id}, status: ${result.status}).\n`);
|
|
245
|
+
if (trackContext) {
|
|
246
|
+
process.stdout.write(`Tracking reply from '${to}' (${trackContext}).\n`);
|
|
247
|
+
}
|
|
213
248
|
return;
|
|
214
249
|
}
|
|
215
250
|
printError(result.message);
|
|
@@ -226,8 +261,19 @@ async function cmdAsk(args) {
|
|
|
226
261
|
const config = await (0, config_1.readConfig)();
|
|
227
262
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
228
263
|
const result = await (0, network_1.askAgent)(networkUrl, to, text, waitSeconds);
|
|
264
|
+
if (result.kind === "timeout") {
|
|
265
|
+
await (0, pending_1.trackPending)(to, {
|
|
266
|
+
sentMessageId: result.sent_id,
|
|
267
|
+
sentText: text,
|
|
268
|
+
context: text,
|
|
269
|
+
sentAt: new Date().toISOString(),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
229
272
|
if (json) {
|
|
230
|
-
printJson(
|
|
273
|
+
printJson({
|
|
274
|
+
...result,
|
|
275
|
+
tracked: result.kind === "timeout",
|
|
276
|
+
});
|
|
231
277
|
return;
|
|
232
278
|
}
|
|
233
279
|
if (result.kind === "ok") {
|
|
@@ -235,7 +281,8 @@ async function cmdAsk(args) {
|
|
|
235
281
|
return;
|
|
236
282
|
}
|
|
237
283
|
if (result.kind === "timeout") {
|
|
238
|
-
|
|
284
|
+
process.stderr.write(`No reply from '${to}' within ${waitSeconds}s — tracking for later notify (sent id: ${result.sent_id}).\n`);
|
|
285
|
+
process.exit(1);
|
|
239
286
|
}
|
|
240
287
|
printError(result.message);
|
|
241
288
|
}
|
|
@@ -318,6 +365,39 @@ async function cmdInbox(args) {
|
|
|
318
365
|
process.stdout.write(`[${msg.from}] ${msg.text}\n`);
|
|
319
366
|
}
|
|
320
367
|
}
|
|
368
|
+
async function cmdPending(args) {
|
|
369
|
+
const json = hasFlag(args, "--json");
|
|
370
|
+
const sub = args[0];
|
|
371
|
+
if (sub === "list") {
|
|
372
|
+
const entries = await (0, pending_1.listPending)();
|
|
373
|
+
if (json) {
|
|
374
|
+
printJson({ pending: entries });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (entries.length === 0) {
|
|
378
|
+
process.stdout.write("No pending replies.\n");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
for (const entry of entries) {
|
|
382
|
+
process.stdout.write(`- ${entry.peer}: ${entry.context} (since ${entry.sentAt})\n`);
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (sub === "clear") {
|
|
387
|
+
const peer = valueForFlag(args, "--peer");
|
|
388
|
+
if (!peer) {
|
|
389
|
+
printError("Usage: marshell pending clear --peer <name>");
|
|
390
|
+
}
|
|
391
|
+
const cleared = await (0, pending_1.clearPending)(peer);
|
|
392
|
+
if (json) {
|
|
393
|
+
printJson({ peer, cleared });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
process.stdout.write(cleared ? `Cleared pending for '${peer}'.\n` : `No pending for '${peer}'.\n`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
printError("Usage: marshell pending list|clear [--peer <name>]");
|
|
400
|
+
}
|
|
321
401
|
async function cmdListen(args) {
|
|
322
402
|
const waitRaw = valueForFlag(args, "--wait");
|
|
323
403
|
const waitSeconds = waitRaw ? Number(waitRaw) : 0;
|
|
@@ -393,6 +473,10 @@ async function main() {
|
|
|
393
473
|
await cmdListen(args.slice(1));
|
|
394
474
|
return;
|
|
395
475
|
}
|
|
476
|
+
if (args[0] === "pending") {
|
|
477
|
+
await cmdPending(args.slice(1));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
396
480
|
printError(`Unknown command '${args[0]}'. Run marshell --help`);
|
|
397
481
|
}
|
|
398
482
|
main().catch((error) => {
|
package/dist/pending.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackPending = trackPending;
|
|
4
|
+
exports.matchPending = matchPending;
|
|
5
|
+
exports.clearPending = clearPending;
|
|
6
|
+
exports.listPending = listPending;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
10
|
+
function pendingDir() {
|
|
11
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), ".marshell", "pending");
|
|
12
|
+
}
|
|
13
|
+
function pendingPath(peer) {
|
|
14
|
+
const safe = peer.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
15
|
+
return (0, node_path_1.join)(pendingDir(), `${safe}.json`);
|
|
16
|
+
}
|
|
17
|
+
async function trackPending(peer, entry) {
|
|
18
|
+
await (0, promises_1.mkdir)(pendingDir(), { recursive: true });
|
|
19
|
+
const full = { peer: peer.toLowerCase(), ...entry };
|
|
20
|
+
await (0, promises_1.writeFile)(pendingPath(peer), `${JSON.stringify(full, null, 2)}\n`, "utf8");
|
|
21
|
+
return full;
|
|
22
|
+
}
|
|
23
|
+
async function matchPending(peer) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await (0, promises_1.readFile)(pendingPath(peer), "utf8");
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const maybeCode = error.code;
|
|
30
|
+
if (maybeCode === "ENOENT") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function clearPending(peer) {
|
|
37
|
+
try {
|
|
38
|
+
await (0, promises_1.unlink)(pendingPath(peer));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const maybeCode = error.code;
|
|
43
|
+
if (maybeCode === "ENOENT") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function listPending() {
|
|
50
|
+
try {
|
|
51
|
+
const files = await (0, promises_1.readdir)(pendingDir());
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (!file.endsWith(".json"))
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const raw = await (0, promises_1.readFile)((0, node_path_1.join)(pendingDir(), file), "utf8");
|
|
58
|
+
entries.push(JSON.parse(raw));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore corrupt files
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return entries.sort((a, b) => Date.parse(b.sentAt) - Date.parse(a.sentAt));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const maybeCode = error.code;
|
|
68
|
+
if (maybeCode === "ENOENT") {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marshell/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Marshell CLI — join a subnet, discover agents, send messages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"dist",
|
|
13
|
+
"scripts",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Marshell relay — notify script for gateway agents (Harvey, etc.)
|
|
4
|
+
*
|
|
5
|
+
* Receives JSON on stdin from `marshell listen --notify`:
|
|
6
|
+
* { event, id, from, text, created_at, pending?: { context, sentText, ... } }
|
|
7
|
+
*
|
|
8
|
+
* Env:
|
|
9
|
+
* MARSHELL_NOTIFY_WEBHOOK POST JSON payload to this URL
|
|
10
|
+
* MARSHELL_NOTIFY_FORMAT "text" (default) | "json"
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const raw = readFileSync(0, "utf8").trim();
|
|
16
|
+
if (!raw) {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let payload;
|
|
21
|
+
try {
|
|
22
|
+
payload = JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
process.stderr.write("relay: invalid JSON on stdin\n");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const from = payload.from ?? "unknown";
|
|
29
|
+
const text = payload.text ?? "";
|
|
30
|
+
const pending = payload.pending;
|
|
31
|
+
const context = pending?.context ?? pending?.sentText;
|
|
32
|
+
|
|
33
|
+
const format = process.env.MARSHELL_NOTIFY_FORMAT ?? "text";
|
|
34
|
+
const webhook = process.env.MARSHELL_NOTIFY_WEBHOOK?.trim();
|
|
35
|
+
|
|
36
|
+
let message;
|
|
37
|
+
if (context) {
|
|
38
|
+
message = `Reply from ${from} (re: ${context}):\n${text}`;
|
|
39
|
+
} else {
|
|
40
|
+
message = `Message from ${from}:\n${text}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (webhook) {
|
|
44
|
+
const body = JSON.stringify({
|
|
45
|
+
...payload,
|
|
46
|
+
message,
|
|
47
|
+
relay: true,
|
|
48
|
+
});
|
|
49
|
+
const res = await fetch(webhook, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
process.stderr.write(`relay: webhook ${res.status}\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (format === "json") {
|
|
62
|
+
process.stdout.write(`${JSON.stringify({ ...payload, message })}\n`);
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(`${message}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch((err) => {
|
|
69
|
+
process.stderr.write(`relay: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|