openmagic 0.8.2 → 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 +84 -68
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +140 -165
- 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,
|
|
@@ -1135,16 +1126,26 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
|
|
|
1135
1126
|
});
|
|
1136
1127
|
}
|
|
1137
1128
|
}
|
|
1129
|
+
const usesCompletionTokens = provider === "openai" && (model.startsWith("gpt-5") || model.startsWith("o3") || model.startsWith("o4") || model.startsWith("codex"));
|
|
1138
1130
|
const body = {
|
|
1139
1131
|
model,
|
|
1140
1132
|
messages: apiMessages,
|
|
1141
|
-
stream: true
|
|
1142
|
-
max_tokens: 4096
|
|
1133
|
+
stream: true
|
|
1143
1134
|
};
|
|
1135
|
+
if (usesCompletionTokens) {
|
|
1136
|
+
body.max_completion_tokens = 4096;
|
|
1137
|
+
} else {
|
|
1138
|
+
body.max_tokens = 4096;
|
|
1139
|
+
}
|
|
1144
1140
|
const modelInfo = providerConfig.models.find((m) => m.id === model);
|
|
1145
1141
|
if (modelInfo?.thinking?.supported && modelInfo.thinking.paramType === "level") {
|
|
1146
1142
|
body.reasoning_effort = modelInfo.thinking.defaultLevel || "medium";
|
|
1147
|
-
|
|
1143
|
+
const limit = Math.min(modelInfo.maxOutput, 16384);
|
|
1144
|
+
if (usesCompletionTokens) {
|
|
1145
|
+
body.max_completion_tokens = limit;
|
|
1146
|
+
} else {
|
|
1147
|
+
body.max_tokens = limit;
|
|
1148
|
+
}
|
|
1148
1149
|
}
|
|
1149
1150
|
try {
|
|
1150
1151
|
const headers = {
|
|
@@ -1209,9 +1210,11 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
|
|
|
1209
1210
|
async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone, onError) {
|
|
1210
1211
|
const url = "https://api.anthropic.com/v1/messages";
|
|
1211
1212
|
const apiMessages = [];
|
|
1212
|
-
|
|
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];
|
|
1213
1216
|
if (msg.role === "system") continue;
|
|
1214
|
-
if (msg.role === "user" && typeof msg.content === "string") {
|
|
1217
|
+
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1215
1218
|
const contextParts = {};
|
|
1216
1219
|
if (context.selectedElement) {
|
|
1217
1220
|
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
@@ -1336,10 +1339,12 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
|
|
|
1336
1339
|
async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onError) {
|
|
1337
1340
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}&alt=sse`;
|
|
1338
1341
|
const contents = [];
|
|
1339
|
-
|
|
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];
|
|
1340
1345
|
if (msg.role === "system") continue;
|
|
1341
1346
|
const role = msg.role === "assistant" ? "model" : "user";
|
|
1342
|
-
if (msg.role === "user" && typeof msg.content === "string") {
|
|
1347
|
+
if (msg.role === "user" && typeof msg.content === "string" && i === lastUserIdx) {
|
|
1343
1348
|
const contextParts = {};
|
|
1344
1349
|
if (context.selectedElement) {
|
|
1345
1350
|
contextParts.selectedElement = context.selectedElement.outerHTML;
|
|
@@ -1381,9 +1386,7 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
|
|
|
1381
1386
|
const generationConfig = {
|
|
1382
1387
|
maxOutputTokens: 8192
|
|
1383
1388
|
};
|
|
1384
|
-
|
|
1385
|
-
generationConfig.thinking_level = thinkingLevel.toUpperCase();
|
|
1386
|
-
}
|
|
1389
|
+
const thinkingConfig = thinkingLevel && thinkingLevel !== "none" ? { thinkingLevel: thinkingLevel.toUpperCase() } : void 0;
|
|
1387
1390
|
const body = {
|
|
1388
1391
|
system_instruction: {
|
|
1389
1392
|
parts: [{ text: SYSTEM_PROMPT }]
|
|
@@ -1391,6 +1394,9 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
|
|
|
1391
1394
|
contents,
|
|
1392
1395
|
generationConfig
|
|
1393
1396
|
};
|
|
1397
|
+
if (thinkingConfig) {
|
|
1398
|
+
body.thinkingConfig = thinkingConfig;
|
|
1399
|
+
}
|
|
1394
1400
|
try {
|
|
1395
1401
|
const response = await fetch(url, {
|
|
1396
1402
|
method: "POST",
|
|
@@ -1514,7 +1520,7 @@ function createOpenMagicServer(proxyPort, roots) {
|
|
|
1514
1520
|
"Content-Type": "application/json",
|
|
1515
1521
|
"Access-Control-Allow-Origin": "*"
|
|
1516
1522
|
});
|
|
1517
|
-
res.end(JSON.stringify({ status: "ok", version: "0.
|
|
1523
|
+
res.end(JSON.stringify({ status: "ok", version: "0.10.0" }));
|
|
1518
1524
|
return;
|
|
1519
1525
|
}
|
|
1520
1526
|
res.writeHead(404);
|
|
@@ -1525,7 +1531,12 @@ function createOpenMagicServer(proxyPort, roots) {
|
|
|
1525
1531
|
path: "/__openmagic__/ws"
|
|
1526
1532
|
});
|
|
1527
1533
|
const clientStates = /* @__PURE__ */ new WeakMap();
|
|
1528
|
-
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
|
+
}
|
|
1529
1540
|
clientStates.set(ws, { authenticated: false });
|
|
1530
1541
|
ws.on("message", async (data) => {
|
|
1531
1542
|
let msg;
|
|
@@ -1572,7 +1583,7 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1572
1583
|
id: msg.id,
|
|
1573
1584
|
type: "handshake.ok",
|
|
1574
1585
|
payload: {
|
|
1575
|
-
version: "0.
|
|
1586
|
+
version: "0.10.0",
|
|
1576
1587
|
roots,
|
|
1577
1588
|
config: {
|
|
1578
1589
|
provider: config.provider,
|
|
@@ -1625,7 +1636,8 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1625
1636
|
case "llm.chat": {
|
|
1626
1637
|
const payload = msg.payload;
|
|
1627
1638
|
const config = loadConfig();
|
|
1628
|
-
|
|
1639
|
+
const providerMeta = MODEL_REGISTRY?.[payload.provider || config.provider || ""];
|
|
1640
|
+
if (!config.apiKey && !providerMeta?.local) {
|
|
1629
1641
|
sendError(ws, "config_error", "API key not configured", msg.id);
|
|
1630
1642
|
return;
|
|
1631
1643
|
}
|
|
@@ -1669,7 +1681,6 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
|
|
|
1669
1681
|
if (payload.provider !== void 0) updates.provider = payload.provider;
|
|
1670
1682
|
if (payload.model !== void 0) updates.model = payload.model;
|
|
1671
1683
|
if (payload.apiKey !== void 0) updates.apiKey = payload.apiKey;
|
|
1672
|
-
if (payload.roots !== void 0) updates.roots = payload.roots;
|
|
1673
1684
|
saveConfig(updates);
|
|
1674
1685
|
send(ws, {
|
|
1675
1686
|
id: msg.id,
|
|
@@ -1903,7 +1914,7 @@ process.on("uncaughtException", (err) => {
|
|
|
1903
1914
|
process.exit(1);
|
|
1904
1915
|
});
|
|
1905
1916
|
var childProcesses = [];
|
|
1906
|
-
var VERSION = "0.
|
|
1917
|
+
var VERSION = "0.10.0";
|
|
1907
1918
|
function ask(question) {
|
|
1908
1919
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1909
1920
|
return new Promise((resolve3) => {
|
|
@@ -2038,6 +2049,11 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2038
2049
|
if (!started) {
|
|
2039
2050
|
process.exit(1);
|
|
2040
2051
|
}
|
|
2052
|
+
const recheck = await detectDevServer();
|
|
2053
|
+
if (recheck) {
|
|
2054
|
+
targetPort = recheck.port;
|
|
2055
|
+
targetHost = recheck.host;
|
|
2056
|
+
}
|
|
2041
2057
|
}
|
|
2042
2058
|
} else {
|
|
2043
2059
|
console.log(chalk.dim(" Scanning for dev server..."));
|