@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 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }