openmagic 0.32.0 → 0.33.1

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
@@ -82,9 +82,12 @@ import {
82
82
  readdirSync,
83
83
  copyFileSync,
84
84
  mkdirSync as mkdirSync2,
85
- realpathSync
85
+ realpathSync,
86
+ rmSync
86
87
  } from "fs";
87
88
  import { join as join2, resolve, relative, dirname, extname } from "path";
89
+ import { tmpdir } from "os";
90
+ import { createHash } from "crypto";
88
91
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
89
92
  "node_modules",
90
93
  ".git",
@@ -145,6 +148,25 @@ function readFileSafe(filePath, roots) {
145
148
  return { error: `Failed to read file: ${e.message}` };
146
149
  }
147
150
  }
151
+ var BACKUP_DIR = join2(tmpdir(), "openmagic-backups");
152
+ var backupMap = /* @__PURE__ */ new Map();
153
+ function getBackupPath(filePath) {
154
+ const hash = createHash("md5").update(resolve(filePath)).digest("hex").slice(0, 12);
155
+ const name = filePath.split(/[/\\]/).pop() || "file";
156
+ return join2(BACKUP_DIR, `${hash}_${name}`);
157
+ }
158
+ function getBackupForFile(filePath) {
159
+ return backupMap.get(resolve(filePath));
160
+ }
161
+ function cleanupBackups() {
162
+ try {
163
+ if (existsSync2(BACKUP_DIR)) {
164
+ rmSync(BACKUP_DIR, { recursive: true, force: true });
165
+ }
166
+ } catch {
167
+ }
168
+ backupMap.clear();
169
+ }
148
170
  function writeFileSafe(filePath, content, roots) {
149
171
  if (!isPathSafe(filePath, roots)) {
150
172
  return { ok: false, error: "Path is outside allowed roots" };
@@ -152,8 +174,10 @@ function writeFileSafe(filePath, content, roots) {
152
174
  try {
153
175
  let backupPath;
154
176
  if (existsSync2(filePath)) {
155
- backupPath = filePath + ".openmagic-backup";
177
+ if (!existsSync2(BACKUP_DIR)) mkdirSync2(BACKUP_DIR, { recursive: true });
178
+ backupPath = getBackupPath(filePath);
156
179
  copyFileSync(filePath, backupPath);
180
+ backupMap.set(resolve(filePath), backupPath);
157
181
  }
158
182
  const dir = dirname(filePath);
159
183
  if (!existsSync2(dir)) {
@@ -204,6 +228,79 @@ function listFiles(rootPath, roots, maxDepth = 4) {
204
228
  walk(rootPath, 0);
205
229
  return entries;
206
230
  }
231
+ var GREP_EXTENSIONS = /* @__PURE__ */ new Set([
232
+ ".js",
233
+ ".jsx",
234
+ ".ts",
235
+ ".tsx",
236
+ ".mjs",
237
+ ".cjs",
238
+ ".vue",
239
+ ".svelte",
240
+ ".astro",
241
+ ".html",
242
+ ".htm",
243
+ ".css",
244
+ ".scss",
245
+ ".less",
246
+ ".json",
247
+ ".md",
248
+ ".yaml",
249
+ ".yml",
250
+ ".php",
251
+ ".py",
252
+ ".rb"
253
+ ]);
254
+ function grepFiles(pattern, searchRoot, roots, maxResults = 30) {
255
+ if (!isPathSafe(searchRoot, roots)) return [];
256
+ const results = [];
257
+ const lowerPattern = pattern.toLowerCase();
258
+ function walk(dir, depth) {
259
+ if (depth > 5 || results.length >= maxResults) return;
260
+ let items;
261
+ try {
262
+ items = readdirSync(dir);
263
+ } catch {
264
+ return;
265
+ }
266
+ for (const item of items) {
267
+ if (results.length >= maxResults) return;
268
+ if (IGNORED_DIRS.has(item) || item.startsWith(".") && item !== ".env.example") continue;
269
+ const fullPath = join2(dir, item);
270
+ let stat;
271
+ try {
272
+ stat = lstatSync(fullPath);
273
+ } catch {
274
+ continue;
275
+ }
276
+ if (stat.isSymbolicLink()) continue;
277
+ if (stat.isDirectory()) {
278
+ walk(fullPath, depth + 1);
279
+ } else if (stat.isFile()) {
280
+ const ext = extname(item).toLowerCase();
281
+ if (!GREP_EXTENSIONS.has(ext)) continue;
282
+ try {
283
+ const content = readFileSync2(fullPath, "utf-8");
284
+ const lines = content.split("\n");
285
+ let fileMatches = 0;
286
+ for (let i = 0; i < lines.length && fileMatches < 5; i++) {
287
+ if (lines[i].toLowerCase().includes(lowerPattern)) {
288
+ results.push({
289
+ file: relative(searchRoot, fullPath),
290
+ lineNum: i + 1,
291
+ line: lines[i].trim().slice(0, 200)
292
+ });
293
+ fileMatches++;
294
+ }
295
+ }
296
+ } catch {
297
+ }
298
+ }
299
+ }
300
+ }
301
+ walk(searchRoot, 0);
302
+ return results;
303
+ }
207
304
  function getProjectTree(roots) {
208
305
  const lines = [];
209
306
  for (const root of roots) {
@@ -909,6 +1006,7 @@ You MUST respond with valid JSON in this exact format:
909
1006
  4. Include 3-5 lines of surrounding context in the search field to ensure uniqueness
910
1007
  5. Keep modifications minimal \u2014 change only what's needed. Do NOT rewrite entire files.
911
1008
  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"}
1009
+ 7. To search for a pattern across the codebase, return: {"modifications":[],"explanation":"SEARCH_FILES: \\"pattern\\" in optional/path/"}
912
1010
  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
913
1011
  8. Use the selected element's cssSelector, className, parentContainerStyles, and siblings to understand the full layout context
914
1012
  9. Use the page URL route and componentHint to identify the correct source file to modify
@@ -949,6 +1047,12 @@ function buildContextParts(context) {
949
1047
  if (el.reactProps) {
950
1048
  elementData.reactProps = el.reactProps;
951
1049
  }
1050
+ if (el.childrenLayout?.length) {
1051
+ elementData.childrenLayout = el.childrenLayout;
1052
+ }
1053
+ if (el.resolvedClasses?.length) {
1054
+ elementData.resolvedClasses = el.resolvedClasses;
1055
+ }
952
1056
  parts.selectedElement = JSON.stringify(elementData, null, 2);
953
1057
  }
954
1058
  if (context.files?.length) {
@@ -959,6 +1063,12 @@ function buildContextParts(context) {
959
1063
  if (context.pageTitle) parts.pageTitle = context.pageTitle;
960
1064
  if (context.networkLogs) parts.networkLogs = context.networkLogs.map((l) => `${l.method} ${l.url} \u2192 ${l.status || "pending"}`).join("\n");
961
1065
  if (context.consoleLogs) parts.consoleLogs = context.consoleLogs.map((l) => `[${l.level}] ${l.args.join(" ")}`).join("\n");
1066
+ if (context.searchResults?.length) {
1067
+ parts.searchResults = context.searchResults.map(
1068
+ (s) => `Search: "${s.query}"
1069
+ ${s.matches.map((m) => ` ${m.file}:${m.lineNum}: ${m.line}`).join("\n")}`
1070
+ ).join("\n\n");
1071
+ }
962
1072
  return parts;
963
1073
  }
964
1074
  function buildUserMessage(userPrompt, context) {
@@ -1002,6 +1112,12 @@ ${context.networkLogs}
1002
1112
  parts.push(`## Console Output
1003
1113
  \`\`\`
1004
1114
  ${context.consoleLogs}
1115
+ \`\`\``);
1116
+ }
1117
+ if (context.searchResults) {
1118
+ parts.push(`## Search Results
1119
+ \`\`\`
1120
+ ${context.searchResults}
1005
1121
  \`\`\``);
1006
1122
  }
1007
1123
  parts.push(`## User Request
@@ -1614,6 +1730,30 @@ async function handleMessage(ws, msg, state, roots) {
1614
1730
  }
1615
1731
  break;
1616
1732
  }
1733
+ case "fs.undo": {
1734
+ const payload = msg.payload;
1735
+ if (!payload?.path) {
1736
+ sendError(ws, "invalid_payload", "Missing path", msg.id);
1737
+ break;
1738
+ }
1739
+ const backupPath = getBackupForFile(payload.path);
1740
+ if (!backupPath) {
1741
+ sendError(ws, "fs_error", "No backup found", msg.id);
1742
+ break;
1743
+ }
1744
+ try {
1745
+ const backupContent = readFileSync3(backupPath, "utf-8");
1746
+ const writeResult = writeFileSafe(payload.path, backupContent, roots);
1747
+ if (!writeResult.ok) {
1748
+ sendError(ws, "fs_error", writeResult.error || "Undo failed", msg.id);
1749
+ break;
1750
+ }
1751
+ send(ws, { id: msg.id, type: "fs.undone", payload: { path: payload.path, ok: true } });
1752
+ } catch (e) {
1753
+ sendError(ws, "fs_error", `Backup read failed: ${e.message}`, msg.id);
1754
+ }
1755
+ break;
1756
+ }
1617
1757
  case "fs.list": {
1618
1758
  const payload = msg.payload;
1619
1759
  const root = payload?.root || roots[0];
@@ -1694,6 +1834,17 @@ async function handleMessage(ws, msg, state, roots) {
1694
1834
  }
1695
1835
  break;
1696
1836
  }
1837
+ case "fs.grep": {
1838
+ const payload = msg.payload;
1839
+ if (!payload?.pattern) {
1840
+ sendError(ws, "invalid_payload", "Missing pattern", msg.id);
1841
+ break;
1842
+ }
1843
+ const searchRoot = payload.path ? join3(roots[0] || process.cwd(), payload.path) : roots[0] || process.cwd();
1844
+ const results = grepFiles(payload.pattern, searchRoot, roots);
1845
+ send(ws, { id: msg.id, type: "fs.grep.result", payload: { results } });
1846
+ break;
1847
+ }
1697
1848
  case "debug.logs": {
1698
1849
  send(ws, {
1699
1850
  id: msg.id,
@@ -2289,6 +2440,7 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
2289
2440
  const shutdown = () => {
2290
2441
  console.log("");
2291
2442
  console.log(chalk.dim(" Shutting down OpenMagic..."));
2443
+ cleanupBackups();
2292
2444
  proxyServer.close();
2293
2445
  process.exit(0);
2294
2446
  };