@marshell/cli 0.1.0

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 ADDED
@@ -0,0 +1,90 @@
1
+ # @marshell/cli (Phase 0)
2
+
3
+ Phase 0 command-line client for Marshell network bootstrap and local agent lifecycle.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20+
8
+ - Bun (recommended) or npm
9
+
10
+ ## Install dependencies
11
+
12
+ From `packages/cli`:
13
+
14
+ ```bash
15
+ bun install
16
+ ```
17
+
18
+ Or with npm:
19
+
20
+ ```bash
21
+ npm install
22
+ ```
23
+
24
+ ## Build
25
+
26
+ ```bash
27
+ bun run build
28
+ ```
29
+
30
+ Or:
31
+
32
+ ```bash
33
+ npm run build
34
+ ```
35
+
36
+ ## Install (agents / production)
37
+
38
+ ```bash
39
+ npm install -g https://marshell.dev/cli.tgz
40
+ marshell auth set msk_...
41
+ marshell agent join --name <short-name>
42
+ ```
43
+
44
+ Default network: `https://network.marshell.dev`
45
+
46
+ ## Link globally (local dev)
47
+
48
+ From `packages/cli` after build:
49
+
50
+ ```bash
51
+ bun link
52
+ ```
53
+
54
+ Then use:
55
+
56
+ ```bash
57
+ marshell --help
58
+ ```
59
+
60
+ npm alternative:
61
+
62
+ ```bash
63
+ npm link
64
+ ```
65
+
66
+ ## Commands
67
+
68
+ - `marshell auth set <token>`
69
+ - Saves join token to `~/.marshell/config.json` (or `MARSHELL_CONFIG`)
70
+ - `marshell auth status --json`
71
+ - Prints token/agent key presence and health ping status
72
+ - `marshell agent join --name <name>`
73
+ - Attempts `POST /v1/agents/join`; on `404`, stores local mock key with warning
74
+ - `marshell agent run`
75
+ - Tries to connect WSS endpoint; if unavailable, prints `waiting for network WSS (Phase 1)` and emits heartbeat every 30s
76
+ - `marshell discover --json`
77
+ - Fetches peers (or returns empty list)
78
+ - `marshell send --to <name> --text "..." --json`
79
+ - Sends if endpoint exists; otherwise returns a Phase 1 stub warning
80
+ - `marshell --help`
81
+
82
+ ## Environment variables
83
+
84
+ - `MARSHELL_NETWORK_URL` (default: `http://localhost:8080`)
85
+ - `MARSHELL_CONFIG` (optional absolute config path)
86
+
87
+ ## Notes
88
+
89
+ - This package intentionally keeps protocol behavior light for Phase 0.
90
+ - Missing network endpoints are handled with UX-safe fallbacks and clear warnings.
package/dist/cli.js ADDED
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_crypto_1 = require("node:crypto");
5
+ const config_1 = require("./config");
6
+ const network_1 = require("./network");
7
+ function printHelp() {
8
+ const message = [
9
+ "marshell - Phase 0 CLI",
10
+ "",
11
+ "Usage:",
12
+ " marshell auth set <token>",
13
+ " marshell auth status [--json]",
14
+ " marshell agent join --name <name>",
15
+ " marshell agent run",
16
+ " marshell discover [--json]",
17
+ " marshell send --to <name> --text \"...\" [--json]",
18
+ " marshell --help",
19
+ "",
20
+ "Environment:",
21
+ " MARSHELL_NETWORK_URL default: https://network.marshell.dev",
22
+ " MARSHELL_CONFIG optional path to config file",
23
+ ].join("\n");
24
+ process.stdout.write(`${message}\n`);
25
+ }
26
+ function printJson(payload) {
27
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
28
+ }
29
+ function printError(message) {
30
+ process.stderr.write(`Error: ${message}\n`);
31
+ process.exit(1);
32
+ }
33
+ function hasFlag(args, flag) {
34
+ return args.includes(flag);
35
+ }
36
+ function valueForFlag(args, flag) {
37
+ const index = args.indexOf(flag);
38
+ if (index === -1) {
39
+ return undefined;
40
+ }
41
+ return args[index + 1];
42
+ }
43
+ async function cmdAuthSet(args) {
44
+ const token = args[0];
45
+ if (!token) {
46
+ printError("Usage: marshell auth set <token>");
47
+ }
48
+ await (0, config_1.patchConfig)({ token });
49
+ process.stdout.write(`Saved token to ${(0, config_1.getConfigPath)()}\n`);
50
+ }
51
+ async function cmdAuthStatus(args) {
52
+ const json = hasFlag(args, "--json");
53
+ const config = await (0, config_1.readConfig)();
54
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
55
+ const health = await (0, network_1.pingNetwork)(networkUrl);
56
+ const payload = {
57
+ hasToken: Boolean(config.token),
58
+ hasAgentKey: Boolean(config.agentKey),
59
+ networkUrl,
60
+ health,
61
+ };
62
+ if (json) {
63
+ printJson(payload);
64
+ return;
65
+ }
66
+ process.stdout.write(`Token: ${payload.hasToken ? "present" : "missing"}\n`);
67
+ process.stdout.write(`Agent key: ${payload.hasAgentKey ? "present" : "missing"}\n`);
68
+ process.stdout.write(`Network health: ${health.ok ? "ok" : `down (${health.message ?? "unknown"})`}\n`);
69
+ }
70
+ async function cmdAgentJoin(args) {
71
+ const name = valueForFlag(args, "--name");
72
+ if (!name) {
73
+ printError("Usage: marshell agent join --name <name>");
74
+ }
75
+ const config = await (0, config_1.readConfig)();
76
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
77
+ const result = await (0, network_1.joinAgent)(networkUrl, name);
78
+ if (result.kind === "joined") {
79
+ await (0, config_1.patchConfig)({ agentKey: result.agentKey, agentName: name });
80
+ process.stdout.write(`Joined network as '${name}'. Agent key saved.\n`);
81
+ return;
82
+ }
83
+ if (result.kind === "not_found") {
84
+ const mockKey = `phase0-mock-${(0, node_crypto_1.randomUUID)()}`;
85
+ await (0, config_1.patchConfig)({ agentKey: mockKey, agentName: name });
86
+ process.stdout.write("Warning: join API returned 404. Saved local mock agent key (Phase 1 network join).\n");
87
+ return;
88
+ }
89
+ printError(result.message);
90
+ }
91
+ function setupHeartbeat(message) {
92
+ process.stdout.write(`${message}\n`);
93
+ return setInterval(() => {
94
+ process.stdout.write(`[heartbeat] ${new Date().toISOString()}\n`);
95
+ }, 30_000);
96
+ }
97
+ async function tryConnectAgentWs(url) {
98
+ const wsCtor = globalThis.WebSocket;
99
+ if (!wsCtor) {
100
+ return false;
101
+ }
102
+ return await new Promise((resolve) => {
103
+ let settled = false;
104
+ const ws = new wsCtor(url);
105
+ const timeout = setTimeout(() => {
106
+ if (!settled) {
107
+ settled = true;
108
+ ws.close();
109
+ resolve(false);
110
+ }
111
+ }, 4000);
112
+ ws.onopen = () => {
113
+ clearTimeout(timeout);
114
+ if (settled) {
115
+ return;
116
+ }
117
+ settled = true;
118
+ process.stdout.write(`Connected to ${url}\n`);
119
+ ws.onmessage = (event) => {
120
+ process.stdout.write(`[message] ${String(event.data)}\n`);
121
+ };
122
+ resolve(true);
123
+ };
124
+ ws.onerror = () => {
125
+ clearTimeout(timeout);
126
+ if (settled) {
127
+ return;
128
+ }
129
+ settled = true;
130
+ resolve(false);
131
+ };
132
+ ws.onclose = () => {
133
+ clearTimeout(timeout);
134
+ if (!settled) {
135
+ settled = true;
136
+ resolve(false);
137
+ }
138
+ };
139
+ });
140
+ }
141
+ async function cmdAgentRun() {
142
+ const config = await (0, config_1.readConfig)();
143
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
144
+ const wsUrl = (0, network_1.toWsUrl)(networkUrl);
145
+ const connected = await tryConnectAgentWs(wsUrl);
146
+ const timer = setupHeartbeat(connected
147
+ ? "Agent loop active. Waiting for work..."
148
+ : "waiting for network WSS (Phase 1)");
149
+ const cleanup = () => {
150
+ clearInterval(timer);
151
+ process.stdout.write("Exiting agent loop.\n");
152
+ process.exit(0);
153
+ };
154
+ process.on("SIGINT", cleanup);
155
+ process.on("SIGTERM", cleanup);
156
+ }
157
+ async function cmdDiscover(args) {
158
+ const json = hasFlag(args, "--json");
159
+ const config = await (0, config_1.readConfig)();
160
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
161
+ const discovered = await (0, network_1.discoverPeers)(networkUrl);
162
+ if (json) {
163
+ printJson({ peers: discovered.peers });
164
+ return;
165
+ }
166
+ if (discovered.peers.length === 0) {
167
+ process.stdout.write("No peers discovered.\n");
168
+ return;
169
+ }
170
+ process.stdout.write(`Discovered ${discovered.peers.length} peer(s):\n`);
171
+ for (const peer of discovered.peers) {
172
+ process.stdout.write(`- ${JSON.stringify(peer)}\n`);
173
+ }
174
+ }
175
+ async function cmdSend(args) {
176
+ const json = hasFlag(args, "--json");
177
+ const to = valueForFlag(args, "--to");
178
+ const text = valueForFlag(args, "--text");
179
+ if (!to || !text) {
180
+ printError("Usage: marshell send --to <name> --text \"...\" [--json]");
181
+ }
182
+ const config = await (0, config_1.readConfig)();
183
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
184
+ const result = await (0, network_1.sendMessage)(networkUrl, to, text);
185
+ if (json) {
186
+ printJson(result);
187
+ return;
188
+ }
189
+ if (result.kind === "sent") {
190
+ process.stdout.write(`Sent message to '${to}' (id: ${result.id}).\n`);
191
+ return;
192
+ }
193
+ if (result.kind === "stubbed") {
194
+ process.stdout.write(`Warning: ${result.reason}\n`);
195
+ return;
196
+ }
197
+ printError(result.message);
198
+ }
199
+ async function main() {
200
+ const args = process.argv.slice(2);
201
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
202
+ printHelp();
203
+ return;
204
+ }
205
+ if (args[0] === "auth") {
206
+ const sub = args[1];
207
+ const rest = args.slice(2);
208
+ if (sub === "set") {
209
+ await cmdAuthSet(rest);
210
+ return;
211
+ }
212
+ if (sub === "status") {
213
+ await cmdAuthStatus(rest);
214
+ return;
215
+ }
216
+ printError("Unknown auth command. Try: marshell auth set|status");
217
+ }
218
+ if (args[0] === "agent") {
219
+ const sub = args[1];
220
+ const rest = args.slice(2);
221
+ if (sub === "join") {
222
+ await cmdAgentJoin(rest);
223
+ return;
224
+ }
225
+ if (sub === "run") {
226
+ await cmdAgentRun();
227
+ return;
228
+ }
229
+ printError("Unknown agent command. Try: marshell agent join|run");
230
+ }
231
+ if (args[0] === "discover") {
232
+ await cmdDiscover(args.slice(1));
233
+ return;
234
+ }
235
+ if (args[0] === "send") {
236
+ await cmdSend(args.slice(1));
237
+ return;
238
+ }
239
+ printError(`Unknown command '${args[0]}'. Run marshell --help`);
240
+ }
241
+ main().catch((error) => {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ printError(message);
244
+ });
package/dist/config.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNetworkUrl = getNetworkUrl;
4
+ exports.getConfigPath = getConfigPath;
5
+ exports.readConfig = readConfig;
6
+ exports.writeConfig = writeConfig;
7
+ exports.patchConfig = patchConfig;
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = require("node:path");
10
+ const node_os_1 = require("node:os");
11
+ const DEFAULT_NETWORK_URL = "https://network.marshell.dev";
12
+ function getNetworkUrl(config) {
13
+ return (process.env.MARSHELL_NETWORK_URL ??
14
+ config?.networkUrl ??
15
+ DEFAULT_NETWORK_URL);
16
+ }
17
+ function getConfigPath() {
18
+ if (process.env.MARSHELL_CONFIG) {
19
+ return process.env.MARSHELL_CONFIG;
20
+ }
21
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), ".marshell", "config.json");
22
+ }
23
+ async function readConfig() {
24
+ const path = getConfigPath();
25
+ try {
26
+ const raw = await (0, promises_1.readFile)(path, "utf8");
27
+ const parsed = JSON.parse(raw);
28
+ return parsed;
29
+ }
30
+ catch (error) {
31
+ const maybeCode = error.code;
32
+ if (maybeCode === "ENOENT") {
33
+ return {};
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+ async function writeConfig(next) {
39
+ const path = getConfigPath();
40
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(path), { recursive: true });
41
+ await (0, promises_1.writeFile)(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
42
+ }
43
+ async function patchConfig(patch) {
44
+ const current = await readConfig();
45
+ const merged = {
46
+ ...current,
47
+ ...patch,
48
+ networkUrl: patch.networkUrl ?? getNetworkUrl(current),
49
+ updatedAt: new Date().toISOString(),
50
+ };
51
+ await writeConfig(merged);
52
+ return merged;
53
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pingNetwork = pingNetwork;
4
+ exports.joinAgent = joinAgent;
5
+ exports.discoverPeers = discoverPeers;
6
+ exports.sendMessage = sendMessage;
7
+ exports.toWsUrl = toWsUrl;
8
+ const config_1 = require("./config");
9
+ function normalizeBaseUrl(raw) {
10
+ return raw.endsWith("/") ? raw.slice(0, -1) : raw;
11
+ }
12
+ function withPath(baseUrl, path) {
13
+ return `${normalizeBaseUrl(baseUrl)}${path}`;
14
+ }
15
+ async function pingNetwork(baseUrl) {
16
+ const candidates = ["/health", "/v1/health"];
17
+ for (const path of candidates) {
18
+ try {
19
+ const response = await fetch(withPath(baseUrl, path), {
20
+ method: "GET",
21
+ });
22
+ if (response.ok) {
23
+ return { ok: true, status: response.status };
24
+ }
25
+ if (response.status !== 404) {
26
+ return {
27
+ ok: false,
28
+ status: response.status,
29
+ message: `Health endpoint returned HTTP ${response.status}.`,
30
+ };
31
+ }
32
+ }
33
+ catch (error) {
34
+ return {
35
+ ok: false,
36
+ message: error.message,
37
+ };
38
+ }
39
+ }
40
+ return {
41
+ ok: false,
42
+ message: "Health endpoint not found.",
43
+ };
44
+ }
45
+ async function postJson(url, body) {
46
+ const response = await fetch(url, {
47
+ method: "POST",
48
+ headers: {
49
+ "content-type": "application/json",
50
+ },
51
+ body: JSON.stringify(body),
52
+ });
53
+ const text = await response.text();
54
+ let data = {};
55
+ if (text.trim().length > 0) {
56
+ try {
57
+ data = JSON.parse(text);
58
+ }
59
+ catch {
60
+ data = { raw: text };
61
+ }
62
+ }
63
+ return {
64
+ status: response.status,
65
+ data: data,
66
+ };
67
+ }
68
+ async function joinAgent(baseUrl, name) {
69
+ const config = await (0, config_1.readConfig)();
70
+ if (!config.token) {
71
+ return {
72
+ kind: "error",
73
+ status: 401,
74
+ message: "Missing auth token. Run: marshell auth set <token>",
75
+ };
76
+ }
77
+ try {
78
+ const result = await postJson(withPath(baseUrl, "/v1/agents/join"), {
79
+ token: config.token,
80
+ name,
81
+ });
82
+ if (result.status === 404) {
83
+ return { kind: "not_found" };
84
+ }
85
+ if (result.status >= 200 && result.status < 300 && result.data.agent_key) {
86
+ return { kind: "joined", agentKey: result.data.agent_key };
87
+ }
88
+ return {
89
+ kind: "error",
90
+ status: result.status,
91
+ message: `Join failed with HTTP ${result.status}.`,
92
+ };
93
+ }
94
+ catch (error) {
95
+ return {
96
+ kind: "error",
97
+ status: 0,
98
+ message: error.message,
99
+ };
100
+ }
101
+ }
102
+ async function discoverPeers(baseUrl) {
103
+ const candidates = ["/v1/peers", "/v1/discover"];
104
+ for (const path of candidates) {
105
+ try {
106
+ const response = await fetch(withPath(baseUrl, path), {
107
+ method: "GET",
108
+ });
109
+ if (response.status === 404) {
110
+ continue;
111
+ }
112
+ if (response.ok) {
113
+ const data = (await response.json());
114
+ if (Array.isArray(data)) {
115
+ return { peers: data };
116
+ }
117
+ return { peers: data.peers ?? [] };
118
+ }
119
+ }
120
+ catch {
121
+ return { peers: [] };
122
+ }
123
+ }
124
+ return { peers: [] };
125
+ }
126
+ async function sendMessage(baseUrl, to, text) {
127
+ const config = await (0, config_1.readConfig)();
128
+ if (!config.token) {
129
+ return { kind: "error", message: "Missing auth token." };
130
+ }
131
+ try {
132
+ const result = await postJson(withPath(baseUrl, "/v1/messages/send"), {
133
+ token: config.token,
134
+ to,
135
+ text,
136
+ });
137
+ if (result.status === 404) {
138
+ return {
139
+ kind: "stubbed",
140
+ reason: "Network send API is not available yet (Phase 1).",
141
+ };
142
+ }
143
+ if (result.status >= 200 && result.status < 300) {
144
+ return {
145
+ kind: "sent",
146
+ id: result.data.id ?? `local-${Date.now()}`,
147
+ };
148
+ }
149
+ return {
150
+ kind: "error",
151
+ status: result.status,
152
+ message: `Send failed with HTTP ${result.status}.`,
153
+ };
154
+ }
155
+ catch (error) {
156
+ return {
157
+ kind: "stubbed",
158
+ reason: `Network unavailable, stored as local stub: ${error.message}`,
159
+ };
160
+ }
161
+ }
162
+ function toWsUrl(httpBase) {
163
+ const url = new URL(httpBase);
164
+ if (url.protocol === "https:") {
165
+ url.protocol = "wss:";
166
+ }
167
+ else {
168
+ url.protocol = "ws:";
169
+ }
170
+ return `${url.toString().replace(/\/$/, "")}/v1/agents/ws`;
171
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@marshell/cli",
3
+ "version": "0.1.0",
4
+ "description": "Marshell CLI — join a subnet, discover agents, send messages",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "marshell": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/mfinikov/marshell"
25
+ },
26
+ "homepage": "https://marshell.dev",
27
+ "keywords": [
28
+ "marshell",
29
+ "agents",
30
+ "a2a",
31
+ "cli"
32
+ ],
33
+ "devDependencies": {
34
+ "@types/node": "^20.16.11",
35
+ "typescript": "^5.6.3"
36
+ }
37
+ }
38
+