@mindstudio-ai/remy 0.1.255 → 0.1.257

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -266,7 +266,7 @@ function isRetryableError(error) {
266
266
  return /Network error/i.test(error) || /HTTP 5\d\d/i.test(error) || /Stream stalled/i.test(error) || /overloaded/i.test(error) || /terminated/i.test(error);
267
267
  }
268
268
  function sleep(ms) {
269
- return new Promise((resolve2) => setTimeout(resolve2, ms));
269
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
270
270
  }
271
271
  async function* streamChatWithRetry(params, options) {
272
272
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -402,8 +402,8 @@ var init_readSpec = __esm({
402
402
  init_helpers();
403
403
  DEFAULT_MAX_LINES = 500;
404
404
  readSpecTool = {
405
- clearable: true,
406
405
  definition: {
406
+ clearable: true,
407
407
  name: "readSpec",
408
408
  description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
409
409
  inputSchema: {
@@ -535,8 +535,8 @@ var init_writeSpec = __esm({
535
535
  init_diff();
536
536
  init_fileLock();
537
537
  writeSpecTool = {
538
- clearable: true,
539
538
  definition: {
539
+ clearable: true,
540
540
  name: "writeSpec",
541
541
  description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
542
542
  inputSchema: {
@@ -686,8 +686,8 @@ var init_editSpec = __esm({
686
686
  init_diff();
687
687
  init_fileLock();
688
688
  editSpecTool = {
689
- clearable: true,
690
689
  definition: {
690
+ clearable: true,
691
691
  name: "editSpec",
692
692
  description: "Make a targeted find/replace edit to a spec file (src/*.md). old_string must appear exactly once (minor indentation differences are handled automatically); set replace_all to true to replace every occurrence. Read the file with readSpec first so you match the exact text, and include the full enclosing structure rather than an inner fragment. The file's YAML frontmatter (the leading --- \u2026 --- block, which holds required fields like name) is protected \u2014 an edit that would remove or malform it is refused. For a full rewrite, use writeSpec.",
693
693
  inputSchema: {
@@ -820,8 +820,8 @@ var init_listSpecFiles = __esm({
820
820
  "src/tools/spec/listSpecFiles.ts"() {
821
821
  "use strict";
822
822
  listSpecFilesTool = {
823
- clearable: false,
824
823
  definition: {
824
+ clearable: false,
825
825
  name: "listSpecFiles",
826
826
  description: "List all files in the src/ directory (spec files, brand guidelines, interface specs, references). Use this to understand what spec files exist before reading or editing them.",
827
827
  inputSchema: {
@@ -854,8 +854,8 @@ var init_presentPublishPlan = __esm({
854
854
  "src/tools/spec/presentPublishPlan.ts"() {
855
855
  "use strict";
856
856
  presentPublishPlanTool = {
857
- clearable: false,
858
857
  definition: {
858
+ clearable: false,
859
859
  name: "presentPublishPlan",
860
860
  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.",
861
861
  inputSchema: {
@@ -885,8 +885,8 @@ var init_writePlan = __esm({
885
885
  "use strict";
886
886
  PLAN_FILE = ".remy-plan.md";
887
887
  writePlanTool = {
888
- clearable: false,
889
888
  definition: {
889
+ clearable: false,
890
890
  name: "writePlan",
891
891
  description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, 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. The plan is displayed standalone in the UI with approve/reject buttons attached. The plan body ends with its last substantive section \u2014 do not write any closing line addressed to the reader. No 'If this lands, I'll start building,' no 'Let me know what to adjust,' no 'Approve when ready,' no trailing horizontal rule with a sign-off. Wrap-up text belongs in your chat message alongside the plan, not in the plan file. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
892
892
  inputSchema: {
@@ -922,8 +922,8 @@ var init_updatePlanStatus = __esm({
922
922
  "use strict";
923
923
  PLAN_FILE2 = ".remy-plan.md";
924
924
  updatePlanStatusTool = {
925
- clearable: false,
926
925
  definition: {
926
+ clearable: false,
927
927
  name: "updatePlanStatus",
928
928
  description: 'Update the status of the current implementation plan. Use when the user approves or rejects the plan via chat (e.g. "looks good, go ahead" or "scrap it"). Approving sets the plan to active so you can begin implementation. Rejecting deletes the plan.',
929
929
  inputSchema: {
@@ -970,8 +970,8 @@ var init_setProjectOnboardingState = __esm({
970
970
  "src/tools/common/setProjectOnboardingState.ts"() {
971
971
  "use strict";
972
972
  setProjectOnboardingStateTool = {
973
- clearable: false,
974
973
  definition: {
974
+ clearable: false,
975
975
  name: "setProjectOnboardingState",
976
976
  description: "Advance the project onboarding state. Forward-only: building \u2192 buildComplete \u2192 onboardingFinished. Normally driven by automated actions \u2014 don't call this out of order during a normal build, or you'll skip stages the user hasn't experienced. Exception: if the project has been in `building` for a while, the build is clearly done (the user is iterating on a working app, deploying, etc.), and the user reports the editor seems stuck \u2014 disabled Preview/Spec/Code tabs, no reveal, etc. \u2014 call `setProjectOnboardingState({ state: 'buildComplete' })` to unstick them. `onboardingFinished` is always set by the frontend after the user dismisses the reveal; never call it yourself.",
977
977
  inputSchema: {
@@ -999,8 +999,8 @@ var init_promptUser = __esm({
999
999
  "src/tools/common/promptUser.ts"() {
1000
1000
  "use strict";
1001
1001
  promptUserTool = {
1002
- clearable: false,
1003
1002
  definition: {
1003
+ clearable: false,
1004
1004
  name: "promptUser",
1005
1005
  description: 'Ask the user structured questions. Choose type first: "form" for structured intake (5+ questions, takes over screen), "inline" for quick clarifications or confirmations. Blocks until the user responds. Result contains `_dismissed: true` if the user dismisses without answering.',
1006
1006
  inputSchema: {
@@ -1137,8 +1137,8 @@ var init_confirmDestructiveAction = __esm({
1137
1137
  "src/tools/common/confirmDestructiveAction.ts"() {
1138
1138
  "use strict";
1139
1139
  confirmDestructiveActionTool = {
1140
- clearable: false,
1141
1140
  definition: {
1141
+ clearable: false,
1142
1142
  name: "confirmDestructiveAction",
1143
1143
  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 presentPublishPlan or writePlan (those already include approval). Do not use before onboarding state transitions.",
1144
1144
  inputSchema: {
@@ -1176,7 +1176,7 @@ function formatCliResult(r) {
1176
1176
  return logBlock + body + truncNote;
1177
1177
  }
1178
1178
  function runCli(command, args2, options) {
1179
- return new Promise((resolve2) => {
1179
+ return new Promise((resolve3) => {
1180
1180
  const timeout = options?.timeout ?? 6e4;
1181
1181
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1182
1182
  let finalArgs = args2;
@@ -1205,7 +1205,7 @@ function runCli(command, args2, options) {
1205
1205
  if (killTimer) {
1206
1206
  clearTimeout(killTimer);
1207
1207
  }
1208
- resolve2(result);
1208
+ resolve3(result);
1209
1209
  };
1210
1210
  const child = spawn(command, finalArgs, {
1211
1211
  stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
@@ -1324,8 +1324,8 @@ var init_sdkConsultant = __esm({
1324
1324
  "use strict";
1325
1325
  init_runCli();
1326
1326
  askMindStudioSdkTool = {
1327
- clearable: false,
1328
1327
  definition: {
1328
+ clearable: false,
1329
1329
  name: "askMindStudioSdk",
1330
1330
  description: "@mindstudio-ai/agent backend SDK expert. Knows every backend action, AI model, connector, and configuration option. Returns architectural guidance and working code. Only covers the backend SDK (@mindstudio-ai/agent) \u2014 do NOT use for frontend/interface SDK questions (@mindstudio-ai/interface) like file uploads, auth, or client-side APIs. Describe what you want to build, not just what API method you need. Batch related questions into a single query.",
1331
1331
  inputSchema: {
@@ -1486,8 +1486,8 @@ var init_searchGoogle = __esm({
1486
1486
  init_runMindstudioCli();
1487
1487
  init_runCli();
1488
1488
  searchGoogleTool = {
1489
- clearable: false,
1490
1489
  definition: {
1490
+ clearable: false,
1491
1491
  name: "searchGoogle",
1492
1492
  description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help.",
1493
1493
  inputSchema: {
@@ -1523,8 +1523,8 @@ var init_setProjectMetadata = __esm({
1523
1523
  "src/tools/common/setProjectMetadata.ts"() {
1524
1524
  "use strict";
1525
1525
  setProjectMetadataTool = {
1526
- clearable: false,
1527
1526
  definition: {
1527
+ clearable: false,
1528
1528
  name: "setProjectMetadata",
1529
1529
  description: "Set project metadata. Can update any combination of: display name, short description, app icon, and Open Graph share image. Provide only the fields you want to change.",
1530
1530
  inputSchema: {
@@ -1559,16 +1559,6 @@ var init_setProjectMetadata = __esm({
1559
1559
  // src/assets.ts
1560
1560
  import fs9 from "fs";
1561
1561
  import path3 from "path";
1562
- function findRoot(start) {
1563
- let dir = start;
1564
- while (dir !== path3.dirname(dir)) {
1565
- if (fs9.existsSync(path3.join(dir, "package.json"))) {
1566
- return dir;
1567
- }
1568
- dir = path3.dirname(dir);
1569
- }
1570
- return start;
1571
- }
1572
1562
  function assetPath(...segments) {
1573
1563
  return path3.join(ASSETS_BASE, ...segments);
1574
1564
  }
@@ -1588,14 +1578,11 @@ function readJsonAsset(fallback, ...segments) {
1588
1578
  return fallback;
1589
1579
  }
1590
1580
  }
1591
- var ROOT, ASSETS_BASE;
1581
+ var ASSETS_BASE;
1592
1582
  var init_assets = __esm({
1593
1583
  "src/assets.ts"() {
1594
1584
  "use strict";
1595
- ROOT = findRoot(
1596
- import.meta.dirname ?? path3.dirname(new URL(import.meta.url).pathname)
1597
- );
1598
- ASSETS_BASE = fs9.existsSync(path3.join(ROOT, "dist", "prompt")) ? path3.join(ROOT, "dist") : path3.join(ROOT, "src");
1585
+ ASSETS_BASE = import.meta.dirname ?? path3.dirname(new URL(import.meta.url).pathname);
1599
1586
  }
1600
1587
  });
1601
1588
 
@@ -2238,6 +2225,9 @@ function findLastSummaryCheckpoint(messages, name) {
2238
2225
  }
2239
2226
  return -1;
2240
2227
  }
2228
+ function portableToolCallId(id) {
2229
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
2230
+ }
2241
2231
  function fixOrphanedToolCalls(messages) {
2242
2232
  const toolResultIds = /* @__PURE__ */ new Set();
2243
2233
  for (const msg of messages) {
@@ -2329,14 +2319,30 @@ ${summaryBlock.text}
2329
2319
 
2330
2320
  ${content}` : attachmentHeader;
2331
2321
  }
2332
- return { ...rest, content };
2322
+ return {
2323
+ ...rest,
2324
+ content,
2325
+ ...msg.toolCallId && {
2326
+ toolCallId: portableToolCallId(msg.toolCallId)
2327
+ }
2328
+ };
2333
2329
  }
2334
2330
  if (!Array.isArray(msg.content)) {
2335
2331
  return msg;
2336
2332
  }
2337
2333
  const blocks = msg.content;
2338
2334
  const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2339
- const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
2335
+ const toolBlocks = blocks.filter(
2336
+ (b) => b.type === "tool"
2337
+ );
2338
+ const toolCalls = toolBlocks.map((b) => ({
2339
+ id: portableToolCallId(b.id),
2340
+ name: b.name,
2341
+ input: b.input
2342
+ }));
2343
+ const rewroteToolIds = toolBlocks.some(
2344
+ (b) => portableToolCallId(b.id) !== b.id
2345
+ );
2340
2346
  const cleaned2 = {
2341
2347
  role: msg.role,
2342
2348
  content: text
@@ -2344,7 +2350,7 @@ ${content}` : attachmentHeader;
2344
2350
  if (toolCalls.length > 0) {
2345
2351
  cleaned2.toolCalls = toolCalls;
2346
2352
  }
2347
- if (msg.providerMetadata) {
2353
+ if (msg.providerMetadata && !rewroteToolIds) {
2348
2354
  cleaned2.providerMetadata = msg.providerMetadata;
2349
2355
  }
2350
2356
  if (msg.hidden) {
@@ -2800,8 +2806,8 @@ var init_compactConversation = __esm({
2800
2806
  "use strict";
2801
2807
  init_trigger();
2802
2808
  compactConversationTool = {
2803
- clearable: false,
2804
2809
  definition: {
2810
+ clearable: false,
2805
2811
  name: "compactConversation",
2806
2812
  description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful. Runs in the background.",
2807
2813
  inputSchema: {
@@ -2825,8 +2831,136 @@ var init_compactConversation = __esm({
2825
2831
  }
2826
2832
  });
2827
2833
 
2834
+ // src/prompt/skills/catalog.ts
2835
+ import fs11 from "fs";
2836
+ import path5 from "path";
2837
+ function parseFrontmatter(content) {
2838
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
2839
+ if (!match) {
2840
+ return {};
2841
+ }
2842
+ const fields = {};
2843
+ for (const line of match[1].split("\n")) {
2844
+ const sep = line.indexOf(":");
2845
+ if (sep > 0) {
2846
+ fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
2847
+ }
2848
+ }
2849
+ return fields;
2850
+ }
2851
+ function loadCatalog() {
2852
+ let files;
2853
+ try {
2854
+ files = fs11.readdirSync(SKILLS_DIR).filter((f) => f.endsWith(".md"));
2855
+ } catch {
2856
+ return [];
2857
+ }
2858
+ const skills = [];
2859
+ for (const file of files.sort()) {
2860
+ const full = path5.join(SKILLS_DIR, file);
2861
+ const id = file.replace(/\.md$/, "");
2862
+ const fields = parseFrontmatter(fs11.readFileSync(full, "utf-8"));
2863
+ if (!fields.name || !fields.what || !fields.when) {
2864
+ continue;
2865
+ }
2866
+ skills.push({
2867
+ id,
2868
+ name: fields.name,
2869
+ what: fields.what,
2870
+ when: fields.when,
2871
+ path: full
2872
+ });
2873
+ }
2874
+ return skills;
2875
+ }
2876
+ function getSkill(id) {
2877
+ return SKILLS.find((s) => s.id === id);
2878
+ }
2879
+ function readSkillBody(skill) {
2880
+ return fs11.readFileSync(skill.path, "utf-8").replace(/^---[\s\S]*?---\s*/, "").trim();
2881
+ }
2882
+ function loadSkillsCatalog() {
2883
+ if (SKILLS.length === 0) {
2884
+ return "";
2885
+ }
2886
+ const entries = SKILLS.map(
2887
+ (s) => [
2888
+ `### ${s.name} (\`${s.id}\`)`,
2889
+ s.what,
2890
+ "",
2891
+ `When to load: ${s.when}`,
2892
+ `Reference: ${s.path}`
2893
+ ].join("\n")
2894
+ );
2895
+ return `<available_skills>
2896
+ Platform capabilities most apps don't use, so their references are kept out of this prompt rather than competing for your attention on every task \u2014 not because they're marginal.
2897
+
2898
+ Read what follows as part of what the platform can do, not as a lookup table. Recognising that one of these fits a feature is your job, and proposing one is fair game \u2014 several of them are the difference between an app that works and an app worth showing off. When a trigger fires, load the reference with loadSkill before writing the code rather than after. Loading is cheap and expected; guessing at one of these APIs is not.
2899
+
2900
+ A loaded reference drops out of the conversation once it ages out. Re-read it at the path listed with readFile whenever you need it again.
2901
+
2902
+ ${entries.join("\n\n")}
2903
+ </available_skills>`;
2904
+ }
2905
+ var SKILLS_DIR, SKILLS, SKILL_IDS;
2906
+ var init_catalog = __esm({
2907
+ "src/prompt/skills/catalog.ts"() {
2908
+ "use strict";
2909
+ init_assets();
2910
+ SKILLS_DIR = assetPath("prompt", "skills");
2911
+ SKILLS = loadCatalog();
2912
+ SKILL_IDS = SKILLS.map((s) => s.id);
2913
+ }
2914
+ });
2915
+
2916
+ // src/tools/common/loadSkill.ts
2917
+ var loadSkillTool;
2918
+ var init_loadSkill = __esm({
2919
+ "src/tools/common/loadSkill.ts"() {
2920
+ "use strict";
2921
+ init_catalog();
2922
+ loadSkillTool = {
2923
+ definition: {
2924
+ clearable: true,
2925
+ name: "loadSkill",
2926
+ description: "Load the full reference for a platform capability that isn't in your system prompt \u2014 task agents, agent interfaces, MCP interfaces, data sources. The available skills and the trigger for each are listed in <available_skills>. Load one before writing code in its area, not after: these are APIs where a plausible-looking guess is usually wrong. Calling this is cheap and expected \u2014 if you're unsure whether you need it, load it. Only covers the capabilities listed in the catalog; for backend SDK actions and model IDs use askMindStudioSdk.",
2927
+ inputSchema: {
2928
+ type: "object",
2929
+ properties: {
2930
+ skill: {
2931
+ type: "string",
2932
+ // Omitted when the catalog is empty: an empty enum is a schema no
2933
+ // provider accepts, and failing one tool call beats failing every
2934
+ // request if the docs ever go missing from a build.
2935
+ ...SKILL_IDS.length > 0 ? { enum: SKILL_IDS } : {},
2936
+ description: "The skill id, as listed in <available_skills>."
2937
+ }
2938
+ },
2939
+ required: ["skill"]
2940
+ }
2941
+ },
2942
+ async execute(input) {
2943
+ const id = String(input.skill ?? "");
2944
+ const skill = getSkill(id);
2945
+ if (!skill) {
2946
+ return `Error: unknown skill "${id}". Available: ${SKILL_IDS.join(", ") || "(none)"}`;
2947
+ }
2948
+ try {
2949
+ const body = readSkillBody(skill);
2950
+ return `${body}
2951
+
2952
+ ---
2953
+ This reference lives at ${skill.path}. Re-read it with readFile if you need it again later \u2014 it won't stay in the conversation.`;
2954
+ } catch (err) {
2955
+ return `Error loading skill "${id}": ${err.message}`;
2956
+ }
2957
+ }
2958
+ };
2959
+ }
2960
+ });
2961
+
2828
2962
  // src/tools/code/readFile.ts
2829
- import fs11 from "fs/promises";
2963
+ import fs12 from "fs/promises";
2830
2964
  function isBinary(buffer) {
2831
2965
  const sample = buffer.subarray(0, 8192);
2832
2966
  for (let i = 0; i < sample.length; i++) {
@@ -2843,8 +2977,8 @@ var init_readFile = __esm({
2843
2977
  DEFAULT_WINDOW = 500;
2844
2978
  MAX_BYTES = 64 * 1024;
2845
2979
  readFileTool = {
2846
- clearable: true,
2847
2980
  definition: {
2981
+ clearable: true,
2848
2982
  name: "readFile",
2849
2983
  description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. By default returns the first 500 lines, and at most 64KB \u2014 a file with very wide lines (a CSV, a minified bundle) comes back short of 500 lines, so read a narrower range or grep rather than paging through it. To read a specific range, pass startLine and endLine (1-indexed, inclusive) \u2014 e.g. to read lines 253\u2013343, pass startLine: 253, endLine: 343. To read the end of a file or log, pass tail (the number of lines from the end). Line numbers in the output correspond to what editFile expects. For a large file, locate the relevant section first (symbols or grep), then read just that range.",
2850
2984
  inputSchema: {
@@ -2852,7 +2986,7 @@ var init_readFile = __esm({
2852
2986
  properties: {
2853
2987
  path: {
2854
2988
  type: "string",
2855
- description: "The file path to read, relative to the project root."
2989
+ description: "The file path to read. Relative paths resolve from the project root; an absolute path is also accepted, which is how you re-read a reference doc from the path a loadSkill result gave you."
2856
2990
  },
2857
2991
  startLine: {
2858
2992
  type: "number",
@@ -2872,7 +3006,7 @@ var init_readFile = __esm({
2872
3006
  },
2873
3007
  async execute(input) {
2874
3008
  try {
2875
- const buffer = await fs11.readFile(input.path);
3009
+ const buffer = await fs12.readFile(input.path);
2876
3010
  if (isBinary(buffer)) {
2877
3011
  const size = buffer.length;
2878
3012
  const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
@@ -2946,8 +3080,8 @@ var init_readFile = __esm({
2946
3080
  });
2947
3081
 
2948
3082
  // src/tools/code/writeFile.ts
2949
- import fs12 from "fs/promises";
2950
- import path5 from "path";
3083
+ import fs13 from "fs/promises";
3084
+ import path6 from "path";
2951
3085
  var writeFileTool;
2952
3086
  var init_writeFile = __esm({
2953
3087
  "src/tools/code/writeFile.ts"() {
@@ -2955,8 +3089,8 @@ var init_writeFile = __esm({
2955
3089
  init_diff();
2956
3090
  init_fileLock();
2957
3091
  writeFileTool = {
2958
- clearable: true,
2959
3092
  definition: {
3093
+ clearable: true,
2960
3094
  name: "writeFile",
2961
3095
  description: "Create a new file or completely overwrite an existing one. Parent directories are created automatically. Use this for new files or full rewrites. For targeted changes to existing files, use editFile instead \u2014 it preserves the parts you don't want to change and avoids errors from forgetting to include unchanged code.",
2962
3096
  inputSchema: {
@@ -2990,7 +3124,7 @@ var init_writeFile = __esm({
2990
3124
  lastNewlineCount = newlineCount;
2991
3125
  const lastNewline = partial.content.lastIndexOf("\n");
2992
3126
  const completeContent = partial.content.substring(0, lastNewline + 1);
2993
- const oldContent = await fs12.readFile(partial.path, "utf-8").catch(() => "");
3127
+ const oldContent = await fs13.readFile(partial.path, "utf-8").catch(() => "");
2994
3128
  return `Writing ${partial.path} (${newlineCount} lines)
2995
3129
  ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2996
3130
  }
@@ -2999,13 +3133,13 @@ ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2999
3133
  async execute(input) {
3000
3134
  const release = await acquireFileLock(input.path);
3001
3135
  try {
3002
- await fs12.mkdir(path5.dirname(input.path), { recursive: true });
3136
+ await fs13.mkdir(path6.dirname(input.path), { recursive: true });
3003
3137
  let oldContent = null;
3004
3138
  try {
3005
- oldContent = await fs12.readFile(input.path, "utf-8");
3139
+ oldContent = await fs13.readFile(input.path, "utf-8");
3006
3140
  } catch {
3007
3141
  }
3008
- await fs12.writeFile(input.path, input.content, "utf-8");
3142
+ await fs13.writeFile(input.path, input.content, "utf-8");
3009
3143
  const lineCount = input.content.split("\n").length;
3010
3144
  const label = oldContent !== null ? "Wrote" : "Created";
3011
3145
  return `${label} ${input.path} (${lineCount} lines)
@@ -3021,7 +3155,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
3021
3155
  });
3022
3156
 
3023
3157
  // src/tools/code/editFile/index.ts
3024
- import fs13 from "fs/promises";
3158
+ import fs14 from "fs/promises";
3025
3159
  var editFileTool;
3026
3160
  var init_editFile = __esm({
3027
3161
  "src/tools/code/editFile/index.ts"() {
@@ -3030,8 +3164,8 @@ var init_editFile = __esm({
3030
3164
  init_fileLock();
3031
3165
  init_helpers2();
3032
3166
  editFileTool = {
3033
- clearable: true,
3034
3167
  definition: {
3168
+ clearable: true,
3035
3169
  name: "editFile",
3036
3170
  description: "Replace a string in a file. old_string must appear exactly once (minor indentation differences are handled automatically). Set replace_all to true to replace every occurrence at once. For bulk mechanical substitutions (renaming a variable, swapping colors), prefer replace_all. Always read the file first so you know the exact text to match. When editing nested structures (objects, function bodies, arrays, template literals), always include the full enclosing structure in old_string rather than just an inner fragment. Replacing a partial slice from the middle of nested code is the most common source of syntax errors.",
3037
3171
  inputSchema: {
@@ -3060,7 +3194,7 @@ var init_editFile = __esm({
3060
3194
  async execute(input) {
3061
3195
  const release = await acquireFileLock(input.path);
3062
3196
  try {
3063
- const content = await fs13.readFile(input.path, "utf-8");
3197
+ const content = await fs14.readFile(input.path, "utf-8");
3064
3198
  const { old_string, new_string, replace_all } = input;
3065
3199
  const occurrences = findOccurrences(content, old_string);
3066
3200
  if (replace_all) {
@@ -3076,7 +3210,7 @@ var init_editFile = __esm({
3076
3210
  new_string
3077
3211
  );
3078
3212
  }
3079
- await fs13.writeFile(input.path, updated, "utf-8");
3213
+ await fs14.writeFile(input.path, updated, "utf-8");
3080
3214
  return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
3081
3215
  ${unifiedDiff(input.path, content, updated)}`;
3082
3216
  }
@@ -3087,7 +3221,7 @@ ${unifiedDiff(input.path, content, updated)}`;
3087
3221
  old_string.length,
3088
3222
  new_string
3089
3223
  );
3090
- await fs13.writeFile(input.path, updated, "utf-8");
3224
+ await fs14.writeFile(input.path, updated, "utf-8");
3091
3225
  return `Updated ${input.path}
3092
3226
  ${unifiedDiff(input.path, content, updated)}`;
3093
3227
  }
@@ -3103,7 +3237,7 @@ ${unifiedDiff(input.path, content, updated)}`;
3103
3237
  flex.matchedText.length,
3104
3238
  new_string
3105
3239
  );
3106
- await fs13.writeFile(input.path, updated, "utf-8");
3240
+ await fs14.writeFile(input.path, updated, "utf-8");
3107
3241
  return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
3108
3242
  ${unifiedDiff(input.path, content, updated)}`;
3109
3243
  }
@@ -3129,7 +3263,7 @@ var init_projectRoot = __esm({
3129
3263
 
3130
3264
  // src/tools/code/bash.ts
3131
3265
  import { spawn as spawn2 } from "child_process";
3132
- import path6 from "path";
3266
+ import path7 from "path";
3133
3267
  var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINES2, MAX_OUTPUT_BYTES, bashTool;
3134
3268
  var init_bash = __esm({
3135
3269
  "src/tools/code/bash.ts"() {
@@ -3139,8 +3273,8 @@ var init_bash = __esm({
3139
3273
  DEFAULT_MAX_LINES2 = 500;
3140
3274
  MAX_OUTPUT_BYTES = 3e4;
3141
3275
  bashTool = {
3142
- clearable: true,
3143
3276
  definition: {
3277
+ clearable: true,
3144
3278
  name: "bash",
3145
3279
  description: "Run a shell command and return stdout + stderr. 120-second timeout by default (configurable). Use for: npm install/build/test, git operations, tsc --noEmit, or any CLI tool. Prefer dedicated tools over bash when available (use grep instead of bash + rg, readFile instead of bash + cat). Output is truncated to 500 lines or 30KB, whichever comes first. If a command would emit a lot of data, narrow it down (grep, head/tail, --short flags) rather than reading everything.",
3146
3280
  inputSchema: {
@@ -3169,11 +3303,11 @@ var init_bash = __esm({
3169
3303
  async execute(input, context) {
3170
3304
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
3171
3305
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
3172
- return new Promise((resolve2) => {
3306
+ return new Promise((resolve3) => {
3173
3307
  const child = spawn2("sh", ["-c", input.command], {
3174
3308
  // Pinned rather than inherited. `undefined` here means "wherever the
3175
3309
  // process happens to be", which is the project root only by luck.
3176
- cwd: input.cwd ? path6.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
3310
+ cwd: input.cwd ? path7.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
3177
3311
  env: { ...process.env, FORCE_COLOR: "1" }
3178
3312
  });
3179
3313
  let output = "";
@@ -3194,9 +3328,9 @@ var init_bash = __esm({
3194
3328
  clearTimeout(timer);
3195
3329
  if (!output) {
3196
3330
  if (code && code !== 0) {
3197
- resolve2(`Error: process exited with code ${code}`);
3331
+ resolve3(`Error: process exited with code ${code}`);
3198
3332
  } else {
3199
- resolve2("(no output)");
3333
+ resolve3("(no output)");
3200
3334
  }
3201
3335
  return;
3202
3336
  }
@@ -3222,18 +3356,18 @@ var init_bash = __esm({
3222
3356
  `${(MAX_OUTPUT_BYTES / 1024).toFixed(0)}KB of ${(totalBytes / 1024).toFixed(0)}KB`
3223
3357
  );
3224
3358
  }
3225
- resolve2(
3359
+ resolve3(
3226
3360
  truncated + `
3227
3361
 
3228
3362
  (truncated at ${reasons.join(" / ")} \u2014 narrow the command (grep, head/tail, smaller paths) instead of increasing limits)`
3229
3363
  );
3230
3364
  } else {
3231
- resolve2(output);
3365
+ resolve3(output);
3232
3366
  }
3233
3367
  });
3234
3368
  child.on("error", (err) => {
3235
3369
  clearTimeout(timer);
3236
- resolve2(`Error: ${err.message}`);
3370
+ resolve3(`Error: ${err.message}`);
3237
3371
  });
3238
3372
  });
3239
3373
  }
@@ -3267,8 +3401,8 @@ var init_grep = __esm({
3267
3401
  "use strict";
3268
3402
  DEFAULT_MAX = 50;
3269
3403
  grepTool = {
3270
- clearable: true,
3271
3404
  definition: {
3405
+ clearable: true,
3272
3406
  name: "grep",
3273
3407
  description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers (default 50 results). Use this to find where something is used, locate function definitions, or search for patterns across the codebase. Set outputMode to 'count' for per-file match counts (like grep -c) or 'filesWithMatches' for just the file paths (like grep -l). Add context (like grep -C), or contextBefore/contextAfter (like grep -B/-A), to include surrounding lines. Set caseInsensitive (like grep -i) for a case-insensitive search. For finding a symbol's definition precisely, prefer the definition tool if LSP is available. Automatically excludes node_modules and .git.",
3274
3408
  inputSchema: {
@@ -3357,17 +3491,17 @@ var init_grep = __esm({
3357
3491
  }
3358
3492
  const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
3359
3493
  const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
3360
- return new Promise((resolve2) => {
3494
+ return new Promise((resolve3) => {
3361
3495
  exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
3362
3496
  if (stdout?.trim()) {
3363
- resolve2(formatResults(stdout, max, mode));
3497
+ resolve3(formatResults(stdout, max, mode));
3364
3498
  return;
3365
3499
  }
3366
3500
  exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
3367
3501
  if (grepStdout?.trim()) {
3368
- resolve2(formatResults(grepStdout, max, mode));
3502
+ resolve3(formatResults(grepStdout, max, mode));
3369
3503
  } else {
3370
- resolve2("No matches found.");
3504
+ resolve3("No matches found.");
3371
3505
  }
3372
3506
  });
3373
3507
  });
@@ -3385,8 +3519,8 @@ var init_glob = __esm({
3385
3519
  "use strict";
3386
3520
  DEFAULT_MAX2 = 200;
3387
3521
  globTool = {
3388
- clearable: true,
3389
3522
  definition: {
3523
+ clearable: true,
3390
3524
  name: "glob",
3391
3525
  description: 'Find files matching a glob pattern. Returns matching file paths sorted alphabetically (default 200 results). Use this to discover project structure, find files by name or extension, or check if a file exists. Common patterns: "**/*.ts" (all TypeScript files), "src/**/*.tsx" (React components in src), "*.json" (root-level JSON files). Automatically excludes node_modules and .git.',
3392
3526
  inputSchema: {
@@ -3432,10 +3566,10 @@ var init_glob = __esm({
3432
3566
  });
3433
3567
 
3434
3568
  // src/tools/code/listDir.ts
3435
- import fs14 from "fs/promises";
3436
- import path7 from "path";
3569
+ import fs15 from "fs/promises";
3570
+ import path8 from "path";
3437
3571
  async function readAndSort(dirPath) {
3438
- const entries = await fs14.readdir(dirPath, { withFileTypes: true });
3572
+ const entries = await fs15.readdir(dirPath, { withFileTypes: true });
3439
3573
  return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
3440
3574
  if (a.isDirectory() && !b.isDirectory()) {
3441
3575
  return -1;
@@ -3448,7 +3582,7 @@ async function readAndSort(dirPath) {
3448
3582
  }
3449
3583
  async function collapsePath(basePath, name) {
3450
3584
  let display = name;
3451
- let current = path7.join(basePath, name);
3585
+ let current = path8.join(basePath, name);
3452
3586
  for (; ; ) {
3453
3587
  let children;
3454
3588
  try {
@@ -3458,7 +3592,7 @@ async function collapsePath(basePath, name) {
3458
3592
  }
3459
3593
  if (children.length === 1 && children[0].isDirectory()) {
3460
3594
  display += "/" + children[0].name;
3461
- current = path7.join(current, children[0].name);
3595
+ current = path8.join(current, children[0].name);
3462
3596
  } else {
3463
3597
  break;
3464
3598
  }
@@ -3476,8 +3610,8 @@ function formatSize(bytes) {
3476
3610
  }
3477
3611
  async function formatFile(dirPath, name, indent) {
3478
3612
  try {
3479
- const stat = await fs14.stat(path7.join(dirPath, name));
3480
- return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
3613
+ const stat2 = await fs15.stat(path8.join(dirPath, name));
3614
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat2.size)}`;
3481
3615
  } catch {
3482
3616
  return `${indent}${name}`;
3483
3617
  }
@@ -3489,8 +3623,8 @@ var init_listDir = __esm({
3489
3623
  EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
3490
3624
  MAX_CHILDREN = 15;
3491
3625
  listDirTool = {
3492
- clearable: true,
3493
3626
  definition: {
3627
+ clearable: true,
3494
3628
  name: "listDir",
3495
3629
  description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
3496
3630
  inputSchema: {
@@ -3572,8 +3706,8 @@ var init_editsFinished = __esm({
3572
3706
  "src/tools/code/editsFinished.ts"() {
3573
3707
  "use strict";
3574
3708
  editsFinishedTool = {
3575
- clearable: false,
3576
3709
  definition: {
3710
+ clearable: false,
3577
3711
  name: "editsFinished",
3578
3712
  description: "Signal that file edits are complete. Call this after you finish writing/editing files so the live preview updates cleanly. The preview is paused while you edit to avoid showing broken intermediate states \u2014 this unpauses it. If you forget to call this, the preview updates when your turn ends.",
3579
3713
  inputSchema: {
@@ -3599,6 +3733,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
3599
3733
  throw new Error("Sidecar not available");
3600
3734
  }
3601
3735
  const url = `${baseUrl}${endpoint}`;
3736
+ let data;
3602
3737
  try {
3603
3738
  const res = await fetch(url, {
3604
3739
  method: "POST",
@@ -3610,12 +3745,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
3610
3745
  log5.error("Sidecar error", { endpoint, status: res.status });
3611
3746
  throw new Error(`Sidecar error: ${res.status}`);
3612
3747
  }
3613
- const data = await res.json();
3614
- if (data?.success === false) {
3615
- const code = data.errorCode ? ` [${data.errorCode}]` : "";
3616
- throw new Error(`${data.error || "Unknown error"}${code}`);
3617
- }
3618
- return data;
3748
+ data = await res.json();
3619
3749
  } catch (err) {
3620
3750
  if (err.message.startsWith("Sidecar error")) {
3621
3751
  throw err;
@@ -3623,6 +3753,16 @@ async function sidecarRequest(endpoint, body = {}, options) {
3623
3753
  log5.error("Sidecar connection error", { endpoint, error: err.message });
3624
3754
  throw new Error(`Sidecar connection error: ${err.message}`);
3625
3755
  }
3756
+ if (data?.success === false) {
3757
+ const code = data.errorCode ? ` [${data.errorCode}]` : "";
3758
+ log5.error("Sidecar command failed", {
3759
+ endpoint,
3760
+ error: data.error,
3761
+ errorCode: data.errorCode
3762
+ });
3763
+ throw new Error(`${data.error || "Unknown error"}${code}`);
3764
+ }
3765
+ return data;
3626
3766
  }
3627
3767
  var log5, baseUrl;
3628
3768
  var init_sidecar = __esm({
@@ -3654,8 +3794,8 @@ var init_lspDiagnostics = __esm({
3654
3794
  "use strict";
3655
3795
  init_lsp();
3656
3796
  lspDiagnosticsTool = {
3657
- clearable: true,
3658
3797
  definition: {
3798
+ clearable: true,
3659
3799
  name: "lspDiagnostics",
3660
3800
  description: "Get TypeScript diagnostics (type errors, warnings) for a file, with suggested fixes when available. Use this after editing a file to check for errors.",
3661
3801
  inputSchema: {
@@ -3710,8 +3850,8 @@ var init_restartProcess = __esm({
3710
3850
  "use strict";
3711
3851
  init_lsp();
3712
3852
  restartProcessTool = {
3713
- clearable: false,
3714
3853
  definition: {
3854
+ clearable: false,
3715
3855
  name: "restartProcess",
3716
3856
  description: "Restart a managed sandbox process. Use this after running npm install or changing package.json to restart the dev server so it picks up new dependencies.",
3717
3857
  inputSchema: {
@@ -3728,7 +3868,7 @@ var init_restartProcess = __esm({
3728
3868
  async execute(input) {
3729
3869
  const data = await lspRequest("/restart-process", { name: input.name });
3730
3870
  if (data.ok) {
3731
- await new Promise((resolve2) => setTimeout(resolve2, 5e3));
3871
+ await new Promise((resolve3) => setTimeout(resolve3, 5e3));
3732
3872
  return `Restarted ${input.name}.`;
3733
3873
  }
3734
3874
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -3743,8 +3883,8 @@ var init_runScenario = __esm({
3743
3883
  "src/tools/code/runScenario.ts"() {
3744
3884
  "use strict";
3745
3885
  runScenarioTool = {
3746
- clearable: true,
3747
3886
  definition: {
3887
+ clearable: true,
3748
3888
  name: "runScenario",
3749
3889
  description: "Run a scenario to seed the dev database with test data. By default truncates all tables first, then executes the seed function and impersonates the scenario roles. Use skipTruncate to run the seed function against existing data without resetting. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details. Return synchronously - no need to sleep before checking results.",
3750
3890
  inputSchema: {
@@ -3775,8 +3915,8 @@ var init_runMethod = __esm({
3775
3915
  "src/tools/code/runMethod.ts"() {
3776
3916
  "use strict";
3777
3917
  runMethodTool = {
3778
- clearable: true,
3779
3918
  definition: {
3919
+ clearable: true,
3780
3920
  name: "runMethod",
3781
3921
  description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
3782
3922
  inputSchema: {
@@ -3816,8 +3956,8 @@ var init_queryDatabase = __esm({
3816
3956
  "src/tools/code/queryDatabase.ts"() {
3817
3957
  "use strict";
3818
3958
  queryDatabaseTool = {
3819
- clearable: true,
3820
3959
  definition: {
3960
+ clearable: true,
3821
3961
  name: "queryDatabase",
3822
3962
  description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
3823
3963
  inputSchema: {
@@ -3838,26 +3978,136 @@ var init_queryDatabase = __esm({
3838
3978
  }
3839
3979
  });
3840
3980
 
3981
+ // src/tools/_helpers/uploadImage.ts
3982
+ import { readFile, stat } from "fs/promises";
3983
+ import { basename, extname, resolve } from "path";
3984
+ function isFetchableUrl(ref) {
3985
+ return /^(?:https?:|data:)/i.test(ref);
3986
+ }
3987
+ async function resolveImageRef(ref, apiConfig) {
3988
+ const trimmed = ref.trim();
3989
+ return isFetchableUrl(trimmed) ? trimmed : uploadLocalImage(trimmed, apiConfig);
3990
+ }
3991
+ function resolveImageRefs(refs, apiConfig) {
3992
+ return Promise.all(refs.map((ref) => resolveImageRef(ref, apiConfig)));
3993
+ }
3994
+ async function uploadLocalImage(localPath, apiConfig) {
3995
+ const absolute = resolve(PROJECT_ROOT, localPath);
3996
+ const ext = extname(absolute).toLowerCase();
3997
+ const contentType = CONTENT_TYPES[ext];
3998
+ if (!contentType) {
3999
+ throw new Error(
4000
+ `Cannot use "${localPath}" as an image \u2014 vision models read ${Object.keys(
4001
+ CONTENT_TYPES
4002
+ ).join(", ")}. Convert it first, or pass an image URL.`
4003
+ );
4004
+ }
4005
+ let stats;
4006
+ try {
4007
+ stats = await stat(absolute);
4008
+ } catch {
4009
+ throw new Error(
4010
+ `No file at "${localPath}". Paths resolve from the project root; list the directory to check the name, or pass an image URL.`
4011
+ );
4012
+ }
4013
+ const cacheKey = `${absolute}:${stats.mtimeMs}:${stats.size}`;
4014
+ const cached3 = hosted.get(cacheKey);
4015
+ if (cached3) {
4016
+ return cached3;
4017
+ }
4018
+ if (!apiConfig?.appId) {
4019
+ throw new Error(
4020
+ `Cannot host "${localPath}" for analysis \u2014 this session has no app id. Pass an image URL instead.`
4021
+ );
4022
+ }
4023
+ const bytes = await readFile(absolute);
4024
+ const target = await requestUploadTarget(apiConfig, ext.slice(1));
4025
+ const form = new FormData();
4026
+ for (const [field, value] of Object.entries(target.uploadFields)) {
4027
+ form.append(field, value);
4028
+ }
4029
+ form.append(
4030
+ "file",
4031
+ new Blob([bytes], { type: contentType }),
4032
+ basename(absolute)
4033
+ );
4034
+ const res = await fetch(target.uploadUrl, {
4035
+ method: "POST",
4036
+ body: form,
4037
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
4038
+ });
4039
+ if (!res.ok) {
4040
+ throw new Error(`Upload of "${localPath}" failed: HTTP ${res.status}`);
4041
+ }
4042
+ log6.info("Local image hosted", {
4043
+ path: localPath,
4044
+ bytes: stats.size,
4045
+ url: target.publicUrl
4046
+ });
4047
+ hosted.set(cacheKey, target.publicUrl);
4048
+ return target.publicUrl;
4049
+ }
4050
+ async function requestUploadTarget(apiConfig, extension) {
4051
+ const url = `${apiConfig.baseUrl}/_internal/v2/apps/${apiConfig.appId}/dev/manage/upload`;
4052
+ const res = await fetch(url, {
4053
+ method: "POST",
4054
+ headers: {
4055
+ "Content-Type": "application/json",
4056
+ Authorization: `Bearer ${apiConfig.apiKey}`
4057
+ },
4058
+ body: JSON.stringify({ extension }),
4059
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
4060
+ });
4061
+ if (!res.ok) {
4062
+ throw new Error(`Could not get an upload URL: HTTP ${res.status}`);
4063
+ }
4064
+ return await res.json();
4065
+ }
4066
+ var log6, UPLOAD_TIMEOUT_MS, CONTENT_TYPES, hosted;
4067
+ var init_uploadImage = __esm({
4068
+ "src/tools/_helpers/uploadImage.ts"() {
4069
+ "use strict";
4070
+ init_projectRoot();
4071
+ init_logger();
4072
+ log6 = createLogger("uploadImage");
4073
+ UPLOAD_TIMEOUT_MS = 6e4;
4074
+ CONTENT_TYPES = {
4075
+ ".png": "image/png",
4076
+ ".jpg": "image/jpeg",
4077
+ ".jpeg": "image/jpeg",
4078
+ ".gif": "image/gif",
4079
+ ".webp": "image/webp"
4080
+ };
4081
+ hosted = /* @__PURE__ */ new Map();
4082
+ }
4083
+ });
4084
+
3841
4085
  // src/subagents/common/analyzeImage.ts
3842
4086
  async function analyzeImage(params) {
3843
- const { prompt, imageUrl, model, timeout = 2e5, onLog } = params;
3844
- return runMindstudioCli(
4087
+ const { prompt, image, apiConfig, model, timeout = 2e5, onLog } = params;
4088
+ const url = await resolveImageRef(image, apiConfig);
4089
+ const result = await runMindstudioCliResult(
3845
4090
  [
3846
4091
  "analyze-image",
3847
4092
  "--prompt",
3848
4093
  prompt,
3849
4094
  "--image-url",
3850
- imageUrl,
4095
+ url,
3851
4096
  "--vision-model-override",
3852
4097
  JSON.stringify({ model })
3853
4098
  ],
3854
4099
  { outputKey: "analysis", timeout, onLog }
3855
4100
  );
4101
+ if (!result.ok) {
4102
+ throw new Error(`Could not analyze ${url}: ${result.value}`);
4103
+ }
4104
+ return { url, analysis: result.value };
3856
4105
  }
3857
4106
  var init_analyzeImage = __esm({
3858
4107
  "src/subagents/common/analyzeImage.ts"() {
3859
4108
  "use strict";
3860
4109
  init_runMindstudioCli();
4110
+ init_uploadImage();
3861
4111
  }
3862
4112
  });
3863
4113
 
@@ -3879,13 +4129,14 @@ ${ANALYSIS_RESPONSE_FORMAT}`;
3879
4129
  return p;
3880
4130
  }
3881
4131
  async function streamScreenshotAnalysis(opts) {
3882
- const { url, prompt, styleMap, onLog, model } = opts;
4132
+ const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
4133
+ const url = await resolveImageRef(image, apiConfig);
3883
4134
  onLog?.(JSON.stringify({ url, analysis: null }));
3884
4135
  const analysisPrompt = buildScreenshotAnalysisPrompt({ prompt, styleMap });
3885
4136
  let accumulated = "";
3886
- const analysis = await analyzeImage({
4137
+ const { analysis } = await analyzeImage({
3887
4138
  prompt: analysisPrompt,
3888
- imageUrl: url,
4139
+ image: url,
3889
4140
  model,
3890
4141
  onLog: (chunk) => {
3891
4142
  accumulated += chunk;
@@ -3896,18 +4147,19 @@ async function streamScreenshotAnalysis(opts) {
3896
4147
  }
3897
4148
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
3898
4149
  let prompt;
3899
- let existingUrl;
4150
+ let existingImage;
3900
4151
  let onLog;
3901
4152
  let model;
3902
- let path14;
4153
+ let apiConfig;
4154
+ let path15;
3903
4155
  let fullPage = true;
3904
4156
  let width;
3905
4157
  let height;
3906
4158
  let format;
3907
4159
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
3908
4160
  prompt = promptOrOptions.prompt;
3909
- existingUrl = promptOrOptions.imageUrl;
3910
- path14 = promptOrOptions.path;
4161
+ existingImage = promptOrOptions.image;
4162
+ path15 = promptOrOptions.path;
3911
4163
  if (promptOrOptions.fullPage !== void 0) {
3912
4164
  fullPage = promptOrOptions.fullPage;
3913
4165
  }
@@ -3916,6 +4168,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3916
4168
  format = promptOrOptions.format;
3917
4169
  onLog = promptOrOptions.onLog;
3918
4170
  model = promptOrOptions.model;
4171
+ apiConfig = promptOrOptions.apiConfig;
3919
4172
  } else {
3920
4173
  prompt = promptOrOptions;
3921
4174
  }
@@ -3924,18 +4177,20 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3924
4177
  }
3925
4178
  let url;
3926
4179
  let styleMap;
3927
- if (existingUrl) {
3928
- url = existingUrl;
4180
+ if (existingImage) {
4181
+ url = existingImage;
3929
4182
  } else {
3930
4183
  const ssResult = await sidecarRequest(
3931
4184
  fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
3932
4185
  {
3933
- ...path14 ? { path: path14 } : {},
4186
+ ...path15 ? { path: path15 } : {},
3934
4187
  ...width != null ? { width } : {},
3935
4188
  ...height != null ? { height } : {},
3936
4189
  ...format ? { format } : {}
3937
4190
  },
3938
- { timeout: fullPage ? 12e4 : 3e4 }
4191
+ {
4192
+ timeout: fullPage ? FULLPAGE_CAPTURE_TIMEOUT_MS : VIEWPORT_CAPTURE_TIMEOUT_MS
4193
+ }
3939
4194
  );
3940
4195
  url = ssResult?.url || ssResult?.screenshotUrl;
3941
4196
  if (!url) {
@@ -3946,7 +4201,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3946
4201
  styleMap = ssResult?.styleMap;
3947
4202
  }
3948
4203
  if (prompt === false) {
3949
- return url;
4204
+ return resolveImageRef(url, apiConfig);
3950
4205
  }
3951
4206
  if (!model) {
3952
4207
  throw new Error(
@@ -3954,19 +4209,23 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3954
4209
  );
3955
4210
  }
3956
4211
  return streamScreenshotAnalysis({
3957
- url,
4212
+ image: url,
4213
+ apiConfig,
3958
4214
  prompt: prompt || void 0,
3959
4215
  styleMap,
3960
4216
  onLog,
3961
4217
  model
3962
4218
  });
3963
4219
  }
3964
- var SCREENSHOT_ANALYSIS_PROMPT, ANALYSIS_RESPONSE_FORMAT;
4220
+ var VIEWPORT_CAPTURE_TIMEOUT_MS, FULLPAGE_CAPTURE_TIMEOUT_MS, SCREENSHOT_ANALYSIS_PROMPT, ANALYSIS_RESPONSE_FORMAT;
3965
4221
  var init_screenshot = __esm({
3966
4222
  "src/tools/_helpers/screenshot.ts"() {
3967
4223
  "use strict";
3968
4224
  init_sidecar();
3969
4225
  init_analyzeImage();
4226
+ init_uploadImage();
4227
+ VIEWPORT_CAPTURE_TIMEOUT_MS = 45e3;
4228
+ FULLPAGE_CAPTURE_TIMEOUT_MS = 135e3;
3970
4229
  SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
3971
4230
  ANALYSIS_RESPONSE_FORMAT = `Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
3972
4231
  }
@@ -3991,12 +4250,12 @@ var init_browserLock = __esm({
3991
4250
  });
3992
4251
 
3993
4252
  // src/toolRegistry.ts
3994
- var log6, USER_CANCELLED_RESULT, ToolRegistry;
4253
+ var log7, USER_CANCELLED_RESULT, ToolRegistry;
3995
4254
  var init_toolRegistry = __esm({
3996
4255
  "src/toolRegistry.ts"() {
3997
4256
  "use strict";
3998
4257
  init_logger();
3999
- log6 = createLogger("tool-registry");
4258
+ log7 = createLogger("tool-registry");
4000
4259
  USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
4001
4260
  ToolRegistry = class {
4002
4261
  entries = /* @__PURE__ */ new Map();
@@ -4023,7 +4282,7 @@ var init_toolRegistry = __esm({
4023
4282
  if (!entry) {
4024
4283
  return false;
4025
4284
  }
4026
- log6.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
4285
+ log7.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
4027
4286
  entry.abortController.abort(mode);
4028
4287
  if (mode === "graceful") {
4029
4288
  const partial = entry.getPartialResult?.() ?? "";
@@ -4056,7 +4315,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
4056
4315
  if (!entry) {
4057
4316
  return false;
4058
4317
  }
4059
- log6.info("Tool restarted", { toolCallId: id, name: entry.name });
4318
+ log7.info("Tool restarted", { toolCallId: id, name: entry.name });
4060
4319
  entry.abortController.abort("restart");
4061
4320
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
4062
4321
  this.onEvent?.({
@@ -4165,7 +4424,7 @@ async function runSubAgent(config) {
4165
4424
  const signal = background ? bgAbort.signal : parentSignal;
4166
4425
  const agentName = subAgentId || "sub-agent";
4167
4426
  const runStart = Date.now();
4168
- log7.info("Sub-agent started", { requestId, parentToolId, agentName });
4427
+ log8.info("Sub-agent started", { requestId, parentToolId, agentName });
4169
4428
  const emit = (e) => {
4170
4429
  onEvent({ ...e, parentToolId });
4171
4430
  };
@@ -4177,7 +4436,6 @@ async function runSubAgent(config) {
4177
4436
  const fullSystem = `${system}
4178
4437
 
4179
4438
  Current date: ${dateStr}`;
4180
- const excludeToolsFromClearing = tools2.filter((t) => t.clearable === false).map((t) => t.name);
4181
4439
  let turns = 0;
4182
4440
  const run = async () => {
4183
4441
  const historyLen = (history ?? []).length;
@@ -4249,7 +4507,6 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4249
4507
  system: fullSystem,
4250
4508
  messages: cleanMessagesForApi(messages),
4251
4509
  tools: tools2,
4252
- excludeToolsFromClearing,
4253
4510
  signal
4254
4511
  },
4255
4512
  {
@@ -4385,7 +4642,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4385
4642
  ...hasArtifacts ? { artifacts } : {}
4386
4643
  };
4387
4644
  }
4388
- log7.info("Tools executing", {
4645
+ log8.info("Tools executing", {
4389
4646
  requestId,
4390
4647
  parentToolId,
4391
4648
  count: toolCalls.length,
@@ -4462,7 +4719,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4462
4719
  run2(tc.input);
4463
4720
  const r = await resultPromise;
4464
4721
  toolRegistry?.unregister(tc.id);
4465
- log7.info("Tool completed", {
4722
+ log8.info("Tool completed", {
4466
4723
  requestId,
4467
4724
  parentToolId,
4468
4725
  toolCallId: tc.id,
@@ -4513,7 +4770,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4513
4770
  const wrapRun = async () => {
4514
4771
  try {
4515
4772
  const result = await run();
4516
- log7.info("Sub-agent complete", {
4773
+ log8.info("Sub-agent complete", {
4517
4774
  requestId,
4518
4775
  parentToolId,
4519
4776
  agentName,
@@ -4522,7 +4779,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4522
4779
  });
4523
4780
  return result;
4524
4781
  } catch (err) {
4525
- log7.warn("Sub-agent error", {
4782
+ log8.warn("Sub-agent error", {
4526
4783
  requestId,
4527
4784
  parentToolId,
4528
4785
  agentName,
@@ -4534,7 +4791,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4534
4791
  if (!background) {
4535
4792
  return wrapRun();
4536
4793
  }
4537
- log7.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
4794
+ log8.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
4538
4795
  toolRegistry?.register({
4539
4796
  id: parentToolId,
4540
4797
  name: agentName,
@@ -4569,7 +4826,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4569
4826
  });
4570
4827
  return { text: ack, messages: [], backgrounded: true };
4571
4828
  }
4572
- var log7;
4829
+ var log8;
4573
4830
  var init_runner = __esm({
4574
4831
  "src/subagents/runner.ts"() {
4575
4832
  "use strict";
@@ -4580,7 +4837,7 @@ var init_runner = __esm({
4580
4837
  init_toolResultCap();
4581
4838
  init_statusWatcher();
4582
4839
  init_cleanMessages();
4583
- log7 = createLogger("sub-agent");
4840
+ log8 = createLogger("sub-agent");
4584
4841
  }
4585
4842
  });
4586
4843
 
@@ -4673,7 +4930,7 @@ var init_tools2 = __esm({
4673
4930
  "screenshotViewport",
4674
4931
  "setViewport"
4675
4932
  ],
4676
- description: 'snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). select: select a dropdown option by text. wait: wait for an element to appear (polls 100ms, waits for network). navigate: navigate to a URL within the app (waits for load, subsequent steps run on new page). evaluate: run JS in the page. styles: read computed CSS styles from elements (pass properties array with camelCase names, or omit for defaults). screenshotFullPage: full-page viewport-stitched screenshot (returns CDN url with dimensions). screenshotViewport: screenshot of just the visible viewport \u2014 pass `scrollToSelector` (or `scrollY`) on this step to scroll a section into view and capture it in one atomic step (no separate scroll needed). setViewport: switch the browser between desktop and mobile rendering (pass `mode`: "desktop" or "mobile"). Reloads the page so responsive layouts, media queries, and matchMedia re-evaluate \u2014 use it to QA mobile/responsive views.'
4933
+ description: 'snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). select: select a dropdown option by text. wait: wait for an element to appear (polls 100ms, waits for network). navigate: navigate to a URL within the app (waits for load, subsequent steps run on new page). evaluate: run JS in the page. styles: read computed CSS styles from elements (pass properties array with camelCase names, or omit for defaults). screenshotFullPage: screenshot of the whole page top-to-bottom (returns a CDN url with dimensions and a written analysis). screenshotViewport: screenshot of just the visible viewport \u2014 pass `scrollToSelector` (or `scrollY`) on this step to scroll a section into view and capture it in one atomic step (no separate scroll needed). setViewport: switch the browser between desktop and mobile rendering (pass `mode`: "desktop" or "mobile"). Reloads the page so responsive layouts, media queries, and matchMedia re-evaluate \u2014 use it to QA mobile/responsive views.'
4677
4934
  },
4678
4935
  ref: {
4679
4936
  type: "string",
@@ -4741,20 +4998,11 @@ var init_tools2 = __esm({
4741
4998
  required: ["steps"]
4742
4999
  }
4743
5000
  },
4744
- {
4745
- clearable: true,
4746
- name: "screenshotFullPage",
4747
- description: "Capture a full-height screenshot of the current page. Returns a CDN URL with full text analysis and description.",
4748
- inputSchema: {
4749
- type: "object",
4750
- properties: {
4751
- path: {
4752
- type: "string",
4753
- description: 'Navigate to this path before capturing (e.g. "/settings"). If omitted, screenshots the current page.'
4754
- }
4755
- }
4756
- }
4757
- },
5001
+ // Captures are `browserCommand` steps only — there is deliberately no
5002
+ // standalone screenshot tool here. Both used to exist for full-page, with
5003
+ // different budgets, different result plumbing, and analysis on only one of
5004
+ // them, so which door you picked changed what you got back.
5005
+ //
4758
5006
  // Read tools so the QA agent can pull full spec detail on demand — the spec
4759
5007
  // context in its prompt is a lightweight index (see prompt.ts) that points
4760
5008
  // here. Routed to the global executeTool in index.ts, mirroring specSync.
@@ -4766,13 +5014,13 @@ var init_tools2 = __esm({
4766
5014
  });
4767
5015
 
4768
5016
  // src/subagents/common/context.ts
4769
- import fs15 from "fs";
4770
- import path8 from "path";
5017
+ import fs16 from "fs";
5018
+ import path9 from "path";
4771
5019
  function walkMdFiles(dir, skip) {
4772
5020
  const files = [];
4773
5021
  try {
4774
- for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
4775
- const full = path8.join(dir, entry.name);
5022
+ for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
5023
+ const full = path9.join(dir, entry.name);
4776
5024
  if (entry.isDirectory()) {
4777
5025
  if (!skip?.has(entry.name)) {
4778
5026
  files.push(...walkMdFiles(full, skip));
@@ -4785,9 +5033,9 @@ function walkMdFiles(dir, skip) {
4785
5033
  }
4786
5034
  return files.sort();
4787
5035
  }
4788
- function parseFrontmatter(filePath) {
5036
+ function parseFrontmatter2(filePath) {
4789
5037
  try {
4790
- const content = fs15.readFileSync(filePath, "utf-8");
5038
+ const content = fs16.readFileSync(filePath, "utf-8");
4791
5039
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4792
5040
  if (!match) {
4793
5041
  return {};
@@ -4812,7 +5060,7 @@ function loadSpecIndex() {
4812
5060
  return "";
4813
5061
  }
4814
5062
  const lines = files.map((f) => {
4815
- const fm = parseFrontmatter(f);
5063
+ const fm = parseFrontmatter2(f);
4816
5064
  let line = `- ${f}`;
4817
5065
  if (fm.name) {
4818
5066
  line += ` \u2014 "${fm.name}"`;
@@ -4833,7 +5081,7 @@ function loadRoadmapIndex() {
4833
5081
  const parts = [];
4834
5082
  try {
4835
5083
  const indexJson = JSON.parse(
4836
- fs15.readFileSync("src/roadmap/index.json", "utf-8")
5084
+ fs16.readFileSync("src/roadmap/index.json", "utf-8")
4837
5085
  );
4838
5086
  if (indexJson.lanes?.length > 0) {
4839
5087
  const laneLines = indexJson.lanes.map(
@@ -4853,7 +5101,7 @@ ${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
4853
5101
  const files = walkMdFiles("src/roadmap");
4854
5102
  if (files.length > 0) {
4855
5103
  const lines = files.map((f) => {
4856
- const fm = parseFrontmatter(f);
5104
+ const fm = parseFrontmatter2(f);
4857
5105
  let line = `- ${f}`;
4858
5106
  if (fm.name) {
4859
5107
  line += ` \u2014 "${fm.name}"`;
@@ -4973,7 +5221,7 @@ async function runBrowserAutomation(task, context, opts) {
4973
5221
  );
4974
5222
  } catch {
4975
5223
  }
4976
- let lastBrowserCommandViewport;
5224
+ let lastCapture = {};
4977
5225
  const result = await runSubAgent({
4978
5226
  system: getBrowserAutomationPrompt(),
4979
5227
  task,
@@ -4995,22 +5243,6 @@ async function runBrowserAutomation(task, context, opts) {
4995
5243
  return `Error setting up browser: ${err.message}`;
4996
5244
  }
4997
5245
  }
4998
- if (name === "screenshotFullPage") {
4999
- try {
5000
- return await captureAndAnalyzeScreenshot({
5001
- path: _input.path,
5002
- fullPage: true,
5003
- onLog,
5004
- model: resolveModel(
5005
- "imageAnalysis",
5006
- context.models,
5007
- context.model
5008
- )
5009
- });
5010
- } catch (err) {
5011
- return `Error taking screenshot: ${err.message}`;
5012
- }
5013
- }
5014
5246
  if (COMMON_READ_TOOL_NAMES.has(name) || name === readSpecTool.definition.name) {
5015
5247
  return executeTool(name, _input, context);
5016
5248
  }
@@ -5032,14 +5264,16 @@ async function runBrowserAutomation(task, context, opts) {
5032
5264
  try {
5033
5265
  const parsed = JSON.parse(result2);
5034
5266
  const screenshotSteps = (parsed.steps || []).filter(
5035
- (s) => s.command === "screenshotViewport" && s.result?.url
5267
+ (s) => CAPTURE_COMMANDS.has(s.command) && s.result?.url
5036
5268
  );
5037
5269
  if (screenshotSteps.length > 0) {
5038
- const lastStep = screenshotSteps[screenshotSteps.length - 1];
5039
- lastBrowserCommandViewport = {
5040
- url: lastStep.result.url,
5041
- styleMap: lastStep.result.styleMap
5042
- };
5270
+ for (const step of screenshotSteps) {
5271
+ const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
5272
+ lastCapture[kind] = {
5273
+ url: step.result.url,
5274
+ styleMap: step.result.styleMap
5275
+ };
5276
+ }
5043
5277
  const visionOverride = {
5044
5278
  model: resolveModel(
5045
5279
  "imageAnalysis",
@@ -5063,15 +5297,14 @@ async function runBrowserAutomation(task, context, opts) {
5063
5297
  );
5064
5298
  try {
5065
5299
  const analyses = JSON.parse(batchResult);
5066
- let ai = 0;
5067
- for (const step of parsed.steps) {
5068
- if (step.command === "screenshotViewport" && step.result?.url && ai < analyses.length) {
5069
- step.result.analysis = analyses[ai]?.output?.analysis || analyses[ai]?.output || "";
5070
- ai++;
5300
+ screenshotSteps.forEach((step, i) => {
5301
+ if (i >= analyses.length) {
5302
+ return;
5071
5303
  }
5072
- }
5304
+ step.result.analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
5305
+ });
5073
5306
  } catch {
5074
- log8.debug("Failed to parse batch analysis result", {
5307
+ log9.debug("Failed to parse batch analysis result", {
5075
5308
  batchResult
5076
5309
  });
5077
5310
  }
@@ -5082,13 +5315,10 @@ async function runBrowserAutomation(task, context, opts) {
5082
5315
  }
5083
5316
  return result2;
5084
5317
  },
5085
- toolRegistry: context.toolRegistry,
5086
- captureArtifacts: ["screenshotFullPage"]
5318
+ toolRegistry: context.toolRegistry
5087
5319
  });
5088
5320
  context.subAgentMessages?.set(context.toolCallId, result.messages);
5089
- const fullPage = result.artifacts?.screenshotFullPage;
5090
- const viewport = lastBrowserCommandViewport;
5091
- const preferred = opts?.capture === "viewport" ? viewport ?? fullPage : fullPage ?? viewport;
5321
+ const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5092
5322
  return {
5093
5323
  text: result.text,
5094
5324
  ...preferred?.url ? { screenshot: { url: preferred.url, styleMap: preferred.styleMap } } : {}
@@ -5097,7 +5327,7 @@ async function runBrowserAutomation(task, context, opts) {
5097
5327
  release();
5098
5328
  }
5099
5329
  }
5100
- var log8, browserAutomationTool;
5330
+ var log9, CAPTURE_COMMANDS, browserAutomationTool;
5101
5331
  var init_browserAutomation = __esm({
5102
5332
  "src/subagents/browserAutomation/index.ts"() {
5103
5333
  "use strict";
@@ -5113,10 +5343,11 @@ var init_browserAutomation = __esm({
5113
5343
  init_runMindstudioCli();
5114
5344
  init_surfaces();
5115
5345
  init_logger();
5116
- log8 = createLogger("browser-automation");
5346
+ log9 = createLogger("browser-automation");
5347
+ CAPTURE_COMMANDS = /* @__PURE__ */ new Set(["screenshotViewport", "screenshotFullPage"]);
5117
5348
  browserAutomationTool = {
5118
- clearable: true,
5119
5349
  definition: {
5350
+ clearable: true,
5120
5351
  name: "runAutomatedBrowserTest",
5121
5352
  description: "Run an automated browser test against the live preview. Describe what to test \u2014 the agent figures out how. Use after meaningful changes to frontend code, to reproduce user-reported issues, or to test end-to-end flows. Never give it explicit values to use when filling out forms or creating accounts \u2014 it will use its own judgement (often it needs specific values to trigger dev-mode bypasses of things like login verification codes).",
5122
5353
  inputSchema: {
@@ -5147,7 +5378,58 @@ var init_browserAutomation = __esm({
5147
5378
  });
5148
5379
 
5149
5380
  // src/tools/code/screenshot.ts
5150
- var screenshotTool;
5381
+ async function executeScreenshot(input, onLog, context) {
5382
+ const fullPage = input.fullPage === true;
5383
+ const model = resolveModel("imageAnalysis", context?.models, context?.model);
5384
+ try {
5385
+ if (input.imageUrl) {
5386
+ return await captureAndAnalyzeScreenshot({
5387
+ prompt: input.prompt,
5388
+ image: input.imageUrl,
5389
+ onLog,
5390
+ model,
5391
+ apiConfig: context?.apiConfig
5392
+ });
5393
+ }
5394
+ if (input.instructions && context) {
5395
+ const shotKind = fullPage ? "full-page" : "viewport";
5396
+ const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
5397
+ const result = await runBrowserAutomation(task, context, {
5398
+ capture: fullPage ? "fullPage" : "viewport"
5399
+ });
5400
+ if (!result.screenshot) {
5401
+ return result.text;
5402
+ }
5403
+ return await streamScreenshotAnalysis({
5404
+ image: result.screenshot.url,
5405
+ prompt: input.prompt,
5406
+ styleMap: result.screenshot.styleMap,
5407
+ onLog,
5408
+ model,
5409
+ apiConfig: context?.apiConfig
5410
+ });
5411
+ }
5412
+ const release = await acquireBrowserLock();
5413
+ try {
5414
+ return await captureAndAnalyzeScreenshot({
5415
+ prompt: input.prompt,
5416
+ path: input.path,
5417
+ fullPage,
5418
+ width: input.width,
5419
+ height: input.height,
5420
+ format: input.format,
5421
+ onLog,
5422
+ model,
5423
+ apiConfig: context?.apiConfig
5424
+ });
5425
+ } finally {
5426
+ release();
5427
+ }
5428
+ } catch (err) {
5429
+ return `Error taking screenshot: ${err.message}`;
5430
+ }
5431
+ }
5432
+ var screenshotDefinition, screenshotTool;
5151
5433
  var init_screenshot2 = __esm({
5152
5434
  "src/tools/code/screenshot.ts"() {
5153
5435
  "use strict";
@@ -5155,99 +5437,54 @@ var init_screenshot2 = __esm({
5155
5437
  init_browserLock();
5156
5438
  init_browserAutomation();
5157
5439
  init_surfaces();
5158
- screenshotTool = {
5440
+ screenshotDefinition = {
5159
5441
  clearable: true,
5160
- definition: {
5161
- name: "screenshot",
5162
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as imageUrl to skip recapture. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
5163
- inputSchema: {
5164
- type: "object",
5165
- properties: {
5166
- fullPage: {
5167
- type: "boolean",
5168
- description: "true = full-height capture of the entire page; false = just the visible viewport. Pick based on whether you need the whole page or a specific section."
5169
- },
5170
- prompt: {
5171
- type: "string",
5172
- description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
5173
- },
5174
- imageUrl: {
5175
- type: "string",
5176
- description: "URL of an existing screenshot to analyze instead of capturing a new one. Use this for additional questions about a previous screenshot."
5177
- },
5178
- path: {
5179
- type: "string",
5180
- description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
5181
- },
5182
- width: {
5183
- type: "number",
5184
- description: "Exact capture width in pixels. Set together with `height` to render a fixed-size image; clips to exactly this viewport instead of the default preview size."
5185
- },
5186
- height: {
5187
- type: "number",
5188
- description: "Exact capture height in pixels. Set together with `width`."
5189
- },
5190
- format: {
5191
- type: "string",
5192
- enum: ["png", "jpeg"],
5193
- description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
5194
- },
5195
- instructions: {
5196
- type: "string",
5197
- description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, navigating a flow, scrolling to a section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start its test at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. Use only when interaction is required to *reach* the state you want to capture \u2014 log in, dismiss a modal, switch a tab, follow a route, scroll to a section. If your steps are exercising the app's functionality across multiple states (running flows, asserting behavior under interaction, multi-step QA), use `runAutomatedBrowserTest` instead."
5198
- }
5442
+ name: "screenshot",
5443
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
5444
+ inputSchema: {
5445
+ type: "object",
5446
+ properties: {
5447
+ fullPage: {
5448
+ type: "boolean",
5449
+ description: "true = full-height capture of the entire page; false = just the visible viewport. Pick based on whether you need the whole page or a specific section."
5199
5450
  },
5200
- required: ["fullPage"]
5201
- }
5202
- },
5203
- async execute(input, context) {
5204
- const fullPage = input.fullPage === true;
5205
- const shotKind = fullPage ? "full-page" : "viewport";
5206
- try {
5207
- if (input.imageUrl) {
5208
- return await captureAndAnalyzeScreenshot({
5209
- prompt: input.prompt,
5210
- imageUrl: input.imageUrl,
5211
- onLog: context?.onLog,
5212
- model: resolveModel("imageAnalysis", context?.models, context?.model)
5213
- });
5214
- }
5215
- if (input.instructions && context) {
5216
- const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
5217
- const result = await runBrowserAutomation(task, context, {
5218
- capture: fullPage ? "fullPage" : "viewport"
5219
- });
5220
- if (!result.screenshot) {
5221
- return result.text;
5222
- }
5223
- return await streamScreenshotAnalysis({
5224
- url: result.screenshot.url,
5225
- prompt: input.prompt,
5226
- styleMap: result.screenshot.styleMap,
5227
- onLog: context?.onLog,
5228
- model: resolveModel("imageAnalysis", context?.models, context?.model)
5229
- });
5230
- }
5231
- const release = await acquireBrowserLock();
5232
- try {
5233
- return await captureAndAnalyzeScreenshot({
5234
- prompt: input.prompt,
5235
- path: input.path,
5236
- fullPage,
5237
- width: input.width,
5238
- height: input.height,
5239
- format: input.format,
5240
- onLog: context?.onLog,
5241
- model: resolveModel("imageAnalysis", context?.models, context?.model)
5242
- });
5243
- } finally {
5244
- release();
5451
+ prompt: {
5452
+ type: "string",
5453
+ description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
5454
+ },
5455
+ imageUrl: {
5456
+ type: "string",
5457
+ description: "An existing image to analyze instead of capturing a new one \u2014 the URL of a previous screenshot (for follow-up questions about it), or the path of an image file on disk such as a user upload. Local files are hosted automatically and the returned URL can be reused."
5458
+ },
5459
+ path: {
5460
+ type: "string",
5461
+ description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
5462
+ },
5463
+ width: {
5464
+ type: "number",
5465
+ description: "Exact capture width in pixels. Set together with `height` to render a fixed-size image; clips to exactly this viewport instead of the default preview size."
5466
+ },
5467
+ height: {
5468
+ type: "number",
5469
+ description: "Exact capture height in pixels. Set together with `width`."
5470
+ },
5471
+ format: {
5472
+ type: "string",
5473
+ enum: ["png", "jpeg"],
5474
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
5475
+ },
5476
+ instructions: {
5477
+ type: "string",
5478
+ description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, navigating a flow, scrolling to a section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start its test at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. Use only when interaction is required to *reach* the state you want to capture \u2014 log in, dismiss a modal, switch a tab, follow a route, scroll to a section. If your steps are exercising the app's functionality across multiple states (running flows, asserting behavior under interaction, multi-step QA), use `runAutomatedBrowserTest` instead."
5245
5479
  }
5246
- } catch (err) {
5247
- return `Error taking screenshot: ${err.message}`;
5248
- }
5480
+ },
5481
+ required: ["fullPage"]
5249
5482
  }
5250
5483
  };
5484
+ screenshotTool = {
5485
+ definition: screenshotDefinition,
5486
+ execute: (input, context) => executeScreenshot(input, context?.onLog, context)
5487
+ };
5251
5488
  }
5252
5489
  });
5253
5490
 
@@ -5344,9 +5581,9 @@ __export(analyzeDesign_exports, {
5344
5581
  async function execute3(input, onLog, context) {
5345
5582
  const url = input.url;
5346
5583
  const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
5347
- const isImageUrl = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
5348
- let imageUrl = url;
5349
- if (!isImageUrl) {
5584
+ const isImage = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
5585
+ let image = url;
5586
+ if (!isImage) {
5350
5587
  const ss = await runMindstudioCliResult(
5351
5588
  [
5352
5589
  "screenshot-url",
@@ -5369,15 +5606,16 @@ async function execute3(input, onLog, context) {
5369
5606
  if (!ss.ok) {
5370
5607
  return `Could not screenshot ${url}: ${ss.value}`;
5371
5608
  }
5372
- imageUrl = ss.value;
5609
+ image = ss.value;
5373
5610
  }
5374
- const analysis = await analyzeImage({
5611
+ const analyzed = await analyzeImage({
5375
5612
  prompt: analysisPrompt,
5376
- imageUrl,
5613
+ image,
5614
+ apiConfig: context?.apiConfig,
5377
5615
  onLog,
5378
5616
  model: resolveModel("imageAnalysis", context?.models, context?.model)
5379
5617
  });
5380
- return JSON.stringify({ url: imageUrl, analysis });
5618
+ return JSON.stringify({ url: analyzed.url, analysis: analyzed.analysis });
5381
5619
  }
5382
5620
  var DESIGN_REFERENCE_PROMPT, definition3;
5383
5621
  var init_analyzeDesign = __esm({
@@ -5412,13 +5650,13 @@ Respond only with your analysis as Markdown and absolutely no other text. Do not
5412
5650
  definition3 = {
5413
5651
  clearable: false,
5414
5652
  name: "analyzeDesign",
5415
- description: "Analyze the visual design of a website or image URL. Websites are automatically screenshotted first. Provides static image analysis only, will not capture animations or video. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
5653
+ description: "Analyze the visual design of a website, an image URL, or an image file on disk. Websites are automatically screenshotted first. Provides static image analysis only, will not capture animations or video. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
5416
5654
  inputSchema: {
5417
5655
  type: "object",
5418
5656
  properties: {
5419
5657
  url: {
5420
5658
  type: "string",
5421
- description: "URL to analyze. Can be an image URL or a website URL (will be screenshotted)."
5659
+ description: "What to analyze: a website URL (will be screenshotted), an image URL, or the path of an image file on disk (e.g. a reference the user uploaded, under src/.user-uploads/). Local files are hosted automatically and their URL comes back in the result."
5422
5660
  },
5423
5661
  prompt: {
5424
5662
  type: "string",
@@ -5438,17 +5676,17 @@ __export(analyzeImage_exports, {
5438
5676
  execute: () => execute4
5439
5677
  });
5440
5678
  async function execute4(input, onLog, context) {
5441
- const imageUrl = input.imageUrl;
5442
5679
  const prompt = buildScreenshotAnalysisPrompt({
5443
5680
  prompt: input.prompt
5444
5681
  });
5445
- const analysis = await analyzeImage({
5682
+ const { url, analysis } = await analyzeImage({
5446
5683
  prompt,
5447
- imageUrl,
5684
+ image: input.imageUrl,
5685
+ apiConfig: context?.apiConfig,
5448
5686
  onLog,
5449
5687
  model: resolveModel("imageAnalysis", context?.models, context?.model)
5450
5688
  });
5451
- return JSON.stringify({ url: imageUrl, analysis });
5689
+ return JSON.stringify({ url, analysis });
5452
5690
  }
5453
5691
  var definition4;
5454
5692
  var init_analyzeImage2 = __esm({
@@ -5460,13 +5698,16 @@ var init_analyzeImage2 = __esm({
5460
5698
  definition4 = {
5461
5699
  clearable: true,
5462
5700
  name: "analyzeImage",
5463
- description: "Analyze an image by URL using a vision model. Provides static image analysis only, will not capture animations or video. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
5701
+ description: "Analyze an image using a vision model. Provides static image analysis only, will not capture animations or video. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
5464
5702
  inputSchema: {
5465
5703
  type: "object",
5466
5704
  properties: {
5705
+ // Still `imageUrl` even though a path works: the builder renders this
5706
+ // tool's thumbnail from `input.imageUrl` and deploys separately, so
5707
+ // renaming the property would blank the view under version skew.
5467
5708
  imageUrl: {
5468
5709
  type: "string",
5469
- description: "The image URL to analyze."
5710
+ description: "The image to analyze: either a URL, or the path of an image file on disk (e.g. a reference screenshot the user uploaded, under src/.user-uploads/). Local files are hosted automatically; the URL comes back in the result and can be reused for follow-up questions or embedded in a spec."
5470
5711
  },
5471
5712
  prompt: {
5472
5713
  type: "string",
@@ -5479,88 +5720,6 @@ var init_analyzeImage2 = __esm({
5479
5720
  }
5480
5721
  });
5481
5722
 
5482
- // src/subagents/designExpert/tools/screenshot.ts
5483
- var screenshot_exports = {};
5484
- __export(screenshot_exports, {
5485
- definition: () => definition5,
5486
- execute: () => execute5
5487
- });
5488
- async function execute5(input, onLog, context) {
5489
- const fullPage = input.fullPage === true;
5490
- const shotKind = fullPage ? "full-page" : "viewport";
5491
- if (input.instructions && context) {
5492
- try {
5493
- const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
5494
- const result = await runBrowserAutomation(task, context, {
5495
- capture: fullPage ? "fullPage" : "viewport"
5496
- });
5497
- if (!result.screenshot) {
5498
- return result.text;
5499
- }
5500
- return await streamScreenshotAnalysis({
5501
- url: result.screenshot.url,
5502
- prompt: input.prompt,
5503
- styleMap: result.screenshot.styleMap,
5504
- onLog,
5505
- model: resolveModel("imageAnalysis", context?.models, context?.model)
5506
- });
5507
- } catch (err) {
5508
- return `Error taking interactive screenshot: ${err.message}`;
5509
- }
5510
- }
5511
- const release = await acquireBrowserLock();
5512
- try {
5513
- return await captureAndAnalyzeScreenshot({
5514
- prompt: input.prompt,
5515
- path: input.path,
5516
- fullPage,
5517
- onLog,
5518
- model: resolveModel("imageAnalysis", context?.models, context?.model)
5519
- });
5520
- } catch (err) {
5521
- return `Error taking screenshot: ${err.message}`;
5522
- } finally {
5523
- release();
5524
- }
5525
- }
5526
- var definition5;
5527
- var init_screenshot3 = __esm({
5528
- "src/subagents/designExpert/tools/screenshot.ts"() {
5529
- "use strict";
5530
- init_screenshot();
5531
- init_browserLock();
5532
- init_browserAutomation();
5533
- init_surfaces();
5534
- definition5 = {
5535
- clearable: true,
5536
- name: "screenshot",
5537
- description: "Capture a screenshot of the current app preview and get it back with visual analysis. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 use it to review a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 use it to review overall composition or a layout you can't see in one screen). Use to review the current state of the UI being built. Remember, the screenshot analysis is not overly precise - for example, it cannot reliably identify specific fonts by name \u2014 it can only describe what letterforms look like.",
5538
- inputSchema: {
5539
- type: "object",
5540
- properties: {
5541
- fullPage: {
5542
- type: "boolean",
5543
- description: "true = full-height capture of the entire page; false = just the visible viewport. Pick based on whether you need the whole page or a specific section."
5544
- },
5545
- prompt: {
5546
- type: "string",
5547
- description: "Optional specific question about the screenshot. Use a bulleted list to ask many questions at once."
5548
- },
5549
- path: {
5550
- type: "string",
5551
- description: 'Navigate to this path before capturing (e.g. "/settings"). If omitted, screenshots the current page.'
5552
- },
5553
- instructions: {
5554
- type: "string",
5555
- description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, scrolling to a specific section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing."
5556
- }
5557
- },
5558
- required: ["fullPage"]
5559
- }
5560
- };
5561
- }
5562
- });
5563
-
5564
5723
  // src/subagents/designExpert/tools/images/enhancePrompt.ts
5565
5724
  async function enhanceImagePrompt(params) {
5566
5725
  const {
@@ -5627,14 +5786,15 @@ var init_enhancePrompt = __esm({
5627
5786
  async function generateImageAssets(opts) {
5628
5787
  const {
5629
5788
  prompts,
5630
- sourceImages,
5631
5789
  transparentBackground,
5632
5790
  enhancePrompts,
5633
5791
  onLog,
5792
+ apiConfig,
5634
5793
  imageGenerationModel: genModel,
5635
5794
  imageAnalysisModel,
5636
5795
  imagePromptEnhancerModel
5637
5796
  } = opts;
5797
+ const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
5638
5798
  const width = opts.width || 2048;
5639
5799
  const height = opts.height || 2048;
5640
5800
  const config = { width, height };
@@ -5740,10 +5900,10 @@ async function generateImageAssets(opts) {
5740
5900
  }
5741
5901
  const analysis = await analyzeImage({
5742
5902
  prompt: ANALYZE_PROMPT,
5743
- imageUrl: url,
5903
+ image: url,
5744
5904
  onLog,
5745
5905
  model: imageAnalysisModel
5746
- });
5906
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
5747
5907
  return {
5748
5908
  url,
5749
5909
  prompt: prompts[i],
@@ -5762,6 +5922,7 @@ var init_imageGenerator = __esm({
5762
5922
  "use strict";
5763
5923
  init_runMindstudioCli();
5764
5924
  init_analyzeImage();
5925
+ init_uploadImage();
5765
5926
  init_enhancePrompt();
5766
5927
  ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
5767
5928
  }
@@ -5770,10 +5931,10 @@ var init_imageGenerator = __esm({
5770
5931
  // src/subagents/designExpert/tools/images/generateImages.ts
5771
5932
  var generateImages_exports = {};
5772
5933
  __export(generateImages_exports, {
5773
- definition: () => definition6,
5774
- execute: () => execute6
5934
+ definition: () => definition5,
5935
+ execute: () => execute5
5775
5936
  });
5776
- async function execute6(input, onLog, context) {
5937
+ async function execute5(input, onLog, context) {
5777
5938
  return generateImageAssets({
5778
5939
  prompts: input.prompts,
5779
5940
  width: input.width,
@@ -5782,6 +5943,7 @@ async function execute6(input, onLog, context) {
5782
5943
  sourceImages: input.referenceImage ? [input.referenceImage] : void 0,
5783
5944
  enhancePrompts: true,
5784
5945
  onLog,
5946
+ apiConfig: context?.apiConfig,
5785
5947
  imageGenerationModel: resolveModel(
5786
5948
  "imageGeneration",
5787
5949
  context?.models,
@@ -5799,13 +5961,13 @@ async function execute6(input, onLog, context) {
5799
5961
  )
5800
5962
  });
5801
5963
  }
5802
- var definition6;
5964
+ var definition5;
5803
5965
  var init_generateImages = __esm({
5804
5966
  "src/subagents/designExpert/tools/images/generateImages.ts"() {
5805
5967
  "use strict";
5806
5968
  init_imageGenerator();
5807
5969
  init_surfaces();
5808
- definition6 = {
5970
+ definition5 = {
5809
5971
  clearable: false,
5810
5972
  name: "generateImages",
5811
5973
  description: "Generate images. Returns CDN URLs with a quality analysis for each image. Produces high-quality results for everything from photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
@@ -5821,7 +5983,7 @@ var init_generateImages = __esm({
5821
5983
  },
5822
5984
  referenceImage: {
5823
5985
  type: "string",
5824
- description: "Optional URL of a single reference image to guide the generation \u2014 for style, subject, character consistency, or composition. Your prompt still describes the desired result; the reference conditions it. Applies to every prompt in the batch."
5986
+ description: "Optional single reference image to guide the generation \u2014 for style, subject, character consistency, or composition. Either a URL or the path of an image file on disk (e.g. a user upload under src/.user-uploads/). Your prompt still describes the desired result; the reference conditions it. Applies to every prompt in the batch."
5825
5987
  },
5826
5988
  width: {
5827
5989
  type: "number",
@@ -5845,10 +6007,10 @@ var init_generateImages = __esm({
5845
6007
  // src/subagents/designExpert/tools/images/editImages.ts
5846
6008
  var editImages_exports = {};
5847
6009
  __export(editImages_exports, {
5848
- definition: () => definition7,
5849
- execute: () => execute7
6010
+ definition: () => definition6,
6011
+ execute: () => execute6
5850
6012
  });
5851
- async function execute7(input, onLog, context) {
6013
+ async function execute6(input, onLog, context) {
5852
6014
  return generateImageAssets({
5853
6015
  prompts: input.prompts,
5854
6016
  sourceImages: input.sourceImages,
@@ -5857,6 +6019,7 @@ async function execute7(input, onLog, context) {
5857
6019
  transparentBackground: input.transparentBackground,
5858
6020
  enhancePrompts: false,
5859
6021
  onLog,
6022
+ apiConfig: context?.apiConfig,
5860
6023
  imageGenerationModel: resolveModel(
5861
6024
  "imageGeneration",
5862
6025
  context?.models,
@@ -5874,13 +6037,13 @@ async function execute7(input, onLog, context) {
5874
6037
  )
5875
6038
  });
5876
6039
  }
5877
- var definition7;
6040
+ var definition6;
5878
6041
  var init_editImages = __esm({
5879
6042
  "src/subagents/designExpert/tools/images/editImages.ts"() {
5880
6043
  "use strict";
5881
6044
  init_imageGenerator();
5882
6045
  init_surfaces();
5883
- definition7 = {
6046
+ definition6 = {
5884
6047
  clearable: false,
5885
6048
  name: "editImages",
5886
6049
  description: "Edit or transform existing images. Provide one or more source image URLs as reference and a prompt describing the desired edit. Use for compositing, style transfer, subject transformation, blending multiple references, or incorporating one or more references into something new. Returns CDN URLs with analysis.",
@@ -5899,7 +6062,7 @@ var init_editImages = __esm({
5899
6062
  items: {
5900
6063
  type: "string"
5901
6064
  },
5902
- description: "One or more source/reference image URLs. These are used as the basis for the edit \u2014 the AI will use them as reference for style, subject, or composition."
6065
+ description: "One or more source/reference images, each a URL or the path of an image file on disk (e.g. a user upload under src/.user-uploads/). These are used as the basis for the edit \u2014 the AI will use them as reference for style, subject, or composition."
5903
6066
  },
5904
6067
  width: {
5905
6068
  type: "number",
@@ -5943,8 +6106,8 @@ var init_copyEditor = __esm({
5943
6106
  init_surfaces();
5944
6107
  BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
5945
6108
  copyEditorTool = {
5946
- clearable: false,
5947
6109
  definition: {
6110
+ clearable: false,
5948
6111
  name: "copyEditor",
5949
6112
  description: "Hand it user-facing copy and it hands back a sharper version \u2014 better structured for its audience and free of the overused words, telltale constructions, and rhythms that make text read as AI-generated. Think of it as a design expert for words: it elevates how the copy communicates and strips the AI fingerprints, but it never invents facts or claims you didn't give it. Use it on anything users will read: in-app strings, empty states, errors, the Build Overview, deck copy, launch posts, Slack announcements. Readonly.",
5950
6113
  inputSchema: {
@@ -5995,18 +6158,18 @@ var init_copyEditor = __esm({
5995
6158
  // src/subagents/designExpert/tools/polishCopy.ts
5996
6159
  var polishCopy_exports = {};
5997
6160
  __export(polishCopy_exports, {
5998
- definition: () => definition8,
5999
- execute: () => execute8
6161
+ definition: () => definition7,
6162
+ execute: () => execute7
6000
6163
  });
6001
- async function execute8(input, _onLog, context) {
6164
+ async function execute7(input, _onLog, context) {
6002
6165
  return copyEditorTool.execute(input, context);
6003
6166
  }
6004
- var definition8;
6167
+ var definition7;
6005
6168
  var init_polishCopy = __esm({
6006
6169
  "src/subagents/designExpert/tools/polishCopy.ts"() {
6007
6170
  "use strict";
6008
6171
  init_copyEditor();
6009
- definition8 = {
6172
+ definition7 = {
6010
6173
  clearable: false,
6011
6174
  name: "polishCopy",
6012
6175
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
@@ -6043,16 +6206,19 @@ var init_tools4 = __esm({
6043
6206
  init_scrapeWebUrl();
6044
6207
  init_analyzeDesign();
6045
6208
  init_analyzeImage2();
6046
- init_screenshot3();
6047
6209
  init_generateImages();
6048
6210
  init_editImages();
6049
6211
  init_polishCopy();
6212
+ init_screenshot2();
6050
6213
  tools = {
6051
6214
  searchGoogle: searchGoogle_exports,
6052
6215
  scrapeWebUrl: scrapeWebUrl_exports,
6053
6216
  analyzeDesign: analyzeDesign_exports,
6054
6217
  analyzeImage: analyzeImage_exports,
6055
- screenshot: screenshot_exports,
6218
+ // Same tool the main agent offers, imported rather than reimplemented — the
6219
+ // two used to be near-identical copies and had already drifted apart. Its core
6220
+ // already takes (input, onLog, context), which is this registry's convention.
6221
+ screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
6056
6222
  generateImages: generateImages_exports,
6057
6223
  editImages: editImages_exports,
6058
6224
  polishCopy: polishCopy_exports
@@ -6070,7 +6236,7 @@ async function initOrgContext(config) {
6070
6236
  cached = await fetchRemyContext(config);
6071
6237
  const orgDefaultModels2 = filterModelPicks(cached?.defaultModels);
6072
6238
  setOrgDefaultModels(orgDefaultModels2);
6073
- log9.debug("org context loaded", {
6239
+ log10.debug("org context loaded", {
6074
6240
  delegatedAvailable: cached?.auth?.delegatedAvailable ?? false,
6075
6241
  requireDelegatedOnly: cached?.auth?.requireDelegatedOnly ?? false,
6076
6242
  hasOrgName: !!cached?.org?.name,
@@ -6079,7 +6245,7 @@ async function initOrgContext(config) {
6079
6245
  } catch (err) {
6080
6246
  cached = null;
6081
6247
  setOrgDefaultModels({});
6082
- log9.debug("org context init failed", { error: err.message });
6248
+ log10.debug("org context init failed", { error: err.message });
6083
6249
  }
6084
6250
  }
6085
6251
  function getOrgContext() {
@@ -6115,20 +6281,20 @@ function renderOrgContextBlock() {
6115
6281
  lines.push("</org_context>");
6116
6282
  return lines.join("\n");
6117
6283
  }
6118
- var log9, cached;
6284
+ var log10, cached;
6119
6285
  var init_orgContext = __esm({
6120
6286
  "src/orgContext.ts"() {
6121
6287
  "use strict";
6122
6288
  init_api();
6123
6289
  init_surfaces();
6124
6290
  init_logger();
6125
- log9 = createLogger("orgContext");
6291
+ log10 = createLogger("orgContext");
6126
6292
  cached = null;
6127
6293
  }
6128
6294
  });
6129
6295
 
6130
6296
  // src/subagents/designExpert/data/sampleCache.ts
6131
- import fs16 from "fs";
6297
+ import fs17 from "fs";
6132
6298
  function generateIndices(poolSize, sampleSize) {
6133
6299
  const n = Math.min(sampleSize, poolSize);
6134
6300
  const indices = Array.from({ length: poolSize }, (_, i) => i);
@@ -6140,14 +6306,14 @@ function generateIndices(poolSize, sampleSize) {
6140
6306
  }
6141
6307
  function load() {
6142
6308
  try {
6143
- return JSON.parse(fs16.readFileSync(SAMPLE_FILE, "utf-8"));
6309
+ return JSON.parse(fs17.readFileSync(SAMPLE_FILE, "utf-8"));
6144
6310
  } catch {
6145
6311
  return null;
6146
6312
  }
6147
6313
  }
6148
6314
  function save(indices) {
6149
6315
  try {
6150
- fs16.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
6316
+ fs17.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
6151
6317
  } catch {
6152
6318
  }
6153
6319
  }
@@ -6497,8 +6663,8 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
6497
6663
  editFileTool.definition
6498
6664
  ];
6499
6665
  designExpertTool = {
6500
- clearable: false,
6501
6666
  definition: {
6667
+ clearable: false,
6502
6668
  name: "visualDesignExpert",
6503
6669
  description: DESCRIPTION,
6504
6670
  inputSchema: {
@@ -6593,23 +6759,23 @@ var init_tools5 = __esm({
6593
6759
  });
6594
6760
 
6595
6761
  // src/subagents/productVision/executor.ts
6596
- import fs17 from "fs";
6597
- import path9 from "path";
6598
- function resolve(filePath) {
6599
- return path9.join(ROADMAP_DIR, filePath);
6762
+ import fs18 from "fs";
6763
+ import path10 from "path";
6764
+ function resolve2(filePath) {
6765
+ return path10.join(ROADMAP_DIR, filePath);
6600
6766
  }
6601
6767
  async function executeVisionTool(name, input, context) {
6602
6768
  switch (name) {
6603
6769
  case "writeFile": {
6604
- const filePath = resolve(input.path);
6770
+ const filePath = resolve2(input.path);
6605
6771
  try {
6606
- fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
6772
+ fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
6607
6773
  let oldContent = null;
6608
6774
  try {
6609
- oldContent = fs17.readFileSync(filePath, "utf-8");
6775
+ oldContent = fs18.readFileSync(filePath, "utf-8");
6610
6776
  } catch {
6611
6777
  }
6612
- fs17.writeFileSync(filePath, input.content, "utf-8");
6778
+ fs18.writeFileSync(filePath, input.content, "utf-8");
6613
6779
  const lineCount = input.content.split("\n").length;
6614
6780
  const label = oldContent !== null ? "Wrote" : "Created";
6615
6781
  return `${label} ${filePath} (${lineCount} lines)
@@ -6619,13 +6785,13 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
6619
6785
  }
6620
6786
  }
6621
6787
  case "deleteFile": {
6622
- const filePath = resolve(input.path);
6788
+ const filePath = resolve2(input.path);
6623
6789
  try {
6624
- if (!fs17.existsSync(filePath)) {
6790
+ if (!fs18.existsSync(filePath)) {
6625
6791
  return `Error: ${filePath} does not exist`;
6626
6792
  }
6627
- const oldContent = fs17.readFileSync(filePath, "utf-8");
6628
- fs17.unlinkSync(filePath);
6793
+ const oldContent = fs18.readFileSync(filePath, "utf-8");
6794
+ fs18.unlinkSync(filePath);
6629
6795
  return `Deleted ${filePath}
6630
6796
  ${unifiedDiff(filePath, oldContent, "")}`;
6631
6797
  } catch (err) {
@@ -6636,11 +6802,11 @@ ${unifiedDiff(filePath, oldContent, "")}`;
6636
6802
  if (!context) {
6637
6803
  return "Error: writePitchDeck requires execution context for design expert delegation";
6638
6804
  }
6639
- const filePath = resolve("pitch.html");
6805
+ const filePath = resolve2("pitch.html");
6640
6806
  try {
6641
- fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
6642
- const exists = fs17.existsSync(filePath);
6643
- const before = exists ? fs17.statSync(filePath).mtimeMs : null;
6807
+ fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
6808
+ const exists = fs18.existsSync(filePath);
6809
+ const before = exists ? fs18.statSync(filePath).mtimeMs : null;
6644
6810
  const delivery = exists ? `### Your deliverable
6645
6811
  The pitch deck already exists at \`${filePath}\`. Read it, then update it for the new <pitch_content>, keeping the presentation scaffolding intact \u2014 change only what needs to change.
6646
6812
 
@@ -6670,11 +6836,11 @@ Maintain the bones of the presentation scaffolding. Always keep the progress bar
6670
6836
  ${delivery}`;
6671
6837
  const result = await runDesignExpertRender({ task }, context);
6672
6838
  context.subAgentMessages?.set(context.toolCallId, result.messages);
6673
- if (!fs17.existsSync(filePath)) {
6839
+ if (!fs18.existsSync(filePath)) {
6674
6840
  return `Error: the design expert did not write ${filePath}. Its reply was:
6675
6841
  ${result.text}`;
6676
6842
  }
6677
- if (before !== null && fs17.statSync(filePath).mtimeMs === before) {
6843
+ if (before !== null && fs18.statSync(filePath).mtimeMs === before) {
6678
6844
  return `Error: the pitch deck at ${filePath} was not modified. The design expert's reply was:
6679
6845
  ${result.text}`;
6680
6846
  }
@@ -6740,8 +6906,8 @@ var init_productVision = __esm({
6740
6906
  init_history();
6741
6907
  init_surfaces();
6742
6908
  productVisionTool = {
6743
- clearable: false,
6744
6909
  definition: {
6910
+ clearable: false,
6745
6911
  name: "productVision",
6746
6912
  description: "Owns the product roadmap. Reads spec and roadmap files automatically. Creates, updates, and deletes roadmap items in src/roadmap/. Describe the situation and what needs to happen.",
6747
6913
  inputSchema: {
@@ -6872,8 +7038,8 @@ var init_codeSanityCheck = __esm({
6872
7038
  init_surfaces();
6873
7039
  BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
6874
7040
  codeSanityCheckTool = {
6875
- clearable: false,
6876
7041
  definition: {
7042
+ clearable: false,
6877
7043
  name: "codeSanityCheck",
6878
7044
  description: 'Quick sanity check on an approach before building. Reviews architecture, package choices, and flags potential issues. Usually responds with "looks good." Occasionally catches something important. Readonly \u2014 can search the web and read code but cannot modify anything.',
6879
7045
  inputSchema: {
@@ -6977,9 +7143,9 @@ ${readAsset(
6977
7143
  )}
6978
7144
  </mindstudio_flavored_markdown_spec_docs>`;
6979
7145
  specSyncTool = {
6980
- clearable: false,
6981
7146
  backgroundOnly: true,
6982
7147
  definition: {
7148
+ clearable: false,
6983
7149
  name: "specSync",
6984
7150
  description: "Reconcile the spec to bring it in line with code changes you have made. Provide a brief, bulleted list of what changed and why; it finds the affected spec sections and updates them to match. Always runs in the background \u2014 it returns immediately and reports back when done.",
6985
7151
  inputSchema: {
@@ -7054,8 +7220,8 @@ var init_scrapeWebUrl2 = __esm({
7054
7220
  init_runMindstudioCli();
7055
7221
  init_runCli();
7056
7222
  scrapeWebUrlTool = {
7057
- clearable: false,
7058
7223
  definition: {
7224
+ clearable: false,
7059
7225
  name: "scrapeWebUrl",
7060
7226
  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",
7061
7227
  inputSchema: {
@@ -7100,7 +7266,7 @@ var init_scrapeWebUrl2 = __esm({
7100
7266
  });
7101
7267
 
7102
7268
  // src/tools/spec/writeBuildOverview.ts
7103
- import fs18 from "fs";
7269
+ import fs19 from "fs";
7104
7270
  function initialDelivery() {
7105
7271
  return `### Your deliverable
7106
7272
  Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
@@ -7162,8 +7328,8 @@ The copy in <overview_copy> is final \u2014 it was authored and edited before it
7162
7328
  </body>
7163
7329
  </html>`;
7164
7330
  buildOverviewTool = {
7165
- clearable: false,
7166
7331
  definition: {
7332
+ clearable: false,
7167
7333
  name: "writeBuildOverview",
7168
7334
  description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build and refresh it after meaningful work.",
7169
7335
  inputSchema: {
@@ -7185,7 +7351,7 @@ The copy in <overview_copy> is final \u2014 it was authored and edited before it
7185
7351
  if (!content) {
7186
7352
  return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
7187
7353
  }
7188
- const exists = fs18.existsSync(OVERVIEW_FILE);
7354
+ const exists = fs19.existsSync(OVERVIEW_FILE);
7189
7355
  const task = `<overview_copy>${content}</overview_copy>
7190
7356
 
7191
7357
  ${DESIGN_BRIEF}
@@ -7203,7 +7369,7 @@ ${exists ? refreshDelivery() : initialDelivery()}`;
7203
7369
  }
7204
7370
  const result = await runDesignExpertRender({ task }, context);
7205
7371
  context.subAgentMessages?.set(context.toolCallId, result.messages);
7206
- if (!fs18.existsSync(OVERVIEW_FILE)) {
7372
+ if (!fs19.existsSync(OVERVIEW_FILE)) {
7207
7373
  return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
7208
7374
  ${result.text}`;
7209
7375
  }
@@ -7233,7 +7399,7 @@ function executeTool(name, input, context) {
7233
7399
  }
7234
7400
  return tool.execute(input, context);
7235
7401
  }
7236
- var ALL_TOOLS, CLEARABLE_TOOLS, SUBAGENT_TOOL_NAMES;
7402
+ var ALL_TOOLS, SUBAGENT_TOOL_NAMES;
7237
7403
  var init_tools8 = __esm({
7238
7404
  "src/tools/index.ts"() {
7239
7405
  "use strict";
@@ -7251,6 +7417,7 @@ var init_tools8 = __esm({
7251
7417
  init_searchGoogle();
7252
7418
  init_setProjectMetadata();
7253
7419
  init_compactConversation();
7420
+ init_loadSkill();
7254
7421
  init_readFile();
7255
7422
  init_writeFile();
7256
7423
  init_editFile();
@@ -7315,11 +7482,11 @@ var init_tools8 = __esm({
7315
7482
  browserAutomationTool,
7316
7483
  // LSP
7317
7484
  lspDiagnosticsTool,
7318
- restartProcessTool
7485
+ restartProcessTool,
7486
+ // Appended rather than grouped: position is part of the cache prefix, so a
7487
+ // new tool goes at the end to leave every existing session's prefix intact.
7488
+ loadSkillTool
7319
7489
  ];
7320
- CLEARABLE_TOOLS = new Set(
7321
- ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
7322
- );
7323
7490
  SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7324
7491
  "visualDesignExpert",
7325
7492
  "productVision",
@@ -7542,35 +7709,35 @@ var init_errors = __esm({
7542
7709
  });
7543
7710
 
7544
7711
  // src/brandExtraction/index.ts
7545
- import fs19 from "fs";
7546
- import path10 from "path";
7712
+ import fs20 from "fs";
7713
+ import path11 from "path";
7547
7714
  import { createHash } from "crypto";
7548
7715
  async function runExtraction(apiConfig, model) {
7549
7716
  const inputHash = computeInputHash();
7550
7717
  const cached3 = readCache();
7551
7718
  if (cached3 && cached3.inputHash === inputHash) {
7552
- log10.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
7719
+ log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
7553
7720
  return null;
7554
7721
  }
7555
- log10.info("Extracting brand", { inputHash });
7722
+ log11.info("Extracting brand", { inputHash });
7556
7723
  const brand = await extractBrand(apiConfig, model);
7557
7724
  if (!brand) {
7558
- log10.warn("Brand extraction failed \u2014 leaving cache untouched");
7725
+ log11.warn("Brand extraction failed \u2014 leaving cache untouched");
7559
7726
  return null;
7560
7727
  }
7561
7728
  persistBrand(brand, inputHash);
7562
- log10.info("Brand persisted", { inputHash });
7729
+ log11.info("Brand persisted", { inputHash });
7563
7730
  return brand;
7564
7731
  }
7565
7732
  function isDedicatedBrandFile(filePath) {
7566
- if (filePath.split(path10.sep).includes("@brand")) {
7733
+ if (filePath.split(path11.sep).includes("@brand")) {
7567
7734
  return true;
7568
7735
  }
7569
- const { type } = parseFrontmatter2(filePath);
7736
+ const { type } = parseFrontmatter3(filePath);
7570
7737
  return type.startsWith("design/color") || type.startsWith("design/typography");
7571
7738
  }
7572
7739
  function isBrandRelevant(filePath) {
7573
- return filePath === path10.join("src", "app.md") || isDedicatedBrandFile(filePath);
7740
+ return filePath === path11.join("src", "app.md") || isDedicatedBrandFile(filePath);
7574
7741
  }
7575
7742
  function computeInputHash() {
7576
7743
  const entries = [];
@@ -7592,7 +7759,7 @@ function sha256(input) {
7592
7759
  }
7593
7760
  function readSafe(filePath) {
7594
7761
  try {
7595
- return fs19.readFileSync(filePath, "utf-8");
7762
+ return fs20.readFileSync(filePath, "utf-8");
7596
7763
  } catch {
7597
7764
  return "";
7598
7765
  }
@@ -7618,9 +7785,9 @@ function readBrandManifest() {
7618
7785
  function walkMdFiles2(dir) {
7619
7786
  const results = [];
7620
7787
  try {
7621
- const entries = fs19.readdirSync(dir, { withFileTypes: true });
7788
+ const entries = fs20.readdirSync(dir, { withFileTypes: true });
7622
7789
  for (const entry of entries) {
7623
- const full = path10.join(dir, entry.name);
7790
+ const full = path11.join(dir, entry.name);
7624
7791
  if (entry.isDirectory()) {
7625
7792
  results.push(...walkMdFiles2(full));
7626
7793
  } else if (entry.name.endsWith(".md")) {
@@ -7631,9 +7798,9 @@ function walkMdFiles2(dir) {
7631
7798
  }
7632
7799
  return results.sort();
7633
7800
  }
7634
- function parseFrontmatter2(filePath) {
7801
+ function parseFrontmatter3(filePath) {
7635
7802
  try {
7636
- const content = fs19.readFileSync(filePath, "utf-8");
7803
+ const content = fs20.readFileSync(filePath, "utf-8");
7637
7804
  const match = content.match(/^---\n([\s\S]*?)\n---/);
7638
7805
  if (!match) {
7639
7806
  return { type: "" };
@@ -7648,7 +7815,7 @@ function parseFrontmatter2(filePath) {
7648
7815
  async function extractBrand(apiConfig, model) {
7649
7816
  const corpus = buildCorpus();
7650
7817
  if (!corpus.trim()) {
7651
- log10.debug("No spec corpus \u2014 emitting empty brand");
7818
+ log11.debug("No spec corpus \u2014 emitting empty brand");
7652
7819
  return { version: 1 };
7653
7820
  }
7654
7821
  let responseText = "";
@@ -7679,17 +7846,17 @@ async function extractBrand(apiConfig, model) {
7679
7846
  toolNames: []
7680
7847
  });
7681
7848
  } else if (event.type === "error") {
7682
- log10.error("Brand extraction stream error", { error: event.error });
7849
+ log11.error("Brand extraction stream error", { error: event.error });
7683
7850
  return null;
7684
7851
  }
7685
7852
  }
7686
7853
  } catch (err) {
7687
- log10.error("Brand extraction threw", { error: err?.message });
7854
+ log11.error("Brand extraction threw", { error: err?.message });
7688
7855
  return null;
7689
7856
  }
7690
7857
  const parsed = parseJsonResponse(responseText);
7691
7858
  if (!parsed) {
7692
- log10.warn("Brand extraction returned unparseable JSON", {
7859
+ log11.warn("Brand extraction returned unparseable JSON", {
7693
7860
  preview: responseText.slice(0, 200)
7694
7861
  });
7695
7862
  return null;
@@ -7841,14 +8008,14 @@ function pickFont(raw) {
7841
8008
  }
7842
8009
  function persistBrand(brand, inputHash) {
7843
8010
  const tmp = `${BRAND_FILE}.tmp`;
7844
- fs19.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7845
- fs19.renameSync(tmp, BRAND_FILE);
8011
+ fs20.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
8012
+ fs20.renameSync(tmp, BRAND_FILE);
7846
8013
  const cache = { inputHash, generatedAt: Date.now() };
7847
- fs19.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
8014
+ fs20.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7848
8015
  }
7849
8016
  function readCache() {
7850
8017
  try {
7851
- const raw = fs19.readFileSync(CACHE_FILE, "utf-8");
8018
+ const raw = fs20.readFileSync(CACHE_FILE, "utf-8");
7852
8019
  const parsed = JSON.parse(raw);
7853
8020
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
7854
8021
  return parsed;
@@ -7858,7 +8025,7 @@ function readCache() {
7858
8025
  return null;
7859
8026
  }
7860
8027
  }
7861
- var log10, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE, HEAD_SLICE_CHARS, BRAND_CORPUS_CHAR_LIMIT;
8028
+ var log11, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE, HEAD_SLICE_CHARS, BRAND_CORPUS_CHAR_LIMIT;
7862
8029
  var init_brandExtraction = __esm({
7863
8030
  "src/brandExtraction/index.ts"() {
7864
8031
  "use strict";
@@ -7866,7 +8033,7 @@ var init_brandExtraction = __esm({
7866
8033
  init_assets();
7867
8034
  init_logger();
7868
8035
  init_usageLedger();
7869
- log10 = createLogger("brandExtraction");
8036
+ log11 = createLogger("brandExtraction");
7870
8037
  EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
7871
8038
  BRAND_FILE = ".remy-brand.json";
7872
8039
  CACHE_FILE = ".remy-brand.cache.json";
@@ -7883,7 +8050,7 @@ function triggerBrandExtraction(apiConfig, model) {
7883
8050
  }
7884
8051
  inflight = true;
7885
8052
  void runExtraction(apiConfig, model).catch((err) => {
7886
- log11.error("Brand extraction failed", { error: err?.message });
8053
+ log12.error("Brand extraction failed", { error: err?.message });
7887
8054
  }).finally(() => {
7888
8055
  inflight = false;
7889
8056
  if (dirty) {
@@ -7892,13 +8059,13 @@ function triggerBrandExtraction(apiConfig, model) {
7892
8059
  }
7893
8060
  });
7894
8061
  }
7895
- var log11, inflight, dirty;
8062
+ var log12, inflight, dirty;
7896
8063
  var init_trigger2 = __esm({
7897
8064
  "src/brandExtraction/trigger.ts"() {
7898
8065
  "use strict";
7899
8066
  init_brandExtraction();
7900
8067
  init_logger();
7901
- log11 = createLogger("brandExtraction:trigger");
8068
+ log12 = createLogger("brandExtraction:trigger");
7902
8069
  inflight = false;
7903
8070
  dirty = false;
7904
8071
  }
@@ -7941,12 +8108,11 @@ async function runTurn(params) {
7941
8108
  onBackgroundComplete
7942
8109
  } = params;
7943
8110
  const tools2 = getToolDefinitions(onboardingState);
7944
- const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
7945
8111
  const buildModelOverride = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
7946
8112
  const baseline = resolveModel("parent", state.models, model);
7947
8113
  const parentModel = buildModelOverride ?? baseline;
7948
8114
  const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
7949
- log12.info("Turn started", {
8115
+ log13.info("Turn started", {
7950
8116
  requestId,
7951
8117
  model,
7952
8118
  buildModel: buildModelOverride,
@@ -8117,7 +8283,6 @@ async function runTurn(params) {
8117
8283
  system,
8118
8284
  messages: cleanMessagesForApi(state.messages),
8119
8285
  tools: tools2,
8120
- excludeToolsFromClearing,
8121
8286
  signal
8122
8287
  },
8123
8288
  {
@@ -8209,7 +8374,7 @@ async function runTurn(params) {
8209
8374
  const acc = toolInputAccumulators.get(event.id);
8210
8375
  const wasStreamed = acc?.started ?? false;
8211
8376
  const isInputStreaming = !!tool?.streaming?.partialInput;
8212
- log12.info("Tool received", {
8377
+ log13.info("Tool received", {
8213
8378
  requestId,
8214
8379
  toolCallId: event.id,
8215
8380
  name: event.name
@@ -8332,7 +8497,7 @@ async function runTurn(params) {
8332
8497
  });
8333
8498
  return;
8334
8499
  }
8335
- log12.info("Tools executing", {
8500
+ log13.info("Tools executing", {
8336
8501
  requestId,
8337
8502
  count: toolCalls.length,
8338
8503
  tools: toolCalls.map((tc) => tc.name)
@@ -8379,7 +8544,7 @@ async function runTurn(params) {
8379
8544
  let result;
8380
8545
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8381
8546
  saveSession(state);
8382
- log12.info("Waiting for external tool result", {
8547
+ log13.info("Waiting for external tool result", {
8383
8548
  requestId,
8384
8549
  toolCallId: tc.id,
8385
8550
  name: tc.name
@@ -8447,7 +8612,7 @@ async function runTurn(params) {
8447
8612
  if (!isBackgroundCall(tc)) {
8448
8613
  toolRegistry?.unregister(tc.id);
8449
8614
  }
8450
- log12.info("Tool completed", {
8615
+ log13.info("Tool completed", {
8451
8616
  requestId,
8452
8617
  toolCallId: tc.id,
8453
8618
  name: tc.name,
@@ -8506,7 +8671,7 @@ async function runTurn(params) {
8506
8671
  }
8507
8672
  }
8508
8673
  }
8509
- var log12, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
8674
+ var log13, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
8510
8675
  var init_agent = __esm({
8511
8676
  "src/agent.ts"() {
8512
8677
  "use strict";
@@ -8519,13 +8684,12 @@ var init_agent = __esm({
8519
8684
  init_statusWatcher();
8520
8685
  init_errors();
8521
8686
  init_cleanMessages();
8522
- init_tools8();
8523
8687
  init_sentinel();
8524
8688
  init_trigger2();
8525
8689
  init_surfaces();
8526
8690
  init_toolRegistry();
8527
8691
  init_toolResultCap();
8528
- log12 = createLogger("agent");
8692
+ log13 = createLogger("agent");
8529
8693
  BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
8530
8694
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
8531
8695
  "promptUser",
@@ -8547,8 +8711,8 @@ var init_agent = __esm({
8547
8711
  });
8548
8712
 
8549
8713
  // src/prompt/static/projectContext.ts
8550
- import fs20 from "fs";
8551
- import path11 from "path";
8714
+ import fs21 from "fs";
8715
+ import path12 from "path";
8552
8716
  function loadProjectRoot() {
8553
8717
  return `
8554
8718
  ## Project Root
@@ -8558,7 +8722,7 @@ File paths are relative to this directory. Every tool operates here and bash com
8558
8722
  }
8559
8723
  function loadProjectManifest() {
8560
8724
  try {
8561
- const manifest = fs20.readFileSync("mindstudio.json", "utf-8");
8725
+ const manifest = fs21.readFileSync("mindstudio.json", "utf-8");
8562
8726
  return `
8563
8727
  ## Project Manifest (mindstudio.json)
8564
8728
  \`\`\`json
@@ -8576,7 +8740,7 @@ function loadSpecFileMetadata() {
8576
8740
  }
8577
8741
  const entries = [];
8578
8742
  for (const filePath of files) {
8579
- const { name, description, type } = parseFrontmatter3(filePath);
8743
+ const { name, description, type } = parseFrontmatter4(filePath);
8580
8744
  let line = `- ${filePath}`;
8581
8745
  if (name) {
8582
8746
  line += ` \u2014 "${name}"`;
@@ -8599,9 +8763,9 @@ ${entries.join("\n")}`;
8599
8763
  function walkMdFiles3(dir) {
8600
8764
  const results = [];
8601
8765
  try {
8602
- const entries = fs20.readdirSync(dir, { withFileTypes: true });
8766
+ const entries = fs21.readdirSync(dir, { withFileTypes: true });
8603
8767
  for (const entry of entries) {
8604
- const full = path11.join(dir, entry.name);
8768
+ const full = path12.join(dir, entry.name);
8605
8769
  if (entry.isDirectory()) {
8606
8770
  results.push(...walkMdFiles3(full));
8607
8771
  } else if (entry.name.endsWith(".md")) {
@@ -8612,9 +8776,9 @@ function walkMdFiles3(dir) {
8612
8776
  }
8613
8777
  return results.sort();
8614
8778
  }
8615
- function parseFrontmatter3(filePath) {
8779
+ function parseFrontmatter4(filePath) {
8616
8780
  try {
8617
- const content = fs20.readFileSync(filePath, "utf-8");
8781
+ const content = fs21.readFileSync(filePath, "utf-8");
8618
8782
  const match = content.match(/^---\n([\s\S]*?)\n---/);
8619
8783
  if (!match) {
8620
8784
  return { name: "", description: "", type: "" };
@@ -8630,7 +8794,7 @@ function parseFrontmatter3(filePath) {
8630
8794
  }
8631
8795
  function loadPlanStatus(onboardingState) {
8632
8796
  try {
8633
- const content = fs20.readFileSync(".remy-plan.md", "utf-8");
8797
+ const content = fs21.readFileSync(".remy-plan.md", "utf-8");
8634
8798
  const match = content.match(/^---\n([\s\S]*?)\n---/);
8635
8799
  const status = match?.[1]?.match(/^status:\s*(.+)$/m)?.[1]?.trim();
8636
8800
  if (status === "pending" && onboardingState === "intake") {
@@ -8658,7 +8822,7 @@ The user has approved your implementation plan in .remy-plan.md. You may referen
8658
8822
  }
8659
8823
  function loadProjectFileListing() {
8660
8824
  try {
8661
- const entries = fs20.readdirSync(".", { withFileTypes: true });
8825
+ const entries = fs21.readdirSync(".", { withFileTypes: true });
8662
8826
  const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
8663
8827
  if (a.isDirectory() && !b.isDirectory()) {
8664
8828
  return -1;
@@ -8738,18 +8902,6 @@ Current date: ${now}
8738
8902
  {{compiled/design.md}}
8739
8903
  </design>
8740
8904
 
8741
- <building_agent_interfaces>
8742
- {{compiled/agent-interfaces.md}}
8743
- </building_agent_interfaces>
8744
-
8745
- <building_mcp_interfaces>
8746
- {{compiled/mcp-interfaces.md}}
8747
- </building_mcp_interfaces>
8748
-
8749
- <media_cdn>
8750
- {{compiled/media-cdn.md}}
8751
- </media_cdn>
8752
-
8753
8905
  <app_files>
8754
8906
  {{compiled/files.md}}
8755
8907
  </app_files>
@@ -8767,10 +8919,10 @@ Current date: ${now}
8767
8919
  </secrets>
8768
8920
  </platform_docs>
8769
8921
 
8922
+ ${loadSkillsCatalog()}
8923
+
8770
8924
  <mindstudio_agent_sdk_docs>
8771
8925
  {{compiled/sdk-actions.md}}
8772
-
8773
- {{compiled/task-agents.md}}
8774
8926
  </mindstudio_agent_sdk_docs>
8775
8927
 
8776
8928
  <mindstudio_flavored_markdown_spec_docs>
@@ -8841,20 +8993,21 @@ var init_prompt4 = __esm({
8841
8993
  init_assets();
8842
8994
  init_projectContext();
8843
8995
  init_orgContext();
8996
+ init_catalog();
8844
8997
  }
8845
8998
  });
8846
8999
 
8847
9000
  // src/config.ts
8848
- import fs21 from "fs";
8849
- import path12 from "path";
9001
+ import fs22 from "fs";
9002
+ import path13 from "path";
8850
9003
  import os from "os";
8851
9004
  function loadConfigFile() {
8852
9005
  try {
8853
- const raw = fs21.readFileSync(CONFIG_PATH, "utf-8");
8854
- log13.debug("Loaded config file", { path: CONFIG_PATH });
9006
+ const raw = fs22.readFileSync(CONFIG_PATH, "utf-8");
9007
+ log14.debug("Loaded config file", { path: CONFIG_PATH });
8855
9008
  return JSON.parse(raw);
8856
9009
  } catch (err) {
8857
- log13.debug("No config file found", {
9010
+ log14.debug("No config file found", {
8858
9011
  path: CONFIG_PATH,
8859
9012
  error: err.message
8860
9013
  });
@@ -8869,13 +9022,13 @@ function resolveConfig(flags2) {
8869
9022
  const baseUrl2 = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
8870
9023
  const appId = process.env.MINDSTUDIO_APP_ID || void 0;
8871
9024
  if (!apiKey) {
8872
- log13.error("No API key found");
9025
+ log14.error("No API key found");
8873
9026
  throw new Error(
8874
9027
  "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
8875
9028
  );
8876
9029
  }
8877
9030
  const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
8878
- log13.info("Config resolved", {
9031
+ log14.info("Config resolved", {
8879
9032
  baseUrl: baseUrl2,
8880
9033
  keySource,
8881
9034
  environment: activeEnv,
@@ -8883,13 +9036,13 @@ function resolveConfig(flags2) {
8883
9036
  });
8884
9037
  return { apiKey, baseUrl: baseUrl2, appId };
8885
9038
  }
8886
- var log13, CONFIG_PATH, DEFAULT_BASE_URL;
9039
+ var log14, CONFIG_PATH, DEFAULT_BASE_URL;
8887
9040
  var init_config = __esm({
8888
9041
  "src/config.ts"() {
8889
9042
  "use strict";
8890
9043
  init_logger();
8891
- log13 = createLogger("config");
8892
- CONFIG_PATH = path12.join(
9044
+ log14 = createLogger("config");
9045
+ CONFIG_PATH = path13.join(
8893
9046
  os.homedir(),
8894
9047
  ".mindstudio-local-tunnel",
8895
9048
  "config.json"
@@ -8901,11 +9054,11 @@ var init_config = __esm({
8901
9054
  // src/headless/attachments.ts
8902
9055
  import { mkdirSync, existsSync } from "fs";
8903
9056
  import { writeFile } from "fs/promises";
8904
- import { basename, join, extname } from "path";
9057
+ import { basename as basename2, join, extname as extname2 } from "path";
8905
9058
  function filenameFromUrl(url) {
8906
9059
  try {
8907
9060
  const pathname = new URL(url).pathname;
8908
- const name = basename(pathname);
9061
+ const name = basename2(pathname);
8909
9062
  return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
8910
9063
  } catch {
8911
9064
  return `upload-${Date.now()}`;
@@ -8916,7 +9069,7 @@ function resolveUniqueFilename(name, claimed) {
8916
9069
  if (isFree(name)) {
8917
9070
  return name;
8918
9071
  }
8919
- const ext = extname(name);
9072
+ const ext = extname2(name);
8920
9073
  const base = name.slice(0, name.length - ext.length);
8921
9074
  let counter = 1;
8922
9075
  while (!isFree(`${base}-${counter}${ext}`)) {
@@ -8926,7 +9079,7 @@ function resolveUniqueFilename(name, claimed) {
8926
9079
  }
8927
9080
  function isImageAttachment(att) {
8928
9081
  const name = att.filename || filenameFromUrl(att.url);
8929
- return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
9082
+ return IMAGE_EXTENSIONS.has(extname2(name).toLowerCase());
8930
9083
  }
8931
9084
  async function persistAttachments(attachments) {
8932
9085
  const nonVoice = attachments.filter((a) => !a.isVoice);
@@ -8955,7 +9108,7 @@ async function persistAttachments(attachments) {
8955
9108
  }
8956
9109
  const buffer = Buffer.from(await res.arrayBuffer());
8957
9110
  await writeFile(localPath, buffer);
8958
- log14.info("Attachment saved", {
9111
+ log15.info("Attachment saved", {
8959
9112
  filename: name,
8960
9113
  path: localPath,
8961
9114
  bytes: buffer.length
@@ -8969,7 +9122,7 @@ async function persistAttachments(attachments) {
8969
9122
  if (textRes.ok) {
8970
9123
  extractedTextPath = `${localPath}.txt`;
8971
9124
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
8972
- log14.info("Extracted text saved", { path: extractedTextPath });
9125
+ log15.info("Extracted text saved", { path: extractedTextPath });
8973
9126
  }
8974
9127
  } catch {
8975
9128
  }
@@ -9014,12 +9167,12 @@ function buildUploadHeader(results) {
9014
9167
  return `[Uploaded files]
9015
9168
  ${lines.join("\n")}`;
9016
9169
  }
9017
- var log14, UPLOADS_DIR, IMAGE_EXTENSIONS;
9170
+ var log15, UPLOADS_DIR, IMAGE_EXTENSIONS;
9018
9171
  var init_attachments = __esm({
9019
9172
  "src/headless/attachments.ts"() {
9020
9173
  "use strict";
9021
9174
  init_logger();
9022
- log14 = createLogger("headless:attachments");
9175
+ log15 = createLogger("headless:attachments");
9023
9176
  UPLOADS_DIR = "src/.user-uploads";
9024
9177
  IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
9025
9178
  }
@@ -9233,7 +9386,7 @@ var headless_exports = {};
9233
9386
  __export(headless_exports, {
9234
9387
  HeadlessSession: () => HeadlessSession
9235
9388
  });
9236
- var log15, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, FORCED_COMPACTION_THRESHOLD_TOKENS, HeadlessSession;
9389
+ var log16, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, FORCED_COMPACTION_THRESHOLD_TOKENS, HeadlessSession;
9237
9390
  var init_headless = __esm({
9238
9391
  "src/headless/index.ts"() {
9239
9392
  "use strict";
@@ -9254,7 +9407,7 @@ var init_headless = __esm({
9254
9407
  init_messageQueue();
9255
9408
  init_resolve();
9256
9409
  init_sentinel();
9257
- log15 = createLogger("headless");
9410
+ log16 = createLogger("headless");
9258
9411
  EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
9259
9412
  USER_FACING_TOOLS = /* @__PURE__ */ new Set([
9260
9413
  "promptUser",
@@ -9399,7 +9552,7 @@ var init_headless = __esm({
9399
9552
  }
9400
9553
  const line = JSON.stringify(payload) + "\n";
9401
9554
  if (event === "history") {
9402
- log15.info("Wrote history event to stdout", {
9555
+ log16.info("Wrote history event to stdout", {
9403
9556
  requestId,
9404
9557
  bytes: line.length
9405
9558
  });
@@ -9476,7 +9629,7 @@ var init_headless = __esm({
9476
9629
  if (this.sessionStats.lastContextSize <= FORCED_COMPACTION_THRESHOLD_TOKENS) {
9477
9630
  return;
9478
9631
  }
9479
- log15.info("Forced compaction gate triggered", {
9632
+ log16.info("Forced compaction gate triggered", {
9480
9633
  contextSize: this.sessionStats.lastContextSize,
9481
9634
  threshold: FORCED_COMPACTION_THRESHOLD_TOKENS,
9482
9635
  requestId
@@ -9493,7 +9646,7 @@ var init_headless = __esm({
9493
9646
  }
9494
9647
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
9495
9648
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
9496
- log15.info("Background complete", {
9649
+ log16.info("Background complete", {
9497
9650
  toolCallId,
9498
9651
  name,
9499
9652
  requestId: this.currentRequestId
@@ -9530,17 +9683,17 @@ var init_headless = __esm({
9530
9683
  return Promise.resolve(early);
9531
9684
  }
9532
9685
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
9533
- return new Promise((resolve2) => {
9686
+ return new Promise((resolve3) => {
9534
9687
  const timeout = shouldTimeout ? setTimeout(() => {
9535
9688
  this.pendingTools.delete(id);
9536
- resolve2(
9689
+ resolve3(
9537
9690
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
9538
9691
  );
9539
9692
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
9540
9693
  this.pendingTools.set(id, {
9541
9694
  resolve: (result) => {
9542
9695
  clearTimeout(timeout);
9543
- resolve2(result);
9696
+ resolve3(result);
9544
9697
  },
9545
9698
  timeout
9546
9699
  });
@@ -9731,7 +9884,7 @@ var init_headless = __esm({
9731
9884
  await this.runForcedCompactionIfNeeded(requestId);
9732
9885
  const attachments = parsed.attachments;
9733
9886
  if (attachments?.length) {
9734
- log15.info("Message has attachments", {
9887
+ log16.info("Message has attachments", {
9735
9888
  count: attachments.length,
9736
9889
  urls: attachments.map((a) => a.url)
9737
9890
  });
@@ -9747,7 +9900,7 @@ var init_headless = __esm({
9747
9900
  attachmentHeader = header;
9748
9901
  }
9749
9902
  } catch (err) {
9750
- log15.warn("Attachment persistence failed", { error: err.message });
9903
+ log16.warn("Attachment persistence failed", { error: err.message });
9751
9904
  }
9752
9905
  }
9753
9906
  let resolved = null;
@@ -9811,7 +9964,7 @@ var init_headless = __esm({
9811
9964
  error: "Turn ended unexpectedly"
9812
9965
  });
9813
9966
  }
9814
- log15.info("Turn complete", {
9967
+ log16.info("Turn complete", {
9815
9968
  requestId,
9816
9969
  durationMs: Date.now() - this.turnStart
9817
9970
  });
@@ -9823,7 +9976,7 @@ var init_headless = __esm({
9823
9976
  error: err.message
9824
9977
  });
9825
9978
  }
9826
- log15.warn("Command failed", {
9979
+ log16.warn("Command failed", {
9827
9980
  action: "message",
9828
9981
  requestId,
9829
9982
  error: err.message
@@ -9980,7 +10133,7 @@ var init_headless = __esm({
9980
10133
  try {
9981
10134
  parsed = JSON.parse(line);
9982
10135
  } catch (err) {
9983
- log15.warn("Invalid JSON on stdin", {
10136
+ log16.warn("Invalid JSON on stdin", {
9984
10137
  error: err.message,
9985
10138
  lineLength: line.length,
9986
10139
  preview: line.slice(0, 200)
@@ -9989,7 +10142,7 @@ var init_headless = __esm({
9989
10142
  return;
9990
10143
  }
9991
10144
  const { action, requestId } = parsed;
9992
- log15.info("Command received", { action, requestId });
10145
+ log16.info("Command received", { action, requestId });
9993
10146
  if (action === "tool_result" && parsed.id) {
9994
10147
  const id = parsed.id;
9995
10148
  const result = parsed.result ?? "";
@@ -9998,7 +10151,7 @@ var init_headless = __esm({
9998
10151
  this.pendingTools.delete(id);
9999
10152
  pending2.resolve(result);
10000
10153
  } else if (!this.running) {
10001
- log15.info("Late tool_result while idle, dismissing", { id });
10154
+ log16.info("Late tool_result while idle, dismissing", { id });
10002
10155
  this.emit("completed", { success: true }, requestId);
10003
10156
  } else {
10004
10157
  this.earlyResults.set(id, result);
@@ -10011,7 +10164,7 @@ var init_headless = __esm({
10011
10164
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
10012
10165
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
10013
10166
  });
10014
- log15.info("History response", {
10167
+ log16.info("History response", {
10015
10168
  requestId,
10016
10169
  startIndex: page.startIndex,
10017
10170
  endIndex: page.endIndex,
@@ -10171,8 +10324,8 @@ var init_headless = __esm({
10171
10324
  // src/index.tsx
10172
10325
  import { render } from "ink";
10173
10326
  import os2 from "os";
10174
- import fs22 from "fs";
10175
- import path13 from "path";
10327
+ import fs23 from "fs";
10328
+ import path14 from "path";
10176
10329
 
10177
10330
  // src/tui/App.tsx
10178
10331
  import { useState as useState2, useCallback, useRef } from "react";
@@ -10227,6 +10380,8 @@ function summarizeInput(name, input) {
10227
10380
  return input.pattern || "";
10228
10381
  case "listDir":
10229
10382
  return input.path || ".";
10383
+ case "loadSkill":
10384
+ return input.skill || "";
10230
10385
  default:
10231
10386
  return JSON.stringify(input).slice(0, 60);
10232
10387
  }
@@ -10251,6 +10406,8 @@ function summarizeResult(name, result) {
10251
10406
  return `${lines} files`;
10252
10407
  case "listDir":
10253
10408
  return `${lines} entries`;
10409
+ case "loadSkill":
10410
+ return "reference loaded";
10254
10411
  default:
10255
10412
  return `${lines} lines`;
10256
10413
  }
@@ -10493,8 +10650,8 @@ for (let i = 0; i < args.length; i++) {
10493
10650
  var startupLog = createLogger("startup");
10494
10651
  function printDebugInfo(config) {
10495
10652
  const pkg = JSON.parse(
10496
- fs22.readFileSync(
10497
- path13.join(import.meta.dirname, "..", "package.json"),
10653
+ fs23.readFileSync(
10654
+ path14.join(import.meta.dirname, "..", "package.json"),
10498
10655
  "utf-8"
10499
10656
  )
10500
10657
  );