@marshell/cli 0.3.1 → 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/bin/marshell.js CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  require("../dist/cli.js");
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_child_process_1 = require("node:child_process");
4
5
  const node_os_1 = require("node:os");
5
6
  const config_1 = require("./config");
6
7
  const network_1 = require("./network");
@@ -12,14 +13,17 @@ function printHelp() {
12
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",
22
25
  " MARSHELL_AGENT_NAME default agent name for auth set",
26
+ " MARSHELL_WORKSPACE workspace for --auto-reply (cursor runtime)",
23
27
  " MARSHELL_CONFIG optional path to config file",
24
28
  ].join("\n");
25
29
  process.stdout.write(`${message}\n`);
@@ -124,71 +128,123 @@ async function cmdAgentJoin(args) {
124
128
  }
125
129
  printError(result.message);
126
130
  }
127
- function setupHeartbeat(message) {
128
- process.stdout.write(`${message}\n`);
129
- return setInterval(() => {
130
- process.stdout.write(`[heartbeat] ${new Date().toISOString()}\n`);
131
- }, 30_000);
132
- }
133
- async function tryConnectAgentWs(url) {
134
- const wsCtor = globalThis.WebSocket;
135
- if (!wsCtor) {
136
- return false;
137
- }
138
- return await new Promise((resolve) => {
139
- let settled = false;
140
- const ws = new wsCtor(url);
141
- const timeout = setTimeout(() => {
142
- if (!settled) {
143
- settled = true;
144
- ws.close();
145
- resolve(false);
146
- }
147
- }, 4000);
148
- ws.onopen = () => {
149
- clearTimeout(timeout);
150
- if (settled) {
151
- return;
152
- }
153
- settled = true;
154
- process.stdout.write(`Connected to ${url}\n`);
155
- ws.onmessage = (event) => {
156
- process.stdout.write(`[message] ${String(event.data)}\n`);
157
- };
158
- resolve(true);
159
- };
160
- ws.onerror = () => {
161
- clearTimeout(timeout);
162
- if (settled) {
163
- return;
164
- }
165
- settled = true;
166
- resolve(false);
167
- };
168
- ws.onclose = () => {
169
- clearTimeout(timeout);
170
- if (!settled) {
171
- settled = true;
172
- resolve(false);
173
- }
174
- };
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
+ });
175
152
  });
176
153
  }
177
- 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";
178
223
  const config = await (0, config_1.readConfig)();
179
224
  const networkUrl = (0, config_1.getNetworkUrl)(config);
180
- const wsUrl = (0, network_1.toWsUrl)(networkUrl);
181
- const connected = await tryConnectAgentWs(wsUrl);
182
- const timer = setupHeartbeat(connected
183
- ? "Agent loop active. Waiting for work..."
184
- : "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;
185
227
  const cleanup = () => {
186
- clearInterval(timer);
228
+ if (stopped)
229
+ return;
230
+ stopped = true;
187
231
  process.stdout.write("Exiting agent loop.\n");
188
232
  process.exit(0);
189
233
  };
190
234
  process.on("SIGINT", cleanup);
191
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
+ }
192
248
  }
193
249
  async function cmdDiscover(args) {
194
250
  const json = hasFlag(args, "--json");
@@ -223,14 +279,72 @@ async function cmdSend(args) {
223
279
  return;
224
280
  }
225
281
  if (result.kind === "sent") {
226
- 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`);
227
283
  return;
228
284
  }
229
- if (result.kind === "stubbed") {
230
- 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
+ }
231
318
  return;
232
319
  }
233
- 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);
234
348
  }
235
349
  async function main() {
236
350
  const args = process.argv.slice(2);
@@ -259,7 +373,7 @@ async function main() {
259
373
  return;
260
374
  }
261
375
  if (sub === "run") {
262
- await cmdAgentRun();
376
+ await cmdAgentRun(rest);
263
377
  return;
264
378
  }
265
379
  printError("Unknown agent command. Try: marshell agent join|run");
@@ -272,6 +386,14 @@ async function main() {
272
386
  await cmdSend(args.slice(1));
273
387
  return;
274
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
+ }
275
397
  printError(`Unknown command '${args[0]}'. Run marshell --help`);
276
398
  }
277
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,70 +123,115 @@ async function joinAgent(baseUrl, name) {
100
123
  }
101
124
  }
102
125
  async function discoverPeers(baseUrl) {
103
- const config = await (0, config_1.readConfig)();
104
- const headers = {};
105
- if (config.token) {
106
- headers.authorization = `Bearer ${config.token}`;
107
- }
108
- const candidates = ["/v1/peers", "/v1/discover"];
109
- for (const path of candidates) {
110
- try {
111
- const response = await fetch(withPath(baseUrl, path), {
112
- method: "GET",
113
- headers,
114
- });
115
- if (response.status === 404) {
116
- continue;
117
- }
118
- if (response.ok) {
119
- const data = (await response.json());
120
- if (Array.isArray(data)) {
121
- return { peers: data };
122
- }
123
- 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 };
124
136
  }
137
+ return { peers: data.peers ?? [] };
125
138
  }
126
- catch {
127
- return { peers: [] };
128
- }
139
+ }
140
+ catch {
141
+ return { peers: [] };
129
142
  }
130
143
  return { peers: [] };
131
144
  }
132
145
  async function sendMessage(baseUrl, to, text) {
133
- const config = await (0, config_1.readConfig)();
134
- if (!config.token) {
135
- return { kind: "error", message: "Missing auth token." };
136
- }
137
146
  try {
138
- const result = await postJson(withPath(baseUrl, "/v1/messages/send"), {
139
- token: config.token,
140
- to,
141
- text,
142
- });
143
- if (result.status === 404) {
144
- return {
145
- kind: "stubbed",
146
- reason: "Network send API is not available yet (Phase 1).",
147
- };
148
- }
149
- 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) {
150
150
  return {
151
151
  kind: "sent",
152
- id: result.data.id ?? `local-${Date.now()}`,
152
+ id: result.data.id,
153
+ status: result.data.status ?? "delivered",
153
154
  };
154
155
  }
155
156
  return {
156
157
  kind: "error",
157
158
  status: result.status,
158
- message: `Send failed with HTTP ${result.status}.`,
159
+ message: result.data.error ?? `Send failed with HTTP ${result.status}.`,
159
160
  };
160
161
  }
161
162
  catch (error) {
162
163
  return {
163
- kind: "stubbed",
164
- reason: `Network unavailable, stored as local stub: ${error.message}`,
164
+ kind: "error",
165
+ message: error.message,
166
+ };
167
+ }
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}.`,
165
211
  };
166
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" };
167
235
  }
168
236
  function toWsUrl(httpBase) {
169
237
  const url = new URL(httpBase);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshell/cli",
3
- "version": "0.3.1",
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",