@mindstudio-ai/remy 0.1.275 → 0.1.277

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.
Files changed (28) hide show
  1. package/dist/headless.js +344 -79
  2. package/dist/index.js +382 -83
  3. package/dist/prompt/skills/agentInterfaces.md +13 -39
  4. package/dist/prompt/skills/auth.md +1 -7
  5. package/dist/prompt/skills/dataSources.md +20 -66
  6. package/dist/prompt/skills/files.md +17 -47
  7. package/dist/prompt/skills/inboundEmail.md +11 -32
  8. package/dist/prompt/skills/mcpInterfaces.md +21 -63
  9. package/dist/prompt/skills/restApi.md +7 -21
  10. package/dist/prompt/skills/scenarios.md +3 -7
  11. package/dist/prompt/skills/scheduledJobs.md +3 -8
  12. package/dist/prompt/skills/voiceInterfaces.md +117 -366
  13. package/dist/prompt/skills/webhooks.md +10 -31
  14. package/dist/prompt/static/authoring.md +1 -2
  15. package/dist/prompt/static/team.md +1 -1
  16. package/dist/subagents/browserAutomation/prompt.md +6 -22
  17. package/dist/subagents/codeSanityCheck/prompt.md +1 -2
  18. package/dist/subagents/designExpert/prompt.md +0 -3
  19. package/dist/subagents/designExpert/prompts/instructions.md +1 -0
  20. package/dist/subagents/designExpert/prompts/ui-patterns.md +7 -21
  21. package/dist/subagents/designExpert/skills/authExperience.md +60 -0
  22. package/dist/subagents/designExpert/skills/chatExperience.md +59 -0
  23. package/dist/subagents/designExpert/skills/dataViz.md +82 -0
  24. package/dist/subagents/designExpert/{prompts → skills}/images.md +17 -1
  25. package/dist/subagents/designExpert/skills/voiceExperience.md +103 -0
  26. package/dist/subagents/designExpert/tools/images/enhance-image-prompt.md +7 -3
  27. package/dist/subagents/productVision/prompt.md +2 -0
  28. package/package.json +1 -1
package/dist/headless.js CHANGED
@@ -289,7 +289,7 @@ function isRetryableError(error) {
289
289
  /Unable to download/i.test(error);
290
290
  }
291
291
  function sleep(ms) {
292
- return new Promise((resolve3) => setTimeout(resolve3, ms));
292
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
293
293
  }
294
294
  async function* streamChatWithRetry(params, options) {
295
295
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -433,7 +433,7 @@ var MODEL_SURFACES = {
433
433
  userPickable: true
434
434
  },
435
435
  imageGeneration: {
436
- default: "seedream-4.5",
436
+ default: "gpt-image-2",
437
437
  label: "Image Generation",
438
438
  description: "Creates images for your product \u2014 icons, illustrations, photos, and any other visual assets.",
439
439
  modelType: "image_generation",
@@ -681,10 +681,9 @@ The user has approved your implementation plan in .remy-plan.md. You may referen
681
681
  }
682
682
  }
683
683
 
684
- // src/prompt/skills/catalog.ts
684
+ // src/skillCatalog.ts
685
685
  import fs5 from "fs";
686
686
  import path3 from "path";
687
- var SKILLS_DIR = assetPath("prompt", "skills");
688
687
  function parseFrontmatter(content) {
689
688
  const match = content.match(/^---\n([\s\S]*?)\n---/);
690
689
  if (!match) {
@@ -692,23 +691,23 @@ function parseFrontmatter(content) {
692
691
  }
693
692
  const fields = {};
694
693
  for (const line of match[1].split("\n")) {
695
- const sep = line.indexOf(":");
696
- if (sep > 0) {
697
- fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
694
+ const sep2 = line.indexOf(":");
695
+ if (sep2 > 0) {
696
+ fields[line.slice(0, sep2).trim()] = line.slice(sep2 + 1).trim();
698
697
  }
699
698
  }
700
699
  return fields;
701
700
  }
702
- function loadCatalog() {
701
+ function buildSkillCatalog(opts) {
703
702
  let files;
704
703
  try {
705
- files = fs5.readdirSync(SKILLS_DIR).filter((f) => f.endsWith(".md"));
704
+ files = fs5.readdirSync(opts.dir).filter((f) => f.endsWith(".md"));
706
705
  } catch {
707
- return [];
706
+ files = [];
708
707
  }
709
708
  const skills = [];
710
709
  for (const file of files.sort()) {
711
- const full = path3.join(SKILLS_DIR, file);
710
+ const full = path3.join(opts.dir, file);
712
711
  const id = file.replace(/\.md$/, "");
713
712
  const fields = parseFrontmatter(fs5.readFileSync(full, "utf-8"));
714
713
  if (!fields.name || !fields.what || !fields.when) {
@@ -722,38 +721,58 @@ function loadCatalog() {
722
721
  path: full
723
722
  });
724
723
  }
725
- return skills;
724
+ return {
725
+ skills,
726
+ ids: skills.map((s) => s.id),
727
+ get(id) {
728
+ return skills.find((s) => s.id === id);
729
+ },
730
+ readBody(skill) {
731
+ return fs5.readFileSync(skill.path, "utf-8").replace(/^---[\s\S]*?---\s*/, "").trim();
732
+ },
733
+ renderCatalogBlock() {
734
+ if (skills.length === 0) {
735
+ return "";
736
+ }
737
+ const entries = skills.map(
738
+ (s) => [
739
+ `### ${s.name} (\`${s.id}\`)`,
740
+ s.what,
741
+ "",
742
+ `When to load: ${s.when}`,
743
+ `Reference: ${s.path}`
744
+ ].join("\n")
745
+ );
746
+ return `<${opts.tag}>
747
+ ${opts.intro}
748
+
749
+ ${entries.join("\n\n")}
750
+ </${opts.tag}>`;
751
+ }
752
+ };
726
753
  }
727
- var SKILLS = loadCatalog();
728
- var SKILL_IDS = SKILLS.map((s) => s.id);
754
+
755
+ // src/prompt/skills/_catalog.ts
756
+ var INTRO = `Platform capabilities most apps don't use, so their references are kept out of this prompt rather than competing for your attention on every task \u2014 not because they're marginal.
757
+
758
+ Read what follows as part of what the platform can do, not as a lookup table. Recognising that one of these fits a feature is your job, and proposing one is fair game \u2014 several of them are the difference between an app that works and an app worth showing off. When a trigger fires, load the reference with loadSkill before writing the code rather than after. Loading is cheap and expected; guessing at one of these APIs is not.
759
+
760
+ A loaded reference drops out of the conversation once it ages out. Re-read it at the path listed with readFile whenever you need it again.`;
761
+ var catalog = buildSkillCatalog({
762
+ dir: assetPath("prompt", "skills"),
763
+ tag: "available_skills",
764
+ intro: INTRO
765
+ });
766
+ var SKILLS = catalog.skills;
767
+ var SKILL_IDS = catalog.ids;
729
768
  function getSkill(id) {
730
- return SKILLS.find((s) => s.id === id);
769
+ return catalog.get(id);
731
770
  }
732
771
  function readSkillBody(skill) {
733
- return fs5.readFileSync(skill.path, "utf-8").replace(/^---[\s\S]*?---\s*/, "").trim();
772
+ return catalog.readBody(skill);
734
773
  }
735
774
  function loadSkillsCatalog() {
736
- if (SKILLS.length === 0) {
737
- return "";
738
- }
739
- const entries = SKILLS.map(
740
- (s) => [
741
- `### ${s.name} (\`${s.id}\`)`,
742
- s.what,
743
- "",
744
- `When to load: ${s.when}`,
745
- `Reference: ${s.path}`
746
- ].join("\n")
747
- );
748
- return `<available_skills>
749
- Platform capabilities most apps don't use, so their references are kept out of this prompt rather than competing for your attention on every task \u2014 not because they're marginal.
750
-
751
- Read what follows as part of what the platform can do, not as a lookup table. Recognising that one of these fits a feature is your job, and proposing one is fair game \u2014 several of them are the difference between an app that works and an app worth showing off. When a trigger fires, load the reference with loadSkill before writing the code rather than after. Loading is cheap and expected; guessing at one of these APIs is not.
752
-
753
- A loaded reference drops out of the conversation once it ages out. Re-read it at the path listed with readFile whenever you need it again.
754
-
755
- ${entries.join("\n\n")}
756
- </available_skills>`;
775
+ return catalog.renderCatalogBlock();
757
776
  }
758
777
 
759
778
  // src/prompt/index.ts
@@ -1663,7 +1682,7 @@ function formatCliResult(r) {
1663
1682
  return logBlock + body + truncNote;
1664
1683
  }
1665
1684
  function runCli(command, args, options) {
1666
- return new Promise((resolve3) => {
1685
+ return new Promise((resolve4) => {
1667
1686
  const timeout = options?.timeout ?? 6e4;
1668
1687
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1669
1688
  let finalArgs = args;
@@ -1692,7 +1711,7 @@ function runCli(command, args, options) {
1692
1711
  if (killTimer) {
1693
1712
  clearTimeout(killTimer);
1694
1713
  }
1695
- resolve3(result);
1714
+ resolve4(result);
1696
1715
  };
1697
1716
  const child = spawn(command, finalArgs, {
1698
1717
  stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
@@ -2389,7 +2408,7 @@ var bashTool = {
2389
2408
  async execute(input, context) {
2390
2409
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
2391
2410
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2392
- return new Promise((resolve3) => {
2411
+ return new Promise((resolve4) => {
2393
2412
  const child = spawn2("sh", ["-c", input.command], {
2394
2413
  // Pinned rather than inherited. `undefined` here means "wherever the
2395
2414
  // process happens to be", which is the project root only by luck.
@@ -2414,9 +2433,9 @@ var bashTool = {
2414
2433
  clearTimeout(timer);
2415
2434
  if (!output) {
2416
2435
  if (code && code !== 0) {
2417
- resolve3(`Error: process exited with code ${code}`);
2436
+ resolve4(`Error: process exited with code ${code}`);
2418
2437
  } else {
2419
- resolve3("(no output)");
2438
+ resolve4("(no output)");
2420
2439
  }
2421
2440
  return;
2422
2441
  }
@@ -2442,18 +2461,18 @@ var bashTool = {
2442
2461
  `${(MAX_OUTPUT_BYTES / 1024).toFixed(0)}KB of ${(totalBytes / 1024).toFixed(0)}KB`
2443
2462
  );
2444
2463
  }
2445
- resolve3(
2464
+ resolve4(
2446
2465
  truncated + `
2447
2466
 
2448
2467
  (truncated at ${reasons.join(" / ")} \u2014 narrow the command (grep, head/tail, smaller paths) instead of increasing limits)`
2449
2468
  );
2450
2469
  } else {
2451
- resolve3(output);
2470
+ resolve4(output);
2452
2471
  }
2453
2472
  });
2454
2473
  child.on("error", (err) => {
2455
2474
  clearTimeout(timer);
2456
- resolve3(`Error: ${err.message}`);
2475
+ resolve4(`Error: ${err.message}`);
2457
2476
  });
2458
2477
  });
2459
2478
  }
@@ -2570,17 +2589,17 @@ var grepTool = {
2570
2589
  }
2571
2590
  const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
2572
2591
  const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
2573
- return new Promise((resolve3) => {
2592
+ return new Promise((resolve4) => {
2574
2593
  exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
2575
2594
  if (stdout?.trim()) {
2576
- resolve3(formatResults(stdout, max, mode));
2595
+ resolve4(formatResults(stdout, max, mode));
2577
2596
  return;
2578
2597
  }
2579
2598
  exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
2580
2599
  if (grepStdout?.trim()) {
2581
- resolve3(formatResults(grepStdout, max, mode));
2600
+ resolve4(formatResults(grepStdout, max, mode));
2582
2601
  } else {
2583
- resolve3("No matches found.");
2602
+ resolve4("No matches found.");
2584
2603
  }
2585
2604
  });
2586
2605
  });
@@ -2896,7 +2915,7 @@ var restartProcessTool = {
2896
2915
  async execute(input) {
2897
2916
  const data = await lspRequest("/restart-process", { name: input.name });
2898
2917
  if (data.ok) {
2899
- await new Promise((resolve3) => setTimeout(resolve3, 5e3));
2918
+ await new Promise((resolve4) => setTimeout(resolve4, 5e3));
2900
2919
  return `Restarted ${input.name}.`;
2901
2920
  }
2902
2921
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -3123,6 +3142,26 @@ ${opts.styleMap}
3123
3142
  ${ANALYSIS_RESPONSE_FORMAT}`;
3124
3143
  return p;
3125
3144
  }
3145
+ async function renderHtmlViaSidecar(opts) {
3146
+ const result = await sidecarRequest(
3147
+ "/render-html",
3148
+ {
3149
+ html: opts.html,
3150
+ width: opts.width,
3151
+ height: opts.height,
3152
+ ...opts.transparent ? { transparent: true } : {},
3153
+ ...opts.scale != null ? { scale: opts.scale } : {}
3154
+ },
3155
+ { timeout: VIEWPORT_CAPTURE_TIMEOUT_MS }
3156
+ );
3157
+ const url = result?.url;
3158
+ if (!url) {
3159
+ throw new Error(
3160
+ `No URL in sidecar render response. The browser may not be ready yet. Response: ${JSON.stringify(result)}`
3161
+ );
3162
+ }
3163
+ return { url, width: result.width, height: result.height };
3164
+ }
3126
3165
  async function streamScreenshotAnalysis(opts) {
3127
3166
  const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
3128
3167
  const url = await resolveImageRef(image, apiConfig);
@@ -4262,10 +4301,10 @@ function parseFrontmatter2(filePath) {
4262
4301
  }
4263
4302
  const fm = {};
4264
4303
  for (const line of match[1].split("\n")) {
4265
- const sep = line.indexOf(":");
4266
- if (sep > 0) {
4267
- const key = line.slice(0, sep).trim();
4268
- const val = line.slice(sep + 1).trim();
4304
+ const sep2 = line.indexOf(":");
4305
+ if (sep2 > 0) {
4306
+ const key = line.slice(0, sep2).trim();
4307
+ const val = line.slice(sep2 + 1).trim();
4269
4308
  fm[key] = val;
4270
4309
  }
4271
4310
  }
@@ -4569,7 +4608,7 @@ var browserAutomationTool = {
4569
4608
  // src/tools/code/screenshot.ts
4570
4609
  var screenshotDefinition = {
4571
4610
  name: "screenshot",
4572
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
4611
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To capture at exact pixel dimensions, set `width` and `height`: the tool clips to exactly that viewport and returns the image URL.",
4573
4612
  inputSchema: {
4574
4613
  type: "object",
4575
4614
  properties: {
@@ -4600,7 +4639,7 @@ var screenshotDefinition = {
4600
4639
  format: {
4601
4640
  type: "string",
4602
4641
  enum: ["png", "jpeg"],
4603
- description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
4642
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics where JPEG artifacts show on sharp type and edges."
4604
4643
  },
4605
4644
  instructions: {
4606
4645
  type: "string",
@@ -4922,7 +4961,36 @@ ${brief}
4922
4961
  }
4923
4962
 
4924
4963
  // src/subagents/designExpert/tools/images/imageGenerator.ts
4925
- var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4964
+ var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), whether the artwork extends fully to all four edges of the canvas or sits inset (call out any baked-in border, frame, rounded-corner mask, drop-shadow margin, or mockup presentation such as an icon rendered on a background or device), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4965
+ var OPENAI_IMAGE_SIZES = [
4966
+ [768, 1024],
4967
+ [1024, 768],
4968
+ [1024, 1024],
4969
+ [1024, 1536],
4970
+ [1536, 1024],
4971
+ [1280, 2560],
4972
+ [2560, 1280],
4973
+ [1440, 2560],
4974
+ [2560, 1440],
4975
+ [1792, 2400],
4976
+ [2400, 1792],
4977
+ [2400, 2880],
4978
+ [2560, 2560]
4979
+ ];
4980
+ function snapToOpenAiImageSize(width, height) {
4981
+ let best = OPENAI_IMAGE_SIZES[0];
4982
+ let bestScore = Infinity;
4983
+ for (const [w, h] of OPENAI_IMAGE_SIZES) {
4984
+ const aspectDiff = Math.abs(Math.log(w / h / (width / height)));
4985
+ const areaDiff = Math.abs(Math.log(w * h / (width * height)));
4986
+ const score = aspectDiff * 8 + areaDiff;
4987
+ if (score < bestScore) {
4988
+ bestScore = score;
4989
+ best = [w, h];
4990
+ }
4991
+ }
4992
+ return `${best[0]}x${best[1]}`;
4993
+ }
4926
4994
  async function generateImageAssets(opts) {
4927
4995
  const {
4928
4996
  prompts,
@@ -4937,7 +5005,11 @@ async function generateImageAssets(opts) {
4937
5005
  const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
4938
5006
  const width = opts.width || 2048;
4939
5007
  const height = opts.height || 2048;
4940
- const config = { width, height };
5008
+ const config = {
5009
+ width,
5010
+ height,
5011
+ size: snapToOpenAiImageSize(width, height)
5012
+ };
4941
5013
  if (sourceImages?.length) {
4942
5014
  const [firstImage] = sourceImages;
4943
5015
  config.images = sourceImages;
@@ -5189,11 +5261,143 @@ async function execute6(input, onLog, context) {
5189
5261
  });
5190
5262
  }
5191
5263
 
5264
+ // src/subagents/designExpert/tools/images/renderImage.ts
5265
+ var renderImage_exports = {};
5266
+ __export(renderImage_exports, {
5267
+ definition: () => definition7,
5268
+ execute: () => execute7
5269
+ });
5270
+ import { mkdir, unlink, writeFile } from "fs/promises";
5271
+ import { tmpdir } from "os";
5272
+ import { dirname, join, resolve as resolve2, sep } from "path";
5273
+ import { randomUUID } from "crypto";
5274
+ var MIN_DIMENSION = 16;
5275
+ var MAX_DIMENSION = 4096;
5276
+ var 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.';
5277
+ var definition7 = {
5278
+ name: "renderImage",
5279
+ 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.",
5280
+ inputSchema: {
5281
+ type: "object",
5282
+ properties: {
5283
+ html: {
5284
+ type: "string",
5285
+ 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."
5286
+ },
5287
+ width: {
5288
+ type: "number",
5289
+ description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
5290
+ },
5291
+ height: {
5292
+ type: "number",
5293
+ description: "Viewport height in CSS pixels. Range: 16-4096."
5294
+ },
5295
+ scale: {
5296
+ type: "number",
5297
+ 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)."
5298
+ },
5299
+ transparentBackground: {
5300
+ type: "boolean",
5301
+ 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."
5302
+ },
5303
+ savePath: {
5304
+ type: "string",
5305
+ 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)."
5306
+ }
5307
+ },
5308
+ required: ["html", "width", "height"]
5309
+ }
5310
+ };
5311
+ async function execute7(input, onLog, context) {
5312
+ const html = typeof input.html === "string" ? input.html : "";
5313
+ const width = Math.round(Number(input.width));
5314
+ const height = Math.round(Number(input.height));
5315
+ if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
5316
+ return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
5317
+ }
5318
+ const release = await acquireBrowserLock();
5319
+ let rendered;
5320
+ try {
5321
+ onLog?.("Rendering document in the sandbox browser...");
5322
+ rendered = await renderHtmlViaSidecar({
5323
+ html,
5324
+ width,
5325
+ height,
5326
+ transparent: input.transparentBackground === true,
5327
+ scale: typeof input.scale === "number" ? input.scale : void 0
5328
+ });
5329
+ } catch (err) {
5330
+ return `Error: render failed: ${err?.message ?? err}`;
5331
+ } finally {
5332
+ release();
5333
+ }
5334
+ let bytes;
5335
+ try {
5336
+ const res = await fetch(rendered.url);
5337
+ if (!res.ok) {
5338
+ throw new Error(`fetch returned ${res.status}`);
5339
+ }
5340
+ bytes = Buffer.from(await res.arrayBuffer());
5341
+ } catch (err) {
5342
+ return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
5343
+ }
5344
+ let savedPath;
5345
+ if (typeof input.savePath === "string" && input.savePath) {
5346
+ const absolute = resolve2(PROJECT_ROOT, input.savePath);
5347
+ if (!absolute.startsWith(PROJECT_ROOT + sep)) {
5348
+ return "Error: savePath must resolve inside the project.";
5349
+ }
5350
+ await mkdir(dirname(absolute), { recursive: true });
5351
+ await writeFile(absolute, bytes);
5352
+ savedPath = input.savePath;
5353
+ }
5354
+ onLog?.("Hosting the capture...");
5355
+ const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
5356
+ let url = rendered.url;
5357
+ let temporary = true;
5358
+ try {
5359
+ await writeFile(tmpPath, bytes);
5360
+ const upload = await runMindstudioCliResult(["upload", tmpPath], {
5361
+ timeout: 6e4,
5362
+ onLog,
5363
+ caller: "designExpert"
5364
+ });
5365
+ const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
5366
+ if (match?.length) {
5367
+ url = match[match.length - 1];
5368
+ temporary = false;
5369
+ }
5370
+ } finally {
5371
+ await unlink(tmpPath).catch(() => {
5372
+ });
5373
+ }
5374
+ const analysis = await analyzeImage({
5375
+ prompt: RENDER_ANALYZE_PROMPT,
5376
+ image: url,
5377
+ onLog,
5378
+ model: resolveModel("imageAnalysis", context?.models, context?.model)
5379
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
5380
+ return JSON.stringify({
5381
+ images: [
5382
+ {
5383
+ url,
5384
+ ...temporary ? {
5385
+ 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."
5386
+ } : {},
5387
+ ...savedPath ? { savedPath } : {},
5388
+ analysis,
5389
+ width: rendered.width,
5390
+ height: rendered.height
5391
+ }
5392
+ ]
5393
+ });
5394
+ }
5395
+
5192
5396
  // src/subagents/designExpert/tools/polishCopy.ts
5193
5397
  var polishCopy_exports = {};
5194
5398
  __export(polishCopy_exports, {
5195
- definition: () => definition7,
5196
- execute: () => execute7
5399
+ definition: () => definition8,
5400
+ execute: () => execute8
5197
5401
  });
5198
5402
 
5199
5403
  // src/subagents/copyEditor/tools.ts
@@ -5249,7 +5453,7 @@ var copyEditorTool = {
5249
5453
  };
5250
5454
 
5251
5455
  // src/subagents/designExpert/tools/polishCopy.ts
5252
- var definition7 = {
5456
+ var definition8 = {
5253
5457
  name: "polishCopy",
5254
5458
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
5255
5459
  inputSchema: {
@@ -5263,10 +5467,63 @@ var definition7 = {
5263
5467
  required: ["task"]
5264
5468
  }
5265
5469
  };
5266
- async function execute7(input, _onLog, context) {
5470
+ async function execute8(input, _onLog, context) {
5267
5471
  return copyEditorTool.execute(input, context);
5268
5472
  }
5269
5473
 
5474
+ // src/subagents/designExpert/tools/loadSkill.ts
5475
+ var loadSkill_exports = {};
5476
+ __export(loadSkill_exports, {
5477
+ definition: () => definition9,
5478
+ execute: () => execute9
5479
+ });
5480
+
5481
+ // src/subagents/designExpert/skills/_catalog.ts
5482
+ var INTRO2 = `Deep craft references for specific surfaces, kept out of this prompt so they can go far beyond what a resident section could \u2014 full technique recipes, worked examples, quality bars. Recognising that one of these fits the brief is part of your job as much as the design work itself: when a trigger fires, load the reference with loadSkill before designing in that area, not after. The craft in these docs is the difference between a defaulted artifact and a designed one.
5483
+
5484
+ A loaded reference drops out of the conversation once it ages out. Re-read it at the path listed with readFile whenever you need it again.`;
5485
+ var designSkillCatalog = buildSkillCatalog({
5486
+ dir: assetPath("subagents/designExpert", "skills"),
5487
+ tag: "available_skills",
5488
+ intro: INTRO2
5489
+ });
5490
+
5491
+ // src/subagents/designExpert/tools/loadSkill.ts
5492
+ var definition9 = {
5493
+ name: "loadSkill",
5494
+ description: "Load the full craft reference for a design surface that isn't in your prompt. The available skills and the trigger for each are listed in <available_skills>. Load one before designing in its area, not after \u2014 these are hard-won technique recipes, and the defaulted version of these surfaces is exactly what they exist to prevent. Calling this is cheap and expected \u2014 if you're unsure whether you need it, load it.",
5495
+ inputSchema: {
5496
+ type: "object",
5497
+ properties: {
5498
+ skill: {
5499
+ type: "string",
5500
+ // Omitted when the catalog is empty: an empty enum is a schema no
5501
+ // provider accepts, and failing one tool call beats failing every
5502
+ // request if the docs ever go missing from a build.
5503
+ ...designSkillCatalog.ids.length > 0 ? { enum: designSkillCatalog.ids } : {},
5504
+ description: "The skill id, as listed in <available_skills>."
5505
+ }
5506
+ },
5507
+ required: ["skill"]
5508
+ }
5509
+ };
5510
+ async function execute9(input) {
5511
+ const id = String(input.skill ?? "");
5512
+ const skill = designSkillCatalog.get(id);
5513
+ if (!skill) {
5514
+ return `Error: unknown skill "${id}". Available: ${designSkillCatalog.ids.join(", ") || "(none)"}`;
5515
+ }
5516
+ try {
5517
+ const body = designSkillCatalog.readBody(skill);
5518
+ return `${body}
5519
+
5520
+ ---
5521
+ This reference lives at ${skill.path}. Re-read it with readFile if you need it again later \u2014 it won't stay in the conversation.`;
5522
+ } catch (err) {
5523
+ return `Error loading skill "${id}": ${err.message}`;
5524
+ }
5525
+ }
5526
+
5270
5527
  // src/subagents/designExpert/tools/index.ts
5271
5528
  var tools = {
5272
5529
  searchGoogle: searchGoogle_exports,
@@ -5279,7 +5536,9 @@ var tools = {
5279
5536
  screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
5280
5537
  generateImages: generateImages_exports,
5281
5538
  editImages: editImages_exports,
5282
- polishCopy: polishCopy_exports
5539
+ renderImage: renderImage_exports,
5540
+ polishCopy: polishCopy_exports,
5541
+ loadSkill: loadSkill_exports
5283
5542
  };
5284
5543
  var DESIGN_EXPERT_TOOLS = [
5285
5544
  ...COMMON_READ_TOOLS,
@@ -5475,6 +5734,12 @@ function getDesignExpertPrompt(onboardingState, opts) {
5475
5734
  "{{ui_case_studies}}",
5476
5735
  getUiInspirationSample(indices.uiInspiration)
5477
5736
  );
5737
+ const skillsBlock = designSkillCatalog.renderCatalogBlock();
5738
+ if (skillsBlock) {
5739
+ prompt += `
5740
+
5741
+ ${skillsBlock}`;
5742
+ }
5478
5743
  prompt += "\n\n<!-- cache_breakpoint -->";
5479
5744
  if (specContext) {
5480
5745
  prompt += `
@@ -5698,13 +5963,13 @@ var PITCH_DECK_SHELL = readAsset(
5698
5963
  "subagents/productVision",
5699
5964
  "pitch-deck-shell.html"
5700
5965
  );
5701
- function resolve2(filePath) {
5966
+ function resolve3(filePath) {
5702
5967
  return path10.join(ROADMAP_DIR, filePath);
5703
5968
  }
5704
5969
  async function executeVisionTool(name, input, context) {
5705
5970
  switch (name) {
5706
5971
  case "writeFile": {
5707
- const filePath = resolve2(input.path);
5972
+ const filePath = resolve3(input.path);
5708
5973
  try {
5709
5974
  fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5710
5975
  let oldContent = null;
@@ -5722,7 +5987,7 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
5722
5987
  }
5723
5988
  }
5724
5989
  case "deleteFile": {
5725
- const filePath = resolve2(input.path);
5990
+ const filePath = resolve3(input.path);
5726
5991
  try {
5727
5992
  if (!fs19.existsSync(filePath)) {
5728
5993
  return `Error: ${filePath} does not exist`;
@@ -5739,7 +6004,7 @@ ${unifiedDiff(filePath, oldContent, "")}`;
5739
6004
  if (!context) {
5740
6005
  return "Error: writePitchDeck requires execution context for design expert delegation";
5741
6006
  }
5742
- const filePath = resolve2("pitch.html");
6007
+ const filePath = resolve3("pitch.html");
5743
6008
  try {
5744
6009
  fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5745
6010
  const exists = fs19.existsSync(filePath);
@@ -7360,14 +7625,14 @@ function buildCorpus() {
7360
7625
  });
7361
7626
  }
7362
7627
  }
7363
- const sep = "\n\n---\n\n";
7628
+ const sep2 = "\n\n---\n\n";
7364
7629
  const sections = [];
7365
7630
  let usedChars = 0;
7366
7631
  for (const { path: p, content } of files) {
7367
7632
  const section = `## File: ${p}
7368
7633
 
7369
7634
  ${content}`;
7370
- const added = section.length + (sections.length > 0 ? sep.length : 0);
7635
+ const added = section.length + (sections.length > 0 ? sep2.length : 0);
7371
7636
  if (sections.length > 0 && usedChars + added > BRAND_CORPUS_CHAR_LIMIT) {
7372
7637
  sections.push(
7373
7638
  `(brand corpus truncated: included ${sections.length} of ${files.length} files, ~${(usedChars / 1024).toFixed(0)}KB; brand-relevant files were prioritized.)`
@@ -7377,7 +7642,7 @@ ${content}`;
7377
7642
  sections.push(section);
7378
7643
  usedChars += added;
7379
7644
  }
7380
- return sections.join(sep);
7645
+ return sections.join(sep2);
7381
7646
  }
7382
7647
  function headSlice(content) {
7383
7648
  if (content.length <= HEAD_SLICE_CHARS) {
@@ -8474,8 +8739,8 @@ async function runTurn(params) {
8474
8739
 
8475
8740
  // src/headless/attachments.ts
8476
8741
  import { mkdirSync, existsSync } from "fs";
8477
- import { writeFile } from "fs/promises";
8478
- import { basename as basename2, join, extname as extname2 } from "path";
8742
+ import { writeFile as writeFile2 } from "fs/promises";
8743
+ import { basename as basename2, join as join2, extname as extname2 } from "path";
8479
8744
  var log15 = createLogger("headless:attachments");
8480
8745
  var UPLOADS_DIR = "src/.user-uploads";
8481
8746
  function filenameFromUrl(url) {
@@ -8488,7 +8753,7 @@ function filenameFromUrl(url) {
8488
8753
  }
8489
8754
  }
8490
8755
  function resolveUniqueFilename(name, claimed) {
8491
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join(UPLOADS_DIR, candidate));
8756
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join2(UPLOADS_DIR, candidate));
8492
8757
  if (isFree(name)) {
8493
8758
  return name;
8494
8759
  }
@@ -8523,7 +8788,7 @@ async function persistAttachments(attachments) {
8523
8788
  const results = await Promise.allSettled(
8524
8789
  nonVoice.map(async (att, i) => {
8525
8790
  const name = names[i];
8526
- const localPath = join(UPLOADS_DIR, name);
8791
+ const localPath = join2(UPLOADS_DIR, name);
8527
8792
  const res = await fetch(att.url, {
8528
8793
  signal: AbortSignal.timeout(3e4)
8529
8794
  });
@@ -8531,7 +8796,7 @@ async function persistAttachments(attachments) {
8531
8796
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
8532
8797
  }
8533
8798
  const buffer = Buffer.from(await res.arrayBuffer());
8534
- await writeFile(localPath, buffer);
8799
+ await writeFile2(localPath, buffer);
8535
8800
  log15.info("Attachment saved", {
8536
8801
  filename: name,
8537
8802
  path: localPath,
@@ -8545,7 +8810,7 @@ async function persistAttachments(attachments) {
8545
8810
  });
8546
8811
  if (textRes.ok) {
8547
8812
  extractedTextPath = `${localPath}.txt`;
8548
- await writeFile(extractedTextPath, await textRes.text(), "utf-8");
8813
+ await writeFile2(extractedTextPath, await textRes.text(), "utf-8");
8549
8814
  log15.info("Extracted text saved", { path: extractedTextPath });
8550
8815
  }
8551
8816
  } catch {
@@ -9132,17 +9397,17 @@ var HeadlessSession = class {
9132
9397
  return Promise.resolve(early);
9133
9398
  }
9134
9399
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
9135
- return new Promise((resolve3) => {
9400
+ return new Promise((resolve4) => {
9136
9401
  const timeout = shouldTimeout ? setTimeout(() => {
9137
9402
  this.pendingTools.delete(id);
9138
- resolve3(
9403
+ resolve4(
9139
9404
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
9140
9405
  );
9141
9406
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
9142
9407
  this.pendingTools.set(id, {
9143
9408
  resolve: (result) => {
9144
9409
  clearTimeout(timeout);
9145
- resolve3(result);
9410
+ resolve4(result);
9146
9411
  },
9147
9412
  timeout
9148
9413
  });