@mindstudio-ai/remy 0.1.255 → 0.1.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/headless.js +674 -537
- package/dist/index.js +685 -528
- package/dist/prompt/compiled/files.md +17 -3
- package/dist/prompt/compiled/interfaces.md +10 -11
- package/dist/prompt/compiled/methods.md +4 -5
- package/dist/prompt/compiled/scenarios.md +16 -0
- package/dist/prompt/compiled/sdk-actions.md +1 -2
- package/dist/prompt/{compiled/agent-interfaces.md → skills/agentInterfaces.md} +29 -10
- package/dist/prompt/skills/dataSources.md +131 -0
- package/dist/prompt/skills/mcpInterfaces.md +80 -0
- package/dist/prompt/{compiled/task-agents.md → skills/taskAgents.md} +10 -8
- package/dist/prompt/static/instructions.md +1 -1
- package/dist/subagents/browserAutomation/prompt.md +2 -1
- package/dist/subagents/designExpert/prompts/images.md +1 -1
- package/package.json +2 -2
- package/dist/prompt/.notes.md +0 -194
- package/dist/prompt/compiled/README.md +0 -100
- package/dist/prompt/compiled/mcp-interfaces.md +0 -34
- package/dist/prompt/compiled/media-cdn.md +0 -51
- package/dist/prompt/sources/llms.txt +0 -1618
- package/dist/subagents/.notes-background-agents.md +0 -64
- package/dist/subagents/codeSanityCheck/.notes.md +0 -44
- package/dist/subagents/designExpert/.notes.md +0 -265
- package/dist/subagents/productVision/.notes.md +0 -79
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((
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1050
|
-
import
|
|
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
|
|
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
|
|
1190
|
+
await fs7.mkdir(path5.dirname(input.path), { recursive: true });
|
|
1141
1191
|
let oldContent = null;
|
|
1142
1192
|
try {
|
|
1143
|
-
oldContent = await
|
|
1193
|
+
oldContent = await fs7.readFile(input.path, "utf-8");
|
|
1144
1194
|
} catch {
|
|
1145
1195
|
}
|
|
1146
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1346
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1533
|
+
await fs11.unlink(PLAN_FILE2);
|
|
1484
1534
|
return "Plan rejected and removed.";
|
|
1485
1535
|
}
|
|
1486
|
-
await
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1863
|
-
fd =
|
|
1912
|
+
fs12.mkdirSync(".logs", { recursive: true });
|
|
1913
|
+
fd = fs12.openSync(LEDGER_FILE, "a");
|
|
1864
1914
|
}
|
|
1865
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2170
|
-
import
|
|
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
|
|
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
|
|
2305
|
+
await fs14.mkdir(path7.dirname(input.path), { recursive: true });
|
|
2217
2306
|
let oldContent = null;
|
|
2218
2307
|
try {
|
|
2219
|
-
oldContent = await
|
|
2308
|
+
oldContent = await fs14.readFile(input.path, "utf-8");
|
|
2220
2309
|
} catch {
|
|
2221
2310
|
}
|
|
2222
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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 ?
|
|
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
|
-
|
|
2475
|
+
resolve3(`Error: process exited with code ${code}`);
|
|
2387
2476
|
} else {
|
|
2388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2509
|
+
resolve3(output);
|
|
2421
2510
|
}
|
|
2422
2511
|
});
|
|
2423
2512
|
child.on("error", (err) => {
|
|
2424
2513
|
clearTimeout(timer);
|
|
2425
|
-
|
|
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((
|
|
2632
|
+
return new Promise((resolve3) => {
|
|
2544
2633
|
exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
|
|
2545
2634
|
if (stdout?.trim()) {
|
|
2546
|
-
|
|
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
|
-
|
|
2640
|
+
resolve3(formatResults(grepStdout, max, mode));
|
|
2552
2641
|
} else {
|
|
2553
|
-
|
|
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
|
|
2611
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
2657
|
-
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(
|
|
2745
|
+
const stat2 = await fs16.stat(path9.join(dirPath, name));
|
|
2746
|
+
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat2.size)}`;
|
|
2658
2747
|
} catch {
|
|
2659
2748
|
return `${indent}${name}`;
|
|
2660
2749
|
}
|
|
2661
2750
|
}
|
|
2662
2751
|
var listDirTool = {
|
|
2663
|
-
clearable: true,
|
|
2664
2752
|
definition: {
|
|
2753
|
+
clearable: true,
|
|
2665
2754
|
name: "listDir",
|
|
2666
2755
|
description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
|
|
2667
2756
|
inputSchema: {
|
|
@@ -2737,8 +2826,8 @@ var listDirTool = {
|
|
|
2737
2826
|
|
|
2738
2827
|
// src/tools/code/editsFinished.ts
|
|
2739
2828
|
var editsFinishedTool = {
|
|
2740
|
-
clearable: false,
|
|
2741
2829
|
definition: {
|
|
2830
|
+
clearable: false,
|
|
2742
2831
|
name: "editsFinished",
|
|
2743
2832
|
description: "Signal that file edits are complete. Call this after you finish writing/editing files so the live preview updates cleanly. The preview is paused while you edit to avoid showing broken intermediate states \u2014 this unpauses it. If you forget to call this, the preview updates when your turn ends.",
|
|
2744
2833
|
inputSchema: {
|
|
@@ -2764,6 +2853,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
|
|
|
2764
2853
|
throw new Error("Sidecar not available");
|
|
2765
2854
|
}
|
|
2766
2855
|
const url = `${baseUrl}${endpoint}`;
|
|
2856
|
+
let data;
|
|
2767
2857
|
try {
|
|
2768
2858
|
const res = await fetch(url, {
|
|
2769
2859
|
method: "POST",
|
|
@@ -2775,12 +2865,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
|
|
|
2775
2865
|
log4.error("Sidecar error", { endpoint, status: res.status });
|
|
2776
2866
|
throw new Error(`Sidecar error: ${res.status}`);
|
|
2777
2867
|
}
|
|
2778
|
-
|
|
2779
|
-
if (data?.success === false) {
|
|
2780
|
-
const code = data.errorCode ? ` [${data.errorCode}]` : "";
|
|
2781
|
-
throw new Error(`${data.error || "Unknown error"}${code}`);
|
|
2782
|
-
}
|
|
2783
|
-
return data;
|
|
2868
|
+
data = await res.json();
|
|
2784
2869
|
} catch (err) {
|
|
2785
2870
|
if (err.message.startsWith("Sidecar error")) {
|
|
2786
2871
|
throw err;
|
|
@@ -2788,6 +2873,16 @@ async function sidecarRequest(endpoint, body = {}, options) {
|
|
|
2788
2873
|
log4.error("Sidecar connection error", { endpoint, error: err.message });
|
|
2789
2874
|
throw new Error(`Sidecar connection error: ${err.message}`);
|
|
2790
2875
|
}
|
|
2876
|
+
if (data?.success === false) {
|
|
2877
|
+
const code = data.errorCode ? ` [${data.errorCode}]` : "";
|
|
2878
|
+
log4.error("Sidecar command failed", {
|
|
2879
|
+
endpoint,
|
|
2880
|
+
error: data.error,
|
|
2881
|
+
errorCode: data.errorCode
|
|
2882
|
+
});
|
|
2883
|
+
throw new Error(`${data.error || "Unknown error"}${code}`);
|
|
2884
|
+
}
|
|
2885
|
+
return data;
|
|
2791
2886
|
}
|
|
2792
2887
|
|
|
2793
2888
|
// src/tools/_helpers/lsp.ts
|
|
@@ -2798,8 +2893,8 @@ async function lspRequest(endpoint, body) {
|
|
|
2798
2893
|
|
|
2799
2894
|
// src/tools/code/lspDiagnostics.ts
|
|
2800
2895
|
var lspDiagnosticsTool = {
|
|
2801
|
-
clearable: true,
|
|
2802
2896
|
definition: {
|
|
2897
|
+
clearable: true,
|
|
2803
2898
|
name: "lspDiagnostics",
|
|
2804
2899
|
description: "Get TypeScript diagnostics (type errors, warnings) for a file, with suggested fixes when available. Use this after editing a file to check for errors.",
|
|
2805
2900
|
inputSchema: {
|
|
@@ -2847,8 +2942,8 @@ var lspDiagnosticsTool = {
|
|
|
2847
2942
|
|
|
2848
2943
|
// src/tools/code/restartProcess.ts
|
|
2849
2944
|
var restartProcessTool = {
|
|
2850
|
-
clearable: false,
|
|
2851
2945
|
definition: {
|
|
2946
|
+
clearable: false,
|
|
2852
2947
|
name: "restartProcess",
|
|
2853
2948
|
description: "Restart a managed sandbox process. Use this after running npm install or changing package.json to restart the dev server so it picks up new dependencies.",
|
|
2854
2949
|
inputSchema: {
|
|
@@ -2865,7 +2960,7 @@ var restartProcessTool = {
|
|
|
2865
2960
|
async execute(input) {
|
|
2866
2961
|
const data = await lspRequest("/restart-process", { name: input.name });
|
|
2867
2962
|
if (data.ok) {
|
|
2868
|
-
await new Promise((
|
|
2963
|
+
await new Promise((resolve3) => setTimeout(resolve3, 5e3));
|
|
2869
2964
|
return `Restarted ${input.name}.`;
|
|
2870
2965
|
}
|
|
2871
2966
|
return `Error: unexpected response: ${JSON.stringify(data)}`;
|
|
@@ -2874,8 +2969,8 @@ var restartProcessTool = {
|
|
|
2874
2969
|
|
|
2875
2970
|
// src/tools/code/runScenario.ts
|
|
2876
2971
|
var runScenarioTool = {
|
|
2877
|
-
clearable: true,
|
|
2878
2972
|
definition: {
|
|
2973
|
+
clearable: true,
|
|
2879
2974
|
name: "runScenario",
|
|
2880
2975
|
description: "Run a scenario to seed the dev database with test data. By default truncates all tables first, then executes the seed function and impersonates the scenario roles. Use skipTruncate to run the seed function against existing data without resetting. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details. Return synchronously - no need to sleep before checking results.",
|
|
2881
2976
|
inputSchema: {
|
|
@@ -2900,8 +2995,8 @@ var runScenarioTool = {
|
|
|
2900
2995
|
|
|
2901
2996
|
// src/tools/code/runMethod.ts
|
|
2902
2997
|
var runMethodTool = {
|
|
2903
|
-
clearable: true,
|
|
2904
2998
|
definition: {
|
|
2999
|
+
clearable: true,
|
|
2905
3000
|
name: "runMethod",
|
|
2906
3001
|
description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
|
|
2907
3002
|
inputSchema: {
|
|
@@ -2935,8 +3030,8 @@ var runMethodTool = {
|
|
|
2935
3030
|
|
|
2936
3031
|
// src/tools/code/queryDatabase.ts
|
|
2937
3032
|
var queryDatabaseTool = {
|
|
2938
|
-
clearable: true,
|
|
2939
3033
|
definition: {
|
|
3034
|
+
clearable: true,
|
|
2940
3035
|
name: "queryDatabase",
|
|
2941
3036
|
description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
|
|
2942
3037
|
inputSchema: {
|
|
@@ -2955,24 +3050,127 @@ var queryDatabaseTool = {
|
|
|
2955
3050
|
}
|
|
2956
3051
|
};
|
|
2957
3052
|
|
|
3053
|
+
// src/tools/_helpers/uploadImage.ts
|
|
3054
|
+
import { readFile, stat } from "fs/promises";
|
|
3055
|
+
import { basename, extname, resolve } from "path";
|
|
3056
|
+
var log5 = createLogger("uploadImage");
|
|
3057
|
+
var UPLOAD_TIMEOUT_MS = 6e4;
|
|
3058
|
+
var CONTENT_TYPES = {
|
|
3059
|
+
".png": "image/png",
|
|
3060
|
+
".jpg": "image/jpeg",
|
|
3061
|
+
".jpeg": "image/jpeg",
|
|
3062
|
+
".gif": "image/gif",
|
|
3063
|
+
".webp": "image/webp"
|
|
3064
|
+
};
|
|
3065
|
+
var hosted = /* @__PURE__ */ new Map();
|
|
3066
|
+
function isFetchableUrl(ref) {
|
|
3067
|
+
return /^(?:https?:|data:)/i.test(ref);
|
|
3068
|
+
}
|
|
3069
|
+
async function resolveImageRef(ref, apiConfig) {
|
|
3070
|
+
const trimmed = ref.trim();
|
|
3071
|
+
return isFetchableUrl(trimmed) ? trimmed : uploadLocalImage(trimmed, apiConfig);
|
|
3072
|
+
}
|
|
3073
|
+
function resolveImageRefs(refs, apiConfig) {
|
|
3074
|
+
return Promise.all(refs.map((ref) => resolveImageRef(ref, apiConfig)));
|
|
3075
|
+
}
|
|
3076
|
+
async function uploadLocalImage(localPath, apiConfig) {
|
|
3077
|
+
const absolute = resolve(PROJECT_ROOT, localPath);
|
|
3078
|
+
const ext = extname(absolute).toLowerCase();
|
|
3079
|
+
const contentType = CONTENT_TYPES[ext];
|
|
3080
|
+
if (!contentType) {
|
|
3081
|
+
throw new Error(
|
|
3082
|
+
`Cannot use "${localPath}" as an image \u2014 vision models read ${Object.keys(
|
|
3083
|
+
CONTENT_TYPES
|
|
3084
|
+
).join(", ")}. Convert it first, or pass an image URL.`
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
let stats;
|
|
3088
|
+
try {
|
|
3089
|
+
stats = await stat(absolute);
|
|
3090
|
+
} catch {
|
|
3091
|
+
throw new Error(
|
|
3092
|
+
`No file at "${localPath}". Paths resolve from the project root; list the directory to check the name, or pass an image URL.`
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
const cacheKey = `${absolute}:${stats.mtimeMs}:${stats.size}`;
|
|
3096
|
+
const cached3 = hosted.get(cacheKey);
|
|
3097
|
+
if (cached3) {
|
|
3098
|
+
return cached3;
|
|
3099
|
+
}
|
|
3100
|
+
if (!apiConfig?.appId) {
|
|
3101
|
+
throw new Error(
|
|
3102
|
+
`Cannot host "${localPath}" for analysis \u2014 this session has no app id. Pass an image URL instead.`
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
const bytes = await readFile(absolute);
|
|
3106
|
+
const target = await requestUploadTarget(apiConfig, ext.slice(1));
|
|
3107
|
+
const form = new FormData();
|
|
3108
|
+
for (const [field, value] of Object.entries(target.uploadFields)) {
|
|
3109
|
+
form.append(field, value);
|
|
3110
|
+
}
|
|
3111
|
+
form.append(
|
|
3112
|
+
"file",
|
|
3113
|
+
new Blob([bytes], { type: contentType }),
|
|
3114
|
+
basename(absolute)
|
|
3115
|
+
);
|
|
3116
|
+
const res = await fetch(target.uploadUrl, {
|
|
3117
|
+
method: "POST",
|
|
3118
|
+
body: form,
|
|
3119
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
3120
|
+
});
|
|
3121
|
+
if (!res.ok) {
|
|
3122
|
+
throw new Error(`Upload of "${localPath}" failed: HTTP ${res.status}`);
|
|
3123
|
+
}
|
|
3124
|
+
log5.info("Local image hosted", {
|
|
3125
|
+
path: localPath,
|
|
3126
|
+
bytes: stats.size,
|
|
3127
|
+
url: target.publicUrl
|
|
3128
|
+
});
|
|
3129
|
+
hosted.set(cacheKey, target.publicUrl);
|
|
3130
|
+
return target.publicUrl;
|
|
3131
|
+
}
|
|
3132
|
+
async function requestUploadTarget(apiConfig, extension) {
|
|
3133
|
+
const url = `${apiConfig.baseUrl}/_internal/v2/apps/${apiConfig.appId}/dev/manage/upload`;
|
|
3134
|
+
const res = await fetch(url, {
|
|
3135
|
+
method: "POST",
|
|
3136
|
+
headers: {
|
|
3137
|
+
"Content-Type": "application/json",
|
|
3138
|
+
Authorization: `Bearer ${apiConfig.apiKey}`
|
|
3139
|
+
},
|
|
3140
|
+
body: JSON.stringify({ extension }),
|
|
3141
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
3142
|
+
});
|
|
3143
|
+
if (!res.ok) {
|
|
3144
|
+
throw new Error(`Could not get an upload URL: HTTP ${res.status}`);
|
|
3145
|
+
}
|
|
3146
|
+
return await res.json();
|
|
3147
|
+
}
|
|
3148
|
+
|
|
2958
3149
|
// src/subagents/common/analyzeImage.ts
|
|
2959
3150
|
async function analyzeImage(params) {
|
|
2960
|
-
const { prompt,
|
|
2961
|
-
|
|
3151
|
+
const { prompt, image, apiConfig, model, timeout = 2e5, onLog } = params;
|
|
3152
|
+
const url = await resolveImageRef(image, apiConfig);
|
|
3153
|
+
const result = await runMindstudioCliResult(
|
|
2962
3154
|
[
|
|
2963
3155
|
"analyze-image",
|
|
2964
3156
|
"--prompt",
|
|
2965
3157
|
prompt,
|
|
2966
3158
|
"--image-url",
|
|
2967
|
-
|
|
3159
|
+
url,
|
|
2968
3160
|
"--vision-model-override",
|
|
2969
3161
|
JSON.stringify({ model })
|
|
2970
3162
|
],
|
|
2971
3163
|
{ outputKey: "analysis", timeout, onLog }
|
|
2972
3164
|
);
|
|
3165
|
+
if (!result.ok) {
|
|
3166
|
+
throw new Error(`Could not analyze ${url}: ${result.value}`);
|
|
3167
|
+
}
|
|
3168
|
+
return { url, analysis: result.value };
|
|
2973
3169
|
}
|
|
2974
3170
|
|
|
2975
3171
|
// src/tools/_helpers/screenshot.ts
|
|
3172
|
+
var VIEWPORT_CAPTURE_TIMEOUT_MS = 45e3;
|
|
3173
|
+
var FULLPAGE_CAPTURE_TIMEOUT_MS = 135e3;
|
|
2976
3174
|
var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
|
|
2977
3175
|
var ANALYSIS_RESPONSE_FORMAT = `Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
|
|
2978
3176
|
function buildScreenshotAnalysisPrompt(opts) {
|
|
@@ -2992,13 +3190,14 @@ ${ANALYSIS_RESPONSE_FORMAT}`;
|
|
|
2992
3190
|
return p;
|
|
2993
3191
|
}
|
|
2994
3192
|
async function streamScreenshotAnalysis(opts) {
|
|
2995
|
-
const {
|
|
3193
|
+
const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
|
|
3194
|
+
const url = await resolveImageRef(image, apiConfig);
|
|
2996
3195
|
onLog?.(JSON.stringify({ url, analysis: null }));
|
|
2997
3196
|
const analysisPrompt = buildScreenshotAnalysisPrompt({ prompt, styleMap });
|
|
2998
3197
|
let accumulated = "";
|
|
2999
|
-
const analysis = await analyzeImage({
|
|
3198
|
+
const { analysis } = await analyzeImage({
|
|
3000
3199
|
prompt: analysisPrompt,
|
|
3001
|
-
|
|
3200
|
+
image: url,
|
|
3002
3201
|
model,
|
|
3003
3202
|
onLog: (chunk) => {
|
|
3004
3203
|
accumulated += chunk;
|
|
@@ -3009,18 +3208,19 @@ async function streamScreenshotAnalysis(opts) {
|
|
|
3009
3208
|
}
|
|
3010
3209
|
async function captureAndAnalyzeScreenshot(promptOrOptions) {
|
|
3011
3210
|
let prompt;
|
|
3012
|
-
let
|
|
3211
|
+
let existingImage;
|
|
3013
3212
|
let onLog;
|
|
3014
3213
|
let model;
|
|
3015
|
-
let
|
|
3214
|
+
let apiConfig;
|
|
3215
|
+
let path14;
|
|
3016
3216
|
let fullPage = true;
|
|
3017
3217
|
let width;
|
|
3018
3218
|
let height;
|
|
3019
3219
|
let format;
|
|
3020
3220
|
if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
|
|
3021
3221
|
prompt = promptOrOptions.prompt;
|
|
3022
|
-
|
|
3023
|
-
|
|
3222
|
+
existingImage = promptOrOptions.image;
|
|
3223
|
+
path14 = promptOrOptions.path;
|
|
3024
3224
|
if (promptOrOptions.fullPage !== void 0) {
|
|
3025
3225
|
fullPage = promptOrOptions.fullPage;
|
|
3026
3226
|
}
|
|
@@ -3029,6 +3229,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
|
|
|
3029
3229
|
format = promptOrOptions.format;
|
|
3030
3230
|
onLog = promptOrOptions.onLog;
|
|
3031
3231
|
model = promptOrOptions.model;
|
|
3232
|
+
apiConfig = promptOrOptions.apiConfig;
|
|
3032
3233
|
} else {
|
|
3033
3234
|
prompt = promptOrOptions;
|
|
3034
3235
|
}
|
|
@@ -3037,18 +3238,20 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
|
|
|
3037
3238
|
}
|
|
3038
3239
|
let url;
|
|
3039
3240
|
let styleMap;
|
|
3040
|
-
if (
|
|
3041
|
-
url =
|
|
3241
|
+
if (existingImage) {
|
|
3242
|
+
url = existingImage;
|
|
3042
3243
|
} else {
|
|
3043
3244
|
const ssResult = await sidecarRequest(
|
|
3044
3245
|
fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
|
|
3045
3246
|
{
|
|
3046
|
-
...
|
|
3247
|
+
...path14 ? { path: path14 } : {},
|
|
3047
3248
|
...width != null ? { width } : {},
|
|
3048
3249
|
...height != null ? { height } : {},
|
|
3049
3250
|
...format ? { format } : {}
|
|
3050
3251
|
},
|
|
3051
|
-
{
|
|
3252
|
+
{
|
|
3253
|
+
timeout: fullPage ? FULLPAGE_CAPTURE_TIMEOUT_MS : VIEWPORT_CAPTURE_TIMEOUT_MS
|
|
3254
|
+
}
|
|
3052
3255
|
);
|
|
3053
3256
|
url = ssResult?.url || ssResult?.screenshotUrl;
|
|
3054
3257
|
if (!url) {
|
|
@@ -3059,7 +3262,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
|
|
|
3059
3262
|
styleMap = ssResult?.styleMap;
|
|
3060
3263
|
}
|
|
3061
3264
|
if (prompt === false) {
|
|
3062
|
-
return url;
|
|
3265
|
+
return resolveImageRef(url, apiConfig);
|
|
3063
3266
|
}
|
|
3064
3267
|
if (!model) {
|
|
3065
3268
|
throw new Error(
|
|
@@ -3067,7 +3270,8 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
|
|
|
3067
3270
|
);
|
|
3068
3271
|
}
|
|
3069
3272
|
return streamScreenshotAnalysis({
|
|
3070
|
-
url,
|
|
3273
|
+
image: url,
|
|
3274
|
+
apiConfig,
|
|
3071
3275
|
prompt: prompt || void 0,
|
|
3072
3276
|
styleMap,
|
|
3073
3277
|
onLog,
|
|
@@ -3088,7 +3292,7 @@ function acquireBrowserLock() {
|
|
|
3088
3292
|
}
|
|
3089
3293
|
|
|
3090
3294
|
// src/toolRegistry.ts
|
|
3091
|
-
var
|
|
3295
|
+
var log6 = createLogger("tool-registry");
|
|
3092
3296
|
var USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
|
|
3093
3297
|
var ToolRegistry = class {
|
|
3094
3298
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -3115,7 +3319,7 @@ var ToolRegistry = class {
|
|
|
3115
3319
|
if (!entry) {
|
|
3116
3320
|
return false;
|
|
3117
3321
|
}
|
|
3118
|
-
|
|
3322
|
+
log6.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
|
|
3119
3323
|
entry.abortController.abort(mode);
|
|
3120
3324
|
if (mode === "graceful") {
|
|
3121
3325
|
const partial = entry.getPartialResult?.() ?? "";
|
|
@@ -3148,7 +3352,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
3148
3352
|
if (!entry) {
|
|
3149
3353
|
return false;
|
|
3150
3354
|
}
|
|
3151
|
-
|
|
3355
|
+
log6.info("Tool restarted", { toolCallId: id, name: entry.name });
|
|
3152
3356
|
entry.abortController.abort("restart");
|
|
3153
3357
|
const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
|
|
3154
3358
|
this.onEvent?.({
|
|
@@ -3250,6 +3454,9 @@ function findLastSummaryCheckpoint(messages, name) {
|
|
|
3250
3454
|
}
|
|
3251
3455
|
return -1;
|
|
3252
3456
|
}
|
|
3457
|
+
function portableToolCallId(id) {
|
|
3458
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3459
|
+
}
|
|
3253
3460
|
function fixOrphanedToolCalls(messages) {
|
|
3254
3461
|
const toolResultIds = /* @__PURE__ */ new Set();
|
|
3255
3462
|
for (const msg of messages) {
|
|
@@ -3341,14 +3548,30 @@ ${summaryBlock.text}
|
|
|
3341
3548
|
|
|
3342
3549
|
${content}` : attachmentHeader;
|
|
3343
3550
|
}
|
|
3344
|
-
return {
|
|
3551
|
+
return {
|
|
3552
|
+
...rest,
|
|
3553
|
+
content,
|
|
3554
|
+
...msg.toolCallId && {
|
|
3555
|
+
toolCallId: portableToolCallId(msg.toolCallId)
|
|
3556
|
+
}
|
|
3557
|
+
};
|
|
3345
3558
|
}
|
|
3346
3559
|
if (!Array.isArray(msg.content)) {
|
|
3347
3560
|
return msg;
|
|
3348
3561
|
}
|
|
3349
3562
|
const blocks = msg.content;
|
|
3350
3563
|
const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
3351
|
-
const
|
|
3564
|
+
const toolBlocks = blocks.filter(
|
|
3565
|
+
(b) => b.type === "tool"
|
|
3566
|
+
);
|
|
3567
|
+
const toolCalls = toolBlocks.map((b) => ({
|
|
3568
|
+
id: portableToolCallId(b.id),
|
|
3569
|
+
name: b.name,
|
|
3570
|
+
input: b.input
|
|
3571
|
+
}));
|
|
3572
|
+
const rewroteToolIds = toolBlocks.some(
|
|
3573
|
+
(b) => portableToolCallId(b.id) !== b.id
|
|
3574
|
+
);
|
|
3352
3575
|
const cleaned2 = {
|
|
3353
3576
|
role: msg.role,
|
|
3354
3577
|
content: text
|
|
@@ -3356,7 +3579,7 @@ ${content}` : attachmentHeader;
|
|
|
3356
3579
|
if (toolCalls.length > 0) {
|
|
3357
3580
|
cleaned2.toolCalls = toolCalls;
|
|
3358
3581
|
}
|
|
3359
|
-
if (msg.providerMetadata) {
|
|
3582
|
+
if (msg.providerMetadata && !rewroteToolIds) {
|
|
3360
3583
|
cleaned2.providerMetadata = msg.providerMetadata;
|
|
3361
3584
|
}
|
|
3362
3585
|
if (msg.hidden) {
|
|
@@ -3368,7 +3591,7 @@ ${content}` : attachmentHeader;
|
|
|
3368
3591
|
}
|
|
3369
3592
|
|
|
3370
3593
|
// src/subagents/runner.ts
|
|
3371
|
-
var
|
|
3594
|
+
var log7 = createLogger("sub-agent");
|
|
3372
3595
|
async function runSubAgent(config) {
|
|
3373
3596
|
const {
|
|
3374
3597
|
system,
|
|
@@ -3396,7 +3619,7 @@ async function runSubAgent(config) {
|
|
|
3396
3619
|
const signal = background ? bgAbort.signal : parentSignal;
|
|
3397
3620
|
const agentName = subAgentId || "sub-agent";
|
|
3398
3621
|
const runStart = Date.now();
|
|
3399
|
-
|
|
3622
|
+
log7.info("Sub-agent started", { requestId, parentToolId, agentName });
|
|
3400
3623
|
const emit = (e) => {
|
|
3401
3624
|
onEvent({ ...e, parentToolId });
|
|
3402
3625
|
};
|
|
@@ -3408,7 +3631,6 @@ async function runSubAgent(config) {
|
|
|
3408
3631
|
const fullSystem = `${system}
|
|
3409
3632
|
|
|
3410
3633
|
Current date: ${dateStr}`;
|
|
3411
|
-
const excludeToolsFromClearing = tools2.filter((t) => t.clearable === false).map((t) => t.name);
|
|
3412
3634
|
let turns = 0;
|
|
3413
3635
|
const run = async () => {
|
|
3414
3636
|
const historyLen = (history ?? []).length;
|
|
@@ -3480,7 +3702,6 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3480
3702
|
system: fullSystem,
|
|
3481
3703
|
messages: cleanMessagesForApi(messages),
|
|
3482
3704
|
tools: tools2,
|
|
3483
|
-
excludeToolsFromClearing,
|
|
3484
3705
|
signal
|
|
3485
3706
|
},
|
|
3486
3707
|
{
|
|
@@ -3616,7 +3837,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3616
3837
|
...hasArtifacts ? { artifacts } : {}
|
|
3617
3838
|
};
|
|
3618
3839
|
}
|
|
3619
|
-
|
|
3840
|
+
log7.info("Tools executing", {
|
|
3620
3841
|
requestId,
|
|
3621
3842
|
parentToolId,
|
|
3622
3843
|
count: toolCalls.length,
|
|
@@ -3693,7 +3914,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3693
3914
|
run2(tc.input);
|
|
3694
3915
|
const r = await resultPromise;
|
|
3695
3916
|
toolRegistry?.unregister(tc.id);
|
|
3696
|
-
|
|
3917
|
+
log7.info("Tool completed", {
|
|
3697
3918
|
requestId,
|
|
3698
3919
|
parentToolId,
|
|
3699
3920
|
toolCallId: tc.id,
|
|
@@ -3744,7 +3965,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3744
3965
|
const wrapRun = async () => {
|
|
3745
3966
|
try {
|
|
3746
3967
|
const result = await run();
|
|
3747
|
-
|
|
3968
|
+
log7.info("Sub-agent complete", {
|
|
3748
3969
|
requestId,
|
|
3749
3970
|
parentToolId,
|
|
3750
3971
|
agentName,
|
|
@@ -3753,7 +3974,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3753
3974
|
});
|
|
3754
3975
|
return result;
|
|
3755
3976
|
} catch (err) {
|
|
3756
|
-
|
|
3977
|
+
log7.warn("Sub-agent error", {
|
|
3757
3978
|
requestId,
|
|
3758
3979
|
parentToolId,
|
|
3759
3980
|
agentName,
|
|
@@ -3765,7 +3986,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3765
3986
|
if (!background) {
|
|
3766
3987
|
return wrapRun();
|
|
3767
3988
|
}
|
|
3768
|
-
|
|
3989
|
+
log7.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
|
|
3769
3990
|
toolRegistry?.register({
|
|
3770
3991
|
id: parentToolId,
|
|
3771
3992
|
name: agentName,
|
|
@@ -3874,7 +4095,7 @@ var BROWSER_TOOLS = [
|
|
|
3874
4095
|
"screenshotViewport",
|
|
3875
4096
|
"setViewport"
|
|
3876
4097
|
],
|
|
3877
|
-
description: 'snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). select: select a dropdown option by text. wait: wait for an element to appear (polls 100ms, waits for network). navigate: navigate to a URL within the app (waits for load, subsequent steps run on new page). evaluate: run JS in the page. styles: read computed CSS styles from elements (pass properties array with camelCase names, or omit for defaults). screenshotFullPage:
|
|
4098
|
+
description: 'snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). select: select a dropdown option by text. wait: wait for an element to appear (polls 100ms, waits for network). navigate: navigate to a URL within the app (waits for load, subsequent steps run on new page). evaluate: run JS in the page. styles: read computed CSS styles from elements (pass properties array with camelCase names, or omit for defaults). screenshotFullPage: screenshot of the whole page top-to-bottom (returns a CDN url with dimensions and a written analysis). screenshotViewport: screenshot of just the visible viewport \u2014 pass `scrollToSelector` (or `scrollY`) on this step to scroll a section into view and capture it in one atomic step (no separate scroll needed). setViewport: switch the browser between desktop and mobile rendering (pass `mode`: "desktop" or "mobile"). Reloads the page so responsive layouts, media queries, and matchMedia re-evaluate \u2014 use it to QA mobile/responsive views.'
|
|
3878
4099
|
},
|
|
3879
4100
|
ref: {
|
|
3880
4101
|
type: "string",
|
|
@@ -3942,20 +4163,11 @@ var BROWSER_TOOLS = [
|
|
|
3942
4163
|
required: ["steps"]
|
|
3943
4164
|
}
|
|
3944
4165
|
},
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
type: "object",
|
|
3951
|
-
properties: {
|
|
3952
|
-
path: {
|
|
3953
|
-
type: "string",
|
|
3954
|
-
description: 'Navigate to this path before capturing (e.g. "/settings"). If omitted, screenshots the current page.'
|
|
3955
|
-
}
|
|
3956
|
-
}
|
|
3957
|
-
}
|
|
3958
|
-
},
|
|
4166
|
+
// Captures are `browserCommand` steps only — there is deliberately no
|
|
4167
|
+
// standalone screenshot tool here. Both used to exist for full-page, with
|
|
4168
|
+
// different budgets, different result plumbing, and analysis on only one of
|
|
4169
|
+
// them, so which door you picked changed what you got back.
|
|
4170
|
+
//
|
|
3959
4171
|
// Read tools so the QA agent can pull full spec detail on demand — the spec
|
|
3960
4172
|
// context in its prompt is a lightweight index (see prompt.ts) that points
|
|
3961
4173
|
// here. Routed to the global executeTool in index.ts, mirroring specSync.
|
|
@@ -3965,13 +4177,13 @@ var BROWSER_TOOLS = [
|
|
|
3965
4177
|
var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
|
|
3966
4178
|
|
|
3967
4179
|
// src/subagents/common/context.ts
|
|
3968
|
-
import
|
|
3969
|
-
import
|
|
4180
|
+
import fs17 from "fs";
|
|
4181
|
+
import path10 from "path";
|
|
3970
4182
|
function walkMdFiles2(dir, skip) {
|
|
3971
4183
|
const files = [];
|
|
3972
4184
|
try {
|
|
3973
|
-
for (const entry of
|
|
3974
|
-
const full =
|
|
4185
|
+
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
4186
|
+
const full = path10.join(dir, entry.name);
|
|
3975
4187
|
if (entry.isDirectory()) {
|
|
3976
4188
|
if (!skip?.has(entry.name)) {
|
|
3977
4189
|
files.push(...walkMdFiles2(full, skip));
|
|
@@ -3984,9 +4196,9 @@ function walkMdFiles2(dir, skip) {
|
|
|
3984
4196
|
}
|
|
3985
4197
|
return files.sort();
|
|
3986
4198
|
}
|
|
3987
|
-
function
|
|
4199
|
+
function parseFrontmatter3(filePath) {
|
|
3988
4200
|
try {
|
|
3989
|
-
const content =
|
|
4201
|
+
const content = fs17.readFileSync(filePath, "utf-8");
|
|
3990
4202
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
3991
4203
|
if (!match) {
|
|
3992
4204
|
return {};
|
|
@@ -4011,7 +4223,7 @@ function loadSpecIndex() {
|
|
|
4011
4223
|
return "";
|
|
4012
4224
|
}
|
|
4013
4225
|
const lines = files.map((f) => {
|
|
4014
|
-
const fm =
|
|
4226
|
+
const fm = parseFrontmatter3(f);
|
|
4015
4227
|
let line = `- ${f}`;
|
|
4016
4228
|
if (fm.name) {
|
|
4017
4229
|
line += ` \u2014 "${fm.name}"`;
|
|
@@ -4032,7 +4244,7 @@ function loadRoadmapIndex() {
|
|
|
4032
4244
|
const parts = [];
|
|
4033
4245
|
try {
|
|
4034
4246
|
const indexJson = JSON.parse(
|
|
4035
|
-
|
|
4247
|
+
fs17.readFileSync("src/roadmap/index.json", "utf-8")
|
|
4036
4248
|
);
|
|
4037
4249
|
if (indexJson.lanes?.length > 0) {
|
|
4038
4250
|
const laneLines = indexJson.lanes.map(
|
|
@@ -4052,7 +4264,7 @@ ${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
|
|
|
4052
4264
|
const files = walkMdFiles2("src/roadmap");
|
|
4053
4265
|
if (files.length > 0) {
|
|
4054
4266
|
const lines = files.map((f) => {
|
|
4055
|
-
const fm =
|
|
4267
|
+
const fm = parseFrontmatter3(f);
|
|
4056
4268
|
let line = `- ${f}`;
|
|
4057
4269
|
if (fm.name) {
|
|
4058
4270
|
line += ` \u2014 "${fm.name}"`;
|
|
@@ -4148,7 +4360,8 @@ function getBrowserAutomationPrompt() {
|
|
|
4148
4360
|
}
|
|
4149
4361
|
|
|
4150
4362
|
// src/subagents/browserAutomation/index.ts
|
|
4151
|
-
var
|
|
4363
|
+
var log8 = createLogger("browser-automation");
|
|
4364
|
+
var CAPTURE_COMMANDS = /* @__PURE__ */ new Set(["screenshotViewport", "screenshotFullPage"]);
|
|
4152
4365
|
async function runBrowserAutomation(task, context, opts) {
|
|
4153
4366
|
const release = await acquireBrowserLock();
|
|
4154
4367
|
try {
|
|
@@ -4160,7 +4373,7 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4160
4373
|
);
|
|
4161
4374
|
} catch {
|
|
4162
4375
|
}
|
|
4163
|
-
let
|
|
4376
|
+
let lastCapture = {};
|
|
4164
4377
|
const result = await runSubAgent({
|
|
4165
4378
|
system: getBrowserAutomationPrompt(),
|
|
4166
4379
|
task,
|
|
@@ -4182,22 +4395,6 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4182
4395
|
return `Error setting up browser: ${err.message}`;
|
|
4183
4396
|
}
|
|
4184
4397
|
}
|
|
4185
|
-
if (name === "screenshotFullPage") {
|
|
4186
|
-
try {
|
|
4187
|
-
return await captureAndAnalyzeScreenshot({
|
|
4188
|
-
path: _input.path,
|
|
4189
|
-
fullPage: true,
|
|
4190
|
-
onLog,
|
|
4191
|
-
model: resolveModel(
|
|
4192
|
-
"imageAnalysis",
|
|
4193
|
-
context.models,
|
|
4194
|
-
context.model
|
|
4195
|
-
)
|
|
4196
|
-
});
|
|
4197
|
-
} catch (err) {
|
|
4198
|
-
return `Error taking screenshot: ${err.message}`;
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
4398
|
if (COMMON_READ_TOOL_NAMES.has(name) || name === readSpecTool.definition.name) {
|
|
4202
4399
|
return executeTool(name, _input, context);
|
|
4203
4400
|
}
|
|
@@ -4219,14 +4416,16 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4219
4416
|
try {
|
|
4220
4417
|
const parsed = JSON.parse(result2);
|
|
4221
4418
|
const screenshotSteps = (parsed.steps || []).filter(
|
|
4222
|
-
(s) => s.command
|
|
4419
|
+
(s) => CAPTURE_COMMANDS.has(s.command) && s.result?.url
|
|
4223
4420
|
);
|
|
4224
4421
|
if (screenshotSteps.length > 0) {
|
|
4225
|
-
const
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4422
|
+
for (const step of screenshotSteps) {
|
|
4423
|
+
const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
|
|
4424
|
+
lastCapture[kind] = {
|
|
4425
|
+
url: step.result.url,
|
|
4426
|
+
styleMap: step.result.styleMap
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4230
4429
|
const visionOverride = {
|
|
4231
4430
|
model: resolveModel(
|
|
4232
4431
|
"imageAnalysis",
|
|
@@ -4250,15 +4449,14 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4250
4449
|
);
|
|
4251
4450
|
try {
|
|
4252
4451
|
const analyses = JSON.parse(batchResult);
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
step.result.analysis = analyses[ai]?.output?.analysis || analyses[ai]?.output || "";
|
|
4257
|
-
ai++;
|
|
4452
|
+
screenshotSteps.forEach((step, i) => {
|
|
4453
|
+
if (i >= analyses.length) {
|
|
4454
|
+
return;
|
|
4258
4455
|
}
|
|
4259
|
-
|
|
4456
|
+
step.result.analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
|
|
4457
|
+
});
|
|
4260
4458
|
} catch {
|
|
4261
|
-
|
|
4459
|
+
log8.debug("Failed to parse batch analysis result", {
|
|
4262
4460
|
batchResult
|
|
4263
4461
|
});
|
|
4264
4462
|
}
|
|
@@ -4269,13 +4467,10 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4269
4467
|
}
|
|
4270
4468
|
return result2;
|
|
4271
4469
|
},
|
|
4272
|
-
toolRegistry: context.toolRegistry
|
|
4273
|
-
captureArtifacts: ["screenshotFullPage"]
|
|
4470
|
+
toolRegistry: context.toolRegistry
|
|
4274
4471
|
});
|
|
4275
4472
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
4276
|
-
const
|
|
4277
|
-
const viewport = lastBrowserCommandViewport;
|
|
4278
|
-
const preferred = opts?.capture === "viewport" ? viewport ?? fullPage : fullPage ?? viewport;
|
|
4473
|
+
const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
|
|
4279
4474
|
return {
|
|
4280
4475
|
text: result.text,
|
|
4281
4476
|
...preferred?.url ? { screenshot: { url: preferred.url, styleMap: preferred.styleMap } } : {}
|
|
@@ -4285,8 +4480,8 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4285
4480
|
}
|
|
4286
4481
|
}
|
|
4287
4482
|
var browserAutomationTool = {
|
|
4288
|
-
clearable: true,
|
|
4289
4483
|
definition: {
|
|
4484
|
+
clearable: true,
|
|
4290
4485
|
name: "runAutomatedBrowserTest",
|
|
4291
4486
|
description: "Run an automated browser test against the live preview. Describe what to test \u2014 the agent figures out how. Use after meaningful changes to frontend code, to reproduce user-reported issues, or to test end-to-end flows. Never give it explicit values to use when filling out forms or creating accounts \u2014 it will use its own judgement (often it needs specific values to trigger dev-mode bypasses of things like login verification codes).",
|
|
4292
4487
|
inputSchema: {
|
|
@@ -4315,98 +4510,104 @@ var browserAutomationTool = {
|
|
|
4315
4510
|
};
|
|
4316
4511
|
|
|
4317
4512
|
// src/tools/code/screenshot.ts
|
|
4318
|
-
var
|
|
4513
|
+
var screenshotDefinition = {
|
|
4319
4514
|
clearable: true,
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
description: "true = full-height capture of the entire page; false = just the visible viewport. Pick based on whether you need the whole page or a specific section."
|
|
4329
|
-
},
|
|
4330
|
-
prompt: {
|
|
4331
|
-
type: "string",
|
|
4332
|
-
description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
|
|
4333
|
-
},
|
|
4334
|
-
imageUrl: {
|
|
4335
|
-
type: "string",
|
|
4336
|
-
description: "URL of an existing screenshot to analyze instead of capturing a new one. Use this for additional questions about a previous screenshot."
|
|
4337
|
-
},
|
|
4338
|
-
path: {
|
|
4339
|
-
type: "string",
|
|
4340
|
-
description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
|
|
4341
|
-
},
|
|
4342
|
-
width: {
|
|
4343
|
-
type: "number",
|
|
4344
|
-
description: "Exact capture width in pixels. Set together with `height` to render a fixed-size image; clips to exactly this viewport instead of the default preview size."
|
|
4345
|
-
},
|
|
4346
|
-
height: {
|
|
4347
|
-
type: "number",
|
|
4348
|
-
description: "Exact capture height in pixels. Set together with `width`."
|
|
4349
|
-
},
|
|
4350
|
-
format: {
|
|
4351
|
-
type: "string",
|
|
4352
|
-
enum: ["png", "jpeg"],
|
|
4353
|
-
description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
|
|
4354
|
-
},
|
|
4355
|
-
instructions: {
|
|
4356
|
-
type: "string",
|
|
4357
|
-
description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, navigating a flow, scrolling to a section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start its test at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. Use only when interaction is required to *reach* the state you want to capture \u2014 log in, dismiss a modal, switch a tab, follow a route, scroll to a section. If your steps are exercising the app's functionality across multiple states (running flows, asserting behavior under interaction, multi-step QA), use `runAutomatedBrowserTest` instead."
|
|
4358
|
-
}
|
|
4515
|
+
name: "screenshot",
|
|
4516
|
+
description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
|
|
4517
|
+
inputSchema: {
|
|
4518
|
+
type: "object",
|
|
4519
|
+
properties: {
|
|
4520
|
+
fullPage: {
|
|
4521
|
+
type: "boolean",
|
|
4522
|
+
description: "true = full-height capture of the entire page; false = just the visible viewport. Pick based on whether you need the whole page or a specific section."
|
|
4359
4523
|
},
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
model: resolveModel("imageAnalysis", context?.models, context?.model)
|
|
4389
|
-
});
|
|
4524
|
+
prompt: {
|
|
4525
|
+
type: "string",
|
|
4526
|
+
description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
|
|
4527
|
+
},
|
|
4528
|
+
imageUrl: {
|
|
4529
|
+
type: "string",
|
|
4530
|
+
description: "An existing image to analyze instead of capturing a new one \u2014 the URL of a previous screenshot (for follow-up questions about it), or the path of an image file on disk such as a user upload. Local files are hosted automatically and the returned URL can be reused."
|
|
4531
|
+
},
|
|
4532
|
+
path: {
|
|
4533
|
+
type: "string",
|
|
4534
|
+
description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
|
|
4535
|
+
},
|
|
4536
|
+
width: {
|
|
4537
|
+
type: "number",
|
|
4538
|
+
description: "Exact capture width in pixels. Set together with `height` to render a fixed-size image; clips to exactly this viewport instead of the default preview size."
|
|
4539
|
+
},
|
|
4540
|
+
height: {
|
|
4541
|
+
type: "number",
|
|
4542
|
+
description: "Exact capture height in pixels. Set together with `width`."
|
|
4543
|
+
},
|
|
4544
|
+
format: {
|
|
4545
|
+
type: "string",
|
|
4546
|
+
enum: ["png", "jpeg"],
|
|
4547
|
+
description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
|
|
4548
|
+
},
|
|
4549
|
+
instructions: {
|
|
4550
|
+
type: "string",
|
|
4551
|
+
description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, navigating a flow, scrolling to a section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start its test at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. Use only when interaction is required to *reach* the state you want to capture \u2014 log in, dismiss a modal, switch a tab, follow a route, scroll to a section. If your steps are exercising the app's functionality across multiple states (running flows, asserting behavior under interaction, multi-step QA), use `runAutomatedBrowserTest` instead."
|
|
4390
4552
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4553
|
+
},
|
|
4554
|
+
required: ["fullPage"]
|
|
4555
|
+
}
|
|
4556
|
+
};
|
|
4557
|
+
async function executeScreenshot(input, onLog, context) {
|
|
4558
|
+
const fullPage = input.fullPage === true;
|
|
4559
|
+
const model = resolveModel("imageAnalysis", context?.models, context?.model);
|
|
4560
|
+
try {
|
|
4561
|
+
if (input.imageUrl) {
|
|
4562
|
+
return await captureAndAnalyzeScreenshot({
|
|
4563
|
+
prompt: input.prompt,
|
|
4564
|
+
image: input.imageUrl,
|
|
4565
|
+
onLog,
|
|
4566
|
+
model,
|
|
4567
|
+
apiConfig: context?.apiConfig
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
if (input.instructions && context) {
|
|
4571
|
+
const shotKind = fullPage ? "full-page" : "viewport";
|
|
4572
|
+
const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
|
|
4573
|
+
const result = await runBrowserAutomation(task, context, {
|
|
4574
|
+
capture: fullPage ? "fullPage" : "viewport"
|
|
4575
|
+
});
|
|
4576
|
+
if (!result.screenshot) {
|
|
4577
|
+
return result.text;
|
|
4405
4578
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4579
|
+
return await streamScreenshotAnalysis({
|
|
4580
|
+
image: result.screenshot.url,
|
|
4581
|
+
prompt: input.prompt,
|
|
4582
|
+
styleMap: result.screenshot.styleMap,
|
|
4583
|
+
onLog,
|
|
4584
|
+
model,
|
|
4585
|
+
apiConfig: context?.apiConfig
|
|
4586
|
+
});
|
|
4408
4587
|
}
|
|
4588
|
+
const release = await acquireBrowserLock();
|
|
4589
|
+
try {
|
|
4590
|
+
return await captureAndAnalyzeScreenshot({
|
|
4591
|
+
prompt: input.prompt,
|
|
4592
|
+
path: input.path,
|
|
4593
|
+
fullPage,
|
|
4594
|
+
width: input.width,
|
|
4595
|
+
height: input.height,
|
|
4596
|
+
format: input.format,
|
|
4597
|
+
onLog,
|
|
4598
|
+
model,
|
|
4599
|
+
apiConfig: context?.apiConfig
|
|
4600
|
+
});
|
|
4601
|
+
} finally {
|
|
4602
|
+
release();
|
|
4603
|
+
}
|
|
4604
|
+
} catch (err) {
|
|
4605
|
+
return `Error taking screenshot: ${err.message}`;
|
|
4409
4606
|
}
|
|
4607
|
+
}
|
|
4608
|
+
var screenshotTool = {
|
|
4609
|
+
definition: screenshotDefinition,
|
|
4610
|
+
execute: (input, context) => executeScreenshot(input, context?.onLog, context)
|
|
4410
4611
|
};
|
|
4411
4612
|
|
|
4412
4613
|
// src/subagents/designExpert/tools/searchGoogle.ts
|
|
@@ -4509,13 +4710,13 @@ Respond only with your analysis as Markdown and absolutely no other text. Do not
|
|
|
4509
4710
|
var definition3 = {
|
|
4510
4711
|
clearable: false,
|
|
4511
4712
|
name: "analyzeDesign",
|
|
4512
|
-
description: "Analyze the visual design of a website or image
|
|
4713
|
+
description: "Analyze the visual design of a website, an image URL, or an image file on disk. Websites are automatically screenshotted first. Provides static image analysis only, will not capture animations or video. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
|
|
4513
4714
|
inputSchema: {
|
|
4514
4715
|
type: "object",
|
|
4515
4716
|
properties: {
|
|
4516
4717
|
url: {
|
|
4517
4718
|
type: "string",
|
|
4518
|
-
description: "
|
|
4719
|
+
description: "What to analyze: a website URL (will be screenshotted), an image URL, or the path of an image file on disk (e.g. a reference the user uploaded, under src/.user-uploads/). Local files are hosted automatically and their URL comes back in the result."
|
|
4519
4720
|
},
|
|
4520
4721
|
prompt: {
|
|
4521
4722
|
type: "string",
|
|
@@ -4528,9 +4729,9 @@ var definition3 = {
|
|
|
4528
4729
|
async function execute3(input, onLog, context) {
|
|
4529
4730
|
const url = input.url;
|
|
4530
4731
|
const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
|
|
4531
|
-
const
|
|
4532
|
-
let
|
|
4533
|
-
if (!
|
|
4732
|
+
const isImage = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
|
|
4733
|
+
let image = url;
|
|
4734
|
+
if (!isImage) {
|
|
4534
4735
|
const ss = await runMindstudioCliResult(
|
|
4535
4736
|
[
|
|
4536
4737
|
"screenshot-url",
|
|
@@ -4553,15 +4754,16 @@ async function execute3(input, onLog, context) {
|
|
|
4553
4754
|
if (!ss.ok) {
|
|
4554
4755
|
return `Could not screenshot ${url}: ${ss.value}`;
|
|
4555
4756
|
}
|
|
4556
|
-
|
|
4757
|
+
image = ss.value;
|
|
4557
4758
|
}
|
|
4558
|
-
const
|
|
4759
|
+
const analyzed = await analyzeImage({
|
|
4559
4760
|
prompt: analysisPrompt,
|
|
4560
|
-
|
|
4761
|
+
image,
|
|
4762
|
+
apiConfig: context?.apiConfig,
|
|
4561
4763
|
onLog,
|
|
4562
4764
|
model: resolveModel("imageAnalysis", context?.models, context?.model)
|
|
4563
4765
|
});
|
|
4564
|
-
return JSON.stringify({ url:
|
|
4766
|
+
return JSON.stringify({ url: analyzed.url, analysis: analyzed.analysis });
|
|
4565
4767
|
}
|
|
4566
4768
|
|
|
4567
4769
|
// src/subagents/designExpert/tools/analyzeImage.ts
|
|
@@ -4573,13 +4775,16 @@ __export(analyzeImage_exports, {
|
|
|
4573
4775
|
var definition4 = {
|
|
4574
4776
|
clearable: true,
|
|
4575
4777
|
name: "analyzeImage",
|
|
4576
|
-
description: "Analyze an image
|
|
4778
|
+
description: "Analyze an image using a vision model. Provides static image analysis only, will not capture animations or video. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
|
|
4577
4779
|
inputSchema: {
|
|
4578
4780
|
type: "object",
|
|
4579
4781
|
properties: {
|
|
4782
|
+
// Still `imageUrl` even though a path works: the builder renders this
|
|
4783
|
+
// tool's thumbnail from `input.imageUrl` and deploys separately, so
|
|
4784
|
+
// renaming the property would blank the view under version skew.
|
|
4580
4785
|
imageUrl: {
|
|
4581
4786
|
type: "string",
|
|
4582
|
-
description: "The image URL
|
|
4787
|
+
description: "The image to analyze: either a URL, or the path of an image file on disk (e.g. a reference screenshot the user uploaded, under src/.user-uploads/). Local files are hosted automatically; the URL comes back in the result and can be reused for follow-up questions or embedded in a spec."
|
|
4583
4788
|
},
|
|
4584
4789
|
prompt: {
|
|
4585
4790
|
type: "string",
|
|
@@ -4590,96 +4795,24 @@ var definition4 = {
|
|
|
4590
4795
|
}
|
|
4591
4796
|
};
|
|
4592
4797
|
async function execute4(input, onLog, context) {
|
|
4593
|
-
const imageUrl = input.imageUrl;
|
|
4594
4798
|
const prompt = buildScreenshotAnalysisPrompt({
|
|
4595
4799
|
prompt: input.prompt
|
|
4596
4800
|
});
|
|
4597
|
-
const analysis = await analyzeImage({
|
|
4801
|
+
const { url, analysis } = await analyzeImage({
|
|
4598
4802
|
prompt,
|
|
4599
|
-
imageUrl,
|
|
4803
|
+
image: input.imageUrl,
|
|
4804
|
+
apiConfig: context?.apiConfig,
|
|
4600
4805
|
onLog,
|
|
4601
4806
|
model: resolveModel("imageAnalysis", context?.models, context?.model)
|
|
4602
4807
|
});
|
|
4603
|
-
return JSON.stringify({ url
|
|
4604
|
-
}
|
|
4605
|
-
|
|
4606
|
-
// src/subagents/designExpert/tools/screenshot.ts
|
|
4607
|
-
var screenshot_exports = {};
|
|
4608
|
-
__export(screenshot_exports, {
|
|
4609
|
-
definition: () => definition5,
|
|
4610
|
-
execute: () => execute5
|
|
4611
|
-
});
|
|
4612
|
-
var definition5 = {
|
|
4613
|
-
clearable: true,
|
|
4614
|
-
name: "screenshot",
|
|
4615
|
-
description: "Capture a screenshot of the current app preview and get it back with visual analysis. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 use it to review a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 use it to review overall composition or a layout you can't see in one screen). Use to review the current state of the UI being built. Remember, the screenshot analysis is not overly precise - for example, it cannot reliably identify specific fonts by name \u2014 it can only describe what letterforms look like.",
|
|
4616
|
-
inputSchema: {
|
|
4617
|
-
type: "object",
|
|
4618
|
-
properties: {
|
|
4619
|
-
fullPage: {
|
|
4620
|
-
type: "boolean",
|
|
4621
|
-
description: "true = full-height capture of the entire page; false = just the visible viewport. Pick based on whether you need the whole page or a specific section."
|
|
4622
|
-
},
|
|
4623
|
-
prompt: {
|
|
4624
|
-
type: "string",
|
|
4625
|
-
description: "Optional specific question about the screenshot. Use a bulleted list to ask many questions at once."
|
|
4626
|
-
},
|
|
4627
|
-
path: {
|
|
4628
|
-
type: "string",
|
|
4629
|
-
description: 'Navigate to this path before capturing (e.g. "/settings"). If omitted, screenshots the current page.'
|
|
4630
|
-
},
|
|
4631
|
-
instructions: {
|
|
4632
|
-
type: "string",
|
|
4633
|
-
description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, scrolling to a specific section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing."
|
|
4634
|
-
}
|
|
4635
|
-
},
|
|
4636
|
-
required: ["fullPage"]
|
|
4637
|
-
}
|
|
4638
|
-
};
|
|
4639
|
-
async function execute5(input, onLog, context) {
|
|
4640
|
-
const fullPage = input.fullPage === true;
|
|
4641
|
-
const shotKind = fullPage ? "full-page" : "viewport";
|
|
4642
|
-
if (input.instructions && context) {
|
|
4643
|
-
try {
|
|
4644
|
-
const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
|
|
4645
|
-
const result = await runBrowserAutomation(task, context, {
|
|
4646
|
-
capture: fullPage ? "fullPage" : "viewport"
|
|
4647
|
-
});
|
|
4648
|
-
if (!result.screenshot) {
|
|
4649
|
-
return result.text;
|
|
4650
|
-
}
|
|
4651
|
-
return await streamScreenshotAnalysis({
|
|
4652
|
-
url: result.screenshot.url,
|
|
4653
|
-
prompt: input.prompt,
|
|
4654
|
-
styleMap: result.screenshot.styleMap,
|
|
4655
|
-
onLog,
|
|
4656
|
-
model: resolveModel("imageAnalysis", context?.models, context?.model)
|
|
4657
|
-
});
|
|
4658
|
-
} catch (err) {
|
|
4659
|
-
return `Error taking interactive screenshot: ${err.message}`;
|
|
4660
|
-
}
|
|
4661
|
-
}
|
|
4662
|
-
const release = await acquireBrowserLock();
|
|
4663
|
-
try {
|
|
4664
|
-
return await captureAndAnalyzeScreenshot({
|
|
4665
|
-
prompt: input.prompt,
|
|
4666
|
-
path: input.path,
|
|
4667
|
-
fullPage,
|
|
4668
|
-
onLog,
|
|
4669
|
-
model: resolveModel("imageAnalysis", context?.models, context?.model)
|
|
4670
|
-
});
|
|
4671
|
-
} catch (err) {
|
|
4672
|
-
return `Error taking screenshot: ${err.message}`;
|
|
4673
|
-
} finally {
|
|
4674
|
-
release();
|
|
4675
|
-
}
|
|
4808
|
+
return JSON.stringify({ url, analysis });
|
|
4676
4809
|
}
|
|
4677
4810
|
|
|
4678
4811
|
// src/subagents/designExpert/tools/images/generateImages.ts
|
|
4679
4812
|
var generateImages_exports = {};
|
|
4680
4813
|
__export(generateImages_exports, {
|
|
4681
|
-
definition: () =>
|
|
4682
|
-
execute: () =>
|
|
4814
|
+
definition: () => definition5,
|
|
4815
|
+
execute: () => execute5
|
|
4683
4816
|
});
|
|
4684
4817
|
|
|
4685
4818
|
// src/subagents/designExpert/tools/images/enhancePrompt.ts
|
|
@@ -4741,14 +4874,15 @@ var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcin
|
|
|
4741
4874
|
async function generateImageAssets(opts) {
|
|
4742
4875
|
const {
|
|
4743
4876
|
prompts,
|
|
4744
|
-
sourceImages,
|
|
4745
4877
|
transparentBackground,
|
|
4746
4878
|
enhancePrompts,
|
|
4747
4879
|
onLog,
|
|
4880
|
+
apiConfig,
|
|
4748
4881
|
imageGenerationModel: genModel,
|
|
4749
4882
|
imageAnalysisModel,
|
|
4750
4883
|
imagePromptEnhancerModel
|
|
4751
4884
|
} = opts;
|
|
4885
|
+
const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
|
|
4752
4886
|
const width = opts.width || 2048;
|
|
4753
4887
|
const height = opts.height || 2048;
|
|
4754
4888
|
const config = { width, height };
|
|
@@ -4854,10 +4988,10 @@ async function generateImageAssets(opts) {
|
|
|
4854
4988
|
}
|
|
4855
4989
|
const analysis = await analyzeImage({
|
|
4856
4990
|
prompt: ANALYZE_PROMPT,
|
|
4857
|
-
|
|
4991
|
+
image: url,
|
|
4858
4992
|
onLog,
|
|
4859
4993
|
model: imageAnalysisModel
|
|
4860
|
-
});
|
|
4994
|
+
}).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
|
|
4861
4995
|
return {
|
|
4862
4996
|
url,
|
|
4863
4997
|
prompt: prompts[i],
|
|
@@ -4872,7 +5006,7 @@ async function generateImageAssets(opts) {
|
|
|
4872
5006
|
}
|
|
4873
5007
|
|
|
4874
5008
|
// src/subagents/designExpert/tools/images/generateImages.ts
|
|
4875
|
-
var
|
|
5009
|
+
var definition5 = {
|
|
4876
5010
|
clearable: false,
|
|
4877
5011
|
name: "generateImages",
|
|
4878
5012
|
description: "Generate images. Returns CDN URLs with a quality analysis for each image. Produces high-quality results for everything from photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
|
|
@@ -4888,7 +5022,7 @@ var definition6 = {
|
|
|
4888
5022
|
},
|
|
4889
5023
|
referenceImage: {
|
|
4890
5024
|
type: "string",
|
|
4891
|
-
description: "Optional
|
|
5025
|
+
description: "Optional single reference image to guide the generation \u2014 for style, subject, character consistency, or composition. Either a URL or the path of an image file on disk (e.g. a user upload under src/.user-uploads/). Your prompt still describes the desired result; the reference conditions it. Applies to every prompt in the batch."
|
|
4892
5026
|
},
|
|
4893
5027
|
width: {
|
|
4894
5028
|
type: "number",
|
|
@@ -4906,7 +5040,7 @@ var definition6 = {
|
|
|
4906
5040
|
required: ["prompts"]
|
|
4907
5041
|
}
|
|
4908
5042
|
};
|
|
4909
|
-
async function
|
|
5043
|
+
async function execute5(input, onLog, context) {
|
|
4910
5044
|
return generateImageAssets({
|
|
4911
5045
|
prompts: input.prompts,
|
|
4912
5046
|
width: input.width,
|
|
@@ -4915,6 +5049,7 @@ async function execute6(input, onLog, context) {
|
|
|
4915
5049
|
sourceImages: input.referenceImage ? [input.referenceImage] : void 0,
|
|
4916
5050
|
enhancePrompts: true,
|
|
4917
5051
|
onLog,
|
|
5052
|
+
apiConfig: context?.apiConfig,
|
|
4918
5053
|
imageGenerationModel: resolveModel(
|
|
4919
5054
|
"imageGeneration",
|
|
4920
5055
|
context?.models,
|
|
@@ -4936,10 +5071,10 @@ async function execute6(input, onLog, context) {
|
|
|
4936
5071
|
// src/subagents/designExpert/tools/images/editImages.ts
|
|
4937
5072
|
var editImages_exports = {};
|
|
4938
5073
|
__export(editImages_exports, {
|
|
4939
|
-
definition: () =>
|
|
4940
|
-
execute: () =>
|
|
5074
|
+
definition: () => definition6,
|
|
5075
|
+
execute: () => execute6
|
|
4941
5076
|
});
|
|
4942
|
-
var
|
|
5077
|
+
var definition6 = {
|
|
4943
5078
|
clearable: false,
|
|
4944
5079
|
name: "editImages",
|
|
4945
5080
|
description: "Edit or transform existing images. Provide one or more source image URLs as reference and a prompt describing the desired edit. Use for compositing, style transfer, subject transformation, blending multiple references, or incorporating one or more references into something new. Returns CDN URLs with analysis.",
|
|
@@ -4958,7 +5093,7 @@ var definition7 = {
|
|
|
4958
5093
|
items: {
|
|
4959
5094
|
type: "string"
|
|
4960
5095
|
},
|
|
4961
|
-
description: "One or more source/reference image
|
|
5096
|
+
description: "One or more source/reference images, each a URL or the path of an image file on disk (e.g. a user upload under src/.user-uploads/). These are used as the basis for the edit \u2014 the AI will use them as reference for style, subject, or composition."
|
|
4962
5097
|
},
|
|
4963
5098
|
width: {
|
|
4964
5099
|
type: "number",
|
|
@@ -4976,7 +5111,7 @@ var definition7 = {
|
|
|
4976
5111
|
required: ["prompts", "sourceImages"]
|
|
4977
5112
|
}
|
|
4978
5113
|
};
|
|
4979
|
-
async function
|
|
5114
|
+
async function execute6(input, onLog, context) {
|
|
4980
5115
|
return generateImageAssets({
|
|
4981
5116
|
prompts: input.prompts,
|
|
4982
5117
|
sourceImages: input.sourceImages,
|
|
@@ -4985,6 +5120,7 @@ async function execute7(input, onLog, context) {
|
|
|
4985
5120
|
transparentBackground: input.transparentBackground,
|
|
4986
5121
|
enhancePrompts: false,
|
|
4987
5122
|
onLog,
|
|
5123
|
+
apiConfig: context?.apiConfig,
|
|
4988
5124
|
imageGenerationModel: resolveModel(
|
|
4989
5125
|
"imageGeneration",
|
|
4990
5126
|
context?.models,
|
|
@@ -5006,8 +5142,8 @@ async function execute7(input, onLog, context) {
|
|
|
5006
5142
|
// src/subagents/designExpert/tools/polishCopy.ts
|
|
5007
5143
|
var polishCopy_exports = {};
|
|
5008
5144
|
__export(polishCopy_exports, {
|
|
5009
|
-
definition: () =>
|
|
5010
|
-
execute: () =>
|
|
5145
|
+
definition: () => definition7,
|
|
5146
|
+
execute: () => execute7
|
|
5011
5147
|
});
|
|
5012
5148
|
|
|
5013
5149
|
// src/subagents/copyEditor/tools.ts
|
|
@@ -5016,8 +5152,8 @@ var COPY_EDITOR_TOOLS = [...COMMON_READ_TOOLS];
|
|
|
5016
5152
|
// src/subagents/copyEditor/index.ts
|
|
5017
5153
|
var BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
|
|
5018
5154
|
var copyEditorTool = {
|
|
5019
|
-
clearable: false,
|
|
5020
5155
|
definition: {
|
|
5156
|
+
clearable: false,
|
|
5021
5157
|
name: "copyEditor",
|
|
5022
5158
|
description: "Hand it user-facing copy and it hands back a sharper version \u2014 better structured for its audience and free of the overused words, telltale constructions, and rhythms that make text read as AI-generated. Think of it as a design expert for words: it elevates how the copy communicates and strips the AI fingerprints, but it never invents facts or claims you didn't give it. Use it on anything users will read: in-app strings, empty states, errors, the Build Overview, deck copy, launch posts, Slack announcements. Readonly.",
|
|
5023
5159
|
inputSchema: {
|
|
@@ -5064,7 +5200,7 @@ var copyEditorTool = {
|
|
|
5064
5200
|
};
|
|
5065
5201
|
|
|
5066
5202
|
// src/subagents/designExpert/tools/polishCopy.ts
|
|
5067
|
-
var
|
|
5203
|
+
var definition7 = {
|
|
5068
5204
|
clearable: false,
|
|
5069
5205
|
name: "polishCopy",
|
|
5070
5206
|
description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
|
|
@@ -5079,7 +5215,7 @@ var definition8 = {
|
|
|
5079
5215
|
required: ["task"]
|
|
5080
5216
|
}
|
|
5081
5217
|
};
|
|
5082
|
-
async function
|
|
5218
|
+
async function execute7(input, _onLog, context) {
|
|
5083
5219
|
return copyEditorTool.execute(input, context);
|
|
5084
5220
|
}
|
|
5085
5221
|
|
|
@@ -5089,7 +5225,10 @@ var tools = {
|
|
|
5089
5225
|
scrapeWebUrl: scrapeWebUrl_exports,
|
|
5090
5226
|
analyzeDesign: analyzeDesign_exports,
|
|
5091
5227
|
analyzeImage: analyzeImage_exports,
|
|
5092
|
-
|
|
5228
|
+
// Same tool the main agent offers, imported rather than reimplemented — the
|
|
5229
|
+
// two used to be near-identical copies and had already drifted apart. Its core
|
|
5230
|
+
// already takes (input, onLog, context), which is this registry's convention.
|
|
5231
|
+
screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
|
|
5093
5232
|
generateImages: generateImages_exports,
|
|
5094
5233
|
editImages: editImages_exports,
|
|
5095
5234
|
polishCopy: polishCopy_exports
|
|
@@ -5108,7 +5247,7 @@ async function executeDesignExpertTool(name, input, context, toolCallId, onLog)
|
|
|
5108
5247
|
}
|
|
5109
5248
|
|
|
5110
5249
|
// src/subagents/designExpert/data/sampleCache.ts
|
|
5111
|
-
import
|
|
5250
|
+
import fs18 from "fs";
|
|
5112
5251
|
var SAMPLE_FILE = ".remy-design-sample.json";
|
|
5113
5252
|
var cached2 = null;
|
|
5114
5253
|
function generateIndices(poolSize, sampleSize) {
|
|
@@ -5122,14 +5261,14 @@ function generateIndices(poolSize, sampleSize) {
|
|
|
5122
5261
|
}
|
|
5123
5262
|
function load() {
|
|
5124
5263
|
try {
|
|
5125
|
-
return JSON.parse(
|
|
5264
|
+
return JSON.parse(fs18.readFileSync(SAMPLE_FILE, "utf-8"));
|
|
5126
5265
|
} catch {
|
|
5127
5266
|
return null;
|
|
5128
5267
|
}
|
|
5129
5268
|
}
|
|
5130
5269
|
function save(indices) {
|
|
5131
5270
|
try {
|
|
5132
|
-
|
|
5271
|
+
fs18.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
|
|
5133
5272
|
} catch {
|
|
5134
5273
|
}
|
|
5135
5274
|
}
|
|
@@ -5413,8 +5552,8 @@ async function runDesignExpert(opts, context) {
|
|
|
5413
5552
|
});
|
|
5414
5553
|
}
|
|
5415
5554
|
var designExpertTool = {
|
|
5416
|
-
clearable: false,
|
|
5417
5555
|
definition: {
|
|
5556
|
+
clearable: false,
|
|
5418
5557
|
name: "visualDesignExpert",
|
|
5419
5558
|
description: DESCRIPTION,
|
|
5420
5559
|
inputSchema: {
|
|
@@ -5503,28 +5642,28 @@ var VISION_TOOLS = [
|
|
|
5503
5642
|
];
|
|
5504
5643
|
|
|
5505
5644
|
// src/subagents/productVision/executor.ts
|
|
5506
|
-
import
|
|
5507
|
-
import
|
|
5645
|
+
import fs19 from "fs";
|
|
5646
|
+
import path11 from "path";
|
|
5508
5647
|
var ROADMAP_DIR = "src/roadmap";
|
|
5509
5648
|
var PITCH_DECK_SHELL = readAsset(
|
|
5510
5649
|
"subagents/productVision",
|
|
5511
5650
|
"pitch-deck-shell.html"
|
|
5512
5651
|
);
|
|
5513
|
-
function
|
|
5514
|
-
return
|
|
5652
|
+
function resolve2(filePath) {
|
|
5653
|
+
return path11.join(ROADMAP_DIR, filePath);
|
|
5515
5654
|
}
|
|
5516
5655
|
async function executeVisionTool(name, input, context) {
|
|
5517
5656
|
switch (name) {
|
|
5518
5657
|
case "writeFile": {
|
|
5519
|
-
const filePath =
|
|
5658
|
+
const filePath = resolve2(input.path);
|
|
5520
5659
|
try {
|
|
5521
|
-
|
|
5660
|
+
fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
|
|
5522
5661
|
let oldContent = null;
|
|
5523
5662
|
try {
|
|
5524
|
-
oldContent =
|
|
5663
|
+
oldContent = fs19.readFileSync(filePath, "utf-8");
|
|
5525
5664
|
} catch {
|
|
5526
5665
|
}
|
|
5527
|
-
|
|
5666
|
+
fs19.writeFileSync(filePath, input.content, "utf-8");
|
|
5528
5667
|
const lineCount = input.content.split("\n").length;
|
|
5529
5668
|
const label = oldContent !== null ? "Wrote" : "Created";
|
|
5530
5669
|
return `${label} ${filePath} (${lineCount} lines)
|
|
@@ -5534,13 +5673,13 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
|
|
|
5534
5673
|
}
|
|
5535
5674
|
}
|
|
5536
5675
|
case "deleteFile": {
|
|
5537
|
-
const filePath =
|
|
5676
|
+
const filePath = resolve2(input.path);
|
|
5538
5677
|
try {
|
|
5539
|
-
if (!
|
|
5678
|
+
if (!fs19.existsSync(filePath)) {
|
|
5540
5679
|
return `Error: ${filePath} does not exist`;
|
|
5541
5680
|
}
|
|
5542
|
-
const oldContent =
|
|
5543
|
-
|
|
5681
|
+
const oldContent = fs19.readFileSync(filePath, "utf-8");
|
|
5682
|
+
fs19.unlinkSync(filePath);
|
|
5544
5683
|
return `Deleted ${filePath}
|
|
5545
5684
|
${unifiedDiff(filePath, oldContent, "")}`;
|
|
5546
5685
|
} catch (err) {
|
|
@@ -5551,11 +5690,11 @@ ${unifiedDiff(filePath, oldContent, "")}`;
|
|
|
5551
5690
|
if (!context) {
|
|
5552
5691
|
return "Error: writePitchDeck requires execution context for design expert delegation";
|
|
5553
5692
|
}
|
|
5554
|
-
const filePath =
|
|
5693
|
+
const filePath = resolve2("pitch.html");
|
|
5555
5694
|
try {
|
|
5556
|
-
|
|
5557
|
-
const exists =
|
|
5558
|
-
const before = exists ?
|
|
5695
|
+
fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
|
|
5696
|
+
const exists = fs19.existsSync(filePath);
|
|
5697
|
+
const before = exists ? fs19.statSync(filePath).mtimeMs : null;
|
|
5559
5698
|
const delivery = exists ? `### Your deliverable
|
|
5560
5699
|
The pitch deck already exists at \`${filePath}\`. Read it, then update it for the new <pitch_content>, keeping the presentation scaffolding intact \u2014 change only what needs to change.
|
|
5561
5700
|
|
|
@@ -5585,11 +5724,11 @@ Maintain the bones of the presentation scaffolding. Always keep the progress bar
|
|
|
5585
5724
|
${delivery}`;
|
|
5586
5725
|
const result = await runDesignExpertRender({ task }, context);
|
|
5587
5726
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
5588
|
-
if (!
|
|
5727
|
+
if (!fs19.existsSync(filePath)) {
|
|
5589
5728
|
return `Error: the design expert did not write ${filePath}. Its reply was:
|
|
5590
5729
|
${result.text}`;
|
|
5591
5730
|
}
|
|
5592
|
-
if (before !== null &&
|
|
5731
|
+
if (before !== null && fs19.statSync(filePath).mtimeMs === before) {
|
|
5593
5732
|
return `Error: the pitch deck at ${filePath} was not modified. The design expert's reply was:
|
|
5594
5733
|
${result.text}`;
|
|
5595
5734
|
}
|
|
@@ -5621,8 +5760,8 @@ function getProductVisionPrompt() {
|
|
|
5621
5760
|
|
|
5622
5761
|
// src/subagents/productVision/index.ts
|
|
5623
5762
|
var productVisionTool = {
|
|
5624
|
-
clearable: false,
|
|
5625
5763
|
definition: {
|
|
5764
|
+
clearable: false,
|
|
5626
5765
|
name: "productVision",
|
|
5627
5766
|
description: "Owns the product roadmap. Reads spec and roadmap files automatically. Creates, updates, and deletes roadmap items in src/roadmap/. Describe the situation and what needs to happen.",
|
|
5628
5767
|
inputSchema: {
|
|
@@ -5734,8 +5873,8 @@ var SANITY_CHECK_TOOLS = [
|
|
|
5734
5873
|
// src/subagents/codeSanityCheck/index.ts
|
|
5735
5874
|
var BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
|
|
5736
5875
|
var codeSanityCheckTool = {
|
|
5737
|
-
clearable: false,
|
|
5738
5876
|
definition: {
|
|
5877
|
+
clearable: false,
|
|
5739
5878
|
name: "codeSanityCheck",
|
|
5740
5879
|
description: 'Quick sanity check on an approach before building. Reviews architecture, package choices, and flags potential issues. Usually responds with "looks good." Occasionally catches something important. Readonly \u2014 can search the web and read code but cannot modify anything.',
|
|
5741
5880
|
inputSchema: {
|
|
@@ -5810,9 +5949,9 @@ ${readAsset(
|
|
|
5810
5949
|
)}
|
|
5811
5950
|
</mindstudio_flavored_markdown_spec_docs>`;
|
|
5812
5951
|
var specSyncTool = {
|
|
5813
|
-
clearable: false,
|
|
5814
5952
|
backgroundOnly: true,
|
|
5815
5953
|
definition: {
|
|
5954
|
+
clearable: false,
|
|
5816
5955
|
name: "specSync",
|
|
5817
5956
|
description: "Reconcile the spec to bring it in line with code changes you have made. Provide a brief, bulleted list of what changed and why; it finds the affected spec sections and updates them to match. Always runs in the background \u2014 it returns immediately and reports back when done.",
|
|
5818
5957
|
inputSchema: {
|
|
@@ -5879,8 +6018,8 @@ var specSyncTool = {
|
|
|
5879
6018
|
|
|
5880
6019
|
// src/tools/common/scrapeWebUrl.ts
|
|
5881
6020
|
var scrapeWebUrlTool = {
|
|
5882
|
-
clearable: false,
|
|
5883
6021
|
definition: {
|
|
6022
|
+
clearable: false,
|
|
5884
6023
|
name: "scrapeWebUrl",
|
|
5885
6024
|
description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
|
|
5886
6025
|
inputSchema: {
|
|
@@ -5923,7 +6062,7 @@ var scrapeWebUrlTool = {
|
|
|
5923
6062
|
};
|
|
5924
6063
|
|
|
5925
6064
|
// src/tools/spec/writeBuildOverview.ts
|
|
5926
|
-
import
|
|
6065
|
+
import fs20 from "fs";
|
|
5927
6066
|
var OVERVIEW_FILE = "src/overview.html";
|
|
5928
6067
|
var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
|
|
5929
6068
|
|
|
@@ -5980,8 +6119,8 @@ The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update
|
|
|
5980
6119
|
Then reply with a one-line summary of what you changed.`;
|
|
5981
6120
|
}
|
|
5982
6121
|
var buildOverviewTool = {
|
|
5983
|
-
clearable: false,
|
|
5984
6122
|
definition: {
|
|
6123
|
+
clearable: false,
|
|
5985
6124
|
name: "writeBuildOverview",
|
|
5986
6125
|
description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build and refresh it after meaningful work.",
|
|
5987
6126
|
inputSchema: {
|
|
@@ -6003,7 +6142,7 @@ var buildOverviewTool = {
|
|
|
6003
6142
|
if (!content) {
|
|
6004
6143
|
return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
|
|
6005
6144
|
}
|
|
6006
|
-
const exists =
|
|
6145
|
+
const exists = fs20.existsSync(OVERVIEW_FILE);
|
|
6007
6146
|
const task = `<overview_copy>${content}</overview_copy>
|
|
6008
6147
|
|
|
6009
6148
|
${DESIGN_BRIEF}
|
|
@@ -6021,7 +6160,7 @@ ${exists ? refreshDelivery() : initialDelivery()}`;
|
|
|
6021
6160
|
}
|
|
6022
6161
|
const result = await runDesignExpertRender({ task }, context);
|
|
6023
6162
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
6024
|
-
if (!
|
|
6163
|
+
if (!fs20.existsSync(OVERVIEW_FILE)) {
|
|
6025
6164
|
return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
|
|
6026
6165
|
${result.text}`;
|
|
6027
6166
|
}
|
|
@@ -6078,11 +6217,11 @@ var ALL_TOOLS = [
|
|
|
6078
6217
|
browserAutomationTool,
|
|
6079
6218
|
// LSP
|
|
6080
6219
|
lspDiagnosticsTool,
|
|
6081
|
-
restartProcessTool
|
|
6220
|
+
restartProcessTool,
|
|
6221
|
+
// Appended rather than grouped: position is part of the cache prefix, so a
|
|
6222
|
+
// new tool goes at the end to leave every existing session's prefix intact.
|
|
6223
|
+
loadSkillTool
|
|
6082
6224
|
];
|
|
6083
|
-
var CLEARABLE_TOOLS = new Set(
|
|
6084
|
-
ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
|
|
6085
|
-
);
|
|
6086
6225
|
var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
6087
6226
|
"visualDesignExpert",
|
|
6088
6227
|
"productVision",
|
|
@@ -6107,7 +6246,7 @@ function executeTool(name, input, context) {
|
|
|
6107
6246
|
}
|
|
6108
6247
|
|
|
6109
6248
|
// src/compaction/index.ts
|
|
6110
|
-
var
|
|
6249
|
+
var log9 = createLogger("compaction");
|
|
6111
6250
|
var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
|
|
6112
6251
|
var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
|
|
6113
6252
|
var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
|
|
@@ -6156,7 +6295,7 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
6156
6295
|
if (text) {
|
|
6157
6296
|
summaries.push({ name, text });
|
|
6158
6297
|
} else {
|
|
6159
|
-
|
|
6298
|
+
log9.warn("Subagent summary unusable \u2014 leaving its history intact", {
|
|
6160
6299
|
name
|
|
6161
6300
|
});
|
|
6162
6301
|
}
|
|
@@ -6184,7 +6323,7 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
6184
6323
|
}
|
|
6185
6324
|
]
|
|
6186
6325
|
}));
|
|
6187
|
-
|
|
6326
|
+
log9.info("Compaction complete", {
|
|
6188
6327
|
summaries: summaries.length,
|
|
6189
6328
|
recentNarrativeChars: recent.length
|
|
6190
6329
|
});
|
|
@@ -6386,7 +6525,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
6386
6525
|
messagesToSummarize.slice(0, mid),
|
|
6387
6526
|
messagesToSummarize.slice(mid)
|
|
6388
6527
|
];
|
|
6389
|
-
|
|
6528
|
+
log9.info("Chunking summary", {
|
|
6390
6529
|
name,
|
|
6391
6530
|
messageCount: messagesToSummarize.length,
|
|
6392
6531
|
serializedLength: serialized.length,
|
|
@@ -6416,7 +6555,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
6416
6555
|
const parts = results.filter((p) => p !== null);
|
|
6417
6556
|
return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
|
|
6418
6557
|
}
|
|
6419
|
-
|
|
6558
|
+
log9.info("Generating summary", {
|
|
6420
6559
|
name,
|
|
6421
6560
|
messageCount: messagesToSummarize.length,
|
|
6422
6561
|
serializedLength: serialized.length
|
|
@@ -6432,10 +6571,10 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
6432
6571
|
return null;
|
|
6433
6572
|
}
|
|
6434
6573
|
if (summaryText.length >= MIN_SUMMARY_CHARS) {
|
|
6435
|
-
|
|
6574
|
+
log9.info("Summary generated", { name, summaryLength: summaryText.length });
|
|
6436
6575
|
return summaryText;
|
|
6437
6576
|
}
|
|
6438
|
-
|
|
6577
|
+
log9.warn("Summary too short to be real", {
|
|
6439
6578
|
name,
|
|
6440
6579
|
summaryLength: summaryText.length,
|
|
6441
6580
|
minimum: MIN_SUMMARY_CHARS,
|
|
@@ -6491,21 +6630,21 @@ Write the summary of the conversation above, following your instructions.`;
|
|
|
6491
6630
|
toolNames: []
|
|
6492
6631
|
});
|
|
6493
6632
|
} else if (event.type === "error") {
|
|
6494
|
-
|
|
6633
|
+
log9.error("Summary generation failed", { name, error: event.error });
|
|
6495
6634
|
return null;
|
|
6496
6635
|
}
|
|
6497
6636
|
}
|
|
6498
6637
|
if (!summaryText.trim()) {
|
|
6499
|
-
|
|
6638
|
+
log9.warn("Empty summary generated", { name });
|
|
6500
6639
|
return null;
|
|
6501
6640
|
}
|
|
6502
6641
|
return summaryText.trim();
|
|
6503
6642
|
}
|
|
6504
6643
|
|
|
6505
6644
|
// src/session.ts
|
|
6506
|
-
import
|
|
6507
|
-
import
|
|
6508
|
-
var
|
|
6645
|
+
import fs21 from "fs";
|
|
6646
|
+
import path12 from "path";
|
|
6647
|
+
var log10 = createLogger("session");
|
|
6509
6648
|
var SESSION_FILE = ".remy-session.json";
|
|
6510
6649
|
var ARCHIVE_DIR = ".logs/sessions";
|
|
6511
6650
|
var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
|
|
@@ -6522,14 +6661,14 @@ var ARCHIVE_MSG_CACHE_MAX = 3;
|
|
|
6522
6661
|
function loadSession(state) {
|
|
6523
6662
|
pruneArchives();
|
|
6524
6663
|
try {
|
|
6525
|
-
const raw =
|
|
6664
|
+
const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
|
|
6526
6665
|
const data = JSON.parse(raw);
|
|
6527
6666
|
if (data.models && typeof data.models === "object") {
|
|
6528
6667
|
state.models = data.models;
|
|
6529
6668
|
}
|
|
6530
6669
|
if (Array.isArray(data.messages) && data.messages.length > 0) {
|
|
6531
6670
|
state.messages = sanitizeMessages(data.messages);
|
|
6532
|
-
|
|
6671
|
+
log10.info("Session loaded", {
|
|
6533
6672
|
messageCount: state.messages.length,
|
|
6534
6673
|
...state.models && { models: state.models }
|
|
6535
6674
|
});
|
|
@@ -6595,33 +6734,33 @@ function buildPayload(state) {
|
|
|
6595
6734
|
return payload;
|
|
6596
6735
|
}
|
|
6597
6736
|
function archiveMessages(messages, label, models) {
|
|
6598
|
-
|
|
6737
|
+
fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
6599
6738
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6600
6739
|
const count = messages.length;
|
|
6601
|
-
let dest =
|
|
6740
|
+
let dest = path12.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
|
|
6602
6741
|
let n = 1;
|
|
6603
|
-
while (
|
|
6604
|
-
dest =
|
|
6742
|
+
while (fs21.existsSync(dest)) {
|
|
6743
|
+
dest = path12.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.c${count}.json`);
|
|
6605
6744
|
}
|
|
6606
6745
|
const payload = { messages };
|
|
6607
6746
|
if (models && Object.keys(models).length > 0) {
|
|
6608
6747
|
payload.models = models;
|
|
6609
6748
|
}
|
|
6610
|
-
|
|
6611
|
-
archiveCountCache.set(
|
|
6612
|
-
|
|
6749
|
+
fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
|
|
6750
|
+
archiveCountCache.set(path12.basename(dest), count);
|
|
6751
|
+
log10.info("Session archived", { label, dest, messageCount: count });
|
|
6613
6752
|
pruneArchives();
|
|
6614
6753
|
return dest;
|
|
6615
6754
|
}
|
|
6616
6755
|
function pruneArchives() {
|
|
6617
6756
|
try {
|
|
6618
|
-
const entries =
|
|
6757
|
+
const entries = fs21.readdirSync(ARCHIVE_DIR).filter((name) => ARCHIVE_NAME_RE.test(name));
|
|
6619
6758
|
if (entries.length <= 1) {
|
|
6620
6759
|
return;
|
|
6621
6760
|
}
|
|
6622
6761
|
const archives = entries.map((name) => ({
|
|
6623
6762
|
name,
|
|
6624
|
-
size:
|
|
6763
|
+
size: fs21.statSync(path12.join(ARCHIVE_DIR, name)).size
|
|
6625
6764
|
})).sort(
|
|
6626
6765
|
(a, b) => archiveSortKey(b.name).localeCompare(archiveSortKey(a.name))
|
|
6627
6766
|
);
|
|
@@ -6639,14 +6778,14 @@ function pruneArchives() {
|
|
|
6639
6778
|
let freed = 0;
|
|
6640
6779
|
for (let i = cut; i < archives.length; i++) {
|
|
6641
6780
|
try {
|
|
6642
|
-
|
|
6781
|
+
fs21.unlinkSync(path12.join(ARCHIVE_DIR, archives[i].name));
|
|
6643
6782
|
freed += archives[i].size;
|
|
6644
6783
|
removed++;
|
|
6645
6784
|
} catch {
|
|
6646
6785
|
}
|
|
6647
6786
|
}
|
|
6648
6787
|
if (removed > 0) {
|
|
6649
|
-
|
|
6788
|
+
log10.info("Session archives pruned", {
|
|
6650
6789
|
removed,
|
|
6651
6790
|
freedBytes: freed,
|
|
6652
6791
|
keptBytes: kept
|
|
@@ -6663,7 +6802,7 @@ function parseArchive(name) {
|
|
|
6663
6802
|
return cached3;
|
|
6664
6803
|
}
|
|
6665
6804
|
try {
|
|
6666
|
-
const raw =
|
|
6805
|
+
const raw = fs21.readFileSync(path12.join(ARCHIVE_DIR, name), "utf-8");
|
|
6667
6806
|
const data = JSON.parse(raw);
|
|
6668
6807
|
const messages = Array.isArray(data?.messages) ? data.messages : [];
|
|
6669
6808
|
archiveCountCache.set(name, messages.length);
|
|
@@ -6677,7 +6816,7 @@ function parseArchive(name) {
|
|
|
6677
6816
|
}
|
|
6678
6817
|
return messages;
|
|
6679
6818
|
} catch (err) {
|
|
6680
|
-
|
|
6819
|
+
log10.warn("Session archive unreadable", { name, error: err?.message });
|
|
6681
6820
|
return null;
|
|
6682
6821
|
}
|
|
6683
6822
|
}
|
|
@@ -6701,7 +6840,7 @@ function readArchiveMessages(name) {
|
|
|
6701
6840
|
function listConversationArchives() {
|
|
6702
6841
|
let names;
|
|
6703
6842
|
try {
|
|
6704
|
-
names =
|
|
6843
|
+
names = fs21.readdirSync(ARCHIVE_DIR).filter((n) => ARCHIVE_NAME_RE.test(n));
|
|
6705
6844
|
} catch {
|
|
6706
6845
|
return { slots: [], archivedCount: 0 };
|
|
6707
6846
|
}
|
|
@@ -6800,7 +6939,7 @@ function rotate(state) {
|
|
|
6800
6939
|
}
|
|
6801
6940
|
archiveMessages(messages.slice(0, cut), "rotated", state.models);
|
|
6802
6941
|
state.messages = messages.slice(cut);
|
|
6803
|
-
|
|
6942
|
+
log10.info("Session rotated", {
|
|
6804
6943
|
archived: cut,
|
|
6805
6944
|
retained: state.messages.length
|
|
6806
6945
|
});
|
|
@@ -6812,10 +6951,10 @@ function saveSession(state) {
|
|
|
6812
6951
|
if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
|
|
6813
6952
|
serialized = JSON.stringify(buildPayload(state));
|
|
6814
6953
|
}
|
|
6815
|
-
|
|
6816
|
-
|
|
6954
|
+
fs21.writeFileSync(SESSION_FILE, serialized, "utf-8");
|
|
6955
|
+
log10.info("Session saved", { messageCount: state.messages.length });
|
|
6817
6956
|
} catch (err) {
|
|
6818
|
-
|
|
6957
|
+
log10.warn("Session save failed", { error: err.message });
|
|
6819
6958
|
}
|
|
6820
6959
|
}
|
|
6821
6960
|
function clearSession(state) {
|
|
@@ -6824,22 +6963,22 @@ function clearSession(state) {
|
|
|
6824
6963
|
archiveMessages(state.messages, "cleared", state.models);
|
|
6825
6964
|
}
|
|
6826
6965
|
} catch (err) {
|
|
6827
|
-
|
|
6966
|
+
log10.warn("Session archive on clear failed", { error: err.message });
|
|
6828
6967
|
}
|
|
6829
6968
|
state.messages = [];
|
|
6830
6969
|
try {
|
|
6831
|
-
if (
|
|
6832
|
-
|
|
6970
|
+
if (fs21.existsSync(SESSION_FILE)) {
|
|
6971
|
+
fs21.unlinkSync(SESSION_FILE);
|
|
6833
6972
|
}
|
|
6834
6973
|
} catch (err) {
|
|
6835
|
-
|
|
6974
|
+
log10.warn("Session clear: could not remove live file", {
|
|
6836
6975
|
error: err.message
|
|
6837
6976
|
});
|
|
6838
6977
|
}
|
|
6839
6978
|
}
|
|
6840
6979
|
|
|
6841
6980
|
// src/compaction/trigger.ts
|
|
6842
|
-
var
|
|
6981
|
+
var log11 = createLogger("compaction:trigger");
|
|
6843
6982
|
var pending = null;
|
|
6844
6983
|
var inflightCompaction = null;
|
|
6845
6984
|
function applyPendingSummaries(state) {
|
|
@@ -6856,7 +6995,7 @@ function applyPendingSummaries(state) {
|
|
|
6856
6995
|
idx = at === -1 ? 0 : at + 1;
|
|
6857
6996
|
}
|
|
6858
6997
|
state.messages.splice(idx, 0, ...drained.checkpoints);
|
|
6859
|
-
|
|
6998
|
+
log11.info("Checkpoint applied", {
|
|
6860
6999
|
index: idx,
|
|
6861
7000
|
messageCount: state.messages.length
|
|
6862
7001
|
});
|
|
@@ -6871,7 +7010,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
6871
7010
|
return inflightCompaction;
|
|
6872
7011
|
}
|
|
6873
7012
|
if (pending) {
|
|
6874
|
-
|
|
7013
|
+
log11.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
|
|
6875
7014
|
return Promise.resolve();
|
|
6876
7015
|
}
|
|
6877
7016
|
const { blocking = false, requestId, model } = opts;
|
|
@@ -6883,11 +7022,11 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
6883
7022
|
).then((result) => {
|
|
6884
7023
|
pending = result;
|
|
6885
7024
|
listener?.({ type: "complete", requestId });
|
|
6886
|
-
|
|
7025
|
+
log11.info("Compaction complete");
|
|
6887
7026
|
}).catch((err) => {
|
|
6888
7027
|
const message = err.message || "Compaction failed";
|
|
6889
7028
|
listener?.({ type: "complete", error: message, requestId });
|
|
6890
|
-
|
|
7029
|
+
log11.error("Compaction failed", { error: message });
|
|
6891
7030
|
throw err;
|
|
6892
7031
|
}).finally(() => {
|
|
6893
7032
|
inflightCompaction = null;
|
|
@@ -6896,10 +7035,10 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
6896
7035
|
}
|
|
6897
7036
|
|
|
6898
7037
|
// src/brandExtraction/index.ts
|
|
6899
|
-
import
|
|
6900
|
-
import
|
|
7038
|
+
import fs22 from "fs";
|
|
7039
|
+
import path13 from "path";
|
|
6901
7040
|
import { createHash } from "crypto";
|
|
6902
|
-
var
|
|
7041
|
+
var log12 = createLogger("brandExtraction");
|
|
6903
7042
|
var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
|
|
6904
7043
|
var BRAND_FILE = ".remy-brand.json";
|
|
6905
7044
|
var CACHE_FILE = ".remy-brand.cache.json";
|
|
@@ -6907,28 +7046,28 @@ async function runExtraction(apiConfig, model) {
|
|
|
6907
7046
|
const inputHash = computeInputHash();
|
|
6908
7047
|
const cached3 = readCache();
|
|
6909
7048
|
if (cached3 && cached3.inputHash === inputHash) {
|
|
6910
|
-
|
|
7049
|
+
log12.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
|
|
6911
7050
|
return null;
|
|
6912
7051
|
}
|
|
6913
|
-
|
|
7052
|
+
log12.info("Extracting brand", { inputHash });
|
|
6914
7053
|
const brand = await extractBrand(apiConfig, model);
|
|
6915
7054
|
if (!brand) {
|
|
6916
|
-
|
|
7055
|
+
log12.warn("Brand extraction failed \u2014 leaving cache untouched");
|
|
6917
7056
|
return null;
|
|
6918
7057
|
}
|
|
6919
7058
|
persistBrand(brand, inputHash);
|
|
6920
|
-
|
|
7059
|
+
log12.info("Brand persisted", { inputHash });
|
|
6921
7060
|
return brand;
|
|
6922
7061
|
}
|
|
6923
7062
|
function isDedicatedBrandFile(filePath) {
|
|
6924
|
-
if (filePath.split(
|
|
7063
|
+
if (filePath.split(path13.sep).includes("@brand")) {
|
|
6925
7064
|
return true;
|
|
6926
7065
|
}
|
|
6927
|
-
const { type } =
|
|
7066
|
+
const { type } = parseFrontmatter4(filePath);
|
|
6928
7067
|
return type.startsWith("design/color") || type.startsWith("design/typography");
|
|
6929
7068
|
}
|
|
6930
7069
|
function isBrandRelevant(filePath) {
|
|
6931
|
-
return filePath ===
|
|
7070
|
+
return filePath === path13.join("src", "app.md") || isDedicatedBrandFile(filePath);
|
|
6932
7071
|
}
|
|
6933
7072
|
function computeInputHash() {
|
|
6934
7073
|
const entries = [];
|
|
@@ -6950,7 +7089,7 @@ function sha256(input) {
|
|
|
6950
7089
|
}
|
|
6951
7090
|
function readSafe(filePath) {
|
|
6952
7091
|
try {
|
|
6953
|
-
return
|
|
7092
|
+
return fs22.readFileSync(filePath, "utf-8");
|
|
6954
7093
|
} catch {
|
|
6955
7094
|
return "";
|
|
6956
7095
|
}
|
|
@@ -6976,9 +7115,9 @@ function readBrandManifest() {
|
|
|
6976
7115
|
function walkMdFiles3(dir) {
|
|
6977
7116
|
const results = [];
|
|
6978
7117
|
try {
|
|
6979
|
-
const entries =
|
|
7118
|
+
const entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
6980
7119
|
for (const entry of entries) {
|
|
6981
|
-
const full =
|
|
7120
|
+
const full = path13.join(dir, entry.name);
|
|
6982
7121
|
if (entry.isDirectory()) {
|
|
6983
7122
|
results.push(...walkMdFiles3(full));
|
|
6984
7123
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -6989,9 +7128,9 @@ function walkMdFiles3(dir) {
|
|
|
6989
7128
|
}
|
|
6990
7129
|
return results.sort();
|
|
6991
7130
|
}
|
|
6992
|
-
function
|
|
7131
|
+
function parseFrontmatter4(filePath) {
|
|
6993
7132
|
try {
|
|
6994
|
-
const content =
|
|
7133
|
+
const content = fs22.readFileSync(filePath, "utf-8");
|
|
6995
7134
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6996
7135
|
if (!match) {
|
|
6997
7136
|
return { type: "" };
|
|
@@ -7006,7 +7145,7 @@ function parseFrontmatter3(filePath) {
|
|
|
7006
7145
|
async function extractBrand(apiConfig, model) {
|
|
7007
7146
|
const corpus = buildCorpus();
|
|
7008
7147
|
if (!corpus.trim()) {
|
|
7009
|
-
|
|
7148
|
+
log12.debug("No spec corpus \u2014 emitting empty brand");
|
|
7010
7149
|
return { version: 1 };
|
|
7011
7150
|
}
|
|
7012
7151
|
let responseText = "";
|
|
@@ -7037,17 +7176,17 @@ async function extractBrand(apiConfig, model) {
|
|
|
7037
7176
|
toolNames: []
|
|
7038
7177
|
});
|
|
7039
7178
|
} else if (event.type === "error") {
|
|
7040
|
-
|
|
7179
|
+
log12.error("Brand extraction stream error", { error: event.error });
|
|
7041
7180
|
return null;
|
|
7042
7181
|
}
|
|
7043
7182
|
}
|
|
7044
7183
|
} catch (err) {
|
|
7045
|
-
|
|
7184
|
+
log12.error("Brand extraction threw", { error: err?.message });
|
|
7046
7185
|
return null;
|
|
7047
7186
|
}
|
|
7048
7187
|
const parsed = parseJsonResponse(responseText);
|
|
7049
7188
|
if (!parsed) {
|
|
7050
|
-
|
|
7189
|
+
log12.warn("Brand extraction returned unparseable JSON", {
|
|
7051
7190
|
preview: responseText.slice(0, 200)
|
|
7052
7191
|
});
|
|
7053
7192
|
return null;
|
|
@@ -7201,14 +7340,14 @@ function pickFont(raw) {
|
|
|
7201
7340
|
}
|
|
7202
7341
|
function persistBrand(brand, inputHash) {
|
|
7203
7342
|
const tmp = `${BRAND_FILE}.tmp`;
|
|
7204
|
-
|
|
7205
|
-
|
|
7343
|
+
fs22.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
|
|
7344
|
+
fs22.renameSync(tmp, BRAND_FILE);
|
|
7206
7345
|
const cache = { inputHash, generatedAt: Date.now() };
|
|
7207
|
-
|
|
7346
|
+
fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
7208
7347
|
}
|
|
7209
7348
|
function readCache() {
|
|
7210
7349
|
try {
|
|
7211
|
-
const raw =
|
|
7350
|
+
const raw = fs22.readFileSync(CACHE_FILE, "utf-8");
|
|
7212
7351
|
const parsed = JSON.parse(raw);
|
|
7213
7352
|
if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
|
|
7214
7353
|
return parsed;
|
|
@@ -7220,7 +7359,7 @@ function readCache() {
|
|
|
7220
7359
|
}
|
|
7221
7360
|
|
|
7222
7361
|
// src/brandExtraction/trigger.ts
|
|
7223
|
-
var
|
|
7362
|
+
var log13 = createLogger("brandExtraction:trigger");
|
|
7224
7363
|
var inflight = false;
|
|
7225
7364
|
var dirty = false;
|
|
7226
7365
|
function triggerBrandExtraction(apiConfig, model) {
|
|
@@ -7230,7 +7369,7 @@ function triggerBrandExtraction(apiConfig, model) {
|
|
|
7230
7369
|
}
|
|
7231
7370
|
inflight = true;
|
|
7232
7371
|
void runExtraction(apiConfig, model).catch((err) => {
|
|
7233
|
-
|
|
7372
|
+
log13.error("Brand extraction failed", { error: err?.message });
|
|
7234
7373
|
}).finally(() => {
|
|
7235
7374
|
inflight = false;
|
|
7236
7375
|
if (dirty) {
|
|
@@ -7438,7 +7577,7 @@ function friendlyError(raw) {
|
|
|
7438
7577
|
}
|
|
7439
7578
|
|
|
7440
7579
|
// src/agent.ts
|
|
7441
|
-
var
|
|
7580
|
+
var log14 = createLogger("agent");
|
|
7442
7581
|
var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
|
|
7443
7582
|
function getTextContent(blocks) {
|
|
7444
7583
|
return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
@@ -7492,12 +7631,11 @@ async function runTurn(params) {
|
|
|
7492
7631
|
onBackgroundComplete
|
|
7493
7632
|
} = params;
|
|
7494
7633
|
const tools2 = getToolDefinitions(onboardingState);
|
|
7495
|
-
const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
|
|
7496
7634
|
const buildModelOverride = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
|
|
7497
7635
|
const baseline = resolveModel("parent", state.models, model);
|
|
7498
7636
|
const parentModel = buildModelOverride ?? baseline;
|
|
7499
7637
|
const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
|
|
7500
|
-
|
|
7638
|
+
log14.info("Turn started", {
|
|
7501
7639
|
requestId,
|
|
7502
7640
|
model,
|
|
7503
7641
|
buildModel: buildModelOverride,
|
|
@@ -7668,7 +7806,6 @@ async function runTurn(params) {
|
|
|
7668
7806
|
system,
|
|
7669
7807
|
messages: cleanMessagesForApi(state.messages),
|
|
7670
7808
|
tools: tools2,
|
|
7671
|
-
excludeToolsFromClearing,
|
|
7672
7809
|
signal
|
|
7673
7810
|
},
|
|
7674
7811
|
{
|
|
@@ -7760,7 +7897,7 @@ async function runTurn(params) {
|
|
|
7760
7897
|
const acc = toolInputAccumulators.get(event.id);
|
|
7761
7898
|
const wasStreamed = acc?.started ?? false;
|
|
7762
7899
|
const isInputStreaming = !!tool?.streaming?.partialInput;
|
|
7763
|
-
|
|
7900
|
+
log14.info("Tool received", {
|
|
7764
7901
|
requestId,
|
|
7765
7902
|
toolCallId: event.id,
|
|
7766
7903
|
name: event.name
|
|
@@ -7883,7 +8020,7 @@ async function runTurn(params) {
|
|
|
7883
8020
|
});
|
|
7884
8021
|
return;
|
|
7885
8022
|
}
|
|
7886
|
-
|
|
8023
|
+
log14.info("Tools executing", {
|
|
7887
8024
|
requestId,
|
|
7888
8025
|
count: toolCalls.length,
|
|
7889
8026
|
tools: toolCalls.map((tc) => tc.name)
|
|
@@ -7930,7 +8067,7 @@ async function runTurn(params) {
|
|
|
7930
8067
|
let result;
|
|
7931
8068
|
if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
|
|
7932
8069
|
saveSession(state);
|
|
7933
|
-
|
|
8070
|
+
log14.info("Waiting for external tool result", {
|
|
7934
8071
|
requestId,
|
|
7935
8072
|
toolCallId: tc.id,
|
|
7936
8073
|
name: tc.name
|
|
@@ -7998,7 +8135,7 @@ async function runTurn(params) {
|
|
|
7998
8135
|
if (!isBackgroundCall(tc)) {
|
|
7999
8136
|
toolRegistry?.unregister(tc.id);
|
|
8000
8137
|
}
|
|
8001
|
-
|
|
8138
|
+
log14.info("Tool completed", {
|
|
8002
8139
|
requestId,
|
|
8003
8140
|
toolCallId: tc.id,
|
|
8004
8141
|
name: tc.name,
|
|
@@ -8061,13 +8198,13 @@ async function runTurn(params) {
|
|
|
8061
8198
|
// src/headless/attachments.ts
|
|
8062
8199
|
import { mkdirSync, existsSync } from "fs";
|
|
8063
8200
|
import { writeFile } from "fs/promises";
|
|
8064
|
-
import { basename, join, extname } from "path";
|
|
8065
|
-
var
|
|
8201
|
+
import { basename as basename2, join, extname as extname2 } from "path";
|
|
8202
|
+
var log15 = createLogger("headless:attachments");
|
|
8066
8203
|
var UPLOADS_DIR = "src/.user-uploads";
|
|
8067
8204
|
function filenameFromUrl(url) {
|
|
8068
8205
|
try {
|
|
8069
8206
|
const pathname = new URL(url).pathname;
|
|
8070
|
-
const name =
|
|
8207
|
+
const name = basename2(pathname);
|
|
8071
8208
|
return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
|
|
8072
8209
|
} catch {
|
|
8073
8210
|
return `upload-${Date.now()}`;
|
|
@@ -8078,7 +8215,7 @@ function resolveUniqueFilename(name, claimed) {
|
|
|
8078
8215
|
if (isFree(name)) {
|
|
8079
8216
|
return name;
|
|
8080
8217
|
}
|
|
8081
|
-
const ext =
|
|
8218
|
+
const ext = extname2(name);
|
|
8082
8219
|
const base = name.slice(0, name.length - ext.length);
|
|
8083
8220
|
let counter = 1;
|
|
8084
8221
|
while (!isFree(`${base}-${counter}${ext}`)) {
|
|
@@ -8089,7 +8226,7 @@ function resolveUniqueFilename(name, claimed) {
|
|
|
8089
8226
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
8090
8227
|
function isImageAttachment(att) {
|
|
8091
8228
|
const name = att.filename || filenameFromUrl(att.url);
|
|
8092
|
-
return IMAGE_EXTENSIONS.has(
|
|
8229
|
+
return IMAGE_EXTENSIONS.has(extname2(name).toLowerCase());
|
|
8093
8230
|
}
|
|
8094
8231
|
async function persistAttachments(attachments) {
|
|
8095
8232
|
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
@@ -8118,7 +8255,7 @@ async function persistAttachments(attachments) {
|
|
|
8118
8255
|
}
|
|
8119
8256
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
8120
8257
|
await writeFile(localPath, buffer);
|
|
8121
|
-
|
|
8258
|
+
log15.info("Attachment saved", {
|
|
8122
8259
|
filename: name,
|
|
8123
8260
|
path: localPath,
|
|
8124
8261
|
bytes: buffer.length
|
|
@@ -8132,7 +8269,7 @@ async function persistAttachments(attachments) {
|
|
|
8132
8269
|
if (textRes.ok) {
|
|
8133
8270
|
extractedTextPath = `${localPath}.txt`;
|
|
8134
8271
|
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
8135
|
-
|
|
8272
|
+
log15.info("Extracted text saved", { path: extractedTextPath });
|
|
8136
8273
|
}
|
|
8137
8274
|
} catch {
|
|
8138
8275
|
}
|
|
@@ -8355,7 +8492,7 @@ function getActionChain(startName) {
|
|
|
8355
8492
|
}
|
|
8356
8493
|
|
|
8357
8494
|
// src/headless/index.ts
|
|
8358
|
-
var
|
|
8495
|
+
var log16 = createLogger("headless");
|
|
8359
8496
|
var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
8360
8497
|
var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
|
|
8361
8498
|
"promptUser",
|
|
@@ -8500,7 +8637,7 @@ var HeadlessSession = class {
|
|
|
8500
8637
|
}
|
|
8501
8638
|
const line = JSON.stringify(payload) + "\n";
|
|
8502
8639
|
if (event === "history") {
|
|
8503
|
-
|
|
8640
|
+
log16.info("Wrote history event to stdout", {
|
|
8504
8641
|
requestId,
|
|
8505
8642
|
bytes: line.length
|
|
8506
8643
|
});
|
|
@@ -8577,7 +8714,7 @@ var HeadlessSession = class {
|
|
|
8577
8714
|
if (this.sessionStats.lastContextSize <= FORCED_COMPACTION_THRESHOLD_TOKENS) {
|
|
8578
8715
|
return;
|
|
8579
8716
|
}
|
|
8580
|
-
|
|
8717
|
+
log16.info("Forced compaction gate triggered", {
|
|
8581
8718
|
contextSize: this.sessionStats.lastContextSize,
|
|
8582
8719
|
threshold: FORCED_COMPACTION_THRESHOLD_TOKENS,
|
|
8583
8720
|
requestId
|
|
@@ -8594,7 +8731,7 @@ var HeadlessSession = class {
|
|
|
8594
8731
|
}
|
|
8595
8732
|
onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
|
|
8596
8733
|
this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
8597
|
-
|
|
8734
|
+
log16.info("Background complete", {
|
|
8598
8735
|
toolCallId,
|
|
8599
8736
|
name,
|
|
8600
8737
|
requestId: this.currentRequestId
|
|
@@ -8631,17 +8768,17 @@ var HeadlessSession = class {
|
|
|
8631
8768
|
return Promise.resolve(early);
|
|
8632
8769
|
}
|
|
8633
8770
|
const shouldTimeout = !USER_FACING_TOOLS.has(name);
|
|
8634
|
-
return new Promise((
|
|
8771
|
+
return new Promise((resolve3) => {
|
|
8635
8772
|
const timeout = shouldTimeout ? setTimeout(() => {
|
|
8636
8773
|
this.pendingTools.delete(id);
|
|
8637
|
-
|
|
8774
|
+
resolve3(
|
|
8638
8775
|
"Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
|
|
8639
8776
|
);
|
|
8640
8777
|
}, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
|
|
8641
8778
|
this.pendingTools.set(id, {
|
|
8642
8779
|
resolve: (result) => {
|
|
8643
8780
|
clearTimeout(timeout);
|
|
8644
|
-
|
|
8781
|
+
resolve3(result);
|
|
8645
8782
|
},
|
|
8646
8783
|
timeout
|
|
8647
8784
|
});
|
|
@@ -8832,7 +8969,7 @@ var HeadlessSession = class {
|
|
|
8832
8969
|
await this.runForcedCompactionIfNeeded(requestId);
|
|
8833
8970
|
const attachments = parsed.attachments;
|
|
8834
8971
|
if (attachments?.length) {
|
|
8835
|
-
|
|
8972
|
+
log16.info("Message has attachments", {
|
|
8836
8973
|
count: attachments.length,
|
|
8837
8974
|
urls: attachments.map((a) => a.url)
|
|
8838
8975
|
});
|
|
@@ -8848,7 +8985,7 @@ var HeadlessSession = class {
|
|
|
8848
8985
|
attachmentHeader = header;
|
|
8849
8986
|
}
|
|
8850
8987
|
} catch (err) {
|
|
8851
|
-
|
|
8988
|
+
log16.warn("Attachment persistence failed", { error: err.message });
|
|
8852
8989
|
}
|
|
8853
8990
|
}
|
|
8854
8991
|
let resolved = null;
|
|
@@ -8912,7 +9049,7 @@ var HeadlessSession = class {
|
|
|
8912
9049
|
error: "Turn ended unexpectedly"
|
|
8913
9050
|
});
|
|
8914
9051
|
}
|
|
8915
|
-
|
|
9052
|
+
log16.info("Turn complete", {
|
|
8916
9053
|
requestId,
|
|
8917
9054
|
durationMs: Date.now() - this.turnStart
|
|
8918
9055
|
});
|
|
@@ -8924,7 +9061,7 @@ var HeadlessSession = class {
|
|
|
8924
9061
|
error: err.message
|
|
8925
9062
|
});
|
|
8926
9063
|
}
|
|
8927
|
-
|
|
9064
|
+
log16.warn("Command failed", {
|
|
8928
9065
|
action: "message",
|
|
8929
9066
|
requestId,
|
|
8930
9067
|
error: err.message
|
|
@@ -9081,7 +9218,7 @@ var HeadlessSession = class {
|
|
|
9081
9218
|
try {
|
|
9082
9219
|
parsed = JSON.parse(line);
|
|
9083
9220
|
} catch (err) {
|
|
9084
|
-
|
|
9221
|
+
log16.warn("Invalid JSON on stdin", {
|
|
9085
9222
|
error: err.message,
|
|
9086
9223
|
lineLength: line.length,
|
|
9087
9224
|
preview: line.slice(0, 200)
|
|
@@ -9090,7 +9227,7 @@ var HeadlessSession = class {
|
|
|
9090
9227
|
return;
|
|
9091
9228
|
}
|
|
9092
9229
|
const { action, requestId } = parsed;
|
|
9093
|
-
|
|
9230
|
+
log16.info("Command received", { action, requestId });
|
|
9094
9231
|
if (action === "tool_result" && parsed.id) {
|
|
9095
9232
|
const id = parsed.id;
|
|
9096
9233
|
const result = parsed.result ?? "";
|
|
@@ -9099,7 +9236,7 @@ var HeadlessSession = class {
|
|
|
9099
9236
|
this.pendingTools.delete(id);
|
|
9100
9237
|
pending2.resolve(result);
|
|
9101
9238
|
} else if (!this.running) {
|
|
9102
|
-
|
|
9239
|
+
log16.info("Late tool_result while idle, dismissing", { id });
|
|
9103
9240
|
this.emit("completed", { success: true }, requestId);
|
|
9104
9241
|
} else {
|
|
9105
9242
|
this.earlyResults.set(id, result);
|
|
@@ -9112,7 +9249,7 @@ var HeadlessSession = class {
|
|
|
9112
9249
|
...typeof parsed.before === "number" ? { before: parsed.before } : {},
|
|
9113
9250
|
...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
|
|
9114
9251
|
});
|
|
9115
|
-
|
|
9252
|
+
log16.info("History response", {
|
|
9116
9253
|
requestId,
|
|
9117
9254
|
startIndex: page.startIndex,
|
|
9118
9255
|
endIndex: page.endIndex,
|