openmagic 0.10.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
@@ -31,134 +31,7 @@ function validateToken(token) {
31
31
  return token === sessionToken;
32
32
  }
33
33
 
34
- // src/proxy.ts
35
- var gunzipAsync = promisify(gunzip);
36
- var inflateAsync = promisify(inflate);
37
- var brotliAsync = promisify(brotliDecompress);
38
- function createProxyServer(targetHost, targetPort, serverPort) {
39
- const proxy = httpProxy.createProxyServer({
40
- target: `http://${targetHost}:${targetPort}`,
41
- ws: true,
42
- selfHandleResponse: true
43
- });
44
- const token = getSessionToken();
45
- proxy.on("proxyRes", (proxyRes, req, res) => {
46
- const contentType = proxyRes.headers["content-type"] || "";
47
- const isHtml = contentType.includes("text/html");
48
- if (!isHtml) {
49
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
50
- proxyRes.pipe(res);
51
- return;
52
- }
53
- collectBody(proxyRes).then((body) => {
54
- const toolbarScript = buildInjectionScript(serverPort, token);
55
- if (body.includes("</body>")) {
56
- body = body.replace("</body>", `${toolbarScript}</body>`);
57
- } else if (body.includes("</html>")) {
58
- body = body.replace("</html>", `${toolbarScript}</html>`);
59
- } else {
60
- body += toolbarScript;
61
- }
62
- const headers = { ...proxyRes.headers };
63
- delete headers["content-length"];
64
- delete headers["content-encoding"];
65
- delete headers["transfer-encoding"];
66
- delete headers["content-security-policy"];
67
- delete headers["content-security-policy-report-only"];
68
- delete headers["x-content-security-policy"];
69
- delete headers["etag"];
70
- delete headers["last-modified"];
71
- res.writeHead(proxyRes.statusCode || 200, headers);
72
- res.end(body);
73
- }).catch(() => {
74
- try {
75
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
76
- res.end();
77
- } catch {
78
- }
79
- });
80
- });
81
- proxy.on("error", (err, _req, res) => {
82
- if (res instanceof http.ServerResponse && !res.headersSent) {
83
- const toolbarScript = buildInjectionScript(serverPort, token);
84
- res.writeHead(502, { "Content-Type": "text/html" });
85
- res.end(
86
- `<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
87
- <h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
88
- <p>Could not reach <code>${targetHost}:${targetPort}</code></p>
89
- <p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
90
- <p style="color:#666;font-size:13px;">${err.message}</p>
91
- ${toolbarScript}
92
- </body></html>`
93
- );
94
- }
95
- });
96
- const server = http.createServer((req, res) => {
97
- if (req.url?.startsWith("/__openmagic__/")) {
98
- handleToolbarAsset(req, res, serverPort);
99
- return;
100
- }
101
- proxy.web(req, res);
102
- });
103
- server.on("upgrade", (req, socket, head) => {
104
- if (req.url?.startsWith("/__openmagic__")) {
105
- return;
106
- }
107
- proxy.ws(req, socket, head);
108
- });
109
- return server;
110
- }
111
- async function collectBody(stream) {
112
- const rawBuffer = await new Promise((resolve3, reject) => {
113
- const chunks = [];
114
- stream.on("data", (chunk) => chunks.push(chunk));
115
- stream.on("end", () => resolve3(Buffer.concat(chunks)));
116
- stream.on("error", reject);
117
- });
118
- const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
119
- if (!encoding || encoding === "identity") {
120
- return rawBuffer.toString("utf-8");
121
- }
122
- try {
123
- let decompressed;
124
- if (encoding === "gzip" || encoding === "x-gzip") {
125
- decompressed = await gunzipAsync(rawBuffer);
126
- } else if (encoding === "deflate") {
127
- decompressed = await inflateAsync(rawBuffer);
128
- } else if (encoding === "br") {
129
- decompressed = await brotliAsync(rawBuffer);
130
- } else {
131
- return rawBuffer.toString("utf-8");
132
- }
133
- return decompressed.toString("utf-8");
134
- } catch {
135
- return rawBuffer.toString("utf-8");
136
- }
137
- }
138
- function handleToolbarAsset(_req, res, _serverPort) {
139
- res.writeHead(404, { "Content-Type": "text/plain" });
140
- res.end("Not found");
141
- }
142
- function buildInjectionScript(serverPort, token) {
143
- return `
144
- <script data-openmagic="true">
145
- (function() {
146
- if (window.__OPENMAGIC_LOADED__) return;
147
- window.__OPENMAGIC_LOADED__ = true;
148
- window.__OPENMAGIC_CONFIG__ = {
149
- wsPort: ${serverPort},
150
- token: "${token}"
151
- };
152
- var script = document.createElement("script");
153
- script.src = "http://127.0.0.1:${serverPort}/__openmagic__/toolbar.js";
154
- script.dataset.openmagic = "true";
155
- document.body.appendChild(script);
156
- })();
157
- </script>`;
158
- }
159
-
160
34
  // src/server.ts
161
- import http2 from "http";
162
35
  import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
163
36
  import { join as join3, dirname as dirname2 } from "path";
164
37
  import { fileURLToPath } from "url";
@@ -1508,24 +1381,25 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
1508
1381
  }
1509
1382
 
1510
1383
  // src/server.ts
1384
+ var VERSION = "0.11.0";
1511
1385
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1512
- function createOpenMagicServer(proxyPort, roots) {
1513
- const httpServer = http2.createServer((req, res) => {
1386
+ function attachOpenMagic(httpServer, roots) {
1387
+ function handleRequest(req, res) {
1388
+ if (!req.url?.startsWith("/__openmagic__/")) return false;
1514
1389
  if (req.url === "/__openmagic__/toolbar.js") {
1515
1390
  serveToolbarBundle(res);
1516
- return;
1391
+ return true;
1517
1392
  }
1518
1393
  if (req.url === "/__openmagic__/health") {
1519
1394
  res.writeHead(200, {
1520
1395
  "Content-Type": "application/json",
1521
1396
  "Access-Control-Allow-Origin": "*"
1522
1397
  });
1523
- res.end(JSON.stringify({ status: "ok", version: "0.10.0" }));
1524
- return;
1398
+ res.end(JSON.stringify({ status: "ok", version: VERSION }));
1399
+ return true;
1525
1400
  }
1526
- res.writeHead(404);
1527
- res.end("Not found");
1528
- });
1401
+ return false;
1402
+ }
1529
1403
  const wss = new WebSocketServer({
1530
1404
  server: httpServer,
1531
1405
  path: "/__openmagic__/ws"
@@ -1552,7 +1426,7 @@ function createOpenMagicServer(proxyPort, roots) {
1552
1426
  return;
1553
1427
  }
1554
1428
  try {
1555
- await handleMessage(ws, msg, state, roots, proxyPort);
1429
+ await handleMessage(ws, msg, state, roots);
1556
1430
  } catch (e) {
1557
1431
  sendError(ws, "internal_error", e.message, msg.id);
1558
1432
  }
@@ -1561,9 +1435,9 @@ function createOpenMagicServer(proxyPort, roots) {
1561
1435
  clientStates.delete(ws);
1562
1436
  });
1563
1437
  });
1564
- return { httpServer, wss };
1438
+ return { wss, handleRequest };
1565
1439
  }
1566
- async function handleMessage(ws, msg, state, roots, _proxyPort) {
1440
+ async function handleMessage(ws, msg, state, roots) {
1567
1441
  switch (msg.type) {
1568
1442
  case "handshake": {
1569
1443
  const payload = msg.payload;
@@ -1583,12 +1457,13 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1583
1457
  id: msg.id,
1584
1458
  type: "handshake.ok",
1585
1459
  payload: {
1586
- version: "0.10.0",
1460
+ version: VERSION,
1587
1461
  roots,
1588
1462
  config: {
1589
1463
  provider: config.provider,
1590
1464
  model: config.model,
1591
- hasApiKey: !!config.apiKey
1465
+ hasApiKey: !!config.apiKey,
1466
+ apiKeys: config.apiKeys || {}
1592
1467
  }
1593
1468
  }
1594
1469
  });
@@ -1614,12 +1489,20 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1614
1489
  }
1615
1490
  case "fs.write": {
1616
1491
  const payload = msg.payload;
1617
- const result = writeFileSafe(payload.path, payload.content, roots);
1618
- send(ws, {
1619
- id: msg.id,
1620
- type: "fs.written",
1621
- payload: { path: payload.path, ok: result.ok, error: result.error }
1622
- });
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
+ }
1623
1506
  break;
1624
1507
  }
1625
1508
  case "fs.list": {
@@ -1636,16 +1519,18 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1636
1519
  case "llm.chat": {
1637
1520
  const payload = msg.payload;
1638
1521
  const config = loadConfig();
1639
- const providerMeta = MODEL_REGISTRY?.[payload.provider || config.provider || ""];
1640
- if (!config.apiKey && !providerMeta?.local) {
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) {
1641
1526
  sendError(ws, "config_error", "API key not configured", msg.id);
1642
1527
  return;
1643
1528
  }
1644
1529
  await handleLlmChat(
1645
1530
  {
1646
- provider: payload.provider || config.provider || "openai",
1531
+ provider,
1647
1532
  model: payload.model || config.model || "gpt-4o",
1648
- apiKey: config.apiKey,
1533
+ apiKey,
1649
1534
  messages: payload.messages,
1650
1535
  context: payload.context
1651
1536
  },
@@ -1669,8 +1554,11 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1669
1554
  payload: {
1670
1555
  provider: config.provider,
1671
1556
  model: config.model,
1672
- hasApiKey: !!config.apiKey,
1673
- 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
+ )
1674
1562
  }
1675
1563
  });
1676
1564
  break;
@@ -1680,7 +1568,15 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1680
1568
  const updates = {};
1681
1569
  if (payload.provider !== void 0) updates.provider = payload.provider;
1682
1570
  if (payload.model !== void 0) updates.model = payload.model;
1683
- if (payload.apiKey !== void 0) updates.apiKey = payload.apiKey;
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
+ }
1684
1580
  saveConfig(updates);
1685
1581
  send(ws, {
1686
1582
  id: msg.id,
@@ -1699,11 +1595,7 @@ function send(ws, msg) {
1699
1595
  }
1700
1596
  }
1701
1597
  function sendError(ws, code, message, id) {
1702
- send(ws, {
1703
- id: id || "error",
1704
- type: "error",
1705
- payload: { code, message }
1706
- });
1598
+ send(ws, { id: id || "error", type: "error", payload: { code, message } });
1707
1599
  }
1708
1600
  function serveToolbarBundle(res) {
1709
1601
  const bundlePaths = [
@@ -1730,14 +1622,124 @@ function serveToolbarBundle(res) {
1730
1622
  "Content-Type": "application/javascript",
1731
1623
  "Access-Control-Allow-Origin": "*"
1732
1624
  });
1733
- res.end(`
1734
- (function() {
1735
- var div = document.createElement("div");
1736
- 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);";
1737
- div.textContent = "OpenMagic: Toolbar bundle not found. Run 'npm run build:toolbar' first.";
1738
- document.body.appendChild(div);
1739
- })();
1740
- `);
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>`;
1741
1743
  }
1742
1744
 
1743
1745
  // src/detect.ts
@@ -1914,7 +1916,7 @@ process.on("uncaughtException", (err) => {
1914
1916
  process.exit(1);
1915
1917
  });
1916
1918
  var childProcesses = [];
1917
- var VERSION = "0.10.0";
1919
+ var VERSION2 = "0.11.0";
1918
1920
  function ask(question) {
1919
1921
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1920
1922
  return new Promise((resolve3) => {
@@ -2026,7 +2028,7 @@ function formatDevServerLine(line) {
2026
2028
  return chalk.dim(` \u2502 ${trimmed}`);
2027
2029
  }
2028
2030
  var program = new Command();
2029
- 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(
2030
2032
  "-l, --listen <port>",
2031
2033
  "Port for the OpenMagic proxy",
2032
2034
  "4567"
@@ -2036,7 +2038,7 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2036
2038
  ).option("--no-open", "Don't auto-open browser").option("--host <host>", "Dev server host", "127.0.0.1").action(async (opts) => {
2037
2039
  console.log("");
2038
2040
  console.log(
2039
- chalk.bold.magenta(" \u2728 OpenMagic") + chalk.dim(` v${VERSION}`)
2041
+ chalk.bold.magenta(" \u2728 OpenMagic") + chalk.dim(` v${VERSION2}`)
2040
2042
  );
2041
2043
  console.log("");
2042
2044
  let targetPort;
@@ -2085,28 +2087,20 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2085
2087
  );
2086
2088
  const config = loadConfig();
2087
2089
  saveConfig({ ...config, roots, targetPort });
2088
- const token = generateSessionToken();
2090
+ generateSessionToken();
2089
2091
  let proxyPort = parseInt(opts.listen, 10);
2090
- while (await isPortOpen(proxyPort) || await isPortOpen(proxyPort + 1)) {
2092
+ while (await isPortOpen(proxyPort)) {
2091
2093
  proxyPort++;
2092
2094
  if (proxyPort > parseInt(opts.listen, 10) + 100) {
2093
- console.log(chalk.red(" Could not find two consecutive free ports."));
2095
+ console.log(chalk.red(" Could not find an available port."));
2094
2096
  process.exit(1);
2095
2097
  }
2096
2098
  }
2097
- const companionPort = proxyPort + 1;
2098
- const { httpServer: omServer } = createOpenMagicServer(companionPort, roots);
2099
- omServer.listen(companionPort, "127.0.0.1", () => {
2100
- });
2101
- const proxyServer = createProxyServer(
2102
- targetHost,
2103
- targetPort,
2104
- companionPort
2105
- );
2099
+ const proxyServer = createProxyServer(targetHost, targetPort, roots);
2106
2100
  proxyServer.listen(proxyPort, "127.0.0.1", async () => {
2107
2101
  console.log("");
2108
2102
  console.log(
2109
- 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}`)
2110
2104
  );
2111
2105
  console.log("");
2112
2106
  await healthCheck(proxyPort, targetPort);
@@ -2123,16 +2117,10 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2123
2117
  });
2124
2118
  }
2125
2119
  });
2126
- proxyServer.on("upgrade", (req, socket, head) => {
2127
- if (req.url?.startsWith("/__openmagic__")) {
2128
- omServer.emit("upgrade", req, socket, head);
2129
- }
2130
- });
2131
2120
  const shutdown = () => {
2132
2121
  console.log("");
2133
2122
  console.log(chalk.dim(" Shutting down OpenMagic..."));
2134
2123
  proxyServer.close();
2135
- omServer.close();
2136
2124
  process.exit(0);
2137
2125
  };
2138
2126
  process.on("SIGINT", shutdown);