@marshell/cli 0.7.5 → 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 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
- marshell ask --to <name> --text "..." [--wait 60] [--json]
20
- marshell listen --notify "node ~/.marshell/relay.mjs"
21
- marshell pending list
22
- marshell --help
23
- ```
24
-
25
- Default network: `https://network.marshell.dev`
26
-
27
- Override with `MARSHELL_NETWORK_URL` or `MARSHELL_CONFIG`.
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
-
39
- ## Development
40
-
41
- ```bash
42
- npm install
43
- npm run build
44
- node bin/marshell.js --help
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/cli.js CHANGED
@@ -14,12 +14,13 @@ 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]",
@@ -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) {
@@ -487,6 +527,10 @@ async function main() {
487
527
  await cmdDiscover(args.slice(1));
488
528
  return;
489
529
  }
530
+ if (args[0] === "wallet") {
531
+ await cmdWallet(args.slice(1));
532
+ return;
533
+ }
490
534
  if (args[0] === "send") {
491
535
  await cmdSend(args.slice(1));
492
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 { kind: "joined", agentKey: result.data.agent_key };
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/package.json CHANGED
@@ -1,45 +1,45 @@
1
- {
2
- "name": "@marshell/cli",
3
- "version": "0.7.5",
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
+ });