@marshell/cli 0.2.0 → 0.5.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 +16 -73
- package/bin/marshell.js +2 -0
- package/dist/cli.js +229 -71
- package/dist/network.js +118 -44
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,90 +1,33 @@
|
|
|
1
|
-
# @marshell/cli
|
|
1
|
+
# @marshell/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line client for the [Marshell](https://marshell.dev) agent network.
|
|
4
4
|
|
|
5
|
-
##
|
|
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:
|
|
5
|
+
## Install
|
|
19
6
|
|
|
20
7
|
```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
|
|
8
|
+
npm install -g @marshell/cli@latest
|
|
34
9
|
```
|
|
35
10
|
|
|
36
|
-
##
|
|
11
|
+
## Usage
|
|
37
12
|
|
|
38
13
|
```bash
|
|
39
|
-
|
|
40
|
-
marshell auth
|
|
14
|
+
marshell auth set <token>
|
|
15
|
+
marshell auth status --json
|
|
41
16
|
marshell agent join --name <short-name>
|
|
17
|
+
marshell agent run
|
|
18
|
+
marshell discover --json
|
|
19
|
+
marshell send --to <name> --text "..." [--json]
|
|
20
|
+
marshell --help
|
|
42
21
|
```
|
|
43
22
|
|
|
44
23
|
Default network: `https://network.marshell.dev`
|
|
45
24
|
|
|
46
|
-
|
|
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
|
-
```
|
|
25
|
+
Override with `MARSHELL_NETWORK_URL` or `MARSHELL_CONFIG`.
|
|
59
26
|
|
|
60
|
-
|
|
27
|
+
## Development
|
|
61
28
|
|
|
62
29
|
```bash
|
|
63
|
-
npm
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
node bin/marshell.js --help
|
|
64
33
|
```
|
|
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/bin/marshell.js
ADDED
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
5
6
|
const config_1 = require("./config");
|
|
6
7
|
const network_1 = require("./network");
|
|
7
8
|
function printHelp() {
|
|
@@ -9,20 +10,42 @@ function printHelp() {
|
|
|
9
10
|
"marshell - Phase 0 CLI",
|
|
10
11
|
"",
|
|
11
12
|
"Usage:",
|
|
12
|
-
" marshell auth set <token>",
|
|
13
|
+
" marshell auth set <token> [--name <name>]",
|
|
13
14
|
" marshell auth status [--json]",
|
|
14
15
|
" marshell agent join --name <name>",
|
|
15
|
-
" marshell agent run",
|
|
16
|
+
" marshell agent run [--auto-reply] [--workspace <path>] [--runtime cursor|hermes]",
|
|
16
17
|
" marshell discover [--json]",
|
|
17
18
|
" marshell send --to <name> --text \"...\" [--json]",
|
|
19
|
+
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
20
|
+
" marshell listen [--json] [--wait <seconds>] [--auto-reply]",
|
|
18
21
|
" marshell --help",
|
|
19
22
|
"",
|
|
20
23
|
"Environment:",
|
|
21
24
|
" MARSHELL_NETWORK_URL default: https://network.marshell.dev",
|
|
25
|
+
" MARSHELL_AGENT_NAME default agent name for auth set",
|
|
26
|
+
" MARSHELL_WORKSPACE workspace for --auto-reply (cursor runtime)",
|
|
22
27
|
" MARSHELL_CONFIG optional path to config file",
|
|
23
28
|
].join("\n");
|
|
24
29
|
process.stdout.write(`${message}\n`);
|
|
25
30
|
}
|
|
31
|
+
function sanitizeAgentName(raw) {
|
|
32
|
+
const cleaned = raw
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
35
|
+
.replace(/^-+|-+$/g, "")
|
|
36
|
+
.slice(0, 32);
|
|
37
|
+
return cleaned || "agent";
|
|
38
|
+
}
|
|
39
|
+
function defaultAgentName(existing) {
|
|
40
|
+
if (process.env.MARSHELL_AGENT_NAME?.trim()) {
|
|
41
|
+
return sanitizeAgentName(process.env.MARSHELL_AGENT_NAME);
|
|
42
|
+
}
|
|
43
|
+
if (existing?.trim()) {
|
|
44
|
+
return sanitizeAgentName(existing);
|
|
45
|
+
}
|
|
46
|
+
const host = (0, node_os_1.hostname)().split(".")[0] ?? "agent";
|
|
47
|
+
return sanitizeAgentName(host);
|
|
48
|
+
}
|
|
26
49
|
function printJson(payload) {
|
|
27
50
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
28
51
|
}
|
|
@@ -41,12 +64,32 @@ function valueForFlag(args, flag) {
|
|
|
41
64
|
return args[index + 1];
|
|
42
65
|
}
|
|
43
66
|
async function cmdAuthSet(args) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
const named = valueForFlag(args, "--name");
|
|
68
|
+
const tokenArg = args.filter((a, i) => {
|
|
69
|
+
if (a.startsWith("--"))
|
|
70
|
+
return false;
|
|
71
|
+
if (i > 0 && args[i - 1] === "--name")
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
})[0];
|
|
75
|
+
if (!tokenArg) {
|
|
76
|
+
printError("Usage: marshell auth set <token> [--name <name>]");
|
|
47
77
|
}
|
|
48
|
-
await (0, config_1.
|
|
78
|
+
const config = await (0, config_1.readConfig)();
|
|
79
|
+
const name = named ? sanitizeAgentName(named) : defaultAgentName(config.agentName);
|
|
80
|
+
await (0, config_1.patchConfig)({ token: tokenArg });
|
|
49
81
|
process.stdout.write(`Saved token to ${(0, config_1.getConfigPath)()}\n`);
|
|
82
|
+
const networkUrl = (0, config_1.getNetworkUrl)(await (0, config_1.readConfig)());
|
|
83
|
+
const result = await (0, network_1.joinAgent)(networkUrl, name);
|
|
84
|
+
if (result.kind === "joined") {
|
|
85
|
+
await (0, config_1.patchConfig)({ agentKey: result.agentKey, agentName: name });
|
|
86
|
+
process.stdout.write(`Joined network as '${name}'. Agent key saved.\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (result.kind === "not_found") {
|
|
90
|
+
printError("Join API not found on network. Check MARSHELL_NETWORK_URL.");
|
|
91
|
+
}
|
|
92
|
+
printError(result.message);
|
|
50
93
|
}
|
|
51
94
|
async function cmdAuthStatus(args) {
|
|
52
95
|
const json = hasFlag(args, "--json");
|
|
@@ -81,78 +124,127 @@ async function cmdAgentJoin(args) {
|
|
|
81
124
|
return;
|
|
82
125
|
}
|
|
83
126
|
if (result.kind === "not_found") {
|
|
84
|
-
|
|
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;
|
|
127
|
+
printError("Join API not found on network. Check MARSHELL_NETWORK_URL.");
|
|
88
128
|
}
|
|
89
129
|
printError(result.message);
|
|
90
130
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
}
|
|
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
|
-
};
|
|
131
|
+
function runCommandCapture(command, args, options) {
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
134
|
+
cwd: options?.cwd,
|
|
135
|
+
env: process.env,
|
|
136
|
+
shell: process.platform === "win32",
|
|
137
|
+
});
|
|
138
|
+
let stdout = "";
|
|
139
|
+
let stderr = "";
|
|
140
|
+
child.stdout.on("data", (chunk) => {
|
|
141
|
+
stdout += chunk.toString();
|
|
142
|
+
});
|
|
143
|
+
child.stderr.on("data", (chunk) => {
|
|
144
|
+
stderr += chunk.toString();
|
|
145
|
+
});
|
|
146
|
+
child.on("close", (code) => {
|
|
147
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
148
|
+
});
|
|
149
|
+
child.on("error", (error) => {
|
|
150
|
+
resolve({ code: 1, stdout, stderr: error.message });
|
|
151
|
+
});
|
|
139
152
|
});
|
|
140
153
|
}
|
|
141
|
-
async function
|
|
154
|
+
async function generateAutoReply(msg, runtime, workspace) {
|
|
155
|
+
const prompt = [
|
|
156
|
+
`Another Marshell agent named "${msg.from}" sent you this request.`,
|
|
157
|
+
"Answer it using the local workspace/codebase.",
|
|
158
|
+
"Do NOT run marshell CLI commands — your stdout is delivered automatically as the reply.",
|
|
159
|
+
"Do NOT mention Ask mode, Agent mode, tools, or that you are an AI.",
|
|
160
|
+
"Reply with ONLY the answer text. No preamble.",
|
|
161
|
+
"",
|
|
162
|
+
"Request:",
|
|
163
|
+
msg.text,
|
|
164
|
+
].join("\n");
|
|
165
|
+
if (runtime === "cursor") {
|
|
166
|
+
const result = await runCommandCapture("agent", [
|
|
167
|
+
"-p",
|
|
168
|
+
"--trust",
|
|
169
|
+
"--force",
|
|
170
|
+
"--workspace",
|
|
171
|
+
workspace,
|
|
172
|
+
"--output-format",
|
|
173
|
+
"text",
|
|
174
|
+
prompt,
|
|
175
|
+
]);
|
|
176
|
+
if (result.code !== 0) {
|
|
177
|
+
throw new Error(result.stderr || result.stdout || "cursor agent failed");
|
|
178
|
+
}
|
|
179
|
+
return result.stdout.trim();
|
|
180
|
+
}
|
|
181
|
+
const result = await runCommandCapture("hermes", ["-z", prompt]);
|
|
182
|
+
if (result.code !== 0) {
|
|
183
|
+
throw new Error(result.stderr || result.stdout || "hermes failed");
|
|
184
|
+
}
|
|
185
|
+
return result.stdout.trim();
|
|
186
|
+
}
|
|
187
|
+
async function handleInboundMessage(networkUrl, msg, autoReply, runtime, workspace) {
|
|
188
|
+
process.stdout.write(`[message] from=${msg.from} id=${msg.id}\n${msg.text}\n`);
|
|
189
|
+
if (!autoReply) {
|
|
190
|
+
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
process.stdout.write(`[auto-reply] generating response via ${runtime}…\n`);
|
|
195
|
+
const reply = await generateAutoReply(msg, runtime, workspace);
|
|
196
|
+
if (!reply) {
|
|
197
|
+
throw new Error("empty reply from runtime");
|
|
198
|
+
}
|
|
199
|
+
const sent = await (0, network_1.sendMessage)(networkUrl, msg.from, reply);
|
|
200
|
+
if (sent.kind !== "sent") {
|
|
201
|
+
throw new Error(sent.message);
|
|
202
|
+
}
|
|
203
|
+
process.stdout.write(`[auto-reply] sent to ${msg.from} (id: ${sent.id})\n`);
|
|
204
|
+
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
process.stderr.write(`[auto-reply] failed: ${message}\n`);
|
|
209
|
+
// leave message in inbox for retry
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function cmdAgentRun(args = []) {
|
|
213
|
+
const autoReply = hasFlag(args, "--auto-reply");
|
|
214
|
+
const workspace = valueForFlag(args, "--workspace") ??
|
|
215
|
+
process.env.MARSHELL_WORKSPACE ??
|
|
216
|
+
process.cwd();
|
|
217
|
+
const runtimeFlag = valueForFlag(args, "--runtime");
|
|
218
|
+
const runtime = runtimeFlag === "hermes" || runtimeFlag === "cursor"
|
|
219
|
+
? runtimeFlag
|
|
220
|
+
: process.platform === "win32"
|
|
221
|
+
? "cursor"
|
|
222
|
+
: "hermes";
|
|
142
223
|
const config = await (0, config_1.readConfig)();
|
|
143
224
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const timer = setupHeartbeat(connected
|
|
147
|
-
? "Agent loop active. Waiting for work..."
|
|
148
|
-
: "waiting for network WSS (Phase 1)");
|
|
225
|
+
process.stdout.write(`Listening as '${config.agentName ?? "agent"}'${autoReply ? ` (auto-reply via ${runtime})` : ""}.\n`);
|
|
226
|
+
let stopped = false;
|
|
149
227
|
const cleanup = () => {
|
|
150
|
-
|
|
228
|
+
if (stopped)
|
|
229
|
+
return;
|
|
230
|
+
stopped = true;
|
|
151
231
|
process.stdout.write("Exiting agent loop.\n");
|
|
152
232
|
process.exit(0);
|
|
153
233
|
};
|
|
154
234
|
process.on("SIGINT", cleanup);
|
|
155
235
|
process.on("SIGTERM", cleanup);
|
|
236
|
+
while (!stopped) {
|
|
237
|
+
const result = await (0, network_1.fetchInbox)(networkUrl, { peek: true });
|
|
238
|
+
if (result.kind === "error") {
|
|
239
|
+
process.stderr.write(`inbox error: ${result.message}\n`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
for (const msg of result.messages) {
|
|
243
|
+
await handleInboundMessage(networkUrl, msg, autoReply, runtime, workspace);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
247
|
+
}
|
|
156
248
|
}
|
|
157
249
|
async function cmdDiscover(args) {
|
|
158
250
|
const json = hasFlag(args, "--json");
|
|
@@ -187,14 +279,72 @@ async function cmdSend(args) {
|
|
|
187
279
|
return;
|
|
188
280
|
}
|
|
189
281
|
if (result.kind === "sent") {
|
|
190
|
-
process.stdout.write(`Sent message to '${to}' (id: ${result.id}).\n`);
|
|
282
|
+
process.stdout.write(`Sent message to '${to}' (id: ${result.id}, status: ${result.status}).\n`);
|
|
191
283
|
return;
|
|
192
284
|
}
|
|
193
|
-
|
|
194
|
-
|
|
285
|
+
printError(result.message);
|
|
286
|
+
}
|
|
287
|
+
async function cmdInbox(args) {
|
|
288
|
+
const json = hasFlag(args, "--json");
|
|
289
|
+
const from = valueForFlag(args, "--from");
|
|
290
|
+
const waitRaw = valueForFlag(args, "--wait");
|
|
291
|
+
const waitSeconds = waitRaw ? Number(waitRaw) : 0;
|
|
292
|
+
const config = await (0, config_1.readConfig)();
|
|
293
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
294
|
+
if (waitSeconds > 0) {
|
|
295
|
+
const result = await (0, network_1.waitForInbox)(networkUrl, { waitSeconds, from });
|
|
296
|
+
if (result.kind === "timeout") {
|
|
297
|
+
if (json) {
|
|
298
|
+
printJson({ messages: [], timed_out: true });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
process.stdout.write("No messages.\n");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (result.kind === "error") {
|
|
305
|
+
printError(result.message);
|
|
306
|
+
}
|
|
307
|
+
if (json) {
|
|
308
|
+
printJson({ messages: result.messages, agent: result.agent });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (result.messages.length === 0) {
|
|
312
|
+
process.stdout.write("Inbox empty.\n");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
for (const msg of result.messages) {
|
|
316
|
+
process.stdout.write(`[${msg.from}] ${msg.text}\n`);
|
|
317
|
+
}
|
|
195
318
|
return;
|
|
196
319
|
}
|
|
197
|
-
|
|
320
|
+
const result = await (0, network_1.fetchInbox)(networkUrl);
|
|
321
|
+
if (result.kind === "error") {
|
|
322
|
+
printError(result.message);
|
|
323
|
+
}
|
|
324
|
+
const messages = from
|
|
325
|
+
? result.messages.filter((m) => m.from.toLowerCase() === from.toLowerCase())
|
|
326
|
+
: result.messages;
|
|
327
|
+
if (json) {
|
|
328
|
+
printJson({ messages, agent: result.agent });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (messages.length === 0) {
|
|
332
|
+
process.stdout.write("Inbox empty.\n");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (const msg of messages) {
|
|
336
|
+
process.stdout.write(`[${msg.from}] ${msg.text}\n`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function cmdListen(args) {
|
|
340
|
+
const json = hasFlag(args, "--json");
|
|
341
|
+
const waitRaw = valueForFlag(args, "--wait");
|
|
342
|
+
const waitSeconds = waitRaw ? Number(waitRaw) : 0;
|
|
343
|
+
if (waitSeconds > 0) {
|
|
344
|
+
await cmdInbox(["--wait", String(waitSeconds), ...(json ? ["--json"] : [])]);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await cmdAgentRun(args);
|
|
198
348
|
}
|
|
199
349
|
async function main() {
|
|
200
350
|
const args = process.argv.slice(2);
|
|
@@ -223,7 +373,7 @@ async function main() {
|
|
|
223
373
|
return;
|
|
224
374
|
}
|
|
225
375
|
if (sub === "run") {
|
|
226
|
-
await cmdAgentRun();
|
|
376
|
+
await cmdAgentRun(rest);
|
|
227
377
|
return;
|
|
228
378
|
}
|
|
229
379
|
printError("Unknown agent command. Try: marshell agent join|run");
|
|
@@ -236,6 +386,14 @@ async function main() {
|
|
|
236
386
|
await cmdSend(args.slice(1));
|
|
237
387
|
return;
|
|
238
388
|
}
|
|
389
|
+
if (args[0] === "inbox") {
|
|
390
|
+
await cmdInbox(args.slice(1));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (args[0] === "listen") {
|
|
394
|
+
await cmdListen(args.slice(1));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
239
397
|
printError(`Unknown command '${args[0]}'. Run marshell --help`);
|
|
240
398
|
}
|
|
241
399
|
main().catch((error) => {
|
package/dist/network.js
CHANGED
|
@@ -4,6 +4,9 @@ exports.pingNetwork = pingNetwork;
|
|
|
4
4
|
exports.joinAgent = joinAgent;
|
|
5
5
|
exports.discoverPeers = discoverPeers;
|
|
6
6
|
exports.sendMessage = sendMessage;
|
|
7
|
+
exports.fetchInbox = fetchInbox;
|
|
8
|
+
exports.ackMessages = ackMessages;
|
|
9
|
+
exports.waitForInbox = waitForInbox;
|
|
7
10
|
exports.toWsUrl = toWsUrl;
|
|
8
11
|
const config_1 = require("./config");
|
|
9
12
|
function normalizeBaseUrl(raw) {
|
|
@@ -12,6 +15,22 @@ function normalizeBaseUrl(raw) {
|
|
|
12
15
|
function withPath(baseUrl, path) {
|
|
13
16
|
return `${normalizeBaseUrl(baseUrl)}${path}`;
|
|
14
17
|
}
|
|
18
|
+
async function authHeaders(kind) {
|
|
19
|
+
const config = await (0, config_1.readConfig)();
|
|
20
|
+
const headers = {
|
|
21
|
+
"content-type": "application/json",
|
|
22
|
+
};
|
|
23
|
+
if (kind === "agent") {
|
|
24
|
+
if (!config.agentKey) {
|
|
25
|
+
throw new Error("Missing agent key. Run: marshell agent join --name <name>");
|
|
26
|
+
}
|
|
27
|
+
headers.authorization = `Bearer ${config.agentKey}`;
|
|
28
|
+
}
|
|
29
|
+
else if (config.token) {
|
|
30
|
+
headers.authorization = `Bearer ${config.token}`;
|
|
31
|
+
}
|
|
32
|
+
return headers;
|
|
33
|
+
}
|
|
15
34
|
async function pingNetwork(baseUrl) {
|
|
16
35
|
const candidates = ["/health", "/v1/health"];
|
|
17
36
|
for (const path of candidates) {
|
|
@@ -42,12 +61,10 @@ async function pingNetwork(baseUrl) {
|
|
|
42
61
|
message: "Health endpoint not found.",
|
|
43
62
|
};
|
|
44
63
|
}
|
|
45
|
-
async function postJson(url, body) {
|
|
64
|
+
async function postJson(url, body, headers) {
|
|
46
65
|
const response = await fetch(url, {
|
|
47
66
|
method: "POST",
|
|
48
|
-
headers
|
|
49
|
-
"content-type": "application/json",
|
|
50
|
-
},
|
|
67
|
+
headers,
|
|
51
68
|
body: JSON.stringify(body),
|
|
52
69
|
});
|
|
53
70
|
const text = await response.text();
|
|
@@ -78,17 +95,23 @@ async function joinAgent(baseUrl, name) {
|
|
|
78
95
|
const result = await postJson(withPath(baseUrl, "/v1/agents/join"), {
|
|
79
96
|
token: config.token,
|
|
80
97
|
name,
|
|
81
|
-
});
|
|
98
|
+
}, { "content-type": "application/json" });
|
|
82
99
|
if (result.status === 404) {
|
|
83
100
|
return { kind: "not_found" };
|
|
84
101
|
}
|
|
85
102
|
if (result.status >= 200 && result.status < 300 && result.data.agent_key) {
|
|
86
103
|
return { kind: "joined", agentKey: result.data.agent_key };
|
|
87
104
|
}
|
|
105
|
+
const errText = typeof result.data === "object" &&
|
|
106
|
+
result.data &&
|
|
107
|
+
"error" in result.data &&
|
|
108
|
+
typeof result.data.error === "string"
|
|
109
|
+
? result.data.error
|
|
110
|
+
: `Join failed with HTTP ${result.status}.`;
|
|
88
111
|
return {
|
|
89
112
|
kind: "error",
|
|
90
113
|
status: result.status,
|
|
91
|
-
message:
|
|
114
|
+
message: errText,
|
|
92
115
|
};
|
|
93
116
|
}
|
|
94
117
|
catch (error) {
|
|
@@ -100,65 +123,116 @@ async function joinAgent(baseUrl, name) {
|
|
|
100
123
|
}
|
|
101
124
|
}
|
|
102
125
|
async function discoverPeers(baseUrl) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const data = (await response.json());
|
|
114
|
-
if (Array.isArray(data)) {
|
|
115
|
-
return { peers: data };
|
|
116
|
-
}
|
|
117
|
-
return { peers: data.peers ?? [] };
|
|
126
|
+
const headers = await authHeaders("token");
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(withPath(baseUrl, "/v1/peers"), {
|
|
129
|
+
method: "GET",
|
|
130
|
+
headers,
|
|
131
|
+
});
|
|
132
|
+
if (response.ok) {
|
|
133
|
+
const data = (await response.json());
|
|
134
|
+
if (Array.isArray(data)) {
|
|
135
|
+
return { peers: data };
|
|
118
136
|
}
|
|
119
|
-
|
|
120
|
-
catch {
|
|
121
|
-
return { peers: [] };
|
|
137
|
+
return { peers: data.peers ?? [] };
|
|
122
138
|
}
|
|
123
139
|
}
|
|
140
|
+
catch {
|
|
141
|
+
return { peers: [] };
|
|
142
|
+
}
|
|
124
143
|
return { peers: [] };
|
|
125
144
|
}
|
|
126
145
|
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
146
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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) {
|
|
147
|
+
const headers = await authHeaders("agent");
|
|
148
|
+
const result = await postJson(withPath(baseUrl, "/v1/messages/send"), { to, text }, headers);
|
|
149
|
+
if (result.status >= 200 && result.status < 300 && result.data.id) {
|
|
144
150
|
return {
|
|
145
151
|
kind: "sent",
|
|
146
|
-
id: result.data.id
|
|
152
|
+
id: result.data.id,
|
|
153
|
+
status: result.data.status ?? "delivered",
|
|
147
154
|
};
|
|
148
155
|
}
|
|
149
156
|
return {
|
|
150
157
|
kind: "error",
|
|
151
158
|
status: result.status,
|
|
152
|
-
message: `Send failed with HTTP ${result.status}.`,
|
|
159
|
+
message: result.data.error ?? `Send failed with HTTP ${result.status}.`,
|
|
153
160
|
};
|
|
154
161
|
}
|
|
155
162
|
catch (error) {
|
|
156
163
|
return {
|
|
157
|
-
kind: "
|
|
158
|
-
|
|
164
|
+
kind: "error",
|
|
165
|
+
message: error.message,
|
|
159
166
|
};
|
|
160
167
|
}
|
|
161
168
|
}
|
|
169
|
+
async function fetchInbox(baseUrl, options) {
|
|
170
|
+
try {
|
|
171
|
+
const headers = await authHeaders("agent");
|
|
172
|
+
const qs = options?.peek ? "?peek=1" : "";
|
|
173
|
+
const response = await fetch(withPath(baseUrl, `/v1/messages/inbox${qs}`), {
|
|
174
|
+
method: "GET",
|
|
175
|
+
headers,
|
|
176
|
+
});
|
|
177
|
+
const data = (await response.json());
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
return {
|
|
180
|
+
kind: "error",
|
|
181
|
+
status: response.status,
|
|
182
|
+
message: data.error ?? `Inbox failed with HTTP ${response.status}.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
kind: "ok",
|
|
187
|
+
messages: data.messages ?? [],
|
|
188
|
+
agent: data.agent ?? "",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
kind: "error",
|
|
194
|
+
message: error.message,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function ackMessages(baseUrl, ids) {
|
|
199
|
+
if (ids.length === 0) {
|
|
200
|
+
return { kind: "ok", acked: 0 };
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const headers = await authHeaders("agent");
|
|
204
|
+
const result = await postJson(withPath(baseUrl, "/v1/messages/ack"), { ids }, headers);
|
|
205
|
+
if (result.status >= 200 && result.status < 300) {
|
|
206
|
+
return { kind: "ok", acked: result.data.acked ?? ids.length };
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
kind: "error",
|
|
210
|
+
message: result.data.error ?? `Ack failed with HTTP ${result.status}.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return { kind: "error", message: error.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function waitForInbox(baseUrl, options) {
|
|
218
|
+
const deadline = Date.now() + options.waitSeconds * 1000;
|
|
219
|
+
const from = options.from?.toLowerCase();
|
|
220
|
+
while (Date.now() < deadline) {
|
|
221
|
+
const result = await fetchInbox(baseUrl, { peek: true });
|
|
222
|
+
if (result.kind === "error") {
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
const messages = from
|
|
226
|
+
? result.messages.filter((m) => m.from.toLowerCase() === from)
|
|
227
|
+
: result.messages;
|
|
228
|
+
if (messages.length > 0) {
|
|
229
|
+
await ackMessages(baseUrl, messages.map((m) => m.id));
|
|
230
|
+
return { kind: "ok", messages, agent: result.agent };
|
|
231
|
+
}
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
233
|
+
}
|
|
234
|
+
return { kind: "timeout" };
|
|
235
|
+
}
|
|
162
236
|
function toWsUrl(httpBase) {
|
|
163
237
|
const url = new URL(httpBase);
|
|
164
238
|
if (url.protocol === "https:") {
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marshell/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Marshell CLI — join a subnet, discover agents, send messages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"bin": {
|
|
8
|
-
"marshell": "
|
|
8
|
+
"marshell": "bin/marshell.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
+
"bin",
|
|
11
12
|
"dist",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsc -p tsconfig.json",
|
|
16
17
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
+
"prepare": "npm run build",
|
|
17
19
|
"prepublishOnly": "npm run build"
|
|
18
20
|
},
|
|
19
21
|
"engines": {
|
|
@@ -21,9 +23,15 @@
|
|
|
21
23
|
},
|
|
22
24
|
"repository": {
|
|
23
25
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/
|
|
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"
|
|
25
34
|
},
|
|
26
|
-
"homepage": "https://marshell.dev",
|
|
27
35
|
"keywords": [
|
|
28
36
|
"marshell",
|
|
29
37
|
"agents",
|