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 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,
@@ -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
- body.max_tokens = Math.min(modelInfo.maxOutput, 16384);
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
- 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];
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
- 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];
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
- if (thinkingLevel && thinkingLevel !== "none") {
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.8.2" }));
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.8.2",
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
- if (!config.apiKey) {
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.8.2";
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..."));