openmagic 0.36.4 → 0.38.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
@@ -4,9 +4,10 @@
4
4
  import { Command } from "commander";
5
5
  import chalk from "chalk";
6
6
  import open from "open";
7
- import { resolve as resolve3, join as join5 } from "path";
8
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
9
- import { spawn as spawn4 } from "child_process";
7
+ import { resolve as resolve3, join as join6 } from "path";
8
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
9
+ import { spawn as spawn5, execSync as execSync2 } from "child_process";
10
+ import http2 from "http";
10
11
  import { createInterface } from "readline";
11
12
 
12
13
  // src/proxy.ts
@@ -31,8 +32,8 @@ function validateToken(token) {
31
32
  }
32
33
 
33
34
  // src/server.ts
34
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
35
- import { join as join3, dirname as dirname2 } from "path";
35
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
36
+ import { join as join4, dirname as dirname2 } from "path";
36
37
  import { fileURLToPath } from "url";
37
38
  import { WebSocketServer, WebSocket } from "ws";
38
39
 
@@ -73,19 +74,145 @@ function saveConfig(updates) {
73
74
  }
74
75
  }
75
76
 
77
+ // src/llm/cli-detect.ts
78
+ import { spawn } from "child_process";
79
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
80
+ import { join as join2 } from "path";
81
+ import { homedir as homedir2 } from "os";
82
+ var CLI_AGENTS = [
83
+ { id: "claude-code", name: "Claude Code", command: "claude" },
84
+ { id: "codex-cli", name: "Codex CLI", command: "codex" },
85
+ { id: "gemini-cli", name: "Gemini CLI", command: "gemini" }
86
+ ];
87
+ var cachedResults = null;
88
+ var cacheTimestamp = 0;
89
+ var CACHE_TTL = 6e4;
90
+ function invalidateCliCache() {
91
+ cachedResults = null;
92
+ cacheTimestamp = 0;
93
+ }
94
+ async function detectAvailableClis() {
95
+ if (cachedResults && Date.now() - cacheTimestamp < CACHE_TTL) {
96
+ return cachedResults;
97
+ }
98
+ const results = await Promise.all(
99
+ CLI_AGENTS.map(async (agent) => {
100
+ const { installed, version } = await checkInstalled(agent.command);
101
+ const authenticated = installed ? checkAuthenticated(agent.id) : false;
102
+ return {
103
+ id: agent.id,
104
+ name: agent.name,
105
+ command: agent.command,
106
+ installed,
107
+ authenticated,
108
+ version
109
+ };
110
+ })
111
+ );
112
+ cachedResults = results;
113
+ cacheTimestamp = Date.now();
114
+ return results;
115
+ }
116
+ function checkInstalled(command) {
117
+ return new Promise((resolve4) => {
118
+ try {
119
+ const proc = spawn(command, ["--version"], {
120
+ stdio: ["ignore", "pipe", "pipe"],
121
+ timeout: 5e3
122
+ });
123
+ let stdout = "";
124
+ proc.stdout?.on("data", (d) => {
125
+ stdout += d.toString();
126
+ });
127
+ proc.on("error", () => resolve4({ installed: false }));
128
+ proc.on("close", (code) => {
129
+ if (code === 0) {
130
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
131
+ resolve4({ installed: true, version: match?.[1] });
132
+ } else {
133
+ resolve4({ installed: false });
134
+ }
135
+ });
136
+ } catch {
137
+ resolve4({ installed: false });
138
+ }
139
+ });
140
+ }
141
+ function checkAuthenticated(cliId) {
142
+ switch (cliId) {
143
+ case "claude-code":
144
+ return isClaudeAuthenticated();
145
+ case "codex-cli":
146
+ return isCodexAuthenticated();
147
+ case "gemini-cli":
148
+ return isGeminiAuthenticated();
149
+ default:
150
+ return false;
151
+ }
152
+ }
153
+ function isClaudeAuthenticated() {
154
+ if (process.env.ANTHROPIC_API_KEY) return true;
155
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return true;
156
+ try {
157
+ const configPath = join2(
158
+ process.env.CLAUDE_CONFIG_DIR || join2(homedir2(), ".claude"),
159
+ ".claude.json"
160
+ );
161
+ if (existsSync2(configPath)) {
162
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
163
+ if (config.oauthAccount) return true;
164
+ }
165
+ } catch {
166
+ }
167
+ return false;
168
+ }
169
+ function isCodexAuthenticated() {
170
+ if (process.env.OPENAI_API_KEY) return true;
171
+ try {
172
+ const authPath = join2(
173
+ process.env.CODEX_HOME || join2(homedir2(), ".codex"),
174
+ "auth.json"
175
+ );
176
+ if (existsSync2(authPath)) {
177
+ const auth = JSON.parse(readFileSync2(authPath, "utf-8"));
178
+ if (auth.auth_mode && (auth.OPENAI_API_KEY || auth.tokens)) return true;
179
+ }
180
+ } catch {
181
+ }
182
+ return false;
183
+ }
184
+ function isGeminiAuthenticated() {
185
+ if (process.env.GEMINI_API_KEY) return true;
186
+ if (process.env.GOOGLE_API_KEY) return true;
187
+ try {
188
+ const credsPath = join2(homedir2(), ".gemini", "oauth_creds.json");
189
+ if (existsSync2(credsPath)) {
190
+ const creds = JSON.parse(readFileSync2(credsPath, "utf-8"));
191
+ if (creds.refresh_token) return true;
192
+ }
193
+ } catch {
194
+ }
195
+ return false;
196
+ }
197
+
76
198
  // src/filesystem.ts
77
199
  import {
78
- readFileSync as readFileSync2,
200
+ readFileSync as readFileSync3,
79
201
  writeFileSync as writeFileSync2,
80
- existsSync as existsSync2,
202
+ existsSync as existsSync3,
81
203
  lstatSync,
82
204
  readdirSync,
83
205
  copyFileSync,
84
206
  mkdirSync as mkdirSync2,
85
207
  realpathSync,
86
- rmSync
208
+ unlinkSync,
209
+ rmSync,
210
+ openSync,
211
+ fsyncSync,
212
+ closeSync,
213
+ renameSync as renameSync2
87
214
  } from "fs";
88
- import { join as join2, resolve, relative, dirname, extname } from "path";
215
+ import { join as join3, resolve, relative, dirname, extname } from "path";
89
216
  import { tmpdir } from "os";
90
217
  import { createHash } from "crypto";
91
218
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
@@ -134,33 +261,39 @@ function isPathSafe(filePath, roots) {
134
261
  return !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\") && (!realRel.startsWith("..") && !realRel.startsWith("/") && !realRel.startsWith("\\"));
135
262
  });
136
263
  }
264
+ var fileMetadata = /* @__PURE__ */ new Map();
137
265
  function readFileSafe(filePath, roots) {
138
266
  if (!isPathSafe(filePath, roots)) {
139
267
  return { error: "Path is outside allowed roots" };
140
268
  }
141
- if (!existsSync2(filePath)) {
269
+ if (!existsSync3(filePath)) {
142
270
  return { error: "File not found" };
143
271
  }
144
272
  try {
145
- const content = readFileSync2(filePath, "utf-8");
273
+ const raw = readFileSync3(filePath);
274
+ const hasBOM = raw[0] === 239 && raw[1] === 187 && raw[2] === 191;
275
+ let content = hasBOM ? raw.subarray(3).toString("utf-8") : raw.toString("utf-8");
276
+ const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
277
+ if (lineEnding === "\r\n") content = content.replace(/\r\n/g, "\n");
278
+ fileMetadata.set(resolve(filePath), { hasBOM, lineEnding });
146
279
  return { content };
147
280
  } catch (e) {
148
281
  return { error: `Failed to read file: ${e.message}` };
149
282
  }
150
283
  }
151
- var BACKUP_DIR = join2(tmpdir(), "openmagic-backups");
284
+ var BACKUP_DIR = join3(tmpdir(), "openmagic-backups");
152
285
  var backupMap = /* @__PURE__ */ new Map();
153
286
  function getBackupPath(filePath) {
154
287
  const hash = createHash("md5").update(resolve(filePath)).digest("hex").slice(0, 12);
155
288
  const name = filePath.split(/[/\\]/).pop() || "file";
156
- return join2(BACKUP_DIR, `${hash}_${name}`);
289
+ return join3(BACKUP_DIR, `${hash}_${name}`);
157
290
  }
158
291
  function getBackupForFile(filePath) {
159
292
  return backupMap.get(resolve(filePath));
160
293
  }
161
294
  function cleanupBackups() {
162
295
  try {
163
- if (existsSync2(BACKUP_DIR)) {
296
+ if (existsSync3(BACKUP_DIR)) {
164
297
  rmSync(BACKUP_DIR, { recursive: true, force: true });
165
298
  }
166
299
  } catch {
@@ -173,17 +306,39 @@ function writeFileSafe(filePath, content, roots) {
173
306
  }
174
307
  try {
175
308
  let backupPath;
176
- if (existsSync2(filePath)) {
177
- if (!existsSync2(BACKUP_DIR)) mkdirSync2(BACKUP_DIR, { recursive: true });
309
+ if (existsSync3(filePath)) {
310
+ if (!existsSync3(BACKUP_DIR)) mkdirSync2(BACKUP_DIR, { recursive: true });
178
311
  backupPath = getBackupPath(filePath);
179
312
  copyFileSync(filePath, backupPath);
180
313
  backupMap.set(resolve(filePath), backupPath);
181
314
  }
182
315
  const dir = dirname(filePath);
183
- if (!existsSync2(dir)) {
316
+ if (!existsSync3(dir)) {
184
317
  mkdirSync2(dir, { recursive: true });
185
318
  }
186
- writeFileSync2(filePath, content, "utf-8");
319
+ let output = content;
320
+ const meta = fileMetadata.get(resolve(filePath));
321
+ if (meta) {
322
+ if (meta.lineEnding === "\r\n") output = output.replace(/\n/g, "\r\n");
323
+ if (meta.hasBOM) output = "\uFEFF" + output;
324
+ }
325
+ const tmpPath = filePath + ".openmagic-tmp-" + Date.now();
326
+ writeFileSync2(tmpPath, output, "utf-8");
327
+ try {
328
+ const fd = openSync(tmpPath, "r");
329
+ fsyncSync(fd);
330
+ closeSync(fd);
331
+ } catch {
332
+ }
333
+ try {
334
+ renameSync2(tmpPath, filePath);
335
+ } catch {
336
+ writeFileSync2(filePath, output, "utf-8");
337
+ try {
338
+ unlinkSync(tmpPath);
339
+ } catch {
340
+ }
341
+ }
187
342
  return { ok: true, backupPath };
188
343
  } catch (e) {
189
344
  return { ok: false, error: `Failed to write file: ${e.message}` };
@@ -207,7 +362,7 @@ function listFiles(rootPath, roots, maxDepth = 4) {
207
362
  if (entries.length >= MAX_LIST_ENTRIES) return;
208
363
  if (IGNORED_DIRS.has(item)) continue;
209
364
  if (item.startsWith(".") && item !== ".env.example") continue;
210
- const fullPath = join2(dir, item);
365
+ const fullPath = join3(dir, item);
211
366
  let stat;
212
367
  try {
213
368
  stat = lstatSync(fullPath);
@@ -271,7 +426,7 @@ function grepFiles(pattern, searchRoot, roots, maxResults = 30) {
271
426
  for (const item of items) {
272
427
  if (results.length >= maxResults || filesScanned >= MAX_GREP_FILES_SCANNED) return;
273
428
  if (IGNORED_DIRS.has(item) || item.startsWith(".") && item !== ".env.example") continue;
274
- const fullPath = join2(dir, item);
429
+ const fullPath = join3(dir, item);
275
430
  let stat;
276
431
  try {
277
432
  stat = lstatSync(fullPath);
@@ -287,7 +442,7 @@ function grepFiles(pattern, searchRoot, roots, maxResults = 30) {
287
442
  if (stat.size > MAX_GREP_FILE_SIZE) continue;
288
443
  filesScanned++;
289
444
  try {
290
- const content = readFileSync2(fullPath, "utf-8");
445
+ const content = readFileSync3(fullPath, "utf-8");
291
446
  const lines = content.split("\n");
292
447
  let fileMatches = 0;
293
448
  for (let i = 0; i < lines.length && fileMatches < 5; i++) {
@@ -1065,11 +1220,17 @@ You MUST respond with valid JSON in this exact format:
1065
1220
  5. Keep modifications minimal \u2014 change only what's needed. Do NOT rewrite entire files.
1066
1221
  6. If the grounded files don't contain the code you need to modify, return: {"modifications":[],"explanation":"NEED_FILE: exact/relative/path/to/file.ext"}
1067
1222
  7. To search for a pattern across the codebase, return: {"modifications":[],"explanation":"SEARCH_FILES: \\"pattern\\" in optional/path/"}
1068
- 7. For style changes: check the dependencies (package.json) to know if the project uses Tailwind, MUI, etc. Use the project's styling approach, not raw CSS
1069
- 8. Use the selected element's cssSelector, className, parentContainerStyles, and siblings to understand the full layout context
1070
- 9. Use the page URL route and componentHint to identify the correct source file to modify
1071
- 10. Always preserve existing code style, conventions, and indentation
1072
- 11. ALWAYS respond with valid JSON only \u2014 no text before or after the JSON object`;
1223
+ 8. For style changes: check the dependencies (package.json) to know if the project uses Tailwind, MUI, etc. Use the project's styling approach, not raw CSS
1224
+ 9. Use the selected element's cssSelector, className, parentContainerStyles, and siblings to understand the full layout context
1225
+ 10. Use the page URL route and componentHint to identify the correct source file to modify
1226
+ 11. Use childrenLayout pixel measurements for spacing. gapToNext.vertical=0 means elements are touching.
1227
+ 12. Use resolvedClasses to see actual CSS values behind utility classes (space-y-6 = margin-top: 1.5rem).
1228
+ 13. Check themeState.darkMode \u2014 if true, use dark-mode-aware colors and respect the project's dark mode classes.
1229
+ 14. Use cssVariables to leverage existing design tokens (var(--color-primary)) instead of hardcoding hex values.
1230
+ 15. Check activeBreakpoints to know which responsive breakpoint is active. Suggest responsive-aware changes.
1231
+ 16. Check visibilityState \u2014 the element may be scrolled out of view, hidden, or inside a scrollable container.
1232
+ 17. Always preserve existing code style, conventions, and indentation
1233
+ 18. ALWAYS respond with valid JSON only \u2014 no text before or after the JSON object`;
1073
1234
  function buildContextParts(context) {
1074
1235
  const parts = {};
1075
1236
  if (context.selectedElement) {
@@ -1111,6 +1272,15 @@ function buildContextParts(context) {
1111
1272
  if (el.resolvedClasses?.length) {
1112
1273
  elementData.resolvedClasses = el.resolvedClasses;
1113
1274
  }
1275
+ if (el.themeState) elementData.themeState = el.themeState;
1276
+ if (el.cssVariables && Object.keys(el.cssVariables).length) elementData.cssVariables = el.cssVariables;
1277
+ if (el.stackingContext) elementData.stackingContext = el.stackingContext;
1278
+ if (el.visibilityState) elementData.visibilityState = el.visibilityState;
1279
+ if (el.activeBreakpoints?.length) elementData.activeBreakpoints = el.activeBreakpoints;
1280
+ if (el.pseudoElements && (el.pseudoElements.before !== "none" || el.pseudoElements.after !== "none")) {
1281
+ elementData.pseudoElements = el.pseudoElements;
1282
+ }
1283
+ if (el.formState) elementData.formState = el.formState;
1114
1284
  parts.selectedElement = JSON.stringify(elementData, null, 2);
1115
1285
  }
1116
1286
  if (context.files?.length) {
@@ -1519,13 +1689,13 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
1519
1689
  }
1520
1690
 
1521
1691
  // src/llm/claude-code.ts
1522
- import { spawn } from "child_process";
1692
+ import { spawn as spawn2 } from "child_process";
1523
1693
  async function chatClaudeCode(messages, context, onChunk, onDone, onError) {
1524
1694
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
1525
1695
  const userPrompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "Help me with this element.";
1526
1696
  const contextParts = buildContextParts(context);
1527
1697
  const fullPrompt = buildUserMessage(userPrompt, contextParts);
1528
- const proc = spawn(
1698
+ const proc = spawn2(
1529
1699
  "claude",
1530
1700
  [
1531
1701
  "-p",
@@ -1639,7 +1809,7 @@ function extractText(event) {
1639
1809
  }
1640
1810
 
1641
1811
  // src/llm/codex-cli.ts
1642
- import { spawn as spawn2 } from "child_process";
1812
+ import { spawn as spawn3 } from "child_process";
1643
1813
  async function chatCodexCli(messages, context, onChunk, onDone, onError) {
1644
1814
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
1645
1815
  const userPrompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "Help me with this element.";
@@ -1647,7 +1817,7 @@ async function chatCodexCli(messages, context, onChunk, onDone, onError) {
1647
1817
  const fullPrompt = `${SYSTEM_PROMPT}
1648
1818
 
1649
1819
  ${buildUserMessage(userPrompt, contextParts)}`;
1650
- const proc = spawn2(
1820
+ const proc = spawn3(
1651
1821
  "codex",
1652
1822
  ["exec", "--full-auto", "--json", "--skip-git-repo-check", "-"],
1653
1823
  {
@@ -1709,20 +1879,17 @@ ${buildUserMessage(userPrompt, contextParts)}`;
1709
1879
  });
1710
1880
  }
1711
1881
  function extractCodexText(event) {
1712
- if (event.type === "item.completed" || event.type === "item.updated" || event.type === "item.started") {
1882
+ if (event.type === "item.completed") {
1713
1883
  const item = event.item;
1714
1884
  if (item?.type === "agent_message" && typeof item.text === "string") {
1715
1885
  return item.text;
1716
1886
  }
1717
1887
  }
1718
- if (event.type === "error" && typeof event.message === "string") {
1719
- return void 0;
1720
- }
1721
1888
  return void 0;
1722
1889
  }
1723
1890
 
1724
1891
  // src/llm/gemini-cli.ts
1725
- import { spawn as spawn3 } from "child_process";
1892
+ import { spawn as spawn4 } from "child_process";
1726
1893
  async function chatGeminiCli(messages, context, onChunk, onDone, onError) {
1727
1894
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
1728
1895
  const userPrompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "Help me with this element.";
@@ -1730,13 +1897,9 @@ async function chatGeminiCli(messages, context, onChunk, onDone, onError) {
1730
1897
  const fullPrompt = `${SYSTEM_PROMPT}
1731
1898
 
1732
1899
  ${buildUserMessage(userPrompt, contextParts)}`;
1733
- const proc = spawn3(
1900
+ const proc = spawn4(
1734
1901
  "gemini",
1735
1902
  [
1736
- "-p",
1737
- userPrompt,
1738
- "--output-format",
1739
- "stream-json",
1740
1903
  "--yolo"
1741
1904
  ],
1742
1905
  {
@@ -1744,41 +1907,14 @@ ${buildUserMessage(userPrompt, contextParts)}`;
1744
1907
  cwd: process.cwd()
1745
1908
  }
1746
1909
  );
1747
- proc.stdin.write(`${SYSTEM_PROMPT}
1748
-
1749
- ${buildUserMessage("", contextParts)}`);
1910
+ proc.stdin.write(fullPrompt);
1750
1911
  proc.stdin.end();
1751
1912
  let fullContent = "";
1752
- let resultContent = "";
1753
- let buffer = "";
1754
1913
  let errOutput = "";
1755
1914
  proc.stdout.on("data", (data) => {
1756
- buffer += data.toString();
1757
- const lines = buffer.split("\n");
1758
- buffer = lines.pop() || "";
1759
- for (const line of lines) {
1760
- if (!line.trim()) continue;
1761
- try {
1762
- const event = JSON.parse(line);
1763
- if (event.type === "result") {
1764
- if (typeof event.result === "string") {
1765
- resultContent = event.result;
1766
- }
1767
- continue;
1768
- }
1769
- const text = extractGeminiText(event);
1770
- if (text) {
1771
- fullContent += text;
1772
- onChunk(text);
1773
- }
1774
- } catch {
1775
- const trimmed = line.trim();
1776
- if (trimmed) {
1777
- fullContent += trimmed + "\n";
1778
- onChunk(trimmed + "\n");
1779
- }
1780
- }
1781
- }
1915
+ const text = data.toString();
1916
+ fullContent += text;
1917
+ onChunk(text);
1782
1918
  });
1783
1919
  proc.stderr.on("data", (data) => {
1784
1920
  errOutput += data.toString();
@@ -1791,23 +1927,8 @@ ${buildUserMessage("", contextParts)}`);
1791
1927
  }
1792
1928
  });
1793
1929
  proc.on("close", (code) => {
1794
- if (buffer.trim()) {
1795
- try {
1796
- const event = JSON.parse(buffer);
1797
- if (event.type === "result" && typeof event.result === "string") {
1798
- resultContent = event.result;
1799
- } else {
1800
- const text = extractGeminiText(event);
1801
- if (text) fullContent += text;
1802
- }
1803
- } catch {
1804
- const trimmed = buffer.trim();
1805
- if (trimmed) fullContent += trimmed;
1806
- }
1807
- }
1808
- const finalContent = resultContent || fullContent;
1809
- if (code === 0 || finalContent.trim()) {
1810
- onDone({ content: finalContent });
1930
+ if (code === 0 || fullContent.trim()) {
1931
+ onDone({ content: fullContent });
1811
1932
  } else {
1812
1933
  const err = errOutput.trim();
1813
1934
  if (err.includes("auth") || err.includes("GEMINI_API_KEY") || err.includes("credentials") || err.includes("login")) {
@@ -1818,21 +1939,6 @@ ${buildUserMessage("", contextParts)}`);
1818
1939
  }
1819
1940
  });
1820
1941
  }
1821
- function extractGeminiText(event) {
1822
- if (event.type === "assistant" || event.type === "message") {
1823
- const content = event.content ?? event.message?.content;
1824
- if (Array.isArray(content)) {
1825
- return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("");
1826
- }
1827
- if (typeof content === "string") return content;
1828
- if (typeof event.text === "string") return event.text;
1829
- }
1830
- if (event.type === "content_block_delta") {
1831
- const delta = event.delta;
1832
- if (typeof delta?.text === "string") return delta.text;
1833
- }
1834
- return void 0;
1835
- }
1836
1942
 
1837
1943
  // src/llm/proxy.ts
1838
1944
  var OPENAI_COMPATIBLE_PROVIDERS = /* @__PURE__ */ new Set([
@@ -1850,6 +1956,11 @@ var OPENAI_COMPATIBLE_PROVIDERS = /* @__PURE__ */ new Set([
1850
1956
  "doubao"
1851
1957
  ]);
1852
1958
  function extractJsonFromResponse(content) {
1959
+ try {
1960
+ JSON.parse(content);
1961
+ return content;
1962
+ } catch {
1963
+ }
1853
1964
  const mdMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
1854
1965
  if (mdMatch?.[1]) {
1855
1966
  const candidate = mdMatch[1].trim();
@@ -1888,11 +1999,28 @@ function extractJsonFromResponse(content) {
1888
1999
  JSON.parse(candidate);
1889
2000
  return candidate;
1890
2001
  } catch {
1891
- return null;
2002
+ break;
1892
2003
  }
1893
2004
  }
1894
2005
  }
1895
2006
  }
2007
+ if (depth > 0) {
2008
+ let repaired = content.substring(start);
2009
+ if (inString) repaired += '"';
2010
+ while (depth > 0) {
2011
+ repaired += "}";
2012
+ depth--;
2013
+ }
2014
+ try {
2015
+ JSON.parse(repaired);
2016
+ return repaired;
2017
+ } catch {
2018
+ }
2019
+ }
2020
+ const explMatch = content.match(/"explanation"\s*:\s*"((?:[^"\\]|\\.)*)"/);
2021
+ if (explMatch) {
2022
+ return JSON.stringify({ modifications: [], explanation: JSON.parse('"' + explMatch[1] + '"') });
2023
+ }
1896
2024
  return null;
1897
2025
  }
1898
2026
  async function handleLlmChat(params, onChunk, onDone, onError) {
@@ -1909,13 +2037,19 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
1909
2037
  }
1910
2038
  onDone({ content: result.content, modifications });
1911
2039
  };
2040
+ const cliOnError = (error) => {
2041
+ if (error.includes("not found") || error.includes("ENOENT") || error.includes("not authenticated") || error.includes("not logged in")) {
2042
+ invalidateCliCache();
2043
+ }
2044
+ onError(error);
2045
+ };
1912
2046
  try {
1913
2047
  if (provider === "claude-code") {
1914
- await chatClaudeCode(messages, context, onChunk, wrappedOnDone, onError);
2048
+ await chatClaudeCode(messages, context, onChunk, wrappedOnDone, cliOnError);
1915
2049
  } else if (provider === "codex-cli") {
1916
- await chatCodexCli(messages, context, onChunk, wrappedOnDone, onError);
2050
+ await chatCodexCli(messages, context, onChunk, wrappedOnDone, cliOnError);
1917
2051
  } else if (provider === "gemini-cli") {
1918
- await chatGeminiCli(messages, context, onChunk, wrappedOnDone, onError);
2052
+ await chatGeminiCli(messages, context, onChunk, wrappedOnDone, cliOnError);
1919
2053
  } else if (provider === "anthropic") {
1920
2054
  await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1921
2055
  } else if (provider === "google") {
@@ -2124,7 +2258,7 @@ async function handleMessage(ws, msg, state, roots) {
2124
2258
  break;
2125
2259
  }
2126
2260
  try {
2127
- const backupContent = readFileSync3(backupPath, "utf-8");
2261
+ const backupContent = readFileSync4(backupPath, "utf-8");
2128
2262
  const writeResult = writeFileSafe(payload.path, backupContent, roots);
2129
2263
  if (!writeResult.ok) {
2130
2264
  sendError(ws, "fs_error", writeResult.error || "Undo failed", msg.id);
@@ -2179,17 +2313,35 @@ async function handleMessage(ws, msg, state, roots) {
2179
2313
  }
2180
2314
  case "config.get": {
2181
2315
  const config = loadConfig();
2316
+ const detectedClis = await detectAvailableClis();
2317
+ const cliStatuses = detectedClis.map((c) => ({
2318
+ id: c.id,
2319
+ name: c.name,
2320
+ installed: c.installed,
2321
+ authenticated: c.authenticated,
2322
+ version: c.version
2323
+ }));
2324
+ let provider = config.provider || "";
2325
+ let model = config.model || "";
2326
+ if (!provider) {
2327
+ const bestCli = detectedClis.find((c) => c.installed && c.authenticated);
2328
+ if (bestCli) {
2329
+ provider = bestCli.id;
2330
+ model = bestCli.id;
2331
+ }
2332
+ }
2182
2333
  send(ws, {
2183
2334
  id: msg.id,
2184
2335
  type: "config.value",
2185
2336
  payload: {
2186
- provider: config.provider,
2187
- model: config.model,
2188
- hasApiKey: !!(config.apiKeys?.[config.provider || ""] || config.apiKey),
2337
+ provider,
2338
+ model,
2339
+ hasApiKey: !!(config.apiKeys?.[provider] || config.apiKey),
2189
2340
  roots: config.roots || roots,
2190
2341
  apiKeys: Object.fromEntries(
2191
2342
  Object.entries(config.apiKeys || {}).map(([k]) => [k, true])
2192
- )
2343
+ ),
2344
+ detectedClis: cliStatuses
2193
2345
  }
2194
2346
  });
2195
2347
  break;
@@ -2222,7 +2374,7 @@ async function handleMessage(ws, msg, state, roots) {
2222
2374
  sendError(ws, "invalid_payload", "Missing pattern", msg.id);
2223
2375
  break;
2224
2376
  }
2225
- const searchRoot = payload.path ? join3(roots[0] || process.cwd(), payload.path) : roots[0] || process.cwd();
2377
+ const searchRoot = payload.path ? join4(roots[0] || process.cwd(), payload.path) : roots[0] || process.cwd();
2226
2378
  const results = grepFiles(payload.pattern, searchRoot, roots);
2227
2379
  send(ws, { id: msg.id, type: "fs.grep.result", payload: { results } });
2228
2380
  break;
@@ -2258,13 +2410,13 @@ function sendError(ws, code, message, id) {
2258
2410
  }
2259
2411
  function serveToolbarBundle(res) {
2260
2412
  const bundlePaths = [
2261
- join3(__dirname, "toolbar", "index.global.js"),
2262
- join3(__dirname, "..", "dist", "toolbar", "index.global.js")
2413
+ join4(__dirname, "toolbar", "index.global.js"),
2414
+ join4(__dirname, "..", "dist", "toolbar", "index.global.js")
2263
2415
  ];
2264
2416
  for (const bundlePath of bundlePaths) {
2265
2417
  try {
2266
- if (existsSync3(bundlePath)) {
2267
- const content = readFileSync3(bundlePath, "utf-8");
2418
+ if (existsSync4(bundlePath)) {
2419
+ const content = readFileSync4(bundlePath, "utf-8");
2268
2420
  res.writeHead(200, {
2269
2421
  "Content-Type": "application/javascript",
2270
2422
  "Access-Control-Allow-Origin": "*",
@@ -2407,8 +2559,8 @@ function buildInjectionScript(token) {
2407
2559
 
2408
2560
  // src/detect.ts
2409
2561
  import { createConnection } from "net";
2410
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
2411
- import { join as join4, resolve as resolve2 } from "path";
2562
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
2563
+ import { join as join5, resolve as resolve2 } from "path";
2412
2564
  import { execSync } from "child_process";
2413
2565
  var COMMON_DEV_PORTS = [
2414
2566
  3e3,
@@ -2565,11 +2717,11 @@ var FRAMEWORK_PATTERNS = [
2565
2717
  ];
2566
2718
  var DEV_SCRIPT_NAMES = ["dev", "start", "serve", "develop", "dev:start", "start:dev", "server", "dev:server", "web", "frontend"];
2567
2719
  function detectDevScripts(cwd = process.cwd()) {
2568
- const pkgPath = join4(cwd, "package.json");
2569
- if (!existsSync4(pkgPath)) return [];
2720
+ const pkgPath = join5(cwd, "package.json");
2721
+ if (!existsSync5(pkgPath)) return [];
2570
2722
  let pkg;
2571
2723
  try {
2572
- pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
2724
+ pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
2573
2725
  } catch {
2574
2726
  return [];
2575
2727
  }
@@ -2633,10 +2785,10 @@ function checkNodeCompatibility(framework) {
2633
2785
  function checkEnvPort(cwd = process.cwd()) {
2634
2786
  const envFiles = [".env.local", ".env.development.local", ".env.development", ".env"];
2635
2787
  for (const envFile of envFiles) {
2636
- const envPath = join4(cwd, envFile);
2637
- if (!existsSync4(envPath)) continue;
2788
+ const envPath = join5(cwd, envFile);
2789
+ if (!existsSync5(envPath)) continue;
2638
2790
  try {
2639
- const content = readFileSync4(envPath, "utf-8");
2791
+ const content = readFileSync5(envPath, "utf-8");
2640
2792
  const match = content.match(/^PORT\s*=\s*(\d+)/m);
2641
2793
  if (match) return parseInt(match[1], 10);
2642
2794
  } catch {
@@ -2653,8 +2805,8 @@ function scanParentLockfiles(projectDir) {
2653
2805
  let dir = resolve2(project, "..");
2654
2806
  while (dir.length > 1 && dir.length >= home.length) {
2655
2807
  for (const lockfile of LOCKFILE_NAMES) {
2656
- const p = join4(dir, lockfile);
2657
- if (existsSync4(p)) found.push(p);
2808
+ const p = join5(dir, lockfile);
2809
+ if (existsSync5(p)) found.push(p);
2658
2810
  }
2659
2811
  const parent = resolve2(dir, "..");
2660
2812
  if (parent === dir) break;
@@ -2663,10 +2815,10 @@ function scanParentLockfiles(projectDir) {
2663
2815
  return found;
2664
2816
  }
2665
2817
  function getProjectName(cwd = process.cwd()) {
2666
- const pkgPath = join4(cwd, "package.json");
2667
- if (!existsSync4(pkgPath)) return "this project";
2818
+ const pkgPath = join5(cwd, "package.json");
2819
+ if (!existsSync5(pkgPath)) return "this project";
2668
2820
  try {
2669
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
2821
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
2670
2822
  return pkg.name || "this project";
2671
2823
  } catch {
2672
2824
  return "this project";
@@ -2686,10 +2838,10 @@ var INSTALL_COMMANDS = {
2686
2838
  bun: "bun install"
2687
2839
  };
2688
2840
  function checkDependenciesInstalled(cwd = process.cwd()) {
2689
- const hasNodeModules = existsSync4(join4(cwd, "node_modules"));
2841
+ const hasNodeModules = existsSync5(join5(cwd, "node_modules"));
2690
2842
  let pm = "npm";
2691
2843
  for (const { file, pm: detectedPm } of LOCK_FILES) {
2692
- if (existsSync4(join4(cwd, file))) {
2844
+ if (existsSync5(join5(cwd, file))) {
2693
2845
  pm = detectedPm;
2694
2846
  break;
2695
2847
  }
@@ -2730,6 +2882,59 @@ function ask(question) {
2730
2882
  });
2731
2883
  });
2732
2884
  }
2885
+ function waitForPortClose(port, timeoutMs = 1e4) {
2886
+ const start = Date.now();
2887
+ return new Promise((resolve4) => {
2888
+ const check = async () => {
2889
+ if (!await isPortOpen(port)) {
2890
+ resolve4(true);
2891
+ return;
2892
+ }
2893
+ if (Date.now() - start > timeoutMs) {
2894
+ resolve4(false);
2895
+ return;
2896
+ }
2897
+ setTimeout(check, 300);
2898
+ };
2899
+ check();
2900
+ });
2901
+ }
2902
+ function killPortProcess(port) {
2903
+ try {
2904
+ const pidOutput = execSync2(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null`, {
2905
+ encoding: "utf-8",
2906
+ timeout: 3e3
2907
+ }).trim();
2908
+ if (!pidOutput) return false;
2909
+ const pids = pidOutput.split("\n").map((p) => p.trim()).filter(Boolean);
2910
+ for (const pid of pids) {
2911
+ try {
2912
+ process.kill(parseInt(pid, 10), "SIGTERM");
2913
+ } catch {
2914
+ }
2915
+ }
2916
+ return pids.length > 0;
2917
+ } catch {
2918
+ return false;
2919
+ }
2920
+ }
2921
+ function isPortHealthy(host, port) {
2922
+ return new Promise((resolve4) => {
2923
+ const req = http2.get(
2924
+ `http://${host}:${port}/`,
2925
+ { timeout: 3e3 },
2926
+ (res) => {
2927
+ resolve4(res.statusCode !== void 0 && res.statusCode < 400);
2928
+ res.resume();
2929
+ }
2930
+ );
2931
+ req.on("error", () => resolve4(false));
2932
+ req.on("timeout", () => {
2933
+ req.destroy();
2934
+ resolve4(false);
2935
+ });
2936
+ });
2937
+ }
2733
2938
  function waitForPort(port, timeoutMs = 6e4, shouldAbort) {
2734
2939
  const start = Date.now();
2735
2940
  return new Promise((resolve4) => {
@@ -2756,7 +2961,7 @@ function waitForPort(port, timeoutMs = 6e4, shouldAbort) {
2756
2961
  function runCommand(cmd, args, cwd = process.cwd()) {
2757
2962
  return new Promise((resolve4) => {
2758
2963
  try {
2759
- const child = spawn4(cmd, args, {
2964
+ const child = spawn5(cmd, args, {
2760
2965
  cwd,
2761
2966
  stdio: ["ignore", "pipe", "pipe"],
2762
2967
  shell: true
@@ -2894,8 +3099,49 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2894
3099
  console.log(chalk.dim(" Scanning for dev server..."));
2895
3100
  const detected = await detectDevServer();
2896
3101
  if (detected && detected.fromScripts) {
2897
- targetPort = detected.port;
2898
- targetHost = detected.host;
3102
+ const healthy = await isPortHealthy(detected.host, detected.port);
3103
+ if (healthy) {
3104
+ targetPort = detected.port;
3105
+ targetHost = detected.host;
3106
+ } else {
3107
+ console.log(chalk.yellow(` \u26A0 Dev server on port ${detected.port} is not responding properly.`));
3108
+ console.log(chalk.dim(" Cleaning up orphaned process..."));
3109
+ killPortProcess(detected.port);
3110
+ const freed = await waitForPortClose(detected.port, 5e3);
3111
+ if (!freed) {
3112
+ try {
3113
+ const pidOutput = execSync2(`lsof -i :${detected.port} -sTCP:LISTEN -t 2>/dev/null`, {
3114
+ encoding: "utf-8",
3115
+ timeout: 3e3
3116
+ }).trim();
3117
+ for (const pid of pidOutput.split("\n").filter(Boolean)) {
3118
+ try {
3119
+ process.kill(parseInt(pid, 10), "SIGKILL");
3120
+ } catch {
3121
+ }
3122
+ }
3123
+ await waitForPortClose(detected.port, 3e3);
3124
+ } catch {
3125
+ }
3126
+ }
3127
+ const started = await offerToStartDevServer();
3128
+ if (!started) {
3129
+ process.exit(1);
3130
+ }
3131
+ if (lastDetectedPort) {
3132
+ targetPort = lastDetectedPort;
3133
+ } else {
3134
+ const redetected = await detectDevServer();
3135
+ if (redetected) {
3136
+ targetPort = redetected.port;
3137
+ targetHost = redetected.host;
3138
+ } else {
3139
+ console.log(chalk.red(" \u2717 Could not detect the dev server after starting."));
3140
+ console.log(chalk.dim(" Try specifying the port: npx openmagic --port 3000"));
3141
+ process.exit(1);
3142
+ }
3143
+ }
3144
+ }
2899
3145
  } else if (detected && !detected.fromScripts) {
2900
3146
  const answer = await ask(
2901
3147
  chalk.yellow(` Found a server on port ${detected.port}. Is this your project's dev server? `) + chalk.dim("(y/n) ")
@@ -2987,58 +3233,89 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2987
3233
  }
2988
3234
  });
2989
3235
  let shuttingDown = false;
2990
- const shutdown = () => {
3236
+ const shutdown = async () => {
2991
3237
  if (shuttingDown) return;
2992
3238
  shuttingDown = true;
2993
3239
  console.log("");
2994
3240
  console.log(chalk.dim(" Shutting down OpenMagic..."));
2995
3241
  cleanupBackups();
2996
3242
  proxyServer.close();
2997
- for (const cp of childProcesses) {
3243
+ const alive = childProcesses.filter((cp) => cp.exitCode === null);
3244
+ for (const cp of alive) {
2998
3245
  try {
2999
3246
  cp.kill("SIGTERM");
3000
3247
  } catch {
3001
3248
  }
3002
3249
  }
3003
- const forceExit = setTimeout(() => {
3004
- for (const cp of childProcesses) {
3005
- try {
3006
- cp.kill("SIGKILL");
3007
- } catch {
3250
+ if (targetPort) {
3251
+ killPortProcess(targetPort);
3252
+ }
3253
+ if (targetPort && await isPortOpen(targetPort)) {
3254
+ const freed = await waitForPortClose(targetPort, 4e3);
3255
+ if (!freed) {
3256
+ console.log(chalk.dim(" Force-killing remaining processes..."));
3257
+ if (targetPort) {
3258
+ try {
3259
+ const pids = execSync2(`lsof -i :${targetPort} -sTCP:LISTEN -t 2>/dev/null`, {
3260
+ encoding: "utf-8",
3261
+ timeout: 2e3
3262
+ }).trim().split("\n").filter(Boolean);
3263
+ for (const pid of pids) {
3264
+ try {
3265
+ process.kill(parseInt(pid, 10), "SIGKILL");
3266
+ } catch {
3267
+ }
3268
+ }
3269
+ } catch {
3270
+ }
3008
3271
  }
3009
- }
3010
- process.exit(0);
3011
- }, 2e3);
3012
- forceExit.unref();
3013
- const allDead = childProcesses.every((cp) => cp.killed || cp.exitCode !== null);
3014
- if (allDead) {
3015
- clearTimeout(forceExit);
3016
- process.exit(0);
3017
- }
3018
- let remaining = childProcesses.filter((cp) => !cp.killed && cp.exitCode === null).length;
3019
- if (remaining === 0) {
3020
- clearTimeout(forceExit);
3021
- process.exit(0);
3022
- }
3023
- for (const cp of childProcesses) {
3024
- cp.once("exit", () => {
3025
- remaining--;
3026
- if (remaining <= 0) {
3027
- clearTimeout(forceExit);
3028
- process.exit(0);
3272
+ for (const cp of childProcesses) {
3273
+ if (cp.exitCode === null) {
3274
+ try {
3275
+ cp.kill("SIGKILL");
3276
+ } catch {
3277
+ }
3278
+ }
3029
3279
  }
3030
- });
3280
+ await new Promise((r) => setTimeout(r, 300));
3281
+ }
3282
+ } else {
3283
+ if (alive.length > 0) {
3284
+ await Promise.race([
3285
+ Promise.all(alive.map((cp) => new Promise((r) => {
3286
+ if (cp.exitCode !== null) {
3287
+ r();
3288
+ return;
3289
+ }
3290
+ cp.once("exit", () => r());
3291
+ }))),
3292
+ new Promise((r) => setTimeout(() => {
3293
+ for (const cp of childProcesses) {
3294
+ if (cp.exitCode === null) try {
3295
+ cp.kill("SIGKILL");
3296
+ } catch {
3297
+ }
3298
+ }
3299
+ setTimeout(r, 200);
3300
+ }, 3e3))
3301
+ ]);
3302
+ }
3031
3303
  }
3304
+ process.exit(0);
3032
3305
  };
3033
- process.on("SIGINT", shutdown);
3034
- process.on("SIGTERM", shutdown);
3306
+ process.on("SIGINT", () => {
3307
+ shutdown();
3308
+ });
3309
+ process.on("SIGTERM", () => {
3310
+ shutdown();
3311
+ });
3035
3312
  });
3036
3313
  async function offerToStartDevServer(expectedPort) {
3037
3314
  const projectName = getProjectName();
3038
3315
  const scripts = detectDevScripts();
3039
3316
  if (scripts.length === 0) {
3040
- const htmlPath = join5(process.cwd(), "index.html");
3041
- if (existsSync5(htmlPath)) {
3317
+ const htmlPath = join6(process.cwd(), "index.html");
3318
+ if (existsSync6(htmlPath)) {
3042
3319
  console.log(
3043
3320
  chalk.dim(" No dev scripts found, but index.html detected.")
3044
3321
  );
@@ -3051,7 +3328,7 @@ async function offerToStartDevServer(expectedPort) {
3051
3328
  }
3052
3329
  const staticPort = expectedPort || 8080;
3053
3330
  console.log(chalk.dim(` Starting static server on port ${staticPort}...`));
3054
- const staticChild = spawn4("node", ["-e", `
3331
+ const staticChild = spawn5("node", ["-e", `
3055
3332
  const http = require("http");
3056
3333
  const fs = require("fs");
3057
3334
  const path = require("path");
@@ -3225,7 +3502,7 @@ async function offerToStartDevServer(expectedPort) {
3225
3502
  }
3226
3503
  let child;
3227
3504
  try {
3228
- child = spawn4(runCmd, runArgs, {
3505
+ child = spawn5(runCmd, runArgs, {
3229
3506
  cwd: process.cwd(),
3230
3507
  stdio: "inherit",
3231
3508
  env: {
@@ -3253,25 +3530,16 @@ async function offerToStartDevServer(expectedPort) {
3253
3530
  \u2717 Dev server exited with code ${code}`));
3254
3531
  }
3255
3532
  });
3256
- const cleanup = () => {
3533
+ process.once("exit", () => {
3257
3534
  for (const cp of childProcesses) {
3258
- try {
3259
- cp.kill("SIGTERM");
3260
- } catch {
3261
- }
3262
- }
3263
- setTimeout(() => {
3264
- for (const cp of childProcesses) {
3535
+ if (cp.exitCode === null) {
3265
3536
  try {
3266
3537
  cp.kill("SIGKILL");
3267
3538
  } catch {
3268
3539
  }
3269
3540
  }
3270
- }, 3e3);
3271
- };
3272
- process.once("exit", cleanup);
3273
- process.once("SIGINT", cleanup);
3274
- process.once("SIGTERM", cleanup);
3541
+ }
3542
+ });
3275
3543
  console.log(
3276
3544
  chalk.dim(` Waiting for dev server on port ${port}...`)
3277
3545
  );
@@ -3302,9 +3570,9 @@ async function offerToStartDevServer(expectedPort) {
3302
3570
  );
3303
3571
  console.log("");
3304
3572
  try {
3305
- const pkgPath = join5(process.cwd(), "package.json");
3306
- if (existsSync5(pkgPath)) {
3307
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
3573
+ const pkgPath = join6(process.cwd(), "package.json");
3574
+ if (existsSync6(pkgPath)) {
3575
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
3308
3576
  if (pkg.engines?.node) {
3309
3577
  console.log(chalk.yellow(` This project requires Node.js ${pkg.engines.node}`));
3310
3578
  console.log(chalk.dim(` You are running Node.js ${process.version}`));