@marshell/cli 0.5.0 → 0.6.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/dist/bridge.js +271 -0
- package/dist/cli.js +103 -131
- package/dist/network.js +56 -3
- package/package.json +1 -2
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tryFastReply = tryFastReply;
|
|
4
|
+
exports.handleInbound = handleInbound;
|
|
5
|
+
exports.runBridge = runBridge;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const network_1 = require("./network");
|
|
8
|
+
const DEFAULT_REPLY_TIMEOUT_MS = 120_000;
|
|
9
|
+
/** Instant replies — no LLM spawn. Marshell transport stays fast. */
|
|
10
|
+
function tryFastReply(msg) {
|
|
11
|
+
const text = msg.text.trim();
|
|
12
|
+
if (/^(ping|pong)$/i.test(text)) {
|
|
13
|
+
return "pong";
|
|
14
|
+
}
|
|
15
|
+
if (/^(hi|hello|hey|yo|sup|picun)[!.?\s]*$/i.test(text)) {
|
|
16
|
+
return text.toLowerCase() === "picun" ? "picun" : "hi";
|
|
17
|
+
}
|
|
18
|
+
if (text.toLowerCase().startsWith("echo:")) {
|
|
19
|
+
const body = text.slice(5).trim();
|
|
20
|
+
return body || "(empty)";
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function killProcessTree(pid) {
|
|
25
|
+
if (!pid)
|
|
26
|
+
return;
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
(0, node_child_process_1.spawn)("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
process.kill(-pid, "SIGKILL");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(pid, "SIGKILL");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function spawnCommand(command, args, options) {
|
|
47
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_REPLY_TIMEOUT_MS;
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const child = process.platform === "win32"
|
|
50
|
+
? (0, node_child_process_1.spawn)(command, args, {
|
|
51
|
+
cwd: options?.cwd,
|
|
52
|
+
env: process.env,
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
shell: false,
|
|
55
|
+
})
|
|
56
|
+
: (0, node_child_process_1.spawn)(command, args, {
|
|
57
|
+
cwd: options?.cwd,
|
|
58
|
+
env: process.env,
|
|
59
|
+
detached: true,
|
|
60
|
+
});
|
|
61
|
+
let stdout = "";
|
|
62
|
+
let stderr = "";
|
|
63
|
+
let settled = false;
|
|
64
|
+
const finish = (code, errText) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve({
|
|
70
|
+
code,
|
|
71
|
+
stdout,
|
|
72
|
+
stderr: errText ? `${stderr}\n${errText}`.trim() : stderr,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
killProcessTree(child.pid);
|
|
77
|
+
finish(124, `timeout after ${Math.round(timeoutMs / 1000)}s`);
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
if (options?.input) {
|
|
80
|
+
child.stdin.write(options.input);
|
|
81
|
+
child.stdin.end();
|
|
82
|
+
}
|
|
83
|
+
child.stdout.on("data", (chunk) => {
|
|
84
|
+
stdout += chunk.toString();
|
|
85
|
+
});
|
|
86
|
+
child.stderr.on("data", (chunk) => {
|
|
87
|
+
stderr += chunk.toString();
|
|
88
|
+
});
|
|
89
|
+
child.on("close", (code) => {
|
|
90
|
+
finish(code ?? 1);
|
|
91
|
+
});
|
|
92
|
+
child.on("error", (error) => {
|
|
93
|
+
finish(1, error.message);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function generateRuntimeReply(msg, runtime, workspace, timeoutMs) {
|
|
98
|
+
const prompt = [
|
|
99
|
+
`Marshell agent "${msg.from}" sent:`,
|
|
100
|
+
msg.text,
|
|
101
|
+
"",
|
|
102
|
+
"Reply with ONLY the answer. No tools, no marshell CLI, no meta commentary.",
|
|
103
|
+
].join("\n");
|
|
104
|
+
if (runtime === "cursor") {
|
|
105
|
+
const result = await spawnCommand("agent", [
|
|
106
|
+
"-p",
|
|
107
|
+
"--trust",
|
|
108
|
+
"--mode",
|
|
109
|
+
"ask",
|
|
110
|
+
"--workspace",
|
|
111
|
+
workspace,
|
|
112
|
+
"--output-format",
|
|
113
|
+
"text",
|
|
114
|
+
prompt,
|
|
115
|
+
], { timeoutMs });
|
|
116
|
+
if (result.code === 124) {
|
|
117
|
+
throw new Error(`auto-reply timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
118
|
+
}
|
|
119
|
+
if (result.code !== 0) {
|
|
120
|
+
throw new Error(result.stderr || result.stdout || "cursor agent failed");
|
|
121
|
+
}
|
|
122
|
+
return result.stdout.trim();
|
|
123
|
+
}
|
|
124
|
+
const result = await spawnCommand("hermes", ["-z", prompt], { timeoutMs });
|
|
125
|
+
if (result.code === 124) {
|
|
126
|
+
throw new Error(`auto-reply timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
127
|
+
}
|
|
128
|
+
if (result.code !== 0) {
|
|
129
|
+
throw new Error(result.stderr || result.stdout || "hermes failed");
|
|
130
|
+
}
|
|
131
|
+
return result.stdout.trim();
|
|
132
|
+
}
|
|
133
|
+
async function runHookReply(hook, msg, timeoutMs) {
|
|
134
|
+
const payload = JSON.stringify({
|
|
135
|
+
id: msg.id,
|
|
136
|
+
from: msg.from,
|
|
137
|
+
text: msg.text,
|
|
138
|
+
created_at: msg.created_at,
|
|
139
|
+
});
|
|
140
|
+
if (process.platform === "win32") {
|
|
141
|
+
const result = await spawnCommand("cmd.exe", ["/d", "/s", "/c", hook], { input: payload, timeoutMs });
|
|
142
|
+
if (result.code === 124) {
|
|
143
|
+
throw new Error(`hook timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
144
|
+
}
|
|
145
|
+
if (result.code !== 0) {
|
|
146
|
+
throw new Error(result.stderr || result.stdout || "hook failed");
|
|
147
|
+
}
|
|
148
|
+
return result.stdout.trim();
|
|
149
|
+
}
|
|
150
|
+
const result = await spawnCommand("sh", ["-c", hook], {
|
|
151
|
+
input: payload,
|
|
152
|
+
timeoutMs,
|
|
153
|
+
});
|
|
154
|
+
if (result.code === 124) {
|
|
155
|
+
throw new Error(`hook timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
156
|
+
}
|
|
157
|
+
if (result.code !== 0) {
|
|
158
|
+
throw new Error(result.stderr || result.stdout || "hook failed");
|
|
159
|
+
}
|
|
160
|
+
return result.stdout.trim();
|
|
161
|
+
}
|
|
162
|
+
function emitEvent(json, event) {
|
|
163
|
+
if (json) {
|
|
164
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const kind = event.type;
|
|
168
|
+
if (kind === "message") {
|
|
169
|
+
process.stdout.write(`[message] from=${event.from} id=${event.id}\n${event.text}\n`);
|
|
170
|
+
}
|
|
171
|
+
else if (kind === "reply") {
|
|
172
|
+
process.stdout.write(`[reply] to=${event.to} id=${event.id}\n`);
|
|
173
|
+
}
|
|
174
|
+
else if (kind === "error") {
|
|
175
|
+
process.stderr.write(`[bridge] ${event.message}\n`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function deliverReply(networkUrl, msg, reply, json) {
|
|
179
|
+
const sent = await (0, network_1.sendMessage)(networkUrl, msg.from, reply);
|
|
180
|
+
if (sent.kind !== "sent") {
|
|
181
|
+
throw new Error(sent.message);
|
|
182
|
+
}
|
|
183
|
+
emitEvent(json, {
|
|
184
|
+
type: "reply",
|
|
185
|
+
to: msg.from,
|
|
186
|
+
id: sent.id,
|
|
187
|
+
in_reply_to: msg.id,
|
|
188
|
+
text: reply,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function processReplyAsync(networkUrl, msg, options) {
|
|
192
|
+
try {
|
|
193
|
+
let reply = tryFastReply(msg);
|
|
194
|
+
if (!reply && options.hook) {
|
|
195
|
+
reply = await runHookReply(options.hook, msg, options.replyTimeoutMs);
|
|
196
|
+
}
|
|
197
|
+
else if (!reply && options.autoReply && options.runtime !== "fast") {
|
|
198
|
+
reply = await generateRuntimeReply(msg, options.runtime, options.workspace, options.replyTimeoutMs);
|
|
199
|
+
}
|
|
200
|
+
if (!reply) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!reply.trim()) {
|
|
204
|
+
throw new Error("empty reply");
|
|
205
|
+
}
|
|
206
|
+
await deliverReply(networkUrl, msg, reply.trim(), options.json);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
210
|
+
emitEvent(options.json, { type: "error", message, in_reply_to: msg.id });
|
|
211
|
+
await (0, network_1.sendMessage)(networkUrl, msg.from, `Auto-reply failed (${message}). Please retry.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function handleInbound(networkUrl, msg, options) {
|
|
215
|
+
emitEvent(options.json, {
|
|
216
|
+
type: "message",
|
|
217
|
+
id: msg.id,
|
|
218
|
+
from: msg.from,
|
|
219
|
+
text: msg.text,
|
|
220
|
+
created_at: msg.created_at,
|
|
221
|
+
});
|
|
222
|
+
const fast = tryFastReply(msg);
|
|
223
|
+
if (fast) {
|
|
224
|
+
await deliverReply(networkUrl, msg, fast, options.json);
|
|
225
|
+
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const willReply = Boolean(options.hook) ||
|
|
229
|
+
(options.autoReply && options.runtime !== "fast");
|
|
230
|
+
// Ack immediately so transport never blocks on slow LLM / hook.
|
|
231
|
+
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
232
|
+
if (!willReply) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
void processReplyAsync(networkUrl, msg, options);
|
|
236
|
+
}
|
|
237
|
+
async function runBridge(options) {
|
|
238
|
+
const label = options.agentName ?? "agent";
|
|
239
|
+
const mode = options.hook
|
|
240
|
+
? "hook"
|
|
241
|
+
: options.autoReply
|
|
242
|
+
? options.runtime === "fast"
|
|
243
|
+
? "fast-only"
|
|
244
|
+
: `auto-reply (${options.runtime})`
|
|
245
|
+
: "deliver-only";
|
|
246
|
+
process.stdout.write(`Bridge listening as '${label}' (${mode}).\n`);
|
|
247
|
+
let stopped = false;
|
|
248
|
+
const cleanup = () => {
|
|
249
|
+
if (stopped)
|
|
250
|
+
return;
|
|
251
|
+
stopped = true;
|
|
252
|
+
process.stdout.write("Bridge stopped.\n");
|
|
253
|
+
process.exit(0);
|
|
254
|
+
};
|
|
255
|
+
process.on("SIGINT", cleanup);
|
|
256
|
+
process.on("SIGTERM", cleanup);
|
|
257
|
+
while (!stopped) {
|
|
258
|
+
const result = await (0, network_1.fetchInbox)(options.networkUrl, {
|
|
259
|
+
peek: true,
|
|
260
|
+
waitSeconds: 30,
|
|
261
|
+
});
|
|
262
|
+
if (result.kind === "error") {
|
|
263
|
+
process.stderr.write(`inbox error: ${result.message}\n`);
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
for (const msg of result.messages) {
|
|
268
|
+
await handleInbound(options.networkUrl, msg, options);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
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");
|
|
5
4
|
const node_os_1 = require("node:os");
|
|
5
|
+
const bridge_1 = require("./bridge");
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const network_1 = require("./network");
|
|
8
8
|
function printHelp() {
|
|
9
9
|
const message = [
|
|
10
|
-
"marshell -
|
|
10
|
+
"marshell - agent messaging bridge",
|
|
11
11
|
"",
|
|
12
12
|
"Usage:",
|
|
13
13
|
" marshell auth set <token> [--name <name>]",
|
|
14
14
|
" marshell auth status [--json]",
|
|
15
15
|
" marshell agent join --name <name>",
|
|
16
|
-
" marshell
|
|
16
|
+
" marshell bridge run [--json] [--hook <cmd>]",
|
|
17
|
+
" marshell bridge run --auto-reply [--runtime cursor|hermes|fast] [--workspace <path>]",
|
|
18
|
+
" marshell agent run (alias for bridge run)",
|
|
17
19
|
" marshell discover [--json]",
|
|
18
20
|
" marshell send --to <name> --text \"...\" [--json]",
|
|
21
|
+
" marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]",
|
|
19
22
|
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
20
|
-
" marshell listen [--json]
|
|
23
|
+
" marshell listen [--json] (alias for bridge run)",
|
|
21
24
|
" marshell --help",
|
|
22
25
|
"",
|
|
26
|
+
"Bridge (Happy-style transport):",
|
|
27
|
+
" Keeps a persistent listener. Delivers messages instantly.",
|
|
28
|
+
" Fast path: ping→pong, hi→hi, echo:…→… (no LLM).",
|
|
29
|
+
" --auto-reply spawns LLM only for non-trivial messages (async, non-blocking).",
|
|
30
|
+
" Prefer: marshell ask --to <peer> --text \"...\" for request/response.",
|
|
31
|
+
"",
|
|
23
32
|
"Environment:",
|
|
24
33
|
" MARSHELL_NETWORK_URL default: https://network.marshell.dev",
|
|
25
34
|
" MARSHELL_AGENT_NAME default agent name for auth set",
|
|
26
|
-
" MARSHELL_WORKSPACE workspace for
|
|
35
|
+
" MARSHELL_WORKSPACE workspace for cursor auto-reply",
|
|
36
|
+
" MARSHELL_HOOK default hook command for bridge",
|
|
27
37
|
" MARSHELL_CONFIG optional path to config file",
|
|
28
38
|
].join("\n");
|
|
29
39
|
process.stdout.write(`${message}\n`);
|
|
@@ -63,6 +73,36 @@ function valueForFlag(args, flag) {
|
|
|
63
73
|
}
|
|
64
74
|
return args[index + 1];
|
|
65
75
|
}
|
|
76
|
+
function bridgeOptionsFromArgs(args) {
|
|
77
|
+
const configPromise = (0, config_1.readConfig)();
|
|
78
|
+
// sync read not available — caller must await
|
|
79
|
+
void configPromise;
|
|
80
|
+
const autoReply = hasFlag(args, "--auto-reply");
|
|
81
|
+
const json = hasFlag(args, "--json");
|
|
82
|
+
const workspace = valueForFlag(args, "--workspace") ??
|
|
83
|
+
process.env.MARSHELL_WORKSPACE ??
|
|
84
|
+
process.cwd();
|
|
85
|
+
const hook = valueForFlag(args, "--hook") ?? process.env.MARSHELL_HOOK;
|
|
86
|
+
const runtimeFlag = valueForFlag(args, "--runtime");
|
|
87
|
+
const runtime = runtimeFlag === "hermes" || runtimeFlag === "cursor" || runtimeFlag === "fast"
|
|
88
|
+
? runtimeFlag
|
|
89
|
+
: process.platform === "win32"
|
|
90
|
+
? "cursor"
|
|
91
|
+
: "hermes";
|
|
92
|
+
const timeoutRaw = valueForFlag(args, "--reply-timeout");
|
|
93
|
+
const replyTimeoutMs = timeoutRaw
|
|
94
|
+
? Math.max(5, Number(timeoutRaw)) * 1000
|
|
95
|
+
: 120_000;
|
|
96
|
+
return {
|
|
97
|
+
networkUrl: "",
|
|
98
|
+
autoReply,
|
|
99
|
+
runtime,
|
|
100
|
+
workspace,
|
|
101
|
+
hook,
|
|
102
|
+
json,
|
|
103
|
+
replyTimeoutMs,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
66
106
|
async function cmdAuthSet(args) {
|
|
67
107
|
const named = valueForFlag(args, "--name");
|
|
68
108
|
const tokenArg = args.filter((a, i) => {
|
|
@@ -99,6 +139,7 @@ async function cmdAuthStatus(args) {
|
|
|
99
139
|
const payload = {
|
|
100
140
|
hasToken: Boolean(config.token),
|
|
101
141
|
hasAgentKey: Boolean(config.agentKey),
|
|
142
|
+
agentName: config.agentName ?? null,
|
|
102
143
|
networkUrl,
|
|
103
144
|
health,
|
|
104
145
|
};
|
|
@@ -128,140 +169,30 @@ async function cmdAgentJoin(args) {
|
|
|
128
169
|
}
|
|
129
170
|
printError(result.message);
|
|
130
171
|
}
|
|
131
|
-
function
|
|
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
|
-
});
|
|
152
|
-
});
|
|
153
|
-
}
|
|
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";
|
|
172
|
+
async function cmdBridgeRun(args = []) {
|
|
223
173
|
const config = await (0, config_1.readConfig)();
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (stopped)
|
|
229
|
-
return;
|
|
230
|
-
stopped = true;
|
|
231
|
-
process.stdout.write("Exiting agent loop.\n");
|
|
232
|
-
process.exit(0);
|
|
233
|
-
};
|
|
234
|
-
process.on("SIGINT", cleanup);
|
|
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
|
-
}
|
|
174
|
+
const opts = bridgeOptionsFromArgs(args);
|
|
175
|
+
opts.networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
176
|
+
opts.agentName = config.agentName;
|
|
177
|
+
await (0, bridge_1.runBridge)(opts);
|
|
248
178
|
}
|
|
249
179
|
async function cmdDiscover(args) {
|
|
250
180
|
const json = hasFlag(args, "--json");
|
|
251
181
|
const config = await (0, config_1.readConfig)();
|
|
252
182
|
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
253
|
-
const
|
|
183
|
+
const { peers } = await (0, network_1.discoverPeers)(networkUrl);
|
|
254
184
|
if (json) {
|
|
255
|
-
printJson({ peers
|
|
185
|
+
printJson({ peers });
|
|
256
186
|
return;
|
|
257
187
|
}
|
|
258
|
-
if (
|
|
259
|
-
process.stdout.write("No peers
|
|
188
|
+
if (peers.length === 0) {
|
|
189
|
+
process.stdout.write("No peers found.\n");
|
|
260
190
|
return;
|
|
261
191
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
192
|
+
for (const peer of peers) {
|
|
193
|
+
const name = typeof peer.name === "string" ? peer.name : "?";
|
|
194
|
+
const status = typeof peer.status === "string" ? peer.status : "unknown";
|
|
195
|
+
process.stdout.write(`- ${name} (${status})\n`);
|
|
265
196
|
}
|
|
266
197
|
}
|
|
267
198
|
async function cmdSend(args) {
|
|
@@ -284,6 +215,31 @@ async function cmdSend(args) {
|
|
|
284
215
|
}
|
|
285
216
|
printError(result.message);
|
|
286
217
|
}
|
|
218
|
+
async function cmdAsk(args) {
|
|
219
|
+
const json = hasFlag(args, "--json");
|
|
220
|
+
const to = valueForFlag(args, "--to");
|
|
221
|
+
const text = valueForFlag(args, "--text");
|
|
222
|
+
const waitRaw = valueForFlag(args, "--wait");
|
|
223
|
+
const waitSeconds = waitRaw ? Number(waitRaw) : 120;
|
|
224
|
+
if (!to || !text) {
|
|
225
|
+
printError("Usage: marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]");
|
|
226
|
+
}
|
|
227
|
+
const config = await (0, config_1.readConfig)();
|
|
228
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
229
|
+
const result = await (0, network_1.askAgent)(networkUrl, to, text, waitSeconds);
|
|
230
|
+
if (json) {
|
|
231
|
+
printJson(result);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (result.kind === "ok") {
|
|
235
|
+
process.stdout.write(`${result.reply}\n`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (result.kind === "timeout") {
|
|
239
|
+
printError(`No reply from '${to}' within ${waitSeconds}s (sent id: ${result.sent_id}). Is their bridge running?`);
|
|
240
|
+
}
|
|
241
|
+
printError(result.message);
|
|
242
|
+
}
|
|
287
243
|
async function cmdInbox(args) {
|
|
288
244
|
const json = hasFlag(args, "--json");
|
|
289
245
|
const from = valueForFlag(args, "--from");
|
|
@@ -324,6 +280,9 @@ async function cmdInbox(args) {
|
|
|
324
280
|
const messages = from
|
|
325
281
|
? result.messages.filter((m) => m.from.toLowerCase() === from.toLowerCase())
|
|
326
282
|
: result.messages;
|
|
283
|
+
if (messages.length > 0) {
|
|
284
|
+
await (0, network_1.ackMessages)(networkUrl, messages.map((m) => m.id));
|
|
285
|
+
}
|
|
327
286
|
if (json) {
|
|
328
287
|
printJson({ messages, agent: result.agent });
|
|
329
288
|
return;
|
|
@@ -337,14 +296,14 @@ async function cmdInbox(args) {
|
|
|
337
296
|
}
|
|
338
297
|
}
|
|
339
298
|
async function cmdListen(args) {
|
|
340
|
-
const json = hasFlag(args, "--json");
|
|
341
299
|
const waitRaw = valueForFlag(args, "--wait");
|
|
342
300
|
const waitSeconds = waitRaw ? Number(waitRaw) : 0;
|
|
301
|
+
const json = hasFlag(args, "--json");
|
|
343
302
|
if (waitSeconds > 0) {
|
|
344
303
|
await cmdInbox(["--wait", String(waitSeconds), ...(json ? ["--json"] : [])]);
|
|
345
304
|
return;
|
|
346
305
|
}
|
|
347
|
-
await
|
|
306
|
+
await cmdBridgeRun(args);
|
|
348
307
|
}
|
|
349
308
|
async function main() {
|
|
350
309
|
const args = process.argv.slice(2);
|
|
@@ -373,11 +332,20 @@ async function main() {
|
|
|
373
332
|
return;
|
|
374
333
|
}
|
|
375
334
|
if (sub === "run") {
|
|
376
|
-
await
|
|
335
|
+
await cmdBridgeRun(rest);
|
|
377
336
|
return;
|
|
378
337
|
}
|
|
379
338
|
printError("Unknown agent command. Try: marshell agent join|run");
|
|
380
339
|
}
|
|
340
|
+
if (args[0] === "bridge") {
|
|
341
|
+
const sub = args[1];
|
|
342
|
+
const rest = args.slice(2);
|
|
343
|
+
if (sub === "run") {
|
|
344
|
+
await cmdBridgeRun(rest);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
printError("Unknown bridge command. Try: marshell bridge run");
|
|
348
|
+
}
|
|
381
349
|
if (args[0] === "discover") {
|
|
382
350
|
await cmdDiscover(args.slice(1));
|
|
383
351
|
return;
|
|
@@ -386,6 +354,10 @@ async function main() {
|
|
|
386
354
|
await cmdSend(args.slice(1));
|
|
387
355
|
return;
|
|
388
356
|
}
|
|
357
|
+
if (args[0] === "ask") {
|
|
358
|
+
await cmdAsk(args.slice(1));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
389
361
|
if (args[0] === "inbox") {
|
|
390
362
|
await cmdInbox(args.slice(1));
|
|
391
363
|
return;
|
package/dist/network.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.sendMessage = sendMessage;
|
|
|
7
7
|
exports.fetchInbox = fetchInbox;
|
|
8
8
|
exports.ackMessages = ackMessages;
|
|
9
9
|
exports.waitForInbox = waitForInbox;
|
|
10
|
+
exports.askAgent = askAgent;
|
|
10
11
|
exports.toWsUrl = toWsUrl;
|
|
11
12
|
const config_1 = require("./config");
|
|
12
13
|
function normalizeBaseUrl(raw) {
|
|
@@ -169,7 +170,14 @@ async function sendMessage(baseUrl, to, text) {
|
|
|
169
170
|
async function fetchInbox(baseUrl, options) {
|
|
170
171
|
try {
|
|
171
172
|
const headers = await authHeaders("agent");
|
|
172
|
-
const
|
|
173
|
+
const params = new URLSearchParams();
|
|
174
|
+
if (options?.peek) {
|
|
175
|
+
params.set("peek", "1");
|
|
176
|
+
}
|
|
177
|
+
if (options?.waitSeconds && options.waitSeconds > 0) {
|
|
178
|
+
params.set("wait", String(Math.min(120, options.waitSeconds)));
|
|
179
|
+
}
|
|
180
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
173
181
|
const response = await fetch(withPath(baseUrl, `/v1/messages/inbox${qs}`), {
|
|
174
182
|
method: "GET",
|
|
175
183
|
headers,
|
|
@@ -218,7 +226,11 @@ async function waitForInbox(baseUrl, options) {
|
|
|
218
226
|
const deadline = Date.now() + options.waitSeconds * 1000;
|
|
219
227
|
const from = options.from?.toLowerCase();
|
|
220
228
|
while (Date.now() < deadline) {
|
|
221
|
-
const
|
|
229
|
+
const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
|
|
230
|
+
const result = await fetchInbox(baseUrl, {
|
|
231
|
+
peek: true,
|
|
232
|
+
waitSeconds: remaining,
|
|
233
|
+
});
|
|
222
234
|
if (result.kind === "error") {
|
|
223
235
|
return result;
|
|
224
236
|
}
|
|
@@ -229,10 +241,51 @@ async function waitForInbox(baseUrl, options) {
|
|
|
229
241
|
await ackMessages(baseUrl, messages.map((m) => m.id));
|
|
230
242
|
return { kind: "ok", messages, agent: result.agent };
|
|
231
243
|
}
|
|
232
|
-
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
233
244
|
}
|
|
234
245
|
return { kind: "timeout" };
|
|
235
246
|
}
|
|
247
|
+
async function askAgent(baseUrl, to, text, waitSeconds) {
|
|
248
|
+
// Drain any stale messages from this peer before asking.
|
|
249
|
+
const stale = await fetchInbox(baseUrl, { peek: true });
|
|
250
|
+
if (stale.kind === "ok") {
|
|
251
|
+
const fromPeer = stale.messages.filter((m) => m.from.toLowerCase() === to.toLowerCase());
|
|
252
|
+
if (fromPeer.length > 0) {
|
|
253
|
+
await ackMessages(baseUrl, fromPeer.map((m) => m.id));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const sentAt = Date.now() - 2000;
|
|
257
|
+
const sent = await sendMessage(baseUrl, to, text);
|
|
258
|
+
if (sent.kind !== "sent") {
|
|
259
|
+
return { kind: "error", message: sent.message };
|
|
260
|
+
}
|
|
261
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
262
|
+
const peer = to.toLowerCase();
|
|
263
|
+
while (Date.now() < deadline) {
|
|
264
|
+
const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
|
|
265
|
+
const result = await fetchInbox(baseUrl, {
|
|
266
|
+
peek: true,
|
|
267
|
+
waitSeconds: remaining,
|
|
268
|
+
});
|
|
269
|
+
if (result.kind === "error") {
|
|
270
|
+
return { kind: "error", message: result.message };
|
|
271
|
+
}
|
|
272
|
+
const messages = result.messages.filter((m) => {
|
|
273
|
+
if (m.from.toLowerCase() !== peer)
|
|
274
|
+
return false;
|
|
275
|
+
const created = Date.parse(m.created_at);
|
|
276
|
+
return Number.isNaN(created) || created >= sentAt;
|
|
277
|
+
});
|
|
278
|
+
if (messages.length > 0) {
|
|
279
|
+
await ackMessages(baseUrl, messages.map((m) => m.id));
|
|
280
|
+
return {
|
|
281
|
+
kind: "ok",
|
|
282
|
+
reply: messages.map((m) => m.text.trim()).join("\n\n"),
|
|
283
|
+
message_id: messages[0]?.id ?? sent.id,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { kind: "timeout", sent_id: sent.id };
|
|
288
|
+
}
|
|
236
289
|
function toWsUrl(httpBase) {
|
|
237
290
|
const url = new URL(httpBase);
|
|
238
291
|
if (url.protocol === "https:") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marshell/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Marshell CLI — join a subnet, discover agents, send messages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
17
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
-
"prepare": "npm run build",
|
|
19
18
|
"prepublishOnly": "npm run build"
|
|
20
19
|
},
|
|
21
20
|
"engines": {
|