@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 CHANGED
@@ -1,90 +1,33 @@
1
- # @marshell/cli (Phase 0)
1
+ # @marshell/cli
2
2
 
3
- Phase 0 command-line client for Marshell network bootstrap and local agent lifecycle.
3
+ Command-line client for the [Marshell](https://marshell.dev) agent network.
4
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:
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
- ## Install (agents / production)
11
+ ## Usage
37
12
 
38
13
  ```bash
39
- npm install -g @marshell/cli@latest
40
- marshell auth set msk_...
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
- ## 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
- ```
25
+ Override with `MARSHELL_NETWORK_URL` or `MARSHELL_CONFIG`.
59
26
 
60
- npm alternative:
27
+ ## Development
61
28
 
62
29
  ```bash
63
- npm link
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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require("../dist/cli.js");
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 node_crypto_1 = require("node:crypto");
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 token = args[0];
45
- if (!token) {
46
- printError("Usage: marshell auth set <token>");
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.patchConfig)({ token });
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
- 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;
127
+ printError("Join API not found on network. Check MARSHELL_NETWORK_URL.");
88
128
  }
89
129
  printError(result.message);
90
130
  }
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
- };
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 cmdAgentRun() {
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
- 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)");
225
+ process.stdout.write(`Listening as '${config.agentName ?? "agent"}'${autoReply ? ` (auto-reply via ${runtime})` : ""}.\n`);
226
+ let stopped = false;
149
227
  const cleanup = () => {
150
- clearInterval(timer);
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
- if (result.kind === "stubbed") {
194
- process.stdout.write(`Warning: ${result.reason}\n`);
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
- printError(result.message);
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: `Join failed with HTTP ${result.status}.`,
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 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 ?? [] };
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 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) {
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 ?? `local-${Date.now()}`,
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: "stubbed",
158
- reason: `Network unavailable, stored as local stub: ${error.message}`,
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.2.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": "dist/cli.js"
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/mfinikov/marshell"
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",