@mindstudio-ai/remy 0.1.308 → 0.1.310

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/index.js CHANGED
@@ -4459,6 +4459,7 @@ async function renderHtmlViaSidecar(opts) {
4459
4459
  html: opts.html,
4460
4460
  width: opts.width,
4461
4461
  height: opts.height,
4462
+ ...opts.autoHeight ? { autoHeight: true } : {},
4462
4463
  ...opts.transparent ? { transparent: true } : {},
4463
4464
  ...opts.scale != null ? { scale: opts.scale } : {}
4464
4465
  },
@@ -5624,7 +5625,7 @@ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (Op
5624
5625
  ## What Remy apps are NOT good for
5625
5626
 
5626
5627
  - Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
5627
- - Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
5628
+ - Fast-twitch multiplayer and live co-editing (shared cursors, 60fps sync) \u2014 client\u2192server is always a method invoke, so sub-100ms bidirectional interaction isn't a fit.
5628
5629
  </platform_brief>`;
5629
5630
  }
5630
5631
  var init_context = __esm({
@@ -6045,58 +6046,293 @@ var init_scrapeWebUrl = __esm({
6045
6046
  }
6046
6047
  });
6047
6048
 
6048
- // src/subagents/designExpert/tools/analyzeDesign.ts
6049
- var analyzeDesign_exports = {};
6050
- __export(analyzeDesign_exports, {
6049
+ // src/subagents/designExpert/tools/images/renderImage.ts
6050
+ var renderImage_exports = {};
6051
+ __export(renderImage_exports, {
6052
+ RENDER_ANALYZE_PROMPT: () => RENDER_ANALYZE_PROMPT,
6051
6053
  definition: () => definition3,
6052
6054
  execute: () => execute3
6053
6055
  });
6056
+ import { mkdir, unlink, writeFile } from "fs/promises";
6057
+ import { tmpdir } from "os";
6058
+ import { dirname, join, resolve as resolve2, sep } from "path";
6059
+ import { randomUUID } from "crypto";
6054
6060
  async function execute3(input, onLog, context) {
6055
- const url = input.url;
6056
- const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
6057
- const isImage = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
6058
- let image = url;
6059
- if (!isImage) {
6060
- const ss = await runMindstudioCliResult(
6061
- [
6062
- "screenshot-url",
6063
- "--url",
6064
- url,
6065
- "--mode",
6066
- "viewport",
6067
- "--width",
6068
- "1440",
6069
- "--delay",
6070
- "2000"
6071
- ],
6061
+ const html = typeof input.html === "string" ? input.html : "";
6062
+ const width = Math.round(Number(input.width));
6063
+ const height = Math.round(Number(input.height));
6064
+ if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
6065
+ return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
6066
+ }
6067
+ const release = await acquireBrowserLock();
6068
+ let rendered;
6069
+ try {
6070
+ onLog?.("Rendering document in the sandbox browser...");
6071
+ rendered = await renderHtmlViaSidecar({
6072
+ html,
6073
+ width,
6074
+ height,
6075
+ transparent: input.transparentBackground === true,
6076
+ scale: typeof input.scale === "number" ? input.scale : void 0
6077
+ });
6078
+ } catch (err) {
6079
+ return `Error: render failed: ${err?.message ?? err}`;
6080
+ } finally {
6081
+ release();
6082
+ }
6083
+ let bytes;
6084
+ try {
6085
+ const res = await fetch(rendered.url);
6086
+ if (!res.ok) {
6087
+ throw new Error(`fetch returned ${res.status}`);
6088
+ }
6089
+ bytes = Buffer.from(await res.arrayBuffer());
6090
+ } catch (err) {
6091
+ return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
6092
+ }
6093
+ let savedPath;
6094
+ if (typeof input.savePath === "string" && input.savePath) {
6095
+ const absolute = resolve2(PROJECT_ROOT, input.savePath);
6096
+ if (!absolute.startsWith(PROJECT_ROOT + sep)) {
6097
+ return "Error: savePath must resolve inside the project.";
6098
+ }
6099
+ await mkdir(dirname(absolute), { recursive: true });
6100
+ await writeFile(absolute, bytes);
6101
+ savedPath = input.savePath;
6102
+ }
6103
+ onLog?.("Hosting the capture...");
6104
+ const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
6105
+ let url = rendered.url;
6106
+ let temporary = true;
6107
+ try {
6108
+ await writeFile(tmpPath, bytes);
6109
+ const upload = await runMindstudioCliResult(["upload", tmpPath], {
6110
+ timeout: 6e4,
6111
+ onLog,
6112
+ caller: "designExpert"
6113
+ });
6114
+ const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
6115
+ if (match?.length) {
6116
+ url = match[match.length - 1];
6117
+ temporary = false;
6118
+ }
6119
+ } finally {
6120
+ await unlink(tmpPath).catch(() => {
6121
+ });
6122
+ }
6123
+ const analysis = await analyzeImage({
6124
+ prompt: RENDER_ANALYZE_PROMPT,
6125
+ image: url,
6126
+ onLog,
6127
+ model: resolveModel("imageAnalysis", context?.models, context?.model)
6128
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
6129
+ return JSON.stringify({
6130
+ images: [
6072
6131
  {
6073
- outputKey: "screenshotUrl",
6074
- timeout: 12e4,
6075
- onLog,
6076
- caller: "designExpert"
6132
+ url,
6133
+ ...temporary ? {
6134
+ note: "Durable hosting failed \u2014 this URL is dev-session scratch storage and may expire. Do not use it for app metadata; retry if a durable URL is needed."
6135
+ } : {},
6136
+ ...savedPath ? { savedPath } : {},
6137
+ analysis,
6138
+ width: rendered.width,
6139
+ height: rendered.height
6140
+ }
6141
+ ]
6142
+ });
6143
+ }
6144
+ var MIN_DIMENSION, MAX_DIMENSION, RENDER_ANALYZE_PROMPT, definition3;
6145
+ var init_renderImage = __esm({
6146
+ "src/subagents/designExpert/tools/images/renderImage.ts"() {
6147
+ "use strict";
6148
+ init_screenshot();
6149
+ init_browserLock();
6150
+ init_runMindstudioCli();
6151
+ init_analyzeImage();
6152
+ init_surfaces();
6153
+ init_projectRoot();
6154
+ MIN_DIMENSION = 16;
6155
+ MAX_DIMENSION = 4096;
6156
+ RENDER_ANALYZE_PROMPT = 'You are reviewing a browser-rendered graphic (composed from HTML/CSS by a designer) for fidelity. Report: whether the composition fills the full canvas or leaves unintended gaps at any edge, any clipped or overflowing text, whether custom webfonts appear to have loaded (distinctive letterforms vs generic fallback serif/sans), any misalignment or uneven spacing, any unintended scrollbars or default-styling artifacts, and \u2014 if the background is transparent \u2014 any fringing or stray opaque pixels at the edges. Then briefly describe the overall composition and how polished it looks. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Render Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
6157
+ definition3 = {
6158
+ name: "renderImage",
6159
+ description: "Render a self-contained HTML document in a real browser and capture it as a hosted PNG at exact pixel dimensions, with a fidelity review included. Deterministic \u2014 exact hex colors, real loaded webfonts, precise geometry \u2014 unlike generateImages, which is an image model. Use for token-exact graphics: Open Graph share cards, wordmarks, flat/geometric icon tiles, badges, any composition where letterforms and spacing carry the design. Compose with HTML/CSS (link webfonts from CDNs \u2014 the renderer waits for them to load); inline existing SVG markup when needed, but never hand-write new SVG path data.",
6160
+ inputSchema: {
6161
+ type: "object",
6162
+ properties: {
6163
+ html: {
6164
+ type: "string",
6165
+ description: "A complete, self-contained HTML document sized to fill the viewport (style html/body to the full dimensions with margin 0). External webfonts and images from CDNs are fine \u2014 loading is awaited before capture."
6166
+ },
6167
+ width: {
6168
+ type: "number",
6169
+ description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
6170
+ },
6171
+ height: {
6172
+ type: "number",
6173
+ description: "Viewport height in CSS pixels. Range: 16-4096."
6174
+ },
6175
+ scale: {
6176
+ type: "number",
6177
+ description: "Device scale factor, 1-3. Output pixels = css \xD7 scale. Use 2 for crisp icon masters (e.g. a 512\xD7512 document captured at 1024\xD71024)."
6178
+ },
6179
+ transparentBackground: {
6180
+ type: "boolean",
6181
+ description: "Capture with true alpha: leave the document background transparent (no background on html/body) and the PNG keeps it. No background-removal model involved."
6182
+ },
6183
+ savePath: {
6184
+ type: "string",
6185
+ description: "Optional project-relative path to also save the PNG into the app (e.g. 'dist/interfaces/web/public/og-image.png' so the deployed site self-hosts it)."
6186
+ }
6187
+ },
6188
+ required: ["html", "width", "height"]
6077
6189
  }
6190
+ };
6191
+ }
6192
+ });
6193
+
6194
+ // src/subagents/designExpert/tools/analyzeDesign.ts
6195
+ var analyzeDesign_exports = {};
6196
+ __export(analyzeDesign_exports, {
6197
+ definition: () => definition4,
6198
+ execute: () => execute4
6199
+ });
6200
+ import { readFile as readFile2 } from "fs/promises";
6201
+ import { extname as extname2, join as join2 } from "path";
6202
+ function splitFrontmatter(raw) {
6203
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
6204
+ if (!match) {
6205
+ return { html: raw };
6206
+ }
6207
+ const fields = {};
6208
+ for (const line of match[1].split("\n")) {
6209
+ const idx = line.indexOf(":");
6210
+ if (idx > 0) {
6211
+ fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
6212
+ }
6213
+ }
6214
+ return {
6215
+ name: fields.name,
6216
+ description: fields.description,
6217
+ html: raw.slice(match[0].length)
6218
+ };
6219
+ }
6220
+ async function execute4(input, onLog, context) {
6221
+ const url = String(input.url ?? "").trim();
6222
+ if (!url) {
6223
+ return "Error: url is required.";
6224
+ }
6225
+ const customPrompt = typeof input.prompt === "string" && input.prompt.trim() ? input.prompt : void 0;
6226
+ if (!isFetchableUrl(url)) {
6227
+ if (HTML_EXTS.has(extname2(url).toLowerCase())) {
6228
+ return analyzeLocalHtml(url, customPrompt, onLog, context);
6229
+ }
6230
+ return analyze(
6231
+ url,
6232
+ customPrompt ?? DESIGN_REFERENCE_PROMPT,
6233
+ onLog,
6234
+ context
6235
+ );
6236
+ }
6237
+ if (IMAGE_EXT_RE.test(url)) {
6238
+ return analyze(
6239
+ url,
6240
+ customPrompt ?? DESIGN_REFERENCE_PROMPT,
6241
+ onLog,
6242
+ context
6078
6243
  );
6079
- if (!ss.ok) {
6080
- return `Could not screenshot ${url}: ${ss.value}`;
6244
+ }
6245
+ const ss = await runMindstudioCliResult(
6246
+ [
6247
+ "screenshot-url",
6248
+ "--url",
6249
+ url,
6250
+ "--mode",
6251
+ "viewport",
6252
+ "--width",
6253
+ "1440",
6254
+ "--delay",
6255
+ "2000"
6256
+ ],
6257
+ {
6258
+ outputKey: "screenshotUrl",
6259
+ timeout: 12e4,
6260
+ onLog,
6261
+ caller: "designExpert"
6081
6262
  }
6082
- image = ss.value;
6263
+ );
6264
+ if (!ss.ok) {
6265
+ return `Could not screenshot ${url}: ${ss.value}`;
6083
6266
  }
6267
+ return analyze(
6268
+ ss.value,
6269
+ customPrompt ?? DESIGN_REFERENCE_PROMPT,
6270
+ onLog,
6271
+ context
6272
+ );
6273
+ }
6274
+ async function analyze(image, prompt, onLog, context, extra) {
6084
6275
  const analyzed = await analyzeImage({
6085
- prompt: analysisPrompt,
6276
+ prompt,
6086
6277
  image,
6087
6278
  apiConfig: context?.apiConfig,
6088
6279
  onLog,
6089
6280
  model: resolveModel("imageAnalysis", context?.models, context?.model)
6090
6281
  });
6091
- return JSON.stringify({ url: analyzed.url, analysis: analyzed.analysis });
6282
+ return JSON.stringify({
6283
+ url: analyzed.url,
6284
+ analysis: analyzed.analysis,
6285
+ ...extra
6286
+ });
6092
6287
  }
6093
- var DESIGN_REFERENCE_PROMPT, definition3;
6288
+ async function analyzeLocalHtml(path14, customPrompt, onLog, context) {
6289
+ let raw;
6290
+ try {
6291
+ raw = await readFile2(join2(PROJECT_ROOT, path14), "utf8");
6292
+ } catch {
6293
+ return `No file at "${path14}". Paths resolve from the project root; list the directory to check the name.`;
6294
+ }
6295
+ const { name, description, html } = splitFrontmatter(raw);
6296
+ if (!html.trim()) {
6297
+ return `"${path14}" has no HTML to render.`;
6298
+ }
6299
+ onLog?.(`Rendering ${path14} in the sandbox browser...`);
6300
+ const release = await acquireBrowserLock();
6301
+ let rendered;
6302
+ try {
6303
+ rendered = await renderHtmlViaSidecar({
6304
+ html,
6305
+ width: HTML_RENDER_WIDTH,
6306
+ height: HTML_RENDER_START_HEIGHT,
6307
+ autoHeight: true
6308
+ });
6309
+ } catch (err) {
6310
+ return `Could not render ${path14}: ${err?.message ?? err}`;
6311
+ } finally {
6312
+ release();
6313
+ }
6314
+ const basePrompt = customPrompt ?? RENDER_ANALYZE_PROMPT;
6315
+ const subject = [name, description].filter(Boolean).join(" \u2014 ");
6316
+ const prompt = subject ? `The document rendered below is described by its author as: ${subject}
6317
+
6318
+ ${basePrompt}` : basePrompt;
6319
+ return analyze(rendered.url, prompt, onLog, context, {
6320
+ renderedWidth: rendered.width,
6321
+ renderedHeight: rendered.height
6322
+ });
6323
+ }
6324
+ var DESIGN_REFERENCE_PROMPT, definition4, IMAGE_EXT_RE, HTML_EXTS, HTML_RENDER_WIDTH, HTML_RENDER_START_HEIGHT;
6094
6325
  var init_analyzeDesign = __esm({
6095
6326
  "src/subagents/designExpert/tools/analyzeDesign.ts"() {
6096
6327
  "use strict";
6097
6328
  init_runMindstudioCli();
6098
6329
  init_analyzeImage();
6099
6330
  init_surfaces();
6331
+ init_uploadImage();
6332
+ init_screenshot();
6333
+ init_browserLock();
6334
+ init_projectRoot();
6335
+ init_renderImage();
6100
6336
  DESIGN_REFERENCE_PROMPT = `
6101
6337
  You are analyzing a screenshot of a real website or app for a designer's personal technique/inspiration reference notes.
6102
6338
 
@@ -6120,15 +6356,15 @@ Identify the specific design moves that make this page interesting and unique, d
6120
6356
 
6121
6357
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
6122
6358
  `;
6123
- definition3 = {
6359
+ definition4 = {
6124
6360
  name: "analyzeDesign",
6125
- description: "Analyze the visual design of a website, an image URL, or an image file on disk. Websites are automatically screenshotted first. Provides static image analysis only, will not capture animations or video. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
6361
+ description: "Analyze the visual design of a website, an image (URL or file on disk), or an HTML document on disk. Websites are screenshotted first; an HTML file is rendered in a real browser first and the render is analyzed, which is how you look at a wireframe you authored. Provides static image analysis only, will not capture animations or video. With no prompt, a website or image gets a full design reference analysis (mood, color, typography, layout, distinctiveness) and a rendered HTML document gets a fidelity review (clipping, overflow, webfont loading, alignment). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
6126
6362
  inputSchema: {
6127
6363
  type: "object",
6128
6364
  properties: {
6129
6365
  url: {
6130
6366
  type: "string",
6131
- description: "What to analyze: a website URL (will be screenshotted), an image URL, or the path of an image file on disk (e.g. a reference the user uploaded, under src/.user-uploads/). Local files are hosted automatically and their URL comes back in the result."
6367
+ description: "What to analyze: a website URL (will be screenshotted), an image URL, the path of an image file on disk (e.g. a reference the user uploaded, under src/.user-uploads/), or the path of an HTML file on disk (rendered in a browser, then analyzed). Local files are hosted automatically and their URL comes back in the result."
6132
6368
  },
6133
6369
  prompt: {
6134
6370
  type: "string",
@@ -6138,16 +6374,20 @@ Respond only with your analysis as Markdown and absolutely no other text. Do not
6138
6374
  required: ["url"]
6139
6375
  }
6140
6376
  };
6377
+ IMAGE_EXT_RE = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i;
6378
+ HTML_EXTS = /* @__PURE__ */ new Set([".html", ".htm"]);
6379
+ HTML_RENDER_WIDTH = 1280;
6380
+ HTML_RENDER_START_HEIGHT = 1400;
6141
6381
  }
6142
6382
  });
6143
6383
 
6144
6384
  // src/subagents/designExpert/tools/analyzeImage.ts
6145
6385
  var analyzeImage_exports = {};
6146
6386
  __export(analyzeImage_exports, {
6147
- definition: () => definition4,
6148
- execute: () => execute4
6387
+ definition: () => definition5,
6388
+ execute: () => execute5
6149
6389
  });
6150
- async function execute4(input, onLog, context) {
6390
+ async function execute5(input, onLog, context) {
6151
6391
  const prompt = buildScreenshotAnalysisPrompt({
6152
6392
  prompt: input.prompt
6153
6393
  });
@@ -6160,14 +6400,14 @@ async function execute4(input, onLog, context) {
6160
6400
  });
6161
6401
  return JSON.stringify({ url, analysis });
6162
6402
  }
6163
- var definition4;
6403
+ var definition5;
6164
6404
  var init_analyzeImage2 = __esm({
6165
6405
  "src/subagents/designExpert/tools/analyzeImage.ts"() {
6166
6406
  "use strict";
6167
6407
  init_analyzeImage();
6168
6408
  init_screenshot();
6169
6409
  init_surfaces();
6170
- definition4 = {
6410
+ definition5 = {
6171
6411
  name: "analyzeImage",
6172
6412
  description: "Analyze an image using a vision model. Provides static image analysis only, will not capture animations or video. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
6173
6413
  inputSchema: {
@@ -6435,10 +6675,10 @@ var init_imageGenerator = __esm({
6435
6675
  // src/subagents/designExpert/tools/images/generateImages.ts
6436
6676
  var generateImages_exports = {};
6437
6677
  __export(generateImages_exports, {
6438
- definition: () => definition5,
6439
- execute: () => execute5
6678
+ definition: () => definition6,
6679
+ execute: () => execute6
6440
6680
  });
6441
- async function execute5(input, onLog, context) {
6681
+ async function execute6(input, onLog, context) {
6442
6682
  return generateImageAssets({
6443
6683
  prompts: input.prompts,
6444
6684
  width: input.width,
@@ -6465,13 +6705,13 @@ async function execute5(input, onLog, context) {
6465
6705
  )
6466
6706
  });
6467
6707
  }
6468
- var definition5;
6708
+ var definition6;
6469
6709
  var init_generateImages = __esm({
6470
6710
  "src/subagents/designExpert/tools/images/generateImages.ts"() {
6471
6711
  "use strict";
6472
6712
  init_imageGenerator();
6473
6713
  init_surfaces();
6474
- definition5 = {
6714
+ definition6 = {
6475
6715
  name: "generateImages",
6476
6716
  description: "Generate images. Returns CDN URLs with a quality analysis for each image. Produces high-quality results for everything from photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
6477
6717
  inputSchema: {
@@ -6510,10 +6750,10 @@ var init_generateImages = __esm({
6510
6750
  // src/subagents/designExpert/tools/images/editImages.ts
6511
6751
  var editImages_exports = {};
6512
6752
  __export(editImages_exports, {
6513
- definition: () => definition6,
6514
- execute: () => execute6
6753
+ definition: () => definition7,
6754
+ execute: () => execute7
6515
6755
  });
6516
- async function execute6(input, onLog, context) {
6756
+ async function execute7(input, onLog, context) {
6517
6757
  return generateImageAssets({
6518
6758
  prompts: input.prompts,
6519
6759
  sourceImages: input.sourceImages,
@@ -6540,13 +6780,13 @@ async function execute6(input, onLog, context) {
6540
6780
  )
6541
6781
  });
6542
6782
  }
6543
- var definition6;
6783
+ var definition7;
6544
6784
  var init_editImages = __esm({
6545
6785
  "src/subagents/designExpert/tools/images/editImages.ts"() {
6546
6786
  "use strict";
6547
6787
  init_imageGenerator();
6548
6788
  init_surfaces();
6549
- definition6 = {
6789
+ definition7 = {
6550
6790
  name: "editImages",
6551
6791
  description: "Edit or transform existing images. Provide one or more source image URLs as reference and a prompt describing the desired edit. Use for compositing, style transfer, subject transformation, blending multiple references, or incorporating one or more references into something new. Returns CDN URLs with analysis.",
6552
6792
  inputSchema: {
@@ -6585,150 +6825,6 @@ var init_editImages = __esm({
6585
6825
  }
6586
6826
  });
6587
6827
 
6588
- // src/subagents/designExpert/tools/images/renderImage.ts
6589
- var renderImage_exports = {};
6590
- __export(renderImage_exports, {
6591
- definition: () => definition7,
6592
- execute: () => execute7
6593
- });
6594
- import { mkdir, unlink, writeFile } from "fs/promises";
6595
- import { tmpdir } from "os";
6596
- import { dirname, join, resolve as resolve2, sep } from "path";
6597
- import { randomUUID } from "crypto";
6598
- async function execute7(input, onLog, context) {
6599
- const html = typeof input.html === "string" ? input.html : "";
6600
- const width = Math.round(Number(input.width));
6601
- const height = Math.round(Number(input.height));
6602
- if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
6603
- return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
6604
- }
6605
- const release = await acquireBrowserLock();
6606
- let rendered;
6607
- try {
6608
- onLog?.("Rendering document in the sandbox browser...");
6609
- rendered = await renderHtmlViaSidecar({
6610
- html,
6611
- width,
6612
- height,
6613
- transparent: input.transparentBackground === true,
6614
- scale: typeof input.scale === "number" ? input.scale : void 0
6615
- });
6616
- } catch (err) {
6617
- return `Error: render failed: ${err?.message ?? err}`;
6618
- } finally {
6619
- release();
6620
- }
6621
- let bytes;
6622
- try {
6623
- const res = await fetch(rendered.url);
6624
- if (!res.ok) {
6625
- throw new Error(`fetch returned ${res.status}`);
6626
- }
6627
- bytes = Buffer.from(await res.arrayBuffer());
6628
- } catch (err) {
6629
- return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
6630
- }
6631
- let savedPath;
6632
- if (typeof input.savePath === "string" && input.savePath) {
6633
- const absolute = resolve2(PROJECT_ROOT, input.savePath);
6634
- if (!absolute.startsWith(PROJECT_ROOT + sep)) {
6635
- return "Error: savePath must resolve inside the project.";
6636
- }
6637
- await mkdir(dirname(absolute), { recursive: true });
6638
- await writeFile(absolute, bytes);
6639
- savedPath = input.savePath;
6640
- }
6641
- onLog?.("Hosting the capture...");
6642
- const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
6643
- let url = rendered.url;
6644
- let temporary = true;
6645
- try {
6646
- await writeFile(tmpPath, bytes);
6647
- const upload = await runMindstudioCliResult(["upload", tmpPath], {
6648
- timeout: 6e4,
6649
- onLog,
6650
- caller: "designExpert"
6651
- });
6652
- const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
6653
- if (match?.length) {
6654
- url = match[match.length - 1];
6655
- temporary = false;
6656
- }
6657
- } finally {
6658
- await unlink(tmpPath).catch(() => {
6659
- });
6660
- }
6661
- const analysis = await analyzeImage({
6662
- prompt: RENDER_ANALYZE_PROMPT,
6663
- image: url,
6664
- onLog,
6665
- model: resolveModel("imageAnalysis", context?.models, context?.model)
6666
- }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
6667
- return JSON.stringify({
6668
- images: [
6669
- {
6670
- url,
6671
- ...temporary ? {
6672
- note: "Durable hosting failed \u2014 this URL is dev-session scratch storage and may expire. Do not use it for app metadata; retry if a durable URL is needed."
6673
- } : {},
6674
- ...savedPath ? { savedPath } : {},
6675
- analysis,
6676
- width: rendered.width,
6677
- height: rendered.height
6678
- }
6679
- ]
6680
- });
6681
- }
6682
- var MIN_DIMENSION, MAX_DIMENSION, RENDER_ANALYZE_PROMPT, definition7;
6683
- var init_renderImage = __esm({
6684
- "src/subagents/designExpert/tools/images/renderImage.ts"() {
6685
- "use strict";
6686
- init_screenshot();
6687
- init_browserLock();
6688
- init_runMindstudioCli();
6689
- init_analyzeImage();
6690
- init_surfaces();
6691
- init_projectRoot();
6692
- MIN_DIMENSION = 16;
6693
- MAX_DIMENSION = 4096;
6694
- RENDER_ANALYZE_PROMPT = 'You are reviewing a browser-rendered graphic (composed from HTML/CSS by a designer) for fidelity. Report: whether the composition fills the full canvas or leaves unintended gaps at any edge, any clipped or overflowing text, whether custom webfonts appear to have loaded (distinctive letterforms vs generic fallback serif/sans), any misalignment or uneven spacing, any unintended scrollbars or default-styling artifacts, and \u2014 if the background is transparent \u2014 any fringing or stray opaque pixels at the edges. Then briefly describe the overall composition and how polished it looks. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Render Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
6695
- definition7 = {
6696
- name: "renderImage",
6697
- description: "Render a self-contained HTML document in a real browser and capture it as a hosted PNG at exact pixel dimensions, with a fidelity review included. Deterministic \u2014 exact hex colors, real loaded webfonts, precise geometry \u2014 unlike generateImages, which is an image model. Use for token-exact graphics: Open Graph share cards, wordmarks, flat/geometric icon tiles, badges, any composition where letterforms and spacing carry the design. Compose with HTML/CSS (link webfonts from CDNs \u2014 the renderer waits for them to load); inline existing SVG markup when needed, but never hand-write new SVG path data.",
6698
- inputSchema: {
6699
- type: "object",
6700
- properties: {
6701
- html: {
6702
- type: "string",
6703
- description: "A complete, self-contained HTML document sized to fill the viewport (style html/body to the full dimensions with margin 0). External webfonts and images from CDNs are fine \u2014 loading is awaited before capture."
6704
- },
6705
- width: {
6706
- type: "number",
6707
- description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
6708
- },
6709
- height: {
6710
- type: "number",
6711
- description: "Viewport height in CSS pixels. Range: 16-4096."
6712
- },
6713
- scale: {
6714
- type: "number",
6715
- description: "Device scale factor, 1-3. Output pixels = css \xD7 scale. Use 2 for crisp icon masters (e.g. a 512\xD7512 document captured at 1024\xD71024)."
6716
- },
6717
- transparentBackground: {
6718
- type: "boolean",
6719
- description: "Capture with true alpha: leave the document background transparent (no background on html/body) and the PNG keeps it. No background-removal model involved."
6720
- },
6721
- savePath: {
6722
- type: "string",
6723
- description: "Optional project-relative path to also save the PNG into the app (e.g. 'dist/interfaces/web/public/og-image.png' so the deployed site self-hosts it)."
6724
- }
6725
- },
6726
- required: ["html", "width", "height"]
6727
- }
6728
- };
6729
- }
6730
- });
6731
-
6732
6828
  // src/subagents/copyEditor/tools.ts
6733
6829
  var COPY_EDITOR_TOOLS;
6734
6830
  var init_tools3 = __esm({
@@ -6911,7 +7007,7 @@ __export(createWireframe_exports, {
6911
7007
  execute: () => execute10
6912
7008
  });
6913
7009
  import { mkdir as mkdir2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
6914
- import { join as join2 } from "path";
7010
+ import { join as join3 } from "path";
6915
7011
  function singleLine(value) {
6916
7012
  return value.replace(/\s+/g, " ").trim();
6917
7013
  }
@@ -7000,12 +7096,12 @@ async function execute10(input, onLog, context) {
7000
7096
  html.trim(),
7001
7097
  ""
7002
7098
  ].join("\n");
7003
- await mkdir2(join2(PROJECT_ROOT, WIREFRAMES_DIR), { recursive: true });
7004
- const existed = await stat2(join2(PROJECT_ROOT, relPath)).then(
7099
+ await mkdir2(join3(PROJECT_ROOT, WIREFRAMES_DIR), { recursive: true });
7100
+ const existed = await stat2(join3(PROJECT_ROOT, relPath)).then(
7005
7101
  () => true,
7006
7102
  () => false
7007
7103
  );
7008
- await writeFile2(join2(PROJECT_ROOT, relPath), content, "utf8");
7104
+ await writeFile2(join3(PROJECT_ROOT, relPath), content, "utf8");
7009
7105
  onLog?.(`Wrote ${relPath}, mirroring for preview...`);
7010
7106
  const mirror = await uploadMirror(context, slug, content);
7011
7107
  if (!mirror.ok) {
@@ -7441,7 +7537,7 @@ The guidance about specifying layouts in prose, writing implementation notes, an
7441
7537
 
7442
7538
  // src/subagents/designExpert/validateWireframeRefs.ts
7443
7539
  import { stat as stat3 } from "fs/promises";
7444
- import { join as join3 } from "path";
7540
+ import { join as join4 } from "path";
7445
7541
  async function validateWireframeRefs(text) {
7446
7542
  const refs = [...new Set(text.match(WIREFRAME_REF_RE) ?? [])];
7447
7543
  if (refs.length === 0) {
@@ -7449,7 +7545,7 @@ async function validateWireframeRefs(text) {
7449
7545
  }
7450
7546
  const missing = [];
7451
7547
  for (const ref of refs) {
7452
- const exists = await stat3(join3(PROJECT_ROOT, ref)).then(
7548
+ const exists = await stat3(join4(PROJECT_ROOT, ref)).then(
7453
7549
  (s) => s.isFile(),
7454
7550
  () => false
7455
7551
  );
@@ -10267,7 +10363,7 @@ var init_config = __esm({
10267
10363
  // src/headless/attachments.ts
10268
10364
  import { mkdirSync, existsSync } from "fs";
10269
10365
  import { writeFile as writeFile3 } from "fs/promises";
10270
- import { basename as basename2, join as join4, extname as extname2 } from "path";
10366
+ import { basename as basename2, join as join5, extname as extname3 } from "path";
10271
10367
  function filenameFromUrl(url) {
10272
10368
  try {
10273
10369
  const pathname = new URL(url).pathname;
@@ -10278,11 +10374,11 @@ function filenameFromUrl(url) {
10278
10374
  }
10279
10375
  }
10280
10376
  function resolveUniqueFilename(name, claimed) {
10281
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join4(UPLOADS_DIR, candidate));
10377
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join5(UPLOADS_DIR, candidate));
10282
10378
  if (isFree(name)) {
10283
10379
  return name;
10284
10380
  }
10285
- const ext = extname2(name);
10381
+ const ext = extname3(name);
10286
10382
  const base = name.slice(0, name.length - ext.length);
10287
10383
  let counter = 1;
10288
10384
  while (!isFree(`${base}-${counter}${ext}`)) {
@@ -10292,7 +10388,7 @@ function resolveUniqueFilename(name, claimed) {
10292
10388
  }
10293
10389
  function isImageAttachment(att) {
10294
10390
  const name = att.filename || filenameFromUrl(att.url);
10295
- return IMAGE_EXTENSIONS.has(extname2(name).toLowerCase());
10391
+ return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
10296
10392
  }
10297
10393
  async function persistAttachments(attachments) {
10298
10394
  const nonVoice = attachments.filter((a) => !a.isVoice);
@@ -10312,7 +10408,7 @@ async function persistAttachments(attachments) {
10312
10408
  const results = await Promise.allSettled(
10313
10409
  nonVoice.map(async (att, i) => {
10314
10410
  const name = names[i];
10315
- const localPath = join4(UPLOADS_DIR, name);
10411
+ const localPath = join5(UPLOADS_DIR, name);
10316
10412
  const res = await fetch(att.url, {
10317
10413
  signal: AbortSignal.timeout(3e4)
10318
10414
  });