openmagic 0.9.0 → 0.11.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
@@ -10,7 +10,8 @@ import { createInterface } from "readline";
10
10
 
11
11
  // src/proxy.ts
12
12
  import http from "http";
13
- import { createGunzip, createInflate, createBrotliDecompress } from "zlib";
13
+ import { gunzip, inflate, brotliDecompress } from "zlib";
14
+ import { promisify } from "util";
14
15
  import httpProxy from "http-proxy";
15
16
 
16
17
  // src/security.ts
@@ -30,158 +31,14 @@ function validateToken(token) {
30
31
  return token === sessionToken;
31
32
  }
32
33
 
33
- // src/proxy.ts
34
- function createProxyServer(targetHost, targetPort, serverPort) {
35
- const proxy = httpProxy.createProxyServer({
36
- target: `http://${targetHost}:${targetPort}`,
37
- ws: true,
38
- selfHandleResponse: true
39
- });
40
- const token = getSessionToken();
41
- proxy.on("proxyRes", (proxyRes, req, res) => {
42
- const contentType = proxyRes.headers["content-type"] || "";
43
- const isHtml = contentType.includes("text/html");
44
- if (!isHtml) {
45
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
46
- proxyRes.pipe(res);
47
- return;
48
- }
49
- collectBody(proxyRes).then((body) => {
50
- const toolbarScript = buildInjectionScript(serverPort, token);
51
- if (body.includes("</body>")) {
52
- body = body.replace("</body>", `${toolbarScript}</body>`);
53
- } else if (body.includes("</html>")) {
54
- body = body.replace("</html>", `${toolbarScript}</html>`);
55
- } else {
56
- body += toolbarScript;
57
- }
58
- const headers = { ...proxyRes.headers };
59
- delete headers["content-length"];
60
- delete headers["content-encoding"];
61
- delete headers["transfer-encoding"];
62
- res.writeHead(proxyRes.statusCode || 200, headers);
63
- res.end(body);
64
- }).catch(() => {
65
- try {
66
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
67
- res.end();
68
- } catch {
69
- }
70
- });
71
- });
72
- proxy.on("error", (err, _req, res) => {
73
- if (res instanceof http.ServerResponse && !res.headersSent) {
74
- const toolbarScript = buildInjectionScript(serverPort, token);
75
- res.writeHead(502, { "Content-Type": "text/html" });
76
- res.end(
77
- `<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
78
- <h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
79
- <p>Could not reach <code>${targetHost}:${targetPort}</code></p>
80
- <p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
81
- <p style="color:#666;font-size:13px;">${err.message}</p>
82
- ${toolbarScript}
83
- </body></html>`
84
- );
85
- }
86
- });
87
- const server = http.createServer((req, res) => {
88
- if (req.url?.startsWith("/__openmagic__/")) {
89
- handleToolbarAsset(req, res, serverPort);
90
- return;
91
- }
92
- proxy.web(req, res);
93
- });
94
- server.on("upgrade", (req, socket, head) => {
95
- if (req.url?.startsWith("/__openmagic__")) {
96
- return;
97
- }
98
- proxy.ws(req, socket, head);
99
- });
100
- return server;
101
- }
102
- function collectBody(stream) {
103
- return new Promise((resolve3, reject) => {
104
- const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
105
- const chunks = [];
106
- let source = stream;
107
- if (encoding === "gzip" || encoding === "x-gzip") {
108
- const gunzip = createGunzip();
109
- stream.pipe(gunzip);
110
- source = gunzip;
111
- gunzip.on("error", () => {
112
- collectRaw(stream).then(resolve3).catch(reject);
113
- });
114
- } else if (encoding === "deflate") {
115
- const inflate = createInflate();
116
- stream.pipe(inflate);
117
- source = inflate;
118
- inflate.on("error", () => {
119
- collectRaw(stream).then(resolve3).catch(reject);
120
- });
121
- } else if (encoding === "br") {
122
- const brotli = createBrotliDecompress();
123
- stream.pipe(brotli);
124
- source = brotli;
125
- brotli.on("error", () => {
126
- collectRaw(stream).then(resolve3).catch(reject);
127
- });
128
- }
129
- source.on("data", (chunk) => chunks.push(chunk));
130
- source.on("end", () => {
131
- try {
132
- resolve3(Buffer.concat(chunks).toString("utf-8"));
133
- } catch {
134
- reject(new Error("Failed to decode response body"));
135
- }
136
- });
137
- source.on("error", (err) => reject(err));
138
- });
139
- }
140
- function collectRaw(stream) {
141
- return new Promise((resolve3, reject) => {
142
- const chunks = [];
143
- stream.on("data", (chunk) => chunks.push(chunk));
144
- stream.on("end", () => {
145
- try {
146
- resolve3(Buffer.concat(chunks).toString("utf-8"));
147
- } catch {
148
- reject(new Error("Failed to decode raw body"));
149
- }
150
- });
151
- stream.on("error", reject);
152
- });
153
- }
154
- function handleToolbarAsset(_req, res, _serverPort) {
155
- res.writeHead(404, { "Content-Type": "text/plain" });
156
- res.end("Not found");
157
- }
158
- function buildInjectionScript(serverPort, token) {
159
- return `
160
- <script data-openmagic="true">
161
- (function() {
162
- if (window.__OPENMAGIC_LOADED__) return;
163
- window.__OPENMAGIC_LOADED__ = true;
164
- window.__OPENMAGIC_CONFIG__ = {
165
- wsPort: ${serverPort},
166
- token: "${token}"
167
- };
168
- var script = document.createElement("script");
169
- script.src = "http://127.0.0.1:${serverPort}/__openmagic__/toolbar.js";
170
- script.dataset.openmagic = "true";
171
- document.body.appendChild(script);
172
- })();
173
- </script>`;
174
- }
175
-
176
34
  // src/server.ts
177
- import http2 from "http";
178
35
  import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
179
36
  import { join as join3, dirname as dirname2 } from "path";
180
37
  import { fileURLToPath } from "url";
181
38
  import { WebSocketServer, WebSocket } from "ws";
182
39
 
183
40
  // src/config.ts
184
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
41
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from "fs";
185
42
  import { join } from "path";
186
43
  import { homedir } from "os";
187
44
  var CONFIG_DIR = join(homedir(), ".openmagic");
@@ -208,7 +65,9 @@ function saveConfig(updates) {
208
65
  ensureConfigDir();
209
66
  const existing = loadConfig();
210
67
  const merged = { ...existing, ...updates };
211
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
68
+ const tmpFile = CONFIG_FILE + ".tmp";
69
+ writeFileSync(tmpFile, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
70
+ renameSync(tmpFile, CONFIG_FILE);
212
71
  } catch (e) {
213
72
  console.warn(`[OpenMagic] Warning: Could not save config to ${CONFIG_FILE}: ${e.message}`);
214
73
  }
@@ -260,7 +119,8 @@ function isPathSafe(filePath, roots) {
260
119
  const resolved = resolve(filePath);
261
120
  return roots.some((root) => {
262
121
  const resolvedRoot = resolve(root);
263
- return resolved.startsWith(resolvedRoot);
122
+ const rel = relative(resolvedRoot, resolved);
123
+ return !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
264
124
  });
265
125
  }
266
126
  function readFileSafe(filePath, roots) {
@@ -1093,8 +953,10 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
1093
953
  const apiMessages = [
1094
954
  { role: "system", content: SYSTEM_PROMPT }
1095
955
  ];
1096
- for (const msg of messages) {
1097
- if (msg.role === "user" && typeof msg.content === "string") {
956
+ const lastUserIdx = messages.reduce((acc, m, i) => m.role === "user" ? i : acc, -1);
957
+ for (let i = 0; i < messages.length; i++) {
958
+ const msg = messages[i];
959
+ if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
1098
960
  const contextParts = {};
1099
961
  if (context.selectedElement) {
1100
962
  contextParts.selectedElement = context.selectedElement.outerHTML;
@@ -1128,6 +990,8 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
1128
990
  } else {
1129
991
  apiMessages.push({ role: "user", content: enrichedContent });
1130
992
  }
993
+ } else if (msg.role === "system") {
994
+ continue;
1131
995
  } else {
1132
996
  apiMessages.push({
1133
997
  role: msg.role,
@@ -1219,9 +1083,11 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
1219
1083
  async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone, onError) {
1220
1084
  const url = "https://api.anthropic.com/v1/messages";
1221
1085
  const apiMessages = [];
1222
- for (const msg of messages) {
1086
+ const lastUserIdx = messages.reduce((acc, m, i) => m.role === "user" ? i : acc, -1);
1087
+ for (let i = 0; i < messages.length; i++) {
1088
+ const msg = messages[i];
1223
1089
  if (msg.role === "system") continue;
1224
- if (msg.role === "user" && typeof msg.content === "string") {
1090
+ if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
1225
1091
  const contextParts = {};
1226
1092
  if (context.selectedElement) {
1227
1093
  contextParts.selectedElement = context.selectedElement.outerHTML;
@@ -1346,10 +1212,12 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
1346
1212
  async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onError) {
1347
1213
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}&alt=sse`;
1348
1214
  const contents = [];
1349
- for (const msg of messages) {
1215
+ const lastUserIdx = messages.reduce((acc, m, i) => m.role === "user" ? i : acc, -1);
1216
+ for (let i = 0; i < messages.length; i++) {
1217
+ const msg = messages[i];
1350
1218
  if (msg.role === "system") continue;
1351
1219
  const role = msg.role === "assistant" ? "model" : "user";
1352
- if (msg.role === "user" && typeof msg.content === "string") {
1220
+ if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
1353
1221
  const contextParts = {};
1354
1222
  if (context.selectedElement) {
1355
1223
  contextParts.selectedElement = context.selectedElement.outerHTML;
@@ -1391,9 +1259,7 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
1391
1259
  const generationConfig = {
1392
1260
  maxOutputTokens: 8192
1393
1261
  };
1394
- if (thinkingLevel && thinkingLevel !== "none") {
1395
- generationConfig.thinking_level = thinkingLevel.toUpperCase();
1396
- }
1262
+ const thinkingConfig = thinkingLevel && thinkingLevel !== "none" ? { thinkingLevel: thinkingLevel.toUpperCase() } : void 0;
1397
1263
  const body = {
1398
1264
  system_instruction: {
1399
1265
  parts: [{ text: SYSTEM_PROMPT }]
@@ -1401,6 +1267,9 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
1401
1267
  contents,
1402
1268
  generationConfig
1403
1269
  };
1270
+ if (thinkingConfig) {
1271
+ body.thinkingConfig = thinkingConfig;
1272
+ }
1404
1273
  try {
1405
1274
  const response = await fetch(url, {
1406
1275
  method: "POST",
@@ -1512,30 +1381,36 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
1512
1381
  }
1513
1382
 
1514
1383
  // src/server.ts
1384
+ var VERSION = "0.11.0";
1515
1385
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1516
- function createOpenMagicServer(proxyPort, roots) {
1517
- const httpServer = http2.createServer((req, res) => {
1386
+ function attachOpenMagic(httpServer, roots) {
1387
+ function handleRequest(req, res) {
1388
+ if (!req.url?.startsWith("/__openmagic__/")) return false;
1518
1389
  if (req.url === "/__openmagic__/toolbar.js") {
1519
1390
  serveToolbarBundle(res);
1520
- return;
1391
+ return true;
1521
1392
  }
1522
1393
  if (req.url === "/__openmagic__/health") {
1523
1394
  res.writeHead(200, {
1524
1395
  "Content-Type": "application/json",
1525
1396
  "Access-Control-Allow-Origin": "*"
1526
1397
  });
1527
- res.end(JSON.stringify({ status: "ok", version: "0.9.0" }));
1528
- return;
1398
+ res.end(JSON.stringify({ status: "ok", version: VERSION }));
1399
+ return true;
1529
1400
  }
1530
- res.writeHead(404);
1531
- res.end("Not found");
1532
- });
1401
+ return false;
1402
+ }
1533
1403
  const wss = new WebSocketServer({
1534
1404
  server: httpServer,
1535
1405
  path: "/__openmagic__/ws"
1536
1406
  });
1537
1407
  const clientStates = /* @__PURE__ */ new WeakMap();
1538
- wss.on("connection", (ws) => {
1408
+ wss.on("connection", (ws, req) => {
1409
+ const origin = req.headers.origin || "";
1410
+ if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("http://127.0.0.1")) {
1411
+ ws.close(4003, "Forbidden origin");
1412
+ return;
1413
+ }
1539
1414
  clientStates.set(ws, { authenticated: false });
1540
1415
  ws.on("message", async (data) => {
1541
1416
  let msg;
@@ -1551,7 +1426,7 @@ function createOpenMagicServer(proxyPort, roots) {
1551
1426
  return;
1552
1427
  }
1553
1428
  try {
1554
- await handleMessage(ws, msg, state, roots, proxyPort);
1429
+ await handleMessage(ws, msg, state, roots);
1555
1430
  } catch (e) {
1556
1431
  sendError(ws, "internal_error", e.message, msg.id);
1557
1432
  }
@@ -1560,9 +1435,9 @@ function createOpenMagicServer(proxyPort, roots) {
1560
1435
  clientStates.delete(ws);
1561
1436
  });
1562
1437
  });
1563
- return { httpServer, wss };
1438
+ return { wss, handleRequest };
1564
1439
  }
1565
- async function handleMessage(ws, msg, state, roots, _proxyPort) {
1440
+ async function handleMessage(ws, msg, state, roots) {
1566
1441
  switch (msg.type) {
1567
1442
  case "handshake": {
1568
1443
  const payload = msg.payload;
@@ -1582,12 +1457,13 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1582
1457
  id: msg.id,
1583
1458
  type: "handshake.ok",
1584
1459
  payload: {
1585
- version: "0.9.0",
1460
+ version: VERSION,
1586
1461
  roots,
1587
1462
  config: {
1588
1463
  provider: config.provider,
1589
1464
  model: config.model,
1590
- hasApiKey: !!config.apiKey
1465
+ hasApiKey: !!config.apiKey,
1466
+ apiKeys: config.apiKeys || {}
1591
1467
  }
1592
1468
  }
1593
1469
  });
@@ -1613,12 +1489,20 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1613
1489
  }
1614
1490
  case "fs.write": {
1615
1491
  const payload = msg.payload;
1616
- const result = writeFileSafe(payload.path, payload.content, roots);
1617
- send(ws, {
1618
- id: msg.id,
1619
- type: "fs.written",
1620
- payload: { path: payload.path, ok: result.ok, error: result.error }
1621
- });
1492
+ if (!payload?.path || payload.content === void 0) {
1493
+ sendError(ws, "invalid_payload", "Missing path or content", msg.id);
1494
+ break;
1495
+ }
1496
+ const writeResult = writeFileSafe(payload.path, payload.content, roots);
1497
+ if (!writeResult.ok) {
1498
+ sendError(ws, "fs_error", writeResult.error || "Write failed", msg.id);
1499
+ } else {
1500
+ send(ws, {
1501
+ id: msg.id,
1502
+ type: "fs.written",
1503
+ payload: { path: payload.path, ok: true }
1504
+ });
1505
+ }
1622
1506
  break;
1623
1507
  }
1624
1508
  case "fs.list": {
@@ -1635,15 +1519,18 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1635
1519
  case "llm.chat": {
1636
1520
  const payload = msg.payload;
1637
1521
  const config = loadConfig();
1638
- if (!config.apiKey) {
1522
+ const provider = payload.provider || config.provider || "openai";
1523
+ const apiKey = config.apiKeys?.[provider] || config.apiKey || "";
1524
+ const providerMeta = MODEL_REGISTRY?.[provider];
1525
+ if (!apiKey && !providerMeta?.local) {
1639
1526
  sendError(ws, "config_error", "API key not configured", msg.id);
1640
1527
  return;
1641
1528
  }
1642
1529
  await handleLlmChat(
1643
1530
  {
1644
- provider: payload.provider || config.provider || "openai",
1531
+ provider,
1645
1532
  model: payload.model || config.model || "gpt-4o",
1646
- apiKey: config.apiKey,
1533
+ apiKey,
1647
1534
  messages: payload.messages,
1648
1535
  context: payload.context
1649
1536
  },
@@ -1667,8 +1554,11 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1667
1554
  payload: {
1668
1555
  provider: config.provider,
1669
1556
  model: config.model,
1670
- hasApiKey: !!config.apiKey,
1671
- roots: config.roots || roots
1557
+ hasApiKey: !!(config.apiKeys?.[config.provider || ""] || config.apiKey),
1558
+ roots: config.roots || roots,
1559
+ apiKeys: Object.fromEntries(
1560
+ Object.entries(config.apiKeys || {}).map(([k]) => [k, true])
1561
+ )
1672
1562
  }
1673
1563
  });
1674
1564
  break;
@@ -1678,8 +1568,15 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1678
1568
  const updates = {};
1679
1569
  if (payload.provider !== void 0) updates.provider = payload.provider;
1680
1570
  if (payload.model !== void 0) updates.model = payload.model;
1681
- if (payload.apiKey !== void 0) updates.apiKey = payload.apiKey;
1682
- if (payload.roots !== void 0) updates.roots = payload.roots;
1571
+ if (payload.apiKey !== void 0 && payload.provider) {
1572
+ const existing = loadConfig();
1573
+ const apiKeys = { ...existing.apiKeys || {} };
1574
+ apiKeys[payload.provider] = payload.apiKey;
1575
+ updates.apiKeys = apiKeys;
1576
+ updates.apiKey = payload.apiKey;
1577
+ } else if (payload.apiKey !== void 0) {
1578
+ updates.apiKey = payload.apiKey;
1579
+ }
1683
1580
  saveConfig(updates);
1684
1581
  send(ws, {
1685
1582
  id: msg.id,
@@ -1698,11 +1595,7 @@ function send(ws, msg) {
1698
1595
  }
1699
1596
  }
1700
1597
  function sendError(ws, code, message, id) {
1701
- send(ws, {
1702
- id: id || "error",
1703
- type: "error",
1704
- payload: { code, message }
1705
- });
1598
+ send(ws, { id: id || "error", type: "error", payload: { code, message } });
1706
1599
  }
1707
1600
  function serveToolbarBundle(res) {
1708
1601
  const bundlePaths = [
@@ -1729,14 +1622,124 @@ function serveToolbarBundle(res) {
1729
1622
  "Content-Type": "application/javascript",
1730
1623
  "Access-Control-Allow-Origin": "*"
1731
1624
  });
1732
- res.end(`
1733
- (function() {
1734
- var div = document.createElement("div");
1735
- div.style.cssText = "position:fixed;bottom:20px;right:20px;background:#1a1a2e;color:#e94560;padding:16px 24px;border-radius:12px;font-family:system-ui;font-size:14px;z-index:2147483647;box-shadow:0 4px 24px rgba(0,0,0,0.3);";
1736
- div.textContent = "OpenMagic: Toolbar bundle not found. Run 'npm run build:toolbar' first.";
1737
- document.body.appendChild(div);
1738
- })();
1739
- `);
1625
+ res.end(`(function(){var d=document.createElement("div");d.style.cssText="position:fixed;bottom:20px;right:20px;background:#1a1a2e;color:#e94560;padding:16px 24px;border-radius:12px;font-family:system-ui;font-size:14px;z-index:2147483647;box-shadow:0 4px 24px rgba(0,0,0,0.3);";d.textContent="OpenMagic: Toolbar bundle not found.";document.body.appendChild(d);})();`);
1626
+ }
1627
+
1628
+ // src/proxy.ts
1629
+ var gunzipAsync = promisify(gunzip);
1630
+ var inflateAsync = promisify(inflate);
1631
+ var brotliAsync = promisify(brotliDecompress);
1632
+ function createProxyServer(targetHost, targetPort, roots) {
1633
+ const proxy = httpProxy.createProxyServer({
1634
+ target: `http://${targetHost}:${targetPort}`,
1635
+ ws: true,
1636
+ selfHandleResponse: true
1637
+ });
1638
+ const token = getSessionToken();
1639
+ proxy.on("proxyRes", (proxyRes, req, res) => {
1640
+ const contentType = proxyRes.headers["content-type"] || "";
1641
+ const isHtml = contentType.includes("text/html");
1642
+ if (!isHtml) {
1643
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
1644
+ proxyRes.pipe(res);
1645
+ return;
1646
+ }
1647
+ collectBody(proxyRes).then((body) => {
1648
+ const toolbarScript = buildInjectionScript(token);
1649
+ if (body.includes("</body>")) {
1650
+ body = body.replace("</body>", `${toolbarScript}</body>`);
1651
+ } else if (body.includes("</html>")) {
1652
+ body = body.replace("</html>", `${toolbarScript}</html>`);
1653
+ } else {
1654
+ body += toolbarScript;
1655
+ }
1656
+ const headers = { ...proxyRes.headers };
1657
+ delete headers["content-length"];
1658
+ delete headers["content-encoding"];
1659
+ delete headers["transfer-encoding"];
1660
+ delete headers["content-security-policy"];
1661
+ delete headers["content-security-policy-report-only"];
1662
+ delete headers["x-content-security-policy"];
1663
+ delete headers["etag"];
1664
+ delete headers["last-modified"];
1665
+ res.writeHead(proxyRes.statusCode || 200, headers);
1666
+ res.end(body);
1667
+ }).catch(() => {
1668
+ try {
1669
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
1670
+ res.end();
1671
+ } catch {
1672
+ }
1673
+ });
1674
+ });
1675
+ proxy.on("error", (err, _req, res) => {
1676
+ if (res instanceof http.ServerResponse && !res.headersSent) {
1677
+ const toolbarScript = buildInjectionScript(token);
1678
+ res.writeHead(502, { "Content-Type": "text/html" });
1679
+ res.end(
1680
+ `<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
1681
+ <h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
1682
+ <p>Could not reach <code>${targetHost}:${targetPort}</code></p>
1683
+ <p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
1684
+ <p style="color:#666;font-size:13px;">${err.message}</p>
1685
+ ${toolbarScript}
1686
+ </body></html>`
1687
+ );
1688
+ }
1689
+ });
1690
+ let omHandle = null;
1691
+ const server = http.createServer((req, res) => {
1692
+ if (omHandle && omHandle(req, res)) return;
1693
+ proxy.web(req, res);
1694
+ });
1695
+ const om = attachOpenMagic(server, roots);
1696
+ omHandle = om.handleRequest;
1697
+ server.on("upgrade", (req, socket, head) => {
1698
+ if (req.url?.startsWith("/__openmagic__")) return;
1699
+ proxy.ws(req, socket, head);
1700
+ });
1701
+ return server;
1702
+ }
1703
+ async function collectBody(stream) {
1704
+ const rawBuffer = await new Promise((resolve3, reject) => {
1705
+ const chunks = [];
1706
+ stream.on("data", (chunk) => chunks.push(chunk));
1707
+ stream.on("end", () => resolve3(Buffer.concat(chunks)));
1708
+ stream.on("error", reject);
1709
+ });
1710
+ const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
1711
+ if (!encoding || encoding === "identity") {
1712
+ return rawBuffer.toString("utf-8");
1713
+ }
1714
+ try {
1715
+ let decompressed;
1716
+ if (encoding === "gzip" || encoding === "x-gzip") {
1717
+ decompressed = await gunzipAsync(rawBuffer);
1718
+ } else if (encoding === "deflate") {
1719
+ decompressed = await inflateAsync(rawBuffer);
1720
+ } else if (encoding === "br") {
1721
+ decompressed = await brotliAsync(rawBuffer);
1722
+ } else {
1723
+ return rawBuffer.toString("utf-8");
1724
+ }
1725
+ return decompressed.toString("utf-8");
1726
+ } catch {
1727
+ return rawBuffer.toString("utf-8");
1728
+ }
1729
+ }
1730
+ function buildInjectionScript(token) {
1731
+ return `
1732
+ <script data-openmagic="true">
1733
+ (function() {
1734
+ if (window.__OPENMAGIC_LOADED__) return;
1735
+ window.__OPENMAGIC_LOADED__ = true;
1736
+ window.__OPENMAGIC_TOKEN__ = "${token}";
1737
+ var s = document.createElement("script");
1738
+ s.src = "/__openmagic__/toolbar.js";
1739
+ s.dataset.openmagic = "true";
1740
+ document.body.appendChild(s);
1741
+ })();
1742
+ </script>`;
1740
1743
  }
1741
1744
 
1742
1745
  // src/detect.ts
@@ -1913,7 +1916,7 @@ process.on("uncaughtException", (err) => {
1913
1916
  process.exit(1);
1914
1917
  });
1915
1918
  var childProcesses = [];
1916
- var VERSION = "0.9.0";
1919
+ var VERSION2 = "0.11.0";
1917
1920
  function ask(question) {
1918
1921
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1919
1922
  return new Promise((resolve3) => {
@@ -2025,7 +2028,7 @@ function formatDevServerLine(line) {
2025
2028
  return chalk.dim(` \u2502 ${trimmed}`);
2026
2029
  }
2027
2030
  var program = new Command();
2028
- program.name("openmagic").description("AI-powered coding toolbar for any web application").version(VERSION).option("-p, --port <port>", "Dev server port to proxy", "").option(
2031
+ program.name("openmagic").description("AI-powered coding toolbar for any web application").version(VERSION2).option("-p, --port <port>", "Dev server port to proxy", "").option(
2029
2032
  "-l, --listen <port>",
2030
2033
  "Port for the OpenMagic proxy",
2031
2034
  "4567"
@@ -2035,7 +2038,7 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2035
2038
  ).option("--no-open", "Don't auto-open browser").option("--host <host>", "Dev server host", "127.0.0.1").action(async (opts) => {
2036
2039
  console.log("");
2037
2040
  console.log(
2038
- chalk.bold.magenta(" \u2728 OpenMagic") + chalk.dim(` v${VERSION}`)
2041
+ chalk.bold.magenta(" \u2728 OpenMagic") + chalk.dim(` v${VERSION2}`)
2039
2042
  );
2040
2043
  console.log("");
2041
2044
  let targetPort;
@@ -2048,6 +2051,11 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2048
2051
  if (!started) {
2049
2052
  process.exit(1);
2050
2053
  }
2054
+ const recheck = await detectDevServer();
2055
+ if (recheck) {
2056
+ targetPort = recheck.port;
2057
+ targetHost = recheck.host;
2058
+ }
2051
2059
  }
2052
2060
  } else {
2053
2061
  console.log(chalk.dim(" Scanning for dev server..."));
@@ -2079,28 +2087,20 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2079
2087
  );
2080
2088
  const config = loadConfig();
2081
2089
  saveConfig({ ...config, roots, targetPort });
2082
- const token = generateSessionToken();
2090
+ generateSessionToken();
2083
2091
  let proxyPort = parseInt(opts.listen, 10);
2084
- while (await isPortOpen(proxyPort) || await isPortOpen(proxyPort + 1)) {
2092
+ while (await isPortOpen(proxyPort)) {
2085
2093
  proxyPort++;
2086
2094
  if (proxyPort > parseInt(opts.listen, 10) + 100) {
2087
- console.log(chalk.red(" Could not find two consecutive free ports."));
2095
+ console.log(chalk.red(" Could not find an available port."));
2088
2096
  process.exit(1);
2089
2097
  }
2090
2098
  }
2091
- const companionPort = proxyPort + 1;
2092
- const { httpServer: omServer } = createOpenMagicServer(companionPort, roots);
2093
- omServer.listen(companionPort, "127.0.0.1", () => {
2094
- });
2095
- const proxyServer = createProxyServer(
2096
- targetHost,
2097
- targetPort,
2098
- companionPort
2099
- );
2099
+ const proxyServer = createProxyServer(targetHost, targetPort, roots);
2100
2100
  proxyServer.listen(proxyPort, "127.0.0.1", async () => {
2101
2101
  console.log("");
2102
2102
  console.log(
2103
- chalk.bold.green(` \u{1F680} Proxy running at \u2192 `) + chalk.bold.underline.cyan(`http://localhost:${proxyPort}`)
2103
+ chalk.bold.green(` Proxy running at \u2192 `) + chalk.bold.underline.cyan(`http://localhost:${proxyPort}`)
2104
2104
  );
2105
2105
  console.log("");
2106
2106
  await healthCheck(proxyPort, targetPort);
@@ -2117,16 +2117,10 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2117
2117
  });
2118
2118
  }
2119
2119
  });
2120
- proxyServer.on("upgrade", (req, socket, head) => {
2121
- if (req.url?.startsWith("/__openmagic__")) {
2122
- omServer.emit("upgrade", req, socket, head);
2123
- }
2124
- });
2125
2120
  const shutdown = () => {
2126
2121
  console.log("");
2127
2122
  console.log(chalk.dim(" Shutting down OpenMagic..."));
2128
2123
  proxyServer.close();
2129
- omServer.close();
2130
2124
  process.exit(0);
2131
2125
  };
2132
2126
  process.on("SIGINT", shutdown);