@mindstudio-ai/remy 0.1.256 → 0.1.258

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: {
@@ -2804,8 +2893,8 @@ async function lspRequest(endpoint, body) {
2804
2893
 
2805
2894
  // src/tools/code/lspDiagnostics.ts
2806
2895
  var lspDiagnosticsTool = {
2807
- clearable: true,
2808
2896
  definition: {
2897
+ clearable: true,
2809
2898
  name: "lspDiagnostics",
2810
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.",
2811
2900
  inputSchema: {
@@ -2853,8 +2942,8 @@ var lspDiagnosticsTool = {
2853
2942
 
2854
2943
  // src/tools/code/restartProcess.ts
2855
2944
  var restartProcessTool = {
2856
- clearable: false,
2857
2945
  definition: {
2946
+ clearable: false,
2858
2947
  name: "restartProcess",
2859
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.",
2860
2949
  inputSchema: {
@@ -2871,7 +2960,7 @@ var restartProcessTool = {
2871
2960
  async execute(input) {
2872
2961
  const data = await lspRequest("/restart-process", { name: input.name });
2873
2962
  if (data.ok) {
2874
- await new Promise((resolve2) => setTimeout(resolve2, 5e3));
2963
+ await new Promise((resolve3) => setTimeout(resolve3, 5e3));
2875
2964
  return `Restarted ${input.name}.`;
2876
2965
  }
2877
2966
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -2880,8 +2969,8 @@ var restartProcessTool = {
2880
2969
 
2881
2970
  // src/tools/code/runScenario.ts
2882
2971
  var runScenarioTool = {
2883
- clearable: true,
2884
2972
  definition: {
2973
+ clearable: true,
2885
2974
  name: "runScenario",
2886
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.",
2887
2976
  inputSchema: {
@@ -2906,8 +2995,8 @@ var runScenarioTool = {
2906
2995
 
2907
2996
  // src/tools/code/runMethod.ts
2908
2997
  var runMethodTool = {
2909
- clearable: true,
2910
2998
  definition: {
2999
+ clearable: true,
2911
3000
  name: "runMethod",
2912
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.',
2913
3002
  inputSchema: {
@@ -2941,8 +3030,8 @@ var runMethodTool = {
2941
3030
 
2942
3031
  // src/tools/code/queryDatabase.ts
2943
3032
  var queryDatabaseTool = {
2944
- clearable: true,
2945
3033
  definition: {
3034
+ clearable: true,
2946
3035
  name: "queryDatabase",
2947
3036
  description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
2948
3037
  inputSchema: {
@@ -2961,21 +3050,122 @@ var queryDatabaseTool = {
2961
3050
  }
2962
3051
  };
2963
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
+
2964
3149
  // src/subagents/common/analyzeImage.ts
2965
3150
  async function analyzeImage(params) {
2966
- const { prompt, imageUrl, model, timeout = 2e5, onLog } = params;
2967
- return runMindstudioCli(
3151
+ const { prompt, image, apiConfig, model, timeout = 2e5, onLog } = params;
3152
+ const url = await resolveImageRef(image, apiConfig);
3153
+ const result = await runMindstudioCliResult(
2968
3154
  [
2969
3155
  "analyze-image",
2970
3156
  "--prompt",
2971
3157
  prompt,
2972
3158
  "--image-url",
2973
- imageUrl,
3159
+ url,
2974
3160
  "--vision-model-override",
2975
3161
  JSON.stringify({ model })
2976
3162
  ],
2977
3163
  { outputKey: "analysis", timeout, onLog }
2978
3164
  );
3165
+ if (!result.ok) {
3166
+ throw new Error(`Could not analyze ${url}: ${result.value}`);
3167
+ }
3168
+ return { url, analysis: result.value };
2979
3169
  }
2980
3170
 
2981
3171
  // src/tools/_helpers/screenshot.ts
@@ -3000,13 +3190,14 @@ ${ANALYSIS_RESPONSE_FORMAT}`;
3000
3190
  return p;
3001
3191
  }
3002
3192
  async function streamScreenshotAnalysis(opts) {
3003
- const { url, prompt, styleMap, onLog, model } = opts;
3193
+ const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
3194
+ const url = await resolveImageRef(image, apiConfig);
3004
3195
  onLog?.(JSON.stringify({ url, analysis: null }));
3005
3196
  const analysisPrompt = buildScreenshotAnalysisPrompt({ prompt, styleMap });
3006
3197
  let accumulated = "";
3007
- const analysis = await analyzeImage({
3198
+ const { analysis } = await analyzeImage({
3008
3199
  prompt: analysisPrompt,
3009
- imageUrl: url,
3200
+ image: url,
3010
3201
  model,
3011
3202
  onLog: (chunk) => {
3012
3203
  accumulated += chunk;
@@ -3017,18 +3208,19 @@ async function streamScreenshotAnalysis(opts) {
3017
3208
  }
3018
3209
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
3019
3210
  let prompt;
3020
- let existingUrl;
3211
+ let existingImage;
3021
3212
  let onLog;
3022
3213
  let model;
3023
- let path13;
3214
+ let apiConfig;
3215
+ let path14;
3024
3216
  let fullPage = true;
3025
3217
  let width;
3026
3218
  let height;
3027
3219
  let format;
3028
3220
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
3029
3221
  prompt = promptOrOptions.prompt;
3030
- existingUrl = promptOrOptions.imageUrl;
3031
- path13 = promptOrOptions.path;
3222
+ existingImage = promptOrOptions.image;
3223
+ path14 = promptOrOptions.path;
3032
3224
  if (promptOrOptions.fullPage !== void 0) {
3033
3225
  fullPage = promptOrOptions.fullPage;
3034
3226
  }
@@ -3037,6 +3229,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3037
3229
  format = promptOrOptions.format;
3038
3230
  onLog = promptOrOptions.onLog;
3039
3231
  model = promptOrOptions.model;
3232
+ apiConfig = promptOrOptions.apiConfig;
3040
3233
  } else {
3041
3234
  prompt = promptOrOptions;
3042
3235
  }
@@ -3045,13 +3238,13 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3045
3238
  }
3046
3239
  let url;
3047
3240
  let styleMap;
3048
- if (existingUrl) {
3049
- url = existingUrl;
3241
+ if (existingImage) {
3242
+ url = existingImage;
3050
3243
  } else {
3051
3244
  const ssResult = await sidecarRequest(
3052
3245
  fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
3053
3246
  {
3054
- ...path13 ? { path: path13 } : {},
3247
+ ...path14 ? { path: path14 } : {},
3055
3248
  ...width != null ? { width } : {},
3056
3249
  ...height != null ? { height } : {},
3057
3250
  ...format ? { format } : {}
@@ -3069,7 +3262,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3069
3262
  styleMap = ssResult?.styleMap;
3070
3263
  }
3071
3264
  if (prompt === false) {
3072
- return url;
3265
+ return resolveImageRef(url, apiConfig);
3073
3266
  }
3074
3267
  if (!model) {
3075
3268
  throw new Error(
@@ -3077,7 +3270,8 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3077
3270
  );
3078
3271
  }
3079
3272
  return streamScreenshotAnalysis({
3080
- url,
3273
+ image: url,
3274
+ apiConfig,
3081
3275
  prompt: prompt || void 0,
3082
3276
  styleMap,
3083
3277
  onLog,
@@ -3098,7 +3292,7 @@ function acquireBrowserLock() {
3098
3292
  }
3099
3293
 
3100
3294
  // src/toolRegistry.ts
3101
- var log5 = createLogger("tool-registry");
3295
+ var log6 = createLogger("tool-registry");
3102
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.";
3103
3297
  var ToolRegistry = class {
3104
3298
  entries = /* @__PURE__ */ new Map();
@@ -3125,7 +3319,7 @@ var ToolRegistry = class {
3125
3319
  if (!entry) {
3126
3320
  return false;
3127
3321
  }
3128
- log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
3322
+ log6.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
3129
3323
  entry.abortController.abort(mode);
3130
3324
  if (mode === "graceful") {
3131
3325
  const partial = entry.getPartialResult?.() ?? "";
@@ -3158,7 +3352,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3158
3352
  if (!entry) {
3159
3353
  return false;
3160
3354
  }
3161
- log5.info("Tool restarted", { toolCallId: id, name: entry.name });
3355
+ log6.info("Tool restarted", { toolCallId: id, name: entry.name });
3162
3356
  entry.abortController.abort("restart");
3163
3357
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
3164
3358
  this.onEvent?.({
@@ -3397,7 +3591,7 @@ ${content}` : attachmentHeader;
3397
3591
  }
3398
3592
 
3399
3593
  // src/subagents/runner.ts
3400
- var log6 = createLogger("sub-agent");
3594
+ var log7 = createLogger("sub-agent");
3401
3595
  async function runSubAgent(config) {
3402
3596
  const {
3403
3597
  system,
@@ -3425,7 +3619,7 @@ async function runSubAgent(config) {
3425
3619
  const signal = background ? bgAbort.signal : parentSignal;
3426
3620
  const agentName = subAgentId || "sub-agent";
3427
3621
  const runStart = Date.now();
3428
- log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3622
+ log7.info("Sub-agent started", { requestId, parentToolId, agentName });
3429
3623
  const emit = (e) => {
3430
3624
  onEvent({ ...e, parentToolId });
3431
3625
  };
@@ -3437,7 +3631,6 @@ async function runSubAgent(config) {
3437
3631
  const fullSystem = `${system}
3438
3632
 
3439
3633
  Current date: ${dateStr}`;
3440
- const excludeToolsFromClearing = tools2.filter((t) => t.clearable === false).map((t) => t.name);
3441
3634
  let turns = 0;
3442
3635
  const run = async () => {
3443
3636
  const historyLen = (history ?? []).length;
@@ -3509,7 +3702,6 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3509
3702
  system: fullSystem,
3510
3703
  messages: cleanMessagesForApi(messages),
3511
3704
  tools: tools2,
3512
- excludeToolsFromClearing,
3513
3705
  signal
3514
3706
  },
3515
3707
  {
@@ -3645,7 +3837,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3645
3837
  ...hasArtifacts ? { artifacts } : {}
3646
3838
  };
3647
3839
  }
3648
- log6.info("Tools executing", {
3840
+ log7.info("Tools executing", {
3649
3841
  requestId,
3650
3842
  parentToolId,
3651
3843
  count: toolCalls.length,
@@ -3722,7 +3914,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3722
3914
  run2(tc.input);
3723
3915
  const r = await resultPromise;
3724
3916
  toolRegistry?.unregister(tc.id);
3725
- log6.info("Tool completed", {
3917
+ log7.info("Tool completed", {
3726
3918
  requestId,
3727
3919
  parentToolId,
3728
3920
  toolCallId: tc.id,
@@ -3773,7 +3965,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3773
3965
  const wrapRun = async () => {
3774
3966
  try {
3775
3967
  const result = await run();
3776
- log6.info("Sub-agent complete", {
3968
+ log7.info("Sub-agent complete", {
3777
3969
  requestId,
3778
3970
  parentToolId,
3779
3971
  agentName,
@@ -3782,7 +3974,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3782
3974
  });
3783
3975
  return result;
3784
3976
  } catch (err) {
3785
- log6.warn("Sub-agent error", {
3977
+ log7.warn("Sub-agent error", {
3786
3978
  requestId,
3787
3979
  parentToolId,
3788
3980
  agentName,
@@ -3794,7 +3986,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3794
3986
  if (!background) {
3795
3987
  return wrapRun();
3796
3988
  }
3797
- log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3989
+ log7.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3798
3990
  toolRegistry?.register({
3799
3991
  id: parentToolId,
3800
3992
  name: agentName,
@@ -3985,13 +4177,13 @@ var BROWSER_TOOLS = [
3985
4177
  var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
3986
4178
 
3987
4179
  // src/subagents/common/context.ts
3988
- import fs16 from "fs";
3989
- import path9 from "path";
4180
+ import fs17 from "fs";
4181
+ import path10 from "path";
3990
4182
  function walkMdFiles2(dir, skip) {
3991
4183
  const files = [];
3992
4184
  try {
3993
- for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
3994
- 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);
3995
4187
  if (entry.isDirectory()) {
3996
4188
  if (!skip?.has(entry.name)) {
3997
4189
  files.push(...walkMdFiles2(full, skip));
@@ -4004,9 +4196,9 @@ function walkMdFiles2(dir, skip) {
4004
4196
  }
4005
4197
  return files.sort();
4006
4198
  }
4007
- function parseFrontmatter2(filePath) {
4199
+ function parseFrontmatter3(filePath) {
4008
4200
  try {
4009
- const content = fs16.readFileSync(filePath, "utf-8");
4201
+ const content = fs17.readFileSync(filePath, "utf-8");
4010
4202
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4011
4203
  if (!match) {
4012
4204
  return {};
@@ -4031,7 +4223,7 @@ function loadSpecIndex() {
4031
4223
  return "";
4032
4224
  }
4033
4225
  const lines = files.map((f) => {
4034
- const fm = parseFrontmatter2(f);
4226
+ const fm = parseFrontmatter3(f);
4035
4227
  let line = `- ${f}`;
4036
4228
  if (fm.name) {
4037
4229
  line += ` \u2014 "${fm.name}"`;
@@ -4052,7 +4244,7 @@ function loadRoadmapIndex() {
4052
4244
  const parts = [];
4053
4245
  try {
4054
4246
  const indexJson = JSON.parse(
4055
- fs16.readFileSync("src/roadmap/index.json", "utf-8")
4247
+ fs17.readFileSync("src/roadmap/index.json", "utf-8")
4056
4248
  );
4057
4249
  if (indexJson.lanes?.length > 0) {
4058
4250
  const laneLines = indexJson.lanes.map(
@@ -4072,7 +4264,7 @@ ${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
4072
4264
  const files = walkMdFiles2("src/roadmap");
4073
4265
  if (files.length > 0) {
4074
4266
  const lines = files.map((f) => {
4075
- const fm = parseFrontmatter2(f);
4267
+ const fm = parseFrontmatter3(f);
4076
4268
  let line = `- ${f}`;
4077
4269
  if (fm.name) {
4078
4270
  line += ` \u2014 "${fm.name}"`;
@@ -4168,7 +4360,7 @@ function getBrowserAutomationPrompt() {
4168
4360
  }
4169
4361
 
4170
4362
  // src/subagents/browserAutomation/index.ts
4171
- var log7 = createLogger("browser-automation");
4363
+ var log8 = createLogger("browser-automation");
4172
4364
  var CAPTURE_COMMANDS = /* @__PURE__ */ new Set(["screenshotViewport", "screenshotFullPage"]);
4173
4365
  async function runBrowserAutomation(task, context, opts) {
4174
4366
  const release = await acquireBrowserLock();
@@ -4264,7 +4456,7 @@ async function runBrowserAutomation(task, context, opts) {
4264
4456
  step.result.analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
4265
4457
  });
4266
4458
  } catch {
4267
- log7.debug("Failed to parse batch analysis result", {
4459
+ log8.debug("Failed to parse batch analysis result", {
4268
4460
  batchResult
4269
4461
  });
4270
4462
  }
@@ -4288,8 +4480,8 @@ async function runBrowserAutomation(task, context, opts) {
4288
4480
  }
4289
4481
  }
4290
4482
  var browserAutomationTool = {
4291
- clearable: true,
4292
4483
  definition: {
4484
+ clearable: true,
4293
4485
  name: "runAutomatedBrowserTest",
4294
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).",
4295
4487
  inputSchema: {
@@ -4321,7 +4513,7 @@ var browserAutomationTool = {
4321
4513
  var screenshotDefinition = {
4322
4514
  clearable: true,
4323
4515
  name: "screenshot",
4324
- 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. 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.",
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.",
4325
4517
  inputSchema: {
4326
4518
  type: "object",
4327
4519
  properties: {
@@ -4335,7 +4527,7 @@ var screenshotDefinition = {
4335
4527
  },
4336
4528
  imageUrl: {
4337
4529
  type: "string",
4338
- description: "URL of an existing screenshot to analyze instead of capturing a new one. Use this for additional questions about a previous screenshot."
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."
4339
4531
  },
4340
4532
  path: {
4341
4533
  type: "string",
@@ -4369,9 +4561,10 @@ async function executeScreenshot(input, onLog, context) {
4369
4561
  if (input.imageUrl) {
4370
4562
  return await captureAndAnalyzeScreenshot({
4371
4563
  prompt: input.prompt,
4372
- imageUrl: input.imageUrl,
4564
+ image: input.imageUrl,
4373
4565
  onLog,
4374
- model
4566
+ model,
4567
+ apiConfig: context?.apiConfig
4375
4568
  });
4376
4569
  }
4377
4570
  if (input.instructions && context) {
@@ -4384,11 +4577,12 @@ async function executeScreenshot(input, onLog, context) {
4384
4577
  return result.text;
4385
4578
  }
4386
4579
  return await streamScreenshotAnalysis({
4387
- url: result.screenshot.url,
4580
+ image: result.screenshot.url,
4388
4581
  prompt: input.prompt,
4389
4582
  styleMap: result.screenshot.styleMap,
4390
4583
  onLog,
4391
- model
4584
+ model,
4585
+ apiConfig: context?.apiConfig
4392
4586
  });
4393
4587
  }
4394
4588
  const release = await acquireBrowserLock();
@@ -4401,7 +4595,8 @@ async function executeScreenshot(input, onLog, context) {
4401
4595
  height: input.height,
4402
4596
  format: input.format,
4403
4597
  onLog,
4404
- model
4598
+ model,
4599
+ apiConfig: context?.apiConfig
4405
4600
  });
4406
4601
  } finally {
4407
4602
  release();
@@ -4411,7 +4606,6 @@ async function executeScreenshot(input, onLog, context) {
4411
4606
  }
4412
4607
  }
4413
4608
  var screenshotTool = {
4414
- clearable: true,
4415
4609
  definition: screenshotDefinition,
4416
4610
  execute: (input, context) => executeScreenshot(input, context?.onLog, context)
4417
4611
  };
@@ -4516,13 +4710,13 @@ Respond only with your analysis as Markdown and absolutely no other text. Do not
4516
4710
  var definition3 = {
4517
4711
  clearable: false,
4518
4712
  name: "analyzeDesign",
4519
- 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.",
4520
4714
  inputSchema: {
4521
4715
  type: "object",
4522
4716
  properties: {
4523
4717
  url: {
4524
4718
  type: "string",
4525
- 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."
4526
4720
  },
4527
4721
  prompt: {
4528
4722
  type: "string",
@@ -4535,9 +4729,9 @@ var definition3 = {
4535
4729
  async function execute3(input, onLog, context) {
4536
4730
  const url = input.url;
4537
4731
  const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
4538
- const isImageUrl = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
4539
- let imageUrl = url;
4540
- if (!isImageUrl) {
4732
+ const isImage = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
4733
+ let image = url;
4734
+ if (!isImage) {
4541
4735
  const ss = await runMindstudioCliResult(
4542
4736
  [
4543
4737
  "screenshot-url",
@@ -4560,15 +4754,16 @@ async function execute3(input, onLog, context) {
4560
4754
  if (!ss.ok) {
4561
4755
  return `Could not screenshot ${url}: ${ss.value}`;
4562
4756
  }
4563
- imageUrl = ss.value;
4757
+ image = ss.value;
4564
4758
  }
4565
- const analysis = await analyzeImage({
4759
+ const analyzed = await analyzeImage({
4566
4760
  prompt: analysisPrompt,
4567
- imageUrl,
4761
+ image,
4762
+ apiConfig: context?.apiConfig,
4568
4763
  onLog,
4569
4764
  model: resolveModel("imageAnalysis", context?.models, context?.model)
4570
4765
  });
4571
- return JSON.stringify({ url: imageUrl, analysis });
4766
+ return JSON.stringify({ url: analyzed.url, analysis: analyzed.analysis });
4572
4767
  }
4573
4768
 
4574
4769
  // src/subagents/designExpert/tools/analyzeImage.ts
@@ -4580,13 +4775,16 @@ __export(analyzeImage_exports, {
4580
4775
  var definition4 = {
4581
4776
  clearable: true,
4582
4777
  name: "analyzeImage",
4583
- 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.",
4584
4779
  inputSchema: {
4585
4780
  type: "object",
4586
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.
4587
4785
  imageUrl: {
4588
4786
  type: "string",
4589
- 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."
4590
4788
  },
4591
4789
  prompt: {
4592
4790
  type: "string",
@@ -4597,17 +4795,17 @@ var definition4 = {
4597
4795
  }
4598
4796
  };
4599
4797
  async function execute4(input, onLog, context) {
4600
- const imageUrl = input.imageUrl;
4601
4798
  const prompt = buildScreenshotAnalysisPrompt({
4602
4799
  prompt: input.prompt
4603
4800
  });
4604
- const analysis = await analyzeImage({
4801
+ const { url, analysis } = await analyzeImage({
4605
4802
  prompt,
4606
- imageUrl,
4803
+ image: input.imageUrl,
4804
+ apiConfig: context?.apiConfig,
4607
4805
  onLog,
4608
4806
  model: resolveModel("imageAnalysis", context?.models, context?.model)
4609
4807
  });
4610
- return JSON.stringify({ url: imageUrl, analysis });
4808
+ return JSON.stringify({ url, analysis });
4611
4809
  }
4612
4810
 
4613
4811
  // src/subagents/designExpert/tools/images/generateImages.ts
@@ -4676,14 +4874,15 @@ var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcin
4676
4874
  async function generateImageAssets(opts) {
4677
4875
  const {
4678
4876
  prompts,
4679
- sourceImages,
4680
4877
  transparentBackground,
4681
4878
  enhancePrompts,
4682
4879
  onLog,
4880
+ apiConfig,
4683
4881
  imageGenerationModel: genModel,
4684
4882
  imageAnalysisModel,
4685
4883
  imagePromptEnhancerModel
4686
4884
  } = opts;
4885
+ const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
4687
4886
  const width = opts.width || 2048;
4688
4887
  const height = opts.height || 2048;
4689
4888
  const config = { width, height };
@@ -4789,10 +4988,10 @@ async function generateImageAssets(opts) {
4789
4988
  }
4790
4989
  const analysis = await analyzeImage({
4791
4990
  prompt: ANALYZE_PROMPT,
4792
- imageUrl: url,
4991
+ image: url,
4793
4992
  onLog,
4794
4993
  model: imageAnalysisModel
4795
- });
4994
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
4796
4995
  return {
4797
4996
  url,
4798
4997
  prompt: prompts[i],
@@ -4823,7 +5022,7 @@ var definition5 = {
4823
5022
  },
4824
5023
  referenceImage: {
4825
5024
  type: "string",
4826
- 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."
4827
5026
  },
4828
5027
  width: {
4829
5028
  type: "number",
@@ -4850,6 +5049,7 @@ async function execute5(input, onLog, context) {
4850
5049
  sourceImages: input.referenceImage ? [input.referenceImage] : void 0,
4851
5050
  enhancePrompts: true,
4852
5051
  onLog,
5052
+ apiConfig: context?.apiConfig,
4853
5053
  imageGenerationModel: resolveModel(
4854
5054
  "imageGeneration",
4855
5055
  context?.models,
@@ -4893,7 +5093,7 @@ var definition6 = {
4893
5093
  items: {
4894
5094
  type: "string"
4895
5095
  },
4896
- 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."
4897
5097
  },
4898
5098
  width: {
4899
5099
  type: "number",
@@ -4920,6 +5120,7 @@ async function execute6(input, onLog, context) {
4920
5120
  transparentBackground: input.transparentBackground,
4921
5121
  enhancePrompts: false,
4922
5122
  onLog,
5123
+ apiConfig: context?.apiConfig,
4923
5124
  imageGenerationModel: resolveModel(
4924
5125
  "imageGeneration",
4925
5126
  context?.models,
@@ -4951,8 +5152,8 @@ var COPY_EDITOR_TOOLS = [...COMMON_READ_TOOLS];
4951
5152
  // src/subagents/copyEditor/index.ts
4952
5153
  var BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
4953
5154
  var copyEditorTool = {
4954
- clearable: false,
4955
5155
  definition: {
5156
+ clearable: false,
4956
5157
  name: "copyEditor",
4957
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.",
4958
5159
  inputSchema: {
@@ -5046,7 +5247,7 @@ async function executeDesignExpertTool(name, input, context, toolCallId, onLog)
5046
5247
  }
5047
5248
 
5048
5249
  // src/subagents/designExpert/data/sampleCache.ts
5049
- import fs17 from "fs";
5250
+ import fs18 from "fs";
5050
5251
  var SAMPLE_FILE = ".remy-design-sample.json";
5051
5252
  var cached2 = null;
5052
5253
  function generateIndices(poolSize, sampleSize) {
@@ -5060,14 +5261,14 @@ function generateIndices(poolSize, sampleSize) {
5060
5261
  }
5061
5262
  function load() {
5062
5263
  try {
5063
- return JSON.parse(fs17.readFileSync(SAMPLE_FILE, "utf-8"));
5264
+ return JSON.parse(fs18.readFileSync(SAMPLE_FILE, "utf-8"));
5064
5265
  } catch {
5065
5266
  return null;
5066
5267
  }
5067
5268
  }
5068
5269
  function save(indices) {
5069
5270
  try {
5070
- fs17.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
5271
+ fs18.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
5071
5272
  } catch {
5072
5273
  }
5073
5274
  }
@@ -5351,8 +5552,8 @@ async function runDesignExpert(opts, context) {
5351
5552
  });
5352
5553
  }
5353
5554
  var designExpertTool = {
5354
- clearable: false,
5355
5555
  definition: {
5556
+ clearable: false,
5356
5557
  name: "visualDesignExpert",
5357
5558
  description: DESCRIPTION,
5358
5559
  inputSchema: {
@@ -5441,28 +5642,28 @@ var VISION_TOOLS = [
5441
5642
  ];
5442
5643
 
5443
5644
  // src/subagents/productVision/executor.ts
5444
- import fs18 from "fs";
5445
- import path10 from "path";
5645
+ import fs19 from "fs";
5646
+ import path11 from "path";
5446
5647
  var ROADMAP_DIR = "src/roadmap";
5447
5648
  var PITCH_DECK_SHELL = readAsset(
5448
5649
  "subagents/productVision",
5449
5650
  "pitch-deck-shell.html"
5450
5651
  );
5451
- function resolve(filePath) {
5452
- return path10.join(ROADMAP_DIR, filePath);
5652
+ function resolve2(filePath) {
5653
+ return path11.join(ROADMAP_DIR, filePath);
5453
5654
  }
5454
5655
  async function executeVisionTool(name, input, context) {
5455
5656
  switch (name) {
5456
5657
  case "writeFile": {
5457
- const filePath = resolve(input.path);
5658
+ const filePath = resolve2(input.path);
5458
5659
  try {
5459
- fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
5660
+ fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5460
5661
  let oldContent = null;
5461
5662
  try {
5462
- oldContent = fs18.readFileSync(filePath, "utf-8");
5663
+ oldContent = fs19.readFileSync(filePath, "utf-8");
5463
5664
  } catch {
5464
5665
  }
5465
- fs18.writeFileSync(filePath, input.content, "utf-8");
5666
+ fs19.writeFileSync(filePath, input.content, "utf-8");
5466
5667
  const lineCount = input.content.split("\n").length;
5467
5668
  const label = oldContent !== null ? "Wrote" : "Created";
5468
5669
  return `${label} ${filePath} (${lineCount} lines)
@@ -5472,13 +5673,13 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
5472
5673
  }
5473
5674
  }
5474
5675
  case "deleteFile": {
5475
- const filePath = resolve(input.path);
5676
+ const filePath = resolve2(input.path);
5476
5677
  try {
5477
- if (!fs18.existsSync(filePath)) {
5678
+ if (!fs19.existsSync(filePath)) {
5478
5679
  return `Error: ${filePath} does not exist`;
5479
5680
  }
5480
- const oldContent = fs18.readFileSync(filePath, "utf-8");
5481
- fs18.unlinkSync(filePath);
5681
+ const oldContent = fs19.readFileSync(filePath, "utf-8");
5682
+ fs19.unlinkSync(filePath);
5482
5683
  return `Deleted ${filePath}
5483
5684
  ${unifiedDiff(filePath, oldContent, "")}`;
5484
5685
  } catch (err) {
@@ -5489,11 +5690,11 @@ ${unifiedDiff(filePath, oldContent, "")}`;
5489
5690
  if (!context) {
5490
5691
  return "Error: writePitchDeck requires execution context for design expert delegation";
5491
5692
  }
5492
- const filePath = resolve("pitch.html");
5693
+ const filePath = resolve2("pitch.html");
5493
5694
  try {
5494
- fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
5495
- const exists = fs18.existsSync(filePath);
5496
- 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;
5497
5698
  const delivery = exists ? `### Your deliverable
5498
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.
5499
5700
 
@@ -5523,11 +5724,11 @@ Maintain the bones of the presentation scaffolding. Always keep the progress bar
5523
5724
  ${delivery}`;
5524
5725
  const result = await runDesignExpertRender({ task }, context);
5525
5726
  context.subAgentMessages?.set(context.toolCallId, result.messages);
5526
- if (!fs18.existsSync(filePath)) {
5727
+ if (!fs19.existsSync(filePath)) {
5527
5728
  return `Error: the design expert did not write ${filePath}. Its reply was:
5528
5729
  ${result.text}`;
5529
5730
  }
5530
- if (before !== null && fs18.statSync(filePath).mtimeMs === before) {
5731
+ if (before !== null && fs19.statSync(filePath).mtimeMs === before) {
5531
5732
  return `Error: the pitch deck at ${filePath} was not modified. The design expert's reply was:
5532
5733
  ${result.text}`;
5533
5734
  }
@@ -5559,8 +5760,8 @@ function getProductVisionPrompt() {
5559
5760
 
5560
5761
  // src/subagents/productVision/index.ts
5561
5762
  var productVisionTool = {
5562
- clearable: false,
5563
5763
  definition: {
5764
+ clearable: false,
5564
5765
  name: "productVision",
5565
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.",
5566
5767
  inputSchema: {
@@ -5672,8 +5873,8 @@ var SANITY_CHECK_TOOLS = [
5672
5873
  // src/subagents/codeSanityCheck/index.ts
5673
5874
  var BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
5674
5875
  var codeSanityCheckTool = {
5675
- clearable: false,
5676
5876
  definition: {
5877
+ clearable: false,
5677
5878
  name: "codeSanityCheck",
5678
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.',
5679
5880
  inputSchema: {
@@ -5748,9 +5949,9 @@ ${readAsset(
5748
5949
  )}
5749
5950
  </mindstudio_flavored_markdown_spec_docs>`;
5750
5951
  var specSyncTool = {
5751
- clearable: false,
5752
5952
  backgroundOnly: true,
5753
5953
  definition: {
5954
+ clearable: false,
5754
5955
  name: "specSync",
5755
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.",
5756
5957
  inputSchema: {
@@ -5817,8 +6018,8 @@ var specSyncTool = {
5817
6018
 
5818
6019
  // src/tools/common/scrapeWebUrl.ts
5819
6020
  var scrapeWebUrlTool = {
5820
- clearable: false,
5821
6021
  definition: {
6022
+ clearable: false,
5822
6023
  name: "scrapeWebUrl",
5823
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",
5824
6025
  inputSchema: {
@@ -5861,7 +6062,7 @@ var scrapeWebUrlTool = {
5861
6062
  };
5862
6063
 
5863
6064
  // src/tools/spec/writeBuildOverview.ts
5864
- import fs19 from "fs";
6065
+ import fs20 from "fs";
5865
6066
  var OVERVIEW_FILE = "src/overview.html";
5866
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).
5867
6068
 
@@ -5918,8 +6119,8 @@ The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update
5918
6119
  Then reply with a one-line summary of what you changed.`;
5919
6120
  }
5920
6121
  var buildOverviewTool = {
5921
- clearable: false,
5922
6122
  definition: {
6123
+ clearable: false,
5923
6124
  name: "writeBuildOverview",
5924
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.",
5925
6126
  inputSchema: {
@@ -5941,7 +6142,7 @@ var buildOverviewTool = {
5941
6142
  if (!content) {
5942
6143
  return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
5943
6144
  }
5944
- const exists = fs19.existsSync(OVERVIEW_FILE);
6145
+ const exists = fs20.existsSync(OVERVIEW_FILE);
5945
6146
  const task = `<overview_copy>${content}</overview_copy>
5946
6147
 
5947
6148
  ${DESIGN_BRIEF}
@@ -5959,7 +6160,7 @@ ${exists ? refreshDelivery() : initialDelivery()}`;
5959
6160
  }
5960
6161
  const result = await runDesignExpertRender({ task }, context);
5961
6162
  context.subAgentMessages?.set(context.toolCallId, result.messages);
5962
- if (!fs19.existsSync(OVERVIEW_FILE)) {
6163
+ if (!fs20.existsSync(OVERVIEW_FILE)) {
5963
6164
  return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
5964
6165
  ${result.text}`;
5965
6166
  }
@@ -6016,11 +6217,11 @@ var ALL_TOOLS = [
6016
6217
  browserAutomationTool,
6017
6218
  // LSP
6018
6219
  lspDiagnosticsTool,
6019
- 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
6020
6224
  ];
6021
- var CLEARABLE_TOOLS = new Set(
6022
- ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
6023
- );
6024
6225
  var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
6025
6226
  "visualDesignExpert",
6026
6227
  "productVision",
@@ -6045,7 +6246,7 @@ function executeTool(name, input, context) {
6045
6246
  }
6046
6247
 
6047
6248
  // src/compaction/index.ts
6048
- var log8 = createLogger("compaction");
6249
+ var log9 = createLogger("compaction");
6049
6250
  var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
6050
6251
  var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
6051
6252
  var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
@@ -6094,7 +6295,7 @@ async function compactConversation(messages, apiConfig, model) {
6094
6295
  if (text) {
6095
6296
  summaries.push({ name, text });
6096
6297
  } else {
6097
- log8.warn("Subagent summary unusable \u2014 leaving its history intact", {
6298
+ log9.warn("Subagent summary unusable \u2014 leaving its history intact", {
6098
6299
  name
6099
6300
  });
6100
6301
  }
@@ -6122,7 +6323,7 @@ async function compactConversation(messages, apiConfig, model) {
6122
6323
  }
6123
6324
  ]
6124
6325
  }));
6125
- log8.info("Compaction complete", {
6326
+ log9.info("Compaction complete", {
6126
6327
  summaries: summaries.length,
6127
6328
  recentNarrativeChars: recent.length
6128
6329
  });
@@ -6324,7 +6525,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6324
6525
  messagesToSummarize.slice(0, mid),
6325
6526
  messagesToSummarize.slice(mid)
6326
6527
  ];
6327
- log8.info("Chunking summary", {
6528
+ log9.info("Chunking summary", {
6328
6529
  name,
6329
6530
  messageCount: messagesToSummarize.length,
6330
6531
  serializedLength: serialized.length,
@@ -6354,7 +6555,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6354
6555
  const parts = results.filter((p) => p !== null);
6355
6556
  return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
6356
6557
  }
6357
- log8.info("Generating summary", {
6558
+ log9.info("Generating summary", {
6358
6559
  name,
6359
6560
  messageCount: messagesToSummarize.length,
6360
6561
  serializedLength: serialized.length
@@ -6370,10 +6571,10 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
6370
6571
  return null;
6371
6572
  }
6372
6573
  if (summaryText.length >= MIN_SUMMARY_CHARS) {
6373
- log8.info("Summary generated", { name, summaryLength: summaryText.length });
6574
+ log9.info("Summary generated", { name, summaryLength: summaryText.length });
6374
6575
  return summaryText;
6375
6576
  }
6376
- log8.warn("Summary too short to be real", {
6577
+ log9.warn("Summary too short to be real", {
6377
6578
  name,
6378
6579
  summaryLength: summaryText.length,
6379
6580
  minimum: MIN_SUMMARY_CHARS,
@@ -6429,21 +6630,21 @@ Write the summary of the conversation above, following your instructions.`;
6429
6630
  toolNames: []
6430
6631
  });
6431
6632
  } else if (event.type === "error") {
6432
- log8.error("Summary generation failed", { name, error: event.error });
6633
+ log9.error("Summary generation failed", { name, error: event.error });
6433
6634
  return null;
6434
6635
  }
6435
6636
  }
6436
6637
  if (!summaryText.trim()) {
6437
- log8.warn("Empty summary generated", { name });
6638
+ log9.warn("Empty summary generated", { name });
6438
6639
  return null;
6439
6640
  }
6440
6641
  return summaryText.trim();
6441
6642
  }
6442
6643
 
6443
6644
  // src/session.ts
6444
- import fs20 from "fs";
6445
- import path11 from "path";
6446
- var log9 = createLogger("session");
6645
+ import fs21 from "fs";
6646
+ import path12 from "path";
6647
+ var log10 = createLogger("session");
6447
6648
  var SESSION_FILE = ".remy-session.json";
6448
6649
  var ARCHIVE_DIR = ".logs/sessions";
6449
6650
  var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
@@ -6460,14 +6661,14 @@ var ARCHIVE_MSG_CACHE_MAX = 3;
6460
6661
  function loadSession(state) {
6461
6662
  pruneArchives();
6462
6663
  try {
6463
- const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
6664
+ const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
6464
6665
  const data = JSON.parse(raw);
6465
6666
  if (data.models && typeof data.models === "object") {
6466
6667
  state.models = data.models;
6467
6668
  }
6468
6669
  if (Array.isArray(data.messages) && data.messages.length > 0) {
6469
6670
  state.messages = sanitizeMessages(data.messages);
6470
- log9.info("Session loaded", {
6671
+ log10.info("Session loaded", {
6471
6672
  messageCount: state.messages.length,
6472
6673
  ...state.models && { models: state.models }
6473
6674
  });
@@ -6533,33 +6734,33 @@ function buildPayload(state) {
6533
6734
  return payload;
6534
6735
  }
6535
6736
  function archiveMessages(messages, label, models) {
6536
- fs20.mkdirSync(ARCHIVE_DIR, { recursive: true });
6737
+ fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
6537
6738
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6538
6739
  const count = messages.length;
6539
- let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
6740
+ let dest = path12.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
6540
6741
  let n = 1;
6541
- while (fs20.existsSync(dest)) {
6542
- 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`);
6543
6744
  }
6544
6745
  const payload = { messages };
6545
6746
  if (models && Object.keys(models).length > 0) {
6546
6747
  payload.models = models;
6547
6748
  }
6548
- fs20.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6549
- archiveCountCache.set(path11.basename(dest), count);
6550
- 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 });
6551
6752
  pruneArchives();
6552
6753
  return dest;
6553
6754
  }
6554
6755
  function pruneArchives() {
6555
6756
  try {
6556
- 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));
6557
6758
  if (entries.length <= 1) {
6558
6759
  return;
6559
6760
  }
6560
6761
  const archives = entries.map((name) => ({
6561
6762
  name,
6562
- size: fs20.statSync(path11.join(ARCHIVE_DIR, name)).size
6763
+ size: fs21.statSync(path12.join(ARCHIVE_DIR, name)).size
6563
6764
  })).sort(
6564
6765
  (a, b) => archiveSortKey(b.name).localeCompare(archiveSortKey(a.name))
6565
6766
  );
@@ -6577,14 +6778,14 @@ function pruneArchives() {
6577
6778
  let freed = 0;
6578
6779
  for (let i = cut; i < archives.length; i++) {
6579
6780
  try {
6580
- fs20.unlinkSync(path11.join(ARCHIVE_DIR, archives[i].name));
6781
+ fs21.unlinkSync(path12.join(ARCHIVE_DIR, archives[i].name));
6581
6782
  freed += archives[i].size;
6582
6783
  removed++;
6583
6784
  } catch {
6584
6785
  }
6585
6786
  }
6586
6787
  if (removed > 0) {
6587
- log9.info("Session archives pruned", {
6788
+ log10.info("Session archives pruned", {
6588
6789
  removed,
6589
6790
  freedBytes: freed,
6590
6791
  keptBytes: kept
@@ -6601,7 +6802,7 @@ function parseArchive(name) {
6601
6802
  return cached3;
6602
6803
  }
6603
6804
  try {
6604
- const raw = fs20.readFileSync(path11.join(ARCHIVE_DIR, name), "utf-8");
6805
+ const raw = fs21.readFileSync(path12.join(ARCHIVE_DIR, name), "utf-8");
6605
6806
  const data = JSON.parse(raw);
6606
6807
  const messages = Array.isArray(data?.messages) ? data.messages : [];
6607
6808
  archiveCountCache.set(name, messages.length);
@@ -6615,7 +6816,7 @@ function parseArchive(name) {
6615
6816
  }
6616
6817
  return messages;
6617
6818
  } catch (err) {
6618
- log9.warn("Session archive unreadable", { name, error: err?.message });
6819
+ log10.warn("Session archive unreadable", { name, error: err?.message });
6619
6820
  return null;
6620
6821
  }
6621
6822
  }
@@ -6639,7 +6840,7 @@ function readArchiveMessages(name) {
6639
6840
  function listConversationArchives() {
6640
6841
  let names;
6641
6842
  try {
6642
- 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));
6643
6844
  } catch {
6644
6845
  return { slots: [], archivedCount: 0 };
6645
6846
  }
@@ -6738,7 +6939,7 @@ function rotate(state) {
6738
6939
  }
6739
6940
  archiveMessages(messages.slice(0, cut), "rotated", state.models);
6740
6941
  state.messages = messages.slice(cut);
6741
- log9.info("Session rotated", {
6942
+ log10.info("Session rotated", {
6742
6943
  archived: cut,
6743
6944
  retained: state.messages.length
6744
6945
  });
@@ -6750,10 +6951,10 @@ function saveSession(state) {
6750
6951
  if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6751
6952
  serialized = JSON.stringify(buildPayload(state));
6752
6953
  }
6753
- fs20.writeFileSync(SESSION_FILE, serialized, "utf-8");
6754
- 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 });
6755
6956
  } catch (err) {
6756
- log9.warn("Session save failed", { error: err.message });
6957
+ log10.warn("Session save failed", { error: err.message });
6757
6958
  }
6758
6959
  }
6759
6960
  function clearSession(state) {
@@ -6762,22 +6963,22 @@ function clearSession(state) {
6762
6963
  archiveMessages(state.messages, "cleared", state.models);
6763
6964
  }
6764
6965
  } catch (err) {
6765
- log9.warn("Session archive on clear failed", { error: err.message });
6966
+ log10.warn("Session archive on clear failed", { error: err.message });
6766
6967
  }
6767
6968
  state.messages = [];
6768
6969
  try {
6769
- if (fs20.existsSync(SESSION_FILE)) {
6770
- fs20.unlinkSync(SESSION_FILE);
6970
+ if (fs21.existsSync(SESSION_FILE)) {
6971
+ fs21.unlinkSync(SESSION_FILE);
6771
6972
  }
6772
6973
  } catch (err) {
6773
- log9.warn("Session clear: could not remove live file", {
6974
+ log10.warn("Session clear: could not remove live file", {
6774
6975
  error: err.message
6775
6976
  });
6776
6977
  }
6777
6978
  }
6778
6979
 
6779
6980
  // src/compaction/trigger.ts
6780
- var log10 = createLogger("compaction:trigger");
6981
+ var log11 = createLogger("compaction:trigger");
6781
6982
  var pending = null;
6782
6983
  var inflightCompaction = null;
6783
6984
  function applyPendingSummaries(state) {
@@ -6794,7 +6995,7 @@ function applyPendingSummaries(state) {
6794
6995
  idx = at === -1 ? 0 : at + 1;
6795
6996
  }
6796
6997
  state.messages.splice(idx, 0, ...drained.checkpoints);
6797
- log10.info("Checkpoint applied", {
6998
+ log11.info("Checkpoint applied", {
6798
6999
  index: idx,
6799
7000
  messageCount: state.messages.length
6800
7001
  });
@@ -6809,7 +7010,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6809
7010
  return inflightCompaction;
6810
7011
  }
6811
7012
  if (pending) {
6812
- 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");
6813
7014
  return Promise.resolve();
6814
7015
  }
6815
7016
  const { blocking = false, requestId, model } = opts;
@@ -6821,11 +7022,11 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6821
7022
  ).then((result) => {
6822
7023
  pending = result;
6823
7024
  listener?.({ type: "complete", requestId });
6824
- log10.info("Compaction complete");
7025
+ log11.info("Compaction complete");
6825
7026
  }).catch((err) => {
6826
7027
  const message = err.message || "Compaction failed";
6827
7028
  listener?.({ type: "complete", error: message, requestId });
6828
- log10.error("Compaction failed", { error: message });
7029
+ log11.error("Compaction failed", { error: message });
6829
7030
  throw err;
6830
7031
  }).finally(() => {
6831
7032
  inflightCompaction = null;
@@ -6834,10 +7035,10 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6834
7035
  }
6835
7036
 
6836
7037
  // src/brandExtraction/index.ts
6837
- import fs21 from "fs";
6838
- import path12 from "path";
7038
+ import fs22 from "fs";
7039
+ import path13 from "path";
6839
7040
  import { createHash } from "crypto";
6840
- var log11 = createLogger("brandExtraction");
7041
+ var log12 = createLogger("brandExtraction");
6841
7042
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6842
7043
  var BRAND_FILE = ".remy-brand.json";
6843
7044
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -6845,28 +7046,28 @@ async function runExtraction(apiConfig, model) {
6845
7046
  const inputHash = computeInputHash();
6846
7047
  const cached3 = readCache();
6847
7048
  if (cached3 && cached3.inputHash === inputHash) {
6848
- log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
7049
+ log12.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6849
7050
  return null;
6850
7051
  }
6851
- log11.info("Extracting brand", { inputHash });
7052
+ log12.info("Extracting brand", { inputHash });
6852
7053
  const brand = await extractBrand(apiConfig, model);
6853
7054
  if (!brand) {
6854
- log11.warn("Brand extraction failed \u2014 leaving cache untouched");
7055
+ log12.warn("Brand extraction failed \u2014 leaving cache untouched");
6855
7056
  return null;
6856
7057
  }
6857
7058
  persistBrand(brand, inputHash);
6858
- log11.info("Brand persisted", { inputHash });
7059
+ log12.info("Brand persisted", { inputHash });
6859
7060
  return brand;
6860
7061
  }
6861
7062
  function isDedicatedBrandFile(filePath) {
6862
- if (filePath.split(path12.sep).includes("@brand")) {
7063
+ if (filePath.split(path13.sep).includes("@brand")) {
6863
7064
  return true;
6864
7065
  }
6865
- const { type } = parseFrontmatter3(filePath);
7066
+ const { type } = parseFrontmatter4(filePath);
6866
7067
  return type.startsWith("design/color") || type.startsWith("design/typography");
6867
7068
  }
6868
7069
  function isBrandRelevant(filePath) {
6869
- return filePath === path12.join("src", "app.md") || isDedicatedBrandFile(filePath);
7070
+ return filePath === path13.join("src", "app.md") || isDedicatedBrandFile(filePath);
6870
7071
  }
6871
7072
  function computeInputHash() {
6872
7073
  const entries = [];
@@ -6888,7 +7089,7 @@ function sha256(input) {
6888
7089
  }
6889
7090
  function readSafe(filePath) {
6890
7091
  try {
6891
- return fs21.readFileSync(filePath, "utf-8");
7092
+ return fs22.readFileSync(filePath, "utf-8");
6892
7093
  } catch {
6893
7094
  return "";
6894
7095
  }
@@ -6914,9 +7115,9 @@ function readBrandManifest() {
6914
7115
  function walkMdFiles3(dir) {
6915
7116
  const results = [];
6916
7117
  try {
6917
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
7118
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
6918
7119
  for (const entry of entries) {
6919
- const full = path12.join(dir, entry.name);
7120
+ const full = path13.join(dir, entry.name);
6920
7121
  if (entry.isDirectory()) {
6921
7122
  results.push(...walkMdFiles3(full));
6922
7123
  } else if (entry.name.endsWith(".md")) {
@@ -6927,9 +7128,9 @@ function walkMdFiles3(dir) {
6927
7128
  }
6928
7129
  return results.sort();
6929
7130
  }
6930
- function parseFrontmatter3(filePath) {
7131
+ function parseFrontmatter4(filePath) {
6931
7132
  try {
6932
- const content = fs21.readFileSync(filePath, "utf-8");
7133
+ const content = fs22.readFileSync(filePath, "utf-8");
6933
7134
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6934
7135
  if (!match) {
6935
7136
  return { type: "" };
@@ -6944,7 +7145,7 @@ function parseFrontmatter3(filePath) {
6944
7145
  async function extractBrand(apiConfig, model) {
6945
7146
  const corpus = buildCorpus();
6946
7147
  if (!corpus.trim()) {
6947
- log11.debug("No spec corpus \u2014 emitting empty brand");
7148
+ log12.debug("No spec corpus \u2014 emitting empty brand");
6948
7149
  return { version: 1 };
6949
7150
  }
6950
7151
  let responseText = "";
@@ -6975,17 +7176,17 @@ async function extractBrand(apiConfig, model) {
6975
7176
  toolNames: []
6976
7177
  });
6977
7178
  } else if (event.type === "error") {
6978
- log11.error("Brand extraction stream error", { error: event.error });
7179
+ log12.error("Brand extraction stream error", { error: event.error });
6979
7180
  return null;
6980
7181
  }
6981
7182
  }
6982
7183
  } catch (err) {
6983
- log11.error("Brand extraction threw", { error: err?.message });
7184
+ log12.error("Brand extraction threw", { error: err?.message });
6984
7185
  return null;
6985
7186
  }
6986
7187
  const parsed = parseJsonResponse(responseText);
6987
7188
  if (!parsed) {
6988
- log11.warn("Brand extraction returned unparseable JSON", {
7189
+ log12.warn("Brand extraction returned unparseable JSON", {
6989
7190
  preview: responseText.slice(0, 200)
6990
7191
  });
6991
7192
  return null;
@@ -7139,14 +7340,14 @@ function pickFont(raw) {
7139
7340
  }
7140
7341
  function persistBrand(brand, inputHash) {
7141
7342
  const tmp = `${BRAND_FILE}.tmp`;
7142
- fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7143
- fs21.renameSync(tmp, BRAND_FILE);
7343
+ fs22.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7344
+ fs22.renameSync(tmp, BRAND_FILE);
7144
7345
  const cache = { inputHash, generatedAt: Date.now() };
7145
- fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7346
+ fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7146
7347
  }
7147
7348
  function readCache() {
7148
7349
  try {
7149
- const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
7350
+ const raw = fs22.readFileSync(CACHE_FILE, "utf-8");
7150
7351
  const parsed = JSON.parse(raw);
7151
7352
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
7152
7353
  return parsed;
@@ -7158,7 +7359,7 @@ function readCache() {
7158
7359
  }
7159
7360
 
7160
7361
  // src/brandExtraction/trigger.ts
7161
- var log12 = createLogger("brandExtraction:trigger");
7362
+ var log13 = createLogger("brandExtraction:trigger");
7162
7363
  var inflight = false;
7163
7364
  var dirty = false;
7164
7365
  function triggerBrandExtraction(apiConfig, model) {
@@ -7168,7 +7369,7 @@ function triggerBrandExtraction(apiConfig, model) {
7168
7369
  }
7169
7370
  inflight = true;
7170
7371
  void runExtraction(apiConfig, model).catch((err) => {
7171
- log12.error("Brand extraction failed", { error: err?.message });
7372
+ log13.error("Brand extraction failed", { error: err?.message });
7172
7373
  }).finally(() => {
7173
7374
  inflight = false;
7174
7375
  if (dirty) {
@@ -7376,7 +7577,7 @@ function friendlyError(raw) {
7376
7577
  }
7377
7578
 
7378
7579
  // src/agent.ts
7379
- var log13 = createLogger("agent");
7580
+ var log14 = createLogger("agent");
7380
7581
  var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
7381
7582
  function getTextContent(blocks) {
7382
7583
  return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
@@ -7430,12 +7631,11 @@ async function runTurn(params) {
7430
7631
  onBackgroundComplete
7431
7632
  } = params;
7432
7633
  const tools2 = getToolDefinitions(onboardingState);
7433
- const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
7434
7634
  const buildModelOverride = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
7435
7635
  const baseline = resolveModel("parent", state.models, model);
7436
7636
  const parentModel = buildModelOverride ?? baseline;
7437
7637
  const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
7438
- log13.info("Turn started", {
7638
+ log14.info("Turn started", {
7439
7639
  requestId,
7440
7640
  model,
7441
7641
  buildModel: buildModelOverride,
@@ -7606,7 +7806,6 @@ async function runTurn(params) {
7606
7806
  system,
7607
7807
  messages: cleanMessagesForApi(state.messages),
7608
7808
  tools: tools2,
7609
- excludeToolsFromClearing,
7610
7809
  signal
7611
7810
  },
7612
7811
  {
@@ -7698,7 +7897,7 @@ async function runTurn(params) {
7698
7897
  const acc = toolInputAccumulators.get(event.id);
7699
7898
  const wasStreamed = acc?.started ?? false;
7700
7899
  const isInputStreaming = !!tool?.streaming?.partialInput;
7701
- log13.info("Tool received", {
7900
+ log14.info("Tool received", {
7702
7901
  requestId,
7703
7902
  toolCallId: event.id,
7704
7903
  name: event.name
@@ -7821,7 +8020,7 @@ async function runTurn(params) {
7821
8020
  });
7822
8021
  return;
7823
8022
  }
7824
- log13.info("Tools executing", {
8023
+ log14.info("Tools executing", {
7825
8024
  requestId,
7826
8025
  count: toolCalls.length,
7827
8026
  tools: toolCalls.map((tc) => tc.name)
@@ -7868,7 +8067,7 @@ async function runTurn(params) {
7868
8067
  let result;
7869
8068
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
7870
8069
  saveSession(state);
7871
- log13.info("Waiting for external tool result", {
8070
+ log14.info("Waiting for external tool result", {
7872
8071
  requestId,
7873
8072
  toolCallId: tc.id,
7874
8073
  name: tc.name
@@ -7936,7 +8135,7 @@ async function runTurn(params) {
7936
8135
  if (!isBackgroundCall(tc)) {
7937
8136
  toolRegistry?.unregister(tc.id);
7938
8137
  }
7939
- log13.info("Tool completed", {
8138
+ log14.info("Tool completed", {
7940
8139
  requestId,
7941
8140
  toolCallId: tc.id,
7942
8141
  name: tc.name,
@@ -7999,13 +8198,13 @@ async function runTurn(params) {
7999
8198
  // src/headless/attachments.ts
8000
8199
  import { mkdirSync, existsSync } from "fs";
8001
8200
  import { writeFile } from "fs/promises";
8002
- import { basename, join, extname } from "path";
8003
- var log14 = createLogger("headless:attachments");
8201
+ import { basename as basename2, join, extname as extname2 } from "path";
8202
+ var log15 = createLogger("headless:attachments");
8004
8203
  var UPLOADS_DIR = "src/.user-uploads";
8005
8204
  function filenameFromUrl(url) {
8006
8205
  try {
8007
8206
  const pathname = new URL(url).pathname;
8008
- const name = basename(pathname);
8207
+ const name = basename2(pathname);
8009
8208
  return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
8010
8209
  } catch {
8011
8210
  return `upload-${Date.now()}`;
@@ -8016,7 +8215,7 @@ function resolveUniqueFilename(name, claimed) {
8016
8215
  if (isFree(name)) {
8017
8216
  return name;
8018
8217
  }
8019
- const ext = extname(name);
8218
+ const ext = extname2(name);
8020
8219
  const base = name.slice(0, name.length - ext.length);
8021
8220
  let counter = 1;
8022
8221
  while (!isFree(`${base}-${counter}${ext}`)) {
@@ -8027,7 +8226,7 @@ function resolveUniqueFilename(name, claimed) {
8027
8226
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
8028
8227
  function isImageAttachment(att) {
8029
8228
  const name = att.filename || filenameFromUrl(att.url);
8030
- return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
8229
+ return IMAGE_EXTENSIONS.has(extname2(name).toLowerCase());
8031
8230
  }
8032
8231
  async function persistAttachments(attachments) {
8033
8232
  const nonVoice = attachments.filter((a) => !a.isVoice);
@@ -8056,7 +8255,7 @@ async function persistAttachments(attachments) {
8056
8255
  }
8057
8256
  const buffer = Buffer.from(await res.arrayBuffer());
8058
8257
  await writeFile(localPath, buffer);
8059
- log14.info("Attachment saved", {
8258
+ log15.info("Attachment saved", {
8060
8259
  filename: name,
8061
8260
  path: localPath,
8062
8261
  bytes: buffer.length
@@ -8070,7 +8269,7 @@ async function persistAttachments(attachments) {
8070
8269
  if (textRes.ok) {
8071
8270
  extractedTextPath = `${localPath}.txt`;
8072
8271
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
8073
- log14.info("Extracted text saved", { path: extractedTextPath });
8272
+ log15.info("Extracted text saved", { path: extractedTextPath });
8074
8273
  }
8075
8274
  } catch {
8076
8275
  }
@@ -8293,7 +8492,7 @@ function getActionChain(startName) {
8293
8492
  }
8294
8493
 
8295
8494
  // src/headless/index.ts
8296
- var log15 = createLogger("headless");
8495
+ var log16 = createLogger("headless");
8297
8496
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
8298
8497
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
8299
8498
  "promptUser",
@@ -8438,7 +8637,7 @@ var HeadlessSession = class {
8438
8637
  }
8439
8638
  const line = JSON.stringify(payload) + "\n";
8440
8639
  if (event === "history") {
8441
- log15.info("Wrote history event to stdout", {
8640
+ log16.info("Wrote history event to stdout", {
8442
8641
  requestId,
8443
8642
  bytes: line.length
8444
8643
  });
@@ -8515,7 +8714,7 @@ var HeadlessSession = class {
8515
8714
  if (this.sessionStats.lastContextSize <= FORCED_COMPACTION_THRESHOLD_TOKENS) {
8516
8715
  return;
8517
8716
  }
8518
- log15.info("Forced compaction gate triggered", {
8717
+ log16.info("Forced compaction gate triggered", {
8519
8718
  contextSize: this.sessionStats.lastContextSize,
8520
8719
  threshold: FORCED_COMPACTION_THRESHOLD_TOKENS,
8521
8720
  requestId
@@ -8532,7 +8731,7 @@ var HeadlessSession = class {
8532
8731
  }
8533
8732
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
8534
8733
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
8535
- log15.info("Background complete", {
8734
+ log16.info("Background complete", {
8536
8735
  toolCallId,
8537
8736
  name,
8538
8737
  requestId: this.currentRequestId
@@ -8569,17 +8768,17 @@ var HeadlessSession = class {
8569
8768
  return Promise.resolve(early);
8570
8769
  }
8571
8770
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
8572
- return new Promise((resolve2) => {
8771
+ return new Promise((resolve3) => {
8573
8772
  const timeout = shouldTimeout ? setTimeout(() => {
8574
8773
  this.pendingTools.delete(id);
8575
- resolve2(
8774
+ resolve3(
8576
8775
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
8577
8776
  );
8578
8777
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
8579
8778
  this.pendingTools.set(id, {
8580
8779
  resolve: (result) => {
8581
8780
  clearTimeout(timeout);
8582
- resolve2(result);
8781
+ resolve3(result);
8583
8782
  },
8584
8783
  timeout
8585
8784
  });
@@ -8770,7 +8969,7 @@ var HeadlessSession = class {
8770
8969
  await this.runForcedCompactionIfNeeded(requestId);
8771
8970
  const attachments = parsed.attachments;
8772
8971
  if (attachments?.length) {
8773
- log15.info("Message has attachments", {
8972
+ log16.info("Message has attachments", {
8774
8973
  count: attachments.length,
8775
8974
  urls: attachments.map((a) => a.url)
8776
8975
  });
@@ -8786,7 +8985,7 @@ var HeadlessSession = class {
8786
8985
  attachmentHeader = header;
8787
8986
  }
8788
8987
  } catch (err) {
8789
- log15.warn("Attachment persistence failed", { error: err.message });
8988
+ log16.warn("Attachment persistence failed", { error: err.message });
8790
8989
  }
8791
8990
  }
8792
8991
  let resolved = null;
@@ -8850,7 +9049,7 @@ var HeadlessSession = class {
8850
9049
  error: "Turn ended unexpectedly"
8851
9050
  });
8852
9051
  }
8853
- log15.info("Turn complete", {
9052
+ log16.info("Turn complete", {
8854
9053
  requestId,
8855
9054
  durationMs: Date.now() - this.turnStart
8856
9055
  });
@@ -8862,7 +9061,7 @@ var HeadlessSession = class {
8862
9061
  error: err.message
8863
9062
  });
8864
9063
  }
8865
- log15.warn("Command failed", {
9064
+ log16.warn("Command failed", {
8866
9065
  action: "message",
8867
9066
  requestId,
8868
9067
  error: err.message
@@ -9019,7 +9218,7 @@ var HeadlessSession = class {
9019
9218
  try {
9020
9219
  parsed = JSON.parse(line);
9021
9220
  } catch (err) {
9022
- log15.warn("Invalid JSON on stdin", {
9221
+ log16.warn("Invalid JSON on stdin", {
9023
9222
  error: err.message,
9024
9223
  lineLength: line.length,
9025
9224
  preview: line.slice(0, 200)
@@ -9028,7 +9227,7 @@ var HeadlessSession = class {
9028
9227
  return;
9029
9228
  }
9030
9229
  const { action, requestId } = parsed;
9031
- log15.info("Command received", { action, requestId });
9230
+ log16.info("Command received", { action, requestId });
9032
9231
  if (action === "tool_result" && parsed.id) {
9033
9232
  const id = parsed.id;
9034
9233
  const result = parsed.result ?? "";
@@ -9037,7 +9236,7 @@ var HeadlessSession = class {
9037
9236
  this.pendingTools.delete(id);
9038
9237
  pending2.resolve(result);
9039
9238
  } else if (!this.running) {
9040
- log15.info("Late tool_result while idle, dismissing", { id });
9239
+ log16.info("Late tool_result while idle, dismissing", { id });
9041
9240
  this.emit("completed", { success: true }, requestId);
9042
9241
  } else {
9043
9242
  this.earlyResults.set(id, result);
@@ -9050,7 +9249,7 @@ var HeadlessSession = class {
9050
9249
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
9051
9250
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
9052
9251
  });
9053
- log15.info("History response", {
9252
+ log16.info("History response", {
9054
9253
  requestId,
9055
9254
  startIndex: page.startIndex,
9056
9255
  endIndex: page.endIndex,