openmagic 0.12.0 → 0.14.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/README.md CHANGED
@@ -191,6 +191,15 @@ This file is in your home directory, never in your project. It won't be committe
191
191
  - **Path sandboxing** — File operations are restricted to configured root directories. The server cannot read/write outside your project.
192
192
  - **API keys stay local** — Keys are stored in `~/.openmagic/config.json` on your machine. They are proxied through the local server and never exposed to the browser or any third party.
193
193
  - **Zero project modification** — OpenMagic never modifies your `package.json`, config files, or source code during installation. The toolbar exists only in the proxy layer.
194
+ - **Symlink protection** — File operations resolve symlinks and reject paths that escape the project root.
195
+ - **Diff preview** — AI-proposed code changes are shown as diffs with Apply/Reject buttons. Nothing is auto-applied.
196
+
197
+ ### Known Limitations
198
+
199
+ OpenMagic uses a reverse proxy which introduces these tradeoffs:
200
+
201
+ - **Origin change** — Your app runs on `localhost:3000` but is accessed via `localhost:4567`. This can affect OAuth redirects, `localStorage` isolation, and Service Worker scope. Most dev workflows are unaffected, but if your app relies on `window.location.origin`, you may need to adjust your dev config.
202
+ - **CSP meta tags** — OpenMagic strips CSP response headers to allow the toolbar script. However, CSP set via `<meta>` tags in your HTML cannot be modified and may block the toolbar on strict CSP pages.
194
203
 
195
204
  ## Comparison
196
205
 
package/dist/cli.js CHANGED
@@ -10,8 +10,6 @@ import { createInterface } from "readline";
10
10
 
11
11
  // src/proxy.ts
12
12
  import http from "http";
13
- import { gunzip, inflate, brotliDecompress } from "zlib";
14
- import { promisify } from "util";
15
13
  import httpProxy from "http-proxy";
16
14
 
17
15
  // src/security.ts
@@ -68,8 +66,9 @@ function saveConfig(updates) {
68
66
  const tmpFile = CONFIG_FILE + ".tmp";
69
67
  writeFileSync(tmpFile, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
70
68
  renameSync(tmpFile, CONFIG_FILE);
69
+ return { ok: true };
71
70
  } catch (e) {
72
- console.warn(`[OpenMagic] Warning: Could not save config to ${CONFIG_FILE}: ${e.message}`);
71
+ return { ok: false, error: e.message };
73
72
  }
74
73
  }
75
74
 
@@ -78,7 +77,7 @@ import {
78
77
  readFileSync as readFileSync2,
79
78
  writeFileSync as writeFileSync2,
80
79
  existsSync as existsSync2,
81
- statSync,
80
+ lstatSync,
82
81
  readdirSync,
83
82
  copyFileSync,
84
83
  mkdirSync as mkdirSync2,
@@ -184,10 +183,11 @@ function listFiles(rootPath, roots, maxDepth = 4) {
184
183
  const fullPath = join2(dir, item);
185
184
  let stat;
186
185
  try {
187
- stat = statSync(fullPath);
186
+ stat = lstatSync(fullPath);
188
187
  } catch {
189
188
  continue;
190
189
  }
190
+ if (stat.isSymbolicLink()) continue;
191
191
  const relPath = relative(rootPath, fullPath);
192
192
  if (stat.isDirectory()) {
193
193
  entries.push({ path: relPath, type: "dir", name: item });
@@ -1356,7 +1356,7 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
1356
1356
  }
1357
1357
 
1358
1358
  // src/server.ts
1359
- var VERSION = "0.11.0";
1359
+ var VERSION = "0.14.0";
1360
1360
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1361
1361
  function attachOpenMagic(httpServer, roots) {
1362
1362
  function handleRequest(req, res) {
@@ -1438,7 +1438,7 @@ async function handleMessage(ws, msg, state, roots) {
1438
1438
  provider: config.provider,
1439
1439
  model: config.model,
1440
1440
  hasApiKey: !!config.apiKey,
1441
- apiKeys: config.apiKeys || {}
1441
+ apiKeys: Object.fromEntries(Object.entries(config.apiKeys || {}).map(([k]) => [k, true]))
1442
1442
  }
1443
1443
  }
1444
1444
  });
@@ -1504,7 +1504,7 @@ async function handleMessage(ws, msg, state, roots) {
1504
1504
  await handleLlmChat(
1505
1505
  {
1506
1506
  provider,
1507
- model: payload.model || config.model || "gpt-4o",
1507
+ model: payload.model || config.model || MODEL_REGISTRY[provider]?.models[0]?.id || "gpt-4o",
1508
1508
  apiKey,
1509
1509
  messages: payload.messages,
1510
1510
  context: payload.context
@@ -1552,12 +1552,12 @@ async function handleMessage(ws, msg, state, roots) {
1552
1552
  } else if (payload.apiKey !== void 0) {
1553
1553
  updates.apiKey = payload.apiKey;
1554
1554
  }
1555
- saveConfig(updates);
1556
- send(ws, {
1557
- id: msg.id,
1558
- type: "config.saved",
1559
- payload: { ok: true }
1560
- });
1555
+ const result = saveConfig(updates);
1556
+ if (!result.ok) {
1557
+ sendError(ws, "config_error", result.error || "Failed to save", msg.id);
1558
+ } else {
1559
+ send(ws, { id: msg.id, type: "config.saved", payload: { ok: true } });
1560
+ }
1561
1561
  break;
1562
1562
  }
1563
1563
  default:
@@ -1601,9 +1601,6 @@ function serveToolbarBundle(res) {
1601
1601
  }
1602
1602
 
1603
1603
  // src/proxy.ts
1604
- var gunzipAsync = promisify(gunzip);
1605
- var inflateAsync = promisify(inflate);
1606
- var brotliAsync = promisify(brotliDecompress);
1607
1604
  function createProxyServer(targetHost, targetPort, roots) {
1608
1605
  const proxy = httpProxy.createProxyServer({
1609
1606
  target: `http://${targetHost}:${targetPort}`,
@@ -1611,6 +1608,9 @@ function createProxyServer(targetHost, targetPort, roots) {
1611
1608
  selfHandleResponse: true
1612
1609
  });
1613
1610
  const token = getSessionToken();
1611
+ proxy.on("proxyReq", (proxyReq) => {
1612
+ proxyReq.removeHeader("Accept-Encoding");
1613
+ });
1614
1614
  proxy.on("proxyRes", (proxyRes, req, res) => {
1615
1615
  const contentType = proxyRes.headers["content-type"] || "";
1616
1616
  const isHtml = contentType.includes("text/html");
@@ -1619,32 +1619,20 @@ function createProxyServer(targetHost, targetPort, roots) {
1619
1619
  proxyRes.pipe(res);
1620
1620
  return;
1621
1621
  }
1622
- collectBody(proxyRes).then((body) => {
1623
- const toolbarScript = buildInjectionScript(token);
1624
- if (body.includes("</body>")) {
1625
- body = body.replace("</body>", `${toolbarScript}</body>`);
1626
- } else if (body.includes("</html>")) {
1627
- body = body.replace("</html>", `${toolbarScript}</html>`);
1628
- } else {
1629
- body += toolbarScript;
1630
- }
1631
- const headers = { ...proxyRes.headers };
1632
- delete headers["content-length"];
1633
- delete headers["content-encoding"];
1634
- delete headers["transfer-encoding"];
1635
- delete headers["content-security-policy"];
1636
- delete headers["content-security-policy-report-only"];
1637
- delete headers["x-content-security-policy"];
1638
- delete headers["etag"];
1639
- delete headers["last-modified"];
1640
- res.writeHead(proxyRes.statusCode || 200, headers);
1641
- res.end(body);
1642
- }).catch(() => {
1643
- try {
1644
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
1645
- res.end();
1646
- } catch {
1647
- }
1622
+ const headers = { ...proxyRes.headers };
1623
+ delete headers["content-length"];
1624
+ delete headers["content-encoding"];
1625
+ delete headers["transfer-encoding"];
1626
+ delete headers["content-security-policy"];
1627
+ delete headers["content-security-policy-report-only"];
1628
+ delete headers["x-content-security-policy"];
1629
+ delete headers["etag"];
1630
+ delete headers["last-modified"];
1631
+ headers["cache-control"] = "no-store";
1632
+ res.writeHead(proxyRes.statusCode || 200, headers);
1633
+ proxyRes.pipe(res, { end: false });
1634
+ proxyRes.on("end", () => {
1635
+ res.end(buildInjectionScript(token));
1648
1636
  });
1649
1637
  });
1650
1638
  proxy.on("error", (err, _req, res) => {
@@ -1675,46 +1663,8 @@ function createProxyServer(targetHost, targetPort, roots) {
1675
1663
  });
1676
1664
  return server;
1677
1665
  }
1678
- async function collectBody(stream) {
1679
- const rawBuffer = await new Promise((resolve3, reject) => {
1680
- const chunks = [];
1681
- stream.on("data", (chunk) => chunks.push(chunk));
1682
- stream.on("end", () => resolve3(Buffer.concat(chunks)));
1683
- stream.on("error", reject);
1684
- });
1685
- const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
1686
- if (!encoding || encoding === "identity") {
1687
- return rawBuffer.toString("utf-8");
1688
- }
1689
- try {
1690
- let decompressed;
1691
- if (encoding === "gzip" || encoding === "x-gzip") {
1692
- decompressed = await gunzipAsync(rawBuffer);
1693
- } else if (encoding === "deflate") {
1694
- decompressed = await inflateAsync(rawBuffer);
1695
- } else if (encoding === "br") {
1696
- decompressed = await brotliAsync(rawBuffer);
1697
- } else {
1698
- return rawBuffer.toString("utf-8");
1699
- }
1700
- return decompressed.toString("utf-8");
1701
- } catch {
1702
- return rawBuffer.toString("utf-8");
1703
- }
1704
- }
1705
1666
  function buildInjectionScript(token) {
1706
- return `
1707
- <script data-openmagic="true">
1708
- (function() {
1709
- if (window.__OPENMAGIC_LOADED__) return;
1710
- window.__OPENMAGIC_LOADED__ = true;
1711
- window.__OPENMAGIC_TOKEN__ = "${token}";
1712
- var s = document.createElement("script");
1713
- s.src = "/__openmagic__/toolbar.js";
1714
- s.dataset.openmagic = "true";
1715
- document.body.appendChild(s);
1716
- })();
1717
- </script>`;
1667
+ return `<script src="/__openmagic__/toolbar.js?v=${Date.now()}" data-openmagic="true" data-openmagic-token="${token}" defer></script>`;
1718
1668
  }
1719
1669
 
1720
1670
  // src/detect.ts
@@ -1740,6 +1690,8 @@ var COMMON_DEV_PORTS = [
1740
1690
  // Common alternate
1741
1691
  4e3,
1742
1692
  // Phoenix, generic
1693
+ 5e3,
1694
+ // Flask
1743
1695
  1234,
1744
1696
  // Parcel
1745
1697
  4321,
@@ -1891,7 +1843,7 @@ process.on("uncaughtException", (err) => {
1891
1843
  process.exit(1);
1892
1844
  });
1893
1845
  var childProcesses = [];
1894
- var VERSION2 = "0.12.0";
1846
+ var VERSION2 = "0.14.0";
1895
1847
  function ask(question) {
1896
1848
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1897
1849
  return new Promise((resolve3) => {