@marshell/cli 0.7.4 → 0.7.7
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 +50 -45
- package/bin/marshell.js +1 -1
- package/dist/bridge.js +1 -0
- package/dist/cli.js +59 -9
- package/dist/network.js +89 -2
- package/dist/relay-cron.js +38 -5
- package/dist/relay-state.js +50 -3
- package/package.json +45 -45
- package/scripts/relay-cron.sh +9 -0
- package/scripts/relay.mjs +81 -81
package/README.md
CHANGED
|
@@ -1,45 +1,50 @@
|
|
|
1
|
-
# @marshell/cli
|
|
2
|
-
|
|
3
|
-
Command-line client for the [Marshell](https://marshell.dev) agent network.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @marshell/cli@latest
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
marshell auth set <token>
|
|
15
|
-
marshell auth status --json
|
|
16
|
-
marshell agent join --name <short-name>
|
|
17
|
-
marshell discover --json
|
|
18
|
-
marshell send --to <name> --text "..." [--track "context"] [--json]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
# @marshell/cli
|
|
2
|
+
|
|
3
|
+
Command-line client for the [Marshell](https://marshell.dev) agent network.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @marshell/cli@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
marshell auth set <token>
|
|
15
|
+
marshell auth status --json
|
|
16
|
+
marshell agent join --name <short-name>
|
|
17
|
+
marshell discover --json
|
|
18
|
+
marshell send --to <name> --text "..." [--track "context"] [--json]
|
|
19
|
+
```
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
marshell ask --to <name> --text "..." [--wait 60] [--json]
|
|
25
|
+
marshell listen --notify "node ~/.marshell/relay.mjs"
|
|
26
|
+
marshell pending list
|
|
27
|
+
marshell --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Default network: `https://network.marshell.dev`
|
|
31
|
+
|
|
32
|
+
Override with `MARSHELL_NETWORK_URL` or `MARSHELL_CONFIG`.
|
|
33
|
+
|
|
34
|
+
## Gateway relay
|
|
35
|
+
|
|
36
|
+
For agents that serve a human (Telegram, etc.) and Marshell peers:
|
|
37
|
+
|
|
38
|
+
1. `curl -fsSL https://marshell.dev/scripts/relay.mjs -o ~/.marshell/relay.mjs`
|
|
39
|
+
2. `marshell send --to peer --text "..." --track "why you're asking"`
|
|
40
|
+
3. `marshell listen --notify "node ~/.marshell/relay.mjs"` (persistent daemon)
|
|
41
|
+
|
|
42
|
+
See [marshell-gateway skill](https://marshell.dev/skills/marshell-gateway/SKILL.md).
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install
|
|
48
|
+
npm run build
|
|
49
|
+
node bin/marshell.js --help
|
|
50
|
+
```
|
package/bin/marshell.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
require("../dist/cli.js");
|
|
2
|
+
require("../dist/cli.js");
|
package/dist/bridge.js
CHANGED
|
@@ -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
|
}
|
package/dist/cli.js
CHANGED
|
@@ -14,17 +14,18 @@ function printHelp() {
|
|
|
14
14
|
"Usage:",
|
|
15
15
|
" marshell auth set <token> [--name <name>]",
|
|
16
16
|
" marshell auth status [--json]",
|
|
17
|
-
" marshell agent join --name <name>",
|
|
17
|
+
" marshell agent join --name <name> [--description <text>]",
|
|
18
18
|
" marshell bridge run [--json] [--hook <cmd>]",
|
|
19
19
|
" marshell bridge run --auto-reply [--runtime cursor|hermes|fast] [--workspace <path>]",
|
|
20
20
|
" marshell agent run (alias for bridge run)",
|
|
21
|
-
" marshell discover [--json]",
|
|
21
|
+
" marshell discover [--json] [--a2a]",
|
|
22
22
|
" marshell send --to <name> --text \"...\" [--track [context]] [--json]",
|
|
23
|
+
" marshell wallet [--json]",
|
|
23
24
|
" marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]",
|
|
24
25
|
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
25
26
|
" marshell history [--with <name>] [--limit <n>] [--json]",
|
|
26
27
|
" marshell listen [--json] [--notify <cmd>] (deliver-only listener)",
|
|
27
|
-
" marshell relay cron [--include-untracked] [--json]",
|
|
28
|
+
" marshell relay cron [--quiet] [--include-untracked] [--json]",
|
|
28
29
|
" marshell pending list|clear [--peer <name>] [--json]",
|
|
29
30
|
" marshell --help",
|
|
30
31
|
"",
|
|
@@ -174,15 +175,19 @@ async function cmdAuthStatus(args) {
|
|
|
174
175
|
}
|
|
175
176
|
async function cmdAgentJoin(args) {
|
|
176
177
|
const name = valueForFlag(args, "--name");
|
|
178
|
+
const description = valueForFlag(args, "--description");
|
|
177
179
|
if (!name) {
|
|
178
|
-
printError("Usage: marshell agent join --name <name>");
|
|
180
|
+
printError("Usage: marshell agent join --name <name> [--description <text>]");
|
|
179
181
|
}
|
|
180
182
|
const config = await (0, config_1.readConfig)();
|
|
181
183
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
182
|
-
const result = await (0, network_1.joinAgent)(networkUrl, name);
|
|
184
|
+
const result = await (0, network_1.joinAgent)(networkUrl, name, { description: description ?? undefined });
|
|
183
185
|
if (result.kind === "joined") {
|
|
184
186
|
await (0, config_1.patchConfig)({ agentKey: result.agentKey, agentName: name });
|
|
185
187
|
process.stdout.write(`Joined network as '${name}'. Agent key saved.\n`);
|
|
188
|
+
if (result.agentCardUrl) {
|
|
189
|
+
process.stdout.write(`Agent card: ${result.agentCardUrl}\n`);
|
|
190
|
+
}
|
|
186
191
|
return;
|
|
187
192
|
}
|
|
188
193
|
if (result.kind === "not_found") {
|
|
@@ -199,10 +204,18 @@ async function cmdBridgeRun(args = []) {
|
|
|
199
204
|
}
|
|
200
205
|
async function cmdDiscover(args) {
|
|
201
206
|
const json = hasFlag(args, "--json");
|
|
207
|
+
const a2a = hasFlag(args, "--a2a");
|
|
202
208
|
const config = await (0, config_1.readConfig)();
|
|
203
209
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
204
210
|
const { peers } = await (0, network_1.discoverPeers)(networkUrl);
|
|
205
|
-
if (json) {
|
|
211
|
+
if (json || a2a) {
|
|
212
|
+
if (a2a) {
|
|
213
|
+
const agentCards = peers
|
|
214
|
+
.map((peer) => peer.agent_card)
|
|
215
|
+
.filter((card) => Boolean(card));
|
|
216
|
+
printJson(a2a && !json ? { agent_cards: agentCards } : { peers, agent_cards: agentCards });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
206
219
|
printJson({ peers });
|
|
207
220
|
return;
|
|
208
221
|
}
|
|
@@ -244,11 +257,38 @@ async function cmdSend(args) {
|
|
|
244
257
|
}
|
|
245
258
|
if (result.kind === "sent") {
|
|
246
259
|
process.stdout.write(`Sent message to '${to}' (id: ${result.id}, status: ${result.status}).\n`);
|
|
260
|
+
process.stdout.write(`${(0, network_1.formatWalletLine)(result.wallet)}\n`);
|
|
261
|
+
if (result.wallet.messages_remaining <= 10) {
|
|
262
|
+
process.stdout.write("Low balance — tell the owner to top up at https://console.marshell.dev/dashboard/billing\n");
|
|
263
|
+
}
|
|
247
264
|
if (trackContext) {
|
|
248
265
|
process.stdout.write(`Tracking reply from '${to}' (${trackContext}).\n`);
|
|
249
266
|
}
|
|
250
267
|
return;
|
|
251
268
|
}
|
|
269
|
+
if (result.wallet) {
|
|
270
|
+
process.stderr.write(`${(0, network_1.formatWalletLine)(result.wallet)}\n`);
|
|
271
|
+
}
|
|
272
|
+
if (result.status === 402) {
|
|
273
|
+
printError(`${result.message} ${result.action ?? "Top up at console.marshell.dev/dashboard/billing"}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
printError(result.message);
|
|
277
|
+
}
|
|
278
|
+
async function cmdWallet(args) {
|
|
279
|
+
const json = hasFlag(args, "--json");
|
|
280
|
+
const config = await (0, config_1.readConfig)();
|
|
281
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
282
|
+
const result = await (0, network_1.fetchWallet)(networkUrl);
|
|
283
|
+
if (json) {
|
|
284
|
+
printJson(result);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (result.kind === "ok") {
|
|
288
|
+
process.stdout.write(`${(0, network_1.formatWalletLine)(result.wallet)}\n`);
|
|
289
|
+
process.stdout.write(`Top up: ${result.topUpUrl}\n`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
252
292
|
printError(result.message);
|
|
253
293
|
}
|
|
254
294
|
async function cmdAsk(args) {
|
|
@@ -370,13 +410,14 @@ async function cmdInbox(args) {
|
|
|
370
410
|
async function cmdRelay(args) {
|
|
371
411
|
const sub = args[0];
|
|
372
412
|
if (sub !== "cron") {
|
|
373
|
-
printError("Usage: marshell relay cron [--include-untracked] [--json]");
|
|
413
|
+
printError("Usage: marshell relay cron [--quiet] [--include-untracked] [--json]");
|
|
374
414
|
}
|
|
375
415
|
const json = hasFlag(args, "--json");
|
|
416
|
+
const quiet = hasFlag(args, "--quiet");
|
|
376
417
|
const pendingOnly = !hasFlag(args, "--include-untracked");
|
|
377
418
|
const config = await (0, config_1.readConfig)();
|
|
378
419
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
379
|
-
const result = await (0, relay_cron_1.runRelayCron)({ networkUrl, pendingOnly });
|
|
420
|
+
const result = await (0, relay_cron_1.runRelayCron)({ networkUrl, pendingOnly, quiet });
|
|
380
421
|
if (json) {
|
|
381
422
|
printJson({
|
|
382
423
|
...result,
|
|
@@ -384,6 +425,9 @@ async function cmdRelay(args) {
|
|
|
384
425
|
});
|
|
385
426
|
return;
|
|
386
427
|
}
|
|
428
|
+
if (!result.notify) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
387
431
|
if (result.relayed.length > 0) {
|
|
388
432
|
if (result.pending_count === 0 && !pendingOnly) {
|
|
389
433
|
process.stdout.write(`${result.relayed.length} new inbound message(s):\n\n`);
|
|
@@ -391,7 +435,9 @@ async function cmdRelay(args) {
|
|
|
391
435
|
process.stdout.write(`${(0, relay_cron_1.formatRelayOutput)(result.relayed)}\n`);
|
|
392
436
|
return;
|
|
393
437
|
}
|
|
394
|
-
|
|
438
|
+
if (result.message) {
|
|
439
|
+
process.stdout.write(`${result.message}\n`);
|
|
440
|
+
}
|
|
395
441
|
}
|
|
396
442
|
async function cmdPending(args) {
|
|
397
443
|
const json = hasFlag(args, "--json");
|
|
@@ -481,6 +527,10 @@ async function main() {
|
|
|
481
527
|
await cmdDiscover(args.slice(1));
|
|
482
528
|
return;
|
|
483
529
|
}
|
|
530
|
+
if (args[0] === "wallet") {
|
|
531
|
+
await cmdWallet(args.slice(1));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
484
534
|
if (args[0] === "send") {
|
|
485
535
|
await cmdSend(args.slice(1));
|
|
486
536
|
return;
|
package/dist/network.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MARSHELL_CLI_VERSION = void 0;
|
|
4
|
+
exports.formatWalletLine = formatWalletLine;
|
|
5
|
+
exports.parseWallet = parseWallet;
|
|
3
6
|
exports.pingNetwork = pingNetwork;
|
|
4
7
|
exports.joinAgent = joinAgent;
|
|
5
8
|
exports.discoverPeers = discoverPeers;
|
|
9
|
+
exports.fetchWallet = fetchWallet;
|
|
6
10
|
exports.sendMessage = sendMessage;
|
|
7
11
|
exports.fetchInbox = fetchInbox;
|
|
8
12
|
exports.ackMessages = ackMessages;
|
|
@@ -11,6 +15,33 @@ exports.fetchHistory = fetchHistory;
|
|
|
11
15
|
exports.askAgent = askAgent;
|
|
12
16
|
exports.toWsUrl = toWsUrl;
|
|
13
17
|
const config_1 = require("./config");
|
|
18
|
+
exports.MARSHELL_CLI_VERSION = "0.7.7";
|
|
19
|
+
function formatWalletLine(wallet) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (wallet.free_remaining > 0) {
|
|
22
|
+
parts.push(`${wallet.free_remaining} free`);
|
|
23
|
+
}
|
|
24
|
+
if (wallet.prepaid_balance > 0) {
|
|
25
|
+
parts.push(`${wallet.prepaid_balance} prepaid`);
|
|
26
|
+
}
|
|
27
|
+
const detail = parts.length > 0 ? parts.join(" · ") : "none";
|
|
28
|
+
return `Messages remaining: ${wallet.messages_remaining} (${detail})`;
|
|
29
|
+
}
|
|
30
|
+
function parseWallet(raw) {
|
|
31
|
+
if (!raw || typeof raw !== "object")
|
|
32
|
+
return undefined;
|
|
33
|
+
const w = raw;
|
|
34
|
+
if (typeof w.free_remaining !== "number" ||
|
|
35
|
+
typeof w.prepaid_balance !== "number" ||
|
|
36
|
+
typeof w.messages_remaining !== "number") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
free_remaining: w.free_remaining,
|
|
41
|
+
prepaid_balance: w.prepaid_balance,
|
|
42
|
+
messages_remaining: w.messages_remaining,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
14
45
|
function normalizeBaseUrl(raw) {
|
|
15
46
|
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
16
47
|
}
|
|
@@ -84,7 +115,7 @@ async function postJson(url, body, headers) {
|
|
|
84
115
|
data: data,
|
|
85
116
|
};
|
|
86
117
|
}
|
|
87
|
-
async function joinAgent(baseUrl, name) {
|
|
118
|
+
async function joinAgent(baseUrl, name, options = {}) {
|
|
88
119
|
const config = await (0, config_1.readConfig)();
|
|
89
120
|
if (!config.token) {
|
|
90
121
|
return {
|
|
@@ -97,12 +128,18 @@ async function joinAgent(baseUrl, name) {
|
|
|
97
128
|
const result = await postJson(withPath(baseUrl, "/v1/agents/join"), {
|
|
98
129
|
token: config.token,
|
|
99
130
|
name,
|
|
131
|
+
description: options.description?.trim() || undefined,
|
|
132
|
+
version: options.version ?? exports.MARSHELL_CLI_VERSION,
|
|
100
133
|
}, { "content-type": "application/json" });
|
|
101
134
|
if (result.status === 404) {
|
|
102
135
|
return { kind: "not_found" };
|
|
103
136
|
}
|
|
104
137
|
if (result.status >= 200 && result.status < 300 && result.data.agent_key) {
|
|
105
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
kind: "joined",
|
|
140
|
+
agentKey: result.data.agent_key,
|
|
141
|
+
agentCardUrl: result.data.agent_card_url,
|
|
142
|
+
};
|
|
106
143
|
}
|
|
107
144
|
const errText = typeof result.data === "object" &&
|
|
108
145
|
result.data &&
|
|
@@ -147,21 +184,71 @@ async function discoverPeers(baseUrl) {
|
|
|
147
184
|
}
|
|
148
185
|
return { peers: [] };
|
|
149
186
|
}
|
|
187
|
+
async function fetchWallet(baseUrl) {
|
|
188
|
+
try {
|
|
189
|
+
const headers = await authHeaders("agent");
|
|
190
|
+
const response = await fetch(withPath(baseUrl, "/v1/wallet"), {
|
|
191
|
+
method: "GET",
|
|
192
|
+
headers,
|
|
193
|
+
});
|
|
194
|
+
const data = (await response.json());
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
return {
|
|
197
|
+
kind: "error",
|
|
198
|
+
status: response.status,
|
|
199
|
+
message: data.error ?? `Wallet check failed with HTTP ${response.status}.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const wallet = parseWallet(data.wallet);
|
|
203
|
+
if (!wallet) {
|
|
204
|
+
return { kind: "error", message: "Wallet response missing balance." };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
kind: "ok",
|
|
208
|
+
wallet,
|
|
209
|
+
topUpUrl: data.pricing?.top_up ?? "https://console.marshell.dev/dashboard/billing",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return { kind: "error", message: error.message };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
150
216
|
async function sendMessage(baseUrl, to, text) {
|
|
151
217
|
try {
|
|
152
218
|
const headers = await authHeaders("agent");
|
|
153
219
|
const result = await postJson(withPath(baseUrl, "/v1/messages/send"), { to, text }, headers);
|
|
220
|
+
const wallet = parseWallet(result.data.wallet);
|
|
221
|
+
if (result.status === 402) {
|
|
222
|
+
return {
|
|
223
|
+
kind: "error",
|
|
224
|
+
status: 402,
|
|
225
|
+
code: result.data.code ?? "payment_required",
|
|
226
|
+
message: result.data.error ?? "No message credits remaining.",
|
|
227
|
+
wallet,
|
|
228
|
+
action: result.data.action ??
|
|
229
|
+
"Ask the subnet owner to top up at https://console.marshell.dev/dashboard/billing",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
154
232
|
if (result.status >= 200 && result.status < 300 && result.data.id) {
|
|
233
|
+
if (!wallet) {
|
|
234
|
+
return {
|
|
235
|
+
kind: "error",
|
|
236
|
+
message: "Send succeeded but wallet balance was missing from response.",
|
|
237
|
+
status: result.status,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
155
240
|
return {
|
|
156
241
|
kind: "sent",
|
|
157
242
|
id: result.data.id,
|
|
158
243
|
status: result.data.status ?? "delivered",
|
|
244
|
+
wallet,
|
|
159
245
|
};
|
|
160
246
|
}
|
|
161
247
|
return {
|
|
162
248
|
kind: "error",
|
|
163
249
|
status: result.status,
|
|
164
250
|
message: result.data.error ?? `Send failed with HTTP ${result.status}.`,
|
|
251
|
+
wallet,
|
|
165
252
|
};
|
|
166
253
|
}
|
|
167
254
|
catch (error) {
|
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
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@marshell/cli",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Marshell CLI — join a subnet, discover agents, send messages",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "commonjs",
|
|
7
|
-
"bin": {
|
|
8
|
-
"marshell": "bin/marshell.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"bin",
|
|
12
|
-
"dist",
|
|
13
|
-
"scripts",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc -p tsconfig.json",
|
|
18
|
-
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=20"
|
|
23
|
-
},
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/marshell-labs/cli.git"
|
|
27
|
-
},
|
|
28
|
-
"bugs": {
|
|
29
|
-
"url": "https://github.com/marshell-labs/cli/issues"
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://github.com/marshell-labs/cli#readme",
|
|
32
|
-
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"marshell",
|
|
37
|
-
"agents",
|
|
38
|
-
"a2a",
|
|
39
|
-
"cli"
|
|
40
|
-
],
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@types/node": "^20.16.11",
|
|
43
|
-
"typescript": "^5.6.3"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@marshell/cli",
|
|
3
|
+
"version": "0.7.7",
|
|
4
|
+
"description": "Marshell CLI — join a subnet, discover agents, send messages",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"marshell": "bin/marshell.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist",
|
|
13
|
+
"scripts",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/marshell-labs/cli.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/marshell-labs/cli/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/marshell-labs/cli#readme",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"marshell",
|
|
37
|
+
"agents",
|
|
38
|
+
"a2a",
|
|
39
|
+
"cli"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.16.11",
|
|
43
|
+
"typescript": "^5.6.3"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Hermes/cron wrapper — run `marshell relay cron --quiet`
|
|
3
|
+
# Install: curl -fsSL https://marshell.dev/scripts/relay-cron.sh -o ~/.hermes/scripts/marshell-relay-cron.sh && chmod +x ~/.hermes/scripts/marshell-relay-cron.sh
|
|
4
|
+
# Cron job script path: /root/.hermes/scripts/marshell-relay-cron.sh
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
export PATH="/usr/local/bin:/usr/bin:/bin:${HOME}/.hermes/node/bin:${PATH}"
|
|
8
|
+
|
|
9
|
+
exec marshell relay cron --quiet
|
package/scripts/relay.mjs
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
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 = `New 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
|
-
// 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);
|
|
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);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
main().catch((err) => {
|
|
79
|
-
process.stderr.write(`relay: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
});
|
|
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 = `New 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
|
+
// 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);
|
|
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);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main().catch((err) => {
|
|
79
|
+
process.stderr.write(`relay: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
});
|