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 CHANGED
@@ -10,7 +10,8 @@ import { createInterface } from "readline";
10
10
 
11
11
  // src/proxy.ts
12
12
  import http from "http";
13
- import { createGunzip, createInflate, createBrotliDecompress } from "zlib";
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
- return new Promise((resolve3, reject) => {
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
- let source = stream;
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
- const gunzip = createGunzip();
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
- const inflate = createInflate();
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
- const brotli = createBrotliDecompress();
123
- stream.pipe(brotli);
124
- source = brotli;
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
- 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
- });
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
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
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
- return resolved.startsWith(resolvedRoot);
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
- for (const msg of messages) {
1097
- if (msg.role === "user" && typeof msg.content === "string") {
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
- for (const msg of messages) {
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
- for (const msg of messages) {
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
- if (thinkingLevel && thinkingLevel !== "none") {
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.9.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.9.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
- if (!config.apiKey) {
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.9.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..."));