@mindstudio-ai/remy 0.1.8 → 0.1.10

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 (50) hide show
  1. package/dist/compiled/design.md +34 -38
  2. package/dist/compiled/media-cdn.md +2 -2
  3. package/dist/compiled/msfm.md +58 -2
  4. package/dist/compiled/platform.md +4 -2
  5. package/dist/headless.js +881 -140
  6. package/dist/index.js +1097 -298
  7. package/dist/prompt/.notes.md +138 -0
  8. package/dist/prompt/actions/buildFromInitialSpec.md +7 -0
  9. package/dist/prompt/actions/publish.md +12 -0
  10. package/dist/prompt/actions/sync.md +19 -0
  11. package/dist/prompt/compiled/README.md +100 -0
  12. package/dist/prompt/compiled/auth.md +77 -0
  13. package/dist/prompt/compiled/design.md +250 -0
  14. package/dist/prompt/compiled/dev-and-deploy.md +69 -0
  15. package/dist/prompt/compiled/interfaces.md +238 -0
  16. package/dist/prompt/compiled/manifest.md +107 -0
  17. package/dist/prompt/compiled/media-cdn.md +51 -0
  18. package/dist/prompt/compiled/methods.md +225 -0
  19. package/dist/prompt/compiled/msfm.md +189 -0
  20. package/dist/prompt/compiled/platform.md +103 -0
  21. package/dist/prompt/compiled/scenarios.md +103 -0
  22. package/dist/prompt/compiled/sdk-actions.md +146 -0
  23. package/dist/prompt/compiled/tables.md +211 -0
  24. package/dist/prompt/sources/frontend-design-notes.md +162 -0
  25. package/dist/prompt/sources/media-cdn.md +46 -0
  26. package/dist/prompt/static/authoring.md +57 -0
  27. package/dist/prompt/static/coding.md +29 -0
  28. package/dist/prompt/static/identity.md +1 -0
  29. package/dist/prompt/static/instructions.md +29 -0
  30. package/dist/prompt/static/intake.md +44 -0
  31. package/dist/prompt/static/lsp.md +4 -0
  32. package/dist/static/authoring.md +6 -2
  33. package/dist/static/instructions.md +2 -1
  34. package/dist/static/projectContext.ts +9 -4
  35. package/dist/subagents/browserAutomation/prompt.md +2 -1
  36. package/dist/subagents/designExpert/.notes.md +253 -0
  37. package/dist/subagents/designExpert/data/compile-inspiration.sh +126 -0
  38. package/dist/subagents/designExpert/data/fonts.json +2855 -0
  39. package/dist/subagents/designExpert/data/inspiration.json +540 -0
  40. package/dist/subagents/designExpert/data/inspiration.raw.json +112 -0
  41. package/dist/subagents/designExpert/prompt.md +19 -0
  42. package/dist/subagents/designExpert/prompts/animation.md +19 -0
  43. package/dist/subagents/designExpert/prompts/color.md +35 -0
  44. package/dist/subagents/designExpert/prompts/frontend-design-notes.md +162 -0
  45. package/dist/subagents/designExpert/prompts/icons.md +27 -0
  46. package/dist/subagents/designExpert/prompts/identity.md +71 -0
  47. package/dist/subagents/designExpert/prompts/images.md +50 -0
  48. package/dist/subagents/designExpert/prompts/instructions.md +16 -0
  49. package/dist/subagents/designExpert/prompts/layout.md +34 -0
  50. 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 fs15 from "fs";
4
- import path8 from "path";
3
+ import fs16 from "fs";
4
+ import path9 from "path";
5
5
 
6
6
  // src/config.ts
7
7
  import fs2 from "fs";
@@ -194,11 +194,14 @@ function loadSpecFileMetadata() {
194
194
  }
195
195
  const entries = [];
196
196
  for (const filePath of files) {
197
- const { name, description } = parseFrontmatter(filePath);
197
+ const { name, description, type } = parseFrontmatter(filePath);
198
198
  let line = `- ${filePath}`;
199
199
  if (name) {
200
200
  line += ` \u2014 "${name}"`;
201
201
  }
202
+ if (type) {
203
+ line += ` (${type})`;
204
+ }
202
205
  if (description) {
203
206
  line += ` \u2014 ${description}`;
204
207
  }
@@ -232,14 +235,15 @@ function parseFrontmatter(filePath) {
232
235
  const content = fs3.readFileSync(filePath, "utf-8");
233
236
  const match = content.match(/^---\n([\s\S]*?)\n---/);
234
237
  if (!match) {
235
- return { name: "", description: "" };
238
+ return { name: "", description: "", type: "" };
236
239
  }
237
240
  const fm = match[1];
238
241
  const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
239
242
  const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
240
- return { name, description };
243
+ const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
244
+ return { name, description, type };
241
245
  } catch {
242
- return { name: "", description: "" };
246
+ return { name: "", description: "", type: "" };
243
247
  }
244
248
  }
245
249
  function loadProjectFileListing() {
@@ -448,11 +452,35 @@ async function* streamChat(params) {
448
452
  yield { type: "error", error: errorMessage };
449
453
  return;
450
454
  }
455
+ const STALL_TIMEOUT_MS = 3e5;
451
456
  const reader = res.body.getReader();
452
457
  const decoder = new TextDecoder();
453
458
  let buffer = "";
454
459
  while (true) {
455
- const { done, value } = await reader.read();
460
+ let stallTimer;
461
+ let readResult;
462
+ try {
463
+ readResult = await Promise.race([
464
+ reader.read(),
465
+ new Promise((_, reject) => {
466
+ stallTimer = setTimeout(
467
+ () => reject(new Error("stream_stall")),
468
+ STALL_TIMEOUT_MS
469
+ );
470
+ })
471
+ ]);
472
+ clearTimeout(stallTimer);
473
+ } catch {
474
+ clearTimeout(stallTimer);
475
+ await reader.cancel();
476
+ log.error("Stream stalled", { elapsed: `${Date.now() - startTime}ms` });
477
+ yield {
478
+ type: "error",
479
+ error: "Stream stalled \u2014 no data received for 5 minutes"
480
+ };
481
+ return;
482
+ }
483
+ const { done, value } = readResult;
456
484
  if (done) {
457
485
  break;
458
486
  }
@@ -486,6 +514,50 @@ async function* streamChat(params) {
486
514
  }
487
515
  }
488
516
  }
517
+ var MAX_RETRIES = 3;
518
+ var INITIAL_BACKOFF_MS = 1e3;
519
+ function isRetryableError(error) {
520
+ return /Network error/i.test(error) || /HTTP 5\d\d/i.test(error) || /Stream stalled/i.test(error);
521
+ }
522
+ function sleep(ms) {
523
+ return new Promise((resolve) => setTimeout(resolve, ms));
524
+ }
525
+ async function* streamChatWithRetry(params, options) {
526
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
527
+ const buffer = [];
528
+ let retryableFailure = false;
529
+ for await (const event of streamChat(params)) {
530
+ if (event.type === "error") {
531
+ if (isRetryableError(event.error) && attempt < MAX_RETRIES - 1) {
532
+ log.warn("Retryable error, will retry", {
533
+ attempt: attempt + 1,
534
+ maxRetries: MAX_RETRIES,
535
+ error: event.error
536
+ });
537
+ options?.onRetry?.(attempt, event.error);
538
+ retryableFailure = true;
539
+ break;
540
+ }
541
+ yield event;
542
+ return;
543
+ }
544
+ buffer.push(event);
545
+ }
546
+ if (retryableFailure) {
547
+ if (params.signal?.aborted) {
548
+ return;
549
+ }
550
+ const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;
551
+ log.info("Retrying after backoff", { backoffMs: backoff });
552
+ await sleep(backoff);
553
+ continue;
554
+ }
555
+ for (const event of buffer) {
556
+ yield event;
557
+ }
558
+ return;
559
+ }
560
+ }
489
561
 
490
562
  // src/tools/spec/readSpec.ts
491
563
  import fs5 from "fs/promises";
@@ -893,7 +965,88 @@ async function listRecursive(dir) {
893
965
  return results;
894
966
  }
895
967
 
896
- // src/tools/spec/setProjectOnboardingState.ts
968
+ // src/tools/spec/clearSyncStatus.ts
969
+ var clearSyncStatusTool = {
970
+ definition: {
971
+ name: "clearSyncStatus",
972
+ description: "Clear the sync status flags after syncing spec and code. Call this after finishing a sync operation.",
973
+ inputSchema: {
974
+ type: "object",
975
+ properties: {}
976
+ }
977
+ },
978
+ async execute() {
979
+ return "ok";
980
+ }
981
+ };
982
+
983
+ // src/tools/spec/presentSyncPlan.ts
984
+ var presentSyncPlanTool = {
985
+ definition: {
986
+ name: "presentSyncPlan",
987
+ description: "Present a structured sync plan to the user for approval. Write a clear markdown summary of what changed and what you intend to update. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE making any sync edits.",
988
+ inputSchema: {
989
+ type: "object",
990
+ properties: {
991
+ content: {
992
+ type: "string",
993
+ description: "Markdown plan describing what changed and what will be updated."
994
+ }
995
+ },
996
+ required: ["content"]
997
+ }
998
+ },
999
+ streaming: {},
1000
+ async execute() {
1001
+ return "approved";
1002
+ }
1003
+ };
1004
+
1005
+ // src/tools/spec/presentPublishPlan.ts
1006
+ var presentPublishPlanTool = {
1007
+ definition: {
1008
+ name: "presentPublishPlan",
1009
+ description: "Present a publish changelog to the user for approval. Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
1010
+ inputSchema: {
1011
+ type: "object",
1012
+ properties: {
1013
+ content: {
1014
+ type: "string",
1015
+ description: "Markdown changelog describing what changed and what will be deployed."
1016
+ }
1017
+ },
1018
+ required: ["content"]
1019
+ }
1020
+ },
1021
+ streaming: {},
1022
+ async execute() {
1023
+ return "approved";
1024
+ }
1025
+ };
1026
+
1027
+ // src/tools/spec/presentPlan.ts
1028
+ var presentPlanTool = {
1029
+ definition: {
1030
+ name: "presentPlan",
1031
+ description: "Present an implementation plan for user approval before making changes. Use this only for large, multi-step changes or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. If the user rejects with feedback, revise and present again.",
1032
+ inputSchema: {
1033
+ type: "object",
1034
+ properties: {
1035
+ content: {
1036
+ type: "string",
1037
+ description: "Markdown plan describing what you intend to do."
1038
+ }
1039
+ },
1040
+ required: ["content"]
1041
+ }
1042
+ },
1043
+ streaming: {},
1044
+ async execute() {
1045
+ return "approved";
1046
+ }
1047
+ };
1048
+
1049
+ // src/tools/common/setProjectOnboardingState.ts
897
1050
  var setProjectOnboardingStateTool = {
898
1051
  definition: {
899
1052
  name: "setProjectOnboardingState",
@@ -919,7 +1072,7 @@ var setProjectOnboardingStateTool = {
919
1072
  }
920
1073
  };
921
1074
 
922
- // src/tools/spec/promptUser.ts
1075
+ // src/tools/common/promptUser.ts
923
1076
  var promptUserTool = {
924
1077
  definition: {
925
1078
  name: "promptUser",
@@ -947,8 +1100,8 @@ var promptUserTool = {
947
1100
  },
948
1101
  type: {
949
1102
  type: "string",
950
- enum: ["select", "checklist", "text", "file", "color"],
951
- description: "select: pick one from a list. checklist: pick one or more from a list. text: free-form input. file: file/image upload, returns CDN URL(s) that can be referenced directly or curled onto disk. color: color picker (returns hex)."
1103
+ enum: ["select", "checklist", "text", "file"],
1104
+ description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: file/image upload, returns CDN URL(s) that can be referenced directly or curled onto disk.'
952
1105
  },
953
1106
  helpText: {
954
1107
  type: "string",
@@ -985,10 +1138,6 @@ var promptUserTool = {
985
1138
  type: "boolean",
986
1139
  description: "For file type: allow multiple uploads (returns array of URLs). Defaults to false."
987
1140
  },
988
- allowOther: {
989
- type: "boolean",
990
- description: 'For select and checklist types: adds an "Other" option that lets the user type a custom answer. Use this instead of adding a separate follow-up text field for custom input. Defaults to false.'
991
- },
992
1141
  format: {
993
1142
  type: "string",
994
1143
  enum: ["email", "url", "phone", "number"],
@@ -1046,8 +1195,6 @@ var promptUserTool = {
1046
1195
  line += q.type === "checklist" ? ` (pick one or more: ${opts.join(" / ")})` : ` (${opts.join(" / ")})`;
1047
1196
  } else if (q.type === "file") {
1048
1197
  line += " (upload file)";
1049
- } else if (q.type === "color") {
1050
- line += " (pick a color)";
1051
1198
  }
1052
1199
  return line;
1053
1200
  });
@@ -1056,113 +1203,181 @@ ${lines.join("\n")}`;
1056
1203
  }
1057
1204
  };
1058
1205
 
1059
- // src/tools/spec/clearSyncStatus.ts
1060
- var clearSyncStatusTool = {
1206
+ // src/tools/common/confirmDestructiveAction.ts
1207
+ var confirmDestructiveActionTool = {
1061
1208
  definition: {
1062
- name: "clearSyncStatus",
1063
- description: "Clear the sync status flags after syncing spec and code. Call this after finishing a sync operation.",
1209
+ name: "confirmDestructiveAction",
1210
+ description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentSyncPlan, presentPublishPlan, or presentPlan (those already include approval). Do not use before onboarding state transitions.",
1064
1211
  inputSchema: {
1065
1212
  type: "object",
1066
- properties: {}
1213
+ properties: {
1214
+ message: {
1215
+ type: "string",
1216
+ description: "Explanation of what is about to happen and why confirmation is needed."
1217
+ },
1218
+ confirmLabel: {
1219
+ type: "string",
1220
+ description: 'Custom label for the confirm button (e.g., "Delete", "Reset Database"). Defaults to "Confirm".'
1221
+ },
1222
+ dismissLabel: {
1223
+ type: "string",
1224
+ description: 'Custom label for the dismiss button (e.g., "Keep It", "Go Back"). Defaults to "Cancel".'
1225
+ }
1226
+ },
1227
+ required: ["message"]
1067
1228
  }
1068
1229
  },
1069
1230
  async execute() {
1070
- return "ok";
1231
+ return "confirmed";
1071
1232
  }
1072
1233
  };
1073
1234
 
1074
- // src/tools/spec/presentSyncPlan.ts
1075
- var presentSyncPlanTool = {
1235
+ // src/tools/common/askMindStudioSdk.ts
1236
+ import { exec } from "child_process";
1237
+ var askMindStudioSdkTool = {
1076
1238
  definition: {
1077
- name: "presentSyncPlan",
1078
- description: "Present a structured sync plan to the user for approval. Write a clear markdown summary of what changed and what you intend to update. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE making any sync edits.",
1239
+ name: "askMindStudioSdk",
1240
+ description: "Ask the MindStudio SDK assistant about available actions, AI models, connectors, and integrations using natural language. Returns code examples with correct method signatures, model IDs, and config options. Always use this to verify correct SDK usage, especially model IDs and configuration options. Describe what you need, not what API methods you need; the assistant will figure out the right approach. This runs its own LLM call so it has a few seconds of latency; batch multiple questions into a single query.",
1079
1241
  inputSchema: {
1080
1242
  type: "object",
1081
1243
  properties: {
1082
- content: {
1244
+ query: {
1083
1245
  type: "string",
1084
- description: "Markdown plan describing what changed and what will be updated."
1246
+ description: "Natural language question about the SDK."
1085
1247
  }
1086
1248
  },
1087
- required: ["content"]
1249
+ required: ["query"]
1088
1250
  }
1089
1251
  },
1090
- streaming: {},
1091
- async execute() {
1092
- return "approved";
1252
+ async execute(input) {
1253
+ const query = input.query;
1254
+ return new Promise((resolve) => {
1255
+ exec(
1256
+ `mindstudio ask ${JSON.stringify(query)}`,
1257
+ { timeout: 6e4, maxBuffer: 512 * 1024 },
1258
+ (err, stdout, stderr) => {
1259
+ if (stdout.trim()) {
1260
+ resolve(stdout.trim());
1261
+ return;
1262
+ }
1263
+ if (err) {
1264
+ resolve(`Error: ${stderr.trim() || err.message}`);
1265
+ return;
1266
+ }
1267
+ resolve("(no response)");
1268
+ }
1269
+ );
1270
+ });
1093
1271
  }
1094
1272
  };
1095
1273
 
1096
- // src/tools/spec/presentPublishPlan.ts
1097
- var presentPublishPlanTool = {
1274
+ // src/tools/common/fetchUrl.ts
1275
+ import { exec as exec2 } from "child_process";
1276
+ var fetchUrlTool = {
1098
1277
  definition: {
1099
- name: "presentPublishPlan",
1100
- description: "Present a publish changelog to the user for approval. Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
1278
+ name: "scapeWebUrl",
1279
+ description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
1101
1280
  inputSchema: {
1102
1281
  type: "object",
1103
1282
  properties: {
1104
- content: {
1283
+ url: {
1105
1284
  type: "string",
1106
- description: "Markdown changelog describing what changed and what will be deployed."
1285
+ description: "The URL to fetch."
1286
+ },
1287
+ screenshot: {
1288
+ type: "boolean",
1289
+ description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
1107
1290
  }
1108
1291
  },
1109
- required: ["content"]
1292
+ required: ["url"]
1110
1293
  }
1111
1294
  },
1112
- streaming: {},
1113
- async execute() {
1114
- return "approved";
1295
+ async execute(input) {
1296
+ const url = input.url;
1297
+ const screenshot = input.screenshot;
1298
+ const pageOptions = { onlyMainContent: true };
1299
+ if (screenshot) {
1300
+ pageOptions.screenshot = true;
1301
+ }
1302
+ const cmd = `mindstudio scrape-url --url ${JSON.stringify(url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`;
1303
+ return new Promise((resolve) => {
1304
+ exec2(
1305
+ cmd,
1306
+ { timeout: 6e4, maxBuffer: 1024 * 1024 },
1307
+ (err, stdout, stderr) => {
1308
+ if (stdout.trim()) {
1309
+ resolve(stdout.trim());
1310
+ return;
1311
+ }
1312
+ if (err) {
1313
+ resolve(`Error: ${stderr.trim() || err.message}`);
1314
+ return;
1315
+ }
1316
+ resolve("(no response)");
1317
+ }
1318
+ );
1319
+ });
1115
1320
  }
1116
1321
  };
1117
1322
 
1118
- // src/tools/spec/presentPlan.ts
1119
- var presentPlanTool = {
1323
+ // src/tools/common/searchGoogle.ts
1324
+ import { exec as exec3 } from "child_process";
1325
+ var searchGoogleTool = {
1120
1326
  definition: {
1121
- name: "presentPlan",
1122
- description: "Present an implementation plan for user approval before making changes. Use this only for large, multi-step changes or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. If the user rejects with feedback, revise and present again.",
1327
+ name: "searchGoogle",
1328
+ description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help.",
1123
1329
  inputSchema: {
1124
1330
  type: "object",
1125
1331
  properties: {
1126
- content: {
1332
+ query: {
1127
1333
  type: "string",
1128
- description: "Markdown plan describing what you intend to do."
1334
+ description: "The search query."
1129
1335
  }
1130
1336
  },
1131
- required: ["content"]
1337
+ required: ["query"]
1132
1338
  }
1133
1339
  },
1134
- streaming: {},
1135
- async execute() {
1136
- return "approved";
1340
+ async execute(input) {
1341
+ const query = input.query;
1342
+ const cmd = `mindstudio search-google --query ${JSON.stringify(query)} --export-type json --output-key results --no-meta`;
1343
+ return new Promise((resolve) => {
1344
+ exec3(
1345
+ cmd,
1346
+ { timeout: 6e4, maxBuffer: 512 * 1024 },
1347
+ (err, stdout, stderr) => {
1348
+ if (stdout.trim()) {
1349
+ resolve(stdout.trim());
1350
+ return;
1351
+ }
1352
+ if (err) {
1353
+ resolve(`Error: ${stderr.trim() || err.message}`);
1354
+ return;
1355
+ }
1356
+ resolve("(no response)");
1357
+ }
1358
+ );
1359
+ });
1137
1360
  }
1138
1361
  };
1139
1362
 
1140
- // src/tools/spec/confirmDestructiveAction.ts
1141
- var confirmDestructiveActionTool = {
1363
+ // src/tools/common/setProjectName.ts
1364
+ var setProjectNameTool = {
1142
1365
  definition: {
1143
- name: "confirmDestructiveAction",
1144
- description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentSyncPlan, presentPublishPlan, or presentPlan (those already include approval). Do not use before onboarding state transitions.",
1366
+ name: "setProjectName",
1367
+ description: `Set the project display name. Call this after intake once you have enough context to give the project a clear, descriptive name. Keep it short (2-4 words). Use the app's actual name if the user mentioned one, otherwise pick something descriptive ("Vendor Procurement App", "Recipe Manager").`,
1145
1368
  inputSchema: {
1146
1369
  type: "object",
1147
1370
  properties: {
1148
- message: {
1149
- type: "string",
1150
- description: "Explanation of what is about to happen and why confirmation is needed."
1151
- },
1152
- confirmLabel: {
1153
- type: "string",
1154
- description: 'Custom label for the confirm button (e.g., "Delete", "Reset Database"). Defaults to "Confirm".'
1155
- },
1156
- dismissLabel: {
1371
+ name: {
1157
1372
  type: "string",
1158
- description: 'Custom label for the dismiss button (e.g., "Keep It", "Go Back"). Defaults to "Cancel".'
1373
+ description: "The project name."
1159
1374
  }
1160
1375
  },
1161
- required: ["message"]
1376
+ required: ["name"]
1162
1377
  }
1163
1378
  },
1164
1379
  async execute() {
1165
- return "confirmed";
1380
+ return "ok";
1166
1381
  }
1167
1382
  };
1168
1383
 
@@ -1170,9 +1385,9 @@ var confirmDestructiveActionTool = {
1170
1385
  import fs9 from "fs/promises";
1171
1386
  var DEFAULT_MAX_LINES2 = 500;
1172
1387
  function isBinary(buffer) {
1173
- const sample = buffer.subarray(0, 8192);
1174
- for (let i = 0; i < sample.length; i++) {
1175
- if (sample[i] === 0) {
1388
+ const sample2 = buffer.subarray(0, 8192);
1389
+ for (let i = 0; i < sample2.length; i++) {
1390
+ if (sample2[i] === 0) {
1176
1391
  return true;
1177
1392
  }
1178
1393
  }
@@ -1466,7 +1681,7 @@ ${unifiedDiff(input.path, content, updated)}`;
1466
1681
  };
1467
1682
 
1468
1683
  // src/tools/code/bash.ts
1469
- import { exec } from "child_process";
1684
+ import { exec as exec4 } from "child_process";
1470
1685
  var DEFAULT_TIMEOUT_MS = 12e4;
1471
1686
  var DEFAULT_MAX_LINES3 = 500;
1472
1687
  var bashTool = {
@@ -1500,7 +1715,7 @@ var bashTool = {
1500
1715
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1501
1716
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1502
1717
  return new Promise((resolve) => {
1503
- exec(
1718
+ exec4(
1504
1719
  input.command,
1505
1720
  {
1506
1721
  timeout: timeoutMs,
@@ -1540,7 +1755,7 @@ var bashTool = {
1540
1755
  };
1541
1756
 
1542
1757
  // src/tools/code/grep.ts
1543
- import { exec as exec2 } from "child_process";
1758
+ import { exec as exec5 } from "child_process";
1544
1759
  var DEFAULT_MAX = 50;
1545
1760
  function formatResults(stdout, max) {
1546
1761
  const lines = stdout.trim().split("\n");
@@ -1587,12 +1802,12 @@ var grepTool = {
1587
1802
  const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1588
1803
  const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1589
1804
  return new Promise((resolve) => {
1590
- exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1805
+ exec5(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1591
1806
  if (stdout?.trim()) {
1592
1807
  resolve(formatResults(stdout, max));
1593
1808
  return;
1594
1809
  }
1595
- exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1810
+ exec5(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1596
1811
  if (grepStdout?.trim()) {
1597
1812
  resolve(formatResults(grepStdout, max));
1598
1813
  } else {
@@ -1776,47 +1991,8 @@ var restartProcessTool = {
1776
1991
  }
1777
1992
  };
1778
1993
 
1779
- // src/tools/code/askMindStudioSdk.ts
1780
- import { exec as exec3 } from "child_process";
1781
- var askMindStudioSdkTool = {
1782
- definition: {
1783
- name: "askMindStudioSdk",
1784
- description: "Ask the MindStudio SDK assistant about available actions, AI models, connectors, and integrations using natural language. Returns code examples with correct method signatures, model IDs, and config options. Always use this to verify correct SDK usage, especially model IDs and configuration options. Describe what you need, not what API methods you need; the assistant will figure out the right approach. This runs its own LLM call so it has a few seconds of latency; batch multiple questions into a single query.",
1785
- inputSchema: {
1786
- type: "object",
1787
- properties: {
1788
- query: {
1789
- type: "string",
1790
- description: "Natural language question about the SDK."
1791
- }
1792
- },
1793
- required: ["query"]
1794
- }
1795
- },
1796
- async execute(input) {
1797
- const query = input.query;
1798
- return new Promise((resolve) => {
1799
- exec3(
1800
- `mindstudio ask ${JSON.stringify(query)}`,
1801
- { timeout: 6e4, maxBuffer: 512 * 1024 },
1802
- (err, stdout, stderr) => {
1803
- if (stdout.trim()) {
1804
- resolve(stdout.trim());
1805
- return;
1806
- }
1807
- if (err) {
1808
- resolve(`Error: ${stderr.trim() || err.message}`);
1809
- return;
1810
- }
1811
- resolve("(no response)");
1812
- }
1813
- );
1814
- });
1815
- }
1816
- };
1817
-
1818
- // src/tools/code/runScenario.ts
1819
- var runScenarioTool = {
1994
+ // src/tools/code/runScenario.ts
1995
+ var runScenarioTool = {
1820
1996
  definition: {
1821
1997
  name: "runScenario",
1822
1998
  description: "Run a scenario to seed the dev database with test data. Truncates all tables first, then executes the seed function and impersonates the scenario roles. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details.",
@@ -1903,7 +2079,7 @@ async function runSubAgent(config) {
1903
2079
  const toolCalls = [];
1904
2080
  let stopReason = "end_turn";
1905
2081
  try {
1906
- for await (const event of streamChat({
2082
+ for await (const event of streamChatWithRetry({
1907
2083
  ...apiConfig,
1908
2084
  model,
1909
2085
  system,
@@ -2121,6 +2297,419 @@ var browserAutomationTool = {
2121
2297
  }
2122
2298
  };
2123
2299
 
2300
+ // src/subagents/designExpert/tools.ts
2301
+ import { exec as exec6 } from "child_process";
2302
+ var DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design reference. Assess:
2303
+ 1) Mood/aesthetic
2304
+ 2) Color palette with approximate hex values and palette strategy
2305
+ 3) Typography style
2306
+ 4) Layout composition (symmetric/asymmetric, grid structure, whitespace usage, content density)
2307
+ 5) What makes it distinctive and interesting vs generic AI-generated interfaces
2308
+ Be specific and concise.`;
2309
+ var DESIGN_RESEARCH_TOOLS = [
2310
+ {
2311
+ name: "searchGoogle",
2312
+ description: "Search Google for web results. Use for finding design inspiration, font recommendations, UI patterns, real products in a domain, and reference material.",
2313
+ inputSchema: {
2314
+ type: "object",
2315
+ properties: {
2316
+ query: {
2317
+ type: "string",
2318
+ description: "The search query."
2319
+ }
2320
+ },
2321
+ required: ["query"]
2322
+ }
2323
+ },
2324
+ {
2325
+ name: "fetchUrl",
2326
+ description: "Fetch the content of a web page as markdown. Optionally capture a screenshot to see the visual design. Use to analyze reference sites, read font specimen pages, or extract design details.",
2327
+ inputSchema: {
2328
+ type: "object",
2329
+ properties: {
2330
+ url: {
2331
+ type: "string",
2332
+ description: "The URL to fetch."
2333
+ },
2334
+ screenshot: {
2335
+ type: "boolean",
2336
+ description: "Capture a screenshot of the page. Use when you need to see the visual design, not just the text."
2337
+ }
2338
+ },
2339
+ required: ["url"]
2340
+ }
2341
+ },
2342
+ {
2343
+ name: "analyzeImage",
2344
+ description: 'Analyze an image using a vision model with a custom prompt. Use when you have a specific question about an image (e.g., "what colors dominate this image?", "describe the typography choices").',
2345
+ inputSchema: {
2346
+ type: "object",
2347
+ properties: {
2348
+ prompt: {
2349
+ type: "string",
2350
+ description: "What to analyze or extract from the image."
2351
+ },
2352
+ imageUrl: {
2353
+ type: "string",
2354
+ description: "URL of the image to analyze."
2355
+ }
2356
+ },
2357
+ required: ["prompt", "imageUrl"]
2358
+ }
2359
+ },
2360
+ {
2361
+ name: "analyzeDesignReference",
2362
+ description: "Analyze a screenshot or design image for design inspiration. Returns a structured analysis: mood/aesthetic, color palette with hex values, typography style, layout composition, and what makes it distinctive. Use this instead of analyzeImage when studying a design reference.",
2363
+ inputSchema: {
2364
+ type: "object",
2365
+ properties: {
2366
+ imageUrl: {
2367
+ type: "string",
2368
+ description: "URL of the screenshot or design image to analyze."
2369
+ }
2370
+ },
2371
+ required: ["imageUrl"]
2372
+ }
2373
+ },
2374
+ {
2375
+ name: "screenshotAndAnalyze",
2376
+ description: "Screenshot a live URL and analyze it in one step. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific question about the visual design instead.",
2377
+ inputSchema: {
2378
+ type: "object",
2379
+ properties: {
2380
+ url: {
2381
+ type: "string",
2382
+ description: "The URL to screenshot."
2383
+ },
2384
+ prompt: {
2385
+ type: "string",
2386
+ description: "Optional custom analysis prompt. If omitted, performs the standard design reference analysis."
2387
+ }
2388
+ },
2389
+ required: ["url"]
2390
+ }
2391
+ },
2392
+ {
2393
+ name: "searchStockPhotos",
2394
+ description: 'Search Pexels for stock photos. Returns image URLs with descriptions. Use concrete, descriptive queries ("person working at laptop in modern office" not "business").',
2395
+ inputSchema: {
2396
+ type: "object",
2397
+ properties: {
2398
+ query: {
2399
+ type: "string",
2400
+ description: "What kind of photo to search for."
2401
+ }
2402
+ },
2403
+ required: ["query"]
2404
+ }
2405
+ },
2406
+ {
2407
+ name: "searchProductScreenshots",
2408
+ 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.',
2409
+ inputSchema: {
2410
+ type: "object",
2411
+ properties: {
2412
+ product: {
2413
+ type: "string",
2414
+ description: 'The product or app to find screenshots of (e.g., "stripe dashboard", "figma editor", "mercury banking app").'
2415
+ }
2416
+ },
2417
+ required: ["product"]
2418
+ }
2419
+ },
2420
+ {
2421
+ name: "generateImages",
2422
+ description: "Generate images using AI (Seedream). Returns CDN URLs. Produces high-quality results for both photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel.",
2423
+ inputSchema: {
2424
+ type: "object",
2425
+ properties: {
2426
+ prompts: {
2427
+ type: "array",
2428
+ items: {
2429
+ type: "string"
2430
+ },
2431
+ description: "One or more image generation prompts. Be detailed: describe style, mood, composition, colors. Multiple prompts run in parallel."
2432
+ },
2433
+ width: {
2434
+ type: "number",
2435
+ description: "Image width in pixels. Default 2048. Range: 2048-4096."
2436
+ },
2437
+ height: {
2438
+ type: "number",
2439
+ description: "Image height in pixels. Default 2048. Range: 2048-4096."
2440
+ }
2441
+ },
2442
+ required: ["prompts"]
2443
+ }
2444
+ }
2445
+ ];
2446
+ function runCli(cmd) {
2447
+ return new Promise((resolve) => {
2448
+ exec6(
2449
+ cmd,
2450
+ { timeout: 6e4, maxBuffer: 1024 * 1024 },
2451
+ (err, stdout, stderr) => {
2452
+ if (stdout.trim()) {
2453
+ resolve(stdout.trim());
2454
+ return;
2455
+ }
2456
+ if (err) {
2457
+ resolve(`Error: ${stderr.trim() || err.message}`);
2458
+ return;
2459
+ }
2460
+ resolve("(no response)");
2461
+ }
2462
+ );
2463
+ });
2464
+ }
2465
+ async function executeDesignTool(name, input) {
2466
+ switch (name) {
2467
+ case "searchGoogle":
2468
+ return runCli(
2469
+ `mindstudio search-google --query ${JSON.stringify(input.query)} --export-type json --output-key results --no-meta`
2470
+ );
2471
+ case "fetchUrl": {
2472
+ const pageOptions = { onlyMainContent: true };
2473
+ if (input.screenshot) {
2474
+ pageOptions.screenshot = true;
2475
+ }
2476
+ return runCli(
2477
+ `mindstudio scrape-url --url ${JSON.stringify(input.url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`
2478
+ );
2479
+ }
2480
+ case "analyzeImage":
2481
+ return runCli(
2482
+ `mindstudio analyze-image --prompt ${JSON.stringify(input.prompt)} --image-url ${JSON.stringify(input.imageUrl)} --no-meta`
2483
+ );
2484
+ case "analyzeDesignReference":
2485
+ return runCli(
2486
+ `mindstudio analyze-image --prompt ${JSON.stringify(DESIGN_REFERENCE_PROMPT)} --image-url ${JSON.stringify(input.imageUrl)} --no-meta`
2487
+ );
2488
+ case "screenshotAndAnalyze": {
2489
+ const screenshotResult = await runCli(
2490
+ `mindstudio scrape-url --url ${JSON.stringify(input.url)} --page-options ${JSON.stringify(JSON.stringify({ onlyMainContent: true, screenshot: true }))} --no-meta`
2491
+ );
2492
+ const screenshotMatch = screenshotResult.match(
2493
+ /https:\/\/[^\s"']+(?:\.png|\.jpg|\.jpeg|\.webp|screenshot[^\s"']*)/i
2494
+ );
2495
+ if (!screenshotMatch) {
2496
+ try {
2497
+ const parsed = JSON.parse(screenshotResult);
2498
+ const ssUrl = parsed.screenshot || parsed.screenshotUrl || parsed.content?.screenshotUrl;
2499
+ if (ssUrl) {
2500
+ const analysisPrompt2 = input.prompt || DESIGN_REFERENCE_PROMPT;
2501
+ const analysis2 = await runCli(
2502
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt2)} --image-url ${JSON.stringify(ssUrl)} --no-meta`
2503
+ );
2504
+ return `Screenshot: ${ssUrl}
2505
+
2506
+ ${analysis2}`;
2507
+ }
2508
+ } catch {
2509
+ }
2510
+ return `Fetched ${input.url} but could not extract screenshot URL.
2511
+
2512
+ Page content:
2513
+ ${screenshotResult}`;
2514
+ }
2515
+ const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
2516
+ const analysis = await runCli(
2517
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(screenshotMatch[0])} --no-meta`
2518
+ );
2519
+ return `Screenshot: ${screenshotMatch[0]}
2520
+
2521
+ ${analysis}`;
2522
+ }
2523
+ case "searchStockPhotos": {
2524
+ const encodedQuery = encodeURIComponent(input.query);
2525
+ return runCli(
2526
+ `mindstudio scrape-url --url "https://www.pexels.com/search/${encodedQuery}/" --page-options '{"onlyMainContent": true}' --no-meta`
2527
+ );
2528
+ }
2529
+ case "searchProductScreenshots": {
2530
+ const query = `${input.product} product screenshot UI 2026`;
2531
+ return runCli(
2532
+ `mindstudio search-google-images --query ${JSON.stringify(query)} --export-type json --output-key images --no-meta`
2533
+ );
2534
+ }
2535
+ case "generateImages": {
2536
+ const prompts = input.prompts;
2537
+ const width = input.width || 2048;
2538
+ const height = input.height || 2048;
2539
+ if (prompts.length === 1) {
2540
+ const step = JSON.stringify({
2541
+ prompt: prompts[0],
2542
+ imageModelOverride: {
2543
+ model: "seedream-4.5",
2544
+ config: { width, height }
2545
+ }
2546
+ });
2547
+ return runCli(
2548
+ `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`
2549
+ );
2550
+ }
2551
+ const steps = prompts.map((prompt) => ({
2552
+ stepType: "generateImage",
2553
+ step: {
2554
+ prompt,
2555
+ imageModelOverride: {
2556
+ model: "seedream-4.5",
2557
+ config: { width, height }
2558
+ }
2559
+ }
2560
+ }));
2561
+ return runCli(`mindstudio batch '${JSON.stringify(steps)}' --no-meta`);
2562
+ }
2563
+ default:
2564
+ return `Error: unknown tool "${name}"`;
2565
+ }
2566
+ }
2567
+
2568
+ // src/subagents/designExpert/prompt.ts
2569
+ import fs14 from "fs";
2570
+ import path8 from "path";
2571
+ var base2 = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
2572
+ function resolvePath(filename) {
2573
+ const local2 = path8.join(base2, filename);
2574
+ return fs14.existsSync(local2) ? local2 : path8.join(base2, "subagents", "designExpert", filename);
2575
+ }
2576
+ function readFile(filename) {
2577
+ return fs14.readFileSync(resolvePath(filename), "utf-8").trim();
2578
+ }
2579
+ function readJson(filename, fallback) {
2580
+ try {
2581
+ return JSON.parse(fs14.readFileSync(resolvePath(filename), "utf-8"));
2582
+ } catch {
2583
+ return fallback;
2584
+ }
2585
+ }
2586
+ var RUNTIME_PLACEHOLDERS = /* @__PURE__ */ new Set([
2587
+ "fonts_to_consider",
2588
+ "inspiration_images"
2589
+ ]);
2590
+ var PROMPT_TEMPLATE = readFile("prompt.md").replace(/\{\{([^}]+)\}\}/g, (match, key) => {
2591
+ const k = key.trim();
2592
+ return RUNTIME_PLACEHOLDERS.has(k) ? match : readFile(k);
2593
+ }).replace(/\n{3,}/g, "\n\n");
2594
+ var fontData = readJson("data/fonts.json", {
2595
+ cssUrlPattern: "",
2596
+ fonts: [],
2597
+ pairings: []
2598
+ });
2599
+ var inspirationImages = readJson("data/inspiration.json", {
2600
+ images: []
2601
+ }).images;
2602
+ function sample(arr, n) {
2603
+ if (arr.length <= n) {
2604
+ return [...arr];
2605
+ }
2606
+ const copy = [...arr];
2607
+ for (let i = copy.length - 1; i > 0; i--) {
2608
+ const j = Math.floor(Math.random() * (i + 1));
2609
+ [copy[i], copy[j]] = [copy[j], copy[i]];
2610
+ }
2611
+ return copy.slice(0, n);
2612
+ }
2613
+ function getDesignResearchPrompt() {
2614
+ const fonts = sample(fontData.fonts, 30);
2615
+ const pairings = sample(fontData.pairings, 20);
2616
+ const images = sample(inspirationImages, 40);
2617
+ const fontList = fonts.map((f) => {
2618
+ const tags = f.tags.length ? ` (${f.tags.join(", ")})` : "";
2619
+ let cssInfo = "";
2620
+ if (f.source === "fontshare") {
2621
+ cssInfo = ` CSS: ${fontData.cssUrlPattern.replace("{slug}", f.slug).replace("{weights}", f.weights.join(","))}`;
2622
+ } else if (f.cssUrl) {
2623
+ cssInfo = ` CSS: ${f.cssUrl}`;
2624
+ } else if (f.source === "open-foundry") {
2625
+ cssInfo = " (self-host required)";
2626
+ }
2627
+ return `- **${f.name}** \u2014 ${f.category}${tags}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}`;
2628
+ }).join("\n");
2629
+ const pairingList = pairings.map(
2630
+ (p) => `- **${p.heading.font}** (${p.heading.weight}) heading + **${p.body.font}** (${p.body.weight}) body`
2631
+ ).join("\n");
2632
+ const fontsSection = fonts.length ? `<fonts_to_consider>
2633
+ ## Fonts to consider
2634
+
2635
+ A random sample from Fontshare, Open Foundry, and Google Fonts. Use these as starting points for font selection.
2636
+ CSS URL pattern: ${fontData.cssUrlPattern}
2637
+
2638
+ ${fontList}
2639
+
2640
+ ### Suggested pairings
2641
+
2642
+ ${pairingList}
2643
+ </fonts_to_consider>` : "";
2644
+ const imageList = images.map((img) => `- ${img.analysis}`).join("\n\n");
2645
+ const inspirationSection = images.length ? `<inspiration_images>
2646
+ ## Design inspiration
2647
+
2648
+ A random sample of pre-analyzed design references. Use these observations to inform your recommendations.
2649
+
2650
+ ${imageList}
2651
+ </inspiration_images>` : "";
2652
+ return PROMPT_TEMPLATE.replace("{{fonts_to_consider}}", fontsSection).replace(
2653
+ "{{inspiration_images}}",
2654
+ inspirationSection
2655
+ );
2656
+ }
2657
+
2658
+ // src/subagents/designExpert/index.ts
2659
+ var DESCRIPTION = `
2660
+ A dedicated visual design expert. You have a lot on your plate as a coding agent, and design is a specialized skill \u2014 delegate visual design questions here rather than making those decisions yourself. This agent has curated font catalogs, color theory knowledge, access to design inspiration galleries, and strong opinions about what looks good. It can answer from expertise alone or research the web when needed.
2661
+
2662
+ The visual design expert can be used for all things visual design, from quick questions to comprehensive plans:
2663
+ - Font selection and pairings ("suggest fonts for a <x> app")
2664
+ - Color palettes from a mood, domain, or seed color ("earthy tones for a <x> brand")
2665
+ - Gradient, animation, and visual effect recommendations
2666
+ - Layout and composition ideas that go beyond generic AI defaults
2667
+ - Analyzing a reference site or screenshot for design insights (it can take screenshots and do research on its own)
2668
+ - Sourcing imagery: stock photos, AI-generated images, or product screenshots for reference
2669
+ - Icon recommendations
2670
+ - Proposing full visual directions during intake
2671
+
2672
+ **How to write the task:**
2673
+ Include context about the app \u2014 what it does, who uses it, what mood or feeling the interface should convey. More context produces better results. For quick questions ("three font pairings for a <x> app"), brief is fine. You can ask for multiple topics, multiple options, etc.
2674
+
2675
+ **What it returns:**
2676
+ Concrete resources: hex values, font names with CSS URLs, image URLs, layout descriptions. Use the results directly in brand spec files or as guidance when building the interface.
2677
+ `.trim();
2678
+ var designExpertTool = {
2679
+ definition: {
2680
+ name: "visualDesignExpert",
2681
+ description: DESCRIPTION,
2682
+ inputSchema: {
2683
+ type: "object",
2684
+ properties: {
2685
+ task: {
2686
+ type: "string",
2687
+ description: "What you need, in natural language. Include context about the app when relevant."
2688
+ }
2689
+ },
2690
+ required: ["task"]
2691
+ }
2692
+ },
2693
+ async execute(input, context) {
2694
+ if (!context) {
2695
+ return "Error: design research requires execution context";
2696
+ }
2697
+ return runSubAgent({
2698
+ system: getDesignResearchPrompt(),
2699
+ task: input.task,
2700
+ tools: DESIGN_RESEARCH_TOOLS,
2701
+ externalTools: /* @__PURE__ */ new Set(),
2702
+ executeTool: executeDesignTool,
2703
+ apiConfig: context.apiConfig,
2704
+ model: context.model,
2705
+ signal: context.signal,
2706
+ parentToolId: context.toolCallId,
2707
+ onEvent: context.onEvent,
2708
+ resolveExternalTool: context.resolveExternalTool
2709
+ });
2710
+ }
2711
+ };
2712
+
2124
2713
  // src/tools/index.ts
2125
2714
  function getSpecTools() {
2126
2715
  return [readSpecTool, writeSpecTool, editSpecTool, listSpecFilesTool];
@@ -2135,7 +2724,6 @@ function getCodeTools() {
2135
2724
  globTool,
2136
2725
  listDirTool,
2137
2726
  editsFinishedTool,
2138
- askMindStudioSdkTool,
2139
2727
  runScenarioTool,
2140
2728
  runMethodTool,
2141
2729
  screenshotTool,
@@ -2150,7 +2738,12 @@ function getCommonTools() {
2150
2738
  return [
2151
2739
  setProjectOnboardingStateTool,
2152
2740
  promptUserTool,
2153
- confirmDestructiveActionTool
2741
+ confirmDestructiveActionTool,
2742
+ askMindStudioSdkTool,
2743
+ fetchUrlTool,
2744
+ searchGoogleTool,
2745
+ setProjectNameTool,
2746
+ designExpertTool
2154
2747
  ];
2155
2748
  }
2156
2749
  function getPostOnboardingTools() {
@@ -2197,11 +2790,11 @@ function executeTool(name, input, context) {
2197
2790
  }
2198
2791
 
2199
2792
  // src/session.ts
2200
- import fs14 from "fs";
2793
+ import fs15 from "fs";
2201
2794
  var SESSION_FILE = ".remy-session.json";
2202
2795
  function loadSession(state) {
2203
2796
  try {
2204
- const raw = fs14.readFileSync(SESSION_FILE, "utf-8");
2797
+ const raw = fs15.readFileSync(SESSION_FILE, "utf-8");
2205
2798
  const data = JSON.parse(raw);
2206
2799
  if (Array.isArray(data.messages) && data.messages.length > 0) {
2207
2800
  state.messages = sanitizeMessages(data.messages);
@@ -2243,7 +2836,7 @@ function sanitizeMessages(messages) {
2243
2836
  }
2244
2837
  function saveSession(state) {
2245
2838
  try {
2246
- fs14.writeFileSync(
2839
+ fs15.writeFileSync(
2247
2840
  SESSION_FILE,
2248
2841
  JSON.stringify({ messages: state.messages }, null, 2),
2249
2842
  "utf-8"
@@ -2254,7 +2847,7 @@ function saveSession(state) {
2254
2847
  function clearSession(state) {
2255
2848
  state.messages = [];
2256
2849
  try {
2257
- fs14.unlinkSync(SESSION_FILE);
2850
+ fs15.unlinkSync(SESSION_FILE);
2258
2851
  } catch {
2259
2852
  }
2260
2853
  }
@@ -2422,6 +3015,104 @@ function parsePartialJson(jsonString) {
2422
3015
  return parseAny();
2423
3016
  }
2424
3017
 
3018
+ // src/statusWatcher.ts
3019
+ function startStatusWatcher(config) {
3020
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
3021
+ let lastLabel = "";
3022
+ let inflight = false;
3023
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3024
+ async function tick() {
3025
+ if (signal?.aborted || inflight) {
3026
+ return;
3027
+ }
3028
+ inflight = true;
3029
+ try {
3030
+ const ctx = getContext();
3031
+ if (!ctx.assistantText && !ctx.lastToolName) {
3032
+ log.debug("Status watcher: no context, skipping");
3033
+ return;
3034
+ }
3035
+ log.debug("Status watcher: requesting label", {
3036
+ textLength: ctx.assistantText.length,
3037
+ lastToolName: ctx.lastToolName
3038
+ });
3039
+ const res = await fetch(url, {
3040
+ method: "POST",
3041
+ headers: {
3042
+ "Content-Type": "application/json",
3043
+ Authorization: `Bearer ${apiConfig.apiKey}`
3044
+ },
3045
+ body: JSON.stringify({
3046
+ assistantText: ctx.assistantText.slice(-500),
3047
+ lastToolName: ctx.lastToolName,
3048
+ lastToolResult: ctx.lastToolResult?.slice(-200)
3049
+ }),
3050
+ signal
3051
+ });
3052
+ if (!res.ok) {
3053
+ log.debug("Status watcher: endpoint returned non-ok", {
3054
+ status: res.status
3055
+ });
3056
+ return;
3057
+ }
3058
+ const data = await res.json();
3059
+ if (!data.label) {
3060
+ log.debug("Status watcher: no label in response");
3061
+ return;
3062
+ }
3063
+ if (data.label === lastLabel) {
3064
+ log.debug("Status watcher: duplicate label, skipping", {
3065
+ label: data.label
3066
+ });
3067
+ return;
3068
+ }
3069
+ lastLabel = data.label;
3070
+ log.debug("Status watcher: emitting", { label: data.label });
3071
+ onStatus(data.label);
3072
+ } catch (err) {
3073
+ log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
3074
+ } finally {
3075
+ inflight = false;
3076
+ }
3077
+ }
3078
+ const timer = setInterval(tick, interval);
3079
+ tick().catch(() => {
3080
+ });
3081
+ log.debug("Status watcher started", { interval });
3082
+ return {
3083
+ stop() {
3084
+ clearInterval(timer);
3085
+ log.debug("Status watcher stopped");
3086
+ }
3087
+ };
3088
+ }
3089
+
3090
+ // src/errors.ts
3091
+ var patterns = [
3092
+ [
3093
+ /Network error/i,
3094
+ "Lost connection to the server. Please check your internet connection and try again."
3095
+ ],
3096
+ [
3097
+ /HTTP 429|rate limit/i,
3098
+ "Too many requests. Please wait a moment and try again."
3099
+ ],
3100
+ [/HTTP 40[13]/i, "Authentication failed. Please check your API key."],
3101
+ [
3102
+ /HTTP 5\d\d/i,
3103
+ "The AI service is temporarily unavailable. Please try again."
3104
+ ],
3105
+ [/Stream stalled/i, "The connection was interrupted. Please try again."]
3106
+ ];
3107
+ function friendlyError(raw) {
3108
+ for (const [pattern, message] of patterns) {
3109
+ if (pattern.test(raw)) {
3110
+ return message;
3111
+ }
3112
+ }
3113
+ return `Something went wrong: ${raw}`;
3114
+ }
3115
+
2425
3116
  // src/agent.ts
2426
3117
  var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
2427
3118
  "promptUser",
@@ -2434,7 +3125,8 @@ var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
2434
3125
  "runScenario",
2435
3126
  "runMethod",
2436
3127
  "browserCommand",
2437
- "screenshot"
3128
+ "screenshot",
3129
+ "setProjectName"
2438
3130
  ]);
2439
3131
  function createAgentState() {
2440
3132
  return { messages: [] };
@@ -2464,7 +3156,8 @@ async function runTurn(params) {
2464
3156
  }
2465
3157
  });
2466
3158
  onEvent({ type: "turn_started" });
2467
- const userMsg = { role: "user", content: userMessage };
3159
+ const cleanMessage = userMessage.replace(/^@@automated::[^@]*@@/, "");
3160
+ const userMsg = { role: "user", content: cleanMessage };
2468
3161
  if (hidden) {
2469
3162
  userMsg.hidden = true;
2470
3163
  }
@@ -2558,15 +3251,34 @@ async function runTurn(params) {
2558
3251
  onEvent({ type: "tool_input_delta", id, name, result: content });
2559
3252
  }
2560
3253
  }
3254
+ const statusWatcher = startStatusWatcher({
3255
+ apiConfig,
3256
+ getContext: () => ({
3257
+ assistantText: assistantText.slice(-500),
3258
+ lastToolName: toolCalls.at(-1)?.name
3259
+ }),
3260
+ onStatus: (label) => onEvent({ type: "status", message: label }),
3261
+ signal
3262
+ });
2561
3263
  try {
2562
- for await (const event of streamChat({
2563
- ...apiConfig,
2564
- model,
2565
- system,
2566
- messages: state.messages,
2567
- tools,
2568
- signal
2569
- })) {
3264
+ for await (const event of streamChatWithRetry(
3265
+ {
3266
+ ...apiConfig,
3267
+ model,
3268
+ system,
3269
+ messages: state.messages,
3270
+ tools,
3271
+ signal
3272
+ },
3273
+ {
3274
+ onRetry: (attempt) => {
3275
+ onEvent({
3276
+ type: "status",
3277
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
3278
+ });
3279
+ }
3280
+ }
3281
+ )) {
2570
3282
  if (signal?.aborted) {
2571
3283
  break;
2572
3284
  }
@@ -2634,7 +3346,7 @@ async function runTurn(params) {
2634
3346
  stopReason = event.stopReason;
2635
3347
  break;
2636
3348
  case "error":
2637
- onEvent({ type: "error", error: event.error });
3349
+ onEvent({ type: "error", error: friendlyError(event.error) });
2638
3350
  return;
2639
3351
  }
2640
3352
  }
@@ -2643,6 +3355,8 @@ async function runTurn(params) {
2643
3355
  } else {
2644
3356
  throw err;
2645
3357
  }
3358
+ } finally {
3359
+ statusWatcher.stop();
2646
3360
  }
2647
3361
  if (signal?.aborted) {
2648
3362
  if (assistantText) {
@@ -2670,6 +3384,15 @@ async function runTurn(params) {
2670
3384
  count: toolCalls.length,
2671
3385
  tools: toolCalls.map((tc) => tc.name)
2672
3386
  });
3387
+ const toolStatusWatcher = startStatusWatcher({
3388
+ apiConfig,
3389
+ getContext: () => ({
3390
+ assistantText: assistantText.slice(-500),
3391
+ lastToolName: toolCalls.map((tc) => tc.name).join(", ")
3392
+ }),
3393
+ onStatus: (label) => onEvent({ type: "status", message: label }),
3394
+ signal
3395
+ });
2673
3396
  const results = await Promise.all(
2674
3397
  toolCalls.map(async (tc) => {
2675
3398
  if (signal?.aborted) {
@@ -2727,6 +3450,7 @@ async function runTurn(params) {
2727
3450
  }
2728
3451
  })
2729
3452
  );
3453
+ toolStatusWatcher.stop();
2730
3454
  for (const r of results) {
2731
3455
  state.messages.push({
2732
3456
  role: "user",
@@ -2744,10 +3468,10 @@ async function runTurn(params) {
2744
3468
  }
2745
3469
 
2746
3470
  // src/headless.ts
2747
- var BASE_DIR = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
2748
- var ACTIONS_DIR = path8.join(BASE_DIR, "actions");
3471
+ var BASE_DIR = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
3472
+ var ACTIONS_DIR = path9.join(BASE_DIR, "actions");
2749
3473
  function loadActionPrompt(name) {
2750
- return fs15.readFileSync(path8.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
3474
+ return fs16.readFileSync(path9.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
2751
3475
  }
2752
3476
  function emit(event, data) {
2753
3477
  process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
@@ -2775,6 +3499,7 @@ async function startHeadless(opts = {}) {
2775
3499
  }
2776
3500
  let running = false;
2777
3501
  let currentAbort = null;
3502
+ const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
2778
3503
  const pendingTools = /* @__PURE__ */ new Map();
2779
3504
  const earlyResults = /* @__PURE__ */ new Map();
2780
3505
  function onEvent(e) {
@@ -2829,6 +3554,9 @@ async function startHeadless(opts = {}) {
2829
3554
  case "error":
2830
3555
  emit("error", { error: e.error });
2831
3556
  break;
3557
+ case "status":
3558
+ emit("status", { message: e.message });
3559
+ break;
2832
3560
  }
2833
3561
  }
2834
3562
  function resolveExternalTool(id, _name, _input) {
@@ -2838,7 +3566,19 @@ async function startHeadless(opts = {}) {
2838
3566
  return Promise.resolve(early);
2839
3567
  }
2840
3568
  return new Promise((resolve) => {
2841
- pendingTools.set(id, { resolve });
3569
+ const timeout = setTimeout(() => {
3570
+ pendingTools.delete(id);
3571
+ resolve(
3572
+ "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
3573
+ );
3574
+ }, EXTERNAL_TOOL_TIMEOUT_MS);
3575
+ pendingTools.set(id, {
3576
+ resolve: (result) => {
3577
+ clearTimeout(timeout);
3578
+ resolve(result);
3579
+ },
3580
+ timeout
3581
+ });
2842
3582
  });
2843
3583
  }
2844
3584
  const rl = createInterface({ input: process.stdin });
@@ -2876,6 +3616,7 @@ async function startHeadless(opts = {}) {
2876
3616
  currentAbort.abort();
2877
3617
  }
2878
3618
  for (const [id, pending] of pendingTools) {
3619
+ clearTimeout(pending.timeout);
2879
3620
  pending.resolve("Error: cancelled");
2880
3621
  pendingTools.delete(id);
2881
3622
  }