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 +9 -0
- package/dist/cli.js +35 -83
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +34 -34
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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.
|
|
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) => {
|