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 +213 -219
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +46 -24
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +2 -3
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1517
|
-
|
|
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:
|
|
1528
|
-
return;
|
|
1398
|
+
res.end(JSON.stringify({ status: "ok", version: VERSION }));
|
|
1399
|
+
return true;
|
|
1529
1400
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
|
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 {
|
|
1438
|
+
return { wss, handleRequest };
|
|
1564
1439
|
}
|
|
1565
|
-
async function handleMessage(ws, msg, state, roots
|
|
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:
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
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
|
|
1531
|
+
provider,
|
|
1645
1532
|
model: payload.model || config.model || "gpt-4o",
|
|
1646
|
-
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
|
|
1682
|
-
|
|
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
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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
|
|
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(
|
|
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${
|
|
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
|
-
|
|
2090
|
+
generateSessionToken();
|
|
2083
2091
|
let proxyPort = parseInt(opts.listen, 10);
|
|
2084
|
-
while (await isPortOpen(proxyPort)
|
|
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
|
|
2095
|
+
console.log(chalk.red(" Could not find an available port."));
|
|
2088
2096
|
process.exit(1);
|
|
2089
2097
|
}
|
|
2090
2098
|
}
|
|
2091
|
-
const
|
|
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(`
|
|
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);
|