openmagic 0.9.0 → 0.10.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 +71 -65
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +19 -14
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -31,6 +32,9 @@ function validateToken(token) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// src/proxy.ts
|
|
35
|
+
var gunzipAsync = promisify(gunzip);
|
|
36
|
+
var inflateAsync = promisify(inflate);
|
|
37
|
+
var brotliAsync = promisify(brotliDecompress);
|
|
34
38
|
function createProxyServer(targetHost, targetPort, serverPort) {
|
|
35
39
|
const proxy = httpProxy.createProxyServer({
|
|
36
40
|
target: `http://${targetHost}:${targetPort}`,
|
|
@@ -59,6 +63,11 @@ function createProxyServer(targetHost, targetPort, serverPort) {
|
|
|
59
63
|
delete headers["content-length"];
|
|
60
64
|
delete headers["content-encoding"];
|
|
61
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"];
|
|
62
71
|
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
63
72
|
res.end(body);
|
|
64
73
|
}).catch(() => {
|
|
@@ -99,57 +108,32 @@ function createProxyServer(targetHost, targetPort, serverPort) {
|
|
|
99
108
|
});
|
|
100
109
|
return server;
|
|
101
110
|
}
|
|
102
|
-
function collectBody(stream) {
|
|
103
|
-
|
|
104
|
-
const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
|
|
111
|
+
async function collectBody(stream) {
|
|
112
|
+
const rawBuffer = await new Promise((resolve3, reject) => {
|
|
105
113
|
const chunks = [];
|
|
106
|
-
|
|
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;
|
|
107
124
|
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
108
|
-
|
|
109
|
-
stream.pipe(gunzip);
|
|
110
|
-
source = gunzip;
|
|
111
|
-
gunzip.on("error", () => {
|
|
112
|
-
collectRaw(stream).then(resolve3).catch(reject);
|
|
113
|
-
});
|
|
125
|
+
decompressed = await gunzipAsync(rawBuffer);
|
|
114
126
|
} else if (encoding === "deflate") {
|
|
115
|
-
|
|
116
|
-
stream.pipe(inflate);
|
|
117
|
-
source = inflate;
|
|
118
|
-
inflate.on("error", () => {
|
|
119
|
-
collectRaw(stream).then(resolve3).catch(reject);
|
|
120
|
-
});
|
|
127
|
+
decompressed = await inflateAsync(rawBuffer);
|
|
121
128
|
} else if (encoding === "br") {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
brotli.on("error", () => {
|
|
126
|
-
collectRaw(stream).then(resolve3).catch(reject);
|
|
127
|
-
});
|
|
129
|
+
decompressed = await brotliAsync(rawBuffer);
|
|
130
|
+
} else {
|
|
131
|
+
return rawBuffer.toString("utf-8");
|
|
128
132
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
});
|
|
133
|
+
return decompressed.toString("utf-8");
|
|
134
|
+
} catch {
|
|
135
|
+
return rawBuffer.toString("utf-8");
|
|
136
|
+
}
|
|
153
137
|
}
|
|
154
138
|
function handleToolbarAsset(_req, res, _serverPort) {
|
|
155
139
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
@@ -181,7 +165,7 @@ import { fileURLToPath } from "url";
|
|
|
181
165
|
import { WebSocketServer, WebSocket } from "ws";
|
|
182
166
|
|
|
183
167
|
// src/config.ts
|
|
184
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
168
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from "fs";
|
|
185
169
|
import { join } from "path";
|
|
186
170
|
import { homedir } from "os";
|
|
187
171
|
var CONFIG_DIR = join(homedir(), ".openmagic");
|
|
@@ -208,7 +192,9 @@ function saveConfig(updates) {
|
|
|
208
192
|
ensureConfigDir();
|
|
209
193
|
const existing = loadConfig();
|
|
210
194
|
const merged = { ...existing, ...updates };
|
|
211
|
-
|
|
195
|
+
const tmpFile = CONFIG_FILE + ".tmp";
|
|
196
|
+
writeFileSync(tmpFile, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
|
|
197
|
+
renameSync(tmpFile, CONFIG_FILE);
|
|
212
198
|
} catch (e) {
|
|
213
199
|
console.warn(`[OpenMagic] Warning: Could not save config to ${CONFIG_FILE}: ${e.message}`);
|
|
214
200
|
}
|
|
@@ -260,7 +246,8 @@ function isPathSafe(filePath, roots) {
|
|
|
260
246
|
const resolved = resolve(filePath);
|
|
261
247
|
return roots.some((root) => {
|
|
262
248
|
const resolvedRoot = resolve(root);
|
|
263
|
-
|
|
249
|
+
const rel = relative(resolvedRoot, resolved);
|
|
250
|
+
return !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
|
|
264
251
|
});
|
|
265
252
|
}
|
|
266
253
|
function readFileSafe(filePath, roots) {
|
|
@@ -1093,8 +1080,10 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
|
|
|
1093
1080
|
const apiMessages = [
|
|
1094
1081
|
{ role: "system", content: SYSTEM_PROMPT }
|
|
1095
1082
|
];
|
|
1096
|
-
|
|
1097
|
-
|
|
1083
|
+
const lastUserIdx = messages.reduce((acc, m, i) => m.role === "user" ? i : acc, -1);
|
|
1084
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1085
|
+
const msg = messages[i];
|
|
1086
|
+
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1098
1087
|
const contextParts = {};
|
|
1099
1088
|
if (context.selectedElement) {
|
|
1100
1089
|
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
@@ -1128,6 +1117,8 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
|
|
|
1128
1117
|
} else {
|
|
1129
1118
|
apiMessages.push({ role: "user", content: enrichedContent });
|
|
1130
1119
|
}
|
|
1120
|
+
} else if (msg.role === "system") {
|
|
1121
|
+
continue;
|
|
1131
1122
|
} else {
|
|
1132
1123
|
apiMessages.push({
|
|
1133
1124
|
role: msg.role,
|
|
@@ -1219,9 +1210,11 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
|
|
|
1219
1210
|
async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone, onError) {
|
|
1220
1211
|
const url = "https://api.anthropic.com/v1/messages";
|
|
1221
1212
|
const apiMessages = [];
|
|
1222
|
-
|
|
1213
|
+
const lastUserIdx = messages.reduce((acc, m, i) => m.role === "user" ? i : acc, -1);
|
|
1214
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1215
|
+
const msg = messages[i];
|
|
1223
1216
|
if (msg.role === "system") continue;
|
|
1224
|
-
if (msg.role === "user" && typeof msg.content === "string") {
|
|
1217
|
+
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1225
1218
|
const contextParts = {};
|
|
1226
1219
|
if (context.selectedElement) {
|
|
1227
1220
|
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
@@ -1346,10 +1339,12 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
|
|
|
1346
1339
|
async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onError) {
|
|
1347
1340
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}&alt=sse`;
|
|
1348
1341
|
const contents = [];
|
|
1349
|
-
|
|
1342
|
+
const lastUserIdx = messages.reduce((acc, m, i) => m.role === "user" ? i : acc, -1);
|
|
1343
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1344
|
+
const msg = messages[i];
|
|
1350
1345
|
if (msg.role === "system") continue;
|
|
1351
1346
|
const role = msg.role === "assistant" ? "model" : "user";
|
|
1352
|
-
if (msg.role === "user" && typeof msg.content === "string") {
|
|
1347
|
+
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1353
1348
|
const contextParts = {};
|
|
1354
1349
|
if (context.selectedElement) {
|
|
1355
1350
|
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
@@ -1391,9 +1386,7 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
|
|
|
1391
1386
|
const generationConfig = {
|
|
1392
1387
|
maxOutputTokens: 8192
|
|
1393
1388
|
};
|
|
1394
|
-
|
|
1395
|
-
generationConfig.thinking_level = thinkingLevel.toUpperCase();
|
|
1396
|
-
}
|
|
1389
|
+
const thinkingConfig = thinkingLevel && thinkingLevel !== "none" ? { thinkingLevel: thinkingLevel.toUpperCase() } : void 0;
|
|
1397
1390
|
const body = {
|
|
1398
1391
|
system_instruction: {
|
|
1399
1392
|
parts: [{ text: SYSTEM_PROMPT }]
|
|
@@ -1401,6 +1394,9 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
|
|
|
1401
1394
|
contents,
|
|
1402
1395
|
generationConfig
|
|
1403
1396
|
};
|
|
1397
|
+
if (thinkingConfig) {
|
|
1398
|
+
body.thinkingConfig = thinkingConfig;
|
|
1399
|
+
}
|
|
1404
1400
|
try {
|
|
1405
1401
|
const response = await fetch(url, {
|
|
1406
1402
|
method: "POST",
|
|
@@ -1524,7 +1520,7 @@ function createOpenMagicServer(proxyPort, roots) {
|
|
|
1524
1520
|
"Content-Type": "application/json",
|
|
1525
1521
|
"Access-Control-Allow-Origin": "*"
|
|
1526
1522
|
});
|
|
1527
|
-
res.end(JSON.stringify({ status: "ok", version: "0.
|
|
1523
|
+
res.end(JSON.stringify({ status: "ok", version: "0.10.0" }));
|
|
1528
1524
|
return;
|
|
1529
1525
|
}
|
|
1530
1526
|
res.writeHead(404);
|
|
@@ -1535,7 +1531,12 @@ function createOpenMagicServer(proxyPort, roots) {
|
|
|
1535
1531
|
path: "/__openmagic__/ws"
|
|
1536
1532
|
});
|
|
1537
1533
|
const clientStates = /* @__PURE__ */ new WeakMap();
|
|
1538
|
-
wss.on("connection", (ws) => {
|
|
1534
|
+
wss.on("connection", (ws, req) => {
|
|
1535
|
+
const origin = req.headers.origin || "";
|
|
1536
|
+
if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("http://127.0.0.1")) {
|
|
1537
|
+
ws.close(4003, "Forbidden origin");
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1539
1540
|
clientStates.set(ws, { authenticated: false });
|
|
1540
1541
|
ws.on("message", async (data) => {
|
|
1541
1542
|
let msg;
|
|
@@ -1582,7 +1583,7 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1582
1583
|
id: msg.id,
|
|
1583
1584
|
type: "handshake.ok",
|
|
1584
1585
|
payload: {
|
|
1585
|
-
version: "0.
|
|
1586
|
+
version: "0.10.0",
|
|
1586
1587
|
roots,
|
|
1587
1588
|
config: {
|
|
1588
1589
|
provider: config.provider,
|
|
@@ -1635,7 +1636,8 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1635
1636
|
case "llm.chat": {
|
|
1636
1637
|
const payload = msg.payload;
|
|
1637
1638
|
const config = loadConfig();
|
|
1638
|
-
|
|
1639
|
+
const providerMeta = MODEL_REGISTRY?.[payload.provider || config.provider || ""];
|
|
1640
|
+
if (!config.apiKey && !providerMeta?.local) {
|
|
1639
1641
|
sendError(ws, "config_error", "API key not configured", msg.id);
|
|
1640
1642
|
return;
|
|
1641
1643
|
}
|
|
@@ -1679,7 +1681,6 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1679
1681
|
if (payload.provider !== void 0) updates.provider = payload.provider;
|
|
1680
1682
|
if (payload.model !== void 0) updates.model = payload.model;
|
|
1681
1683
|
if (payload.apiKey !== void 0) updates.apiKey = payload.apiKey;
|
|
1682
|
-
if (payload.roots !== void 0) updates.roots = payload.roots;
|
|
1683
1684
|
saveConfig(updates);
|
|
1684
1685
|
send(ws, {
|
|
1685
1686
|
id: msg.id,
|
|
@@ -1913,7 +1914,7 @@ process.on("uncaughtException", (err) => {
|
|
|
1913
1914
|
process.exit(1);
|
|
1914
1915
|
});
|
|
1915
1916
|
var childProcesses = [];
|
|
1916
|
-
var VERSION = "0.
|
|
1917
|
+
var VERSION = "0.10.0";
|
|
1917
1918
|
function ask(question) {
|
|
1918
1919
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1919
1920
|
return new Promise((resolve3) => {
|
|
@@ -2048,6 +2049,11 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2048
2049
|
if (!started) {
|
|
2049
2050
|
process.exit(1);
|
|
2050
2051
|
}
|
|
2052
|
+
const recheck = await detectDevServer();
|
|
2053
|
+
if (recheck) {
|
|
2054
|
+
targetPort = recheck.port;
|
|
2055
|
+
targetHost = recheck.host;
|
|
2056
|
+
}
|
|
2051
2057
|
}
|
|
2052
2058
|
} else {
|
|
2053
2059
|
console.log(chalk.dim(" Scanning for dev server..."));
|