@mindstudio-ai/remy 0.1.26 → 0.1.27

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 (30) hide show
  1. package/README.md +149 -41
  2. package/dist/compiled/tables.md +53 -1
  3. package/dist/headless.d.ts +10 -2
  4. package/dist/headless.js +524 -270
  5. package/dist/index.js +567 -300
  6. package/dist/prompt/.notes.md +0 -1
  7. package/dist/prompt/compiled/tables.md +53 -1
  8. package/dist/prompt/static/authoring.md +10 -0
  9. package/dist/prompt/static/instructions.md +2 -1
  10. package/dist/prompt/static/team.md +1 -1
  11. package/dist/static/authoring.md +10 -0
  12. package/dist/static/instructions.md +2 -1
  13. package/dist/static/team.md +1 -1
  14. package/dist/subagents/.notes-background-agents.md +80 -0
  15. package/dist/subagents/browserAutomation/prompt.md +37 -2
  16. package/dist/subagents/codeSanityCheck/prompt.md +1 -0
  17. package/dist/subagents/designExpert/.notes.md +2 -2
  18. package/dist/subagents/designExpert/data/compile-font-descriptions.sh +125 -0
  19. package/dist/subagents/designExpert/data/compile-inspiration.sh +6 -1
  20. package/dist/subagents/designExpert/data/fonts.json +497 -869
  21. package/dist/subagents/designExpert/data/inspiration.json +97 -245
  22. package/dist/subagents/designExpert/data/inspiration.raw.json +1 -12
  23. package/dist/subagents/designExpert/prompts/animation.md +1 -1
  24. package/dist/subagents/designExpert/prompts/identity.md +4 -2
  25. package/dist/subagents/designExpert/prompts/instructions.md +2 -3
  26. package/dist/subagents/designExpert/prompts/layout.md +1 -13
  27. package/dist/subagents/designExpert/prompts/tool-prompts/design-analysis.md +22 -0
  28. package/dist/subagents/designExpert/prompts/tool-prompts/font-analysis.md +17 -0
  29. package/dist/subagents/productVision/prompt.md +1 -1
  30. package/package.json +1 -1
package/dist/headless.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/headless.ts
2
2
  import { createInterface } from "readline";
3
- import fs20 from "fs";
4
- import path13 from "path";
3
+ import fs21 from "fs";
4
+ import path14 from "path";
5
5
 
6
6
  // src/config.ts
7
7
  import fs2 from "fs";
@@ -300,14 +300,11 @@ function buildSystemPrompt(onboardingState, viewContext) {
300
300
  loadSpecFileMetadata(),
301
301
  loadProjectFileListing()
302
302
  ].filter(Boolean).join("\n");
303
- const now = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
304
- dateStyle: "full",
305
- timeStyle: "long"
306
- });
303
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
307
304
  const template = `
308
305
  {{static/identity.md}}
309
306
 
310
- The current date is ${now}.
307
+ Current date/time: ${now}
311
308
 
312
309
  <platform_docs>
313
310
  <platform>
@@ -1142,7 +1139,7 @@ var promptUserTool = {
1142
1139
  }
1143
1140
  ]
1144
1141
  },
1145
- description: "Options for select and checklist types. Each can be a string or { label, description }."
1142
+ description: "Options for select and checklist types. Each can be a string or { label, description }. Image URLs (e.g. https://i.mscdn.ai/...) are rendered as visual previews, so use the URL directly as the option string when presenting images for the user to choose between."
1146
1143
  },
1147
1144
  multiple: {
1148
1145
  type: "boolean",
@@ -1243,27 +1240,75 @@ var confirmDestructiveActionTool = {
1243
1240
  };
1244
1241
 
1245
1242
  // src/subagents/common/runCli.ts
1246
- import { exec } from "child_process";
1243
+ import { spawn } from "child_process";
1247
1244
  function runCli(cmd, options) {
1248
1245
  return new Promise((resolve) => {
1249
- exec(
1250
- cmd,
1251
- {
1252
- timeout: options?.timeout ?? 6e4,
1253
- maxBuffer: options?.maxBuffer ?? 1024 * 1024
1254
- },
1255
- (err, stdout, stderr) => {
1256
- if (stdout.trim()) {
1257
- resolve(stdout.trim());
1258
- return;
1246
+ const timeout = options?.timeout ?? 6e4;
1247
+ const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1248
+ const cmdWithLogs = options?.jsonLogs && !cmd.includes("--json-logs") ? cmd.replace(/^(mindstudio\s+\S+)/, "$1 --json-logs") : cmd;
1249
+ const child = spawn("sh", ["-c", cmdWithLogs], {
1250
+ stdio: ["ignore", "pipe", "pipe"]
1251
+ });
1252
+ const logs = [];
1253
+ let stdout = "";
1254
+ let stderr = "";
1255
+ let stdoutSize = 0;
1256
+ let stderrSize = 0;
1257
+ let killed = false;
1258
+ child.stdout.on("data", (chunk) => {
1259
+ stdoutSize += chunk.length;
1260
+ if (stdoutSize <= maxBuffer) {
1261
+ stdout += chunk.toString();
1262
+ } else if (!killed) {
1263
+ killed = true;
1264
+ child.kill();
1265
+ }
1266
+ });
1267
+ child.stderr.on("data", (chunk) => {
1268
+ stderrSize += chunk.length;
1269
+ if (stderrSize > maxBuffer) {
1270
+ if (!killed) {
1271
+ killed = true;
1272
+ child.kill();
1259
1273
  }
1260
- if (err) {
1261
- resolve(`Error: ${stderr.trim() || err.message}`);
1262
- return;
1274
+ return;
1275
+ }
1276
+ const text = chunk.toString();
1277
+ stderr += text;
1278
+ for (const line of text.split("\n")) {
1279
+ const trimmed = line.trim();
1280
+ if (!trimmed || trimmed[0] !== "{") {
1281
+ continue;
1282
+ }
1283
+ try {
1284
+ const entry = JSON.parse(trimmed);
1285
+ if (entry.type === "log" && entry.value) {
1286
+ const prefix = entry.tag ? `[${entry.tag}]` : "[log]";
1287
+ logs.push(`${prefix} ${entry.value}`);
1288
+ }
1289
+ } catch {
1263
1290
  }
1264
- resolve("(no response)");
1265
1291
  }
1266
- );
1292
+ });
1293
+ const timer = setTimeout(() => {
1294
+ killed = true;
1295
+ child.kill();
1296
+ }, timeout);
1297
+ child.on("close", (code) => {
1298
+ clearTimeout(timer);
1299
+ const logBlock = logs.length > 0 ? logs.join("\n") + "\n\n" : "";
1300
+ const out = stdout.trim();
1301
+ if (out) {
1302
+ resolve(logBlock + out);
1303
+ return;
1304
+ }
1305
+ if (code !== 0 || killed) {
1306
+ const errMsg = stderr.trim() || (killed ? "Process timed out" : `Exit code ${code}`);
1307
+ resolve(logBlock + `Error: ${errMsg}`);
1308
+ return;
1309
+ }
1310
+ resolve(logBlock + "(no response)");
1311
+ });
1267
1312
  });
1268
1313
  }
1269
1314
 
@@ -1670,7 +1715,7 @@ ${unifiedDiff(input.path, content, updated)}`;
1670
1715
  };
1671
1716
 
1672
1717
  // src/tools/code/bash.ts
1673
- import { exec as exec2 } from "child_process";
1718
+ import { exec } from "child_process";
1674
1719
  var DEFAULT_TIMEOUT_MS = 12e4;
1675
1720
  var DEFAULT_MAX_LINES3 = 500;
1676
1721
  var bashTool = {
@@ -1704,7 +1749,7 @@ var bashTool = {
1704
1749
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1705
1750
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1706
1751
  return new Promise((resolve) => {
1707
- exec2(
1752
+ exec(
1708
1753
  input.command,
1709
1754
  {
1710
1755
  timeout: timeoutMs,
@@ -1744,7 +1789,7 @@ var bashTool = {
1744
1789
  };
1745
1790
 
1746
1791
  // src/tools/code/grep.ts
1747
- import { exec as exec3 } from "child_process";
1792
+ import { exec as exec2 } from "child_process";
1748
1793
  var DEFAULT_MAX = 50;
1749
1794
  function formatResults(stdout, max) {
1750
1795
  const lines = stdout.trim().split("\n");
@@ -1791,12 +1836,12 @@ var grepTool = {
1791
1836
  const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1792
1837
  const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1793
1838
  return new Promise((resolve) => {
1794
- exec3(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1839
+ exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1795
1840
  if (stdout?.trim()) {
1796
1841
  resolve(formatResults(stdout, max));
1797
1842
  return;
1798
1843
  }
1799
- exec3(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1844
+ exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1800
1845
  if (grepStdout?.trim()) {
1801
1846
  resolve(formatResults(grepStdout, max));
1802
1847
  } else {
@@ -2027,36 +2072,64 @@ var runMethodTool = {
2027
2072
  }
2028
2073
  };
2029
2074
 
2075
+ // src/tools/_helpers/screenshot.ts
2076
+ var SCREENSHOT_ANALYSIS_PROMPT = "Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).";
2077
+ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2078
+ let prompt;
2079
+ let viewportOnly = false;
2080
+ if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2081
+ prompt = promptOrOptions.prompt;
2082
+ viewportOnly = promptOrOptions.viewportOnly ?? false;
2083
+ } else {
2084
+ prompt = promptOrOptions;
2085
+ }
2086
+ const ssResult = await sidecarRequest(
2087
+ "/screenshot",
2088
+ { fullPage: !viewportOnly },
2089
+ { timeout: 12e4 }
2090
+ );
2091
+ log.debug("Screenshot response", { ssResult });
2092
+ const url = ssResult?.url || ssResult?.screenshotUrl;
2093
+ if (!url) {
2094
+ throw new Error(
2095
+ `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2096
+ );
2097
+ }
2098
+ if (prompt === false) {
2099
+ return url;
2100
+ }
2101
+ const analysisPrompt = prompt || SCREENSHOT_ANALYSIS_PROMPT;
2102
+ const analysis = await runCli(
2103
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2104
+ );
2105
+ return JSON.stringify({ url, analysis });
2106
+ }
2107
+
2030
2108
  // src/tools/code/screenshot.ts
2031
- var DEFAULT_PROMPT = "Describe this app screenshot for a developer who cannot see it. What is visible on screen: the layout, content, interactive elements, any loading or error states. Be concise and factual.";
2032
2109
  var screenshotTool = {
2033
2110
  definition: {
2034
2111
  name: "screenshot",
2035
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Optionally provide a specific question about what you're looking for.",
2112
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Optionally provide a specific question about what you're looking for. Set viewportOnly to capture just what the user sees on screen.",
2036
2113
  inputSchema: {
2037
2114
  type: "object",
2038
2115
  properties: {
2039
2116
  prompt: {
2040
2117
  type: "string",
2041
2118
  description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
2119
+ },
2120
+ viewportOnly: {
2121
+ type: "boolean",
2122
+ description: "Capture only the visible viewport instead of the full scrollable page. Use when checking above-the-fold layout or viewport-relative sizing like 100vh."
2042
2123
  }
2043
2124
  }
2044
2125
  }
2045
2126
  },
2046
2127
  async execute(input) {
2047
2128
  try {
2048
- const { url } = await sidecarRequest(
2049
- "/screenshot",
2050
- {},
2051
- { timeout: 12e4 }
2052
- );
2053
- const analysisPrompt = input.prompt || DEFAULT_PROMPT;
2054
- const analysis = await runCli(
2055
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2056
- );
2057
- return `Screenshot: ${url}
2058
-
2059
- ${analysis}`;
2129
+ return await captureAndAnalyzeScreenshot({
2130
+ prompt: input.prompt,
2131
+ viewportOnly: input.viewportOnly
2132
+ });
2060
2133
  } catch (err) {
2061
2134
  return `Error taking screenshot: ${err.message}`;
2062
2135
  }
@@ -2124,7 +2197,9 @@ async function runSubAgent(config) {
2124
2197
  ...apiConfig,
2125
2198
  model,
2126
2199
  subAgentId,
2127
- system,
2200
+ system: `${system}
2201
+
2202
+ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`,
2128
2203
  messages: cleanMessagesForApi(messages),
2129
2204
  tools,
2130
2205
  signal
@@ -2244,6 +2319,13 @@ async function runSubAgent(config) {
2244
2319
  })
2245
2320
  );
2246
2321
  for (const r of results) {
2322
+ const block = contentBlocks.find(
2323
+ (b) => b.type === "tool" && b.id === r.id
2324
+ );
2325
+ if (block?.type === "tool") {
2326
+ block.result = r.result;
2327
+ block.isError = r.isError;
2328
+ }
2247
2329
  messages.push({
2248
2330
  role: "user",
2249
2331
  content: r.result,
@@ -2269,8 +2351,18 @@ var BROWSER_TOOLS = [
2269
2351
  properties: {
2270
2352
  command: {
2271
2353
  type: "string",
2272
- enum: ["snapshot", "click", "type", "wait", "evaluate"],
2273
- description: "snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). wait: wait for an element to appear (polls 100ms, waits for network). evaluate: run JS in the page."
2354
+ enum: [
2355
+ "snapshot",
2356
+ "click",
2357
+ "type",
2358
+ "select",
2359
+ "wait",
2360
+ "navigate",
2361
+ "evaluate",
2362
+ "styles",
2363
+ "screenshot"
2364
+ ],
2365
+ description: "snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). select: select a dropdown option by text. wait: wait for an element to appear (polls 100ms, waits for network). navigate: navigate to a URL within the app (waits for load, subsequent steps run on new page). evaluate: run JS in the page. styles: read computed CSS styles from elements (pass properties array with camelCase names, or omit for defaults). screenshot: full-page viewport-stitched screenshot (returns base64 JPEG with dimensions)."
2274
2366
  },
2275
2367
  ref: {
2276
2368
  type: "string",
@@ -2292,6 +2384,10 @@ var BROWSER_TOOLS = [
2292
2384
  type: "string",
2293
2385
  description: "CSS selector fallback (last resort)."
2294
2386
  },
2387
+ option: {
2388
+ type: "string",
2389
+ description: "For select: the option text to select from a dropdown."
2390
+ },
2295
2391
  clear: {
2296
2392
  type: "boolean",
2297
2393
  description: "For type: clear the field before typing."
@@ -2303,6 +2399,15 @@ var BROWSER_TOOLS = [
2303
2399
  script: {
2304
2400
  type: "string",
2305
2401
  description: "For evaluate: JavaScript to run in the page."
2402
+ },
2403
+ url: {
2404
+ type: "string",
2405
+ description: 'For navigate: the URL to navigate to (e.g., "/quiz", "/settings").'
2406
+ },
2407
+ properties: {
2408
+ type: "array",
2409
+ items: { type: "string" },
2410
+ description: 'For styles: camelCase CSS property names to read (e.g., ["backgroundColor", "borderRadius", "fontSize"]). Omit for a default set.'
2306
2411
  }
2307
2412
  },
2308
2413
  required: ["command"]
@@ -2329,7 +2434,7 @@ var BROWSER_TOOLS = [
2329
2434
  }
2330
2435
  }
2331
2436
  ];
2332
- var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand", "screenshot"]);
2437
+ var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
2333
2438
 
2334
2439
  // src/subagents/browserAutomation/prompt.ts
2335
2440
  import fs13 from "fs";
@@ -2355,13 +2460,13 @@ ${appSpec}
2355
2460
  var browserAutomationTool = {
2356
2461
  definition: {
2357
2462
  name: "runAutomatedBrowserTest",
2358
- description: "Run an automated browser test against the live preview. The test agent always starts on the main page, so include navigation instructions if the test involves a sub-page. The browser uses the current user roles and dev database state, so run a scenario first if you need specific data or roles. Use after writing or modifying frontend code, to reproduce user-reported issues, or to test end-to-end flows.",
2463
+ description: "Run an automated browser test against the live preview. Describe what to test \u2014 the agent figures out how. Use after writing or modifying frontend code, to reproduce user-reported issues, or to test end-to-end flows.",
2359
2464
  inputSchema: {
2360
2465
  type: "object",
2361
2466
  properties: {
2362
2467
  task: {
2363
2468
  type: "string",
2364
- description: "What to test, in natural language. Include how to navigate to the relevant page and what data/roles to expect."
2469
+ description: "What to test, in natural language. Keep it brief \u2014 the agent reads the spec and figures out navigation, data setup, and test strategy on its own."
2365
2470
  }
2366
2471
  },
2367
2472
  required: ["task"]
@@ -2389,6 +2494,13 @@ var browserAutomationTool = {
2389
2494
  tools: BROWSER_TOOLS,
2390
2495
  externalTools: BROWSER_EXTERNAL_TOOLS,
2391
2496
  executeTool: async (name) => {
2497
+ if (name === "screenshot") {
2498
+ try {
2499
+ return await captureAndAnalyzeScreenshot();
2500
+ } catch (err) {
2501
+ return `Error taking screenshot: ${err.message}`;
2502
+ }
2503
+ }
2392
2504
  if (name === "resetBrowser") {
2393
2505
  try {
2394
2506
  await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
@@ -2405,7 +2517,50 @@ var browserAutomationTool = {
2405
2517
  signal: context.signal,
2406
2518
  parentToolId: context.toolCallId,
2407
2519
  onEvent: context.onEvent,
2408
- resolveExternalTool: context.resolveExternalTool
2520
+ resolveExternalTool: async (id, name, input2) => {
2521
+ if (!context.resolveExternalTool) {
2522
+ return "Error: no external tool resolver";
2523
+ }
2524
+ const result2 = await context.resolveExternalTool(id, name, input2);
2525
+ if (name === "browserCommand") {
2526
+ try {
2527
+ const parsed = JSON.parse(result2);
2528
+ const screenshotSteps = (parsed.steps || []).filter(
2529
+ (s) => s.command === "screenshot" && s.result?.url
2530
+ );
2531
+ if (screenshotSteps.length > 0) {
2532
+ const batchInput = screenshotSteps.map((s) => ({
2533
+ stepType: "analyzeImage",
2534
+ step: {
2535
+ imageUrl: s.result.url,
2536
+ prompt: SCREENSHOT_ANALYSIS_PROMPT
2537
+ }
2538
+ }));
2539
+ const batchResult = await runCli(
2540
+ `mindstudio batch --no-meta ${JSON.stringify(JSON.stringify(batchInput))}`,
2541
+ { timeout: 12e4 }
2542
+ );
2543
+ try {
2544
+ const analyses = JSON.parse(batchResult);
2545
+ let ai = 0;
2546
+ for (const step of parsed.steps) {
2547
+ if (step.command === "screenshot" && step.result?.url && ai < analyses.length) {
2548
+ step.result.analysis = analyses[ai]?.output?.analysis || analyses[ai]?.output || "";
2549
+ ai++;
2550
+ }
2551
+ }
2552
+ } catch {
2553
+ log.debug("Failed to parse batch analysis result", {
2554
+ batchResult
2555
+ });
2556
+ }
2557
+ return JSON.stringify(parsed);
2558
+ }
2559
+ } catch {
2560
+ }
2561
+ }
2562
+ return result2;
2563
+ }
2409
2564
  });
2410
2565
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2411
2566
  return result.text;
@@ -2413,13 +2568,14 @@ var browserAutomationTool = {
2413
2568
  };
2414
2569
 
2415
2570
  // src/subagents/designExpert/tools.ts
2416
- var DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design reference. Assess:
2417
- 1) Mood/aesthetic
2418
- 2) Color palette with approximate hex values and palette strategy
2419
- 3) Typography style
2420
- 4) Layout composition (symmetric/asymmetric, grid structure, whitespace usage, content density)
2421
- 5) What makes it distinctive and interesting vs generic AI-generated interfaces
2422
- Be specific and concise.`;
2571
+ import fs14 from "fs";
2572
+ import path8 from "path";
2573
+ var base2 = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
2574
+ function resolvePath(filename) {
2575
+ const local4 = path8.join(base2, filename);
2576
+ return fs14.existsSync(local4) ? local4 : path8.join(base2, "subagents", "designExpert", filename);
2577
+ }
2578
+ var DESIGN_REFERENCE_PROMPT = fs14.readFileSync(resolvePath("prompts/tool-prompts/design-analysis.md"), "utf-8").trim();
2423
2579
  var DESIGN_EXPERT_TOOLS = [
2424
2580
  {
2425
2581
  name: "searchGoogle",
@@ -2473,24 +2629,33 @@ var DESIGN_EXPERT_TOOLS = [
2473
2629
  },
2474
2630
  {
2475
2631
  name: "screenshot",
2476
- description: "Capture a screenshot of the app preview. Returns a CDN URL. Use to review the current state of the UI being built.",
2632
+ description: "Capture a screenshot of the app preview. Returns a CDN URL with visual analysis. Use to review the current state of the UI being built. Set viewportOnly to capture just what the user sees on screen.",
2477
2633
  inputSchema: {
2478
2634
  type: "object",
2479
- properties: {}
2635
+ properties: {
2636
+ prompt: {
2637
+ type: "string",
2638
+ description: "Optional specific question about the screenshot."
2639
+ },
2640
+ viewportOnly: {
2641
+ type: "boolean",
2642
+ description: "Capture only the visible viewport instead of the full scrollable page. Use when checking above-the-fold layout or viewport-relative sizing like 100vh."
2643
+ }
2644
+ }
2480
2645
  }
2481
2646
  },
2482
2647
  {
2483
- name: "searchProductScreenshots",
2484
- description: 'Search for screenshots of real products and apps. Use to find what existing products look like ("stripe dashboard", "linear app", "notion workspace"). Returns image results of actual product UI. Use this for layout and design research on real products, NOT for abstract design inspiration.',
2648
+ name: "runBrowserTest",
2649
+ description: "Run an automated browser test against the live app preview. Use to verify visual implementation: check computed styles, navigate between pages, take analyzed screenshots. Describe what you want to verify and the browser agent handles the interaction.",
2485
2650
  inputSchema: {
2486
2651
  type: "object",
2487
2652
  properties: {
2488
- product: {
2653
+ task: {
2489
2654
  type: "string",
2490
- description: 'The product or app to find screenshots of (e.g., "stripe dashboard", "figma editor", "mercury banking app").'
2655
+ description: 'What to verify, in natural language. E.g., "Check that the hero section cards have border-radius: 24px and the correct rotation angles" or "Navigate to /about and screenshot it".'
2491
2656
  }
2492
2657
  },
2493
- required: ["product"]
2658
+ required: ["task"]
2494
2659
  }
2495
2660
  },
2496
2661
  {
@@ -2519,22 +2684,14 @@ var DESIGN_EXPERT_TOOLS = [
2519
2684
  }
2520
2685
  }
2521
2686
  ];
2522
- async function executeDesignExpertTool(name, input) {
2687
+ async function executeDesignExpertTool(name, input, context) {
2523
2688
  switch (name) {
2524
2689
  case "screenshot": {
2525
2690
  try {
2526
- const { url } = await sidecarRequest(
2527
- "/screenshot",
2528
- {},
2529
- { timeout: 12e4 }
2530
- );
2531
- const analysisPrompt = input.prompt || "Describe this app screenshot for a visual designer reviewing the current state. What is visible: layout, typography, colors, spacing, imagery. Note anything that looks broken or off. Be concise.";
2532
- const analysis = await runCli(
2533
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2534
- );
2535
- return `Screenshot: ${url}
2536
-
2537
- ${analysis}`;
2691
+ return await captureAndAnalyzeScreenshot({
2692
+ prompt: input.prompt,
2693
+ viewportOnly: input.viewportOnly
2694
+ });
2538
2695
  } catch (err) {
2539
2696
  return `Error taking screenshot: ${err.message}`;
2540
2697
  }
@@ -2573,12 +2730,6 @@ ${analysis}`;
2573
2730
 
2574
2731
  ${analysis}`;
2575
2732
  }
2576
- case "searchProductScreenshots": {
2577
- const query = `${input.product} product screenshot UI 2026`;
2578
- return runCli(
2579
- `mindstudio search-google-images --query ${JSON.stringify(query)} --export-type json --output-key images --no-meta`
2580
- );
2581
- }
2582
2733
  case "generateImages": {
2583
2734
  const prompts = input.prompts;
2584
2735
  const width = input.width || 2048;
@@ -2594,7 +2745,8 @@ ${analysis}`;
2594
2745
  }
2595
2746
  });
2596
2747
  const url = await runCli(
2597
- `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`
2748
+ `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`,
2749
+ { jsonLogs: true }
2598
2750
  );
2599
2751
  imageUrls = [url];
2600
2752
  } else {
@@ -2609,7 +2761,8 @@ ${analysis}`;
2609
2761
  }
2610
2762
  }));
2611
2763
  const batchResult = await runCli(
2612
- `mindstudio batch '${JSON.stringify(steps)}' --no-meta`
2764
+ `mindstudio batch '${JSON.stringify(steps)}' --no-meta`,
2765
+ { jsonLogs: true }
2613
2766
  );
2614
2767
  try {
2615
2768
  const parsed = JSON.parse(batchResult);
@@ -2620,20 +2773,24 @@ ${analysis}`;
2620
2773
  return batchResult;
2621
2774
  }
2622
2775
  }
2623
- const analyses = await Promise.all(
2776
+ const images = await Promise.all(
2624
2777
  imageUrls.map(async (url, i) => {
2625
2778
  if (url.startsWith("Error")) {
2626
- return `Image ${i + 1}: ${url}`;
2779
+ return { prompt: prompts[i], error: url };
2627
2780
  }
2628
2781
  const analysis = await runCli(
2629
2782
  `mindstudio analyze-image --prompt ${JSON.stringify(ANALYZE_PROMPT)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2630
2783
  );
2631
- return `**Image ${i + 1}:** ${url}
2632
- Prompt: ${prompts[i]}
2633
- Analysis: ${analysis}`;
2784
+ return { url, prompt: prompts[i], analysis, width, height };
2634
2785
  })
2635
2786
  );
2636
- return analyses.join("\n\n");
2787
+ return `%%JSON%%${JSON.stringify({ images })}`;
2788
+ }
2789
+ case "runBrowserTest": {
2790
+ if (!context) {
2791
+ return "Error: browser testing requires execution context (only available in headless mode)";
2792
+ }
2793
+ return browserAutomationTool.execute({ task: input.task }, context);
2637
2794
  }
2638
2795
  default:
2639
2796
  return `Error: unknown tool "${name}"`;
@@ -2641,17 +2798,17 @@ Analysis: ${analysis}`;
2641
2798
  }
2642
2799
 
2643
2800
  // src/subagents/designExpert/prompt.ts
2644
- import fs15 from "fs";
2645
- import path9 from "path";
2801
+ import fs16 from "fs";
2802
+ import path10 from "path";
2646
2803
 
2647
2804
  // src/subagents/common/context.ts
2648
- import fs14 from "fs";
2649
- import path8 from "path";
2805
+ import fs15 from "fs";
2806
+ import path9 from "path";
2650
2807
  function walkMdFiles2(dir, skip) {
2651
2808
  const files = [];
2652
2809
  try {
2653
- for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
2654
- const full = path8.join(dir, entry.name);
2810
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
2811
+ const full = path9.join(dir, entry.name);
2655
2812
  if (entry.isDirectory()) {
2656
2813
  if (!skip?.has(entry.name)) {
2657
2814
  files.push(...walkMdFiles2(full, skip));
@@ -2671,7 +2828,7 @@ function loadFilesAsXml(dir, tag, skip) {
2671
2828
  }
2672
2829
  const sections = files.map((f) => {
2673
2830
  try {
2674
- const content = fs14.readFileSync(f, "utf-8").trim();
2831
+ const content = fs15.readFileSync(f, "utf-8").trim();
2675
2832
  return `<file path="${f}">
2676
2833
  ${content}
2677
2834
  </file>`;
@@ -2741,17 +2898,17 @@ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (Op
2741
2898
  }
2742
2899
 
2743
2900
  // src/subagents/designExpert/prompt.ts
2744
- var base2 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
2745
- function resolvePath(filename) {
2746
- const local4 = path9.join(base2, filename);
2747
- return fs15.existsSync(local4) ? local4 : path9.join(base2, "subagents", "designExpert", filename);
2901
+ var base3 = import.meta.dirname ?? path10.dirname(new URL(import.meta.url).pathname);
2902
+ function resolvePath2(filename) {
2903
+ const local4 = path10.join(base3, filename);
2904
+ return fs16.existsSync(local4) ? local4 : path10.join(base3, "subagents", "designExpert", filename);
2748
2905
  }
2749
2906
  function readFile(filename) {
2750
- return fs15.readFileSync(resolvePath(filename), "utf-8").trim();
2907
+ return fs16.readFileSync(resolvePath2(filename), "utf-8").trim();
2751
2908
  }
2752
2909
  function readJson(filename, fallback) {
2753
2910
  try {
2754
- return JSON.parse(fs15.readFileSync(resolvePath(filename), "utf-8"));
2911
+ return JSON.parse(fs16.readFileSync(resolvePath2(filename), "utf-8"));
2755
2912
  } catch {
2756
2913
  return fallback;
2757
2914
  }
@@ -2788,7 +2945,6 @@ function getDesignExpertPrompt() {
2788
2945
  const pairings = sample(fontData.pairings, 20);
2789
2946
  const images = sample(inspirationImages, 15);
2790
2947
  const fontList = fonts.map((f) => {
2791
- const tags = f.tags.length ? ` (${f.tags.join(", ")})` : "";
2792
2948
  let cssInfo = "";
2793
2949
  if (f.source === "fontshare") {
2794
2950
  cssInfo = ` CSS: ${fontData.cssUrlPattern.replace("{slug}", f.slug).replace("{weights}", f.weights.join(","))}`;
@@ -2797,7 +2953,8 @@ function getDesignExpertPrompt() {
2797
2953
  } else if (f.source === "open-foundry") {
2798
2954
  cssInfo = " (self-host required)";
2799
2955
  }
2800
- return `- **${f.name}** \u2014 ${f.category}${tags}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}`;
2956
+ const desc = f.description ? ` ${f.description}` : "";
2957
+ return `- **${f.name}** \u2014 ${f.category}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}${desc}`;
2801
2958
  }).join("\n");
2802
2959
  const pairingList = pairings.map(
2803
2960
  (p) => `- **${p.heading.font}** (${p.heading.weight}) heading + **${p.body.font}** (${p.body.weight}) body`
@@ -2815,13 +2972,13 @@ ${fontList}
2815
2972
  ${pairingList}
2816
2973
  </fonts_to_consider>` : "";
2817
2974
  const imageList = images.map((img) => `- ${img.analysis}`).join("\n\n");
2818
- const inspirationSection = images.length ? `<inspiration_images>
2975
+ const inspirationSection = images.length ? `<design_inspiration>
2819
2976
  ## Design inspiration
2820
2977
 
2821
- This is what the bar looks like. These are real sites that made it onto curated design galleries because they did something bold, intentional, and memorable. Study the moves they make \u2014 the confident color choices, the unexpected layouts, the typography that carries the whole page. Your recommendations should feel like they belong in this company.
2978
+ This is what the bar looks like. These are real sites that made it onto curated design galleries because they did something bold, intentional, and memorable. Use them as inspiration and let the takeaways guide your work. Your designs should feel like they belong in this company.
2822
2979
 
2823
2980
  ${imageList}
2824
- </inspiration_images>` : "";
2981
+ </design_inspiration>` : "";
2825
2982
  const specContext = loadSpecContext();
2826
2983
  let prompt = PROMPT_TEMPLATE.replace(
2827
2984
  "{{fonts_to_consider}}",
@@ -2863,7 +3020,7 @@ var designExpertTool = {
2863
3020
  task: input.task,
2864
3021
  tools: DESIGN_EXPERT_TOOLS,
2865
3022
  externalTools: /* @__PURE__ */ new Set(),
2866
- executeTool: executeDesignExpertTool,
3023
+ executeTool: (name, input2) => executeDesignExpertTool(name, input2, context),
2867
3024
  apiConfig: context.apiConfig,
2868
3025
  model: context.model,
2869
3026
  subAgentId: "visualDesignExpert",
@@ -2976,8 +3133,8 @@ var VISION_TOOLS = [
2976
3133
  ];
2977
3134
 
2978
3135
  // src/subagents/productVision/executor.ts
2979
- import fs16 from "fs";
2980
- import path10 from "path";
3136
+ import fs17 from "fs";
3137
+ import path11 from "path";
2981
3138
  var ROADMAP_DIR = "src/roadmap";
2982
3139
  function formatRequires(requires) {
2983
3140
  return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
@@ -2993,9 +3150,10 @@ async function executeVisionTool(name, input) {
2993
3150
  requires,
2994
3151
  body
2995
3152
  } = input;
2996
- const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3153
+ const filePath = path11.join(ROADMAP_DIR, `${slug}.md`);
2997
3154
  try {
2998
- fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
3155
+ fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
3156
+ const oldContent = fs17.existsSync(filePath) ? fs17.readFileSync(filePath, "utf-8") : "";
2999
3157
  const content = `---
3000
3158
  name: ${itemName}
3001
3159
  type: roadmap
@@ -3007,20 +3165,24 @@ requires: ${formatRequires(requires)}
3007
3165
 
3008
3166
  ${body}
3009
3167
  `;
3010
- fs16.writeFileSync(filePath, content, "utf-8");
3011
- return `Wrote ${filePath}`;
3168
+ fs17.writeFileSync(filePath, content, "utf-8");
3169
+ const lineCount = content.split("\n").length;
3170
+ const label = oldContent ? "Updated" : "Wrote";
3171
+ return `${label} ${filePath} (${lineCount} lines)
3172
+ ${unifiedDiff(filePath, oldContent, content)}`;
3012
3173
  } catch (err) {
3013
3174
  return `Error writing ${filePath}: ${err.message}`;
3014
3175
  }
3015
3176
  }
3016
3177
  case "updateRoadmapItem": {
3017
3178
  const { slug } = input;
3018
- const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3179
+ const filePath = path11.join(ROADMAP_DIR, `${slug}.md`);
3019
3180
  try {
3020
- if (!fs16.existsSync(filePath)) {
3181
+ if (!fs17.existsSync(filePath)) {
3021
3182
  return `Error: ${filePath} does not exist`;
3022
3183
  }
3023
- let content = fs16.readFileSync(filePath, "utf-8");
3184
+ const oldContent = fs17.readFileSync(filePath, "utf-8");
3185
+ let content = oldContent;
3024
3186
  if (input.status) {
3025
3187
  content = content.replace(
3026
3188
  /^status:\s*.+$/m,
@@ -3072,21 +3234,25 @@ ${input.appendHistory}
3072
3234
  `;
3073
3235
  }
3074
3236
  }
3075
- fs16.writeFileSync(filePath, content, "utf-8");
3076
- return `Updated ${filePath}`;
3237
+ fs17.writeFileSync(filePath, content, "utf-8");
3238
+ const lineCount = content.split("\n").length;
3239
+ return `Updated ${filePath} (${lineCount} lines)
3240
+ ${unifiedDiff(filePath, oldContent, content)}`;
3077
3241
  } catch (err) {
3078
3242
  return `Error updating ${filePath}: ${err.message}`;
3079
3243
  }
3080
3244
  }
3081
3245
  case "deleteRoadmapItem": {
3082
3246
  const { slug } = input;
3083
- const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3247
+ const filePath = path11.join(ROADMAP_DIR, `${slug}.md`);
3084
3248
  try {
3085
- if (!fs16.existsSync(filePath)) {
3249
+ if (!fs17.existsSync(filePath)) {
3086
3250
  return `Error: ${filePath} does not exist`;
3087
3251
  }
3088
- fs16.unlinkSync(filePath);
3089
- return `Deleted ${filePath}`;
3252
+ const oldContent = fs17.readFileSync(filePath, "utf-8");
3253
+ fs17.unlinkSync(filePath);
3254
+ return `Deleted ${filePath}
3255
+ ${unifiedDiff(filePath, oldContent, "")}`;
3090
3256
  } catch (err) {
3091
3257
  return `Error deleting ${filePath}: ${err.message}`;
3092
3258
  }
@@ -3097,12 +3263,12 @@ ${input.appendHistory}
3097
3263
  }
3098
3264
 
3099
3265
  // src/subagents/productVision/prompt.ts
3100
- import fs17 from "fs";
3101
- import path11 from "path";
3102
- var base3 = import.meta.dirname ?? path11.dirname(new URL(import.meta.url).pathname);
3103
- var local2 = path11.join(base3, "prompt.md");
3104
- var PROMPT_PATH2 = fs17.existsSync(local2) ? local2 : path11.join(base3, "subagents", "productVision", "prompt.md");
3105
- var BASE_PROMPT2 = fs17.readFileSync(PROMPT_PATH2, "utf-8").trim();
3266
+ import fs18 from "fs";
3267
+ import path12 from "path";
3268
+ var base4 = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
3269
+ var local2 = path12.join(base4, "prompt.md");
3270
+ var PROMPT_PATH2 = fs18.existsSync(local2) ? local2 : path12.join(base4, "subagents", "productVision", "prompt.md");
3271
+ var BASE_PROMPT2 = fs18.readFileSync(PROMPT_PATH2, "utf-8").trim();
3106
3272
  function getProductVisionPrompt() {
3107
3273
  const specContext = loadSpecContext();
3108
3274
  const roadmapContext = loadRoadmapContext();
@@ -3156,8 +3322,8 @@ var productVisionTool = {
3156
3322
  };
3157
3323
 
3158
3324
  // src/subagents/codeSanityCheck/index.ts
3159
- import fs18 from "fs";
3160
- import path12 from "path";
3325
+ import fs19 from "fs";
3326
+ import path13 from "path";
3161
3327
 
3162
3328
  // src/subagents/codeSanityCheck/tools.ts
3163
3329
  var SANITY_CHECK_TOOLS = [
@@ -3251,10 +3417,10 @@ var SANITY_CHECK_TOOLS = [
3251
3417
  ];
3252
3418
 
3253
3419
  // src/subagents/codeSanityCheck/index.ts
3254
- var base4 = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
3255
- var local3 = path12.join(base4, "prompt.md");
3256
- var PROMPT_PATH3 = fs18.existsSync(local3) ? local3 : path12.join(base4, "subagents", "codeSanityCheck", "prompt.md");
3257
- var BASE_PROMPT3 = fs18.readFileSync(PROMPT_PATH3, "utf-8").trim();
3420
+ var base5 = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
3421
+ var local3 = path13.join(base5, "prompt.md");
3422
+ var PROMPT_PATH3 = fs19.existsSync(local3) ? local3 : path13.join(base5, "subagents", "codeSanityCheck", "prompt.md");
3423
+ var BASE_PROMPT3 = fs19.readFileSync(PROMPT_PATH3, "utf-8").trim();
3258
3424
  var codeSanityCheckTool = {
3259
3425
  definition: {
3260
3426
  name: "codeSanityCheck",
@@ -3381,11 +3547,11 @@ function executeTool(name, input, context) {
3381
3547
  }
3382
3548
 
3383
3549
  // src/session.ts
3384
- import fs19 from "fs";
3550
+ import fs20 from "fs";
3385
3551
  var SESSION_FILE = ".remy-session.json";
3386
3552
  function loadSession(state) {
3387
3553
  try {
3388
- const raw = fs19.readFileSync(SESSION_FILE, "utf-8");
3554
+ const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
3389
3555
  const data = JSON.parse(raw);
3390
3556
  if (Array.isArray(data.messages) && data.messages.length > 0) {
3391
3557
  state.messages = sanitizeMessages(data.messages);
@@ -3433,7 +3599,7 @@ function sanitizeMessages(messages) {
3433
3599
  }
3434
3600
  function saveSession(state) {
3435
3601
  try {
3436
- fs19.writeFileSync(
3602
+ fs20.writeFileSync(
3437
3603
  SESSION_FILE,
3438
3604
  JSON.stringify({ messages: state.messages }, null, 2),
3439
3605
  "utf-8"
@@ -3444,7 +3610,7 @@ function saveSession(state) {
3444
3610
  function clearSession(state) {
3445
3611
  state.messages = [];
3446
3612
  try {
3447
- fs19.unlinkSync(SESSION_FILE);
3613
+ fs20.unlinkSync(SESSION_FILE);
3448
3614
  } catch {
3449
3615
  }
3450
3616
  }
@@ -4151,13 +4317,46 @@ async function runTurn(params) {
4151
4317
  }
4152
4318
 
4153
4319
  // src/headless.ts
4154
- var BASE_DIR = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
4155
- var ACTIONS_DIR = path13.join(BASE_DIR, "actions");
4320
+ var BASE_DIR = import.meta.dirname ?? path14.dirname(new URL(import.meta.url).pathname);
4321
+ var ACTIONS_DIR = path14.join(BASE_DIR, "actions");
4156
4322
  function loadActionPrompt(name) {
4157
- return fs20.readFileSync(path13.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4323
+ return fs21.readFileSync(path14.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4324
+ }
4325
+ function emit(event, data, requestId) {
4326
+ const payload = { event, ...data };
4327
+ if (requestId) {
4328
+ payload.requestId = requestId;
4329
+ }
4330
+ process.stdout.write(JSON.stringify(payload) + "\n");
4158
4331
  }
4159
- function emit(event, data) {
4160
- process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
4332
+ function handleGetHistory(state) {
4333
+ return { messages: state.messages };
4334
+ }
4335
+ function handleClear(state) {
4336
+ clearSession(state);
4337
+ return {};
4338
+ }
4339
+ function handleCancel(currentAbort, pendingTools) {
4340
+ if (currentAbort) {
4341
+ currentAbort.abort();
4342
+ }
4343
+ for (const [id, pending] of pendingTools) {
4344
+ clearTimeout(pending.timeout);
4345
+ pending.resolve("Error: cancelled");
4346
+ pendingTools.delete(id);
4347
+ }
4348
+ return {};
4349
+ }
4350
+ function dispatchSimple(requestId, eventName, handler) {
4351
+ try {
4352
+ const data = handler();
4353
+ if (eventName) {
4354
+ emit(eventName, data, requestId);
4355
+ }
4356
+ emit("completed", { success: true }, requestId);
4357
+ } catch (err) {
4358
+ emit("completed", { success: false, error: err.message }, requestId);
4359
+ }
4161
4360
  }
4162
4361
  async function startHeadless(opts = {}) {
4163
4362
  const stderrWrite = (...args) => {
@@ -4176,72 +4375,15 @@ async function startHeadless(opts = {}) {
4176
4375
  const state = createAgentState();
4177
4376
  const resumed = loadSession(state);
4178
4377
  if (resumed) {
4179
- emit("session_restored", {
4180
- messageCount: state.messages.length
4181
- });
4378
+ emit("session_restored", { messageCount: state.messages.length });
4182
4379
  }
4183
4380
  let running = false;
4184
4381
  let currentAbort = null;
4382
+ let currentRequestId;
4383
+ let completedEmitted = false;
4185
4384
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
4186
4385
  const pendingTools = /* @__PURE__ */ new Map();
4187
4386
  const earlyResults = /* @__PURE__ */ new Map();
4188
- function onEvent(e) {
4189
- switch (e.type) {
4190
- case "text":
4191
- emit("text", {
4192
- text: e.text,
4193
- ...e.parentToolId && { parentToolId: e.parentToolId }
4194
- });
4195
- break;
4196
- case "thinking":
4197
- emit("thinking", {
4198
- text: e.text,
4199
- ...e.parentToolId && { parentToolId: e.parentToolId }
4200
- });
4201
- break;
4202
- case "tool_input_delta":
4203
- emit("tool_input_delta", {
4204
- id: e.id,
4205
- name: e.name,
4206
- result: e.result,
4207
- ...e.parentToolId && { parentToolId: e.parentToolId }
4208
- });
4209
- break;
4210
- case "tool_start":
4211
- emit("tool_start", {
4212
- id: e.id,
4213
- name: e.name,
4214
- input: e.input,
4215
- ...e.partial && { partial: true },
4216
- ...e.parentToolId && { parentToolId: e.parentToolId }
4217
- });
4218
- break;
4219
- case "tool_done":
4220
- emit("tool_done", {
4221
- id: e.id,
4222
- name: e.name,
4223
- result: e.result,
4224
- isError: e.isError,
4225
- ...e.parentToolId && { parentToolId: e.parentToolId }
4226
- });
4227
- break;
4228
- case "turn_started":
4229
- emit("turn_started");
4230
- break;
4231
- case "turn_done":
4232
- emit("turn_done");
4233
- break;
4234
- case "turn_cancelled":
4235
- emit("turn_cancelled");
4236
- break;
4237
- case "error":
4238
- emit("error", { error: e.error });
4239
- break;
4240
- case "status":
4241
- emit("status", { message: e.message });
4242
- break;
4243
- }
4244
- }
4245
4387
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4246
4388
  "promptUser",
4247
4389
  "confirmDestructiveAction",
@@ -4272,6 +4414,158 @@ async function startHeadless(opts = {}) {
4272
4414
  });
4273
4415
  });
4274
4416
  }
4417
+ function onEvent(e) {
4418
+ const rid = currentRequestId;
4419
+ switch (e.type) {
4420
+ // Suppressed — caller already knows the request started
4421
+ case "turn_started":
4422
+ return;
4423
+ // Terminal events — translate to `completed`
4424
+ case "turn_done":
4425
+ completedEmitted = true;
4426
+ emit("completed", { success: true }, rid);
4427
+ return;
4428
+ case "turn_cancelled":
4429
+ completedEmitted = true;
4430
+ emit("completed", { success: false, error: "cancelled" }, rid);
4431
+ return;
4432
+ // Streaming events — forward with requestId
4433
+ case "text":
4434
+ emit(
4435
+ "text",
4436
+ {
4437
+ text: e.text,
4438
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4439
+ },
4440
+ rid
4441
+ );
4442
+ return;
4443
+ case "thinking":
4444
+ emit(
4445
+ "thinking",
4446
+ {
4447
+ text: e.text,
4448
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4449
+ },
4450
+ rid
4451
+ );
4452
+ return;
4453
+ case "tool_input_delta":
4454
+ emit(
4455
+ "tool_input_delta",
4456
+ {
4457
+ id: e.id,
4458
+ name: e.name,
4459
+ result: e.result,
4460
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4461
+ },
4462
+ rid
4463
+ );
4464
+ return;
4465
+ case "tool_start":
4466
+ emit(
4467
+ "tool_start",
4468
+ {
4469
+ id: e.id,
4470
+ name: e.name,
4471
+ input: e.input,
4472
+ ...e.partial && { partial: true },
4473
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4474
+ },
4475
+ rid
4476
+ );
4477
+ return;
4478
+ case "tool_done":
4479
+ emit(
4480
+ "tool_done",
4481
+ {
4482
+ id: e.id,
4483
+ name: e.name,
4484
+ result: e.result,
4485
+ isError: e.isError,
4486
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4487
+ },
4488
+ rid
4489
+ );
4490
+ return;
4491
+ case "status":
4492
+ emit("status", { message: e.message }, rid);
4493
+ return;
4494
+ case "error":
4495
+ emit("error", { error: e.error }, rid);
4496
+ return;
4497
+ }
4498
+ }
4499
+ async function handleMessage(parsed, requestId) {
4500
+ if (running) {
4501
+ emit(
4502
+ "error",
4503
+ { error: "Agent is already processing a message" },
4504
+ requestId
4505
+ );
4506
+ emit(
4507
+ "completed",
4508
+ { success: false, error: "Agent is already processing a message" },
4509
+ requestId
4510
+ );
4511
+ return;
4512
+ }
4513
+ running = true;
4514
+ currentRequestId = requestId;
4515
+ currentAbort = new AbortController();
4516
+ completedEmitted = false;
4517
+ const attachments = parsed.attachments;
4518
+ if (attachments?.length) {
4519
+ console.warn(
4520
+ `[headless] Message has ${attachments.length} attachment(s):`,
4521
+ attachments.map((a) => a.url)
4522
+ );
4523
+ }
4524
+ let userMessage = parsed.text ?? "";
4525
+ const isCommand = !!parsed.runCommand;
4526
+ if (parsed.runCommand === "sync") {
4527
+ userMessage = loadActionPrompt("sync");
4528
+ } else if (parsed.runCommand === "publish") {
4529
+ userMessage = loadActionPrompt("publish");
4530
+ } else if (parsed.runCommand === "buildFromInitialSpec") {
4531
+ userMessage = loadActionPrompt("buildFromInitialSpec");
4532
+ }
4533
+ const onboardingState = parsed.onboardingState ?? "onboardingFinished";
4534
+ const system = buildSystemPrompt(
4535
+ onboardingState,
4536
+ parsed.viewContext
4537
+ );
4538
+ try {
4539
+ await runTurn({
4540
+ state,
4541
+ userMessage,
4542
+ attachments,
4543
+ apiConfig: config,
4544
+ system,
4545
+ model: opts.model,
4546
+ onboardingState,
4547
+ signal: currentAbort.signal,
4548
+ onEvent,
4549
+ resolveExternalTool,
4550
+ hidden: isCommand
4551
+ });
4552
+ if (!completedEmitted) {
4553
+ emit(
4554
+ "completed",
4555
+ { success: false, error: "Turn ended unexpectedly" },
4556
+ requestId
4557
+ );
4558
+ }
4559
+ } catch (err) {
4560
+ if (!completedEmitted) {
4561
+ emit("error", { error: err.message }, requestId);
4562
+ emit("completed", { success: false, error: err.message }, requestId);
4563
+ }
4564
+ }
4565
+ currentAbort = null;
4566
+ currentRequestId = void 0;
4567
+ running = false;
4568
+ }
4275
4569
  const rl = createInterface({ input: process.stdin });
4276
4570
  rl.on("line", async (line) => {
4277
4571
  let parsed;
@@ -4281,82 +4575,42 @@ async function startHeadless(opts = {}) {
4281
4575
  emit("error", { error: "Invalid JSON on stdin" });
4282
4576
  return;
4283
4577
  }
4284
- if (parsed.action === "tool_result" && parsed.id) {
4285
- const pending = pendingTools.get(parsed.id);
4578
+ const { action, requestId } = parsed;
4579
+ if (action === "tool_result" && parsed.id) {
4580
+ const id = parsed.id;
4581
+ const result = parsed.result ?? "";
4582
+ const pending = pendingTools.get(id);
4286
4583
  if (pending) {
4287
- pendingTools.delete(parsed.id);
4288
- pending.resolve(parsed.result ?? "");
4584
+ pendingTools.delete(id);
4585
+ pending.resolve(result);
4289
4586
  } else {
4290
- earlyResults.set(parsed.id, parsed.result ?? "");
4587
+ earlyResults.set(id, result);
4291
4588
  }
4292
4589
  return;
4293
4590
  }
4294
- if (parsed.action === "get_history") {
4295
- emit("history", {
4296
- messages: state.messages
4297
- });
4591
+ if (action === "get_history") {
4592
+ dispatchSimple(requestId, "history", () => handleGetHistory(state));
4298
4593
  return;
4299
4594
  }
4300
- if (parsed.action === "clear") {
4301
- clearSession(state);
4302
- emit("session_cleared");
4595
+ if (action === "clear") {
4596
+ dispatchSimple(requestId, "session_cleared", () => handleClear(state));
4303
4597
  return;
4304
4598
  }
4305
- if (parsed.action === "cancel") {
4306
- if (currentAbort) {
4307
- currentAbort.abort();
4308
- }
4309
- for (const [id, pending] of pendingTools) {
4310
- clearTimeout(pending.timeout);
4311
- pending.resolve("Error: cancelled");
4312
- pendingTools.delete(id);
4313
- }
4599
+ if (action === "cancel") {
4600
+ handleCancel(currentAbort, pendingTools);
4601
+ emit("completed", { success: true }, requestId);
4314
4602
  return;
4315
4603
  }
4316
- if (parsed.action === "message" && (parsed.text || parsed.runCommand)) {
4317
- if (running) {
4318
- emit("error", { error: "Agent is already processing a message" });
4319
- return;
4320
- }
4321
- running = true;
4322
- currentAbort = new AbortController();
4323
- if (parsed.attachments?.length) {
4324
- console.warn(
4325
- `[headless] Message has ${parsed.attachments.length} attachment(s):`,
4326
- parsed.attachments.map((a) => a.url)
4327
- );
4328
- }
4329
- let userMessage = parsed.text ?? "";
4330
- const isCommand = !!parsed.runCommand;
4331
- if (parsed.runCommand === "sync") {
4332
- userMessage = loadActionPrompt("sync");
4333
- } else if (parsed.runCommand === "publish") {
4334
- userMessage = loadActionPrompt("publish");
4335
- } else if (parsed.runCommand === "buildFromInitialSpec") {
4336
- userMessage = loadActionPrompt("buildFromInitialSpec");
4337
- }
4338
- const onboardingState = parsed.onboardingState ?? "onboardingFinished";
4339
- const system = buildSystemPrompt(onboardingState, parsed.viewContext);
4340
- try {
4341
- await runTurn({
4342
- state,
4343
- userMessage,
4344
- attachments: parsed.attachments,
4345
- apiConfig: config,
4346
- system,
4347
- model: opts.model,
4348
- onboardingState,
4349
- signal: currentAbort.signal,
4350
- onEvent,
4351
- resolveExternalTool,
4352
- hidden: isCommand
4353
- });
4354
- } catch (err) {
4355
- emit("error", { error: err.message });
4356
- }
4357
- currentAbort = null;
4358
- running = false;
4604
+ if (action === "message") {
4605
+ await handleMessage(parsed, requestId);
4606
+ return;
4359
4607
  }
4608
+ emit("error", { error: `Unknown action: ${action}` }, requestId);
4609
+ emit(
4610
+ "completed",
4611
+ { success: false, error: `Unknown action: ${action}` },
4612
+ requestId
4613
+ );
4360
4614
  });
4361
4615
  rl.on("close", () => {
4362
4616
  emit("stopping");