@cordfuse/llmux 0.11.0 → 0.12.1
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 +155 -75
- package/dist/index.js +3268 -78
- package/package.json +17 -4
- package/src/cli.ts +100 -0
- package/src/{client.ts → client/client.ts} +3 -3
- package/src/daemon/agents.ts +193 -0
- package/src/daemon/auth-store.ts +85 -0
- package/src/daemon/config.ts +77 -0
- package/src/daemon/handlers.ts +414 -0
- package/src/daemon/net.ts +113 -0
- package/src/daemon/state.ts +78 -0
- package/src/daemon/tmux.ts +117 -0
- package/src/daemon/token.ts +13 -0
- package/src/daemon/web/server.ts +2277 -0
- package/src/index.ts +386 -37
package/src/index.ts
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, resolve } from 'node:path';
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import { parseArgs, type ParsedArgs } from './cli.ts';
|
|
7
|
+
import * as h from './daemon/handlers.ts';
|
|
8
|
+
import * as state from './daemon/state.ts';
|
|
9
|
+
import * as tmux from './daemon/tmux.ts';
|
|
10
|
+
import * as authStore from './daemon/auth-store.ts';
|
|
11
|
+
import { DEFAULT_AGENTS, isAgentInstalled } from './daemon/agents.ts';
|
|
12
|
+
import { clientCommands } from './client/client.ts';
|
|
13
|
+
|
|
14
|
+
// ----------------- version helper -----------------
|
|
6
15
|
|
|
7
16
|
function readVersion(): string {
|
|
8
17
|
try {
|
|
@@ -18,61 +27,401 @@ function readVersion(): string {
|
|
|
18
27
|
}
|
|
19
28
|
const VERSION = readVersion();
|
|
20
29
|
|
|
30
|
+
// ----------------- argv shape -----------------
|
|
31
|
+
|
|
32
|
+
interface GlobalEnv {
|
|
33
|
+
/** When set, session/agent verbs go over HTTP to this daemon. */
|
|
34
|
+
server?: string;
|
|
35
|
+
/** SAS token for remote auth. */
|
|
36
|
+
token?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip global flags (`--server`, `--token`, `--help`, `--version`) and their
|
|
41
|
+
* values from the head of argv. Returns the remaining tokens plus the
|
|
42
|
+
* extracted env, so the per-noun routers see clean positional args.
|
|
43
|
+
*
|
|
44
|
+
* Global flag values can appear anywhere — operators tend to put them at the
|
|
45
|
+
* end (`llmux session list --server http://…`) or the beginning. We sweep
|
|
46
|
+
* both passes.
|
|
47
|
+
*/
|
|
48
|
+
function stripGlobals(argv: readonly string[]): { rest: string[]; env: GlobalEnv; help: boolean; version: boolean } {
|
|
49
|
+
const env: GlobalEnv = {};
|
|
50
|
+
const rest: string[] = [];
|
|
51
|
+
let help = false;
|
|
52
|
+
let version = false;
|
|
53
|
+
if (process.env.LLMUX_SERVER) env.server = process.env.LLMUX_SERVER;
|
|
54
|
+
if (process.env.LLMUX_TOKEN) env.token = process.env.LLMUX_TOKEN;
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const t = argv[i]!;
|
|
57
|
+
if (t === '--server') {
|
|
58
|
+
const next = argv[i + 1];
|
|
59
|
+
if (next === undefined || next.startsWith('-')) throw new Error('--server requires a URL');
|
|
60
|
+
env.server = next;
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (t.startsWith('--server=')) {
|
|
65
|
+
env.server = t.slice('--server='.length);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (t === '--token') {
|
|
69
|
+
const next = argv[i + 1];
|
|
70
|
+
if (next === undefined || next.startsWith('-')) throw new Error('--token requires a value');
|
|
71
|
+
env.token = next;
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (t.startsWith('--token=')) {
|
|
76
|
+
env.token = t.slice('--token='.length);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (t === '--help' || t === '-h') {
|
|
80
|
+
help = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (t === '--version' || t === '-v') {
|
|
84
|
+
version = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
rest.push(t);
|
|
88
|
+
}
|
|
89
|
+
return { rest, env, help, version };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Map our positional argv → the legacy ParsedArgs shape so we can re-use the
|
|
93
|
+
// existing daemon handlers without rewriting them.
|
|
94
|
+
function asParsedArgs(positional: string[], flags: Record<string, string | boolean> = {}): ParsedArgs {
|
|
95
|
+
return { positional, flags };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ----------------- help -----------------
|
|
99
|
+
|
|
21
100
|
function printRootHelp(): void {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
101
|
+
console.log(
|
|
102
|
+
`llmux v${VERSION} — tmux-based AI agent dispatcher (daemon + client in one binary)
|
|
103
|
+
|
|
104
|
+
Usage:
|
|
105
|
+
llmux <noun> <verb> [args] [--server <url>] [--token <sas>]
|
|
106
|
+
|
|
107
|
+
Session verbs (local by default; pass --server <url> to target a remote daemon):
|
|
108
|
+
session list list tracked sessions
|
|
109
|
+
session start <agent> [--name N] [--cwd P] spawn a new agent in tmux
|
|
110
|
+
[--flags "F"] [--env "K=V"] [--resume-from <id>]
|
|
111
|
+
session stop <name> kill + forget the session
|
|
112
|
+
session restart <name> kill + relaunch with persisted config
|
|
113
|
+
session attach <name> open the terminal (tmux locally, WS remotely)
|
|
114
|
+
session prompt <name> "<text>" [--no-enter] send a prompt
|
|
115
|
+
session broadcast <agent> "<text>" send to every session of an agent type (local)
|
|
116
|
+
session resume <name> --conversation <id> | --latest
|
|
117
|
+
rebind to a past agent conversation
|
|
118
|
+
session history <name> list past conversations for the session's cwd
|
|
119
|
+
|
|
120
|
+
Server verbs (always local):
|
|
121
|
+
server start [--port N] [--no-qr] run the HTTP/WS daemon (formerly: llmuxd serve)
|
|
122
|
+
|
|
123
|
+
Token verbs (always local — managing the daemon-host's auth store):
|
|
124
|
+
token create [--name N] [--expiry ISO] [--qr] [--qr-endpoint <label>]
|
|
125
|
+
token list show active tokens
|
|
126
|
+
token revoke <id> revoke a token by id
|
|
127
|
+
|
|
128
|
+
Agent verbs:
|
|
129
|
+
agent list [--all] [--installed] [--json] list agents (default: installed-only)
|
|
130
|
+
|
|
131
|
+
Global flags:
|
|
132
|
+
--server <url> route session/agent verbs to a remote daemon over HTTP
|
|
133
|
+
--token <sas> SAS token for remote auth (LLMUX_TOKEN env fallback)
|
|
134
|
+
--help / -h print this help
|
|
135
|
+
--version / -v print version
|
|
136
|
+
|
|
137
|
+
Environment:
|
|
138
|
+
LLMUX_SERVER default --server URL
|
|
139
|
+
LLMUX_TOKEN default --token value`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function printVerbHelp(noun: string, verb: string | undefined): void {
|
|
144
|
+
// Light help; full help is in the root.
|
|
145
|
+
if (!verb) {
|
|
146
|
+
console.log(`llmux ${noun} — see \`llmux --help\` for verbs under this noun`);
|
|
147
|
+
return;
|
|
34
148
|
}
|
|
35
|
-
|
|
36
|
-
lines.push('Environment:');
|
|
37
|
-
lines.push(' LLMUX_SERVER Base URL of llmuxd (e.g. http://host:3000)');
|
|
38
|
-
lines.push(' LLMUX_TOKEN SAS token for authenticated requests');
|
|
39
|
-
lines.push('');
|
|
40
|
-
lines.push('Run `llmux <command> --help` for command-specific options.');
|
|
41
|
-
console.log(lines.join('\n'));
|
|
149
|
+
console.log(`llmux ${noun} ${verb} — see \`llmux --help\` for usage`);
|
|
42
150
|
}
|
|
43
151
|
|
|
44
|
-
|
|
45
|
-
const argv = process.argv.slice(2);
|
|
152
|
+
// ----------------- dispatchers -----------------
|
|
46
153
|
|
|
47
|
-
|
|
48
|
-
|
|
154
|
+
async function dispatchSession(verb: string | undefined, args: string[], env: GlobalEnv): Promise<void> {
|
|
155
|
+
if (!verb) {
|
|
156
|
+
printVerbHelp('session', verb);
|
|
49
157
|
return;
|
|
50
158
|
}
|
|
51
|
-
|
|
52
|
-
|
|
159
|
+
// Backward-compat aliases
|
|
160
|
+
const v = verb === 'ls' ? 'list' : verb === 'send' ? 'prompt' : verb === 'spawn' ? 'start' : verb === 'kill' ? 'stop' : verb === 'respawn' ? 'restart' : verb === 'conversations' ? 'history' : verb;
|
|
161
|
+
|
|
162
|
+
if (env.server !== undefined) {
|
|
163
|
+
// Remote — delegate to the existing client command map.
|
|
164
|
+
const cmdMap: Record<string, string> = {
|
|
165
|
+
list: 'ls',
|
|
166
|
+
start: 'spawn',
|
|
167
|
+
stop: 'kill',
|
|
168
|
+
restart: 'restart',
|
|
169
|
+
attach: 'attach',
|
|
170
|
+
prompt: 'send',
|
|
171
|
+
resume: 'resume',
|
|
172
|
+
history: 'conversations',
|
|
173
|
+
};
|
|
174
|
+
const clientCmd = cmdMap[v];
|
|
175
|
+
if (!clientCmd) throw new Error(`session ${v}: no remote equivalent`);
|
|
176
|
+
process.env.LLMUX_SERVER = env.server;
|
|
177
|
+
if (env.token) process.env.LLMUX_TOKEN = env.token;
|
|
178
|
+
const cmd = clientCommands[clientCmd];
|
|
179
|
+
if (!cmd) throw new Error(`internal: client command "${clientCmd}" missing`);
|
|
180
|
+
await cmd.run(args);
|
|
53
181
|
return;
|
|
54
182
|
}
|
|
55
183
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
184
|
+
// Local — call daemon handlers directly.
|
|
185
|
+
const parsed = parseArgs(args, sessionLocalFlags());
|
|
186
|
+
switch (v) {
|
|
187
|
+
case 'list':
|
|
188
|
+
h.handleStatus(parsed);
|
|
189
|
+
return;
|
|
190
|
+
case 'start':
|
|
191
|
+
h.handleSpawn(parsed);
|
|
192
|
+
return;
|
|
193
|
+
case 'stop':
|
|
194
|
+
h.handleKill(parsed);
|
|
195
|
+
return;
|
|
196
|
+
case 'restart':
|
|
197
|
+
h.handleRespawn(parsed);
|
|
198
|
+
return;
|
|
199
|
+
case 'attach':
|
|
200
|
+
h.handleChat(parsed);
|
|
201
|
+
return;
|
|
202
|
+
case 'prompt':
|
|
203
|
+
h.handleSend(parsed);
|
|
204
|
+
return;
|
|
205
|
+
case 'broadcast':
|
|
206
|
+
h.handleBroadcast(parsed);
|
|
207
|
+
return;
|
|
208
|
+
case 'resume': {
|
|
209
|
+
const name = parsed.positional[0];
|
|
210
|
+
if (!name) throw new Error('session resume requires <name>');
|
|
211
|
+
const session = state.get(name);
|
|
212
|
+
if (!session) throw new Error(`no tracked session "${name}"`);
|
|
213
|
+
const agent = DEFAULT_AGENTS[session.agent];
|
|
214
|
+
if (!agent?.history) throw new Error(`agent "${session.agent}" has no history adapter`);
|
|
215
|
+
if (!isAgentInstalled(agent)) throw new Error(`agent "${session.agent}" is not installed`);
|
|
216
|
+
let conversationId = parsed.flags.conversation as string | undefined;
|
|
217
|
+
if (!conversationId) {
|
|
218
|
+
if (!parsed.flags.latest) throw new Error('resume requires --conversation <id> or --latest');
|
|
219
|
+
const convs = agent.history.listConversations(session.cwd);
|
|
220
|
+
if (convs.length === 0) throw new Error(`no past conversations for ${name}`);
|
|
221
|
+
conversationId = convs[0]!.id;
|
|
222
|
+
}
|
|
223
|
+
if (tmux.hasSession(name)) tmux.killSession(name);
|
|
224
|
+
const cmd = `${agent.cmd} ${agent.flags ?? ''} ${agent.history.resumeFlag(conversationId)}`.trim();
|
|
225
|
+
tmux.newSession({
|
|
226
|
+
name,
|
|
227
|
+
command: cmd,
|
|
228
|
+
cwd: session.cwd,
|
|
229
|
+
env: { ...(agent.envDefaults ?? {}), ...(session.env ?? {}), LLMUX_SESSION: name, LLMUX_AGENT: session.agent },
|
|
230
|
+
});
|
|
231
|
+
state.record({ ...session, resumeFrom: conversationId, createdAt: new Date().toISOString() });
|
|
232
|
+
console.log(`${name} resumed from ${conversationId.slice(0, 8)}…`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
case 'history': {
|
|
236
|
+
const name = parsed.positional[0];
|
|
237
|
+
if (!name) throw new Error('session history requires <name>');
|
|
238
|
+
const session = state.get(name);
|
|
239
|
+
if (!session) throw new Error(`no tracked session "${name}"`);
|
|
240
|
+
const agent = DEFAULT_AGENTS[session.agent];
|
|
241
|
+
if (!agent?.history) {
|
|
242
|
+
console.log('agent has no history adapter');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const convs = agent.history.listConversations(session.cwd);
|
|
246
|
+
if (convs.length === 0) {
|
|
247
|
+
console.log('no past conversations');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
for (const c of convs) console.log(`${c.id.slice(0, 8)}… ${c.messageCount.toString().padStart(5)} ${c.title.slice(0, 80)}`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
default:
|
|
254
|
+
throw new Error(`unknown session verb "${v}"`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
59
257
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
258
|
+
function sessionLocalFlags() {
|
|
259
|
+
return {
|
|
260
|
+
name: { kind: 'string' as const, description: 'session name' },
|
|
261
|
+
cwd: { kind: 'string' as const, description: 'working directory' },
|
|
262
|
+
flags: { kind: 'string' as const, description: 'launch flags override' },
|
|
263
|
+
env: { kind: 'string' as const, description: 'env vars (KEY=VAL one per line)' },
|
|
264
|
+
prefix: { kind: 'string' as const, description: 'session-name prefix (start only)' },
|
|
265
|
+
cascade: { kind: 'boolean' as const, description: 'cascade kill to children' },
|
|
266
|
+
conversation: { kind: 'string' as const, description: 'conversation id (resume)' },
|
|
267
|
+
latest: { kind: 'boolean' as const, description: 'resume the most recent conversation' },
|
|
268
|
+
'no-enter': { kind: 'boolean' as const, description: 'do not append Enter to prompt' },
|
|
269
|
+
browser: { kind: 'boolean' as const, description: 'open in web browser (attach)' },
|
|
270
|
+
it: { kind: 'boolean' as const, description: 'interactive (attach)' },
|
|
271
|
+
json: { kind: 'boolean' as const, description: 'emit JSON' },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function dispatchServer(verb: string | undefined, args: string[]): Promise<void> {
|
|
276
|
+
if (!verb) {
|
|
277
|
+
printVerbHelp('server', verb);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const parsed = parseArgs(args, {
|
|
281
|
+
config: { kind: 'string', description: 'Path to .llmux.yaml' },
|
|
282
|
+
port: { kind: 'string', description: 'Listen port' },
|
|
283
|
+
'no-qr': { kind: 'boolean', description: 'Suppress QR codes' },
|
|
284
|
+
});
|
|
285
|
+
switch (verb) {
|
|
286
|
+
case 'start':
|
|
287
|
+
case 'serve':
|
|
288
|
+
await h.handleServe(parsed);
|
|
289
|
+
return;
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`unknown server verb "${verb}"`);
|
|
64
292
|
}
|
|
293
|
+
}
|
|
65
294
|
|
|
66
|
-
|
|
67
|
-
|
|
295
|
+
async function dispatchToken(verb: string | undefined, args: string[]): Promise<void> {
|
|
296
|
+
if (!verb) {
|
|
297
|
+
printVerbHelp('token', verb);
|
|
68
298
|
return;
|
|
69
299
|
}
|
|
300
|
+
const parsed = parseArgs(args, {
|
|
301
|
+
name: { kind: 'string', description: 'token label' },
|
|
302
|
+
expiry: { kind: 'string', description: 'ISO-8601 expiry' },
|
|
303
|
+
qr: { kind: 'boolean', description: 'render QR for first-tap login' },
|
|
304
|
+
'qr-endpoint': { kind: 'string', description: 'endpoint label or URL for QR target' },
|
|
305
|
+
json: { kind: 'boolean', description: 'emit JSON' },
|
|
306
|
+
});
|
|
307
|
+
switch (verb) {
|
|
308
|
+
case 'create':
|
|
309
|
+
await h.handleTokenCreate(parsed);
|
|
310
|
+
return;
|
|
311
|
+
case 'list':
|
|
312
|
+
case 'show':
|
|
313
|
+
h.handleTokenShow(parsed);
|
|
314
|
+
return;
|
|
315
|
+
case 'revoke':
|
|
316
|
+
h.handleTokenRevoke(parsed);
|
|
317
|
+
return;
|
|
318
|
+
default:
|
|
319
|
+
throw new Error(`unknown token verb "${verb}"`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function dispatchAgent(verb: string | undefined, args: string[], env: GlobalEnv): Promise<void> {
|
|
324
|
+
if (!verb) {
|
|
325
|
+
printVerbHelp('agent', verb);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Remote agent verbs route through the client.
|
|
329
|
+
if (env.server !== undefined && verb === 'list') {
|
|
330
|
+
process.env.LLMUX_SERVER = env.server;
|
|
331
|
+
if (env.token) process.env.LLMUX_TOKEN = env.token;
|
|
332
|
+
const cmd = clientCommands['agents'];
|
|
333
|
+
if (!cmd) throw new Error('internal: client agents command missing');
|
|
334
|
+
await cmd.run(args);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const parsed = parseArgs(args, {
|
|
338
|
+
all: { kind: 'boolean', description: 'include not-installed agents' },
|
|
339
|
+
installed: { kind: 'boolean', description: 'only installed agents (default)' },
|
|
340
|
+
json: { kind: 'boolean', description: 'emit JSON' },
|
|
341
|
+
});
|
|
342
|
+
switch (verb) {
|
|
343
|
+
case 'list': {
|
|
344
|
+
const showAll = Boolean(parsed.flags.all);
|
|
345
|
+
const rows = Object.values(DEFAULT_AGENTS)
|
|
346
|
+
.filter((d) => showAll || isAgentInstalled(d))
|
|
347
|
+
.map((d) => ({ key: d.key, displayName: d.displayName, cmd: d.cmd, flags: d.flags ?? '', installed: isAgentInstalled(d) }));
|
|
348
|
+
if (parsed.flags.json) {
|
|
349
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
for (const r of rows) {
|
|
353
|
+
console.log(`${r.key.padEnd(10)} ${r.displayName.padEnd(24)} ${r.installed ? 'installed' : 'not installed'} ${r.flags || '-'}`);
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
throw new Error(`unknown agent verb "${verb}"`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ----------------- main -----------------
|
|
363
|
+
|
|
364
|
+
async function main(): Promise<void> {
|
|
365
|
+
const { rest, env, help, version } = stripGlobals(process.argv.slice(2));
|
|
366
|
+
|
|
367
|
+
if (version) {
|
|
368
|
+
console.log(VERSION);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (rest.length === 0 || help) {
|
|
372
|
+
printRootHelp();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const noun = rest[0]!;
|
|
377
|
+
const verb = rest[1];
|
|
378
|
+
const remainder = rest.slice(2);
|
|
70
379
|
|
|
71
380
|
try {
|
|
72
|
-
|
|
381
|
+
switch (noun) {
|
|
382
|
+
case 'session':
|
|
383
|
+
await dispatchSession(verb, remainder, env);
|
|
384
|
+
return;
|
|
385
|
+
case 'server':
|
|
386
|
+
await dispatchServer(verb, remainder);
|
|
387
|
+
return;
|
|
388
|
+
case 'token':
|
|
389
|
+
await dispatchToken(verb, remainder);
|
|
390
|
+
return;
|
|
391
|
+
case 'agent':
|
|
392
|
+
await dispatchAgent(verb, remainder, env);
|
|
393
|
+
return;
|
|
394
|
+
// Backward-compat shorthand — some shells will already have `llmuxd serve`
|
|
395
|
+
// wired up. These verbs sit at noun-position so all of rest.slice(1) is
|
|
396
|
+
// their args, not just slice(2).
|
|
397
|
+
case 'serve':
|
|
398
|
+
await dispatchServer('start', rest.slice(1));
|
|
399
|
+
return;
|
|
400
|
+
case 'ls':
|
|
401
|
+
case 'status':
|
|
402
|
+
await dispatchSession('list', rest.slice(1), env);
|
|
403
|
+
return;
|
|
404
|
+
case 'help':
|
|
405
|
+
printRootHelp();
|
|
406
|
+
return;
|
|
407
|
+
default: {
|
|
408
|
+
// Treat anything we don't recognise as a client command (e.g. legacy
|
|
409
|
+
// `llmux send`, `llmux spawn`). The client module knows about them.
|
|
410
|
+
const cmd = clientCommands[noun];
|
|
411
|
+
if (cmd) {
|
|
412
|
+
if (env.server) process.env.LLMUX_SERVER = env.server;
|
|
413
|
+
if (env.token) process.env.LLMUX_TOKEN = env.token;
|
|
414
|
+
await cmd.run([verb!, ...remainder].filter((x) => x !== undefined));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
console.error(`llmux: unknown command "${noun}"`);
|
|
418
|
+
console.error('Run `llmux --help` to see the noun-prefix surface.');
|
|
419
|
+
process.exit(64);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
73
422
|
} catch (err) {
|
|
74
423
|
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
-
console.error(`llmux
|
|
424
|
+
console.error(`llmux: ${msg}`);
|
|
76
425
|
process.exit(1);
|
|
77
426
|
}
|
|
78
427
|
}
|