@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/index.js CHANGED
@@ -146,11 +146,35 @@ async function* streamChat(params) {
146
146
  yield { type: "error", error: errorMessage };
147
147
  return;
148
148
  }
149
+ const STALL_TIMEOUT_MS = 3e5;
149
150
  const reader = res.body.getReader();
150
151
  const decoder = new TextDecoder();
151
152
  let buffer = "";
152
153
  while (true) {
153
- const { done, value } = await reader.read();
154
+ let stallTimer;
155
+ let readResult;
156
+ try {
157
+ readResult = await Promise.race([
158
+ reader.read(),
159
+ new Promise((_, reject) => {
160
+ stallTimer = setTimeout(
161
+ () => reject(new Error("stream_stall")),
162
+ STALL_TIMEOUT_MS
163
+ );
164
+ })
165
+ ]);
166
+ clearTimeout(stallTimer);
167
+ } catch {
168
+ clearTimeout(stallTimer);
169
+ await reader.cancel();
170
+ log.error("Stream stalled", { elapsed: `${Date.now() - startTime}ms` });
171
+ yield {
172
+ type: "error",
173
+ error: "Stream stalled \u2014 no data received for 5 minutes"
174
+ };
175
+ return;
176
+ }
177
+ const { done, value } = readResult;
154
178
  if (done) {
155
179
  break;
156
180
  }
@@ -184,10 +208,55 @@ async function* streamChat(params) {
184
208
  }
185
209
  }
186
210
  }
211
+ function isRetryableError(error) {
212
+ return /Network error/i.test(error) || /HTTP 5\d\d/i.test(error) || /Stream stalled/i.test(error);
213
+ }
214
+ function sleep(ms) {
215
+ return new Promise((resolve) => setTimeout(resolve, ms));
216
+ }
217
+ async function* streamChatWithRetry(params, options) {
218
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
219
+ const buffer = [];
220
+ let retryableFailure = false;
221
+ for await (const event of streamChat(params)) {
222
+ if (event.type === "error") {
223
+ if (isRetryableError(event.error) && attempt < MAX_RETRIES - 1) {
224
+ log.warn("Retryable error, will retry", {
225
+ attempt: attempt + 1,
226
+ maxRetries: MAX_RETRIES,
227
+ error: event.error
228
+ });
229
+ options?.onRetry?.(attempt, event.error);
230
+ retryableFailure = true;
231
+ break;
232
+ }
233
+ yield event;
234
+ return;
235
+ }
236
+ buffer.push(event);
237
+ }
238
+ if (retryableFailure) {
239
+ if (params.signal?.aborted) {
240
+ return;
241
+ }
242
+ const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;
243
+ log.info("Retrying after backoff", { backoffMs: backoff });
244
+ await sleep(backoff);
245
+ continue;
246
+ }
247
+ for (const event of buffer) {
248
+ yield event;
249
+ }
250
+ return;
251
+ }
252
+ }
253
+ var MAX_RETRIES, INITIAL_BACKOFF_MS;
187
254
  var init_api = __esm({
188
255
  "src/api.ts"() {
189
256
  "use strict";
190
257
  init_logger();
258
+ MAX_RETRIES = 3;
259
+ INITIAL_BACKOFF_MS = 1e3;
191
260
  }
192
261
  });
193
262
 
@@ -634,10 +703,115 @@ var init_listSpecFiles = __esm({
634
703
  }
635
704
  });
636
705
 
637
- // src/tools/spec/setProjectOnboardingState.ts
706
+ // src/tools/spec/clearSyncStatus.ts
707
+ var clearSyncStatusTool;
708
+ var init_clearSyncStatus = __esm({
709
+ "src/tools/spec/clearSyncStatus.ts"() {
710
+ "use strict";
711
+ clearSyncStatusTool = {
712
+ definition: {
713
+ name: "clearSyncStatus",
714
+ description: "Clear the sync status flags after syncing spec and code. Call this after finishing a sync operation.",
715
+ inputSchema: {
716
+ type: "object",
717
+ properties: {}
718
+ }
719
+ },
720
+ async execute() {
721
+ return "ok";
722
+ }
723
+ };
724
+ }
725
+ });
726
+
727
+ // src/tools/spec/presentSyncPlan.ts
728
+ var presentSyncPlanTool;
729
+ var init_presentSyncPlan = __esm({
730
+ "src/tools/spec/presentSyncPlan.ts"() {
731
+ "use strict";
732
+ presentSyncPlanTool = {
733
+ definition: {
734
+ name: "presentSyncPlan",
735
+ 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.",
736
+ inputSchema: {
737
+ type: "object",
738
+ properties: {
739
+ content: {
740
+ type: "string",
741
+ description: "Markdown plan describing what changed and what will be updated."
742
+ }
743
+ },
744
+ required: ["content"]
745
+ }
746
+ },
747
+ streaming: {},
748
+ async execute() {
749
+ return "approved";
750
+ }
751
+ };
752
+ }
753
+ });
754
+
755
+ // src/tools/spec/presentPublishPlan.ts
756
+ var presentPublishPlanTool;
757
+ var init_presentPublishPlan = __esm({
758
+ "src/tools/spec/presentPublishPlan.ts"() {
759
+ "use strict";
760
+ presentPublishPlanTool = {
761
+ definition: {
762
+ name: "presentPublishPlan",
763
+ 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.",
764
+ inputSchema: {
765
+ type: "object",
766
+ properties: {
767
+ content: {
768
+ type: "string",
769
+ description: "Markdown changelog describing what changed and what will be deployed."
770
+ }
771
+ },
772
+ required: ["content"]
773
+ }
774
+ },
775
+ streaming: {},
776
+ async execute() {
777
+ return "approved";
778
+ }
779
+ };
780
+ }
781
+ });
782
+
783
+ // src/tools/spec/presentPlan.ts
784
+ var presentPlanTool;
785
+ var init_presentPlan = __esm({
786
+ "src/tools/spec/presentPlan.ts"() {
787
+ "use strict";
788
+ presentPlanTool = {
789
+ definition: {
790
+ name: "presentPlan",
791
+ 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.",
792
+ inputSchema: {
793
+ type: "object",
794
+ properties: {
795
+ content: {
796
+ type: "string",
797
+ description: "Markdown plan describing what you intend to do."
798
+ }
799
+ },
800
+ required: ["content"]
801
+ }
802
+ },
803
+ streaming: {},
804
+ async execute() {
805
+ return "approved";
806
+ }
807
+ };
808
+ }
809
+ });
810
+
811
+ // src/tools/common/setProjectOnboardingState.ts
638
812
  var setProjectOnboardingStateTool;
639
813
  var init_setProjectOnboardingState = __esm({
640
- "src/tools/spec/setProjectOnboardingState.ts"() {
814
+ "src/tools/common/setProjectOnboardingState.ts"() {
641
815
  "use strict";
642
816
  setProjectOnboardingStateTool = {
643
817
  definition: {
@@ -666,10 +840,10 @@ var init_setProjectOnboardingState = __esm({
666
840
  }
667
841
  });
668
842
 
669
- // src/tools/spec/promptUser.ts
843
+ // src/tools/common/promptUser.ts
670
844
  var promptUserTool;
671
845
  var init_promptUser = __esm({
672
- "src/tools/spec/promptUser.ts"() {
846
+ "src/tools/common/promptUser.ts"() {
673
847
  "use strict";
674
848
  promptUserTool = {
675
849
  definition: {
@@ -698,8 +872,8 @@ var init_promptUser = __esm({
698
872
  },
699
873
  type: {
700
874
  type: "string",
701
- enum: ["select", "checklist", "text", "file", "color"],
702
- 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)."
875
+ enum: ["select", "checklist", "text", "file"],
876
+ 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.'
703
877
  },
704
878
  helpText: {
705
879
  type: "string",
@@ -736,10 +910,6 @@ var init_promptUser = __esm({
736
910
  type: "boolean",
737
911
  description: "For file type: allow multiple uploads (returns array of URLs). Defaults to false."
738
912
  },
739
- allowOther: {
740
- type: "boolean",
741
- 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.'
742
- },
743
913
  format: {
744
914
  type: "string",
745
915
  enum: ["email", "url", "phone", "number"],
@@ -797,8 +967,6 @@ var init_promptUser = __esm({
797
967
  line += q.type === "checklist" ? ` (pick one or more: ${opts.join(" / ")})` : ` (${opts.join(" / ")})`;
798
968
  } else if (q.type === "file") {
799
969
  line += " (upload file)";
800
- } else if (q.type === "color") {
801
- line += " (pick a color)";
802
970
  }
803
971
  return line;
804
972
  });
@@ -809,141 +977,209 @@ ${lines.join("\n")}`;
809
977
  }
810
978
  });
811
979
 
812
- // src/tools/spec/clearSyncStatus.ts
813
- var clearSyncStatusTool;
814
- var init_clearSyncStatus = __esm({
815
- "src/tools/spec/clearSyncStatus.ts"() {
980
+ // src/tools/common/confirmDestructiveAction.ts
981
+ var confirmDestructiveActionTool;
982
+ var init_confirmDestructiveAction = __esm({
983
+ "src/tools/common/confirmDestructiveAction.ts"() {
816
984
  "use strict";
817
- clearSyncStatusTool = {
985
+ confirmDestructiveActionTool = {
818
986
  definition: {
819
- name: "clearSyncStatus",
820
- description: "Clear the sync status flags after syncing spec and code. Call this after finishing a sync operation.",
987
+ name: "confirmDestructiveAction",
988
+ 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.",
821
989
  inputSchema: {
822
990
  type: "object",
823
- properties: {}
991
+ properties: {
992
+ message: {
993
+ type: "string",
994
+ description: "Explanation of what is about to happen and why confirmation is needed."
995
+ },
996
+ confirmLabel: {
997
+ type: "string",
998
+ description: 'Custom label for the confirm button (e.g., "Delete", "Reset Database"). Defaults to "Confirm".'
999
+ },
1000
+ dismissLabel: {
1001
+ type: "string",
1002
+ description: 'Custom label for the dismiss button (e.g., "Keep It", "Go Back"). Defaults to "Cancel".'
1003
+ }
1004
+ },
1005
+ required: ["message"]
824
1006
  }
825
1007
  },
826
1008
  async execute() {
827
- return "ok";
1009
+ return "confirmed";
828
1010
  }
829
1011
  };
830
1012
  }
831
1013
  });
832
1014
 
833
- // src/tools/spec/presentSyncPlan.ts
834
- var presentSyncPlanTool;
835
- var init_presentSyncPlan = __esm({
836
- "src/tools/spec/presentSyncPlan.ts"() {
1015
+ // src/tools/common/askMindStudioSdk.ts
1016
+ import { exec } from "child_process";
1017
+ var askMindStudioSdkTool;
1018
+ var init_askMindStudioSdk = __esm({
1019
+ "src/tools/common/askMindStudioSdk.ts"() {
837
1020
  "use strict";
838
- presentSyncPlanTool = {
1021
+ askMindStudioSdkTool = {
839
1022
  definition: {
840
- name: "presentSyncPlan",
841
- 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.",
1023
+ name: "askMindStudioSdk",
1024
+ 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.",
842
1025
  inputSchema: {
843
1026
  type: "object",
844
1027
  properties: {
845
- content: {
1028
+ query: {
846
1029
  type: "string",
847
- description: "Markdown plan describing what changed and what will be updated."
1030
+ description: "Natural language question about the SDK."
848
1031
  }
849
1032
  },
850
- required: ["content"]
1033
+ required: ["query"]
851
1034
  }
852
1035
  },
853
- streaming: {},
854
- async execute() {
855
- return "approved";
1036
+ async execute(input) {
1037
+ const query = input.query;
1038
+ return new Promise((resolve) => {
1039
+ exec(
1040
+ `mindstudio ask ${JSON.stringify(query)}`,
1041
+ { timeout: 6e4, maxBuffer: 512 * 1024 },
1042
+ (err, stdout, stderr) => {
1043
+ if (stdout.trim()) {
1044
+ resolve(stdout.trim());
1045
+ return;
1046
+ }
1047
+ if (err) {
1048
+ resolve(`Error: ${stderr.trim() || err.message}`);
1049
+ return;
1050
+ }
1051
+ resolve("(no response)");
1052
+ }
1053
+ );
1054
+ });
856
1055
  }
857
1056
  };
858
1057
  }
859
1058
  });
860
1059
 
861
- // src/tools/spec/presentPublishPlan.ts
862
- var presentPublishPlanTool;
863
- var init_presentPublishPlan = __esm({
864
- "src/tools/spec/presentPublishPlan.ts"() {
1060
+ // src/tools/common/fetchUrl.ts
1061
+ import { exec as exec2 } from "child_process";
1062
+ var fetchUrlTool;
1063
+ var init_fetchUrl = __esm({
1064
+ "src/tools/common/fetchUrl.ts"() {
865
1065
  "use strict";
866
- presentPublishPlanTool = {
1066
+ fetchUrlTool = {
867
1067
  definition: {
868
- name: "presentPublishPlan",
869
- 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.",
1068
+ name: "scapeWebUrl",
1069
+ 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",
870
1070
  inputSchema: {
871
1071
  type: "object",
872
1072
  properties: {
873
- content: {
1073
+ url: {
874
1074
  type: "string",
875
- description: "Markdown changelog describing what changed and what will be deployed."
1075
+ description: "The URL to fetch."
1076
+ },
1077
+ screenshot: {
1078
+ type: "boolean",
1079
+ 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."
876
1080
  }
877
1081
  },
878
- required: ["content"]
1082
+ required: ["url"]
879
1083
  }
880
1084
  },
881
- streaming: {},
882
- async execute() {
883
- return "approved";
1085
+ async execute(input) {
1086
+ const url = input.url;
1087
+ const screenshot = input.screenshot;
1088
+ const pageOptions = { onlyMainContent: true };
1089
+ if (screenshot) {
1090
+ pageOptions.screenshot = true;
1091
+ }
1092
+ const cmd = `mindstudio scrape-url --url ${JSON.stringify(url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`;
1093
+ return new Promise((resolve) => {
1094
+ exec2(
1095
+ cmd,
1096
+ { timeout: 6e4, maxBuffer: 1024 * 1024 },
1097
+ (err, stdout, stderr) => {
1098
+ if (stdout.trim()) {
1099
+ resolve(stdout.trim());
1100
+ return;
1101
+ }
1102
+ if (err) {
1103
+ resolve(`Error: ${stderr.trim() || err.message}`);
1104
+ return;
1105
+ }
1106
+ resolve("(no response)");
1107
+ }
1108
+ );
1109
+ });
884
1110
  }
885
1111
  };
886
1112
  }
887
1113
  });
888
1114
 
889
- // src/tools/spec/presentPlan.ts
890
- var presentPlanTool;
891
- var init_presentPlan = __esm({
892
- "src/tools/spec/presentPlan.ts"() {
1115
+ // src/tools/common/searchGoogle.ts
1116
+ import { exec as exec3 } from "child_process";
1117
+ var searchGoogleTool;
1118
+ var init_searchGoogle = __esm({
1119
+ "src/tools/common/searchGoogle.ts"() {
893
1120
  "use strict";
894
- presentPlanTool = {
1121
+ searchGoogleTool = {
895
1122
  definition: {
896
- name: "presentPlan",
897
- 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.",
1123
+ name: "searchGoogle",
1124
+ description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help.",
898
1125
  inputSchema: {
899
1126
  type: "object",
900
1127
  properties: {
901
- content: {
1128
+ query: {
902
1129
  type: "string",
903
- description: "Markdown plan describing what you intend to do."
1130
+ description: "The search query."
904
1131
  }
905
1132
  },
906
- required: ["content"]
1133
+ required: ["query"]
907
1134
  }
908
1135
  },
909
- streaming: {},
910
- async execute() {
911
- return "approved";
912
- }
913
- };
914
- }
915
- });
916
-
917
- // src/tools/spec/confirmDestructiveAction.ts
918
- var confirmDestructiveActionTool;
919
- var init_confirmDestructiveAction = __esm({
920
- "src/tools/spec/confirmDestructiveAction.ts"() {
1136
+ async execute(input) {
1137
+ const query = input.query;
1138
+ const cmd = `mindstudio search-google --query ${JSON.stringify(query)} --export-type json --output-key results --no-meta`;
1139
+ return new Promise((resolve) => {
1140
+ exec3(
1141
+ cmd,
1142
+ { timeout: 6e4, maxBuffer: 512 * 1024 },
1143
+ (err, stdout, stderr) => {
1144
+ if (stdout.trim()) {
1145
+ resolve(stdout.trim());
1146
+ return;
1147
+ }
1148
+ if (err) {
1149
+ resolve(`Error: ${stderr.trim() || err.message}`);
1150
+ return;
1151
+ }
1152
+ resolve("(no response)");
1153
+ }
1154
+ );
1155
+ });
1156
+ }
1157
+ };
1158
+ }
1159
+ });
1160
+
1161
+ // src/tools/common/setProjectName.ts
1162
+ var setProjectNameTool;
1163
+ var init_setProjectName = __esm({
1164
+ "src/tools/common/setProjectName.ts"() {
921
1165
  "use strict";
922
- confirmDestructiveActionTool = {
1166
+ setProjectNameTool = {
923
1167
  definition: {
924
- name: "confirmDestructiveAction",
925
- 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.",
1168
+ name: "setProjectName",
1169
+ 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").`,
926
1170
  inputSchema: {
927
1171
  type: "object",
928
1172
  properties: {
929
- message: {
930
- type: "string",
931
- description: "Explanation of what is about to happen and why confirmation is needed."
932
- },
933
- confirmLabel: {
934
- type: "string",
935
- description: 'Custom label for the confirm button (e.g., "Delete", "Reset Database"). Defaults to "Confirm".'
936
- },
937
- dismissLabel: {
1173
+ name: {
938
1174
  type: "string",
939
- description: 'Custom label for the dismiss button (e.g., "Keep It", "Go Back"). Defaults to "Cancel".'
1175
+ description: "The project name."
940
1176
  }
941
1177
  },
942
- required: ["message"]
1178
+ required: ["name"]
943
1179
  }
944
1180
  },
945
1181
  async execute() {
946
- return "confirmed";
1182
+ return "ok";
947
1183
  }
948
1184
  };
949
1185
  }
@@ -952,9 +1188,9 @@ var init_confirmDestructiveAction = __esm({
952
1188
  // src/tools/code/readFile.ts
953
1189
  import fs6 from "fs/promises";
954
1190
  function isBinary(buffer) {
955
- const sample = buffer.subarray(0, 8192);
956
- for (let i = 0; i < sample.length; i++) {
957
- if (sample[i] === 0) {
1191
+ const sample2 = buffer.subarray(0, 8192);
1192
+ for (let i = 0; i < sample2.length; i++) {
1193
+ if (sample2[i] === 0) {
958
1194
  return true;
959
1195
  }
960
1196
  }
@@ -1273,7 +1509,7 @@ ${unifiedDiff(input.path, content, updated)}`;
1273
1509
  });
1274
1510
 
1275
1511
  // src/tools/code/bash.ts
1276
- import { exec } from "child_process";
1512
+ import { exec as exec4 } from "child_process";
1277
1513
  var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINES3, bashTool;
1278
1514
  var init_bash = __esm({
1279
1515
  "src/tools/code/bash.ts"() {
@@ -1311,7 +1547,7 @@ var init_bash = __esm({
1311
1547
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1312
1548
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1313
1549
  return new Promise((resolve) => {
1314
- exec(
1550
+ exec4(
1315
1551
  input.command,
1316
1552
  {
1317
1553
  timeout: timeoutMs,
@@ -1353,7 +1589,7 @@ var init_bash = __esm({
1353
1589
  });
1354
1590
 
1355
1591
  // src/tools/code/grep.ts
1356
- import { exec as exec2 } from "child_process";
1592
+ import { exec as exec5 } from "child_process";
1357
1593
  function formatResults(stdout, max) {
1358
1594
  const lines = stdout.trim().split("\n");
1359
1595
  let result = lines.join("\n");
@@ -1404,12 +1640,12 @@ var init_grep = __esm({
1404
1640
  const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1405
1641
  const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1406
1642
  return new Promise((resolve) => {
1407
- exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1643
+ exec5(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1408
1644
  if (stdout?.trim()) {
1409
1645
  resolve(formatResults(stdout, max));
1410
1646
  return;
1411
1647
  }
1412
- exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1648
+ exec5(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1413
1649
  if (grepStdout?.trim()) {
1414
1650
  resolve(formatResults(grepStdout, max));
1415
1651
  } else {
@@ -1669,51 +1905,6 @@ var init_restartProcess = __esm({
1669
1905
  }
1670
1906
  });
1671
1907
 
1672
- // src/tools/code/askMindStudioSdk.ts
1673
- import { exec as exec3 } from "child_process";
1674
- var askMindStudioSdkTool;
1675
- var init_askMindStudioSdk = __esm({
1676
- "src/tools/code/askMindStudioSdk.ts"() {
1677
- "use strict";
1678
- askMindStudioSdkTool = {
1679
- definition: {
1680
- name: "askMindStudioSdk",
1681
- 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.",
1682
- inputSchema: {
1683
- type: "object",
1684
- properties: {
1685
- query: {
1686
- type: "string",
1687
- description: "Natural language question about the SDK."
1688
- }
1689
- },
1690
- required: ["query"]
1691
- }
1692
- },
1693
- async execute(input) {
1694
- const query = input.query;
1695
- return new Promise((resolve) => {
1696
- exec3(
1697
- `mindstudio ask ${JSON.stringify(query)}`,
1698
- { timeout: 6e4, maxBuffer: 512 * 1024 },
1699
- (err, stdout, stderr) => {
1700
- if (stdout.trim()) {
1701
- resolve(stdout.trim());
1702
- return;
1703
- }
1704
- if (err) {
1705
- resolve(`Error: ${stderr.trim() || err.message}`);
1706
- return;
1707
- }
1708
- resolve("(no response)");
1709
- }
1710
- );
1711
- });
1712
- }
1713
- };
1714
- }
1715
- });
1716
-
1717
1908
  // src/tools/code/runScenario.ts
1718
1909
  var runScenarioTool;
1719
1910
  var init_runScenario = __esm({
@@ -1820,7 +2011,7 @@ async function runSubAgent(config) {
1820
2011
  const toolCalls = [];
1821
2012
  let stopReason = "end_turn";
1822
2013
  try {
1823
- for await (const event of streamChat({
2014
+ for await (const event of streamChatWithRetry({
1824
2015
  ...apiConfig,
1825
2016
  model,
1826
2017
  system,
@@ -1912,133 +2103,567 @@ async function runSubAgent(config) {
1912
2103
  });
1913
2104
  return { id: tc.id, result: errorMsg, isError: true };
1914
2105
  }
1915
- })
1916
- );
1917
- for (const r of results) {
1918
- messages.push({
1919
- role: "user",
1920
- content: r.result,
1921
- toolCallId: r.id,
1922
- isToolError: r.isError
1923
- });
1924
- }
1925
- }
1926
- }
1927
- var init_runner = __esm({
1928
- "src/subagents/runner.ts"() {
1929
- "use strict";
1930
- init_api();
1931
- init_logger();
1932
- }
1933
- });
1934
-
1935
- // src/subagents/browserAutomation/tools.ts
1936
- var BROWSER_TOOLS, BROWSER_EXTERNAL_TOOLS;
1937
- var init_tools = __esm({
1938
- "src/subagents/browserAutomation/tools.ts"() {
1939
- "use strict";
1940
- BROWSER_TOOLS = [
2106
+ })
2107
+ );
2108
+ for (const r of results) {
2109
+ messages.push({
2110
+ role: "user",
2111
+ content: r.result,
2112
+ toolCallId: r.id,
2113
+ isToolError: r.isError
2114
+ });
2115
+ }
2116
+ }
2117
+ }
2118
+ var init_runner = __esm({
2119
+ "src/subagents/runner.ts"() {
2120
+ "use strict";
2121
+ init_api();
2122
+ init_logger();
2123
+ }
2124
+ });
2125
+
2126
+ // src/subagents/browserAutomation/tools.ts
2127
+ var BROWSER_TOOLS, BROWSER_EXTERNAL_TOOLS;
2128
+ var init_tools = __esm({
2129
+ "src/subagents/browserAutomation/tools.ts"() {
2130
+ "use strict";
2131
+ BROWSER_TOOLS = [
2132
+ {
2133
+ name: "browserCommand",
2134
+ description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Timeout: 120s.",
2135
+ inputSchema: {
2136
+ type: "object",
2137
+ properties: {
2138
+ steps: {
2139
+ type: "array",
2140
+ items: {
2141
+ type: "object",
2142
+ properties: {
2143
+ command: {
2144
+ type: "string",
2145
+ enum: ["snapshot", "click", "type", "wait", "evaluate"],
2146
+ 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."
2147
+ },
2148
+ ref: {
2149
+ type: "string",
2150
+ description: "Element ref from the last snapshot (most reliable targeting)."
2151
+ },
2152
+ text: {
2153
+ type: "string",
2154
+ description: "For click/wait: match by accessible name or visible text. For type: the text to type."
2155
+ },
2156
+ role: {
2157
+ type: "string",
2158
+ description: "ARIA role to match (used with text for role+text targeting)."
2159
+ },
2160
+ label: {
2161
+ type: "string",
2162
+ description: "Find an input by its associated label text."
2163
+ },
2164
+ selector: {
2165
+ type: "string",
2166
+ description: "CSS selector fallback (last resort)."
2167
+ },
2168
+ clear: {
2169
+ type: "boolean",
2170
+ description: "For type: clear the field before typing."
2171
+ },
2172
+ timeout: {
2173
+ type: "number",
2174
+ description: "For wait: timeout in ms (default 5000)."
2175
+ },
2176
+ script: {
2177
+ type: "string",
2178
+ description: "For evaluate: JavaScript to run in the page."
2179
+ }
2180
+ },
2181
+ required: ["command"]
2182
+ }
2183
+ }
2184
+ },
2185
+ required: ["steps"]
2186
+ }
2187
+ },
2188
+ {
2189
+ name: "screenshot",
2190
+ description: "Capture a screenshot of the current page. Returns a CDN URL with dimensions.",
2191
+ inputSchema: {
2192
+ type: "object",
2193
+ properties: {}
2194
+ }
2195
+ }
2196
+ ];
2197
+ BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand", "screenshot"]);
2198
+ }
2199
+ });
2200
+
2201
+ // src/subagents/browserAutomation/prompt.ts
2202
+ import fs10 from "fs";
2203
+ import path4 from "path";
2204
+ var base, local, PROMPT_PATH, BROWSER_AUTOMATION_PROMPT;
2205
+ var init_prompt = __esm({
2206
+ "src/subagents/browserAutomation/prompt.ts"() {
2207
+ "use strict";
2208
+ base = import.meta.dirname ?? path4.dirname(new URL(import.meta.url).pathname);
2209
+ local = path4.join(base, "prompt.md");
2210
+ PROMPT_PATH = fs10.existsSync(local) ? local : path4.join(base, "subagents", "browserAutomation", "prompt.md");
2211
+ BROWSER_AUTOMATION_PROMPT = fs10.readFileSync(PROMPT_PATH, "utf-8").trim();
2212
+ }
2213
+ });
2214
+
2215
+ // src/subagents/browserAutomation/index.ts
2216
+ var browserAutomationTool;
2217
+ var init_browserAutomation = __esm({
2218
+ "src/subagents/browserAutomation/index.ts"() {
2219
+ "use strict";
2220
+ init_runner();
2221
+ init_tools();
2222
+ init_prompt();
2223
+ browserAutomationTool = {
2224
+ definition: {
2225
+ name: "runAutomatedBrowserTest",
2226
+ 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.",
2227
+ inputSchema: {
2228
+ type: "object",
2229
+ properties: {
2230
+ task: {
2231
+ type: "string",
2232
+ description: "What to test, in natural language. Include how to navigate to the relevant page and what data/roles to expect."
2233
+ }
2234
+ },
2235
+ required: ["task"]
2236
+ }
2237
+ },
2238
+ async execute(input, context) {
2239
+ if (!context) {
2240
+ return "Error: browser automation requires execution context (only available in headless mode)";
2241
+ }
2242
+ return runSubAgent({
2243
+ system: BROWSER_AUTOMATION_PROMPT,
2244
+ task: input.task,
2245
+ tools: BROWSER_TOOLS,
2246
+ externalTools: BROWSER_EXTERNAL_TOOLS,
2247
+ executeTool: async () => "Error: no local tools in browser automation",
2248
+ apiConfig: context.apiConfig,
2249
+ model: context.model,
2250
+ signal: context.signal,
2251
+ parentToolId: context.toolCallId,
2252
+ onEvent: context.onEvent,
2253
+ resolveExternalTool: context.resolveExternalTool
2254
+ });
2255
+ }
2256
+ };
2257
+ }
2258
+ });
2259
+
2260
+ // src/subagents/designExpert/tools.ts
2261
+ import { exec as exec6 } from "child_process";
2262
+ function runCli(cmd) {
2263
+ return new Promise((resolve) => {
2264
+ exec6(
2265
+ cmd,
2266
+ { timeout: 6e4, maxBuffer: 1024 * 1024 },
2267
+ (err, stdout, stderr) => {
2268
+ if (stdout.trim()) {
2269
+ resolve(stdout.trim());
2270
+ return;
2271
+ }
2272
+ if (err) {
2273
+ resolve(`Error: ${stderr.trim() || err.message}`);
2274
+ return;
2275
+ }
2276
+ resolve("(no response)");
2277
+ }
2278
+ );
2279
+ });
2280
+ }
2281
+ async function executeDesignTool(name, input) {
2282
+ switch (name) {
2283
+ case "searchGoogle":
2284
+ return runCli(
2285
+ `mindstudio search-google --query ${JSON.stringify(input.query)} --export-type json --output-key results --no-meta`
2286
+ );
2287
+ case "fetchUrl": {
2288
+ const pageOptions = { onlyMainContent: true };
2289
+ if (input.screenshot) {
2290
+ pageOptions.screenshot = true;
2291
+ }
2292
+ return runCli(
2293
+ `mindstudio scrape-url --url ${JSON.stringify(input.url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`
2294
+ );
2295
+ }
2296
+ case "analyzeImage":
2297
+ return runCli(
2298
+ `mindstudio analyze-image --prompt ${JSON.stringify(input.prompt)} --image-url ${JSON.stringify(input.imageUrl)} --no-meta`
2299
+ );
2300
+ case "analyzeDesignReference":
2301
+ return runCli(
2302
+ `mindstudio analyze-image --prompt ${JSON.stringify(DESIGN_REFERENCE_PROMPT)} --image-url ${JSON.stringify(input.imageUrl)} --no-meta`
2303
+ );
2304
+ case "screenshotAndAnalyze": {
2305
+ const screenshotResult = await runCli(
2306
+ `mindstudio scrape-url --url ${JSON.stringify(input.url)} --page-options ${JSON.stringify(JSON.stringify({ onlyMainContent: true, screenshot: true }))} --no-meta`
2307
+ );
2308
+ const screenshotMatch = screenshotResult.match(
2309
+ /https:\/\/[^\s"']+(?:\.png|\.jpg|\.jpeg|\.webp|screenshot[^\s"']*)/i
2310
+ );
2311
+ if (!screenshotMatch) {
2312
+ try {
2313
+ const parsed = JSON.parse(screenshotResult);
2314
+ const ssUrl = parsed.screenshot || parsed.screenshotUrl || parsed.content?.screenshotUrl;
2315
+ if (ssUrl) {
2316
+ const analysisPrompt2 = input.prompt || DESIGN_REFERENCE_PROMPT;
2317
+ const analysis2 = await runCli(
2318
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt2)} --image-url ${JSON.stringify(ssUrl)} --no-meta`
2319
+ );
2320
+ return `Screenshot: ${ssUrl}
2321
+
2322
+ ${analysis2}`;
2323
+ }
2324
+ } catch {
2325
+ }
2326
+ return `Fetched ${input.url} but could not extract screenshot URL.
2327
+
2328
+ Page content:
2329
+ ${screenshotResult}`;
2330
+ }
2331
+ const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
2332
+ const analysis = await runCli(
2333
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(screenshotMatch[0])} --no-meta`
2334
+ );
2335
+ return `Screenshot: ${screenshotMatch[0]}
2336
+
2337
+ ${analysis}`;
2338
+ }
2339
+ case "searchStockPhotos": {
2340
+ const encodedQuery = encodeURIComponent(input.query);
2341
+ return runCli(
2342
+ `mindstudio scrape-url --url "https://www.pexels.com/search/${encodedQuery}/" --page-options '{"onlyMainContent": true}' --no-meta`
2343
+ );
2344
+ }
2345
+ case "searchProductScreenshots": {
2346
+ const query = `${input.product} product screenshot UI 2026`;
2347
+ return runCli(
2348
+ `mindstudio search-google-images --query ${JSON.stringify(query)} --export-type json --output-key images --no-meta`
2349
+ );
2350
+ }
2351
+ case "generateImages": {
2352
+ const prompts = input.prompts;
2353
+ const width = input.width || 2048;
2354
+ const height = input.height || 2048;
2355
+ if (prompts.length === 1) {
2356
+ const step = JSON.stringify({
2357
+ prompt: prompts[0],
2358
+ imageModelOverride: {
2359
+ model: "seedream-4.5",
2360
+ config: { width, height }
2361
+ }
2362
+ });
2363
+ return runCli(
2364
+ `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`
2365
+ );
2366
+ }
2367
+ const steps = prompts.map((prompt) => ({
2368
+ stepType: "generateImage",
2369
+ step: {
2370
+ prompt,
2371
+ imageModelOverride: {
2372
+ model: "seedream-4.5",
2373
+ config: { width, height }
2374
+ }
2375
+ }
2376
+ }));
2377
+ return runCli(`mindstudio batch '${JSON.stringify(steps)}' --no-meta`);
2378
+ }
2379
+ default:
2380
+ return `Error: unknown tool "${name}"`;
2381
+ }
2382
+ }
2383
+ var DESIGN_REFERENCE_PROMPT, DESIGN_RESEARCH_TOOLS;
2384
+ var init_tools2 = __esm({
2385
+ "src/subagents/designExpert/tools.ts"() {
2386
+ "use strict";
2387
+ DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design reference. Assess:
2388
+ 1) Mood/aesthetic
2389
+ 2) Color palette with approximate hex values and palette strategy
2390
+ 3) Typography style
2391
+ 4) Layout composition (symmetric/asymmetric, grid structure, whitespace usage, content density)
2392
+ 5) What makes it distinctive and interesting vs generic AI-generated interfaces
2393
+ Be specific and concise.`;
2394
+ DESIGN_RESEARCH_TOOLS = [
2395
+ {
2396
+ name: "searchGoogle",
2397
+ description: "Search Google for web results. Use for finding design inspiration, font recommendations, UI patterns, real products in a domain, and reference material.",
2398
+ inputSchema: {
2399
+ type: "object",
2400
+ properties: {
2401
+ query: {
2402
+ type: "string",
2403
+ description: "The search query."
2404
+ }
2405
+ },
2406
+ required: ["query"]
2407
+ }
2408
+ },
2409
+ {
2410
+ name: "fetchUrl",
2411
+ 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.",
2412
+ inputSchema: {
2413
+ type: "object",
2414
+ properties: {
2415
+ url: {
2416
+ type: "string",
2417
+ description: "The URL to fetch."
2418
+ },
2419
+ screenshot: {
2420
+ type: "boolean",
2421
+ description: "Capture a screenshot of the page. Use when you need to see the visual design, not just the text."
2422
+ }
2423
+ },
2424
+ required: ["url"]
2425
+ }
2426
+ },
2427
+ {
2428
+ name: "analyzeImage",
2429
+ 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").',
2430
+ inputSchema: {
2431
+ type: "object",
2432
+ properties: {
2433
+ prompt: {
2434
+ type: "string",
2435
+ description: "What to analyze or extract from the image."
2436
+ },
2437
+ imageUrl: {
2438
+ type: "string",
2439
+ description: "URL of the image to analyze."
2440
+ }
2441
+ },
2442
+ required: ["prompt", "imageUrl"]
2443
+ }
2444
+ },
2445
+ {
2446
+ name: "analyzeDesignReference",
2447
+ 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.",
2448
+ inputSchema: {
2449
+ type: "object",
2450
+ properties: {
2451
+ imageUrl: {
2452
+ type: "string",
2453
+ description: "URL of the screenshot or design image to analyze."
2454
+ }
2455
+ },
2456
+ required: ["imageUrl"]
2457
+ }
2458
+ },
2459
+ {
2460
+ name: "screenshotAndAnalyze",
2461
+ 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.",
2462
+ inputSchema: {
2463
+ type: "object",
2464
+ properties: {
2465
+ url: {
2466
+ type: "string",
2467
+ description: "The URL to screenshot."
2468
+ },
2469
+ prompt: {
2470
+ type: "string",
2471
+ description: "Optional custom analysis prompt. If omitted, performs the standard design reference analysis."
2472
+ }
2473
+ },
2474
+ required: ["url"]
2475
+ }
2476
+ },
1941
2477
  {
1942
- name: "browserCommand",
1943
- description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Timeout: 120s.",
2478
+ name: "searchStockPhotos",
2479
+ description: 'Search Pexels for stock photos. Returns image URLs with descriptions. Use concrete, descriptive queries ("person working at laptop in modern office" not "business").',
1944
2480
  inputSchema: {
1945
2481
  type: "object",
1946
2482
  properties: {
1947
- steps: {
1948
- type: "array",
1949
- items: {
1950
- type: "object",
1951
- properties: {
1952
- command: {
1953
- type: "string",
1954
- enum: ["snapshot", "click", "type", "wait", "evaluate"],
1955
- 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."
1956
- },
1957
- ref: {
1958
- type: "string",
1959
- description: "Element ref from the last snapshot (most reliable targeting)."
1960
- },
1961
- text: {
1962
- type: "string",
1963
- description: "For click/wait: match by accessible name or visible text. For type: the text to type."
1964
- },
1965
- role: {
1966
- type: "string",
1967
- description: "ARIA role to match (used with text for role+text targeting)."
1968
- },
1969
- label: {
1970
- type: "string",
1971
- description: "Find an input by its associated label text."
1972
- },
1973
- selector: {
1974
- type: "string",
1975
- description: "CSS selector fallback (last resort)."
1976
- },
1977
- clear: {
1978
- type: "boolean",
1979
- description: "For type: clear the field before typing."
1980
- },
1981
- timeout: {
1982
- type: "number",
1983
- description: "For wait: timeout in ms (default 5000)."
1984
- },
1985
- script: {
1986
- type: "string",
1987
- description: "For evaluate: JavaScript to run in the page."
1988
- }
1989
- },
1990
- required: ["command"]
1991
- }
2483
+ query: {
2484
+ type: "string",
2485
+ description: "What kind of photo to search for."
1992
2486
  }
1993
2487
  },
1994
- required: ["steps"]
2488
+ required: ["query"]
1995
2489
  }
1996
2490
  },
1997
2491
  {
1998
- name: "screenshot",
1999
- description: "Capture a screenshot of the current page. Returns a CDN URL with dimensions.",
2492
+ name: "searchProductScreenshots",
2493
+ 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.',
2000
2494
  inputSchema: {
2001
2495
  type: "object",
2002
- properties: {}
2496
+ properties: {
2497
+ product: {
2498
+ type: "string",
2499
+ description: 'The product or app to find screenshots of (e.g., "stripe dashboard", "figma editor", "mercury banking app").'
2500
+ }
2501
+ },
2502
+ required: ["product"]
2503
+ }
2504
+ },
2505
+ {
2506
+ name: "generateImages",
2507
+ 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.",
2508
+ inputSchema: {
2509
+ type: "object",
2510
+ properties: {
2511
+ prompts: {
2512
+ type: "array",
2513
+ items: {
2514
+ type: "string"
2515
+ },
2516
+ description: "One or more image generation prompts. Be detailed: describe style, mood, composition, colors. Multiple prompts run in parallel."
2517
+ },
2518
+ width: {
2519
+ type: "number",
2520
+ description: "Image width in pixels. Default 2048. Range: 2048-4096."
2521
+ },
2522
+ height: {
2523
+ type: "number",
2524
+ description: "Image height in pixels. Default 2048. Range: 2048-4096."
2525
+ }
2526
+ },
2527
+ required: ["prompts"]
2003
2528
  }
2004
2529
  }
2005
2530
  ];
2006
- BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand", "screenshot"]);
2007
2531
  }
2008
2532
  });
2009
2533
 
2010
- // src/subagents/browserAutomation/prompt.ts
2011
- import fs10 from "fs";
2012
- import path4 from "path";
2013
- var base, local, PROMPT_PATH, BROWSER_AUTOMATION_PROMPT;
2014
- var init_prompt = __esm({
2015
- "src/subagents/browserAutomation/prompt.ts"() {
2534
+ // src/subagents/designExpert/prompt.ts
2535
+ import fs11 from "fs";
2536
+ import path5 from "path";
2537
+ function resolvePath(filename) {
2538
+ const local2 = path5.join(base2, filename);
2539
+ return fs11.existsSync(local2) ? local2 : path5.join(base2, "subagents", "designExpert", filename);
2540
+ }
2541
+ function readFile(filename) {
2542
+ return fs11.readFileSync(resolvePath(filename), "utf-8").trim();
2543
+ }
2544
+ function readJson(filename, fallback) {
2545
+ try {
2546
+ return JSON.parse(fs11.readFileSync(resolvePath(filename), "utf-8"));
2547
+ } catch {
2548
+ return fallback;
2549
+ }
2550
+ }
2551
+ function sample(arr, n) {
2552
+ if (arr.length <= n) {
2553
+ return [...arr];
2554
+ }
2555
+ const copy = [...arr];
2556
+ for (let i = copy.length - 1; i > 0; i--) {
2557
+ const j = Math.floor(Math.random() * (i + 1));
2558
+ [copy[i], copy[j]] = [copy[j], copy[i]];
2559
+ }
2560
+ return copy.slice(0, n);
2561
+ }
2562
+ function getDesignResearchPrompt() {
2563
+ const fonts = sample(fontData.fonts, 30);
2564
+ const pairings = sample(fontData.pairings, 20);
2565
+ const images = sample(inspirationImages, 40);
2566
+ const fontList = fonts.map((f) => {
2567
+ const tags = f.tags.length ? ` (${f.tags.join(", ")})` : "";
2568
+ let cssInfo = "";
2569
+ if (f.source === "fontshare") {
2570
+ cssInfo = ` CSS: ${fontData.cssUrlPattern.replace("{slug}", f.slug).replace("{weights}", f.weights.join(","))}`;
2571
+ } else if (f.cssUrl) {
2572
+ cssInfo = ` CSS: ${f.cssUrl}`;
2573
+ } else if (f.source === "open-foundry") {
2574
+ cssInfo = " (self-host required)";
2575
+ }
2576
+ return `- **${f.name}** \u2014 ${f.category}${tags}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}`;
2577
+ }).join("\n");
2578
+ const pairingList = pairings.map(
2579
+ (p) => `- **${p.heading.font}** (${p.heading.weight}) heading + **${p.body.font}** (${p.body.weight}) body`
2580
+ ).join("\n");
2581
+ const fontsSection = fonts.length ? `<fonts_to_consider>
2582
+ ## Fonts to consider
2583
+
2584
+ A random sample from Fontshare, Open Foundry, and Google Fonts. Use these as starting points for font selection.
2585
+ CSS URL pattern: ${fontData.cssUrlPattern}
2586
+
2587
+ ${fontList}
2588
+
2589
+ ### Suggested pairings
2590
+
2591
+ ${pairingList}
2592
+ </fonts_to_consider>` : "";
2593
+ const imageList = images.map((img) => `- ${img.analysis}`).join("\n\n");
2594
+ const inspirationSection = images.length ? `<inspiration_images>
2595
+ ## Design inspiration
2596
+
2597
+ A random sample of pre-analyzed design references. Use these observations to inform your recommendations.
2598
+
2599
+ ${imageList}
2600
+ </inspiration_images>` : "";
2601
+ return PROMPT_TEMPLATE.replace("{{fonts_to_consider}}", fontsSection).replace(
2602
+ "{{inspiration_images}}",
2603
+ inspirationSection
2604
+ );
2605
+ }
2606
+ var base2, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE, fontData, inspirationImages;
2607
+ var init_prompt2 = __esm({
2608
+ "src/subagents/designExpert/prompt.ts"() {
2016
2609
  "use strict";
2017
- base = import.meta.dirname ?? path4.dirname(new URL(import.meta.url).pathname);
2018
- local = path4.join(base, "prompt.md");
2019
- PROMPT_PATH = fs10.existsSync(local) ? local : path4.join(base, "subagents", "browserAutomation", "prompt.md");
2020
- BROWSER_AUTOMATION_PROMPT = fs10.readFileSync(PROMPT_PATH, "utf-8").trim();
2610
+ base2 = import.meta.dirname ?? path5.dirname(new URL(import.meta.url).pathname);
2611
+ RUNTIME_PLACEHOLDERS = /* @__PURE__ */ new Set([
2612
+ "fonts_to_consider",
2613
+ "inspiration_images"
2614
+ ]);
2615
+ PROMPT_TEMPLATE = readFile("prompt.md").replace(/\{\{([^}]+)\}\}/g, (match, key) => {
2616
+ const k = key.trim();
2617
+ return RUNTIME_PLACEHOLDERS.has(k) ? match : readFile(k);
2618
+ }).replace(/\n{3,}/g, "\n\n");
2619
+ fontData = readJson("data/fonts.json", {
2620
+ cssUrlPattern: "",
2621
+ fonts: [],
2622
+ pairings: []
2623
+ });
2624
+ inspirationImages = readJson("data/inspiration.json", {
2625
+ images: []
2626
+ }).images;
2021
2627
  }
2022
2628
  });
2023
2629
 
2024
- // src/subagents/browserAutomation/index.ts
2025
- var browserAutomationTool;
2026
- var init_browserAutomation = __esm({
2027
- "src/subagents/browserAutomation/index.ts"() {
2630
+ // src/subagents/designExpert/index.ts
2631
+ var DESCRIPTION, designExpertTool;
2632
+ var init_designExpert = __esm({
2633
+ "src/subagents/designExpert/index.ts"() {
2028
2634
  "use strict";
2029
2635
  init_runner();
2030
- init_tools();
2031
- init_prompt();
2032
- browserAutomationTool = {
2636
+ init_tools2();
2637
+ init_prompt2();
2638
+ DESCRIPTION = `
2639
+ 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.
2640
+
2641
+ The visual design expert can be used for all things visual design, from quick questions to comprehensive plans:
2642
+ - Font selection and pairings ("suggest fonts for a <x> app")
2643
+ - Color palettes from a mood, domain, or seed color ("earthy tones for a <x> brand")
2644
+ - Gradient, animation, and visual effect recommendations
2645
+ - Layout and composition ideas that go beyond generic AI defaults
2646
+ - Analyzing a reference site or screenshot for design insights (it can take screenshots and do research on its own)
2647
+ - Sourcing imagery: stock photos, AI-generated images, or product screenshots for reference
2648
+ - Icon recommendations
2649
+ - Proposing full visual directions during intake
2650
+
2651
+ **How to write the task:**
2652
+ 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.
2653
+
2654
+ **What it returns:**
2655
+ 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.
2656
+ `.trim();
2657
+ designExpertTool = {
2033
2658
  definition: {
2034
- name: "runAutomatedBrowserTest",
2035
- 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.",
2659
+ name: "visualDesignExpert",
2660
+ description: DESCRIPTION,
2036
2661
  inputSchema: {
2037
2662
  type: "object",
2038
2663
  properties: {
2039
2664
  task: {
2040
2665
  type: "string",
2041
- description: "What to test, in natural language. Include how to navigate to the relevant page and what data/roles to expect."
2666
+ description: "What you need, in natural language. Include context about the app when relevant."
2042
2667
  }
2043
2668
  },
2044
2669
  required: ["task"]
@@ -2046,14 +2671,14 @@ var init_browserAutomation = __esm({
2046
2671
  },
2047
2672
  async execute(input, context) {
2048
2673
  if (!context) {
2049
- return "Error: browser automation requires execution context (only available in headless mode)";
2674
+ return "Error: design research requires execution context";
2050
2675
  }
2051
2676
  return runSubAgent({
2052
- system: BROWSER_AUTOMATION_PROMPT,
2677
+ system: getDesignResearchPrompt(),
2053
2678
  task: input.task,
2054
- tools: BROWSER_TOOLS,
2055
- externalTools: BROWSER_EXTERNAL_TOOLS,
2056
- executeTool: async () => "Error: no local tools in browser automation",
2679
+ tools: DESIGN_RESEARCH_TOOLS,
2680
+ externalTools: /* @__PURE__ */ new Set(),
2681
+ executeTool: executeDesignTool,
2057
2682
  apiConfig: context.apiConfig,
2058
2683
  model: context.model,
2059
2684
  signal: context.signal,
@@ -2080,7 +2705,6 @@ function getCodeTools() {
2080
2705
  globTool,
2081
2706
  listDirTool,
2082
2707
  editsFinishedTool,
2083
- askMindStudioSdkTool,
2084
2708
  runScenarioTool,
2085
2709
  runMethodTool,
2086
2710
  screenshotTool,
@@ -2095,7 +2719,12 @@ function getCommonTools() {
2095
2719
  return [
2096
2720
  setProjectOnboardingStateTool,
2097
2721
  promptUserTool,
2098
- confirmDestructiveActionTool
2722
+ confirmDestructiveActionTool,
2723
+ askMindStudioSdkTool,
2724
+ fetchUrlTool,
2725
+ searchGoogleTool,
2726
+ setProjectNameTool,
2727
+ designExpertTool
2099
2728
  ];
2100
2729
  }
2101
2730
  function getPostOnboardingTools() {
@@ -2140,20 +2769,24 @@ function executeTool(name, input, context) {
2140
2769
  }
2141
2770
  return tool.execute(input, context);
2142
2771
  }
2143
- var init_tools2 = __esm({
2772
+ var init_tools3 = __esm({
2144
2773
  "src/tools/index.ts"() {
2145
2774
  "use strict";
2146
2775
  init_readSpec();
2147
2776
  init_writeSpec();
2148
2777
  init_editSpec();
2149
2778
  init_listSpecFiles();
2150
- init_setProjectOnboardingState();
2151
- init_promptUser();
2152
2779
  init_clearSyncStatus();
2153
2780
  init_presentSyncPlan();
2154
2781
  init_presentPublishPlan();
2155
2782
  init_presentPlan();
2783
+ init_setProjectOnboardingState();
2784
+ init_promptUser();
2156
2785
  init_confirmDestructiveAction();
2786
+ init_askMindStudioSdk();
2787
+ init_fetchUrl();
2788
+ init_searchGoogle();
2789
+ init_setProjectName();
2157
2790
  init_readFile();
2158
2791
  init_writeFile();
2159
2792
  init_editFile();
@@ -2165,19 +2798,19 @@ var init_tools2 = __esm({
2165
2798
  init_lsp();
2166
2799
  init_lspDiagnostics();
2167
2800
  init_restartProcess();
2168
- init_askMindStudioSdk();
2169
2801
  init_runScenario();
2170
2802
  init_runMethod();
2171
2803
  init_screenshot();
2172
2804
  init_browserAutomation();
2805
+ init_designExpert();
2173
2806
  }
2174
2807
  });
2175
2808
 
2176
2809
  // src/session.ts
2177
- import fs11 from "fs";
2810
+ import fs12 from "fs";
2178
2811
  function loadSession(state) {
2179
2812
  try {
2180
- const raw = fs11.readFileSync(SESSION_FILE, "utf-8");
2813
+ const raw = fs12.readFileSync(SESSION_FILE, "utf-8");
2181
2814
  const data = JSON.parse(raw);
2182
2815
  if (Array.isArray(data.messages) && data.messages.length > 0) {
2183
2816
  state.messages = sanitizeMessages(data.messages);
@@ -2219,7 +2852,7 @@ function sanitizeMessages(messages) {
2219
2852
  }
2220
2853
  function saveSession(state) {
2221
2854
  try {
2222
- fs11.writeFileSync(
2855
+ fs12.writeFileSync(
2223
2856
  SESSION_FILE,
2224
2857
  JSON.stringify({ messages: state.messages }, null, 2),
2225
2858
  "utf-8"
@@ -2230,7 +2863,7 @@ function saveSession(state) {
2230
2863
  function clearSession(state) {
2231
2864
  state.messages = [];
2232
2865
  try {
2233
- fs11.unlinkSync(SESSION_FILE);
2866
+ fs12.unlinkSync(SESSION_FILE);
2234
2867
  } catch {
2235
2868
  }
2236
2869
  }
@@ -2411,6 +3044,116 @@ var init_parsePartialJson = __esm({
2411
3044
  }
2412
3045
  });
2413
3046
 
3047
+ // src/statusWatcher.ts
3048
+ function startStatusWatcher(config) {
3049
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
3050
+ let lastLabel = "";
3051
+ let inflight = false;
3052
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3053
+ async function tick() {
3054
+ if (signal?.aborted || inflight) {
3055
+ return;
3056
+ }
3057
+ inflight = true;
3058
+ try {
3059
+ const ctx = getContext();
3060
+ if (!ctx.assistantText && !ctx.lastToolName) {
3061
+ log.debug("Status watcher: no context, skipping");
3062
+ return;
3063
+ }
3064
+ log.debug("Status watcher: requesting label", {
3065
+ textLength: ctx.assistantText.length,
3066
+ lastToolName: ctx.lastToolName
3067
+ });
3068
+ const res = await fetch(url, {
3069
+ method: "POST",
3070
+ headers: {
3071
+ "Content-Type": "application/json",
3072
+ Authorization: `Bearer ${apiConfig.apiKey}`
3073
+ },
3074
+ body: JSON.stringify({
3075
+ assistantText: ctx.assistantText.slice(-500),
3076
+ lastToolName: ctx.lastToolName,
3077
+ lastToolResult: ctx.lastToolResult?.slice(-200)
3078
+ }),
3079
+ signal
3080
+ });
3081
+ if (!res.ok) {
3082
+ log.debug("Status watcher: endpoint returned non-ok", {
3083
+ status: res.status
3084
+ });
3085
+ return;
3086
+ }
3087
+ const data = await res.json();
3088
+ if (!data.label) {
3089
+ log.debug("Status watcher: no label in response");
3090
+ return;
3091
+ }
3092
+ if (data.label === lastLabel) {
3093
+ log.debug("Status watcher: duplicate label, skipping", {
3094
+ label: data.label
3095
+ });
3096
+ return;
3097
+ }
3098
+ lastLabel = data.label;
3099
+ log.debug("Status watcher: emitting", { label: data.label });
3100
+ onStatus(data.label);
3101
+ } catch (err) {
3102
+ log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
3103
+ } finally {
3104
+ inflight = false;
3105
+ }
3106
+ }
3107
+ const timer = setInterval(tick, interval);
3108
+ tick().catch(() => {
3109
+ });
3110
+ log.debug("Status watcher started", { interval });
3111
+ return {
3112
+ stop() {
3113
+ clearInterval(timer);
3114
+ log.debug("Status watcher stopped");
3115
+ }
3116
+ };
3117
+ }
3118
+ var init_statusWatcher = __esm({
3119
+ "src/statusWatcher.ts"() {
3120
+ "use strict";
3121
+ init_logger();
3122
+ }
3123
+ });
3124
+
3125
+ // src/errors.ts
3126
+ function friendlyError(raw) {
3127
+ for (const [pattern, message] of patterns) {
3128
+ if (pattern.test(raw)) {
3129
+ return message;
3130
+ }
3131
+ }
3132
+ return `Something went wrong: ${raw}`;
3133
+ }
3134
+ var patterns;
3135
+ var init_errors = __esm({
3136
+ "src/errors.ts"() {
3137
+ "use strict";
3138
+ patterns = [
3139
+ [
3140
+ /Network error/i,
3141
+ "Lost connection to the server. Please check your internet connection and try again."
3142
+ ],
3143
+ [
3144
+ /HTTP 429|rate limit/i,
3145
+ "Too many requests. Please wait a moment and try again."
3146
+ ],
3147
+ [/HTTP 40[13]/i, "Authentication failed. Please check your API key."],
3148
+ [
3149
+ /HTTP 5\d\d/i,
3150
+ "The AI service is temporarily unavailable. Please try again."
3151
+ ],
3152
+ [/Stream stalled/i, "The connection was interrupted. Please try again."]
3153
+ ];
3154
+ }
3155
+ });
3156
+
2414
3157
  // src/agent.ts
2415
3158
  function createAgentState() {
2416
3159
  return { messages: [] };
@@ -2440,7 +3183,8 @@ async function runTurn(params) {
2440
3183
  }
2441
3184
  });
2442
3185
  onEvent({ type: "turn_started" });
2443
- const userMsg = { role: "user", content: userMessage };
3186
+ const cleanMessage = userMessage.replace(/^@@automated::[^@]*@@/, "");
3187
+ const userMsg = { role: "user", content: cleanMessage };
2444
3188
  if (hidden) {
2445
3189
  userMsg.hidden = true;
2446
3190
  }
@@ -2534,15 +3278,34 @@ async function runTurn(params) {
2534
3278
  onEvent({ type: "tool_input_delta", id, name, result: content });
2535
3279
  }
2536
3280
  }
3281
+ const statusWatcher = startStatusWatcher({
3282
+ apiConfig,
3283
+ getContext: () => ({
3284
+ assistantText: assistantText.slice(-500),
3285
+ lastToolName: toolCalls.at(-1)?.name
3286
+ }),
3287
+ onStatus: (label) => onEvent({ type: "status", message: label }),
3288
+ signal
3289
+ });
2537
3290
  try {
2538
- for await (const event of streamChat({
2539
- ...apiConfig,
2540
- model,
2541
- system,
2542
- messages: state.messages,
2543
- tools,
2544
- signal
2545
- })) {
3291
+ for await (const event of streamChatWithRetry(
3292
+ {
3293
+ ...apiConfig,
3294
+ model,
3295
+ system,
3296
+ messages: state.messages,
3297
+ tools,
3298
+ signal
3299
+ },
3300
+ {
3301
+ onRetry: (attempt) => {
3302
+ onEvent({
3303
+ type: "status",
3304
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
3305
+ });
3306
+ }
3307
+ }
3308
+ )) {
2546
3309
  if (signal?.aborted) {
2547
3310
  break;
2548
3311
  }
@@ -2610,7 +3373,7 @@ async function runTurn(params) {
2610
3373
  stopReason = event.stopReason;
2611
3374
  break;
2612
3375
  case "error":
2613
- onEvent({ type: "error", error: event.error });
3376
+ onEvent({ type: "error", error: friendlyError(event.error) });
2614
3377
  return;
2615
3378
  }
2616
3379
  }
@@ -2619,6 +3382,8 @@ async function runTurn(params) {
2619
3382
  } else {
2620
3383
  throw err;
2621
3384
  }
3385
+ } finally {
3386
+ statusWatcher.stop();
2622
3387
  }
2623
3388
  if (signal?.aborted) {
2624
3389
  if (assistantText) {
@@ -2646,6 +3411,15 @@ async function runTurn(params) {
2646
3411
  count: toolCalls.length,
2647
3412
  tools: toolCalls.map((tc) => tc.name)
2648
3413
  });
3414
+ const toolStatusWatcher = startStatusWatcher({
3415
+ apiConfig,
3416
+ getContext: () => ({
3417
+ assistantText: assistantText.slice(-500),
3418
+ lastToolName: toolCalls.map((tc) => tc.name).join(", ")
3419
+ }),
3420
+ onStatus: (label) => onEvent({ type: "status", message: label }),
3421
+ signal
3422
+ });
2649
3423
  const results = await Promise.all(
2650
3424
  toolCalls.map(async (tc) => {
2651
3425
  if (signal?.aborted) {
@@ -2703,6 +3477,7 @@ async function runTurn(params) {
2703
3477
  }
2704
3478
  })
2705
3479
  );
3480
+ toolStatusWatcher.stop();
2706
3481
  for (const r of results) {
2707
3482
  state.messages.push({
2708
3483
  role: "user",
@@ -2723,10 +3498,12 @@ var init_agent = __esm({
2723
3498
  "src/agent.ts"() {
2724
3499
  "use strict";
2725
3500
  init_api();
2726
- init_tools2();
3501
+ init_tools3();
2727
3502
  init_session();
2728
3503
  init_logger();
2729
3504
  init_parsePartialJson();
3505
+ init_statusWatcher();
3506
+ init_errors();
2730
3507
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
2731
3508
  "promptUser",
2732
3509
  "setProjectOnboardingState",
@@ -2738,18 +3515,19 @@ var init_agent = __esm({
2738
3515
  "runScenario",
2739
3516
  "runMethod",
2740
3517
  "browserCommand",
2741
- "screenshot"
3518
+ "screenshot",
3519
+ "setProjectName"
2742
3520
  ]);
2743
3521
  }
2744
3522
  });
2745
3523
 
2746
3524
  // src/prompt/static/projectContext.ts
2747
- import fs12 from "fs";
2748
- import path5 from "path";
3525
+ import fs13 from "fs";
3526
+ import path6 from "path";
2749
3527
  function loadProjectInstructions() {
2750
3528
  for (const file of AGENT_INSTRUCTION_FILES) {
2751
3529
  try {
2752
- const content = fs12.readFileSync(file, "utf-8").trim();
3530
+ const content = fs13.readFileSync(file, "utf-8").trim();
2753
3531
  if (content) {
2754
3532
  return `
2755
3533
  ## Project Instructions (${file})
@@ -2762,7 +3540,7 @@ ${content}`;
2762
3540
  }
2763
3541
  function loadProjectManifest() {
2764
3542
  try {
2765
- const manifest = fs12.readFileSync("mindstudio.json", "utf-8");
3543
+ const manifest = fs13.readFileSync("mindstudio.json", "utf-8");
2766
3544
  return `
2767
3545
  ## Project Manifest (mindstudio.json)
2768
3546
  \`\`\`json
@@ -2780,11 +3558,14 @@ function loadSpecFileMetadata() {
2780
3558
  }
2781
3559
  const entries = [];
2782
3560
  for (const filePath of files) {
2783
- const { name, description } = parseFrontmatter(filePath);
3561
+ const { name, description, type } = parseFrontmatter(filePath);
2784
3562
  let line = `- ${filePath}`;
2785
3563
  if (name) {
2786
3564
  line += ` \u2014 "${name}"`;
2787
3565
  }
3566
+ if (type) {
3567
+ line += ` (${type})`;
3568
+ }
2788
3569
  if (description) {
2789
3570
  line += ` \u2014 ${description}`;
2790
3571
  }
@@ -2800,9 +3581,9 @@ ${entries.join("\n")}`;
2800
3581
  function walkMdFiles(dir) {
2801
3582
  const results = [];
2802
3583
  try {
2803
- const entries = fs12.readdirSync(dir, { withFileTypes: true });
3584
+ const entries = fs13.readdirSync(dir, { withFileTypes: true });
2804
3585
  for (const entry of entries) {
2805
- const full = path5.join(dir, entry.name);
3586
+ const full = path6.join(dir, entry.name);
2806
3587
  if (entry.isDirectory()) {
2807
3588
  results.push(...walkMdFiles(full));
2808
3589
  } else if (entry.name.endsWith(".md")) {
@@ -2815,22 +3596,23 @@ function walkMdFiles(dir) {
2815
3596
  }
2816
3597
  function parseFrontmatter(filePath) {
2817
3598
  try {
2818
- const content = fs12.readFileSync(filePath, "utf-8");
3599
+ const content = fs13.readFileSync(filePath, "utf-8");
2819
3600
  const match = content.match(/^---\n([\s\S]*?)\n---/);
2820
3601
  if (!match) {
2821
- return { name: "", description: "" };
3602
+ return { name: "", description: "", type: "" };
2822
3603
  }
2823
3604
  const fm = match[1];
2824
3605
  const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
2825
3606
  const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
2826
- return { name, description };
3607
+ const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
3608
+ return { name, description, type };
2827
3609
  } catch {
2828
- return { name: "", description: "" };
3610
+ return { name: "", description: "", type: "" };
2829
3611
  }
2830
3612
  }
2831
3613
  function loadProjectFileListing() {
2832
3614
  try {
2833
- const entries = fs12.readdirSync(".", { withFileTypes: true });
3615
+ const entries = fs13.readdirSync(".", { withFileTypes: true });
2834
3616
  const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
2835
3617
  if (a.isDirectory() && !b.isDirectory()) {
2836
3618
  return -1;
@@ -2873,12 +3655,12 @@ var init_projectContext = __esm({
2873
3655
  });
2874
3656
 
2875
3657
  // src/prompt/index.ts
2876
- import fs13 from "fs";
2877
- import path6 from "path";
3658
+ import fs14 from "fs";
3659
+ import path7 from "path";
2878
3660
  function requireFile(filePath) {
2879
- const full = path6.join(PROMPT_DIR, filePath);
3661
+ const full = path7.join(PROMPT_DIR, filePath);
2880
3662
  try {
2881
- return fs13.readFileSync(full, "utf-8").trim();
3663
+ return fs14.readFileSync(full, "utf-8").trim();
2882
3664
  } catch {
2883
3665
  throw new Error(`Required prompt file missing: ${full}`);
2884
3666
  }
@@ -2998,22 +3780,22 @@ ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
2998
3780
  return resolveIncludes(template);
2999
3781
  }
3000
3782
  var PROMPT_DIR;
3001
- var init_prompt2 = __esm({
3783
+ var init_prompt3 = __esm({
3002
3784
  "src/prompt/index.ts"() {
3003
3785
  "use strict";
3004
3786
  init_lsp();
3005
3787
  init_projectContext();
3006
- PROMPT_DIR = import.meta.dirname ?? path6.dirname(new URL(import.meta.url).pathname);
3788
+ PROMPT_DIR = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
3007
3789
  }
3008
3790
  });
3009
3791
 
3010
3792
  // src/config.ts
3011
- import fs14 from "fs";
3012
- import path7 from "path";
3793
+ import fs15 from "fs";
3794
+ import path8 from "path";
3013
3795
  import os from "os";
3014
3796
  function loadConfigFile() {
3015
3797
  try {
3016
- const raw = fs14.readFileSync(CONFIG_PATH, "utf-8");
3798
+ const raw = fs15.readFileSync(CONFIG_PATH, "utf-8");
3017
3799
  log.debug("Loaded config file", { path: CONFIG_PATH });
3018
3800
  return JSON.parse(raw);
3019
3801
  } catch (err) {
@@ -3049,7 +3831,7 @@ var init_config = __esm({
3049
3831
  "src/config.ts"() {
3050
3832
  "use strict";
3051
3833
  init_logger();
3052
- CONFIG_PATH = path7.join(
3834
+ CONFIG_PATH = path8.join(
3053
3835
  os.homedir(),
3054
3836
  ".mindstudio-local-tunnel",
3055
3837
  "config.json"
@@ -3064,10 +3846,10 @@ __export(headless_exports, {
3064
3846
  startHeadless: () => startHeadless
3065
3847
  });
3066
3848
  import { createInterface } from "readline";
3067
- import fs15 from "fs";
3068
- import path8 from "path";
3849
+ import fs16 from "fs";
3850
+ import path9 from "path";
3069
3851
  function loadActionPrompt(name) {
3070
- return fs15.readFileSync(path8.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
3852
+ return fs16.readFileSync(path9.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
3071
3853
  }
3072
3854
  function emit(event, data) {
3073
3855
  process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
@@ -3095,6 +3877,7 @@ async function startHeadless(opts = {}) {
3095
3877
  }
3096
3878
  let running = false;
3097
3879
  let currentAbort = null;
3880
+ const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
3098
3881
  const pendingTools = /* @__PURE__ */ new Map();
3099
3882
  const earlyResults = /* @__PURE__ */ new Map();
3100
3883
  function onEvent(e) {
@@ -3149,6 +3932,9 @@ async function startHeadless(opts = {}) {
3149
3932
  case "error":
3150
3933
  emit("error", { error: e.error });
3151
3934
  break;
3935
+ case "status":
3936
+ emit("status", { message: e.message });
3937
+ break;
3152
3938
  }
3153
3939
  }
3154
3940
  function resolveExternalTool(id, _name, _input) {
@@ -3158,7 +3944,19 @@ async function startHeadless(opts = {}) {
3158
3944
  return Promise.resolve(early);
3159
3945
  }
3160
3946
  return new Promise((resolve) => {
3161
- pendingTools.set(id, { resolve });
3947
+ const timeout = setTimeout(() => {
3948
+ pendingTools.delete(id);
3949
+ resolve(
3950
+ "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
3951
+ );
3952
+ }, EXTERNAL_TOOL_TIMEOUT_MS);
3953
+ pendingTools.set(id, {
3954
+ resolve: (result) => {
3955
+ clearTimeout(timeout);
3956
+ resolve(result);
3957
+ },
3958
+ timeout
3959
+ });
3162
3960
  });
3163
3961
  }
3164
3962
  const rl = createInterface({ input: process.stdin });
@@ -3196,6 +3994,7 @@ async function startHeadless(opts = {}) {
3196
3994
  currentAbort.abort();
3197
3995
  }
3198
3996
  for (const [id, pending] of pendingTools) {
3997
+ clearTimeout(pending.timeout);
3199
3998
  pending.resolve("Error: cancelled");
3200
3999
  pendingTools.delete(id);
3201
4000
  }
@@ -3265,20 +4064,20 @@ var init_headless = __esm({
3265
4064
  "src/headless.ts"() {
3266
4065
  "use strict";
3267
4066
  init_config();
3268
- init_prompt2();
4067
+ init_prompt3();
3269
4068
  init_lsp();
3270
4069
  init_agent();
3271
4070
  init_session();
3272
- BASE_DIR = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
3273
- ACTIONS_DIR = path8.join(BASE_DIR, "actions");
4071
+ BASE_DIR = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
4072
+ ACTIONS_DIR = path9.join(BASE_DIR, "actions");
3274
4073
  }
3275
4074
  });
3276
4075
 
3277
4076
  // src/index.tsx
3278
4077
  import { render } from "ink";
3279
4078
  import os2 from "os";
3280
- import fs16 from "fs";
3281
- import path9 from "path";
4079
+ import fs17 from "fs";
4080
+ import path10 from "path";
3282
4081
 
3283
4082
  // src/tui/App.tsx
3284
4083
  import { useState as useState2, useCallback, useRef } from "react";
@@ -3413,7 +4212,7 @@ function MessageList({ turns }) {
3413
4212
 
3414
4213
  // src/tui/App.tsx
3415
4214
  init_agent();
3416
- init_prompt2();
4215
+ init_prompt3();
3417
4216
  init_session();
3418
4217
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3419
4218
  function App({ apiConfig, model }) {
@@ -3595,8 +4394,8 @@ for (let i = 0; i < args.length; i++) {
3595
4394
  }
3596
4395
  function printDebugInfo(config) {
3597
4396
  const pkg = JSON.parse(
3598
- fs16.readFileSync(
3599
- path9.join(import.meta.dirname, "..", "package.json"),
4397
+ fs17.readFileSync(
4398
+ path10.join(import.meta.dirname, "..", "package.json"),
3600
4399
  "utf-8"
3601
4400
  )
3602
4401
  );