@cydm/magic-shell 0.1.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/cli.d.ts +2 -0
- package/dist/cli.js +592 -0
- package/dist/node-start.d.ts +11 -0
- package/dist/node-start.js +141 -0
- package/package.json +34 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { MagicShellControlClient } from "@cydm/sdk";
|
|
3
|
+
function parseArgs(argv) {
|
|
4
|
+
const options = {
|
|
5
|
+
command: undefined,
|
|
6
|
+
relayUrl: process.env.MAGIC_SHELL_RELAY_URL || "ws://localhost:8080",
|
|
7
|
+
nodeId: process.env.MAGIC_SHELL_NODE_ID || "test-node",
|
|
8
|
+
password: process.env.MAGIC_SHELL_PASSWORD || "123456",
|
|
9
|
+
json: false,
|
|
10
|
+
timeoutMs: 8000,
|
|
11
|
+
help: false,
|
|
12
|
+
debug: false,
|
|
13
|
+
intervalMs: 2000,
|
|
14
|
+
pluginName: "pie",
|
|
15
|
+
autoReconnect: true,
|
|
16
|
+
primaryPluginName: "pie",
|
|
17
|
+
};
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const arg = argv[index];
|
|
20
|
+
if (!arg)
|
|
21
|
+
continue;
|
|
22
|
+
if (!arg.startsWith("-") && !options.command) {
|
|
23
|
+
if (arg === "primary" || arg === "primary-send" || arg === "status" || arg === "workers" || arg === "worker" || arg === "logs" || arg === "watch" || arg === "browse" || arg === "spawn" || arg === "attach" || arg === "input" || arg === "session-message" || arg === "session-summary" || arg === "session-send" || arg === "session-turn" || arg === "stop" || arg === "restart" || arg === "node") {
|
|
24
|
+
options.command = arg;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unknown command: ${arg}`);
|
|
28
|
+
}
|
|
29
|
+
if (!arg.startsWith("-") && options.command === "node" && !options.nodeCommand) {
|
|
30
|
+
if (arg === "start") {
|
|
31
|
+
options.nodeCommand = arg;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Unknown node command: ${arg}`);
|
|
35
|
+
}
|
|
36
|
+
switch (arg) {
|
|
37
|
+
case "--relay":
|
|
38
|
+
options.relayUrl = readRequiredValue(argv, ++index, arg);
|
|
39
|
+
break;
|
|
40
|
+
case "--node-id":
|
|
41
|
+
options.nodeId = readRequiredValue(argv, ++index, arg);
|
|
42
|
+
break;
|
|
43
|
+
case "--password":
|
|
44
|
+
options.password = readRequiredValue(argv, ++index, arg);
|
|
45
|
+
break;
|
|
46
|
+
case "--timeout-ms":
|
|
47
|
+
options.timeoutMs = Number(readRequiredValue(argv, ++index, arg));
|
|
48
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
49
|
+
throw new Error(`Invalid --timeout-ms value: ${argv[index]}`);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "--interval-ms":
|
|
53
|
+
options.intervalMs = Number(readRequiredValue(argv, ++index, arg));
|
|
54
|
+
if (!Number.isFinite(options.intervalMs) || options.intervalMs <= 0) {
|
|
55
|
+
throw new Error(`Invalid --interval-ms value: ${argv[index]}`);
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case "--json":
|
|
59
|
+
options.json = true;
|
|
60
|
+
break;
|
|
61
|
+
case "--session":
|
|
62
|
+
options.sessionId = readRequiredValue(argv, ++index, arg);
|
|
63
|
+
break;
|
|
64
|
+
case "--plugin":
|
|
65
|
+
options.pluginName = readRequiredValue(argv, ++index, arg);
|
|
66
|
+
break;
|
|
67
|
+
case "--primary-plugin":
|
|
68
|
+
options.primaryPluginName = readRequiredValue(argv, ++index, arg);
|
|
69
|
+
break;
|
|
70
|
+
case "--task":
|
|
71
|
+
options.taskSummary = readRequiredValue(argv, ++index, arg);
|
|
72
|
+
break;
|
|
73
|
+
case "--name":
|
|
74
|
+
options.displayName = readRequiredValue(argv, ++index, arg);
|
|
75
|
+
break;
|
|
76
|
+
case "--cwd":
|
|
77
|
+
options.cwd = readRequiredValue(argv, ++index, arg);
|
|
78
|
+
break;
|
|
79
|
+
case "--path":
|
|
80
|
+
options.path = readRequiredValue(argv, ++index, arg);
|
|
81
|
+
break;
|
|
82
|
+
case "--data":
|
|
83
|
+
options.data = readRequiredValue(argv, ++index, arg);
|
|
84
|
+
break;
|
|
85
|
+
case "--mode": {
|
|
86
|
+
const mode = readRequiredValue(argv, ++index, arg);
|
|
87
|
+
if (mode !== "steer" && mode !== "follow_up") {
|
|
88
|
+
throw new Error(`Invalid --mode value: ${mode}`);
|
|
89
|
+
}
|
|
90
|
+
options.mode = mode;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "--poll-interval-ms": {
|
|
94
|
+
const pollIntervalMs = Number(readRequiredValue(argv, ++index, arg));
|
|
95
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
|
|
96
|
+
throw new Error(`Invalid --poll-interval-ms value: ${argv[index]}`);
|
|
97
|
+
}
|
|
98
|
+
options.pollIntervalMs = pollIntervalMs;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "--local-control-port": {
|
|
102
|
+
const localControlPort = Number(readRequiredValue(argv, ++index, arg));
|
|
103
|
+
if (!Number.isFinite(localControlPort) || localControlPort <= 0) {
|
|
104
|
+
throw new Error(`Invalid --local-control-port value: ${argv[index]}`);
|
|
105
|
+
}
|
|
106
|
+
options.localControlPort = localControlPort;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "--local-bind":
|
|
110
|
+
options.localBind = readRequiredValue(argv, ++index, arg);
|
|
111
|
+
break;
|
|
112
|
+
case "--disable-local-direct":
|
|
113
|
+
options.disableLocalDirect = true;
|
|
114
|
+
break;
|
|
115
|
+
case "--no-reconnect":
|
|
116
|
+
options.autoReconnect = false;
|
|
117
|
+
break;
|
|
118
|
+
case "--debug":
|
|
119
|
+
options.debug = true;
|
|
120
|
+
break;
|
|
121
|
+
case "--help":
|
|
122
|
+
case "-h":
|
|
123
|
+
options.help = true;
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return options;
|
|
130
|
+
}
|
|
131
|
+
function readRequiredValue(argv, index, flag) {
|
|
132
|
+
const value = argv[index];
|
|
133
|
+
if (!value) {
|
|
134
|
+
throw new Error(`Missing value for ${flag}`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
function printHelp() {
|
|
139
|
+
console.log(`Magic Shell CLI
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
magic-shell <command> [options]
|
|
143
|
+
|
|
144
|
+
Commands:
|
|
145
|
+
node start Start a local agent node and print connect URLs
|
|
146
|
+
primary Show minimal primary-agent status
|
|
147
|
+
primary-send Send a message to the primary agent
|
|
148
|
+
status Show runtime summary and primary-agent status
|
|
149
|
+
workers List workers in the runtime
|
|
150
|
+
worker Show worker detail for a session
|
|
151
|
+
logs Show buffered output for a worker session
|
|
152
|
+
watch Poll runtime status or workers continuously
|
|
153
|
+
browse List directories for a path
|
|
154
|
+
spawn Start a worker
|
|
155
|
+
attach Attach the current client to a worker session
|
|
156
|
+
input Send input to a worker session
|
|
157
|
+
session-message Read the latest meaningful message from a worker session
|
|
158
|
+
session-summary Read a short summary of a worker session's recent output
|
|
159
|
+
session-send Send a structured message to a worker session
|
|
160
|
+
session-turn Send a structured message and wait for a fresh worker reply/summary
|
|
161
|
+
stop Stop a worker
|
|
162
|
+
restart Restart a worker
|
|
163
|
+
|
|
164
|
+
Options:
|
|
165
|
+
--relay <url> Relay WebSocket URL (default: ws://localhost:8080)
|
|
166
|
+
--node-id <id> Target node ID (default: test-node)
|
|
167
|
+
--password <pwd> Node password (default: 123456)
|
|
168
|
+
--session <id> Target session ID for attach/stop/restart
|
|
169
|
+
--plugin <name> Plugin name for spawn (default: pie)
|
|
170
|
+
--primary-plugin <name> Primary plugin for node start (default: pie)
|
|
171
|
+
--task <text> Task summary for spawn
|
|
172
|
+
--name <text> Display name for spawn
|
|
173
|
+
--cwd <path> Working directory for spawn
|
|
174
|
+
--path <path> Directory path for browse
|
|
175
|
+
--data <text> Input text for the input command
|
|
176
|
+
--mode <kind> Session-send mode: steer | follow_up
|
|
177
|
+
--local-control-port <n> Local direct workbench port for node start
|
|
178
|
+
--local-bind <host> Local direct bind host for node start
|
|
179
|
+
--disable-local-direct Disable the local direct workbench server
|
|
180
|
+
--no-reconnect Disable relay auto-reconnect for node start
|
|
181
|
+
--poll-interval-ms <n> Poll interval for session-turn waiting
|
|
182
|
+
--timeout-ms <n> Request timeout in milliseconds (default: 8000)
|
|
183
|
+
--interval-ms <n> Poll interval for watch in milliseconds (default: 2000)
|
|
184
|
+
--json Print machine-friendly JSON
|
|
185
|
+
--debug Print connection/debug logs
|
|
186
|
+
--help, -h Show help
|
|
187
|
+
|
|
188
|
+
Environment:
|
|
189
|
+
MAGIC_SHELL_RELAY
|
|
190
|
+
MAGIC_SHELL_RELAY_URL
|
|
191
|
+
MAGIC_SHELL_NODE_ID
|
|
192
|
+
MAGIC_SHELL_PASSWORD
|
|
193
|
+
MAGIC_SHELL_PRIMARY_PLUGIN
|
|
194
|
+
MAGIC_SHELL_LOCAL_CONTROL_HOST
|
|
195
|
+
MAGIC_SHELL_LOCAL_CONTROL_PORT
|
|
196
|
+
MAGIC_SHELL_DISABLE_LOCAL_DIRECT`);
|
|
197
|
+
}
|
|
198
|
+
function printStatus(snapshot, json) {
|
|
199
|
+
if (json) {
|
|
200
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const runtime = snapshot.runtime;
|
|
204
|
+
const primary = snapshot.primary;
|
|
205
|
+
const focus = snapshot.focus || [];
|
|
206
|
+
const workers = snapshot.workers || [];
|
|
207
|
+
console.log(`Node: ${snapshot.nodeId || workers[0]?.nodeId || "unknown"}`);
|
|
208
|
+
console.log(`Primary: ${formatPrimary(primary)}`);
|
|
209
|
+
console.log(`Workers: ${runtime?.liveWorkers ?? workers.filter(isLiveWorker).length} live / ${runtime?.totalWorkers ?? workers.length} total`);
|
|
210
|
+
console.log(`Attention: ${runtime?.attentionWorkers ?? 0} need attention`);
|
|
211
|
+
console.log(`Busy: ${runtime?.busyWorkers ?? 0} | Waiting: ${runtime?.waitingWorkers ?? 0} | Failed: ${runtime?.failedWorkers ?? 0}`);
|
|
212
|
+
if (focus.length > 0) {
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log("Focus:");
|
|
215
|
+
for (const item of focus) {
|
|
216
|
+
const coordination = item.coordinationState ? ` ${item.coordinationState}` : "";
|
|
217
|
+
console.log(`- ${item.title} [${item.agentType}]${coordination} ${item.recommendedAction || "none"}${item.reason ? `: ${item.reason}` : ""}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function printWorkers(snapshot, json) {
|
|
222
|
+
const workers = snapshot.workers || [];
|
|
223
|
+
if (json) {
|
|
224
|
+
console.log(JSON.stringify(workers, null, 2));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (workers.length === 0) {
|
|
228
|
+
console.log("No workers");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const worker of workers) {
|
|
232
|
+
const statusBits = [
|
|
233
|
+
worker.status,
|
|
234
|
+
worker.coordinationState,
|
|
235
|
+
worker.activityState,
|
|
236
|
+
worker.phase,
|
|
237
|
+
].filter(Boolean).join(" / ");
|
|
238
|
+
console.log(`${worker.sessionId} ${worker.displayName || worker.agentType} ${statusBits}`);
|
|
239
|
+
console.log(` task: ${worker.taskSummary || "(untitled)"}`);
|
|
240
|
+
console.log(` cwd: ${worker.cwd}`);
|
|
241
|
+
if (worker.recommendedAction && worker.recommendedAction !== "none") {
|
|
242
|
+
console.log(` action: ${worker.recommendedAction}${worker.recommendedActionReason ? ` (${worker.recommendedActionReason})` : ""}`);
|
|
243
|
+
}
|
|
244
|
+
if (worker.lastError) {
|
|
245
|
+
console.log(` error: ${worker.lastError}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function printWorkerDetail(snapshot, json) {
|
|
250
|
+
if (json) {
|
|
251
|
+
console.log(JSON.stringify(snapshot.worker || null, null, 2));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const detail = snapshot.worker;
|
|
255
|
+
if (!detail) {
|
|
256
|
+
console.log("Worker detail unavailable");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const worker = detail.worker;
|
|
260
|
+
console.log(`${worker.sessionId} ${worker.displayName || worker.agentType}`);
|
|
261
|
+
console.log(`Status: ${[worker.status, worker.coordinationState, worker.activityState, worker.phase].filter(Boolean).join(" / ")}`);
|
|
262
|
+
console.log(`Task: ${worker.taskSummary || "(untitled)"}`);
|
|
263
|
+
if (worker.coordinationTaskSummary) {
|
|
264
|
+
console.log(`Delegated: ${worker.coordinationTaskSummary}`);
|
|
265
|
+
}
|
|
266
|
+
console.log(`Cwd: ${worker.cwd}`);
|
|
267
|
+
if (worker.lastError) {
|
|
268
|
+
console.log(`Error: ${worker.lastError}`);
|
|
269
|
+
}
|
|
270
|
+
if (detail.lastBufferedOutput) {
|
|
271
|
+
console.log("");
|
|
272
|
+
console.log("Buffered output:");
|
|
273
|
+
console.log(detail.lastBufferedOutput);
|
|
274
|
+
}
|
|
275
|
+
if (detail.recentEvents.length > 0) {
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log("Recent events:");
|
|
278
|
+
for (const event of detail.recentEvents.slice(-8)) {
|
|
279
|
+
console.log(`- ${new Date(event.timestamp).toLocaleTimeString()}: ${event.type} - ${event.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function printWorkerLogs(snapshot, json) {
|
|
284
|
+
const detail = snapshot.worker;
|
|
285
|
+
if (json) {
|
|
286
|
+
console.log(JSON.stringify({
|
|
287
|
+
sessionId: detail?.worker.sessionId || null,
|
|
288
|
+
lastBufferedOutput: detail?.lastBufferedOutput || "",
|
|
289
|
+
}, null, 2));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!detail) {
|
|
293
|
+
console.log("Worker detail unavailable");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!detail.lastBufferedOutput) {
|
|
297
|
+
console.log(`No buffered output for ${detail.worker.sessionId}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
console.log(detail.lastBufferedOutput);
|
|
301
|
+
}
|
|
302
|
+
function printDirectoryList(snapshot, json) {
|
|
303
|
+
if (json) {
|
|
304
|
+
console.log(JSON.stringify({
|
|
305
|
+
path: snapshot.path || null,
|
|
306
|
+
parentPath: snapshot.parentPath || null,
|
|
307
|
+
repoRoot: snapshot.repoRoot || null,
|
|
308
|
+
entries: snapshot.entries || [],
|
|
309
|
+
}, null, 2));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.log(`Path: ${snapshot.path || "(unknown)"}`);
|
|
313
|
+
if (snapshot.repoRoot) {
|
|
314
|
+
console.log(`Repo root: ${snapshot.repoRoot}`);
|
|
315
|
+
}
|
|
316
|
+
if ((snapshot.entries || []).length === 0) {
|
|
317
|
+
console.log("No directories");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const entry of snapshot.entries || []) {
|
|
321
|
+
console.log(`- ${entry.name} ${entry.path}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function isLiveWorker(worker) {
|
|
325
|
+
return worker.status !== "stopped" && worker.status !== "failed";
|
|
326
|
+
}
|
|
327
|
+
function formatPrimary(primary) {
|
|
328
|
+
if (!primary)
|
|
329
|
+
return "unavailable";
|
|
330
|
+
const details = [primary.pluginName, primary.status, primary.activityState].filter(Boolean).join(" / ");
|
|
331
|
+
return details || "unavailable";
|
|
332
|
+
}
|
|
333
|
+
async function runWatchLoop(client, options) {
|
|
334
|
+
const mode = options.sessionId ? "worker" : "status";
|
|
335
|
+
while (true) {
|
|
336
|
+
process.stdout.write("\x1bc");
|
|
337
|
+
if (mode === "worker") {
|
|
338
|
+
const snapshot = await client.getWorkerDetail(options.sessionId);
|
|
339
|
+
printWorkerDetail(snapshot, false);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const snapshot = await client.getRuntimeSummary();
|
|
343
|
+
printStatus(snapshot, false);
|
|
344
|
+
console.log("");
|
|
345
|
+
printWorkers(snapshot, false);
|
|
346
|
+
}
|
|
347
|
+
console.log("");
|
|
348
|
+
console.log(`Watching ${mode}. Refresh: ${options.intervalMs}ms. Press Ctrl+C to stop.`);
|
|
349
|
+
await new Promise((resolve) => setTimeout(resolve, options.intervalMs));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function main() {
|
|
353
|
+
const argv = process.argv.slice(2);
|
|
354
|
+
const options = parseArgs(argv);
|
|
355
|
+
if (options.help || !options.command || (options.command === "node" && !options.nodeCommand)) {
|
|
356
|
+
printHelp();
|
|
357
|
+
process.exit(options.help ? 0 : 1);
|
|
358
|
+
}
|
|
359
|
+
if (options.command === "node") {
|
|
360
|
+
switch (options.nodeCommand) {
|
|
361
|
+
case "start":
|
|
362
|
+
await import("./node-start.js").then(({ runNodeStart }) => runNodeStart({
|
|
363
|
+
relayUrl: argv.includes("--relay") ? options.relayUrl : undefined,
|
|
364
|
+
nodeId: argv.includes("--node-id") ? options.nodeId : undefined,
|
|
365
|
+
password: argv.includes("--password") ? options.password : undefined,
|
|
366
|
+
localControlPort: options.localControlPort,
|
|
367
|
+
localBind: options.localBind,
|
|
368
|
+
disableLocalDirect: options.disableLocalDirect,
|
|
369
|
+
noReconnect: !options.autoReconnect,
|
|
370
|
+
primaryPluginName: options.primaryPluginName,
|
|
371
|
+
}));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const client = new MagicShellControlClient({
|
|
376
|
+
relayUrl: options.relayUrl,
|
|
377
|
+
nodeId: options.nodeId,
|
|
378
|
+
password: options.password,
|
|
379
|
+
timeoutMs: options.timeoutMs,
|
|
380
|
+
debug: options.debug,
|
|
381
|
+
});
|
|
382
|
+
switch (options.command) {
|
|
383
|
+
case "primary": {
|
|
384
|
+
const snapshot = await client.getPrimaryAgent();
|
|
385
|
+
printPrimary(snapshot, options.json);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
case "primary-send": {
|
|
389
|
+
requireData(options);
|
|
390
|
+
const payload = await client.sendPrimaryMessage(options.data);
|
|
391
|
+
printPrimaryReply(payload, options.json);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
case "status": {
|
|
395
|
+
const snapshot = await client.getRuntimeSummary();
|
|
396
|
+
printStatus(snapshot, options.json);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
case "workers": {
|
|
400
|
+
const snapshot = await client.listWorkers();
|
|
401
|
+
printWorkers(snapshot, options.json);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
case "worker": {
|
|
405
|
+
requireSession(options);
|
|
406
|
+
const snapshot = await client.getWorkerDetail(options.sessionId);
|
|
407
|
+
printWorkerDetail(snapshot, options.json);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
case "logs": {
|
|
411
|
+
requireSession(options);
|
|
412
|
+
const snapshot = await client.getWorkerDetail(options.sessionId);
|
|
413
|
+
printWorkerLogs(snapshot, options.json);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
case "watch": {
|
|
417
|
+
if (options.json) {
|
|
418
|
+
throw new Error("--json is not supported for watch");
|
|
419
|
+
}
|
|
420
|
+
if (options.sessionId) {
|
|
421
|
+
requireSession(options);
|
|
422
|
+
}
|
|
423
|
+
await runWatchLoop(client, options);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
case "browse": {
|
|
427
|
+
const snapshot = await client.browseDirectories(options.path);
|
|
428
|
+
printDirectoryList(snapshot, options.json);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
case "spawn": {
|
|
432
|
+
const result = await client.spawnWorker({
|
|
433
|
+
pluginName: options.pluginName,
|
|
434
|
+
taskSummary: options.taskSummary,
|
|
435
|
+
cwd: options.cwd,
|
|
436
|
+
displayName: options.displayName,
|
|
437
|
+
});
|
|
438
|
+
printMutationResult("spawned worker", result, options.json);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
case "attach": {
|
|
442
|
+
requireSession(options);
|
|
443
|
+
const result = await client.attachSession(options.sessionId);
|
|
444
|
+
printMutationResult(`attached ${options.sessionId}`, result, options.json);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
case "input": {
|
|
448
|
+
requireSession(options);
|
|
449
|
+
requireData(options);
|
|
450
|
+
const result = await client.sendInput(options.sessionId, options.data);
|
|
451
|
+
printMutationResult(`sent input to ${options.sessionId}`, result, options.json);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
case "session-message": {
|
|
455
|
+
requireSession(options);
|
|
456
|
+
const snapshot = await client.getSessionMessage(options.sessionId);
|
|
457
|
+
printSessionText(snapshot, "message", options.json);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
case "session-summary": {
|
|
461
|
+
requireSession(options);
|
|
462
|
+
const snapshot = await client.getSessionSummary(options.sessionId);
|
|
463
|
+
printSessionText(snapshot, "summary", options.json);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
case "session-send": {
|
|
467
|
+
requireSession(options);
|
|
468
|
+
requireData(options);
|
|
469
|
+
const result = await client.sendToSession(options.sessionId, options.data, options.mode || "follow_up");
|
|
470
|
+
printMutationResult(`sent session message to ${options.sessionId}`, result, options.json);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
case "session-turn": {
|
|
474
|
+
requireSession(options);
|
|
475
|
+
requireData(options);
|
|
476
|
+
const result = await client.sessionTurn(options.sessionId, options.data, {
|
|
477
|
+
mode: options.mode || "follow_up",
|
|
478
|
+
timeoutMs: options.timeoutMs,
|
|
479
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
480
|
+
});
|
|
481
|
+
printSessionTurn(result, options.json);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
case "stop": {
|
|
485
|
+
requireSession(options);
|
|
486
|
+
const result = await client.stopWorker(options.sessionId);
|
|
487
|
+
printMutationResult(`stopped ${options.sessionId}`, result, options.json);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
case "restart": {
|
|
491
|
+
requireSession(options);
|
|
492
|
+
const result = await client.restartWorker(options.sessionId);
|
|
493
|
+
printMutationResult(`restarted ${options.sessionId}`, result, options.json);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function requireSession(options) {
|
|
499
|
+
if (!options.sessionId) {
|
|
500
|
+
throw new Error(`--session is required for ${options.command}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function requireData(options) {
|
|
504
|
+
if (!options.data) {
|
|
505
|
+
throw new Error(`--data is required for ${options.command}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function printSessionText(payload, field, json) {
|
|
509
|
+
if (json) {
|
|
510
|
+
console.log(JSON.stringify({
|
|
511
|
+
sessionId: payload.sessionId || null,
|
|
512
|
+
[field]: payload[field] || null,
|
|
513
|
+
}, null, 2));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
console.log(payload[field] || `No ${field} available`);
|
|
517
|
+
}
|
|
518
|
+
function printPrimary(payload, json) {
|
|
519
|
+
if (json) {
|
|
520
|
+
console.log(JSON.stringify(payload.primary || null, null, 2));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (!payload.primary) {
|
|
524
|
+
console.log("Primary agent unavailable");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
console.log(`${payload.primary.pluginName} ${payload.primary.status}${payload.primary.activityState ? ` / ${payload.primary.activityState}` : ""}`);
|
|
528
|
+
if (payload.primary.lastError) {
|
|
529
|
+
console.log(`Error: ${payload.primary.lastError}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function printPrimaryReply(payload, json) {
|
|
533
|
+
if (json) {
|
|
534
|
+
console.log(JSON.stringify({
|
|
535
|
+
text: payload.text || null,
|
|
536
|
+
actionType: payload.actionType || null,
|
|
537
|
+
actionLabel: payload.actionLabel || null,
|
|
538
|
+
actionTaskSummary: payload.actionTaskSummary || null,
|
|
539
|
+
actionSessionId: payload.actionSessionId || null,
|
|
540
|
+
}, null, 2));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
console.log(payload.text || "Primary agent returned no reply");
|
|
544
|
+
if (payload.actionType) {
|
|
545
|
+
console.log(`Action: ${payload.actionType}${payload.actionSessionId ? ` (${payload.actionSessionId})` : ""}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function printMutationResult(label, payload, json) {
|
|
549
|
+
if (json) {
|
|
550
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
console.log(label);
|
|
554
|
+
if (payload.nodeId) {
|
|
555
|
+
console.log(`Node: ${payload.nodeId}`);
|
|
556
|
+
}
|
|
557
|
+
const sessionId = typeof payload.sessionId === "string"
|
|
558
|
+
? payload.sessionId
|
|
559
|
+
: undefined;
|
|
560
|
+
if (sessionId) {
|
|
561
|
+
console.log(`Session: ${sessionId}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function printSessionTurn(payload, json) {
|
|
565
|
+
if (json) {
|
|
566
|
+
console.log(JSON.stringify({
|
|
567
|
+
sessionId: payload.sessionId || null,
|
|
568
|
+
mode: payload.mode || null,
|
|
569
|
+
message: payload.message || null,
|
|
570
|
+
summary: payload.summary || null,
|
|
571
|
+
changed: payload.changed ?? null,
|
|
572
|
+
timedOut: payload.timedOut ?? null,
|
|
573
|
+
}, null, 2));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
console.log(`Session: ${payload.sessionId || "unknown"}`);
|
|
577
|
+
console.log(`Changed: ${payload.changed ? "yes" : "no"}${payload.timedOut ? " (timed out)" : ""}`);
|
|
578
|
+
if (payload.message) {
|
|
579
|
+
console.log("");
|
|
580
|
+
console.log("Latest message:");
|
|
581
|
+
console.log(payload.message);
|
|
582
|
+
}
|
|
583
|
+
if (payload.summary) {
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log("Summary:");
|
|
586
|
+
console.log(payload.summary);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
main().catch((error) => {
|
|
590
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
591
|
+
process.exit(1);
|
|
592
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface NodeStartOptions {
|
|
2
|
+
relayUrl?: string;
|
|
3
|
+
nodeId?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
localControlPort?: number;
|
|
6
|
+
localBind?: string;
|
|
7
|
+
disableLocalDirect?: boolean;
|
|
8
|
+
noReconnect?: boolean;
|
|
9
|
+
primaryPluginName?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runNodeStart(options: NodeStartOptions): Promise<void>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, readdirSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { AgentNode, getDefaultPluginDir } from "@cydm/agent-node";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
export async function runNodeStart(options) {
|
|
9
|
+
ensureSpawnHelperExecutable();
|
|
10
|
+
const nodeOptions = {
|
|
11
|
+
relayUrl: options.relayUrl || process.env.MAGIC_SHELL_RELAY || "wss://magicshell.ai/ws",
|
|
12
|
+
nodeId: options.nodeId || process.env.MAGIC_SHELL_NODE_ID || `node-${generateId(8)}`,
|
|
13
|
+
password: options.password || process.env.MAGIC_SHELL_PASSWORD || randomPassword(24),
|
|
14
|
+
pluginDir: getDefaultPluginDir(),
|
|
15
|
+
primaryPluginName: options.primaryPluginName || process.env.MAGIC_SHELL_PRIMARY_PLUGIN || "pie",
|
|
16
|
+
autoReconnect: options.noReconnect ? false : true,
|
|
17
|
+
enableLocalDirect: options.disableLocalDirect ? false : process.env.MAGIC_SHELL_DISABLE_LOCAL_DIRECT !== "1",
|
|
18
|
+
localControlHost: options.localBind || process.env.MAGIC_SHELL_LOCAL_CONTROL_HOST || "127.0.0.1",
|
|
19
|
+
localControlPort: options.localControlPort || readNumber(process.env.MAGIC_SHELL_LOCAL_CONTROL_PORT) || randomLocalPort(),
|
|
20
|
+
};
|
|
21
|
+
const node = new AgentNode(nodeOptions);
|
|
22
|
+
const webUrl = inferWebUrl(nodeOptions.relayUrl);
|
|
23
|
+
process.on("SIGINT", async () => {
|
|
24
|
+
await node.stop();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
process.on("SIGTERM", async () => {
|
|
28
|
+
await node.stop();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
});
|
|
31
|
+
await node.start();
|
|
32
|
+
const localDirect = node.getLocalDirectInfo();
|
|
33
|
+
const remoteConnectUrl = `${webUrl}?node=${encodeURIComponent(nodeOptions.nodeId)}&pwd=${encodeURIComponent(nodeOptions.password)}`;
|
|
34
|
+
const localDirectUrl = localDirect
|
|
35
|
+
? `${localDirect.webUrl}?relay=${encodeURIComponent(localDirect.wsUrl)}&node=${encodeURIComponent(nodeOptions.nodeId)}&pwd=${encodeURIComponent(nodeOptions.password)}&autoconnect=1`
|
|
36
|
+
: undefined;
|
|
37
|
+
printConnectionInfo({
|
|
38
|
+
webUrl,
|
|
39
|
+
nodeId: nodeOptions.nodeId,
|
|
40
|
+
password: nodeOptions.password,
|
|
41
|
+
localDirectUrl,
|
|
42
|
+
localDirectDisplay: localDirect?.webUrl,
|
|
43
|
+
relayUrl: nodeOptions.relayUrl,
|
|
44
|
+
remoteConnectUrl,
|
|
45
|
+
});
|
|
46
|
+
saveLastSession({
|
|
47
|
+
webUrl,
|
|
48
|
+
relayUrl: nodeOptions.relayUrl,
|
|
49
|
+
localDirectUrl,
|
|
50
|
+
nodeId: nodeOptions.nodeId,
|
|
51
|
+
password: nodeOptions.password,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function printConnectionInfo(info) {
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log("Magic Shell node is running");
|
|
57
|
+
console.log(`Web: ${info.webUrl}`);
|
|
58
|
+
console.log(`Relay: ${info.relayUrl}`);
|
|
59
|
+
if (info.localDirectDisplay) {
|
|
60
|
+
console.log(`Local direct: ${info.localDirectDisplay}`);
|
|
61
|
+
}
|
|
62
|
+
console.log(`Node ID: ${info.nodeId}`);
|
|
63
|
+
console.log(`Password: ${info.password}`);
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(`Open remote: ${info.remoteConnectUrl}`);
|
|
66
|
+
if (info.localDirectUrl) {
|
|
67
|
+
console.log(`Open local: ${info.localDirectUrl}`);
|
|
68
|
+
}
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("Press Ctrl+C to stop.");
|
|
71
|
+
}
|
|
72
|
+
function saveLastSession(info) {
|
|
73
|
+
const infoFile = path.join(os.homedir(), ".magic-shell-last-session.json");
|
|
74
|
+
mkdirSync(path.dirname(infoFile), { recursive: true });
|
|
75
|
+
writeFileSync(infoFile, JSON.stringify({
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
webUrl: info.webUrl,
|
|
78
|
+
relayUrl: info.relayUrl,
|
|
79
|
+
localDirectUrl: info.localDirectUrl || null,
|
|
80
|
+
nodeId: info.nodeId,
|
|
81
|
+
password: info.password,
|
|
82
|
+
}, null, 2));
|
|
83
|
+
console.log(`Saved connection info: ${infoFile}`);
|
|
84
|
+
}
|
|
85
|
+
function inferWebUrl(relayUrl) {
|
|
86
|
+
if (relayUrl.startsWith("wss://")) {
|
|
87
|
+
return `https://${relayUrl.replace(/^wss:\/\//, "").replace(/\/ws$/, "")}`;
|
|
88
|
+
}
|
|
89
|
+
if (relayUrl.startsWith("ws://")) {
|
|
90
|
+
return `http://${relayUrl.replace(/^ws:\/\//, "").replace(/\/ws$/, "")}`;
|
|
91
|
+
}
|
|
92
|
+
return relayUrl;
|
|
93
|
+
}
|
|
94
|
+
function randomPassword(length) {
|
|
95
|
+
return randomBytes(Math.max(length * 2, 32)).toString("base64").replace(/[^a-zA-Z0-9]/g, "").slice(0, length);
|
|
96
|
+
}
|
|
97
|
+
function generateId(length) {
|
|
98
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
99
|
+
let result = "";
|
|
100
|
+
for (let index = 0; index < length; index += 1) {
|
|
101
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
function randomLocalPort() {
|
|
106
|
+
return 43000 + Math.floor(Math.random() * 2000);
|
|
107
|
+
}
|
|
108
|
+
function readNumber(raw) {
|
|
109
|
+
if (!raw)
|
|
110
|
+
return undefined;
|
|
111
|
+
const value = Number(raw);
|
|
112
|
+
return Number.isFinite(value) ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
function ensureSpawnHelperExecutable() {
|
|
115
|
+
try {
|
|
116
|
+
const pkgPath = require.resolve("node-pty/package.json");
|
|
117
|
+
const prebuildRoot = path.join(path.dirname(pkgPath), "prebuilds");
|
|
118
|
+
if (!existsSync(prebuildRoot))
|
|
119
|
+
return;
|
|
120
|
+
for (const entry of walk(prebuildRoot)) {
|
|
121
|
+
if (path.basename(entry) === "spawn-helper") {
|
|
122
|
+
chmodSync(entry, 0o755);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore best-effort fixups during startup.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function walk(dir) {
|
|
131
|
+
const files = [];
|
|
132
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
133
|
+
const fullPath = path.join(dir, entry.name);
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
files.push(...walk(fullPath));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
files.push(fullPath);
|
|
139
|
+
}
|
|
140
|
+
return files;
|
|
141
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cydm/magic-shell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Magic Shell first-party CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"types": "./dist/cli.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"magic-shell": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@cydm/agent-node": "0.1.0",
|
|
24
|
+
"@cydm/pie": "^1.0.5",
|
|
25
|
+
"@cydm/protocol": "0.1.0",
|
|
26
|
+
"@cydm/sdk": "0.1.0",
|
|
27
|
+
"ws": "^8.18.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.10.0",
|
|
31
|
+
"@types/ws": "^8.5.13",
|
|
32
|
+
"typescript": "^5.3.0"
|
|
33
|
+
}
|
|
34
|
+
}
|