openmagic 0.10.0 → 0.12.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 +201 -238
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +56 -19
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +2 -3
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";
|
|
@@ -208,7 +81,8 @@ import {
|
|
|
208
81
|
statSync,
|
|
209
82
|
readdirSync,
|
|
210
83
|
copyFileSync,
|
|
211
|
-
mkdirSync as mkdirSync2
|
|
84
|
+
mkdirSync as mkdirSync2,
|
|
85
|
+
realpathSync
|
|
212
86
|
} from "fs";
|
|
213
87
|
import { join as join2, resolve, relative, dirname, extname } from "path";
|
|
214
88
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -244,10 +118,17 @@ var IGNORED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
244
118
|
]);
|
|
245
119
|
function isPathSafe(filePath, roots) {
|
|
246
120
|
const resolved = resolve(filePath);
|
|
121
|
+
let real;
|
|
122
|
+
try {
|
|
123
|
+
real = realpathSync(resolved);
|
|
124
|
+
} catch {
|
|
125
|
+
real = resolved;
|
|
126
|
+
}
|
|
247
127
|
return roots.some((root) => {
|
|
248
128
|
const resolvedRoot = resolve(root);
|
|
249
129
|
const rel = relative(resolvedRoot, resolved);
|
|
250
|
-
|
|
130
|
+
const realRel = relative(resolvedRoot, real);
|
|
131
|
+
return !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\") && (!realRel.startsWith("..") && !realRel.startsWith("/") && !realRel.startsWith("\\"));
|
|
251
132
|
});
|
|
252
133
|
}
|
|
253
134
|
function readFileSafe(filePath, roots) {
|
|
@@ -1029,6 +910,18 @@ You MUST respond with valid JSON in this exact format:
|
|
|
1029
910
|
6. If the change involves multiple files, include all modifications in the array
|
|
1030
911
|
7. ALWAYS respond with the JSON format above, even for explanations (put them in the "explanation" field)
|
|
1031
912
|
8. If you cannot make the requested change, set modifications to an empty array and explain why`;
|
|
913
|
+
function buildContextParts(context) {
|
|
914
|
+
const parts = {};
|
|
915
|
+
if (context.selectedElement) parts.selectedElement = context.selectedElement.outerHTML;
|
|
916
|
+
if (context.files?.length) {
|
|
917
|
+
parts.filePath = context.files[0].path;
|
|
918
|
+
parts.fileContent = context.files[0].content;
|
|
919
|
+
}
|
|
920
|
+
if (context.projectTree) parts.projectTree = context.projectTree;
|
|
921
|
+
if (context.networkLogs) parts.networkLogs = context.networkLogs.map((l) => `${l.method} ${l.url} \u2192 ${l.status || "pending"}`).join("\n");
|
|
922
|
+
if (context.consoleLogs) parts.consoleLogs = context.consoleLogs.map((l) => `[${l.level}] ${l.args.join(" ")}`).join("\n");
|
|
923
|
+
return parts;
|
|
924
|
+
}
|
|
1032
925
|
function buildUserMessage(userPrompt, context) {
|
|
1033
926
|
const parts = [];
|
|
1034
927
|
if (context.projectTree) {
|
|
@@ -1084,24 +977,7 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
|
|
|
1084
977
|
for (let i = 0; i < messages.length; i++) {
|
|
1085
978
|
const msg = messages[i];
|
|
1086
979
|
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1087
|
-
const
|
|
1088
|
-
if (context.selectedElement) {
|
|
1089
|
-
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
1090
|
-
}
|
|
1091
|
-
if (context.files && context.files.length > 0) {
|
|
1092
|
-
contextParts.filePath = context.files[0].path;
|
|
1093
|
-
contextParts.fileContent = context.files[0].content;
|
|
1094
|
-
}
|
|
1095
|
-
if (context.projectTree) {
|
|
1096
|
-
contextParts.projectTree = context.projectTree;
|
|
1097
|
-
}
|
|
1098
|
-
if (context.networkLogs) {
|
|
1099
|
-
contextParts.networkLogs = context.networkLogs.map((l) => `${l.method} ${l.url} \u2192 ${l.status || "pending"}`).join("\n");
|
|
1100
|
-
}
|
|
1101
|
-
if (context.consoleLogs) {
|
|
1102
|
-
contextParts.consoleLogs = context.consoleLogs.map((l) => `[${l.level}] ${l.args.join(" ")}`).join("\n");
|
|
1103
|
-
}
|
|
1104
|
-
const enrichedContent = buildUserMessage(msg.content, contextParts);
|
|
980
|
+
const enrichedContent = buildUserMessage(msg.content, buildContextParts(context));
|
|
1105
981
|
const modelInfo2 = providerConfig.models.find((m) => m.id === model);
|
|
1106
982
|
if (context.screenshot && modelInfo2?.vision) {
|
|
1107
983
|
apiMessages.push({
|
|
@@ -1215,24 +1091,7 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
|
|
|
1215
1091
|
const msg = messages[i];
|
|
1216
1092
|
if (msg.role === "system") continue;
|
|
1217
1093
|
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1218
|
-
const
|
|
1219
|
-
if (context.selectedElement) {
|
|
1220
|
-
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
1221
|
-
}
|
|
1222
|
-
if (context.files && context.files.length > 0) {
|
|
1223
|
-
contextParts.filePath = context.files[0].path;
|
|
1224
|
-
contextParts.fileContent = context.files[0].content;
|
|
1225
|
-
}
|
|
1226
|
-
if (context.projectTree) {
|
|
1227
|
-
contextParts.projectTree = context.projectTree;
|
|
1228
|
-
}
|
|
1229
|
-
if (context.networkLogs) {
|
|
1230
|
-
contextParts.networkLogs = context.networkLogs.map((l) => `${l.method} ${l.url} \u2192 ${l.status || "pending"}`).join("\n");
|
|
1231
|
-
}
|
|
1232
|
-
if (context.consoleLogs) {
|
|
1233
|
-
contextParts.consoleLogs = context.consoleLogs.map((l) => `[${l.level}] ${l.args.join(" ")}`).join("\n");
|
|
1234
|
-
}
|
|
1235
|
-
const enrichedContent = buildUserMessage(msg.content, contextParts);
|
|
1094
|
+
const enrichedContent = buildUserMessage(msg.content, buildContextParts(context));
|
|
1236
1095
|
if (context.screenshot) {
|
|
1237
1096
|
const base64Data = context.screenshot.replace(
|
|
1238
1097
|
/^data:image\/\w+;base64,/,
|
|
@@ -1345,18 +1204,7 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
|
|
|
1345
1204
|
if (msg.role === "system") continue;
|
|
1346
1205
|
const role = msg.role === "assistant" ? "model" : "user";
|
|
1347
1206
|
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1348
|
-
const
|
|
1349
|
-
if (context.selectedElement) {
|
|
1350
|
-
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
1351
|
-
}
|
|
1352
|
-
if (context.files && context.files.length > 0) {
|
|
1353
|
-
contextParts.filePath = context.files[0].path;
|
|
1354
|
-
contextParts.fileContent = context.files[0].content;
|
|
1355
|
-
}
|
|
1356
|
-
if (context.projectTree) {
|
|
1357
|
-
contextParts.projectTree = context.projectTree;
|
|
1358
|
-
}
|
|
1359
|
-
const enrichedContent = buildUserMessage(msg.content, contextParts);
|
|
1207
|
+
const enrichedContent = buildUserMessage(msg.content, buildContextParts(context));
|
|
1360
1208
|
const parts = [
|
|
1361
1209
|
{ text: enrichedContent }
|
|
1362
1210
|
];
|
|
@@ -1508,24 +1356,25 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
|
|
|
1508
1356
|
}
|
|
1509
1357
|
|
|
1510
1358
|
// src/server.ts
|
|
1359
|
+
var VERSION = "0.11.0";
|
|
1511
1360
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1512
|
-
function
|
|
1513
|
-
|
|
1361
|
+
function attachOpenMagic(httpServer, roots) {
|
|
1362
|
+
function handleRequest(req, res) {
|
|
1363
|
+
if (!req.url?.startsWith("/__openmagic__/")) return false;
|
|
1514
1364
|
if (req.url === "/__openmagic__/toolbar.js") {
|
|
1515
1365
|
serveToolbarBundle(res);
|
|
1516
|
-
return;
|
|
1366
|
+
return true;
|
|
1517
1367
|
}
|
|
1518
1368
|
if (req.url === "/__openmagic__/health") {
|
|
1519
1369
|
res.writeHead(200, {
|
|
1520
1370
|
"Content-Type": "application/json",
|
|
1521
1371
|
"Access-Control-Allow-Origin": "*"
|
|
1522
1372
|
});
|
|
1523
|
-
res.end(JSON.stringify({ status: "ok", version:
|
|
1524
|
-
return;
|
|
1373
|
+
res.end(JSON.stringify({ status: "ok", version: VERSION }));
|
|
1374
|
+
return true;
|
|
1525
1375
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
});
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1529
1378
|
const wss = new WebSocketServer({
|
|
1530
1379
|
server: httpServer,
|
|
1531
1380
|
path: "/__openmagic__/ws"
|
|
@@ -1552,7 +1401,7 @@ function createOpenMagicServer(proxyPort, roots) {
|
|
|
1552
1401
|
return;
|
|
1553
1402
|
}
|
|
1554
1403
|
try {
|
|
1555
|
-
await handleMessage(ws, msg, state, roots
|
|
1404
|
+
await handleMessage(ws, msg, state, roots);
|
|
1556
1405
|
} catch (e) {
|
|
1557
1406
|
sendError(ws, "internal_error", e.message, msg.id);
|
|
1558
1407
|
}
|
|
@@ -1561,9 +1410,9 @@ function createOpenMagicServer(proxyPort, roots) {
|
|
|
1561
1410
|
clientStates.delete(ws);
|
|
1562
1411
|
});
|
|
1563
1412
|
});
|
|
1564
|
-
return {
|
|
1413
|
+
return { wss, handleRequest };
|
|
1565
1414
|
}
|
|
1566
|
-
async function handleMessage(ws, msg, state, roots
|
|
1415
|
+
async function handleMessage(ws, msg, state, roots) {
|
|
1567
1416
|
switch (msg.type) {
|
|
1568
1417
|
case "handshake": {
|
|
1569
1418
|
const payload = msg.payload;
|
|
@@ -1583,12 +1432,13 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1583
1432
|
id: msg.id,
|
|
1584
1433
|
type: "handshake.ok",
|
|
1585
1434
|
payload: {
|
|
1586
|
-
version:
|
|
1435
|
+
version: VERSION,
|
|
1587
1436
|
roots,
|
|
1588
1437
|
config: {
|
|
1589
1438
|
provider: config.provider,
|
|
1590
1439
|
model: config.model,
|
|
1591
|
-
hasApiKey: !!config.apiKey
|
|
1440
|
+
hasApiKey: !!config.apiKey,
|
|
1441
|
+
apiKeys: config.apiKeys || {}
|
|
1592
1442
|
}
|
|
1593
1443
|
}
|
|
1594
1444
|
});
|
|
@@ -1614,12 +1464,20 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1614
1464
|
}
|
|
1615
1465
|
case "fs.write": {
|
|
1616
1466
|
const payload = msg.payload;
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1467
|
+
if (!payload?.path || payload.content === void 0) {
|
|
1468
|
+
sendError(ws, "invalid_payload", "Missing path or content", msg.id);
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
const writeResult = writeFileSafe(payload.path, payload.content, roots);
|
|
1472
|
+
if (!writeResult.ok) {
|
|
1473
|
+
sendError(ws, "fs_error", writeResult.error || "Write failed", msg.id);
|
|
1474
|
+
} else {
|
|
1475
|
+
send(ws, {
|
|
1476
|
+
id: msg.id,
|
|
1477
|
+
type: "fs.written",
|
|
1478
|
+
payload: { path: payload.path, ok: true }
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1623
1481
|
break;
|
|
1624
1482
|
}
|
|
1625
1483
|
case "fs.list": {
|
|
@@ -1636,16 +1494,18 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1636
1494
|
case "llm.chat": {
|
|
1637
1495
|
const payload = msg.payload;
|
|
1638
1496
|
const config = loadConfig();
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1497
|
+
const provider = payload.provider || config.provider || "openai";
|
|
1498
|
+
const apiKey = config.apiKeys?.[provider] || config.apiKey || "";
|
|
1499
|
+
const providerMeta = MODEL_REGISTRY?.[provider];
|
|
1500
|
+
if (!apiKey && !providerMeta?.local) {
|
|
1641
1501
|
sendError(ws, "config_error", "API key not configured", msg.id);
|
|
1642
1502
|
return;
|
|
1643
1503
|
}
|
|
1644
1504
|
await handleLlmChat(
|
|
1645
1505
|
{
|
|
1646
|
-
provider
|
|
1506
|
+
provider,
|
|
1647
1507
|
model: payload.model || config.model || "gpt-4o",
|
|
1648
|
-
apiKey
|
|
1508
|
+
apiKey,
|
|
1649
1509
|
messages: payload.messages,
|
|
1650
1510
|
context: payload.context
|
|
1651
1511
|
},
|
|
@@ -1669,8 +1529,11 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1669
1529
|
payload: {
|
|
1670
1530
|
provider: config.provider,
|
|
1671
1531
|
model: config.model,
|
|
1672
|
-
hasApiKey: !!config.apiKey,
|
|
1673
|
-
roots: config.roots || roots
|
|
1532
|
+
hasApiKey: !!(config.apiKeys?.[config.provider || ""] || config.apiKey),
|
|
1533
|
+
roots: config.roots || roots,
|
|
1534
|
+
apiKeys: Object.fromEntries(
|
|
1535
|
+
Object.entries(config.apiKeys || {}).map(([k]) => [k, true])
|
|
1536
|
+
)
|
|
1674
1537
|
}
|
|
1675
1538
|
});
|
|
1676
1539
|
break;
|
|
@@ -1680,7 +1543,15 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1680
1543
|
const updates = {};
|
|
1681
1544
|
if (payload.provider !== void 0) updates.provider = payload.provider;
|
|
1682
1545
|
if (payload.model !== void 0) updates.model = payload.model;
|
|
1683
|
-
if (payload.apiKey !== void 0
|
|
1546
|
+
if (payload.apiKey !== void 0 && payload.provider) {
|
|
1547
|
+
const existing = loadConfig();
|
|
1548
|
+
const apiKeys = { ...existing.apiKeys || {} };
|
|
1549
|
+
apiKeys[payload.provider] = payload.apiKey;
|
|
1550
|
+
updates.apiKeys = apiKeys;
|
|
1551
|
+
updates.apiKey = payload.apiKey;
|
|
1552
|
+
} else if (payload.apiKey !== void 0) {
|
|
1553
|
+
updates.apiKey = payload.apiKey;
|
|
1554
|
+
}
|
|
1684
1555
|
saveConfig(updates);
|
|
1685
1556
|
send(ws, {
|
|
1686
1557
|
id: msg.id,
|
|
@@ -1699,11 +1570,7 @@ function send(ws, msg) {
|
|
|
1699
1570
|
}
|
|
1700
1571
|
}
|
|
1701
1572
|
function sendError(ws, code, message, id) {
|
|
1702
|
-
send(ws, {
|
|
1703
|
-
id: id || "error",
|
|
1704
|
-
type: "error",
|
|
1705
|
-
payload: { code, message }
|
|
1706
|
-
});
|
|
1573
|
+
send(ws, { id: id || "error", type: "error", payload: { code, message } });
|
|
1707
1574
|
}
|
|
1708
1575
|
function serveToolbarBundle(res) {
|
|
1709
1576
|
const bundlePaths = [
|
|
@@ -1730,14 +1597,124 @@ function serveToolbarBundle(res) {
|
|
|
1730
1597
|
"Content-Type": "application/javascript",
|
|
1731
1598
|
"Access-Control-Allow-Origin": "*"
|
|
1732
1599
|
});
|
|
1733
|
-
res.end(`
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1600
|
+
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);})();`);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/proxy.ts
|
|
1604
|
+
var gunzipAsync = promisify(gunzip);
|
|
1605
|
+
var inflateAsync = promisify(inflate);
|
|
1606
|
+
var brotliAsync = promisify(brotliDecompress);
|
|
1607
|
+
function createProxyServer(targetHost, targetPort, roots) {
|
|
1608
|
+
const proxy = httpProxy.createProxyServer({
|
|
1609
|
+
target: `http://${targetHost}:${targetPort}`,
|
|
1610
|
+
ws: true,
|
|
1611
|
+
selfHandleResponse: true
|
|
1612
|
+
});
|
|
1613
|
+
const token = getSessionToken();
|
|
1614
|
+
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1615
|
+
const contentType = proxyRes.headers["content-type"] || "";
|
|
1616
|
+
const isHtml = contentType.includes("text/html");
|
|
1617
|
+
if (!isHtml) {
|
|
1618
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
1619
|
+
proxyRes.pipe(res);
|
|
1620
|
+
return;
|
|
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
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
1650
|
+
proxy.on("error", (err, _req, res) => {
|
|
1651
|
+
if (res instanceof http.ServerResponse && !res.headersSent) {
|
|
1652
|
+
const toolbarScript = buildInjectionScript(token);
|
|
1653
|
+
res.writeHead(502, { "Content-Type": "text/html" });
|
|
1654
|
+
res.end(
|
|
1655
|
+
`<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
|
|
1656
|
+
<h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
|
|
1657
|
+
<p>Could not reach <code>${targetHost}:${targetPort}</code></p>
|
|
1658
|
+
<p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
|
|
1659
|
+
<p style="color:#666;font-size:13px;">${err.message}</p>
|
|
1660
|
+
${toolbarScript}
|
|
1661
|
+
</body></html>`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
let omHandle = null;
|
|
1666
|
+
const server = http.createServer((req, res) => {
|
|
1667
|
+
if (omHandle && omHandle(req, res)) return;
|
|
1668
|
+
proxy.web(req, res);
|
|
1669
|
+
});
|
|
1670
|
+
const om = attachOpenMagic(server, roots);
|
|
1671
|
+
omHandle = om.handleRequest;
|
|
1672
|
+
server.on("upgrade", (req, socket, head) => {
|
|
1673
|
+
if (req.url?.startsWith("/__openmagic__")) return;
|
|
1674
|
+
proxy.ws(req, socket, head);
|
|
1675
|
+
});
|
|
1676
|
+
return server;
|
|
1677
|
+
}
|
|
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
|
+
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>`;
|
|
1741
1718
|
}
|
|
1742
1719
|
|
|
1743
1720
|
// src/detect.ts
|
|
@@ -1914,7 +1891,7 @@ process.on("uncaughtException", (err) => {
|
|
|
1914
1891
|
process.exit(1);
|
|
1915
1892
|
});
|
|
1916
1893
|
var childProcesses = [];
|
|
1917
|
-
var
|
|
1894
|
+
var VERSION2 = "0.12.0";
|
|
1918
1895
|
function ask(question) {
|
|
1919
1896
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1920
1897
|
return new Promise((resolve3) => {
|
|
@@ -2026,7 +2003,7 @@ function formatDevServerLine(line) {
|
|
|
2026
2003
|
return chalk.dim(` \u2502 ${trimmed}`);
|
|
2027
2004
|
}
|
|
2028
2005
|
var program = new Command();
|
|
2029
|
-
program.name("openmagic").description("AI-powered coding toolbar for any web application").version(
|
|
2006
|
+
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
2007
|
"-l, --listen <port>",
|
|
2031
2008
|
"Port for the OpenMagic proxy",
|
|
2032
2009
|
"4567"
|
|
@@ -2036,7 +2013,7 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2036
2013
|
).option("--no-open", "Don't auto-open browser").option("--host <host>", "Dev server host", "127.0.0.1").action(async (opts) => {
|
|
2037
2014
|
console.log("");
|
|
2038
2015
|
console.log(
|
|
2039
|
-
chalk.bold.magenta(" \u2728 OpenMagic") + chalk.dim(` v${
|
|
2016
|
+
chalk.bold.magenta(" \u2728 OpenMagic") + chalk.dim(` v${VERSION2}`)
|
|
2040
2017
|
);
|
|
2041
2018
|
console.log("");
|
|
2042
2019
|
let targetPort;
|
|
@@ -2085,28 +2062,20 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2085
2062
|
);
|
|
2086
2063
|
const config = loadConfig();
|
|
2087
2064
|
saveConfig({ ...config, roots, targetPort });
|
|
2088
|
-
|
|
2065
|
+
generateSessionToken();
|
|
2089
2066
|
let proxyPort = parseInt(opts.listen, 10);
|
|
2090
|
-
while (await isPortOpen(proxyPort)
|
|
2067
|
+
while (await isPortOpen(proxyPort)) {
|
|
2091
2068
|
proxyPort++;
|
|
2092
2069
|
if (proxyPort > parseInt(opts.listen, 10) + 100) {
|
|
2093
|
-
console.log(chalk.red(" Could not find
|
|
2070
|
+
console.log(chalk.red(" Could not find an available port."));
|
|
2094
2071
|
process.exit(1);
|
|
2095
2072
|
}
|
|
2096
2073
|
}
|
|
2097
|
-
const
|
|
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
|
-
);
|
|
2074
|
+
const proxyServer = createProxyServer(targetHost, targetPort, roots);
|
|
2106
2075
|
proxyServer.listen(proxyPort, "127.0.0.1", async () => {
|
|
2107
2076
|
console.log("");
|
|
2108
2077
|
console.log(
|
|
2109
|
-
chalk.bold.green(`
|
|
2078
|
+
chalk.bold.green(` Proxy running at \u2192 `) + chalk.bold.underline.cyan(`http://localhost:${proxyPort}`)
|
|
2110
2079
|
);
|
|
2111
2080
|
console.log("");
|
|
2112
2081
|
await healthCheck(proxyPort, targetPort);
|
|
@@ -2123,16 +2092,10 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2123
2092
|
});
|
|
2124
2093
|
}
|
|
2125
2094
|
});
|
|
2126
|
-
proxyServer.on("upgrade", (req, socket, head) => {
|
|
2127
|
-
if (req.url?.startsWith("/__openmagic__")) {
|
|
2128
|
-
omServer.emit("upgrade", req, socket, head);
|
|
2129
|
-
}
|
|
2130
|
-
});
|
|
2131
2095
|
const shutdown = () => {
|
|
2132
2096
|
console.log("");
|
|
2133
2097
|
console.log(chalk.dim(" Shutting down OpenMagic..."));
|
|
2134
2098
|
proxyServer.close();
|
|
2135
|
-
omServer.close();
|
|
2136
2099
|
process.exit(0);
|
|
2137
2100
|
};
|
|
2138
2101
|
process.on("SIGINT", shutdown);
|