openmagic 0.1.0 → 0.4.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.js CHANGED
@@ -5,9 +5,12 @@ import { Command } from "commander";
5
5
  import chalk from "chalk";
6
6
  import open from "open";
7
7
  import { resolve as resolve2 } from "path";
8
+ import { spawn } from "child_process";
9
+ import { createInterface } from "readline";
8
10
 
9
11
  // src/proxy.ts
10
12
  import http from "http";
13
+ import { createGunzip, createInflate, createBrotliDecompress } from "zlib";
11
14
  import httpProxy from "http-proxy";
12
15
 
13
16
  // src/security.ts
@@ -43,10 +46,7 @@ function createProxyServer(targetHost, targetPort, serverPort) {
43
46
  proxyRes.pipe(res);
44
47
  return;
45
48
  }
46
- const chunks = [];
47
- proxyRes.on("data", (chunk) => chunks.push(chunk));
48
- proxyRes.on("end", () => {
49
- let body = Buffer.concat(chunks).toString("utf-8");
49
+ collectBody(proxyRes).then((body) => {
50
50
  const toolbarScript = buildInjectionScript(serverPort, token);
51
51
  if (body.includes("</body>")) {
52
52
  body = body.replace("</body>", `${toolbarScript}</body>`);
@@ -58,18 +58,27 @@ function createProxyServer(targetHost, targetPort, serverPort) {
58
58
  const headers = { ...proxyRes.headers };
59
59
  delete headers["content-length"];
60
60
  delete headers["content-encoding"];
61
+ delete headers["transfer-encoding"];
61
62
  res.writeHead(proxyRes.statusCode || 200, headers);
62
63
  res.end(body);
64
+ }).catch(() => {
65
+ try {
66
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
67
+ res.end();
68
+ } catch {
69
+ }
63
70
  });
64
71
  });
65
72
  proxy.on("error", (err, _req, res) => {
66
- console.error("[OpenMagic] Proxy error:", err.message);
67
73
  if (res instanceof http.ServerResponse && !res.headersSent) {
68
- res.writeHead(502, { "Content-Type": "text/plain" });
74
+ res.writeHead(502, { "Content-Type": "text/html" });
69
75
  res.end(
70
- `OpenMagic proxy error: Could not connect to dev server at ${targetHost}:${targetPort}
71
-
72
- Make sure your dev server is running.`
76
+ `<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
77
+ <h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
78
+ <p>Could not reach <code>${targetHost}:${targetPort}</code></p>
79
+ <p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
80
+ <p style="color:#666;font-size:13px;">${err.message}</p>
81
+ </body></html>`
73
82
  );
74
83
  }
75
84
  });
@@ -88,6 +97,58 @@ Make sure your dev server is running.`
88
97
  });
89
98
  return server;
90
99
  }
100
+ function collectBody(stream) {
101
+ return new Promise((resolve3, reject) => {
102
+ const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
103
+ const chunks = [];
104
+ let source = stream;
105
+ if (encoding === "gzip" || encoding === "x-gzip") {
106
+ const gunzip = createGunzip();
107
+ stream.pipe(gunzip);
108
+ source = gunzip;
109
+ gunzip.on("error", () => {
110
+ collectRaw(stream).then(resolve3).catch(reject);
111
+ });
112
+ } else if (encoding === "deflate") {
113
+ const inflate = createInflate();
114
+ stream.pipe(inflate);
115
+ source = inflate;
116
+ inflate.on("error", () => {
117
+ collectRaw(stream).then(resolve3).catch(reject);
118
+ });
119
+ } else if (encoding === "br") {
120
+ const brotli = createBrotliDecompress();
121
+ stream.pipe(brotli);
122
+ source = brotli;
123
+ brotli.on("error", () => {
124
+ collectRaw(stream).then(resolve3).catch(reject);
125
+ });
126
+ }
127
+ source.on("data", (chunk) => chunks.push(chunk));
128
+ source.on("end", () => {
129
+ try {
130
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
131
+ } catch {
132
+ reject(new Error("Failed to decode response body"));
133
+ }
134
+ });
135
+ source.on("error", (err) => reject(err));
136
+ });
137
+ }
138
+ function collectRaw(stream) {
139
+ return new Promise((resolve3, reject) => {
140
+ const chunks = [];
141
+ stream.on("data", (chunk) => chunks.push(chunk));
142
+ stream.on("end", () => {
143
+ try {
144
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
145
+ } catch {
146
+ reject(new Error("Failed to decode raw body"));
147
+ }
148
+ });
149
+ stream.on("error", reject);
150
+ });
151
+ }
91
152
  function handleToolbarAsset(_req, res, _serverPort) {
92
153
  res.writeHead(404, { "Content-Type": "text/plain" });
93
154
  res.end("Not found");
@@ -141,10 +202,14 @@ function loadConfig() {
141
202
  }
142
203
  }
143
204
  function saveConfig(updates) {
144
- ensureConfigDir();
145
- const existing = loadConfig();
146
- const merged = { ...existing, ...updates };
147
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
205
+ try {
206
+ ensureConfigDir();
207
+ const existing = loadConfig();
208
+ const merged = { ...existing, ...updates };
209
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
210
+ } catch (e) {
211
+ console.warn(`[OpenMagic] Warning: Could not save config to ${CONFIG_FILE}: ${e.message}`);
212
+ }
148
213
  }
149
214
 
150
215
  // src/filesystem.ts
@@ -286,85 +351,550 @@ function getProjectTree(roots) {
286
351
 
287
352
  // src/llm/registry.ts
288
353
  var MODEL_REGISTRY = {
354
+ // ─── OpenAI ───────────────────────────────────────────────────
289
355
  openai: {
290
356
  name: "OpenAI",
291
357
  models: [
292
- { id: "gpt-4.1", name: "GPT-4.1", vision: true, context: 1047576 },
293
- { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", vision: true, context: 1047576 },
294
- { id: "gpt-4.1-nano", name: "GPT-4.1 Nano", vision: true, context: 1047576 },
295
- { id: "gpt-4o", name: "GPT-4o", vision: true, context: 128e3 },
296
- { id: "gpt-4o-mini", name: "GPT-4o Mini", vision: true, context: 128e3 },
297
- { id: "o3", name: "o3 (Reasoning)", vision: true, context: 2e5 },
298
- { id: "o4-mini", name: "o4-mini (Reasoning)", vision: true, context: 2e5 }
358
+ // GPT-5.4 family (March 2026 latest flagship)
359
+ {
360
+ id: "gpt-5.4",
361
+ name: "GPT-5.4",
362
+ vision: true,
363
+ context: 105e4,
364
+ maxOutput: 128e3,
365
+ thinking: {
366
+ supported: true,
367
+ paramName: "reasoning_effort",
368
+ paramType: "level",
369
+ levels: ["none", "low", "medium", "high", "xhigh"],
370
+ defaultLevel: "medium"
371
+ }
372
+ },
373
+ {
374
+ id: "gpt-5.4-pro",
375
+ name: "GPT-5.4 Pro",
376
+ vision: true,
377
+ context: 105e4,
378
+ maxOutput: 128e3,
379
+ thinking: {
380
+ supported: true,
381
+ paramName: "reasoning_effort",
382
+ paramType: "level",
383
+ levels: ["none", "low", "medium", "high", "xhigh"],
384
+ defaultLevel: "high"
385
+ }
386
+ },
387
+ {
388
+ id: "gpt-5.4-mini",
389
+ name: "GPT-5.4 Mini",
390
+ vision: true,
391
+ context: 4e5,
392
+ maxOutput: 128e3,
393
+ thinking: {
394
+ supported: true,
395
+ paramName: "reasoning_effort",
396
+ paramType: "level",
397
+ levels: ["none", "low", "medium", "high"],
398
+ defaultLevel: "medium"
399
+ }
400
+ },
401
+ {
402
+ id: "gpt-5.4-nano",
403
+ name: "GPT-5.4 Nano",
404
+ vision: true,
405
+ context: 4e5,
406
+ maxOutput: 128e3,
407
+ thinking: {
408
+ supported: true,
409
+ paramName: "reasoning_effort",
410
+ paramType: "level",
411
+ levels: ["none", "low", "medium", "high"],
412
+ defaultLevel: "low"
413
+ }
414
+ },
415
+ // GPT-5.2 family (reasoning-focused)
416
+ {
417
+ id: "gpt-5.2",
418
+ name: "GPT-5.2 Thinking",
419
+ vision: true,
420
+ context: 272e3,
421
+ maxOutput: 128e3,
422
+ thinking: {
423
+ supported: true,
424
+ paramName: "reasoning_effort",
425
+ paramType: "level",
426
+ levels: ["none", "low", "medium", "high", "xhigh"],
427
+ defaultLevel: "high"
428
+ }
429
+ },
430
+ {
431
+ id: "gpt-5.2-pro",
432
+ name: "GPT-5.2 Pro",
433
+ vision: true,
434
+ context: 272e3,
435
+ maxOutput: 128e3,
436
+ thinking: {
437
+ supported: true,
438
+ paramName: "reasoning_effort",
439
+ paramType: "level",
440
+ levels: ["none", "low", "medium", "high", "xhigh"],
441
+ defaultLevel: "high"
442
+ }
443
+ },
444
+ // o-series reasoning models
445
+ {
446
+ id: "o3",
447
+ name: "o3 (Reasoning)",
448
+ vision: true,
449
+ context: 2e5,
450
+ maxOutput: 1e5,
451
+ thinking: {
452
+ supported: true,
453
+ paramName: "reasoning_effort",
454
+ paramType: "level",
455
+ levels: ["low", "medium", "high"],
456
+ defaultLevel: "medium"
457
+ }
458
+ },
459
+ {
460
+ id: "o4-mini",
461
+ name: "o4-mini (Reasoning)",
462
+ vision: true,
463
+ context: 2e5,
464
+ maxOutput: 1e5,
465
+ thinking: {
466
+ supported: true,
467
+ paramName: "reasoning_effort",
468
+ paramType: "level",
469
+ levels: ["low", "medium", "high"],
470
+ defaultLevel: "medium"
471
+ }
472
+ },
473
+ // GPT-4.1 family
474
+ {
475
+ id: "gpt-4.1",
476
+ name: "GPT-4.1",
477
+ vision: true,
478
+ context: 1047576,
479
+ maxOutput: 32768
480
+ },
481
+ {
482
+ id: "gpt-4.1-mini",
483
+ name: "GPT-4.1 Mini",
484
+ vision: true,
485
+ context: 1047576,
486
+ maxOutput: 32768
487
+ },
488
+ {
489
+ id: "gpt-4.1-nano",
490
+ name: "GPT-4.1 Nano",
491
+ vision: true,
492
+ context: 1047576,
493
+ maxOutput: 32768
494
+ },
495
+ // Codex
496
+ {
497
+ id: "codex-mini-latest",
498
+ name: "Codex Mini",
499
+ vision: false,
500
+ context: 192e3,
501
+ maxOutput: 1e5,
502
+ thinking: {
503
+ supported: true,
504
+ paramName: "reasoning_effort",
505
+ paramType: "level",
506
+ levels: ["low", "medium", "high"],
507
+ defaultLevel: "high"
508
+ }
509
+ }
299
510
  ],
300
511
  apiBase: "https://api.openai.com/v1",
301
512
  keyPrefix: "sk-",
302
513
  keyPlaceholder: "sk-..."
303
514
  },
515
+ // ─── Anthropic ────────────────────────────────────────────────
304
516
  anthropic: {
305
517
  name: "Anthropic",
306
518
  models: [
307
- { id: "claude-opus-4-20250514", name: "Claude Opus 4", vision: true, context: 2e5 },
308
- { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", vision: true, context: 2e5 },
309
- { id: "claude-haiku-4-20250514", name: "Claude Haiku 4", vision: true, context: 2e5 }
519
+ // Claude 4.6 (latest Feb 2026)
520
+ {
521
+ id: "claude-opus-4-6",
522
+ name: "Claude Opus 4.6",
523
+ vision: true,
524
+ context: 1e6,
525
+ maxOutput: 128e3,
526
+ thinking: {
527
+ supported: true,
528
+ paramName: "budget_tokens",
529
+ paramType: "budget",
530
+ defaultBudget: 1e4,
531
+ maxBudget: 128e3
532
+ }
533
+ },
534
+ {
535
+ id: "claude-sonnet-4-6",
536
+ name: "Claude Sonnet 4.6",
537
+ vision: true,
538
+ context: 1e6,
539
+ maxOutput: 64e3,
540
+ thinking: {
541
+ supported: true,
542
+ paramName: "budget_tokens",
543
+ paramType: "budget",
544
+ defaultBudget: 8e3,
545
+ maxBudget: 64e3
546
+ }
547
+ },
548
+ // Claude 4.5
549
+ {
550
+ id: "claude-haiku-4-5-20251001",
551
+ name: "Claude Haiku 4.5",
552
+ vision: true,
553
+ context: 2e5,
554
+ maxOutput: 64e3,
555
+ thinking: {
556
+ supported: true,
557
+ paramName: "budget_tokens",
558
+ paramType: "budget",
559
+ defaultBudget: 5e3,
560
+ maxBudget: 64e3
561
+ }
562
+ },
563
+ {
564
+ id: "claude-sonnet-4-5-20250929",
565
+ name: "Claude Sonnet 4.5",
566
+ vision: true,
567
+ context: 1e6,
568
+ maxOutput: 64e3,
569
+ thinking: {
570
+ supported: true,
571
+ paramName: "budget_tokens",
572
+ paramType: "budget",
573
+ defaultBudget: 8e3,
574
+ maxBudget: 64e3
575
+ }
576
+ },
577
+ {
578
+ id: "claude-opus-4-5-20251101",
579
+ name: "Claude Opus 4.5",
580
+ vision: true,
581
+ context: 2e5,
582
+ maxOutput: 64e3,
583
+ thinking: {
584
+ supported: true,
585
+ paramName: "budget_tokens",
586
+ paramType: "budget",
587
+ defaultBudget: 1e4,
588
+ maxBudget: 64e3
589
+ }
590
+ },
591
+ // Claude 4.0
592
+ {
593
+ id: "claude-sonnet-4-20250514",
594
+ name: "Claude Sonnet 4",
595
+ vision: true,
596
+ context: 2e5,
597
+ maxOutput: 64e3,
598
+ thinking: {
599
+ supported: true,
600
+ paramName: "budget_tokens",
601
+ paramType: "budget",
602
+ defaultBudget: 8e3,
603
+ maxBudget: 64e3
604
+ }
605
+ },
606
+ {
607
+ id: "claude-opus-4-20250514",
608
+ name: "Claude Opus 4",
609
+ vision: true,
610
+ context: 2e5,
611
+ maxOutput: 32e3,
612
+ thinking: {
613
+ supported: true,
614
+ paramName: "budget_tokens",
615
+ paramType: "budget",
616
+ defaultBudget: 1e4,
617
+ maxBudget: 32e3
618
+ }
619
+ }
310
620
  ],
311
621
  apiBase: "https://api.anthropic.com/v1",
312
622
  keyPrefix: "sk-ant-",
313
623
  keyPlaceholder: "sk-ant-..."
314
624
  },
625
+ // ─── Google Gemini ────────────────────────────────────────────
315
626
  google: {
316
627
  name: "Google Gemini",
317
628
  models: [
318
- { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", vision: true, context: 1048576 },
319
- { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", vision: true, context: 1048576 },
320
- { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", vision: true, context: 1048576 }
629
+ // Gemini 3.1 (latest Feb-Mar 2026)
630
+ {
631
+ id: "gemini-3.1-pro-preview",
632
+ name: "Gemini 3.1 Pro",
633
+ vision: true,
634
+ context: 1048576,
635
+ maxOutput: 65536,
636
+ thinking: {
637
+ supported: true,
638
+ paramName: "thinking_level",
639
+ paramType: "level",
640
+ levels: ["none", "low", "medium", "high"],
641
+ defaultLevel: "medium"
642
+ }
643
+ },
644
+ // Gemini 3.0
645
+ {
646
+ id: "gemini-3-flash-preview",
647
+ name: "Gemini 3 Flash",
648
+ vision: true,
649
+ context: 1048576,
650
+ maxOutput: 65536,
651
+ thinking: {
652
+ supported: true,
653
+ paramName: "thinking_level",
654
+ paramType: "level",
655
+ levels: ["none", "low", "medium", "high"],
656
+ defaultLevel: "low"
657
+ }
658
+ },
659
+ {
660
+ id: "gemini-3.1-flash-lite-preview",
661
+ name: "Gemini 3.1 Flash Lite",
662
+ vision: true,
663
+ context: 1048576,
664
+ maxOutput: 65536
665
+ },
666
+ // Gemini 2.5
667
+ {
668
+ id: "gemini-2.5-pro",
669
+ name: "Gemini 2.5 Pro",
670
+ vision: true,
671
+ context: 1048576,
672
+ maxOutput: 65536,
673
+ thinking: {
674
+ supported: true,
675
+ paramName: "thinking_level",
676
+ paramType: "level",
677
+ levels: ["none", "low", "medium", "high"],
678
+ defaultLevel: "medium"
679
+ }
680
+ },
681
+ {
682
+ id: "gemini-2.5-flash",
683
+ name: "Gemini 2.5 Flash",
684
+ vision: true,
685
+ context: 1048576,
686
+ maxOutput: 65536,
687
+ thinking: {
688
+ supported: true,
689
+ paramName: "thinking_level",
690
+ paramType: "level",
691
+ levels: ["none", "low", "medium", "high"],
692
+ defaultLevel: "low"
693
+ }
694
+ },
695
+ {
696
+ id: "gemini-2.5-flash-lite",
697
+ name: "Gemini 2.5 Flash Lite",
698
+ vision: true,
699
+ context: 1048576,
700
+ maxOutput: 65536
701
+ }
321
702
  ],
322
703
  apiBase: "https://generativelanguage.googleapis.com/v1beta",
323
704
  keyPrefix: "AI",
324
705
  keyPlaceholder: "AIza..."
325
706
  },
707
+ // ─── xAI (Grok) ──────────────────────────────────────────────
708
+ xai: {
709
+ name: "xAI (Grok)",
710
+ models: [
711
+ {
712
+ id: "grok-4.20-0309-reasoning",
713
+ name: "Grok 4.20 Reasoning",
714
+ vision: true,
715
+ context: 2e6,
716
+ maxOutput: 128e3,
717
+ thinking: {
718
+ supported: true,
719
+ paramName: "reasoning_effort",
720
+ paramType: "level",
721
+ levels: ["low", "medium", "high"],
722
+ defaultLevel: "medium"
723
+ }
724
+ },
725
+ {
726
+ id: "grok-4.20-0309-non-reasoning",
727
+ name: "Grok 4.20",
728
+ vision: true,
729
+ context: 2e6,
730
+ maxOutput: 128e3
731
+ },
732
+ {
733
+ id: "grok-4-1-fast-reasoning",
734
+ name: "Grok 4.1 Fast Reasoning",
735
+ vision: true,
736
+ context: 2e6,
737
+ maxOutput: 128e3,
738
+ thinking: {
739
+ supported: true,
740
+ paramName: "reasoning_effort",
741
+ paramType: "level",
742
+ levels: ["low", "medium", "high"],
743
+ defaultLevel: "low"
744
+ }
745
+ },
746
+ {
747
+ id: "grok-4-1-fast-non-reasoning",
748
+ name: "Grok 4.1 Fast",
749
+ vision: true,
750
+ context: 2e6,
751
+ maxOutput: 128e3
752
+ }
753
+ ],
754
+ apiBase: "https://api.x.ai/v1",
755
+ keyPrefix: "xai-",
756
+ keyPlaceholder: "xai-..."
757
+ },
758
+ // ─── DeepSeek ─────────────────────────────────────────────────
326
759
  deepseek: {
327
760
  name: "DeepSeek",
328
761
  models: [
329
- { id: "deepseek-chat", name: "DeepSeek V3", vision: false, context: 65536 },
330
- { id: "deepseek-reasoner", name: "DeepSeek R1", vision: false, context: 65536 }
762
+ {
763
+ id: "deepseek-chat",
764
+ name: "DeepSeek V3.2",
765
+ vision: false,
766
+ context: 128e3,
767
+ maxOutput: 8192
768
+ },
769
+ {
770
+ id: "deepseek-reasoner",
771
+ name: "DeepSeek R1",
772
+ vision: false,
773
+ context: 128e3,
774
+ maxOutput: 8192,
775
+ thinking: {
776
+ supported: true,
777
+ paramName: "reasoning_effort",
778
+ paramType: "level",
779
+ levels: ["low", "medium", "high"],
780
+ defaultLevel: "medium"
781
+ }
782
+ }
331
783
  ],
332
784
  apiBase: "https://api.deepseek.com/v1",
333
785
  keyPrefix: "sk-",
334
786
  keyPlaceholder: "sk-..."
335
787
  },
336
- groq: {
337
- name: "Groq",
338
- models: [
339
- { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B", vision: false, context: 131072 },
340
- { id: "llama-3.1-8b-instant", name: "Llama 3.1 8B", vision: false, context: 131072 },
341
- { id: "gemma2-9b-it", name: "Gemma 2 9B", vision: false, context: 8192 }
342
- ],
343
- apiBase: "https://api.groq.com/openai/v1",
344
- keyPrefix: "gsk_",
345
- keyPlaceholder: "gsk_..."
346
- },
788
+ // ─── Mistral ──────────────────────────────────────────────────
347
789
  mistral: {
348
790
  name: "Mistral",
349
791
  models: [
350
- { id: "mistral-large-latest", name: "Mistral Large", vision: false, context: 131072 },
351
- { id: "mistral-small-latest", name: "Mistral Small", vision: false, context: 131072 },
352
- { id: "codestral-latest", name: "Codestral", vision: false, context: 262144 }
792
+ {
793
+ id: "mistral-large-3-25-12",
794
+ name: "Mistral Large 3",
795
+ vision: true,
796
+ context: 131072,
797
+ maxOutput: 32768
798
+ },
799
+ {
800
+ id: "mistral-small-4-0-26-03",
801
+ name: "Mistral Small 4",
802
+ vision: true,
803
+ context: 131072,
804
+ maxOutput: 32768
805
+ },
806
+ {
807
+ id: "mistral-small-3-2-25-06",
808
+ name: "Mistral Small 3.2",
809
+ vision: true,
810
+ context: 131072,
811
+ maxOutput: 32768
812
+ },
813
+ {
814
+ id: "codestral-2508",
815
+ name: "Codestral",
816
+ vision: false,
817
+ context: 262144,
818
+ maxOutput: 32768
819
+ },
820
+ {
821
+ id: "devstral-2-25-12",
822
+ name: "Devstral 2",
823
+ vision: false,
824
+ context: 131072,
825
+ maxOutput: 32768
826
+ },
827
+ {
828
+ id: "magistral-medium-1-2-25-09",
829
+ name: "Magistral Medium (Reasoning)",
830
+ vision: false,
831
+ context: 131072,
832
+ maxOutput: 32768,
833
+ thinking: {
834
+ supported: true,
835
+ paramName: "reasoning_effort",
836
+ paramType: "level",
837
+ levels: ["low", "medium", "high"],
838
+ defaultLevel: "medium"
839
+ }
840
+ },
841
+ {
842
+ id: "magistral-small-1-2-25-09",
843
+ name: "Magistral Small (Reasoning)",
844
+ vision: false,
845
+ context: 131072,
846
+ maxOutput: 32768,
847
+ thinking: {
848
+ supported: true,
849
+ paramName: "reasoning_effort",
850
+ paramType: "level",
851
+ levels: ["low", "medium", "high"],
852
+ defaultLevel: "medium"
853
+ }
854
+ }
353
855
  ],
354
856
  apiBase: "https://api.mistral.ai/v1",
355
857
  keyPrefix: "",
356
858
  keyPlaceholder: "Enter API key..."
357
859
  },
358
- xai: {
359
- name: "xAI (Grok)",
860
+ // ─── Groq ─────────────────────────────────────────────────────
861
+ groq: {
862
+ name: "Groq",
360
863
  models: [
361
- { id: "grok-3", name: "Grok 3", vision: true, context: 131072 },
362
- { id: "grok-3-mini", name: "Grok 3 Mini", vision: true, context: 131072 }
864
+ {
865
+ id: "meta-llama/llama-4-scout-17b-16e-instruct",
866
+ name: "Llama 4 Scout 17B",
867
+ vision: true,
868
+ context: 131072,
869
+ maxOutput: 8192
870
+ },
871
+ {
872
+ id: "llama-3.3-70b-versatile",
873
+ name: "Llama 3.3 70B",
874
+ vision: false,
875
+ context: 131072,
876
+ maxOutput: 32768
877
+ },
878
+ {
879
+ id: "llama-3.1-8b-instant",
880
+ name: "Llama 3.1 8B Instant",
881
+ vision: false,
882
+ context: 131072,
883
+ maxOutput: 8192
884
+ },
885
+ {
886
+ id: "qwen/qwen3-32b",
887
+ name: "Qwen 3 32B",
888
+ vision: false,
889
+ context: 131072,
890
+ maxOutput: 8192
891
+ }
363
892
  ],
364
- apiBase: "https://api.x.ai/v1",
365
- keyPrefix: "xai-",
366
- keyPlaceholder: "xai-..."
893
+ apiBase: "https://api.groq.com/openai/v1",
894
+ keyPrefix: "gsk_",
895
+ keyPlaceholder: "gsk_..."
367
896
  },
897
+ // ─── Ollama (Local) ───────────────────────────────────────────
368
898
  ollama: {
369
899
  name: "Ollama (Local)",
370
900
  models: [],
@@ -373,6 +903,7 @@ var MODEL_REGISTRY = {
373
903
  keyPlaceholder: "not required",
374
904
  local: true
375
905
  },
906
+ // ─── OpenRouter (200+ models) ─────────────────────────────────
376
907
  openrouter: {
377
908
  name: "OpenRouter",
378
909
  models: [],
@@ -492,8 +1023,8 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
492
1023
  contextParts.consoleLogs = context.consoleLogs.map((l) => `[${l.level}] ${l.args.join(" ")}`).join("\n");
493
1024
  }
494
1025
  const enrichedContent = buildUserMessage(msg.content, contextParts);
495
- const modelInfo = providerConfig.models.find((m) => m.id === model);
496
- if (context.screenshot && modelInfo?.vision) {
1026
+ const modelInfo2 = providerConfig.models.find((m) => m.id === model);
1027
+ if (context.screenshot && modelInfo2?.vision) {
497
1028
  apiMessages.push({
498
1029
  role: "user",
499
1030
  content: [
@@ -520,6 +1051,11 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
520
1051
  stream: true,
521
1052
  max_tokens: 4096
522
1053
  };
1054
+ const modelInfo = providerConfig.models.find((m) => m.id === model);
1055
+ if (modelInfo?.thinking?.supported && modelInfo.thinking.paramType === "level") {
1056
+ body.reasoning_effort = modelInfo.thinking.defaultLevel || "medium";
1057
+ body.max_tokens = Math.min(modelInfo.maxOutput, 16384);
1058
+ }
523
1059
  try {
524
1060
  const headers = {
525
1061
  "Content-Type": "application/json"
@@ -534,8 +1070,14 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
534
1070
  body: JSON.stringify(body)
535
1071
  });
536
1072
  if (!response.ok) {
537
- const errorText = await response.text();
538
- onError(`API error ${response.status}: ${errorText}`);
1073
+ const errorText = await response.text().catch(() => "Unknown error");
1074
+ if (response.status === 401 || response.status === 403) {
1075
+ onError(`Invalid API key for ${providerConfig.name}. Check your key in Settings.`);
1076
+ } else if (response.status === 429) {
1077
+ onError(`Rate limit exceeded for ${providerConfig.name}. Wait a moment and try again.`);
1078
+ } else {
1079
+ onError(`${providerConfig.name} API error ${response.status}: ${errorText.slice(0, 200)}`);
1080
+ }
539
1081
  return;
540
1082
  }
541
1083
  if (!response.body) {
@@ -627,13 +1169,22 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
627
1169
  });
628
1170
  }
629
1171
  }
1172
+ const providerConfig = MODEL_REGISTRY.anthropic;
1173
+ const modelInfo = providerConfig?.models.find((m) => m.id === model);
1174
+ const thinkingBudget = modelInfo?.thinking?.defaultBudget || 0;
630
1175
  const body = {
631
1176
  model,
632
- max_tokens: 4096,
1177
+ max_tokens: thinkingBudget > 0 ? Math.max(thinkingBudget + 4096, 16384) : 4096,
633
1178
  system: SYSTEM_PROMPT,
634
1179
  messages: apiMessages,
635
1180
  stream: true
636
1181
  };
1182
+ if (thinkingBudget > 0) {
1183
+ body.thinking = {
1184
+ type: "enabled",
1185
+ budget_tokens: thinkingBudget
1186
+ };
1187
+ }
637
1188
  try {
638
1189
  const response = await fetch(url, {
639
1190
  method: "POST",
@@ -645,8 +1196,14 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
645
1196
  body: JSON.stringify(body)
646
1197
  });
647
1198
  if (!response.ok) {
648
- const errorText = await response.text();
649
- onError(`Anthropic API error ${response.status}: ${errorText}`);
1199
+ const errorText = await response.text().catch(() => "Unknown error");
1200
+ if (response.status === 401 || response.status === 403) {
1201
+ onError("Invalid Anthropic API key. Check your key in Settings.");
1202
+ } else if (response.status === 429) {
1203
+ onError("Anthropic rate limit exceeded. Wait a moment and try again.");
1204
+ } else {
1205
+ onError(`Anthropic API error ${response.status}: ${errorText.slice(0, 200)}`);
1206
+ }
650
1207
  return;
651
1208
  }
652
1209
  if (!response.body) {
@@ -728,14 +1285,21 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
728
1285
  });
729
1286
  }
730
1287
  }
1288
+ const providerConfig = MODEL_REGISTRY.google;
1289
+ const modelInfo = providerConfig?.models.find((m) => m.id === model);
1290
+ const thinkingLevel = modelInfo?.thinking?.defaultLevel;
1291
+ const generationConfig = {
1292
+ maxOutputTokens: 8192
1293
+ };
1294
+ if (thinkingLevel && thinkingLevel !== "none") {
1295
+ generationConfig.thinking_level = thinkingLevel.toUpperCase();
1296
+ }
731
1297
  const body = {
732
1298
  system_instruction: {
733
1299
  parts: [{ text: SYSTEM_PROMPT }]
734
1300
  },
735
1301
  contents,
736
- generationConfig: {
737
- maxOutputTokens: 4096
738
- }
1302
+ generationConfig
739
1303
  };
740
1304
  try {
741
1305
  const response = await fetch(url, {
@@ -744,8 +1308,14 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
744
1308
  body: JSON.stringify(body)
745
1309
  });
746
1310
  if (!response.ok) {
747
- const errorText = await response.text();
748
- onError(`Google API error ${response.status}: ${errorText}`);
1311
+ const errorText = await response.text().catch(() => "Unknown error");
1312
+ if (response.status === 401 || response.status === 403) {
1313
+ onError("Invalid Google API key. Check your key in Settings.");
1314
+ } else if (response.status === 429) {
1315
+ onError("Google API rate limit exceeded. Wait a moment and try again.");
1316
+ } else {
1317
+ onError(`Google API error ${response.status}: ${errorText.slice(0, 200)}`);
1318
+ }
749
1319
  return;
750
1320
  }
751
1321
  if (!response.body) {
@@ -807,23 +1377,32 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
807
1377
  }
808
1378
  onDone({ content: result.content, modifications });
809
1379
  };
810
- if (provider === "anthropic") {
811
- await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
812
- } else if (provider === "google") {
813
- await chatGoogle(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
814
- } else if (OPENAI_COMPATIBLE_PROVIDERS.has(provider)) {
815
- await chatOpenAICompatible(
816
- provider,
817
- model,
818
- apiKey,
819
- messages,
820
- context,
821
- onChunk,
822
- wrappedOnDone,
823
- onError
824
- );
825
- } else {
826
- onError(`Unsupported provider: ${provider}`);
1380
+ try {
1381
+ if (provider === "anthropic") {
1382
+ await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1383
+ } else if (provider === "google") {
1384
+ await chatGoogle(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1385
+ } else if (OPENAI_COMPATIBLE_PROVIDERS.has(provider)) {
1386
+ await chatOpenAICompatible(
1387
+ provider,
1388
+ model,
1389
+ apiKey,
1390
+ messages,
1391
+ context,
1392
+ onChunk,
1393
+ wrappedOnDone,
1394
+ onError
1395
+ );
1396
+ } else {
1397
+ onError(`Unsupported provider: ${provider}. Check your Settings.`);
1398
+ }
1399
+ } catch (e) {
1400
+ const msg = e.message || "Unknown error";
1401
+ if (msg.includes("fetch") || msg.includes("ECONNREFUSED") || msg.includes("network")) {
1402
+ onError(`Network error: Could not reach the ${provider} API. Check your internet connection.`);
1403
+ } else {
1404
+ onError(`Unexpected error with ${provider}: ${msg}`);
1405
+ }
827
1406
  }
828
1407
  }
829
1408
 
@@ -840,7 +1419,7 @@ function createOpenMagicServer(proxyPort, roots) {
840
1419
  "Content-Type": "application/json",
841
1420
  "Access-Control-Allow-Origin": "*"
842
1421
  });
843
- res.end(JSON.stringify({ status: "ok", version: "0.1.0" }));
1422
+ res.end(JSON.stringify({ status: "ok", version: "0.4.0" }));
844
1423
  return;
845
1424
  }
846
1425
  res.writeHead(404);
@@ -882,6 +1461,11 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
882
1461
  switch (msg.type) {
883
1462
  case "handshake": {
884
1463
  const payload = msg.payload;
1464
+ if (!payload?.token) {
1465
+ sendError(ws, "invalid_payload", "Missing token in handshake", msg.id);
1466
+ ws.close();
1467
+ return;
1468
+ }
885
1469
  if (!validateToken(payload.token)) {
886
1470
  sendError(ws, "auth_failed", "Invalid token", msg.id);
887
1471
  ws.close();
@@ -893,7 +1477,7 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
893
1477
  id: msg.id,
894
1478
  type: "handshake.ok",
895
1479
  payload: {
896
- version: "0.1.0",
1480
+ version: "0.4.0",
897
1481
  roots,
898
1482
  config: {
899
1483
  provider: config.provider,
@@ -906,6 +1490,10 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
906
1490
  }
907
1491
  case "fs.read": {
908
1492
  const payload = msg.payload;
1493
+ if (!payload?.path) {
1494
+ sendError(ws, "invalid_payload", "Missing path", msg.id);
1495
+ break;
1496
+ }
909
1497
  const result = readFileSafe(payload.path, roots);
910
1498
  if ("error" in result) {
911
1499
  sendError(ws, "fs_error", result.error, msg.id);
@@ -1017,15 +1605,19 @@ function serveToolbarBundle(res) {
1017
1605
  join3(__dirname, "..", "dist", "toolbar", "index.global.js")
1018
1606
  ];
1019
1607
  for (const bundlePath of bundlePaths) {
1020
- if (existsSync3(bundlePath)) {
1021
- const content = readFileSync3(bundlePath, "utf-8");
1022
- res.writeHead(200, {
1023
- "Content-Type": "application/javascript",
1024
- "Access-Control-Allow-Origin": "*",
1025
- "Cache-Control": "no-cache"
1026
- });
1027
- res.end(content);
1028
- return;
1608
+ try {
1609
+ if (existsSync3(bundlePath)) {
1610
+ const content = readFileSync3(bundlePath, "utf-8");
1611
+ res.writeHead(200, {
1612
+ "Content-Type": "application/javascript",
1613
+ "Access-Control-Allow-Origin": "*",
1614
+ "Cache-Control": "no-cache"
1615
+ });
1616
+ res.end(content);
1617
+ return;
1618
+ }
1619
+ } catch {
1620
+ continue;
1029
1621
  }
1030
1622
  }
1031
1623
  res.writeHead(200, {
@@ -1044,6 +1636,8 @@ function serveToolbarBundle(res) {
1044
1636
 
1045
1637
  // src/detect.ts
1046
1638
  import { createConnection } from "net";
1639
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1640
+ import { join as join4 } from "path";
1047
1641
  var COMMON_DEV_PORTS = [
1048
1642
  3e3,
1049
1643
  // React (CRA), Next.js, Express
@@ -1063,8 +1657,16 @@ var COMMON_DEV_PORTS = [
1063
1657
  // Common alternate
1064
1658
  4e3,
1065
1659
  // Phoenix, generic
1066
- 1234
1660
+ 1234,
1067
1661
  // Parcel
1662
+ 4321,
1663
+ // Astro
1664
+ 3333,
1665
+ // Remix
1666
+ 8081,
1667
+ // Metro (React Native)
1668
+ 9e3
1669
+ // generic
1068
1670
  ];
1069
1671
  function checkPort(port, host = "127.0.0.1") {
1070
1672
  return new Promise((resolve3) => {
@@ -1108,9 +1710,169 @@ async function findAvailablePort(startPort) {
1108
1710
  }
1109
1711
  return port;
1110
1712
  }
1713
+ var FRAMEWORK_PATTERNS = [
1714
+ { match: /\bnext\b/, framework: "Next.js", defaultPort: 3e3 },
1715
+ { match: /\bvite\b/, framework: "Vite", defaultPort: 5173 },
1716
+ { match: /\bnuxt\b/, framework: "Nuxt", defaultPort: 3e3 },
1717
+ { match: /\bng\s+serve\b/, framework: "Angular", defaultPort: 4200 },
1718
+ { match: /\bvue-cli-service\s+serve\b/, framework: "Vue CLI", defaultPort: 8080 },
1719
+ { match: /\bsvelte-kit\b/, framework: "SvelteKit", defaultPort: 5173 },
1720
+ { match: /\bastro\b/, framework: "Astro", defaultPort: 4321 },
1721
+ { match: /\bremix\b/, framework: "Remix", defaultPort: 3e3 },
1722
+ { match: /\breact-scripts\s+start\b/, framework: "Create React App", defaultPort: 3e3 },
1723
+ { match: /\bparcel\b/, framework: "Parcel", defaultPort: 1234 },
1724
+ { match: /\bwebpack\s+serve\b|webpack-dev-server/, framework: "Webpack", defaultPort: 8080 },
1725
+ { match: /\bgatsby\b/, framework: "Gatsby", defaultPort: 8e3 },
1726
+ { match: /\bturborepo\b|\bturbo\b.*dev/, framework: "Turborepo", defaultPort: 3e3 },
1727
+ { match: /\bexpo\b/, framework: "Expo", defaultPort: 8081 },
1728
+ { match: /\bnodemon\b|\bts-node\b|\bnode\b/, framework: "Node.js", defaultPort: 3e3 },
1729
+ { match: /\bflask\b/, framework: "Flask", defaultPort: 5e3 },
1730
+ { match: /\bdjango\b|manage\.py\s+runserver/, framework: "Django", defaultPort: 8e3 },
1731
+ { match: /\brails\b/, framework: "Rails", defaultPort: 3e3 },
1732
+ { match: /\bphp\s+.*serve\b|artisan\s+serve/, framework: "PHP/Laravel", defaultPort: 8e3 }
1733
+ ];
1734
+ var DEV_SCRIPT_NAMES = ["dev", "start", "serve", "develop", "dev:start", "start:dev"];
1735
+ function detectDevScripts(cwd = process.cwd()) {
1736
+ const pkgPath = join4(cwd, "package.json");
1737
+ if (!existsSync4(pkgPath)) return [];
1738
+ let pkg;
1739
+ try {
1740
+ pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1741
+ } catch {
1742
+ return [];
1743
+ }
1744
+ if (!pkg.scripts) return [];
1745
+ const scripts = [];
1746
+ for (const name of DEV_SCRIPT_NAMES) {
1747
+ const command = pkg.scripts[name];
1748
+ if (!command) continue;
1749
+ let framework = "Unknown";
1750
+ let defaultPort = 3e3;
1751
+ for (const pattern of FRAMEWORK_PATTERNS) {
1752
+ if (pattern.match.test(command)) {
1753
+ framework = pattern.framework;
1754
+ defaultPort = pattern.defaultPort;
1755
+ break;
1756
+ }
1757
+ }
1758
+ const portMatch = command.match(/(?:--port|-p)\s+(\d+)/);
1759
+ if (portMatch) {
1760
+ defaultPort = parseInt(portMatch[1], 10);
1761
+ }
1762
+ scripts.push({ name, command, framework, defaultPort });
1763
+ }
1764
+ return scripts;
1765
+ }
1766
+ function getProjectName(cwd = process.cwd()) {
1767
+ const pkgPath = join4(cwd, "package.json");
1768
+ if (!existsSync4(pkgPath)) return "this project";
1769
+ try {
1770
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1771
+ return pkg.name || "this project";
1772
+ } catch {
1773
+ return "this project";
1774
+ }
1775
+ }
1776
+ var LOCK_FILES = [
1777
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
1778
+ { file: "yarn.lock", pm: "yarn" },
1779
+ { file: "bun.lockb", pm: "bun" },
1780
+ { file: "bun.lock", pm: "bun" },
1781
+ { file: "package-lock.json", pm: "npm" }
1782
+ ];
1783
+ var INSTALL_COMMANDS = {
1784
+ npm: "npm install",
1785
+ yarn: "yarn install",
1786
+ pnpm: "pnpm install",
1787
+ bun: "bun install"
1788
+ };
1789
+ function checkDependenciesInstalled(cwd = process.cwd()) {
1790
+ const hasNodeModules = existsSync4(join4(cwd, "node_modules"));
1791
+ let pm = "npm";
1792
+ for (const { file, pm: detectedPm } of LOCK_FILES) {
1793
+ if (existsSync4(join4(cwd, file))) {
1794
+ pm = detectedPm;
1795
+ break;
1796
+ }
1797
+ }
1798
+ return {
1799
+ installed: hasNodeModules,
1800
+ packageManager: pm,
1801
+ installCommand: INSTALL_COMMANDS[pm]
1802
+ };
1803
+ }
1111
1804
 
1112
1805
  // src/cli.ts
1113
- var VERSION = "0.1.0";
1806
+ process.on("unhandledRejection", (err) => {
1807
+ console.error(chalk.red("\n [OpenMagic] Unhandled error:"), err?.message || err);
1808
+ console.error(chalk.dim(" Please report this at https://github.com/Kalmuraee/OpenMagic/issues"));
1809
+ });
1810
+ process.on("uncaughtException", (err) => {
1811
+ console.error(chalk.red("\n [OpenMagic] Fatal error:"), err.message);
1812
+ console.error(chalk.dim(" Please report this at https://github.com/Kalmuraee/OpenMagic/issues"));
1813
+ process.exit(1);
1814
+ });
1815
+ var childProcesses = [];
1816
+ var VERSION = "0.4.0";
1817
+ function ask(question) {
1818
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1819
+ return new Promise((resolve3) => {
1820
+ rl.question(question, (answer) => {
1821
+ rl.close();
1822
+ resolve3(answer.trim());
1823
+ });
1824
+ });
1825
+ }
1826
+ function waitForPort(port, timeoutMs = 3e4, shouldAbort) {
1827
+ const start = Date.now();
1828
+ return new Promise((resolve3) => {
1829
+ const check = async () => {
1830
+ if (shouldAbort?.()) {
1831
+ resolve3(false);
1832
+ return;
1833
+ }
1834
+ if (await isPortOpen(port)) {
1835
+ resolve3(true);
1836
+ return;
1837
+ }
1838
+ if (Date.now() - start > timeoutMs) {
1839
+ resolve3(false);
1840
+ return;
1841
+ }
1842
+ setTimeout(check, 500);
1843
+ };
1844
+ check();
1845
+ });
1846
+ }
1847
+ function runCommand(cmd, args, cwd = process.cwd()) {
1848
+ return new Promise((resolve3) => {
1849
+ try {
1850
+ const child = spawn(cmd, args, {
1851
+ cwd,
1852
+ stdio: ["ignore", "pipe", "pipe"],
1853
+ shell: true
1854
+ });
1855
+ child.stdout?.on("data", (data) => {
1856
+ const lines = data.toString().trim().split("\n");
1857
+ for (const line of lines) {
1858
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1859
+ `));
1860
+ }
1861
+ });
1862
+ child.stderr?.on("data", (data) => {
1863
+ const lines = data.toString().trim().split("\n");
1864
+ for (const line of lines) {
1865
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1866
+ `));
1867
+ }
1868
+ });
1869
+ child.on("error", () => resolve3(false));
1870
+ child.on("close", (code) => resolve3(code === 0));
1871
+ } catch {
1872
+ resolve3(false);
1873
+ }
1874
+ });
1875
+ }
1114
1876
  var program = new Command();
1115
1877
  program.name("openmagic").description("AI-powered coding toolbar for any web application").version(VERSION).option("-p, --port <port>", "Dev server port to proxy", "").option(
1116
1878
  "-l, --listen <port>",
@@ -1131,43 +1893,35 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
1131
1893
  targetPort = parseInt(opts.port, 10);
1132
1894
  const isRunning = await isPortOpen(targetPort);
1133
1895
  if (!isRunning) {
1134
- console.log(
1135
- chalk.yellow(
1136
- ` \u26A0 No server found at ${targetHost}:${targetPort}`
1137
- )
1138
- );
1139
- console.log(
1140
- chalk.dim(
1141
- " Start your dev server first, then run openmagic again."
1142
- )
1143
- );
1144
- console.log("");
1145
- process.exit(1);
1896
+ const started = await offerToStartDevServer(targetPort);
1897
+ if (!started) {
1898
+ process.exit(1);
1899
+ }
1146
1900
  }
1147
1901
  } else {
1148
1902
  console.log(chalk.dim(" Scanning for dev server..."));
1149
1903
  const detected = await detectDevServer();
1150
- if (!detected) {
1151
- console.log(
1152
- chalk.yellow(
1153
- " \u26A0 No dev server detected on common ports (3000, 5173, 8080, etc.)"
1154
- )
1155
- );
1156
- console.log("");
1157
- console.log(
1158
- chalk.white(" Specify the port manually:")
1159
- );
1160
- console.log(
1161
- chalk.cyan(" npx openmagic --port 3000")
1162
- );
1163
- console.log("");
1164
- process.exit(1);
1904
+ if (detected) {
1905
+ targetPort = detected.port;
1906
+ targetHost = detected.host;
1907
+ } else {
1908
+ const started = await offerToStartDevServer();
1909
+ if (!started) {
1910
+ process.exit(1);
1911
+ }
1912
+ const redetected = await detectDevServer();
1913
+ if (!redetected) {
1914
+ console.log(chalk.red(" \u2717 Could not detect the dev server after starting."));
1915
+ console.log(chalk.dim(" Try specifying the port: npx openmagic --port 3000"));
1916
+ console.log("");
1917
+ process.exit(1);
1918
+ }
1919
+ targetPort = redetected.port;
1920
+ targetHost = redetected.host;
1165
1921
  }
1166
- targetPort = detected.port;
1167
- targetHost = detected.host;
1168
1922
  }
1169
1923
  console.log(
1170
- chalk.green(` \u2713 Dev server found at ${targetHost}:${targetPort}`)
1924
+ chalk.green(` \u2713 Dev server running at ${targetHost}:${targetPort}`)
1171
1925
  );
1172
1926
  const roots = (opts.root || [process.cwd()]).map(
1173
1927
  (r) => resolve2(r)
@@ -1218,5 +1972,201 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
1218
1972
  process.on("SIGINT", shutdown);
1219
1973
  process.on("SIGTERM", shutdown);
1220
1974
  });
1975
+ async function offerToStartDevServer(expectedPort) {
1976
+ const projectName = getProjectName();
1977
+ const scripts = detectDevScripts();
1978
+ if (scripts.length === 0) {
1979
+ console.log(
1980
+ chalk.yellow(" \u26A0 No dev server detected and no dev scripts found in package.json")
1981
+ );
1982
+ console.log("");
1983
+ console.log(chalk.white(" Start your dev server manually, then run:"));
1984
+ console.log(chalk.cyan(" npx openmagic --port <your-port>"));
1985
+ console.log("");
1986
+ return false;
1987
+ }
1988
+ const deps = checkDependenciesInstalled();
1989
+ if (!deps.installed) {
1990
+ console.log(
1991
+ chalk.yellow(" \u26A0 node_modules/ not found. Dependencies need to be installed.")
1992
+ );
1993
+ console.log("");
1994
+ const answer = await ask(
1995
+ chalk.white(` Run `) + chalk.cyan(deps.installCommand) + chalk.white("? ") + chalk.dim("(Y/n) ")
1996
+ );
1997
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
1998
+ console.log("");
1999
+ console.log(chalk.dim(` Run ${deps.installCommand} manually, then try again.`));
2000
+ console.log("");
2001
+ return false;
2002
+ }
2003
+ console.log("");
2004
+ console.log(chalk.dim(` Installing dependencies with ${deps.packageManager}...`));
2005
+ const [installCmd, ...installArgs] = deps.installCommand.split(" ");
2006
+ const installed = await runCommand(installCmd, installArgs);
2007
+ if (!installed) {
2008
+ console.log(chalk.red(" \u2717 Dependency installation failed."));
2009
+ console.log(chalk.dim(` Try running ${deps.installCommand} manually.`));
2010
+ console.log("");
2011
+ return false;
2012
+ }
2013
+ console.log(chalk.green(" \u2713 Dependencies installed."));
2014
+ console.log("");
2015
+ }
2016
+ let chosen = scripts[0];
2017
+ if (scripts.length === 1) {
2018
+ console.log(
2019
+ chalk.yellow(" \u26A0 No dev server detected.")
2020
+ );
2021
+ console.log("");
2022
+ console.log(
2023
+ chalk.white(` Found `) + chalk.cyan(`npm run ${chosen.name}`) + chalk.white(` in ${projectName}`) + chalk.dim(` (${chosen.framework})`)
2024
+ );
2025
+ console.log(chalk.dim(` \u2192 ${chosen.command}`));
2026
+ console.log("");
2027
+ const answer = await ask(
2028
+ chalk.white(` Start it now? `) + chalk.dim("(Y/n) ")
2029
+ );
2030
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
2031
+ console.log("");
2032
+ console.log(chalk.dim(" Start your dev server first, then run openmagic again."));
2033
+ console.log("");
2034
+ return false;
2035
+ }
2036
+ } else {
2037
+ console.log(
2038
+ chalk.yellow(" \u26A0 No dev server detected.")
2039
+ );
2040
+ console.log("");
2041
+ console.log(
2042
+ chalk.white(` Found ${scripts.length} dev scripts in ${projectName}:`)
2043
+ );
2044
+ console.log("");
2045
+ scripts.forEach((s, i) => {
2046
+ console.log(
2047
+ chalk.cyan(` ${i + 1}) `) + chalk.white(`npm run ${s.name}`) + chalk.dim(` \u2014 ${s.framework} (port ${s.defaultPort})`)
2048
+ );
2049
+ console.log(chalk.dim(` ${s.command}`));
2050
+ });
2051
+ console.log("");
2052
+ const answer = await ask(
2053
+ chalk.white(` Which one to start? `) + chalk.dim(`(1-${scripts.length}, or n to cancel) `)
2054
+ );
2055
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no" || answer === "") {
2056
+ console.log("");
2057
+ console.log(chalk.dim(" Start your dev server first, then run openmagic again."));
2058
+ console.log("");
2059
+ return false;
2060
+ }
2061
+ const idx = parseInt(answer, 10) - 1;
2062
+ if (idx < 0 || idx >= scripts.length || isNaN(idx)) {
2063
+ chosen = scripts[0];
2064
+ } else {
2065
+ chosen = scripts[idx];
2066
+ }
2067
+ }
2068
+ const port = expectedPort || chosen.defaultPort;
2069
+ console.log("");
2070
+ console.log(
2071
+ chalk.dim(` Starting `) + chalk.cyan(`npm run ${chosen.name}`) + chalk.dim("...")
2072
+ );
2073
+ const depsInfo = checkDependenciesInstalled();
2074
+ const runCmd = depsInfo.packageManager === "yarn" ? "yarn" : depsInfo.packageManager === "pnpm" ? "pnpm" : depsInfo.packageManager === "bun" ? "bun" : "npm";
2075
+ const runArgs = runCmd === "npm" ? ["run", chosen.name] : [chosen.name];
2076
+ let child;
2077
+ try {
2078
+ child = spawn(runCmd, runArgs, {
2079
+ cwd: process.cwd(),
2080
+ stdio: ["ignore", "pipe", "pipe"],
2081
+ detached: false,
2082
+ shell: true,
2083
+ env: {
2084
+ ...process.env,
2085
+ PORT: String(port),
2086
+ BROWSER: "none",
2087
+ BROWSER_NONE: "true"
2088
+ }
2089
+ });
2090
+ } catch (e) {
2091
+ console.log(chalk.red(` \u2717 Failed to start: ${e.message}`));
2092
+ return false;
2093
+ }
2094
+ childProcesses.push(child);
2095
+ let childExited = false;
2096
+ child.stdout?.on("data", (data) => {
2097
+ for (const line of data.toString().trim().split("\n")) {
2098
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
2099
+ `));
2100
+ }
2101
+ });
2102
+ child.stderr?.on("data", (data) => {
2103
+ for (const line of data.toString().trim().split("\n")) {
2104
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
2105
+ `));
2106
+ }
2107
+ });
2108
+ child.on("error", (err) => {
2109
+ childExited = true;
2110
+ console.log(chalk.red(` \u2717 Failed to start: ${err.message}`));
2111
+ });
2112
+ child.on("exit", (code) => {
2113
+ childExited = true;
2114
+ if (code !== null && code !== 0) {
2115
+ console.log(chalk.red(` \u2717 Dev server exited with code ${code}`));
2116
+ }
2117
+ });
2118
+ const cleanup = () => {
2119
+ for (const cp of childProcesses) {
2120
+ try {
2121
+ cp.kill("SIGTERM");
2122
+ } catch {
2123
+ }
2124
+ }
2125
+ setTimeout(() => {
2126
+ for (const cp of childProcesses) {
2127
+ try {
2128
+ cp.kill("SIGKILL");
2129
+ } catch {
2130
+ }
2131
+ }
2132
+ }, 3e3);
2133
+ };
2134
+ process.on("exit", cleanup);
2135
+ process.on("SIGINT", cleanup);
2136
+ process.on("SIGTERM", cleanup);
2137
+ console.log(
2138
+ chalk.dim(` Waiting for port ${port}...`)
2139
+ );
2140
+ const isUp = await waitForPort(port, 3e4, () => childExited);
2141
+ if (childExited && !isUp) {
2142
+ console.log(
2143
+ chalk.red(` \u2717 Dev server exited before it was ready.`)
2144
+ );
2145
+ console.log(
2146
+ chalk.dim(` Check the error output above and fix the issue.`)
2147
+ );
2148
+ console.log("");
2149
+ return false;
2150
+ }
2151
+ if (!isUp) {
2152
+ console.log(
2153
+ chalk.yellow(` \u26A0 Port ${port} didn't open after 30s.`)
2154
+ );
2155
+ console.log(
2156
+ chalk.dim(` The server might use a different port. Check the output above.`)
2157
+ );
2158
+ console.log("");
2159
+ const detected = await detectDevServer();
2160
+ if (detected) {
2161
+ console.log(
2162
+ chalk.green(` \u2713 Found server on port ${detected.port} instead.`)
2163
+ );
2164
+ return true;
2165
+ }
2166
+ return false;
2167
+ }
2168
+ console.log("");
2169
+ return true;
2170
+ }
1221
2171
  program.parse();
1222
2172
  //# sourceMappingURL=cli.js.map