@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 +90 -0
- package/dist/cli.js +244 -0
- package/dist/config.js +53 -0
- package/dist/network.js +171 -0
- package/package.json +38 -0
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
|
+
}
|
package/dist/network.js
ADDED
|
@@ -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
|
+
|