@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/headless.js CHANGED
@@ -285,7 +285,7 @@ function isRetryableError(error) {
285
285
  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);
286
286
  }
287
287
  function sleep(ms) {
288
- return new Promise((resolve2) => setTimeout(resolve2, ms));
288
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
289
289
  }
290
290
  async function* streamChatWithRetry(params, options) {
291
291
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -591,20 +591,7 @@ function renderOrgContextBlock() {
591
591
  // src/assets.ts
592
592
  import fs3 from "fs";
593
593
  import path2 from "path";
594
- var ROOT = findRoot(
595
- import.meta.dirname ?? path2.dirname(new URL(import.meta.url).pathname)
596
- );
597
- function findRoot(start) {
598
- let dir = start;
599
- while (dir !== path2.dirname(dir)) {
600
- if (fs3.existsSync(path2.join(dir, "package.json"))) {
601
- return dir;
602
- }
603
- dir = path2.dirname(dir);
604
- }
605
- return start;
606
- }
607
- var ASSETS_BASE = fs3.existsSync(path2.join(ROOT, "dist", "prompt")) ? path2.join(ROOT, "dist") : path2.join(ROOT, "src");
594
+ var ASSETS_BASE = import.meta.dirname ?? path2.dirname(new URL(import.meta.url).pathname);
608
595
  function assetPath(...segments) {
609
596
  return path2.join(ASSETS_BASE, ...segments);
610
597
  }
@@ -762,6 +749,81 @@ ${listing}
762
749
  }
763
750
  }
764
751
 
752
+ // src/prompt/skills/catalog.ts
753
+ import fs5 from "fs";
754
+ import path4 from "path";
755
+ var SKILLS_DIR = assetPath("prompt", "skills");
756
+ function parseFrontmatter2(content) {
757
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
758
+ if (!match) {
759
+ return {};
760
+ }
761
+ const fields = {};
762
+ for (const line of match[1].split("\n")) {
763
+ const sep = line.indexOf(":");
764
+ if (sep > 0) {
765
+ fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
766
+ }
767
+ }
768
+ return fields;
769
+ }
770
+ function loadCatalog() {
771
+ let files;
772
+ try {
773
+ files = fs5.readdirSync(SKILLS_DIR).filter((f) => f.endsWith(".md"));
774
+ } catch {
775
+ return [];
776
+ }
777
+ const skills = [];
778
+ for (const file of files.sort()) {
779
+ const full = path4.join(SKILLS_DIR, file);
780
+ const id = file.replace(/\.md$/, "");
781
+ const fields = parseFrontmatter2(fs5.readFileSync(full, "utf-8"));
782
+ if (!fields.name || !fields.what || !fields.when) {
783
+ continue;
784
+ }
785
+ skills.push({
786
+ id,
787
+ name: fields.name,
788
+ what: fields.what,
789
+ when: fields.when,
790
+ path: full
791
+ });
792
+ }
793
+ return skills;
794
+ }
795
+ var SKILLS = loadCatalog();
796
+ var SKILL_IDS = SKILLS.map((s) => s.id);
797
+ function getSkill(id) {
798
+ return SKILLS.find((s) => s.id === id);
799
+ }
800
+ function readSkillBody(skill) {
801
+ return fs5.readFileSync(skill.path, "utf-8").replace(/^---[\s\S]*?---\s*/, "").trim();
802
+ }
803
+ function loadSkillsCatalog() {
804
+ if (SKILLS.length === 0) {
805
+ return "";
806
+ }
807
+ const entries = SKILLS.map(
808
+ (s) => [
809
+ `### ${s.name} (\`${s.id}\`)`,
810
+ s.what,
811
+ "",
812
+ `When to load: ${s.when}`,
813
+ `Reference: ${s.path}`
814
+ ].join("\n")
815
+ );
816
+ return `<available_skills>
817
+ 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.
818
+
819
+ 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.
820
+
821
+ 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.
822
+
823
+ ${entries.join("\n\n")}
824
+ </available_skills>`;
825
+ }
826
+
765
827
  // src/prompt/index.ts
766
828
  function resolveIncludes(template) {
767
829
  const result = template.replace(
@@ -816,18 +878,6 @@ Current date: ${now}
816
878
  {{compiled/design.md}}
817
879
  </design>
818
880
 
819
- <building_agent_interfaces>
820
- {{compiled/agent-interfaces.md}}
821
- </building_agent_interfaces>
822
-
823
- <building_mcp_interfaces>
824
- {{compiled/mcp-interfaces.md}}
825
- </building_mcp_interfaces>
826
-
827
- <media_cdn>
828
- {{compiled/media-cdn.md}}
829
- </media_cdn>
830
-
831
881
  <app_files>
832
882
  {{compiled/files.md}}
833
883
  </app_files>
@@ -845,10 +895,10 @@ Current date: ${now}
845
895
  </secrets>
846
896
  </platform_docs>
847
897
 
898
+ ${loadSkillsCatalog()}
899
+
848
900
  <mindstudio_agent_sdk_docs>
849
901
  {{compiled/sdk-actions.md}}
850
-
851
- {{compiled/task-agents.md}}
852
902
  </mindstudio_agent_sdk_docs>
853
903
 
854
904
  <mindstudio_flavored_markdown_spec_docs>
@@ -963,7 +1013,7 @@ function mergeBackgroundResultsMessages(messages) {
963
1013
  }
964
1014
 
965
1015
  // src/tools/spec/readSpec.ts
966
- import fs5 from "fs/promises";
1016
+ import fs6 from "fs/promises";
967
1017
 
968
1018
  // src/tools/spec/_helpers.ts
969
1019
  function validateSpecPath(filePath) {
@@ -988,8 +1038,8 @@ function extractFrontmatter(content) {
988
1038
  // src/tools/spec/readSpec.ts
989
1039
  var DEFAULT_MAX_LINES = 500;
990
1040
  var readSpecTool = {
991
- clearable: true,
992
1041
  definition: {
1042
+ clearable: true,
993
1043
  name: "readSpec",
994
1044
  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).",
995
1045
  inputSchema: {
@@ -1018,7 +1068,7 @@ var readSpecTool = {
1018
1068
  return `Error: ${err.message}`;
1019
1069
  }
1020
1070
  try {
1021
- const content = await fs5.readFile(input.path, "utf-8");
1071
+ const content = await fs6.readFile(input.path, "utf-8");
1022
1072
  const allLines = content.split("\n");
1023
1073
  const totalLines = allLines.length;
1024
1074
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
@@ -1046,8 +1096,8 @@ var readSpecTool = {
1046
1096
  };
1047
1097
 
1048
1098
  // src/tools/spec/writeSpec.ts
1049
- import fs6 from "fs/promises";
1050
- import path4 from "path";
1099
+ import fs7 from "fs/promises";
1100
+ import path5 from "path";
1051
1101
 
1052
1102
  // src/tools/_helpers/diff.ts
1053
1103
  var CONTEXT_LINES = 3;
@@ -1102,8 +1152,8 @@ function acquireFileLock(filePath) {
1102
1152
 
1103
1153
  // src/tools/spec/writeSpec.ts
1104
1154
  var writeSpecTool = {
1105
- clearable: true,
1106
1155
  definition: {
1156
+ clearable: true,
1107
1157
  name: "writeSpec",
1108
1158
  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.",
1109
1159
  inputSchema: {
@@ -1123,7 +1173,7 @@ var writeSpecTool = {
1123
1173
  },
1124
1174
  streaming: {
1125
1175
  transform: async (partial) => {
1126
- const oldContent = await fs6.readFile(partial.path, "utf-8").catch(() => "");
1176
+ const oldContent = await fs7.readFile(partial.path, "utf-8").catch(() => "");
1127
1177
  const lineCount = partial.content.split("\n").length;
1128
1178
  return `Writing ${partial.path} (${lineCount} lines)
1129
1179
  ${unifiedDiff(partial.path, oldContent, partial.content)}`;
@@ -1137,13 +1187,13 @@ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
1137
1187
  }
1138
1188
  const release = await acquireFileLock(input.path);
1139
1189
  try {
1140
- await fs6.mkdir(path4.dirname(input.path), { recursive: true });
1190
+ await fs7.mkdir(path5.dirname(input.path), { recursive: true });
1141
1191
  let oldContent = null;
1142
1192
  try {
1143
- oldContent = await fs6.readFile(input.path, "utf-8");
1193
+ oldContent = await fs7.readFile(input.path, "utf-8");
1144
1194
  } catch {
1145
1195
  }
1146
- await fs6.writeFile(input.path, input.content, "utf-8");
1196
+ await fs7.writeFile(input.path, input.content, "utf-8");
1147
1197
  const lineCount = input.content.split("\n").length;
1148
1198
  const label = oldContent !== null ? "Wrote" : "Created";
1149
1199
  return `${label} ${input.path} (${lineCount} lines)
@@ -1157,7 +1207,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1157
1207
  };
1158
1208
 
1159
1209
  // src/tools/spec/editSpec.ts
1160
- import fs7 from "fs/promises";
1210
+ import fs8 from "fs/promises";
1161
1211
 
1162
1212
  // src/tools/code/editFile/_helpers.ts
1163
1213
  function buildLineOffsets(content) {
@@ -1240,8 +1290,8 @@ function formatOccurrenceError(count, lines, filePath) {
1240
1290
 
1241
1291
  // src/tools/spec/editSpec.ts
1242
1292
  var editSpecTool = {
1243
- clearable: true,
1244
1293
  definition: {
1294
+ clearable: true,
1245
1295
  name: "editSpec",
1246
1296
  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.",
1247
1297
  inputSchema: {
@@ -1277,7 +1327,7 @@ var editSpecTool = {
1277
1327
  try {
1278
1328
  let content;
1279
1329
  try {
1280
- content = await fs7.readFile(input.path, "utf-8");
1330
+ content = await fs8.readFile(input.path, "utf-8");
1281
1331
  } catch (err) {
1282
1332
  return `Error reading file: ${err.message}`;
1283
1333
  }
@@ -1330,7 +1380,7 @@ var editSpecTool = {
1330
1380
  return `Error: that edit would remove or malform the spec's YAML frontmatter (the leading \`--- \u2026 ---\` block, which holds required fields like \`name\`). Narrow old_string to the body content you meant to change and leave the frontmatter block intact.`;
1331
1381
  }
1332
1382
  try {
1333
- await fs7.writeFile(input.path, updated, "utf-8");
1383
+ await fs8.writeFile(input.path, updated, "utf-8");
1334
1384
  } catch (err) {
1335
1385
  return `Error writing file: ${err.message}`;
1336
1386
  }
@@ -1342,11 +1392,11 @@ var editSpecTool = {
1342
1392
  };
1343
1393
 
1344
1394
  // src/tools/spec/listSpecFiles.ts
1345
- import fs8 from "fs/promises";
1346
- import path5 from "path";
1395
+ import fs9 from "fs/promises";
1396
+ import path6 from "path";
1347
1397
  var listSpecFilesTool = {
1348
- clearable: false,
1349
1398
  definition: {
1399
+ clearable: false,
1350
1400
  name: "listSpecFiles",
1351
1401
  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.",
1352
1402
  inputSchema: {
@@ -1372,7 +1422,7 @@ var listSpecFilesTool = {
1372
1422
  };
1373
1423
  async function listRecursive(dir) {
1374
1424
  const results = [];
1375
- const entries = await fs8.readdir(dir, { withFileTypes: true });
1425
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
1376
1426
  entries.sort((a, b) => {
1377
1427
  if (a.isDirectory() && !b.isDirectory()) {
1378
1428
  return -1;
@@ -1383,7 +1433,7 @@ async function listRecursive(dir) {
1383
1433
  return a.name.localeCompare(b.name);
1384
1434
  });
1385
1435
  for (const entry of entries) {
1386
- const fullPath = path5.join(dir, entry.name);
1436
+ const fullPath = path6.join(dir, entry.name);
1387
1437
  if (entry.isDirectory()) {
1388
1438
  results.push(`${fullPath}/`);
1389
1439
  results.push(...await listRecursive(fullPath));
@@ -1396,8 +1446,8 @@ async function listRecursive(dir) {
1396
1446
 
1397
1447
  // src/tools/spec/presentPublishPlan.ts
1398
1448
  var presentPublishPlanTool = {
1399
- clearable: false,
1400
1449
  definition: {
1450
+ clearable: false,
1401
1451
  name: "presentPublishPlan",
1402
1452
  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.",
1403
1453
  inputSchema: {
@@ -1418,11 +1468,11 @@ var presentPublishPlanTool = {
1418
1468
  };
1419
1469
 
1420
1470
  // src/tools/spec/writePlan.ts
1421
- import fs9 from "fs/promises";
1471
+ import fs10 from "fs/promises";
1422
1472
  var PLAN_FILE = ".remy-plan.md";
1423
1473
  var writePlanTool = {
1424
- clearable: false,
1425
1474
  definition: {
1475
+ clearable: false,
1426
1476
  name: "writePlan",
1427
1477
  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.",
1428
1478
  inputSchema: {
@@ -1443,17 +1493,17 @@ status: pending
1443
1493
  ---
1444
1494
 
1445
1495
  ${content}`;
1446
- await fs9.writeFile(PLAN_FILE, file, "utf-8");
1496
+ await fs10.writeFile(PLAN_FILE, file, "utf-8");
1447
1497
  return "Plan written to .remy-plan.md. Waiting for user approval.";
1448
1498
  }
1449
1499
  };
1450
1500
 
1451
1501
  // src/tools/spec/updatePlanStatus.ts
1452
- import fs10 from "fs/promises";
1502
+ import fs11 from "fs/promises";
1453
1503
  var PLAN_FILE2 = ".remy-plan.md";
1454
1504
  var updatePlanStatusTool = {
1455
- clearable: false,
1456
1505
  definition: {
1506
+ clearable: false,
1457
1507
  name: "updatePlanStatus",
1458
1508
  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.',
1459
1509
  inputSchema: {
@@ -1475,15 +1525,15 @@ var updatePlanStatusTool = {
1475
1525
  }
1476
1526
  let content;
1477
1527
  try {
1478
- content = await fs10.readFile(PLAN_FILE2, "utf-8");
1528
+ content = await fs11.readFile(PLAN_FILE2, "utf-8");
1479
1529
  } catch {
1480
1530
  return "No plan file found.";
1481
1531
  }
1482
1532
  if (status === "rejected") {
1483
- await fs10.unlink(PLAN_FILE2);
1533
+ await fs11.unlink(PLAN_FILE2);
1484
1534
  return "Plan rejected and removed.";
1485
1535
  }
1486
- await fs10.writeFile(
1536
+ await fs11.writeFile(
1487
1537
  PLAN_FILE2,
1488
1538
  content.replace(/^status:\s*\w+/m, `status: ${status}`),
1489
1539
  "utf-8"
@@ -1494,8 +1544,8 @@ var updatePlanStatusTool = {
1494
1544
 
1495
1545
  // src/tools/common/setProjectOnboardingState.ts
1496
1546
  var setProjectOnboardingStateTool = {
1497
- clearable: false,
1498
1547
  definition: {
1548
+ clearable: false,
1499
1549
  name: "setProjectOnboardingState",
1500
1550
  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.",
1501
1551
  inputSchema: {
@@ -1517,8 +1567,8 @@ var setProjectOnboardingStateTool = {
1517
1567
 
1518
1568
  // src/tools/common/promptUser.ts
1519
1569
  var promptUserTool = {
1520
- clearable: false,
1521
1570
  definition: {
1571
+ clearable: false,
1522
1572
  name: "promptUser",
1523
1573
  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.',
1524
1574
  inputSchema: {
@@ -1649,8 +1699,8 @@ ${lines.join("\n")}`;
1649
1699
 
1650
1700
  // src/tools/common/confirmDestructiveAction.ts
1651
1701
  var confirmDestructiveActionTool = {
1652
- clearable: false,
1653
1702
  definition: {
1703
+ clearable: false,
1654
1704
  name: "confirmDestructiveAction",
1655
1705
  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.",
1656
1706
  inputSchema: {
@@ -1689,7 +1739,7 @@ function formatCliResult(r) {
1689
1739
  return logBlock + body + truncNote;
1690
1740
  }
1691
1741
  function runCli(command, args, options) {
1692
- return new Promise((resolve2) => {
1742
+ return new Promise((resolve3) => {
1693
1743
  const timeout = options?.timeout ?? 6e4;
1694
1744
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1695
1745
  let finalArgs = args;
@@ -1718,7 +1768,7 @@ function runCli(command, args, options) {
1718
1768
  if (killTimer) {
1719
1769
  clearTimeout(killTimer);
1720
1770
  }
1721
- resolve2(result);
1771
+ resolve3(result);
1722
1772
  };
1723
1773
  const child = spawn(command, finalArgs, {
1724
1774
  stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
@@ -1823,8 +1873,8 @@ function runCli(command, args, options) {
1823
1873
 
1824
1874
  // src/subagents/sdkConsultant/index.ts
1825
1875
  var askMindStudioSdkTool = {
1826
- clearable: false,
1827
1876
  definition: {
1877
+ clearable: false,
1828
1878
  name: "askMindStudioSdk",
1829
1879
  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.",
1830
1880
  inputSchema: {
@@ -1850,7 +1900,7 @@ var askMindStudioSdkTool = {
1850
1900
  };
1851
1901
 
1852
1902
  // src/usageLedger.ts
1853
- import fs11 from "fs";
1903
+ import fs12 from "fs";
1854
1904
  var LEDGER_FILE = ".logs/usage.ndjson";
1855
1905
  var fd = null;
1856
1906
  function nanoToDollars(nano) {
@@ -1859,10 +1909,10 @@ function nanoToDollars(nano) {
1859
1909
  function recordUsage(entry) {
1860
1910
  try {
1861
1911
  if (fd === null) {
1862
- fs11.mkdirSync(".logs", { recursive: true });
1863
- fd = fs11.openSync(LEDGER_FILE, "a");
1912
+ fs12.mkdirSync(".logs", { recursive: true });
1913
+ fd = fs12.openSync(LEDGER_FILE, "a");
1864
1914
  }
1865
- fs11.writeSync(fd, JSON.stringify(entry) + "\n");
1915
+ fs12.writeSync(fd, JSON.stringify(entry) + "\n");
1866
1916
  } catch {
1867
1917
  }
1868
1918
  }
@@ -1964,8 +2014,8 @@ function stripDollarKeys(envelope) {
1964
2014
 
1965
2015
  // src/tools/common/searchGoogle.ts
1966
2016
  var searchGoogleTool = {
1967
- clearable: false,
1968
2017
  definition: {
2018
+ clearable: false,
1969
2019
  name: "searchGoogle",
1970
2020
  description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help.",
1971
2021
  inputSchema: {
@@ -1995,8 +2045,8 @@ var searchGoogleTool = {
1995
2045
 
1996
2046
  // src/tools/common/setProjectMetadata.ts
1997
2047
  var setProjectMetadataTool = {
1998
- clearable: false,
1999
2048
  definition: {
2049
+ clearable: false,
2000
2050
  name: "setProjectMetadata",
2001
2051
  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.",
2002
2052
  inputSchema: {
@@ -2028,8 +2078,8 @@ var setProjectMetadataTool = {
2028
2078
 
2029
2079
  // src/tools/common/compactConversation.ts
2030
2080
  var compactConversationTool = {
2031
- clearable: false,
2032
2081
  definition: {
2082
+ clearable: false,
2033
2083
  name: "compactConversation",
2034
2084
  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.",
2035
2085
  inputSchema: {
@@ -2051,8 +2101,47 @@ var compactConversationTool = {
2051
2101
  }
2052
2102
  };
2053
2103
 
2104
+ // src/tools/common/loadSkill.ts
2105
+ var loadSkillTool = {
2106
+ definition: {
2107
+ clearable: true,
2108
+ name: "loadSkill",
2109
+ 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.",
2110
+ inputSchema: {
2111
+ type: "object",
2112
+ properties: {
2113
+ skill: {
2114
+ type: "string",
2115
+ // Omitted when the catalog is empty: an empty enum is a schema no
2116
+ // provider accepts, and failing one tool call beats failing every
2117
+ // request if the docs ever go missing from a build.
2118
+ ...SKILL_IDS.length > 0 ? { enum: SKILL_IDS } : {},
2119
+ description: "The skill id, as listed in <available_skills>."
2120
+ }
2121
+ },
2122
+ required: ["skill"]
2123
+ }
2124
+ },
2125
+ async execute(input) {
2126
+ const id = String(input.skill ?? "");
2127
+ const skill = getSkill(id);
2128
+ if (!skill) {
2129
+ return `Error: unknown skill "${id}". Available: ${SKILL_IDS.join(", ") || "(none)"}`;
2130
+ }
2131
+ try {
2132
+ const body = readSkillBody(skill);
2133
+ return `${body}
2134
+
2135
+ ---
2136
+ 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.`;
2137
+ } catch (err) {
2138
+ return `Error loading skill "${id}": ${err.message}`;
2139
+ }
2140
+ }
2141
+ };
2142
+
2054
2143
  // src/tools/code/readFile.ts
2055
- import fs12 from "fs/promises";
2144
+ import fs13 from "fs/promises";
2056
2145
  var DEFAULT_WINDOW = 500;
2057
2146
  var MAX_BYTES = 64 * 1024;
2058
2147
  function isBinary(buffer) {
@@ -2065,8 +2154,8 @@ function isBinary(buffer) {
2065
2154
  return false;
2066
2155
  }
2067
2156
  var readFileTool = {
2068
- clearable: true,
2069
2157
  definition: {
2158
+ clearable: true,
2070
2159
  name: "readFile",
2071
2160
  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.",
2072
2161
  inputSchema: {
@@ -2074,7 +2163,7 @@ var readFileTool = {
2074
2163
  properties: {
2075
2164
  path: {
2076
2165
  type: "string",
2077
- description: "The file path to read, relative to the project root."
2166
+ 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."
2078
2167
  },
2079
2168
  startLine: {
2080
2169
  type: "number",
@@ -2094,7 +2183,7 @@ var readFileTool = {
2094
2183
  },
2095
2184
  async execute(input) {
2096
2185
  try {
2097
- const buffer = await fs12.readFile(input.path);
2186
+ const buffer = await fs13.readFile(input.path);
2098
2187
  if (isBinary(buffer)) {
2099
2188
  const size = buffer.length;
2100
2189
  const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
@@ -2166,11 +2255,11 @@ var readFileTool = {
2166
2255
  };
2167
2256
 
2168
2257
  // src/tools/code/writeFile.ts
2169
- import fs13 from "fs/promises";
2170
- import path6 from "path";
2258
+ import fs14 from "fs/promises";
2259
+ import path7 from "path";
2171
2260
  var writeFileTool = {
2172
- clearable: true,
2173
2261
  definition: {
2262
+ clearable: true,
2174
2263
  name: "writeFile",
2175
2264
  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.",
2176
2265
  inputSchema: {
@@ -2204,7 +2293,7 @@ var writeFileTool = {
2204
2293
  lastNewlineCount = newlineCount;
2205
2294
  const lastNewline = partial.content.lastIndexOf("\n");
2206
2295
  const completeContent = partial.content.substring(0, lastNewline + 1);
2207
- const oldContent = await fs13.readFile(partial.path, "utf-8").catch(() => "");
2296
+ const oldContent = await fs14.readFile(partial.path, "utf-8").catch(() => "");
2208
2297
  return `Writing ${partial.path} (${newlineCount} lines)
2209
2298
  ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2210
2299
  }
@@ -2213,13 +2302,13 @@ ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2213
2302
  async execute(input) {
2214
2303
  const release = await acquireFileLock(input.path);
2215
2304
  try {
2216
- await fs13.mkdir(path6.dirname(input.path), { recursive: true });
2305
+ await fs14.mkdir(path7.dirname(input.path), { recursive: true });
2217
2306
  let oldContent = null;
2218
2307
  try {
2219
- oldContent = await fs13.readFile(input.path, "utf-8");
2308
+ oldContent = await fs14.readFile(input.path, "utf-8");
2220
2309
  } catch {
2221
2310
  }
2222
- await fs13.writeFile(input.path, input.content, "utf-8");
2311
+ await fs14.writeFile(input.path, input.content, "utf-8");
2223
2312
  const lineCount = input.content.split("\n").length;
2224
2313
  const label = oldContent !== null ? "Wrote" : "Created";
2225
2314
  return `${label} ${input.path} (${lineCount} lines)
@@ -2233,10 +2322,10 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
2233
2322
  };
2234
2323
 
2235
2324
  // src/tools/code/editFile/index.ts
2236
- import fs14 from "fs/promises";
2325
+ import fs15 from "fs/promises";
2237
2326
  var editFileTool = {
2238
- clearable: true,
2239
2327
  definition: {
2328
+ clearable: true,
2240
2329
  name: "editFile",
2241
2330
  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.",
2242
2331
  inputSchema: {
@@ -2265,7 +2354,7 @@ var editFileTool = {
2265
2354
  async execute(input) {
2266
2355
  const release = await acquireFileLock(input.path);
2267
2356
  try {
2268
- const content = await fs14.readFile(input.path, "utf-8");
2357
+ const content = await fs15.readFile(input.path, "utf-8");
2269
2358
  const { old_string, new_string, replace_all } = input;
2270
2359
  const occurrences = findOccurrences(content, old_string);
2271
2360
  if (replace_all) {
@@ -2281,7 +2370,7 @@ var editFileTool = {
2281
2370
  new_string
2282
2371
  );
2283
2372
  }
2284
- await fs14.writeFile(input.path, updated, "utf-8");
2373
+ await fs15.writeFile(input.path, updated, "utf-8");
2285
2374
  return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
2286
2375
  ${unifiedDiff(input.path, content, updated)}`;
2287
2376
  }
@@ -2292,7 +2381,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2292
2381
  old_string.length,
2293
2382
  new_string
2294
2383
  );
2295
- await fs14.writeFile(input.path, updated, "utf-8");
2384
+ await fs15.writeFile(input.path, updated, "utf-8");
2296
2385
  return `Updated ${input.path}
2297
2386
  ${unifiedDiff(input.path, content, updated)}`;
2298
2387
  }
@@ -2308,7 +2397,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2308
2397
  flex.matchedText.length,
2309
2398
  new_string
2310
2399
  );
2311
- await fs14.writeFile(input.path, updated, "utf-8");
2400
+ await fs15.writeFile(input.path, updated, "utf-8");
2312
2401
  return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
2313
2402
  ${unifiedDiff(input.path, content, updated)}`;
2314
2403
  }
@@ -2323,13 +2412,13 @@ ${unifiedDiff(input.path, content, updated)}`;
2323
2412
 
2324
2413
  // src/tools/code/bash.ts
2325
2414
  import { spawn as spawn2 } from "child_process";
2326
- import path7 from "path";
2415
+ import path8 from "path";
2327
2416
  var DEFAULT_TIMEOUT_MS = 12e4;
2328
2417
  var DEFAULT_MAX_LINES2 = 500;
2329
2418
  var MAX_OUTPUT_BYTES = 3e4;
2330
2419
  var bashTool = {
2331
- clearable: true,
2332
2420
  definition: {
2421
+ clearable: true,
2333
2422
  name: "bash",
2334
2423
  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.",
2335
2424
  inputSchema: {
@@ -2358,11 +2447,11 @@ var bashTool = {
2358
2447
  async execute(input, context) {
2359
2448
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
2360
2449
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2361
- return new Promise((resolve2) => {
2450
+ return new Promise((resolve3) => {
2362
2451
  const child = spawn2("sh", ["-c", input.command], {
2363
2452
  // Pinned rather than inherited. `undefined` here means "wherever the
2364
2453
  // process happens to be", which is the project root only by luck.
2365
- cwd: input.cwd ? path7.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
2454
+ cwd: input.cwd ? path8.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
2366
2455
  env: { ...process.env, FORCE_COLOR: "1" }
2367
2456
  });
2368
2457
  let output = "";
@@ -2383,9 +2472,9 @@ var bashTool = {
2383
2472
  clearTimeout(timer);
2384
2473
  if (!output) {
2385
2474
  if (code && code !== 0) {
2386
- resolve2(`Error: process exited with code ${code}`);
2475
+ resolve3(`Error: process exited with code ${code}`);
2387
2476
  } else {
2388
- resolve2("(no output)");
2477
+ resolve3("(no output)");
2389
2478
  }
2390
2479
  return;
2391
2480
  }
@@ -2411,18 +2500,18 @@ var bashTool = {
2411
2500
  `${(MAX_OUTPUT_BYTES / 1024).toFixed(0)}KB of ${(totalBytes / 1024).toFixed(0)}KB`
2412
2501
  );
2413
2502
  }
2414
- resolve2(
2503
+ resolve3(
2415
2504
  truncated + `
2416
2505
 
2417
2506
  (truncated at ${reasons.join(" / ")} \u2014 narrow the command (grep, head/tail, smaller paths) instead of increasing limits)`
2418
2507
  );
2419
2508
  } else {
2420
- resolve2(output);
2509
+ resolve3(output);
2421
2510
  }
2422
2511
  });
2423
2512
  child.on("error", (err) => {
2424
2513
  clearTimeout(timer);
2425
- resolve2(`Error: ${err.message}`);
2514
+ resolve3(`Error: ${err.message}`);
2426
2515
  });
2427
2516
  });
2428
2517
  }
@@ -2450,8 +2539,8 @@ function formatResults(stdout, max, mode) {
2450
2539
  return result;
2451
2540
  }
2452
2541
  var grepTool = {
2453
- clearable: true,
2454
2542
  definition: {
2543
+ clearable: true,
2455
2544
  name: "grep",
2456
2545
  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.",
2457
2546
  inputSchema: {
@@ -2540,17 +2629,17 @@ var grepTool = {
2540
2629
  }
2541
2630
  const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
2542
2631
  const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
2543
- return new Promise((resolve2) => {
2632
+ return new Promise((resolve3) => {
2544
2633
  exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
2545
2634
  if (stdout?.trim()) {
2546
- resolve2(formatResults(stdout, max, mode));
2635
+ resolve3(formatResults(stdout, max, mode));
2547
2636
  return;
2548
2637
  }
2549
2638
  exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
2550
2639
  if (grepStdout?.trim()) {
2551
- resolve2(formatResults(grepStdout, max, mode));
2640
+ resolve3(formatResults(grepStdout, max, mode));
2552
2641
  } else {
2553
- resolve2("No matches found.");
2642
+ resolve3("No matches found.");
2554
2643
  }
2555
2644
  });
2556
2645
  });
@@ -2562,8 +2651,8 @@ var grepTool = {
2562
2651
  import fg from "fast-glob";
2563
2652
  var DEFAULT_MAX2 = 200;
2564
2653
  var globTool = {
2565
- clearable: true,
2566
2654
  definition: {
2655
+ clearable: true,
2567
2656
  name: "glob",
2568
2657
  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.',
2569
2658
  inputSchema: {
@@ -2607,12 +2696,12 @@ var globTool = {
2607
2696
  };
2608
2697
 
2609
2698
  // src/tools/code/listDir.ts
2610
- import fs15 from "fs/promises";
2611
- import path8 from "path";
2699
+ import fs16 from "fs/promises";
2700
+ import path9 from "path";
2612
2701
  var EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
2613
2702
  var MAX_CHILDREN = 15;
2614
2703
  async function readAndSort(dirPath) {
2615
- const entries = await fs15.readdir(dirPath, { withFileTypes: true });
2704
+ const entries = await fs16.readdir(dirPath, { withFileTypes: true });
2616
2705
  return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
2617
2706
  if (a.isDirectory() && !b.isDirectory()) {
2618
2707
  return -1;
@@ -2625,7 +2714,7 @@ async function readAndSort(dirPath) {
2625
2714
  }
2626
2715
  async function collapsePath(basePath, name) {
2627
2716
  let display = name;
2628
- let current = path8.join(basePath, name);
2717
+ let current = path9.join(basePath, name);
2629
2718
  for (; ; ) {
2630
2719
  let children;
2631
2720
  try {
@@ -2635,7 +2724,7 @@ async function collapsePath(basePath, name) {
2635
2724
  }
2636
2725
  if (children.length === 1 && children[0].isDirectory()) {
2637
2726
  display += "/" + children[0].name;
2638
- current = path8.join(current, children[0].name);
2727
+ current = path9.join(current, children[0].name);
2639
2728
  } else {
2640
2729
  break;
2641
2730
  }
@@ -2653,15 +2742,15 @@ function formatSize(bytes) {
2653
2742
  }
2654
2743
  async function formatFile(dirPath, name, indent) {
2655
2744
  try {
2656
- const stat = await fs15.stat(path8.join(dirPath, name));
2657
- return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
2745
+ const stat2 = await fs16.stat(path9.join(dirPath, name));
2746
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat2.size)}`;
2658
2747
  } catch {
2659
2748
  return `${indent}${name}`;
2660
2749
  }
2661
2750
  }
2662
2751
  var listDirTool = {
2663
- clearable: true,
2664
2752
  definition: {
2753
+ clearable: true,
2665
2754
  name: "listDir",
2666
2755
  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.",
2667
2756
  inputSchema: {
@@ -2737,8 +2826,8 @@ var listDirTool = {
2737
2826
 
2738
2827
  // src/tools/code/editsFinished.ts
2739
2828
  var editsFinishedTool = {
2740
- clearable: false,
2741
2829
  definition: {
2830
+ clearable: false,
2742
2831
  name: "editsFinished",
2743
2832
  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.",
2744
2833
  inputSchema: {
@@ -2764,6 +2853,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
2764
2853
  throw new Error("Sidecar not available");
2765
2854
  }
2766
2855
  const url = `${baseUrl}${endpoint}`;
2856
+ let data;
2767
2857
  try {
2768
2858
  const res = await fetch(url, {
2769
2859
  method: "POST",
@@ -2775,12 +2865,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
2775
2865
  log4.error("Sidecar error", { endpoint, status: res.status });
2776
2866
  throw new Error(`Sidecar error: ${res.status}`);
2777
2867
  }
2778
- const data = await res.json();
2779
- if (data?.success === false) {
2780
- const code = data.errorCode ? ` [${data.errorCode}]` : "";
2781
- throw new Error(`${data.error || "Unknown error"}${code}`);
2782
- }
2783
- return data;
2868
+ data = await res.json();
2784
2869
  } catch (err) {
2785
2870
  if (err.message.startsWith("Sidecar error")) {
2786
2871
  throw err;
@@ -2788,6 +2873,16 @@ async function sidecarRequest(endpoint, body = {}, options) {
2788
2873
  log4.error("Sidecar connection error", { endpoint, error: err.message });
2789
2874
  throw new Error(`Sidecar connection error: ${err.message}`);
2790
2875
  }
2876
+ if (data?.success === false) {
2877
+ const code = data.errorCode ? ` [${data.errorCode}]` : "";
2878
+ log4.error("Sidecar command failed", {
2879
+ endpoint,
2880
+ error: data.error,
2881
+ errorCode: data.errorCode
2882
+ });
2883
+ throw new Error(`${data.error || "Unknown error"}${code}`);
2884
+ }
2885
+ return data;
2791
2886
  }
2792
2887
 
2793
2888
  // src/tools/_helpers/lsp.ts
@@ -2798,8 +2893,8 @@ async function lspRequest(endpoint, body) {
2798
2893
 
2799
2894
  // src/tools/code/lspDiagnostics.ts
2800
2895
  var lspDiagnosticsTool = {
2801
- clearable: true,
2802
2896
  definition: {
2897
+ clearable: true,
2803
2898
  name: "lspDiagnostics",
2804
2899
  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.",
2805
2900
  inputSchema: {
@@ -2847,8 +2942,8 @@ var lspDiagnosticsTool = {
2847
2942
 
2848
2943
  // src/tools/code/restartProcess.ts
2849
2944
  var restartProcessTool = {
2850
- clearable: false,
2851
2945
  definition: {
2946
+ clearable: false,
2852
2947
  name: "restartProcess",
2853
2948
  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.",
2854
2949
  inputSchema: {
@@ -2865,7 +2960,7 @@ var restartProcessTool = {
2865
2960
  async execute(input) {
2866
2961
  const data = await lspRequest("/restart-process", { name: input.name });
2867
2962
  if (data.ok) {
2868
- await new Promise((resolve2) => setTimeout(resolve2, 5e3));
2963
+ await new Promise((resolve3) => setTimeout(resolve3, 5e3));
2869
2964
  return `Restarted ${input.name}.`;
2870
2965
  }
2871
2966
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -2874,8 +2969,8 @@ var restartProcessTool = {
2874
2969
 
2875
2970
  // src/tools/code/runScenario.ts
2876
2971
  var runScenarioTool = {
2877
- clearable: true,
2878
2972
  definition: {
2973
+ clearable: true,
2879
2974
  name: "runScenario",
2880
2975
  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.",
2881
2976
  inputSchema: {
@@ -2900,8 +2995,8 @@ var runScenarioTool = {
2900
2995
 
2901
2996
  // src/tools/code/runMethod.ts
2902
2997
  var runMethodTool = {
2903
- clearable: true,
2904
2998
  definition: {
2999
+ clearable: true,
2905
3000
  name: "runMethod",
2906
3001
  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.',
2907
3002
  inputSchema: {
@@ -2935,8 +3030,8 @@ var runMethodTool = {
2935
3030
 
2936
3031
  // src/tools/code/queryDatabase.ts
2937
3032
  var queryDatabaseTool = {
2938
- clearable: true,
2939
3033
  definition: {
3034
+ clearable: true,
2940
3035
  name: "queryDatabase",
2941
3036
  description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
2942
3037
  inputSchema: {
@@ -2955,24 +3050,127 @@ var queryDatabaseTool = {
2955
3050
  }
2956
3051
  };
2957
3052
 
3053
+ // src/tools/_helpers/uploadImage.ts
3054
+ import { readFile, stat } from "fs/promises";
3055
+ import { basename, extname, resolve } from "path";
3056
+ var log5 = createLogger("uploadImage");
3057
+ var UPLOAD_TIMEOUT_MS = 6e4;
3058
+ var CONTENT_TYPES = {
3059
+ ".png": "image/png",
3060
+ ".jpg": "image/jpeg",
3061
+ ".jpeg": "image/jpeg",
3062
+ ".gif": "image/gif",
3063
+ ".webp": "image/webp"
3064
+ };
3065
+ var hosted = /* @__PURE__ */ new Map();
3066
+ function isFetchableUrl(ref) {
3067
+ return /^(?:https?:|data:)/i.test(ref);
3068
+ }
3069
+ async function resolveImageRef(ref, apiConfig) {
3070
+ const trimmed = ref.trim();
3071
+ return isFetchableUrl(trimmed) ? trimmed : uploadLocalImage(trimmed, apiConfig);
3072
+ }
3073
+ function resolveImageRefs(refs, apiConfig) {
3074
+ return Promise.all(refs.map((ref) => resolveImageRef(ref, apiConfig)));
3075
+ }
3076
+ async function uploadLocalImage(localPath, apiConfig) {
3077
+ const absolute = resolve(PROJECT_ROOT, localPath);
3078
+ const ext = extname(absolute).toLowerCase();
3079
+ const contentType = CONTENT_TYPES[ext];
3080
+ if (!contentType) {
3081
+ throw new Error(
3082
+ `Cannot use "${localPath}" as an image \u2014 vision models read ${Object.keys(
3083
+ CONTENT_TYPES
3084
+ ).join(", ")}. Convert it first, or pass an image URL.`
3085
+ );
3086
+ }
3087
+ let stats;
3088
+ try {
3089
+ stats = await stat(absolute);
3090
+ } catch {
3091
+ throw new Error(
3092
+ `No file at "${localPath}". Paths resolve from the project root; list the directory to check the name, or pass an image URL.`
3093
+ );
3094
+ }
3095
+ const cacheKey = `${absolute}:${stats.mtimeMs}:${stats.size}`;
3096
+ const cached3 = hosted.get(cacheKey);
3097
+ if (cached3) {
3098
+ return cached3;
3099
+ }
3100
+ if (!apiConfig?.appId) {
3101
+ throw new Error(
3102
+ `Cannot host "${localPath}" for analysis \u2014 this session has no app id. Pass an image URL instead.`
3103
+ );
3104
+ }
3105
+ const bytes = await readFile(absolute);
3106
+ const target = await requestUploadTarget(apiConfig, ext.slice(1));
3107
+ const form = new FormData();
3108
+ for (const [field, value] of Object.entries(target.uploadFields)) {
3109
+ form.append(field, value);
3110
+ }
3111
+ form.append(
3112
+ "file",
3113
+ new Blob([bytes], { type: contentType }),
3114
+ basename(absolute)
3115
+ );
3116
+ const res = await fetch(target.uploadUrl, {
3117
+ method: "POST",
3118
+ body: form,
3119
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
3120
+ });
3121
+ if (!res.ok) {
3122
+ throw new Error(`Upload of "${localPath}" failed: HTTP ${res.status}`);
3123
+ }
3124
+ log5.info("Local image hosted", {
3125
+ path: localPath,
3126
+ bytes: stats.size,
3127
+ url: target.publicUrl
3128
+ });
3129
+ hosted.set(cacheKey, target.publicUrl);
3130
+ return target.publicUrl;
3131
+ }
3132
+ async function requestUploadTarget(apiConfig, extension) {
3133
+ const url = `${apiConfig.baseUrl}/_internal/v2/apps/${apiConfig.appId}/dev/manage/upload`;
3134
+ const res = await fetch(url, {
3135
+ method: "POST",
3136
+ headers: {
3137
+ "Content-Type": "application/json",
3138
+ Authorization: `Bearer ${apiConfig.apiKey}`
3139
+ },
3140
+ body: JSON.stringify({ extension }),
3141
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
3142
+ });
3143
+ if (!res.ok) {
3144
+ throw new Error(`Could not get an upload URL: HTTP ${res.status}`);
3145
+ }
3146
+ return await res.json();
3147
+ }
3148
+
2958
3149
  // src/subagents/common/analyzeImage.ts
2959
3150
  async function analyzeImage(params) {
2960
- const { prompt, imageUrl, model, timeout = 2e5, onLog } = params;
2961
- return runMindstudioCli(
3151
+ const { prompt, image, apiConfig, model, timeout = 2e5, onLog } = params;
3152
+ const url = await resolveImageRef(image, apiConfig);
3153
+ const result = await runMindstudioCliResult(
2962
3154
  [
2963
3155
  "analyze-image",
2964
3156
  "--prompt",
2965
3157
  prompt,
2966
3158
  "--image-url",
2967
- imageUrl,
3159
+ url,
2968
3160
  "--vision-model-override",
2969
3161
  JSON.stringify({ model })
2970
3162
  ],
2971
3163
  { outputKey: "analysis", timeout, onLog }
2972
3164
  );
3165
+ if (!result.ok) {
3166
+ throw new Error(`Could not analyze ${url}: ${result.value}`);
3167
+ }
3168
+ return { url, analysis: result.value };
2973
3169
  }
2974
3170
 
2975
3171
  // src/tools/_helpers/screenshot.ts
3172
+ var VIEWPORT_CAPTURE_TIMEOUT_MS = 45e3;
3173
+ var FULLPAGE_CAPTURE_TIMEOUT_MS = 135e3;
2976
3174
  var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
2977
3175
  var 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.`;
2978
3176
  function buildScreenshotAnalysisPrompt(opts) {
@@ -2992,13 +3190,14 @@ ${ANALYSIS_RESPONSE_FORMAT}`;
2992
3190
  return p;
2993
3191
  }
2994
3192
  async function streamScreenshotAnalysis(opts) {
2995
- const { url, prompt, styleMap, onLog, model } = opts;
3193
+ const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
3194
+ const url = await resolveImageRef(image, apiConfig);
2996
3195
  onLog?.(JSON.stringify({ url, analysis: null }));
2997
3196
  const analysisPrompt = buildScreenshotAnalysisPrompt({ prompt, styleMap });
2998
3197
  let accumulated = "";
2999
- const analysis = await analyzeImage({
3198
+ const { analysis } = await analyzeImage({
3000
3199
  prompt: analysisPrompt,
3001
- imageUrl: url,
3200
+ image: url,
3002
3201
  model,
3003
3202
  onLog: (chunk) => {
3004
3203
  accumulated += chunk;
@@ -3009,18 +3208,19 @@ async function streamScreenshotAnalysis(opts) {
3009
3208
  }
3010
3209
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
3011
3210
  let prompt;
3012
- let existingUrl;
3211
+ let existingImage;
3013
3212
  let onLog;
3014
3213
  let model;
3015
- let path13;
3214
+ let apiConfig;
3215
+ let path14;
3016
3216
  let fullPage = true;
3017
3217
  let width;
3018
3218
  let height;
3019
3219
  let format;
3020
3220
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
3021
3221
  prompt = promptOrOptions.prompt;
3022
- existingUrl = promptOrOptions.imageUrl;
3023
- path13 = promptOrOptions.path;
3222
+ existingImage = promptOrOptions.image;
3223
+ path14 = promptOrOptions.path;
3024
3224
  if (promptOrOptions.fullPage !== void 0) {
3025
3225
  fullPage = promptOrOptions.fullPage;
3026
3226
  }
@@ -3029,6 +3229,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3029
3229
  format = promptOrOptions.format;
3030
3230
  onLog = promptOrOptions.onLog;
3031
3231
  model = promptOrOptions.model;
3232
+ apiConfig = promptOrOptions.apiConfig;
3032
3233
  } else {
3033
3234
  prompt = promptOrOptions;
3034
3235
  }
@@ -3037,18 +3238,20 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3037
3238
  }
3038
3239
  let url;
3039
3240
  let styleMap;
3040
- if (existingUrl) {
3041
- url = existingUrl;
3241
+ if (existingImage) {
3242
+ url = existingImage;
3042
3243
  } else {
3043
3244
  const ssResult = await sidecarRequest(
3044
3245
  fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
3045
3246
  {
3046
- ...path13 ? { path: path13 } : {},
3247
+ ...path14 ? { path: path14 } : {},
3047
3248
  ...width != null ? { width } : {},
3048
3249
  ...height != null ? { height } : {},
3049
3250
  ...format ? { format } : {}
3050
3251
  },
3051
- { timeout: fullPage ? 12e4 : 3e4 }
3252
+ {
3253
+ timeout: fullPage ? FULLPAGE_CAPTURE_TIMEOUT_MS : VIEWPORT_CAPTURE_TIMEOUT_MS
3254
+ }
3052
3255
  );
3053
3256
  url = ssResult?.url || ssResult?.screenshotUrl;
3054
3257
  if (!url) {
@@ -3059,7 +3262,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3059
3262
  styleMap = ssResult?.styleMap;
3060
3263
  }
3061
3264
  if (prompt === false) {
3062
- return url;
3265
+ return resolveImageRef(url, apiConfig);
3063
3266
  }
3064
3267
  if (!model) {
3065
3268
  throw new Error(
@@ -3067,7 +3270,8 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3067
3270
  );
3068
3271
  }
3069
3272
  return streamScreenshotAnalysis({
3070
- url,
3273
+ image: url,
3274
+ apiConfig,
3071
3275
  prompt: prompt || void 0,
3072
3276
  styleMap,
3073
3277
  onLog,
@@ -3088,7 +3292,7 @@ function acquireBrowserLock() {
3088
3292
  }
3089
3293
 
3090
3294
  // src/toolRegistry.ts
3091
- var log5 = createLogger("tool-registry");
3295
+ var log6 = createLogger("tool-registry");
3092
3296
  var 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.";
3093
3297
  var ToolRegistry = class {
3094
3298
  entries = /* @__PURE__ */ new Map();
@@ -3115,7 +3319,7 @@ var ToolRegistry = class {
3115
3319
  if (!entry) {
3116
3320
  return false;
3117
3321
  }
3118
- log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
3322
+ log6.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
3119
3323
  entry.abortController.abort(mode);
3120
3324
  if (mode === "graceful") {
3121
3325
  const partial = entry.getPartialResult?.() ?? "";
@@ -3148,7 +3352,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3148
3352
  if (!entry) {
3149
3353
  return false;
3150
3354
  }
3151
- log5.info("Tool restarted", { toolCallId: id, name: entry.name });
3355
+ log6.info("Tool restarted", { toolCallId: id, name: entry.name });
3152
3356
  entry.abortController.abort("restart");
3153
3357
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
3154
3358
  this.onEvent?.({
@@ -3250,6 +3454,9 @@ function findLastSummaryCheckpoint(messages, name) {
3250
3454
  }
3251
3455
  return -1;
3252
3456
  }
3457
+ function portableToolCallId(id) {
3458
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
3459
+ }
3253
3460
  function fixOrphanedToolCalls(messages) {
3254
3461
  const toolResultIds = /* @__PURE__ */ new Set();
3255
3462
  for (const msg of messages) {
@@ -3341,14 +3548,30 @@ ${summaryBlock.text}
3341
3548
 
3342
3549
  ${content}` : attachmentHeader;
3343
3550
  }
3344
- return { ...rest, content };
3551
+ return {
3552
+ ...rest,
3553
+ content,
3554
+ ...msg.toolCallId && {
3555
+ toolCallId: portableToolCallId(msg.toolCallId)
3556
+ }
3557
+ };
3345
3558
  }
3346
3559
  if (!Array.isArray(msg.content)) {
3347
3560
  return msg;
3348
3561
  }
3349
3562
  const blocks = msg.content;
3350
3563
  const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3351
- const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
3564
+ const toolBlocks = blocks.filter(
3565
+ (b) => b.type === "tool"
3566
+ );
3567
+ const toolCalls = toolBlocks.map((b) => ({
3568
+ id: portableToolCallId(b.id),
3569
+ name: b.name,
3570
+ input: b.input
3571
+ }));
3572
+ const rewroteToolIds = toolBlocks.some(
3573
+ (b) => portableToolCallId(b.id) !== b.id
3574
+ );
3352
3575
  const cleaned2 = {
3353
3576
  role: msg.role,
3354
3577
  content: text
@@ -3356,7 +3579,7 @@ ${content}` : attachmentHeader;
3356
3579
  if (toolCalls.length > 0) {
3357
3580
  cleaned2.toolCalls = toolCalls;
3358
3581
  }
3359
- if (msg.providerMetadata) {
3582
+ if (msg.providerMetadata && !rewroteToolIds) {
3360
3583
  cleaned2.providerMetadata = msg.providerMetadata;
3361
3584
  }
3362
3585
  if (msg.hidden) {
@@ -3368,7 +3591,7 @@ ${content}` : attachmentHeader;
3368
3591
  }
3369
3592
 
3370
3593
  // src/subagents/runner.ts
3371
- var log6 = createLogger("sub-agent");
3594
+ var log7 = createLogger("sub-agent");
3372
3595
  async function runSubAgent(config) {
3373
3596
  const {
3374
3597
  system,
@@ -3396,7 +3619,7 @@ async function runSubAgent(config) {
3396
3619
  const signal = background ? bgAbort.signal : parentSignal;
3397
3620
  const agentName = subAgentId || "sub-agent";
3398
3621
  const runStart = Date.now();
3399
- log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3622
+ log7.info("Sub-agent started", { requestId, parentToolId, agentName });
3400
3623
  const emit = (e) => {
3401
3624
  onEvent({ ...e, parentToolId });
3402
3625
  };
@@ -3408,7 +3631,6 @@ async function runSubAgent(config) {
3408
3631
  const fullSystem = `${system}
3409
3632
 
3410
3633
  Current date: ${dateStr}`;
3411
- const excludeToolsFromClearing = tools2.filter((t) => t.clearable === false).map((t) => t.name);
3412
3634
  let turns = 0;
3413
3635
  const run = async () => {
3414
3636
  const historyLen = (history ?? []).length;
@@ -3480,7 +3702,6 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3480
3702
  system: fullSystem,
3481
3703
  messages: cleanMessagesForApi(messages),
3482
3704
  tools: tools2,
3483
- excludeToolsFromClearing,
3484
3705
  signal
3485
3706
  },
3486
3707
  {
@@ -3616,7 +3837,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3616
3837
  ...hasArtifacts ? { artifacts } : {}
3617
3838
  };
3618
3839
  }
3619
- log6.info("Tools executing", {
3840
+ log7.info("Tools executing", {
3620
3841
  requestId,
3621
3842
  parentToolId,
3622
3843
  count: toolCalls.length,
@@ -3693,7 +3914,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3693
3914
  run2(tc.input);
3694
3915
  const r = await resultPromise;
3695
3916
  toolRegistry?.unregister(tc.id);
3696
- log6.info("Tool completed", {
3917
+ log7.info("Tool completed", {
3697
3918
  requestId,
3698
3919
  parentToolId,
3699
3920
  toolCallId: tc.id,
@@ -3744,7 +3965,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3744
3965
  const wrapRun = async () => {
3745
3966
  try {
3746
3967
  const result = await run();
3747
- log6.info("Sub-agent complete", {
3968
+ log7.info("Sub-agent complete", {
3748
3969
  requestId,
3749
3970
  parentToolId,
3750
3971
  agentName,
@@ -3753,7 +3974,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3753
3974
  });
3754
3975
  return result;
3755
3976
  } catch (err) {
3756
- log6.warn("Sub-agent error", {
3977
+ log7.warn("Sub-agent error", {
3757
3978
  requestId,
3758
3979
  parentToolId,
3759
3980
  agentName,
@@ -3765,7 +3986,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3765
3986
  if (!background) {
3766
3987
  return wrapRun();
3767
3988
  }
3768
- log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3989
+ log7.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3769
3990
  toolRegistry?.register({
3770
3991
  id: parentToolId,
3771
3992
  name: agentName,
@@ -3874,7 +4095,7 @@ var BROWSER_TOOLS = [
3874
4095
  "screenshotViewport",
3875
4096
  "setViewport"
3876
4097
  ],
3877
- 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.'
4098
+ 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.'
3878
4099
  },
3879
4100
  ref: {
3880
4101
  type: "string",
@@ -3942,20 +4163,11 @@ var BROWSER_TOOLS = [
3942
4163
  required: ["steps"]
3943
4164
  }
3944
4165
  },
3945
- {
3946
- clearable: true,
3947
- name: "screenshotFullPage",
3948
- description: "Capture a full-height screenshot of the current page. Returns a CDN URL with full text analysis and description.",
3949
- inputSchema: {
3950
- type: "object",
3951
- properties: {
3952
- path: {
3953
- type: "string",
3954
- description: 'Navigate to this path before capturing (e.g. "/settings"). If omitted, screenshots the current page.'
3955
- }
3956
- }
3957
- }
3958
- },
4166
+ // Captures are `browserCommand` steps only — there is deliberately no
4167
+ // standalone screenshot tool here. Both used to exist for full-page, with
4168
+ // different budgets, different result plumbing, and analysis on only one of
4169
+ // them, so which door you picked changed what you got back.
4170
+ //
3959
4171
  // Read tools so the QA agent can pull full spec detail on demand — the spec
3960
4172
  // context in its prompt is a lightweight index (see prompt.ts) that points
3961
4173
  // here. Routed to the global executeTool in index.ts, mirroring specSync.
@@ -3965,13 +4177,13 @@ var BROWSER_TOOLS = [
3965
4177
  var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
3966
4178
 
3967
4179
  // src/subagents/common/context.ts
3968
- import fs16 from "fs";
3969
- import path9 from "path";
4180
+ import fs17 from "fs";
4181
+ import path10 from "path";
3970
4182
  function walkMdFiles2(dir, skip) {
3971
4183
  const files = [];
3972
4184
  try {
3973
- for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
3974
- const full = path9.join(dir, entry.name);
4185
+ for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
4186
+ const full = path10.join(dir, entry.name);
3975
4187
  if (entry.isDirectory()) {
3976
4188
  if (!skip?.has(entry.name)) {
3977
4189
  files.push(...walkMdFiles2(full, skip));
@@ -3984,9 +4196,9 @@ function walkMdFiles2(dir, skip) {
3984
4196
  }
3985
4197
  return files.sort();
3986
4198
  }
3987
- function parseFrontmatter2(filePath) {
4199
+ function parseFrontmatter3(filePath) {
3988
4200
  try {
3989
- const content = fs16.readFileSync(filePath, "utf-8");
4201
+ const content = fs17.readFileSync(filePath, "utf-8");
3990
4202
  const match = content.match(/^---\n([\s\S]*?)\n---/);
3991
4203
  if (!match) {
3992
4204
  return {};
@@ -4011,7 +4223,7 @@ function loadSpecIndex() {
4011
4223
  return "";
4012
4224
  }
4013
4225
  const lines = files.map((f) => {
4014
- const fm = parseFrontmatter2(f);
4226
+ const fm = parseFrontmatter3(f);
4015
4227
  let line = `- ${f}`;
4016
4228
  if (fm.name) {
4017
4229
  line += ` \u2014 "${fm.name}"`;
@@ -4032,7 +4244,7 @@ function loadRoadmapIndex() {
4032
4244
  const parts = [];
4033
4245
  try {
4034
4246
  const indexJson = JSON.parse(
4035
- fs16.readFileSync("src/roadmap/index.json", "utf-8")
4247
+ fs17.readFileSync("src/roadmap/index.json", "utf-8")
4036
4248
  );
4037
4249
  if (indexJson.lanes?.length > 0) {
4038
4250
  const laneLines = indexJson.lanes.map(
@@ -4052,7 +4264,7 @@ ${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
4052
4264
  const files = walkMdFiles2("src/roadmap");
4053
4265
  if (files.length > 0) {
4054
4266
  const lines = files.map((f) => {
4055
- const fm = parseFrontmatter2(f);
4267
+ const fm = parseFrontmatter3(f);
4056
4268
  let line = `- ${f}`;
4057
4269
  if (fm.name) {
4058
4270
  line += ` \u2014 "${fm.name}"`;
@@ -4148,7 +4360,8 @@ function getBrowserAutomationPrompt() {
4148
4360
  }
4149
4361
 
4150
4362
  // src/subagents/browserAutomation/index.ts
4151
- var log7 = createLogger("browser-automation");
4363
+ var log8 = createLogger("browser-automation");
4364
+ var CAPTURE_COMMANDS = /* @__PURE__ */ new Set(["screenshotViewport", "screenshotFullPage"]);
4152
4365
  async function runBrowserAutomation(task, context, opts) {
4153
4366
  const release = await acquireBrowserLock();
4154
4367
  try {
@@ -4160,7 +4373,7 @@ async function runBrowserAutomation(task, context, opts) {
4160
4373
  );
4161
4374
  } catch {
4162
4375
  }
4163
- let lastBrowserCommandViewport;
4376
+ let lastCapture = {};
4164
4377
  const result = await runSubAgent({
4165
4378
  system: getBrowserAutomationPrompt(),
4166
4379
  task,
@@ -4182,22 +4395,6 @@ async function runBrowserAutomation(task, context, opts) {
4182
4395
  return `Error setting up browser: ${err.message}`;
4183
4396
  }
4184
4397
  }
4185
- if (name === "screenshotFullPage") {
4186
- try {
4187
- return await captureAndAnalyzeScreenshot({
4188
- path: _input.path,
4189
- fullPage: true,
4190
- onLog,
4191
- model: resolveModel(
4192
- "imageAnalysis",
4193
- context.models,
4194
- context.model
4195
- )
4196
- });
4197
- } catch (err) {
4198
- return `Error taking screenshot: ${err.message}`;
4199
- }
4200
- }
4201
4398
  if (COMMON_READ_TOOL_NAMES.has(name) || name === readSpecTool.definition.name) {
4202
4399
  return executeTool(name, _input, context);
4203
4400
  }
@@ -4219,14 +4416,16 @@ async function runBrowserAutomation(task, context, opts) {
4219
4416
  try {
4220
4417
  const parsed = JSON.parse(result2);
4221
4418
  const screenshotSteps = (parsed.steps || []).filter(
4222
- (s) => s.command === "screenshotViewport" && s.result?.url
4419
+ (s) => CAPTURE_COMMANDS.has(s.command) && s.result?.url
4223
4420
  );
4224
4421
  if (screenshotSteps.length > 0) {
4225
- const lastStep = screenshotSteps[screenshotSteps.length - 1];
4226
- lastBrowserCommandViewport = {
4227
- url: lastStep.result.url,
4228
- styleMap: lastStep.result.styleMap
4229
- };
4422
+ for (const step of screenshotSteps) {
4423
+ const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
4424
+ lastCapture[kind] = {
4425
+ url: step.result.url,
4426
+ styleMap: step.result.styleMap
4427
+ };
4428
+ }
4230
4429
  const visionOverride = {
4231
4430
  model: resolveModel(
4232
4431
  "imageAnalysis",
@@ -4250,15 +4449,14 @@ async function runBrowserAutomation(task, context, opts) {
4250
4449
  );
4251
4450
  try {
4252
4451
  const analyses = JSON.parse(batchResult);
4253
- let ai = 0;
4254
- for (const step of parsed.steps) {
4255
- if (step.command === "screenshotViewport" && step.result?.url && ai < analyses.length) {
4256
- step.result.analysis = analyses[ai]?.output?.analysis || analyses[ai]?.output || "";
4257
- ai++;
4452
+ screenshotSteps.forEach((step, i) => {
4453
+ if (i >= analyses.length) {
4454
+ return;
4258
4455
  }
4259
- }
4456
+ step.result.analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
4457
+ });
4260
4458
  } catch {
4261
- log7.debug("Failed to parse batch analysis result", {
4459
+ log8.debug("Failed to parse batch analysis result", {
4262
4460
  batchResult
4263
4461
  });
4264
4462
  }
@@ -4269,13 +4467,10 @@ async function runBrowserAutomation(task, context, opts) {
4269
4467
  }
4270
4468
  return result2;
4271
4469
  },
4272
- toolRegistry: context.toolRegistry,
4273
- captureArtifacts: ["screenshotFullPage"]
4470
+ toolRegistry: context.toolRegistry
4274
4471
  });
4275
4472
  context.subAgentMessages?.set(context.toolCallId, result.messages);
4276
- const fullPage = result.artifacts?.screenshotFullPage;
4277
- const viewport = lastBrowserCommandViewport;
4278
- const preferred = opts?.capture === "viewport" ? viewport ?? fullPage : fullPage ?? viewport;
4473
+ const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4279
4474
  return {
4280
4475
  text: result.text,
4281
4476
  ...preferred?.url ? { screenshot: { url: preferred.url, styleMap: preferred.styleMap } } : {}
@@ -4285,8 +4480,8 @@ async function runBrowserAutomation(task, context, opts) {
4285
4480
  }
4286
4481
  }
4287
4482
  var browserAutomationTool = {
4288
- clearable: true,
4289
4483
  definition: {
4484
+ clearable: true,
4290
4485
  name: "runAutomatedBrowserTest",
4291
4486
  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).",
4292
4487
  inputSchema: {
@@ -4315,98 +4510,104 @@ var browserAutomationTool = {
4315
4510
  };
4316
4511
 
4317
4512
  // src/tools/code/screenshot.ts
4318
- var screenshotTool = {
4513
+ var screenshotDefinition = {
4319
4514
  clearable: true,
4320
- definition: {
4321
- name: "screenshot",
4322
- 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.",
4323
- inputSchema: {
4324
- type: "object",
4325
- properties: {
4326
- fullPage: {
4327
- type: "boolean",
4328
- 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."
4329
- },
4330
- prompt: {
4331
- type: "string",
4332
- description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
4333
- },
4334
- imageUrl: {
4335
- type: "string",
4336
- description: "URL of an existing screenshot to analyze instead of capturing a new one. Use this for additional questions about a previous screenshot."
4337
- },
4338
- path: {
4339
- type: "string",
4340
- description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
4341
- },
4342
- width: {
4343
- type: "number",
4344
- 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."
4345
- },
4346
- height: {
4347
- type: "number",
4348
- description: "Exact capture height in pixels. Set together with `width`."
4349
- },
4350
- format: {
4351
- type: "string",
4352
- enum: ["png", "jpeg"],
4353
- 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."
4354
- },
4355
- instructions: {
4356
- type: "string",
4357
- 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."
4358
- }
4515
+ name: "screenshot",
4516
+ 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.",
4517
+ inputSchema: {
4518
+ type: "object",
4519
+ properties: {
4520
+ fullPage: {
4521
+ type: "boolean",
4522
+ 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."
4359
4523
  },
4360
- required: ["fullPage"]
4361
- }
4362
- },
4363
- async execute(input, context) {
4364
- const fullPage = input.fullPage === true;
4365
- const shotKind = fullPage ? "full-page" : "viewport";
4366
- try {
4367
- if (input.imageUrl) {
4368
- return await captureAndAnalyzeScreenshot({
4369
- prompt: input.prompt,
4370
- imageUrl: input.imageUrl,
4371
- onLog: context?.onLog,
4372
- model: resolveModel("imageAnalysis", context?.models, context?.model)
4373
- });
4374
- }
4375
- if (input.instructions && context) {
4376
- 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.`;
4377
- const result = await runBrowserAutomation(task, context, {
4378
- capture: fullPage ? "fullPage" : "viewport"
4379
- });
4380
- if (!result.screenshot) {
4381
- return result.text;
4382
- }
4383
- return await streamScreenshotAnalysis({
4384
- url: result.screenshot.url,
4385
- prompt: input.prompt,
4386
- styleMap: result.screenshot.styleMap,
4387
- onLog: context?.onLog,
4388
- model: resolveModel("imageAnalysis", context?.models, context?.model)
4389
- });
4524
+ prompt: {
4525
+ type: "string",
4526
+ description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
4527
+ },
4528
+ imageUrl: {
4529
+ type: "string",
4530
+ 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."
4531
+ },
4532
+ path: {
4533
+ type: "string",
4534
+ description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
4535
+ },
4536
+ width: {
4537
+ type: "number",
4538
+ 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."
4539
+ },
4540
+ height: {
4541
+ type: "number",
4542
+ description: "Exact capture height in pixels. Set together with `width`."
4543
+ },
4544
+ format: {
4545
+ type: "string",
4546
+ enum: ["png", "jpeg"],
4547
+ 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."
4548
+ },
4549
+ instructions: {
4550
+ type: "string",
4551
+ 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."
4390
4552
  }
4391
- const release = await acquireBrowserLock();
4392
- try {
4393
- return await captureAndAnalyzeScreenshot({
4394
- prompt: input.prompt,
4395
- path: input.path,
4396
- fullPage,
4397
- width: input.width,
4398
- height: input.height,
4399
- format: input.format,
4400
- onLog: context?.onLog,
4401
- model: resolveModel("imageAnalysis", context?.models, context?.model)
4402
- });
4403
- } finally {
4404
- release();
4553
+ },
4554
+ required: ["fullPage"]
4555
+ }
4556
+ };
4557
+ async function executeScreenshot(input, onLog, context) {
4558
+ const fullPage = input.fullPage === true;
4559
+ const model = resolveModel("imageAnalysis", context?.models, context?.model);
4560
+ try {
4561
+ if (input.imageUrl) {
4562
+ return await captureAndAnalyzeScreenshot({
4563
+ prompt: input.prompt,
4564
+ image: input.imageUrl,
4565
+ onLog,
4566
+ model,
4567
+ apiConfig: context?.apiConfig
4568
+ });
4569
+ }
4570
+ if (input.instructions && context) {
4571
+ const shotKind = fullPage ? "full-page" : "viewport";
4572
+ 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.`;
4573
+ const result = await runBrowserAutomation(task, context, {
4574
+ capture: fullPage ? "fullPage" : "viewport"
4575
+ });
4576
+ if (!result.screenshot) {
4577
+ return result.text;
4405
4578
  }
4406
- } catch (err) {
4407
- return `Error taking screenshot: ${err.message}`;
4579
+ return await streamScreenshotAnalysis({
4580
+ image: result.screenshot.url,
4581
+ prompt: input.prompt,
4582
+ styleMap: result.screenshot.styleMap,
4583
+ onLog,
4584
+ model,
4585
+ apiConfig: context?.apiConfig
4586
+ });
4408
4587
  }
4588
+ const release = await acquireBrowserLock();
4589
+ try {
4590
+ return await captureAndAnalyzeScreenshot({
4591
+ prompt: input.prompt,
4592
+ path: input.path,
4593
+ fullPage,
4594
+ width: input.width,
4595
+ height: input.height,
4596
+ format: input.format,
4597
+ onLog,
4598
+ model,
4599
+ apiConfig: context?.apiConfig
4600
+ });
4601
+ } finally {
4602
+ release();
4603
+ }
4604
+ } catch (err) {
4605
+ return `Error taking screenshot: ${err.message}`;
4409
4606
  }
4607
+ }
4608
+ var screenshotTool = {
4609
+ definition: screenshotDefinition,
4610
+ execute: (input, context) => executeScreenshot(input, context?.onLog, context)
4410
4611
  };
4411
4612
 
4412
4613
  // src/subagents/designExpert/tools/searchGoogle.ts
@@ -4509,13 +4710,13 @@ Respond only with your analysis as Markdown and absolutely no other text. Do not
4509
4710
  var definition3 = {
4510
4711
  clearable: false,
4511
4712
  name: "analyzeDesign",
4512
- 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.",
4713
+ 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.",
4513
4714
  inputSchema: {
4514
4715
  type: "object",
4515
4716
  properties: {
4516
4717
  url: {
4517
4718
  type: "string",
4518
- description: "URL to analyze. Can be an image URL or a website URL (will be screenshotted)."
4719
+ 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."
4519
4720
  },
4520
4721
  prompt: {
4521
4722
  type: "string",
@@ -4528,9 +4729,9 @@ var definition3 = {
4528
4729
  async function execute3(input, onLog, context) {
4529
4730
  const url = input.url;
4530
4731
  const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
4531
- const isImageUrl = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
4532
- let imageUrl = url;
4533
- if (!isImageUrl) {
4732
+ const isImage = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
4733
+ let image = url;
4734
+ if (!isImage) {
4534
4735
  const ss = await runMindstudioCliResult(
4535
4736
  [
4536
4737
  "screenshot-url",
@@ -4553,15 +4754,16 @@ async function execute3(input, onLog, context) {
4553
4754
  if (!ss.ok) {
4554
4755
  return `Could not screenshot ${url}: ${ss.value}`;
4555
4756
  }
4556
- imageUrl = ss.value;
4757
+ image = ss.value;
4557
4758
  }
4558
- const analysis = await analyzeImage({
4759
+ const analyzed = await analyzeImage({
4559
4760
  prompt: analysisPrompt,
4560
- imageUrl,
4761
+ image,
4762
+ apiConfig: context?.apiConfig,
4561
4763
  onLog,
4562
4764
  model: resolveModel("imageAnalysis", context?.models, context?.model)
4563
4765
  });
4564
- return JSON.stringify({ url: imageUrl, analysis });
4766
+ return JSON.stringify({ url: analyzed.url, analysis: analyzed.analysis });
4565
4767
  }
4566
4768
 
4567
4769
  // src/subagents/designExpert/tools/analyzeImage.ts
@@ -4573,13 +4775,16 @@ __export(analyzeImage_exports, {
4573
4775
  var definition4 = {
4574
4776
  clearable: true,
4575
4777
  name: "analyzeImage",
4576
- 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.",
4778
+ 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.",
4577
4779
  inputSchema: {
4578
4780
  type: "object",
4579
4781
  properties: {
4782
+ // Still `imageUrl` even though a path works: the builder renders this
4783
+ // tool's thumbnail from `input.imageUrl` and deploys separately, so
4784
+ // renaming the property would blank the view under version skew.
4580
4785
  imageUrl: {
4581
4786
  type: "string",
4582
- description: "The image URL to analyze."
4787
+ 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."
4583
4788
  },
4584
4789
  prompt: {
4585
4790
  type: "string",
@@ -4590,96 +4795,24 @@ var definition4 = {
4590
4795
  }
4591
4796
  };
4592
4797
  async function execute4(input, onLog, context) {
4593
- const imageUrl = input.imageUrl;
4594
4798
  const prompt = buildScreenshotAnalysisPrompt({
4595
4799
  prompt: input.prompt
4596
4800
  });
4597
- const analysis = await analyzeImage({
4801
+ const { url, analysis } = await analyzeImage({
4598
4802
  prompt,
4599
- imageUrl,
4803
+ image: input.imageUrl,
4804
+ apiConfig: context?.apiConfig,
4600
4805
  onLog,
4601
4806
  model: resolveModel("imageAnalysis", context?.models, context?.model)
4602
4807
  });
4603
- return JSON.stringify({ url: imageUrl, analysis });
4604
- }
4605
-
4606
- // src/subagents/designExpert/tools/screenshot.ts
4607
- var screenshot_exports = {};
4608
- __export(screenshot_exports, {
4609
- definition: () => definition5,
4610
- execute: () => execute5
4611
- });
4612
- var definition5 = {
4613
- clearable: true,
4614
- name: "screenshot",
4615
- 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.",
4616
- inputSchema: {
4617
- type: "object",
4618
- properties: {
4619
- fullPage: {
4620
- type: "boolean",
4621
- 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."
4622
- },
4623
- prompt: {
4624
- type: "string",
4625
- description: "Optional specific question about the screenshot. Use a bulleted list to ask many questions at once."
4626
- },
4627
- path: {
4628
- type: "string",
4629
- description: 'Navigate to this path before capturing (e.g. "/settings"). If omitted, screenshots the current page.'
4630
- },
4631
- instructions: {
4632
- type: "string",
4633
- 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."
4634
- }
4635
- },
4636
- required: ["fullPage"]
4637
- }
4638
- };
4639
- async function execute5(input, onLog, context) {
4640
- const fullPage = input.fullPage === true;
4641
- const shotKind = fullPage ? "full-page" : "viewport";
4642
- if (input.instructions && context) {
4643
- try {
4644
- 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.`;
4645
- const result = await runBrowserAutomation(task, context, {
4646
- capture: fullPage ? "fullPage" : "viewport"
4647
- });
4648
- if (!result.screenshot) {
4649
- return result.text;
4650
- }
4651
- return await streamScreenshotAnalysis({
4652
- url: result.screenshot.url,
4653
- prompt: input.prompt,
4654
- styleMap: result.screenshot.styleMap,
4655
- onLog,
4656
- model: resolveModel("imageAnalysis", context?.models, context?.model)
4657
- });
4658
- } catch (err) {
4659
- return `Error taking interactive screenshot: ${err.message}`;
4660
- }
4661
- }
4662
- const release = await acquireBrowserLock();
4663
- try {
4664
- return await captureAndAnalyzeScreenshot({
4665
- prompt: input.prompt,
4666
- path: input.path,
4667
- fullPage,
4668
- onLog,
4669
- model: resolveModel("imageAnalysis", context?.models, context?.model)
4670
- });
4671
- } catch (err) {
4672
- return `Error taking screenshot: ${err.message}`;
4673
- } finally {
4674
- release();
4675
- }
4808
+ return JSON.stringify({ url, analysis });
4676
4809
  }
4677
4810
 
4678
4811
  // src/subagents/designExpert/tools/images/generateImages.ts
4679
4812
  var generateImages_exports = {};
4680
4813
  __export(generateImages_exports, {
4681
- definition: () => definition6,
4682
- execute: () => execute6
4814
+ definition: () => definition5,
4815
+ execute: () => execute5
4683
4816
  });
4684
4817
 
4685
4818
  // src/subagents/designExpert/tools/images/enhancePrompt.ts
@@ -4741,14 +4874,15 @@ var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcin
4741
4874
  async function generateImageAssets(opts) {
4742
4875
  const {
4743
4876
  prompts,
4744
- sourceImages,
4745
4877
  transparentBackground,
4746
4878
  enhancePrompts,
4747
4879
  onLog,
4880
+ apiConfig,
4748
4881
  imageGenerationModel: genModel,
4749
4882
  imageAnalysisModel,
4750
4883
  imagePromptEnhancerModel
4751
4884
  } = opts;
4885
+ const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
4752
4886
  const width = opts.width || 2048;
4753
4887
  const height = opts.height || 2048;
4754
4888
  const config = { width, height };
@@ -4854,10 +4988,10 @@ async function generateImageAssets(opts) {
4854
4988
  }
4855
4989
  const analysis = await analyzeImage({
4856
4990
  prompt: ANALYZE_PROMPT,
4857
- imageUrl: url,
4991
+ image: url,
4858
4992
  onLog,
4859
4993
  model: imageAnalysisModel
4860
- });
4994
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
4861
4995
  return {
4862
4996
  url,
4863
4997
  prompt: prompts[i],
@@ -4872,7 +5006,7 @@ async function generateImageAssets(opts) {
4872
5006
  }
4873
5007
 
4874
5008
  // src/subagents/designExpert/tools/images/generateImages.ts
4875
- var definition6 = {
5009
+ var definition5 = {
4876
5010
  clearable: false,
4877
5011
  name: "generateImages",
4878
5012
  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.",
@@ -4888,7 +5022,7 @@ var definition6 = {
4888
5022
  },
4889
5023
  referenceImage: {
4890
5024
  type: "string",
4891
- 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."
5025
+ 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."
4892
5026
  },
4893
5027
  width: {
4894
5028
  type: "number",
@@ -4906,7 +5040,7 @@ var definition6 = {
4906
5040
  required: ["prompts"]
4907
5041
  }
4908
5042
  };
4909
- async function execute6(input, onLog, context) {
5043
+ async function execute5(input, onLog, context) {
4910
5044
  return generateImageAssets({
4911
5045
  prompts: input.prompts,
4912
5046
  width: input.width,
@@ -4915,6 +5049,7 @@ async function execute6(input, onLog, context) {
4915
5049
  sourceImages: input.referenceImage ? [input.referenceImage] : void 0,
4916
5050
  enhancePrompts: true,
4917
5051
  onLog,
5052
+ apiConfig: context?.apiConfig,
4918
5053
  imageGenerationModel: resolveModel(
4919
5054
  "imageGeneration",
4920
5055
  context?.models,
@@ -4936,10 +5071,10 @@ async function execute6(input, onLog, context) {
4936
5071
  // src/subagents/designExpert/tools/images/editImages.ts
4937
5072
  var editImages_exports = {};
4938
5073
  __export(editImages_exports, {
4939
- definition: () => definition7,
4940
- execute: () => execute7
5074
+ definition: () => definition6,
5075
+ execute: () => execute6
4941
5076
  });
4942
- var definition7 = {
5077
+ var definition6 = {
4943
5078
  clearable: false,
4944
5079
  name: "editImages",
4945
5080
  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.",
@@ -4958,7 +5093,7 @@ var definition7 = {
4958
5093
  items: {
4959
5094
  type: "string"
4960
5095
  },
4961
- 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."
5096
+ 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."
4962
5097
  },
4963
5098
  width: {
4964
5099
  type: "number",
@@ -4976,7 +5111,7 @@ var definition7 = {
4976
5111
  required: ["prompts", "sourceImages"]
4977
5112
  }
4978
5113
  };
4979
- async function execute7(input, onLog, context) {
5114
+ async function execute6(input, onLog, context) {
4980
5115
  return generateImageAssets({
4981
5116
  prompts: input.prompts,
4982
5117
  sourceImages: input.sourceImages,
@@ -4985,6 +5120,7 @@ async function execute7(input, onLog, context) {
4985
5120
  transparentBackground: input.transparentBackground,
4986
5121
  enhancePrompts: false,
4987
5122
  onLog,
5123
+ apiConfig: context?.apiConfig,
4988
5124
  imageGenerationModel: resolveModel(
4989
5125
  "imageGeneration",
4990
5126
  context?.models,
@@ -5006,8 +5142,8 @@ async function execute7(input, onLog, context) {
5006
5142
  // src/subagents/designExpert/tools/polishCopy.ts
5007
5143
  var polishCopy_exports = {};
5008
5144
  __export(polishCopy_exports, {
5009
- definition: () => definition8,
5010
- execute: () => execute8
5145
+ definition: () => definition7,
5146
+ execute: () => execute7
5011
5147
  });
5012
5148
 
5013
5149
  // src/subagents/copyEditor/tools.ts
@@ -5016,8 +5152,8 @@ var COPY_EDITOR_TOOLS = [...COMMON_READ_TOOLS];
5016
5152
  // src/subagents/copyEditor/index.ts
5017
5153
  var BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
5018
5154
  var copyEditorTool = {
5019
- clearable: false,
5020
5155
  definition: {
5156
+ clearable: false,
5021
5157
  name: "copyEditor",
5022
5158
  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.",
5023
5159
  inputSchema: {
@@ -5064,7 +5200,7 @@ var copyEditorTool = {
5064
5200
  };
5065
5201
 
5066
5202
  // src/subagents/designExpert/tools/polishCopy.ts
5067
- var definition8 = {
5203
+ var definition7 = {
5068
5204
  clearable: false,
5069
5205
  name: "polishCopy",
5070
5206
  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).",
@@ -5079,7 +5215,7 @@ var definition8 = {
5079
5215
  required: ["task"]
5080
5216
  }
5081
5217
  };
5082
- async function execute8(input, _onLog, context) {
5218
+ async function execute7(input, _onLog, context) {
5083
5219
  return copyEditorTool.execute(input, context);
5084
5220
  }
5085
5221
 
@@ -5089,7 +5225,10 @@ var tools = {
5089
5225
  scrapeWebUrl: scrapeWebUrl_exports,
5090
5226
  analyzeDesign: analyzeDesign_exports,
5091
5227
  analyzeImage: analyzeImage_exports,
5092
- screenshot: screenshot_exports,
5228
+ // Same tool the main agent offers, imported rather than reimplemented — the
5229
+ // two used to be near-identical copies and had already drifted apart. Its core
5230
+ // already takes (input, onLog, context), which is this registry's convention.
5231
+ screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
5093
5232
  generateImages: generateImages_exports,
5094
5233
  editImages: editImages_exports,
5095
5234
  polishCopy: polishCopy_exports
@@ -5108,7 +5247,7 @@ async function executeDesignExpertTool(name, input, context, toolCallId, onLog)
5108
5247
  }
5109
5248
 
5110
5249
  // src/subagents/designExpert/data/sampleCache.ts
5111
- import fs17 from "fs";
5250
+ import fs18 from "fs";
5112
5251
  var SAMPLE_FILE = ".remy-design-sample.json";
5113
5252
  var cached2 = null;
5114
5253
  function generateIndices(poolSize, sampleSize) {
@@ -5122,14 +5261,14 @@ function generateIndices(poolSize, sampleSize) {
5122
5261
  }
5123
5262
  function load() {
5124
5263
  try {
5125
- return JSON.parse(fs17.readFileSync(SAMPLE_FILE, "utf-8"));
5264
+ return JSON.parse(fs18.readFileSync(SAMPLE_FILE, "utf-8"));
5126
5265
  } catch {
5127
5266
  return null;
5128
5267
  }
5129
5268
  }
5130
5269
  function save(indices) {
5131
5270
  try {
5132
- fs17.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
5271
+ fs18.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
5133
5272
  } catch {
5134
5273
  }
5135
5274
  }
@@ -5413,8 +5552,8 @@ async function runDesignExpert(opts, context) {
5413
5552
  });
5414
5553
  }
5415
5554
  var designExpertTool = {
5416
- clearable: false,
5417
5555
  definition: {
5556
+ clearable: false,
5418
5557
  name: "visualDesignExpert",
5419
5558
  description: DESCRIPTION,
5420
5559
  inputSchema: {
@@ -5503,28 +5642,28 @@ var VISION_TOOLS = [
5503
5642
  ];
5504
5643
 
5505
5644
  // src/subagents/productVision/executor.ts
5506
- import fs18 from "fs";
5507
- import path10 from "path";
5645
+ import fs19 from "fs";
5646
+ import path11 from "path";
5508
5647
  var ROADMAP_DIR = "src/roadmap";
5509
5648
  var PITCH_DECK_SHELL = readAsset(
5510
5649
  "subagents/productVision",
5511
5650
  "pitch-deck-shell.html"
5512
5651
  );
5513
- function resolve(filePath) {
5514
- return path10.join(ROADMAP_DIR, filePath);
5652
+ function resolve2(filePath) {
5653
+ return path11.join(ROADMAP_DIR, filePath);
5515
5654
  }
5516
5655
  async function executeVisionTool(name, input, context) {
5517
5656
  switch (name) {
5518
5657
  case "writeFile": {
5519
- const filePath = resolve(input.path);
5658
+ const filePath = resolve2(input.path);
5520
5659
  try {
5521
- fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
5660
+ fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5522
5661
  let oldContent = null;
5523
5662
  try {
5524
- oldContent = fs18.readFileSync(filePath, "utf-8");
5663
+ oldContent = fs19.readFileSync(filePath, "utf-8");
5525
5664
  } catch {
5526
5665
  }
5527
- fs18.writeFileSync(filePath, input.content, "utf-8");
5666
+ fs19.writeFileSync(filePath, input.content, "utf-8");
5528
5667
  const lineCount = input.content.split("\n").length;
5529
5668
  const label = oldContent !== null ? "Wrote" : "Created";
5530
5669
  return `${label} ${filePath} (${lineCount} lines)
@@ -5534,13 +5673,13 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
5534
5673
  }
5535
5674
  }
5536
5675
  case "deleteFile": {
5537
- const filePath = resolve(input.path);
5676
+ const filePath = resolve2(input.path);
5538
5677
  try {
5539
- if (!fs18.existsSync(filePath)) {
5678
+ if (!fs19.existsSync(filePath)) {
5540
5679
  return `Error: ${filePath} does not exist`;
5541
5680
  }
5542
- const oldContent = fs18.readFileSync(filePath, "utf-8");
5543
- fs18.unlinkSync(filePath);
5681
+ const oldContent = fs19.readFileSync(filePath, "utf-8");
5682
+ fs19.unlinkSync(filePath);
5544
5683
  return `Deleted ${filePath}
5545
5684
  ${unifiedDiff(filePath, oldContent, "")}`;
5546
5685
  } catch (err) {
@@ -5551,11 +5690,11 @@ ${unifiedDiff(filePath, oldContent, "")}`;
5551
5690
  if (!context) {
5552
5691
  return "Error: writePitchDeck requires execution context for design expert delegation";
5553
5692
  }
5554
- const filePath = resolve("pitch.html");
5693
+ const filePath = resolve2("pitch.html");
5555
5694
  try {
5556
- fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
5557
- const exists = fs18.existsSync(filePath);
5558
- const before = exists ? fs18.statSync(filePath).mtimeMs : null;
5695
+ fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5696
+ const exists = fs19.existsSync(filePath);
5697
+ const before = exists ? fs19.statSync(filePath).mtimeMs : null;
5559
5698
  const delivery = exists ? `### Your deliverable
5560
5699
  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.
5561
5700
 
@@ -5585,11 +5724,11 @@ Maintain the bones of the presentation scaffolding. Always keep the progress bar
5585
5724
  ${delivery}`;
5586
5725
  const result = await runDesignExpertRender({ task }, context);
5587
5726
  context.subAgentMessages?.set(context.toolCallId, result.messages);
5588
- if (!fs18.existsSync(filePath)) {
5727
+ if (!fs19.existsSync(filePath)) {
5589
5728
  return `Error: the design expert did not write ${filePath}. Its reply was:
5590
5729
  ${result.text}`;
5591
5730
  }
5592
- if (before !== null && fs18.statSync(filePath).mtimeMs === before) {
5731
+ if (before !== null && fs19.statSync(filePath).mtimeMs === before) {
5593
5732
  return `Error: the pitch deck at ${filePath} was not modified. The design expert's reply was:
5594
5733
  ${result.text}`;
5595
5734
  }
@@ -5621,8 +5760,8 @@ function getProductVisionPrompt() {
5621
5760
 
5622
5761
  // src/subagents/productVision/index.ts
5623
5762
  var productVisionTool = {
5624
- clearable: false,
5625
5763
  definition: {
5764
+ clearable: false,
5626
5765
  name: "productVision",
5627
5766
  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.",
5628
5767
  inputSchema: {
@@ -5734,8 +5873,8 @@ var SANITY_CHECK_TOOLS = [
5734
5873
  // src/subagents/codeSanityCheck/index.ts
5735
5874
  var BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
5736
5875
  var codeSanityCheckTool = {
5737
- clearable: false,
5738
5876
  definition: {
5877
+ clearable: false,
5739
5878
  name: "codeSanityCheck",
5740
5879
  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.',
5741
5880
  inputSchema: {
@@ -5810,9 +5949,9 @@ ${readAsset(
5810
5949
  )}
5811
5950
  </mindstudio_flavored_markdown_spec_docs>`;
5812
5951
  var specSyncTool = {
5813
- clearable: false,
5814
5952
  backgroundOnly: true,
5815
5953
  definition: {
5954
+ clearable: false,
5816
5955
  name: "specSync",
5817
5956
  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.",
5818
5957
  inputSchema: {
@@ -5879,8 +6018,8 @@ var specSyncTool = {
5879
6018
 
5880
6019
  // src/tools/common/scrapeWebUrl.ts
5881
6020
  var scrapeWebUrlTool = {
5882
- clearable: false,
5883
6021
  definition: {
6022
+ clearable: false,
5884
6023
  name: "scrapeWebUrl",
5885
6024
  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",
5886
6025
  inputSchema: {
@@ -5923,7 +6062,7 @@ var scrapeWebUrlTool = {
5923
6062
  };
5924
6063
 
5925
6064
  // src/tools/spec/writeBuildOverview.ts
5926
- import fs19 from "fs";
6065
+ import fs20 from "fs";
5927
6066
  var OVERVIEW_FILE = "src/overview.html";
5928
6067
  var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
5929
6068
 
@@ -5980,8 +6119,8 @@ The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update
5980
6119
  Then reply with a one-line summary of what you changed.`;
5981
6120
  }
5982
6121
  var buildOverviewTool = {
5983
- clearable: false,
5984
6122
  definition: {
6123
+ clearable: false,
5985
6124
  name: "writeBuildOverview",
5986
6125
  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.",
5987
6126
  inputSchema: {
@@ -6003,7 +6142,7 @@ var buildOverviewTool = {
6003
6142
  if (!content) {
6004
6143
  return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6005
6144
  }
6006
- const exists = fs19.existsSync(OVERVIEW_FILE);
6145
+ const exists = fs20.existsSync(OVERVIEW_FILE);
6007
6146
  const task = `<overview_copy>${content}</overview_copy>
6008
6147
 
6009
6148
  ${DESIGN_BRIEF}
@@ -6021,7 +6160,7 @@ ${exists ? refreshDelivery() : initialDelivery()}`;
6021
6160
  }
6022
6161
  const result = await runDesignExpertRender({ task }, context);
6023
6162
  context.subAgentMessages?.set(context.toolCallId, result.messages);
6024
- if (!fs19.existsSync(OVERVIEW_FILE)) {
6163
+ if (!fs20.existsSync(OVERVIEW_FILE)) {
6025
6164
  return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6026
6165
  ${result.text}`;
6027
6166
  }
@@ -6078,11 +6217,11 @@ var ALL_TOOLS = [
6078
6217
  browserAutomationTool,
6079
6218
  // LSP
6080
6219
  lspDiagnosticsTool,
6081
- restartProcessTool
6220
+ restartProcessTool,
6221
+ // Appended rather than grouped: position is part of the cache prefix, so a
6222
+ // new tool goes at the end to leave every existing session's prefix intact.
6223
+ loadSkillTool
6082
6224
  ];
6083
- var CLEARABLE_TOOLS = new Set(
6084
- ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
6085
- );
6086
6225
  var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
6087
6226
  "visualDesignExpert",
6088
6227
  "productVision",
@@ -6107,7 +6246,7 @@ function executeTool(name, input, context) {
6107
6246
  }
6108
6247
 
6109
6248
  // src/compaction/index.ts
6110
- var log8 = createLogger("compaction");
6249
+ var log9 = createLogger("compaction");
6111
6250
  var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
6112
6251
  var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
6113
6252
  var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
@@ -6156,7 +6295,7 @@ async function compactConversation(messages, apiConfig, model) {
6156
6295
  if (text) {
6157
6296
  summaries.push({ name, text });
6158
6297
  } else {
6159
- log8.warn("Subagent summary unusable \u2014 leaving its history intact", {
6298
+ log9.warn("Subagent summary unusable \u2014 leaving its history intact", {
6160
6299
  name
6161
6300
  });
6162
6301
  }
@@ -6184,7 +6323,7 @@ async function compactConversation(messages, apiConfig, model) {
6184
6323
  }
6185
6324
  ]
6186
6325
  }));
6187
- log8.info("Compaction complete", {
6326
+ log9.info("Compaction complete", {
6188
6327
  summaries: summaries.length,
6189
6328
  recentNarrativeChars: recent.length
6190
6329
  });
@@ -6386,7 +6525,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6386
6525
  messagesToSummarize.slice(0, mid),
6387
6526
  messagesToSummarize.slice(mid)
6388
6527
  ];
6389
- log8.info("Chunking summary", {
6528
+ log9.info("Chunking summary", {
6390
6529
  name,
6391
6530
  messageCount: messagesToSummarize.length,
6392
6531
  serializedLength: serialized.length,
@@ -6416,7 +6555,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6416
6555
  const parts = results.filter((p) => p !== null);
6417
6556
  return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
6418
6557
  }
6419
- log8.info("Generating summary", {
6558
+ log9.info("Generating summary", {
6420
6559
  name,
6421
6560
  messageCount: messagesToSummarize.length,
6422
6561
  serializedLength: serialized.length
@@ -6432,10 +6571,10 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6432
6571
  return null;
6433
6572
  }
6434
6573
  if (summaryText.length >= MIN_SUMMARY_CHARS) {
6435
- log8.info("Summary generated", { name, summaryLength: summaryText.length });
6574
+ log9.info("Summary generated", { name, summaryLength: summaryText.length });
6436
6575
  return summaryText;
6437
6576
  }
6438
- log8.warn("Summary too short to be real", {
6577
+ log9.warn("Summary too short to be real", {
6439
6578
  name,
6440
6579
  summaryLength: summaryText.length,
6441
6580
  minimum: MIN_SUMMARY_CHARS,
@@ -6491,21 +6630,21 @@ Write the summary of the conversation above, following your instructions.`;
6491
6630
  toolNames: []
6492
6631
  });
6493
6632
  } else if (event.type === "error") {
6494
- log8.error("Summary generation failed", { name, error: event.error });
6633
+ log9.error("Summary generation failed", { name, error: event.error });
6495
6634
  return null;
6496
6635
  }
6497
6636
  }
6498
6637
  if (!summaryText.trim()) {
6499
- log8.warn("Empty summary generated", { name });
6638
+ log9.warn("Empty summary generated", { name });
6500
6639
  return null;
6501
6640
  }
6502
6641
  return summaryText.trim();
6503
6642
  }
6504
6643
 
6505
6644
  // src/session.ts
6506
- import fs20 from "fs";
6507
- import path11 from "path";
6508
- var log9 = createLogger("session");
6645
+ import fs21 from "fs";
6646
+ import path12 from "path";
6647
+ var log10 = createLogger("session");
6509
6648
  var SESSION_FILE = ".remy-session.json";
6510
6649
  var ARCHIVE_DIR = ".logs/sessions";
6511
6650
  var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
@@ -6522,14 +6661,14 @@ var ARCHIVE_MSG_CACHE_MAX = 3;
6522
6661
  function loadSession(state) {
6523
6662
  pruneArchives();
6524
6663
  try {
6525
- const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
6664
+ const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
6526
6665
  const data = JSON.parse(raw);
6527
6666
  if (data.models && typeof data.models === "object") {
6528
6667
  state.models = data.models;
6529
6668
  }
6530
6669
  if (Array.isArray(data.messages) && data.messages.length > 0) {
6531
6670
  state.messages = sanitizeMessages(data.messages);
6532
- log9.info("Session loaded", {
6671
+ log10.info("Session loaded", {
6533
6672
  messageCount: state.messages.length,
6534
6673
  ...state.models && { models: state.models }
6535
6674
  });
@@ -6595,33 +6734,33 @@ function buildPayload(state) {
6595
6734
  return payload;
6596
6735
  }
6597
6736
  function archiveMessages(messages, label, models) {
6598
- fs20.mkdirSync(ARCHIVE_DIR, { recursive: true });
6737
+ fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
6599
6738
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6600
6739
  const count = messages.length;
6601
- let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
6740
+ let dest = path12.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
6602
6741
  let n = 1;
6603
- while (fs20.existsSync(dest)) {
6604
- dest = path11.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.c${count}.json`);
6742
+ while (fs21.existsSync(dest)) {
6743
+ dest = path12.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.c${count}.json`);
6605
6744
  }
6606
6745
  const payload = { messages };
6607
6746
  if (models && Object.keys(models).length > 0) {
6608
6747
  payload.models = models;
6609
6748
  }
6610
- fs20.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6611
- archiveCountCache.set(path11.basename(dest), count);
6612
- log9.info("Session archived", { label, dest, messageCount: count });
6749
+ fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6750
+ archiveCountCache.set(path12.basename(dest), count);
6751
+ log10.info("Session archived", { label, dest, messageCount: count });
6613
6752
  pruneArchives();
6614
6753
  return dest;
6615
6754
  }
6616
6755
  function pruneArchives() {
6617
6756
  try {
6618
- const entries = fs20.readdirSync(ARCHIVE_DIR).filter((name) => ARCHIVE_NAME_RE.test(name));
6757
+ const entries = fs21.readdirSync(ARCHIVE_DIR).filter((name) => ARCHIVE_NAME_RE.test(name));
6619
6758
  if (entries.length <= 1) {
6620
6759
  return;
6621
6760
  }
6622
6761
  const archives = entries.map((name) => ({
6623
6762
  name,
6624
- size: fs20.statSync(path11.join(ARCHIVE_DIR, name)).size
6763
+ size: fs21.statSync(path12.join(ARCHIVE_DIR, name)).size
6625
6764
  })).sort(
6626
6765
  (a, b) => archiveSortKey(b.name).localeCompare(archiveSortKey(a.name))
6627
6766
  );
@@ -6639,14 +6778,14 @@ function pruneArchives() {
6639
6778
  let freed = 0;
6640
6779
  for (let i = cut; i < archives.length; i++) {
6641
6780
  try {
6642
- fs20.unlinkSync(path11.join(ARCHIVE_DIR, archives[i].name));
6781
+ fs21.unlinkSync(path12.join(ARCHIVE_DIR, archives[i].name));
6643
6782
  freed += archives[i].size;
6644
6783
  removed++;
6645
6784
  } catch {
6646
6785
  }
6647
6786
  }
6648
6787
  if (removed > 0) {
6649
- log9.info("Session archives pruned", {
6788
+ log10.info("Session archives pruned", {
6650
6789
  removed,
6651
6790
  freedBytes: freed,
6652
6791
  keptBytes: kept
@@ -6663,7 +6802,7 @@ function parseArchive(name) {
6663
6802
  return cached3;
6664
6803
  }
6665
6804
  try {
6666
- const raw = fs20.readFileSync(path11.join(ARCHIVE_DIR, name), "utf-8");
6805
+ const raw = fs21.readFileSync(path12.join(ARCHIVE_DIR, name), "utf-8");
6667
6806
  const data = JSON.parse(raw);
6668
6807
  const messages = Array.isArray(data?.messages) ? data.messages : [];
6669
6808
  archiveCountCache.set(name, messages.length);
@@ -6677,7 +6816,7 @@ function parseArchive(name) {
6677
6816
  }
6678
6817
  return messages;
6679
6818
  } catch (err) {
6680
- log9.warn("Session archive unreadable", { name, error: err?.message });
6819
+ log10.warn("Session archive unreadable", { name, error: err?.message });
6681
6820
  return null;
6682
6821
  }
6683
6822
  }
@@ -6701,7 +6840,7 @@ function readArchiveMessages(name) {
6701
6840
  function listConversationArchives() {
6702
6841
  let names;
6703
6842
  try {
6704
- names = fs20.readdirSync(ARCHIVE_DIR).filter((n) => ARCHIVE_NAME_RE.test(n));
6843
+ names = fs21.readdirSync(ARCHIVE_DIR).filter((n) => ARCHIVE_NAME_RE.test(n));
6705
6844
  } catch {
6706
6845
  return { slots: [], archivedCount: 0 };
6707
6846
  }
@@ -6800,7 +6939,7 @@ function rotate(state) {
6800
6939
  }
6801
6940
  archiveMessages(messages.slice(0, cut), "rotated", state.models);
6802
6941
  state.messages = messages.slice(cut);
6803
- log9.info("Session rotated", {
6942
+ log10.info("Session rotated", {
6804
6943
  archived: cut,
6805
6944
  retained: state.messages.length
6806
6945
  });
@@ -6812,10 +6951,10 @@ function saveSession(state) {
6812
6951
  if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6813
6952
  serialized = JSON.stringify(buildPayload(state));
6814
6953
  }
6815
- fs20.writeFileSync(SESSION_FILE, serialized, "utf-8");
6816
- log9.info("Session saved", { messageCount: state.messages.length });
6954
+ fs21.writeFileSync(SESSION_FILE, serialized, "utf-8");
6955
+ log10.info("Session saved", { messageCount: state.messages.length });
6817
6956
  } catch (err) {
6818
- log9.warn("Session save failed", { error: err.message });
6957
+ log10.warn("Session save failed", { error: err.message });
6819
6958
  }
6820
6959
  }
6821
6960
  function clearSession(state) {
@@ -6824,22 +6963,22 @@ function clearSession(state) {
6824
6963
  archiveMessages(state.messages, "cleared", state.models);
6825
6964
  }
6826
6965
  } catch (err) {
6827
- log9.warn("Session archive on clear failed", { error: err.message });
6966
+ log10.warn("Session archive on clear failed", { error: err.message });
6828
6967
  }
6829
6968
  state.messages = [];
6830
6969
  try {
6831
- if (fs20.existsSync(SESSION_FILE)) {
6832
- fs20.unlinkSync(SESSION_FILE);
6970
+ if (fs21.existsSync(SESSION_FILE)) {
6971
+ fs21.unlinkSync(SESSION_FILE);
6833
6972
  }
6834
6973
  } catch (err) {
6835
- log9.warn("Session clear: could not remove live file", {
6974
+ log10.warn("Session clear: could not remove live file", {
6836
6975
  error: err.message
6837
6976
  });
6838
6977
  }
6839
6978
  }
6840
6979
 
6841
6980
  // src/compaction/trigger.ts
6842
- var log10 = createLogger("compaction:trigger");
6981
+ var log11 = createLogger("compaction:trigger");
6843
6982
  var pending = null;
6844
6983
  var inflightCompaction = null;
6845
6984
  function applyPendingSummaries(state) {
@@ -6856,7 +6995,7 @@ function applyPendingSummaries(state) {
6856
6995
  idx = at === -1 ? 0 : at + 1;
6857
6996
  }
6858
6997
  state.messages.splice(idx, 0, ...drained.checkpoints);
6859
- log10.info("Checkpoint applied", {
6998
+ log11.info("Checkpoint applied", {
6860
6999
  index: idx,
6861
7000
  messageCount: state.messages.length
6862
7001
  });
@@ -6871,7 +7010,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6871
7010
  return inflightCompaction;
6872
7011
  }
6873
7012
  if (pending) {
6874
- log10.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
7013
+ log11.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
6875
7014
  return Promise.resolve();
6876
7015
  }
6877
7016
  const { blocking = false, requestId, model } = opts;
@@ -6883,11 +7022,11 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6883
7022
  ).then((result) => {
6884
7023
  pending = result;
6885
7024
  listener?.({ type: "complete", requestId });
6886
- log10.info("Compaction complete");
7025
+ log11.info("Compaction complete");
6887
7026
  }).catch((err) => {
6888
7027
  const message = err.message || "Compaction failed";
6889
7028
  listener?.({ type: "complete", error: message, requestId });
6890
- log10.error("Compaction failed", { error: message });
7029
+ log11.error("Compaction failed", { error: message });
6891
7030
  throw err;
6892
7031
  }).finally(() => {
6893
7032
  inflightCompaction = null;
@@ -6896,10 +7035,10 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6896
7035
  }
6897
7036
 
6898
7037
  // src/brandExtraction/index.ts
6899
- import fs21 from "fs";
6900
- import path12 from "path";
7038
+ import fs22 from "fs";
7039
+ import path13 from "path";
6901
7040
  import { createHash } from "crypto";
6902
- var log11 = createLogger("brandExtraction");
7041
+ var log12 = createLogger("brandExtraction");
6903
7042
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6904
7043
  var BRAND_FILE = ".remy-brand.json";
6905
7044
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -6907,28 +7046,28 @@ async function runExtraction(apiConfig, model) {
6907
7046
  const inputHash = computeInputHash();
6908
7047
  const cached3 = readCache();
6909
7048
  if (cached3 && cached3.inputHash === inputHash) {
6910
- log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
7049
+ log12.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6911
7050
  return null;
6912
7051
  }
6913
- log11.info("Extracting brand", { inputHash });
7052
+ log12.info("Extracting brand", { inputHash });
6914
7053
  const brand = await extractBrand(apiConfig, model);
6915
7054
  if (!brand) {
6916
- log11.warn("Brand extraction failed \u2014 leaving cache untouched");
7055
+ log12.warn("Brand extraction failed \u2014 leaving cache untouched");
6917
7056
  return null;
6918
7057
  }
6919
7058
  persistBrand(brand, inputHash);
6920
- log11.info("Brand persisted", { inputHash });
7059
+ log12.info("Brand persisted", { inputHash });
6921
7060
  return brand;
6922
7061
  }
6923
7062
  function isDedicatedBrandFile(filePath) {
6924
- if (filePath.split(path12.sep).includes("@brand")) {
7063
+ if (filePath.split(path13.sep).includes("@brand")) {
6925
7064
  return true;
6926
7065
  }
6927
- const { type } = parseFrontmatter3(filePath);
7066
+ const { type } = parseFrontmatter4(filePath);
6928
7067
  return type.startsWith("design/color") || type.startsWith("design/typography");
6929
7068
  }
6930
7069
  function isBrandRelevant(filePath) {
6931
- return filePath === path12.join("src", "app.md") || isDedicatedBrandFile(filePath);
7070
+ return filePath === path13.join("src", "app.md") || isDedicatedBrandFile(filePath);
6932
7071
  }
6933
7072
  function computeInputHash() {
6934
7073
  const entries = [];
@@ -6950,7 +7089,7 @@ function sha256(input) {
6950
7089
  }
6951
7090
  function readSafe(filePath) {
6952
7091
  try {
6953
- return fs21.readFileSync(filePath, "utf-8");
7092
+ return fs22.readFileSync(filePath, "utf-8");
6954
7093
  } catch {
6955
7094
  return "";
6956
7095
  }
@@ -6976,9 +7115,9 @@ function readBrandManifest() {
6976
7115
  function walkMdFiles3(dir) {
6977
7116
  const results = [];
6978
7117
  try {
6979
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
7118
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
6980
7119
  for (const entry of entries) {
6981
- const full = path12.join(dir, entry.name);
7120
+ const full = path13.join(dir, entry.name);
6982
7121
  if (entry.isDirectory()) {
6983
7122
  results.push(...walkMdFiles3(full));
6984
7123
  } else if (entry.name.endsWith(".md")) {
@@ -6989,9 +7128,9 @@ function walkMdFiles3(dir) {
6989
7128
  }
6990
7129
  return results.sort();
6991
7130
  }
6992
- function parseFrontmatter3(filePath) {
7131
+ function parseFrontmatter4(filePath) {
6993
7132
  try {
6994
- const content = fs21.readFileSync(filePath, "utf-8");
7133
+ const content = fs22.readFileSync(filePath, "utf-8");
6995
7134
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6996
7135
  if (!match) {
6997
7136
  return { type: "" };
@@ -7006,7 +7145,7 @@ function parseFrontmatter3(filePath) {
7006
7145
  async function extractBrand(apiConfig, model) {
7007
7146
  const corpus = buildCorpus();
7008
7147
  if (!corpus.trim()) {
7009
- log11.debug("No spec corpus \u2014 emitting empty brand");
7148
+ log12.debug("No spec corpus \u2014 emitting empty brand");
7010
7149
  return { version: 1 };
7011
7150
  }
7012
7151
  let responseText = "";
@@ -7037,17 +7176,17 @@ async function extractBrand(apiConfig, model) {
7037
7176
  toolNames: []
7038
7177
  });
7039
7178
  } else if (event.type === "error") {
7040
- log11.error("Brand extraction stream error", { error: event.error });
7179
+ log12.error("Brand extraction stream error", { error: event.error });
7041
7180
  return null;
7042
7181
  }
7043
7182
  }
7044
7183
  } catch (err) {
7045
- log11.error("Brand extraction threw", { error: err?.message });
7184
+ log12.error("Brand extraction threw", { error: err?.message });
7046
7185
  return null;
7047
7186
  }
7048
7187
  const parsed = parseJsonResponse(responseText);
7049
7188
  if (!parsed) {
7050
- log11.warn("Brand extraction returned unparseable JSON", {
7189
+ log12.warn("Brand extraction returned unparseable JSON", {
7051
7190
  preview: responseText.slice(0, 200)
7052
7191
  });
7053
7192
  return null;
@@ -7201,14 +7340,14 @@ function pickFont(raw) {
7201
7340
  }
7202
7341
  function persistBrand(brand, inputHash) {
7203
7342
  const tmp = `${BRAND_FILE}.tmp`;
7204
- fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7205
- fs21.renameSync(tmp, BRAND_FILE);
7343
+ fs22.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7344
+ fs22.renameSync(tmp, BRAND_FILE);
7206
7345
  const cache = { inputHash, generatedAt: Date.now() };
7207
- fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7346
+ fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7208
7347
  }
7209
7348
  function readCache() {
7210
7349
  try {
7211
- const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
7350
+ const raw = fs22.readFileSync(CACHE_FILE, "utf-8");
7212
7351
  const parsed = JSON.parse(raw);
7213
7352
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
7214
7353
  return parsed;
@@ -7220,7 +7359,7 @@ function readCache() {
7220
7359
  }
7221
7360
 
7222
7361
  // src/brandExtraction/trigger.ts
7223
- var log12 = createLogger("brandExtraction:trigger");
7362
+ var log13 = createLogger("brandExtraction:trigger");
7224
7363
  var inflight = false;
7225
7364
  var dirty = false;
7226
7365
  function triggerBrandExtraction(apiConfig, model) {
@@ -7230,7 +7369,7 @@ function triggerBrandExtraction(apiConfig, model) {
7230
7369
  }
7231
7370
  inflight = true;
7232
7371
  void runExtraction(apiConfig, model).catch((err) => {
7233
- log12.error("Brand extraction failed", { error: err?.message });
7372
+ log13.error("Brand extraction failed", { error: err?.message });
7234
7373
  }).finally(() => {
7235
7374
  inflight = false;
7236
7375
  if (dirty) {
@@ -7438,7 +7577,7 @@ function friendlyError(raw) {
7438
7577
  }
7439
7578
 
7440
7579
  // src/agent.ts
7441
- var log13 = createLogger("agent");
7580
+ var log14 = createLogger("agent");
7442
7581
  var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
7443
7582
  function getTextContent(blocks) {
7444
7583
  return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
@@ -7492,12 +7631,11 @@ async function runTurn(params) {
7492
7631
  onBackgroundComplete
7493
7632
  } = params;
7494
7633
  const tools2 = getToolDefinitions(onboardingState);
7495
- const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
7496
7634
  const buildModelOverride = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
7497
7635
  const baseline = resolveModel("parent", state.models, model);
7498
7636
  const parentModel = buildModelOverride ?? baseline;
7499
7637
  const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
7500
- log13.info("Turn started", {
7638
+ log14.info("Turn started", {
7501
7639
  requestId,
7502
7640
  model,
7503
7641
  buildModel: buildModelOverride,
@@ -7668,7 +7806,6 @@ async function runTurn(params) {
7668
7806
  system,
7669
7807
  messages: cleanMessagesForApi(state.messages),
7670
7808
  tools: tools2,
7671
- excludeToolsFromClearing,
7672
7809
  signal
7673
7810
  },
7674
7811
  {
@@ -7760,7 +7897,7 @@ async function runTurn(params) {
7760
7897
  const acc = toolInputAccumulators.get(event.id);
7761
7898
  const wasStreamed = acc?.started ?? false;
7762
7899
  const isInputStreaming = !!tool?.streaming?.partialInput;
7763
- log13.info("Tool received", {
7900
+ log14.info("Tool received", {
7764
7901
  requestId,
7765
7902
  toolCallId: event.id,
7766
7903
  name: event.name
@@ -7883,7 +8020,7 @@ async function runTurn(params) {
7883
8020
  });
7884
8021
  return;
7885
8022
  }
7886
- log13.info("Tools executing", {
8023
+ log14.info("Tools executing", {
7887
8024
  requestId,
7888
8025
  count: toolCalls.length,
7889
8026
  tools: toolCalls.map((tc) => tc.name)
@@ -7930,7 +8067,7 @@ async function runTurn(params) {
7930
8067
  let result;
7931
8068
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
7932
8069
  saveSession(state);
7933
- log13.info("Waiting for external tool result", {
8070
+ log14.info("Waiting for external tool result", {
7934
8071
  requestId,
7935
8072
  toolCallId: tc.id,
7936
8073
  name: tc.name
@@ -7998,7 +8135,7 @@ async function runTurn(params) {
7998
8135
  if (!isBackgroundCall(tc)) {
7999
8136
  toolRegistry?.unregister(tc.id);
8000
8137
  }
8001
- log13.info("Tool completed", {
8138
+ log14.info("Tool completed", {
8002
8139
  requestId,
8003
8140
  toolCallId: tc.id,
8004
8141
  name: tc.name,
@@ -8061,13 +8198,13 @@ async function runTurn(params) {
8061
8198
  // src/headless/attachments.ts
8062
8199
  import { mkdirSync, existsSync } from "fs";
8063
8200
  import { writeFile } from "fs/promises";
8064
- import { basename, join, extname } from "path";
8065
- var log14 = createLogger("headless:attachments");
8201
+ import { basename as basename2, join, extname as extname2 } from "path";
8202
+ var log15 = createLogger("headless:attachments");
8066
8203
  var UPLOADS_DIR = "src/.user-uploads";
8067
8204
  function filenameFromUrl(url) {
8068
8205
  try {
8069
8206
  const pathname = new URL(url).pathname;
8070
- const name = basename(pathname);
8207
+ const name = basename2(pathname);
8071
8208
  return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
8072
8209
  } catch {
8073
8210
  return `upload-${Date.now()}`;
@@ -8078,7 +8215,7 @@ function resolveUniqueFilename(name, claimed) {
8078
8215
  if (isFree(name)) {
8079
8216
  return name;
8080
8217
  }
8081
- const ext = extname(name);
8218
+ const ext = extname2(name);
8082
8219
  const base = name.slice(0, name.length - ext.length);
8083
8220
  let counter = 1;
8084
8221
  while (!isFree(`${base}-${counter}${ext}`)) {
@@ -8089,7 +8226,7 @@ function resolveUniqueFilename(name, claimed) {
8089
8226
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
8090
8227
  function isImageAttachment(att) {
8091
8228
  const name = att.filename || filenameFromUrl(att.url);
8092
- return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
8229
+ return IMAGE_EXTENSIONS.has(extname2(name).toLowerCase());
8093
8230
  }
8094
8231
  async function persistAttachments(attachments) {
8095
8232
  const nonVoice = attachments.filter((a) => !a.isVoice);
@@ -8118,7 +8255,7 @@ async function persistAttachments(attachments) {
8118
8255
  }
8119
8256
  const buffer = Buffer.from(await res.arrayBuffer());
8120
8257
  await writeFile(localPath, buffer);
8121
- log14.info("Attachment saved", {
8258
+ log15.info("Attachment saved", {
8122
8259
  filename: name,
8123
8260
  path: localPath,
8124
8261
  bytes: buffer.length
@@ -8132,7 +8269,7 @@ async function persistAttachments(attachments) {
8132
8269
  if (textRes.ok) {
8133
8270
  extractedTextPath = `${localPath}.txt`;
8134
8271
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
8135
- log14.info("Extracted text saved", { path: extractedTextPath });
8272
+ log15.info("Extracted text saved", { path: extractedTextPath });
8136
8273
  }
8137
8274
  } catch {
8138
8275
  }
@@ -8355,7 +8492,7 @@ function getActionChain(startName) {
8355
8492
  }
8356
8493
 
8357
8494
  // src/headless/index.ts
8358
- var log15 = createLogger("headless");
8495
+ var log16 = createLogger("headless");
8359
8496
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
8360
8497
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
8361
8498
  "promptUser",
@@ -8500,7 +8637,7 @@ var HeadlessSession = class {
8500
8637
  }
8501
8638
  const line = JSON.stringify(payload) + "\n";
8502
8639
  if (event === "history") {
8503
- log15.info("Wrote history event to stdout", {
8640
+ log16.info("Wrote history event to stdout", {
8504
8641
  requestId,
8505
8642
  bytes: line.length
8506
8643
  });
@@ -8577,7 +8714,7 @@ var HeadlessSession = class {
8577
8714
  if (this.sessionStats.lastContextSize <= FORCED_COMPACTION_THRESHOLD_TOKENS) {
8578
8715
  return;
8579
8716
  }
8580
- log15.info("Forced compaction gate triggered", {
8717
+ log16.info("Forced compaction gate triggered", {
8581
8718
  contextSize: this.sessionStats.lastContextSize,
8582
8719
  threshold: FORCED_COMPACTION_THRESHOLD_TOKENS,
8583
8720
  requestId
@@ -8594,7 +8731,7 @@ var HeadlessSession = class {
8594
8731
  }
8595
8732
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
8596
8733
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
8597
- log15.info("Background complete", {
8734
+ log16.info("Background complete", {
8598
8735
  toolCallId,
8599
8736
  name,
8600
8737
  requestId: this.currentRequestId
@@ -8631,17 +8768,17 @@ var HeadlessSession = class {
8631
8768
  return Promise.resolve(early);
8632
8769
  }
8633
8770
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
8634
- return new Promise((resolve2) => {
8771
+ return new Promise((resolve3) => {
8635
8772
  const timeout = shouldTimeout ? setTimeout(() => {
8636
8773
  this.pendingTools.delete(id);
8637
- resolve2(
8774
+ resolve3(
8638
8775
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
8639
8776
  );
8640
8777
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
8641
8778
  this.pendingTools.set(id, {
8642
8779
  resolve: (result) => {
8643
8780
  clearTimeout(timeout);
8644
- resolve2(result);
8781
+ resolve3(result);
8645
8782
  },
8646
8783
  timeout
8647
8784
  });
@@ -8832,7 +8969,7 @@ var HeadlessSession = class {
8832
8969
  await this.runForcedCompactionIfNeeded(requestId);
8833
8970
  const attachments = parsed.attachments;
8834
8971
  if (attachments?.length) {
8835
- log15.info("Message has attachments", {
8972
+ log16.info("Message has attachments", {
8836
8973
  count: attachments.length,
8837
8974
  urls: attachments.map((a) => a.url)
8838
8975
  });
@@ -8848,7 +8985,7 @@ var HeadlessSession = class {
8848
8985
  attachmentHeader = header;
8849
8986
  }
8850
8987
  } catch (err) {
8851
- log15.warn("Attachment persistence failed", { error: err.message });
8988
+ log16.warn("Attachment persistence failed", { error: err.message });
8852
8989
  }
8853
8990
  }
8854
8991
  let resolved = null;
@@ -8912,7 +9049,7 @@ var HeadlessSession = class {
8912
9049
  error: "Turn ended unexpectedly"
8913
9050
  });
8914
9051
  }
8915
- log15.info("Turn complete", {
9052
+ log16.info("Turn complete", {
8916
9053
  requestId,
8917
9054
  durationMs: Date.now() - this.turnStart
8918
9055
  });
@@ -8924,7 +9061,7 @@ var HeadlessSession = class {
8924
9061
  error: err.message
8925
9062
  });
8926
9063
  }
8927
- log15.warn("Command failed", {
9064
+ log16.warn("Command failed", {
8928
9065
  action: "message",
8929
9066
  requestId,
8930
9067
  error: err.message
@@ -9081,7 +9218,7 @@ var HeadlessSession = class {
9081
9218
  try {
9082
9219
  parsed = JSON.parse(line);
9083
9220
  } catch (err) {
9084
- log15.warn("Invalid JSON on stdin", {
9221
+ log16.warn("Invalid JSON on stdin", {
9085
9222
  error: err.message,
9086
9223
  lineLength: line.length,
9087
9224
  preview: line.slice(0, 200)
@@ -9090,7 +9227,7 @@ var HeadlessSession = class {
9090
9227
  return;
9091
9228
  }
9092
9229
  const { action, requestId } = parsed;
9093
- log15.info("Command received", { action, requestId });
9230
+ log16.info("Command received", { action, requestId });
9094
9231
  if (action === "tool_result" && parsed.id) {
9095
9232
  const id = parsed.id;
9096
9233
  const result = parsed.result ?? "";
@@ -9099,7 +9236,7 @@ var HeadlessSession = class {
9099
9236
  this.pendingTools.delete(id);
9100
9237
  pending2.resolve(result);
9101
9238
  } else if (!this.running) {
9102
- log15.info("Late tool_result while idle, dismissing", { id });
9239
+ log16.info("Late tool_result while idle, dismissing", { id });
9103
9240
  this.emit("completed", { success: true }, requestId);
9104
9241
  } else {
9105
9242
  this.earlyResults.set(id, result);
@@ -9112,7 +9249,7 @@ var HeadlessSession = class {
9112
9249
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
9113
9250
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
9114
9251
  });
9115
- log15.info("History response", {
9252
+ log16.info("History response", {
9116
9253
  requestId,
9117
9254
  startIndex: page.startIndex,
9118
9255
  endIndex: page.endIndex,