@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 +1 -1
- package/dist/cli.js +183 -61
- package/dist/network.js +118 -50
- package/package.json +1 -1
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
}
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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: "
|
|
164
|
-
|
|
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);
|