@mindstudio-ai/remy 0.1.319 → 0.1.321
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/automatedActions/reviewExistingProject.md +7 -0
- package/dist/headless.js +270 -29
- package/dist/index.js +319 -42
- package/dist/prompt/skills/dataSources.md +158 -5
- package/dist/prompt/static/coding.md +1 -1
- package/dist/prompt/static/intake.md +1 -1
- package/dist/prompt/static/team.md +7 -1
- package/dist/subagents/browserAutomation/prompt.md +1 -1
- package/dist/subagents/reviewExistingProject/prompt.md +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: reviewExistingProject
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
This is an automated message triggered by the user choosing "Bring an existing project" on the Remy start screen and uploading what they have. They can't see this message. The uploads are attached and already saved under `src/.user-uploads/` (paths are in the attachment header; a folder upload arrives as one zip). Repository URL, blank if none: {{repoUrl}}
|
|
6
|
+
|
|
7
|
+
Let the user know you're going to review their project, and then hand the upload paths (and the URL, if any) to `reviewExistingProject` first, before saying anything substantive, and do not open the archive yourself. Then run intake as usual on top of what comes back: tell the user in a few lines what you found and what you think they were building, and put the review's questions to them as a form. The prior attempt is evidence of intent, not a spec. Oftentimes, the user is uploading something that never worked or was half-finished (hence why they're here!). What actually gets built in this session is decided with the user, not inherited from the upload. Treat the results of the review as a *communication shortcut* to more quickly align with the user, not as a guide for "importing" their project!
|
package/dist/headless.js
CHANGED
|
@@ -441,6 +441,13 @@ var MODEL_SURFACES = {
|
|
|
441
441
|
modelType: "text",
|
|
442
442
|
userPickable: true
|
|
443
443
|
},
|
|
444
|
+
reviewExistingProject: {
|
|
445
|
+
default: "claude-5-sonnet",
|
|
446
|
+
label: "Existing Project Review",
|
|
447
|
+
description: "Reviews a project you bring from another tool and reports what is worth carrying forward.",
|
|
448
|
+
modelType: "text",
|
|
449
|
+
userPickable: true
|
|
450
|
+
},
|
|
444
451
|
copyEditor: {
|
|
445
452
|
default: "claude-5-sonnet",
|
|
446
453
|
label: "Copy Agent",
|
|
@@ -1420,6 +1427,9 @@ async function listRecursive(dir) {
|
|
|
1420
1427
|
for (const entry of entries) {
|
|
1421
1428
|
const fullPath = path5.join(dir, entry.name);
|
|
1422
1429
|
if (entry.isDirectory()) {
|
|
1430
|
+
if (entry.name.startsWith(".")) {
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1423
1433
|
results.push(`${fullPath}/`);
|
|
1424
1434
|
results.push(...await listRecursive(fullPath));
|
|
1425
1435
|
} else {
|
|
@@ -1593,7 +1603,7 @@ var promptUserTool = {
|
|
|
1593
1603
|
type: {
|
|
1594
1604
|
type: "string",
|
|
1595
1605
|
enum: ["select", "checklist", "text", "file"],
|
|
1596
|
-
description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file:
|
|
1606
|
+
description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: the user hands over files, a folder, or a paste (set `multiple: true` to take several). Each answer arrives in one of two shapes. A local path (string) under src/.user-uploads/: the file is already on disk, documents may have an extracted-text sidecar at <path>.txt, and a folder arrives as src/.user-uploads/<folder>.zip for you to unpack with bash. Or, when the user brought a dataset (a folder over about 100 MB or 500 files, or any single file over 100 MB), a store landing object { kind: "store", store, prefix, files, bytes, failed } \u2014 the files were streamed into the app\'s private file store `uploads` under that prefix and are NOT on disk; read them in place with `remy-admin datasources inspect --store <store> --prefix <prefix>` and `remy-admin files get`, never download the whole set. Pass image paths straight to analyzeImage / screenshot tools.'
|
|
1597
1607
|
},
|
|
1598
1608
|
helpText: {
|
|
1599
1609
|
type: "string",
|
|
@@ -1628,7 +1638,7 @@ var promptUserTool = {
|
|
|
1628
1638
|
},
|
|
1629
1639
|
multiple: {
|
|
1630
1640
|
type: "boolean",
|
|
1631
|
-
description:
|
|
1641
|
+
description: 'For file type: allow several files or folders (the answer is an array of local paths and/or store landings). Defaults to false. Set it whenever you ask for "a few examples" or "a sample of your data".'
|
|
1632
1642
|
},
|
|
1633
1643
|
format: {
|
|
1634
1644
|
type: "string",
|
|
@@ -2619,7 +2629,7 @@ async function readAndSort(dirPath) {
|
|
|
2619
2629
|
async function collapsePath(basePath, name) {
|
|
2620
2630
|
let display = name;
|
|
2621
2631
|
let current = path8.join(basePath, name);
|
|
2622
|
-
|
|
2632
|
+
while (true) {
|
|
2623
2633
|
let children;
|
|
2624
2634
|
try {
|
|
2625
2635
|
children = await readAndSort(current);
|
|
@@ -2646,8 +2656,8 @@ function formatSize(bytes) {
|
|
|
2646
2656
|
}
|
|
2647
2657
|
async function formatFile(dirPath, name, indent) {
|
|
2648
2658
|
try {
|
|
2649
|
-
const
|
|
2650
|
-
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(
|
|
2659
|
+
const stat6 = await fs15.stat(path8.join(dirPath, name));
|
|
2660
|
+
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat6.size)}`;
|
|
2651
2661
|
} catch {
|
|
2652
2662
|
return `${indent}${name}`;
|
|
2653
2663
|
}
|
|
@@ -3437,6 +3447,48 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
3437
3447
|
}
|
|
3438
3448
|
};
|
|
3439
3449
|
|
|
3450
|
+
// src/recording.ts
|
|
3451
|
+
function liftRecording(result) {
|
|
3452
|
+
if (!result.includes('"recording"')) {
|
|
3453
|
+
return { result };
|
|
3454
|
+
}
|
|
3455
|
+
let parsed;
|
|
3456
|
+
try {
|
|
3457
|
+
parsed = JSON.parse(result);
|
|
3458
|
+
} catch {
|
|
3459
|
+
return { result };
|
|
3460
|
+
}
|
|
3461
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3462
|
+
return { result };
|
|
3463
|
+
}
|
|
3464
|
+
const obj = parsed;
|
|
3465
|
+
const r = obj.recording;
|
|
3466
|
+
if (!r || typeof r !== "object") {
|
|
3467
|
+
return { result };
|
|
3468
|
+
}
|
|
3469
|
+
const ref = r;
|
|
3470
|
+
if (typeof ref.path !== "string" || typeof ref.sessionId !== "string" || typeof ref.runId !== "string" || typeof ref.seq !== "number") {
|
|
3471
|
+
return { result };
|
|
3472
|
+
}
|
|
3473
|
+
const recording = {
|
|
3474
|
+
path: ref.path,
|
|
3475
|
+
sessionId: ref.sessionId,
|
|
3476
|
+
runId: ref.runId,
|
|
3477
|
+
seq: ref.seq,
|
|
3478
|
+
containsSnapshot: ref.containsSnapshot === true,
|
|
3479
|
+
startTs: typeof ref.startTs === "number" ? ref.startTs : 0,
|
|
3480
|
+
endTs: typeof ref.endTs === "number" ? ref.endTs : 0,
|
|
3481
|
+
width: typeof ref.width === "number" ? ref.width : 0,
|
|
3482
|
+
height: typeof ref.height === "number" ? ref.height : 0
|
|
3483
|
+
};
|
|
3484
|
+
const rest = { ...obj };
|
|
3485
|
+
delete rest.recording;
|
|
3486
|
+
return {
|
|
3487
|
+
result: JSON.stringify({ ...rest, recorded: true }),
|
|
3488
|
+
recording
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3440
3492
|
// src/historyLimits.ts
|
|
3441
3493
|
var MAX_TOOL_RESULT_BYTES = 256 * 1024;
|
|
3442
3494
|
function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
|
|
@@ -3479,6 +3531,13 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
|
|
|
3479
3531
|
continue;
|
|
3480
3532
|
}
|
|
3481
3533
|
if (typeof block.result === "string") {
|
|
3534
|
+
if (block.name === "browserCommand" && !block.recording) {
|
|
3535
|
+
const lifted = liftRecording(block.result);
|
|
3536
|
+
if (lifted.recording) {
|
|
3537
|
+
block.result = lifted.result;
|
|
3538
|
+
block.recording = lifted.recording;
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3482
3541
|
block.result = capToolResult(block.result, maxBytes);
|
|
3483
3542
|
}
|
|
3484
3543
|
if (typeof block.backgroundResult === "string") {
|
|
@@ -4075,25 +4134,36 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4075
4134
|
}
|
|
4076
4135
|
let settle;
|
|
4077
4136
|
const resultPromise = new Promise((res) => {
|
|
4078
|
-
settle = (result, isError) => res({
|
|
4137
|
+
settle = (result, isError, recording) => res({
|
|
4138
|
+
id: tc.id,
|
|
4139
|
+
result,
|
|
4140
|
+
isError,
|
|
4141
|
+
...recording ? { recording } : {}
|
|
4142
|
+
});
|
|
4079
4143
|
});
|
|
4080
4144
|
let toolAbort = new AbortController();
|
|
4081
4145
|
const cascadeAbort = () => toolAbort.abort();
|
|
4082
4146
|
signal?.addEventListener("abort", cascadeAbort, { once: true });
|
|
4083
4147
|
let settled = false;
|
|
4084
|
-
const safeSettle = (result, isError) => {
|
|
4148
|
+
const safeSettle = (result, isError, recording) => {
|
|
4085
4149
|
if (settled) {
|
|
4086
4150
|
return;
|
|
4087
4151
|
}
|
|
4088
4152
|
settled = true;
|
|
4089
4153
|
signal?.removeEventListener("abort", cascadeAbort);
|
|
4090
|
-
settle(result, isError);
|
|
4154
|
+
settle(result, isError, recording);
|
|
4091
4155
|
};
|
|
4092
4156
|
const run2 = async (input) => {
|
|
4093
4157
|
try {
|
|
4094
4158
|
let result;
|
|
4159
|
+
let recording;
|
|
4095
4160
|
if (externalTools.has(tc.name) && resolveExternalTool) {
|
|
4096
4161
|
result = await resolveExternalTool(tc.id, tc.name, input);
|
|
4162
|
+
if (tc.name === "browserCommand") {
|
|
4163
|
+
const lifted = liftRecording(result);
|
|
4164
|
+
result = lifted.result;
|
|
4165
|
+
recording = lifted.recording;
|
|
4166
|
+
}
|
|
4097
4167
|
} else {
|
|
4098
4168
|
const onLog = (line) => emit({
|
|
4099
4169
|
type: "tool_input_delta",
|
|
@@ -4109,7 +4179,11 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4109
4179
|
subAgentMessages
|
|
4110
4180
|
);
|
|
4111
4181
|
}
|
|
4112
|
-
safeSettle(
|
|
4182
|
+
safeSettle(
|
|
4183
|
+
capToolResult(result),
|
|
4184
|
+
result.startsWith("Error"),
|
|
4185
|
+
recording
|
|
4186
|
+
);
|
|
4113
4187
|
} catch (err) {
|
|
4114
4188
|
safeSettle(`Error: ${err.message}`, true);
|
|
4115
4189
|
}
|
|
@@ -4151,7 +4225,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4151
4225
|
id: tc.id,
|
|
4152
4226
|
name: tc.name,
|
|
4153
4227
|
result: r.result,
|
|
4154
|
-
isError: r.isError
|
|
4228
|
+
isError: r.isError,
|
|
4229
|
+
...r.recording ? { recording: r.recording } : {}
|
|
4155
4230
|
});
|
|
4156
4231
|
return r;
|
|
4157
4232
|
})
|
|
@@ -4165,6 +4240,9 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4165
4240
|
block.result = r.result;
|
|
4166
4241
|
block.isError = r.isError;
|
|
4167
4242
|
block.completedAt = Date.now();
|
|
4243
|
+
if (r.recording) {
|
|
4244
|
+
block.recording = r.recording;
|
|
4245
|
+
}
|
|
4168
4246
|
const innerMsgs = subAgentMessages.get(r.id);
|
|
4169
4247
|
if (innerMsgs) {
|
|
4170
4248
|
block.subAgentMessages = capSubAgentTranscript(innerMsgs);
|
|
@@ -4295,7 +4373,7 @@ var BROWSER_TOOLS = [
|
|
|
4295
4373
|
},
|
|
4296
4374
|
{
|
|
4297
4375
|
name: "browserCommand",
|
|
4298
|
-
description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select)
|
|
4376
|
+
description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) are recorded for the user to replay and return `recorded: true`. Timeout: 120s.",
|
|
4299
4377
|
inputSchema: {
|
|
4300
4378
|
type: "object",
|
|
4301
4379
|
properties: {
|
|
@@ -4413,7 +4491,7 @@ function walkMdFiles(dir, skip) {
|
|
|
4413
4491
|
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
4414
4492
|
const full = path9.join(dir, entry.name);
|
|
4415
4493
|
if (entry.isDirectory()) {
|
|
4416
|
-
if (!skip?.has(entry.name)) {
|
|
4494
|
+
if (!skip?.has(entry.name) && !entry.name.startsWith(".")) {
|
|
4417
4495
|
files.push(...walkMdFiles(full, skip));
|
|
4418
4496
|
}
|
|
4419
4497
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4711,8 +4789,14 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4711
4789
|
});
|
|
4712
4790
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
4713
4791
|
const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
|
|
4792
|
+
const recorded = result.messages.some(
|
|
4793
|
+
(m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
|
|
4794
|
+
(b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
|
|
4795
|
+
)
|
|
4796
|
+
);
|
|
4714
4797
|
return {
|
|
4715
4798
|
text: result.text,
|
|
4799
|
+
recorded,
|
|
4716
4800
|
...preferred?.url ? { screenshot: preferred } : {}
|
|
4717
4801
|
};
|
|
4718
4802
|
} finally {
|
|
@@ -4739,12 +4823,18 @@ var browserAutomationTool = {
|
|
|
4739
4823
|
return "Error: browser automation requires execution context (only available in headless mode)";
|
|
4740
4824
|
}
|
|
4741
4825
|
const result = await runBrowserAutomation(input.task, context);
|
|
4826
|
+
let text = result.text;
|
|
4742
4827
|
if (result.screenshot) {
|
|
4743
|
-
|
|
4828
|
+
text += `
|
|
4744
4829
|
|
|
4745
4830
|
`;
|
|
4746
4831
|
}
|
|
4747
|
-
|
|
4832
|
+
if (result.recorded) {
|
|
4833
|
+
text += `
|
|
4834
|
+
|
|
4835
|
+
Replay of this run: `;
|
|
4836
|
+
}
|
|
4837
|
+
return text;
|
|
4748
4838
|
}
|
|
4749
4839
|
};
|
|
4750
4840
|
|
|
@@ -7078,6 +7168,138 @@ After reconciling the spec, refresh the Build Overview: author the complete upda
|
|
|
7078
7168
|
}
|
|
7079
7169
|
};
|
|
7080
7170
|
|
|
7171
|
+
// src/subagents/reviewExistingProject/tools.ts
|
|
7172
|
+
var REVIEW_TOOLS = [
|
|
7173
|
+
...COMMON_READ_TOOLS,
|
|
7174
|
+
bashTool.definition
|
|
7175
|
+
];
|
|
7176
|
+
|
|
7177
|
+
// src/subagents/reviewExistingProject/validatePaths.ts
|
|
7178
|
+
import { stat as stat4 } from "fs/promises";
|
|
7179
|
+
import { join as join5 } from "path";
|
|
7180
|
+
var UPLOAD_REF_RE = /`(src\/\.user-uploads\/[^`\n]+?)\/?`/g;
|
|
7181
|
+
async function validateUploadPaths(text) {
|
|
7182
|
+
const refs = /* @__PURE__ */ new Set();
|
|
7183
|
+
for (const m of text.matchAll(UPLOAD_REF_RE)) {
|
|
7184
|
+
refs.add(m[1]);
|
|
7185
|
+
}
|
|
7186
|
+
if (refs.size === 0) {
|
|
7187
|
+
return null;
|
|
7188
|
+
}
|
|
7189
|
+
const missing = [];
|
|
7190
|
+
for (const ref of refs) {
|
|
7191
|
+
const exists = await stat4(join5(PROJECT_ROOT, ref)).then(
|
|
7192
|
+
() => true,
|
|
7193
|
+
() => false
|
|
7194
|
+
);
|
|
7195
|
+
if (!exists) {
|
|
7196
|
+
missing.push(ref);
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
if (missing.length === 0) {
|
|
7200
|
+
return null;
|
|
7201
|
+
}
|
|
7202
|
+
return [
|
|
7203
|
+
`Your review cites paths under src/.user-uploads/ that do not exist on disk:`,
|
|
7204
|
+
...missing.map((p) => `- ${p}`),
|
|
7205
|
+
``,
|
|
7206
|
+
`Check the real location with listDir \u2014 the trimmed tree may have landed under a different name, or the original archive may already have been deleted \u2014 and correct or remove each path. Then send your complete review again from the top \u2014 it fully replaces your previous response, so include everything, not just the fixes.`
|
|
7207
|
+
].join("\n");
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
// src/subagents/reviewExistingProject/index.ts
|
|
7211
|
+
var BASE_PROMPT7 = readAsset("subagents/reviewExistingProject", "prompt.md");
|
|
7212
|
+
function buildTask(input) {
|
|
7213
|
+
const parts = [];
|
|
7214
|
+
const paths = (input.paths ?? []).filter(
|
|
7215
|
+
(p) => typeof p === "string" && p.trim()
|
|
7216
|
+
);
|
|
7217
|
+
if (paths.length > 0) {
|
|
7218
|
+
parts.push(
|
|
7219
|
+
`Uploads (local paths):
|
|
7220
|
+
${paths.map((p) => `- ${p.trim()}`).join("\n")}`
|
|
7221
|
+
);
|
|
7222
|
+
}
|
|
7223
|
+
if (input.repoUrl?.trim()) {
|
|
7224
|
+
parts.push(`Repository URL: ${input.repoUrl.trim()}`);
|
|
7225
|
+
}
|
|
7226
|
+
if (input.context?.trim()) {
|
|
7227
|
+
parts.push(`Context from the conversation: ${input.context.trim()}`);
|
|
7228
|
+
}
|
|
7229
|
+
return parts.join("\n\n");
|
|
7230
|
+
}
|
|
7231
|
+
async function runReviewExistingProject(input, context) {
|
|
7232
|
+
const task = buildTask(input);
|
|
7233
|
+
if (!task) {
|
|
7234
|
+
return "Error: reviewExistingProject needs at least one upload path or a repository URL.";
|
|
7235
|
+
}
|
|
7236
|
+
const specIndex = loadSpecIndex();
|
|
7237
|
+
const parts = [BASE_PROMPT7, loadPlatformBrief()];
|
|
7238
|
+
parts.push("<!-- cache_breakpoint -->");
|
|
7239
|
+
if (specIndex) {
|
|
7240
|
+
parts.push(specIndex);
|
|
7241
|
+
}
|
|
7242
|
+
const system = parts.join("\n\n");
|
|
7243
|
+
const result = await runSubAgent({
|
|
7244
|
+
system,
|
|
7245
|
+
task,
|
|
7246
|
+
tools: REVIEW_TOOLS,
|
|
7247
|
+
externalTools: /* @__PURE__ */ new Set(),
|
|
7248
|
+
executeTool: (name, toolInput, toolCallId, onLog, sams) => {
|
|
7249
|
+
const childCtx = toolCallId ? {
|
|
7250
|
+
...deriveContext(context, toolCallId, onLog),
|
|
7251
|
+
subAgentMessages: sams
|
|
7252
|
+
} : context;
|
|
7253
|
+
return executeTool(name, toolInput, childCtx);
|
|
7254
|
+
},
|
|
7255
|
+
apiConfig: context.apiConfig,
|
|
7256
|
+
model: resolveModel("reviewExistingProject", context.models, context.model),
|
|
7257
|
+
subAgentId: "reviewExistingProject",
|
|
7258
|
+
signal: context.signal,
|
|
7259
|
+
parentToolId: context.toolCallId,
|
|
7260
|
+
requestId: context.requestId,
|
|
7261
|
+
onEvent: context.onEvent,
|
|
7262
|
+
resolveExternalTool: context.resolveExternalTool,
|
|
7263
|
+
toolRegistry: context.toolRegistry,
|
|
7264
|
+
// Every quoted src/.user-uploads/ path in the review must exist on disk
|
|
7265
|
+
// (see validatePaths.ts) — the paths are the part the caller acts on.
|
|
7266
|
+
validateResult: (text) => validateUploadPaths(text)
|
|
7267
|
+
});
|
|
7268
|
+
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
7269
|
+
return result.text;
|
|
7270
|
+
}
|
|
7271
|
+
var reviewExistingProjectTool = {
|
|
7272
|
+
definition: {
|
|
7273
|
+
name: "reviewExistingProject",
|
|
7274
|
+
description: "Your reviewer for anything a user brings from a prior/extant project or attempt at a project: a zip of a codebase from another coding agent, an export from a vibe coding platform, a folder of specs and docs, a public git URL. It unpacks the upload, strips the noise, lands the trimmed tree in src/.user-uploads/, and returns a short review: what the project was trying to be, the materials worth carrying forward with their paths, what to ignore, state signals, and the questions only the user can answer. Treat the review as evidence of what the user wants, never as a spec. Do not open the archive yourself.",
|
|
7275
|
+
inputSchema: {
|
|
7276
|
+
type: "object",
|
|
7277
|
+
properties: {
|
|
7278
|
+
paths: {
|
|
7279
|
+
type: "array",
|
|
7280
|
+
items: { type: "string" },
|
|
7281
|
+
description: "Local paths under src/.user-uploads/ to the uploaded archive(s) and/or loose files, exactly as given in the attachment header."
|
|
7282
|
+
},
|
|
7283
|
+
context: {
|
|
7284
|
+
type: "string",
|
|
7285
|
+
description: "One or two lines about the user and what they said they want. The reviewer cannot see the conversation."
|
|
7286
|
+
},
|
|
7287
|
+
repoUrl: {
|
|
7288
|
+
type: "string",
|
|
7289
|
+
description: "A public git repository URL to shallow-clone and review, instead of or alongside the uploads."
|
|
7290
|
+
}
|
|
7291
|
+
},
|
|
7292
|
+
required: []
|
|
7293
|
+
}
|
|
7294
|
+
},
|
|
7295
|
+
async execute(input, context) {
|
|
7296
|
+
if (!context) {
|
|
7297
|
+
return "Error: reviewExistingProject requires execution context";
|
|
7298
|
+
}
|
|
7299
|
+
return runReviewExistingProject(input, context);
|
|
7300
|
+
}
|
|
7301
|
+
};
|
|
7302
|
+
|
|
7081
7303
|
// src/tools/common/scrapeWebUrl.ts
|
|
7082
7304
|
var scrapeWebUrlTool = {
|
|
7083
7305
|
definition: {
|
|
@@ -7172,7 +7394,8 @@ var ALL_TOOLS = [
|
|
|
7172
7394
|
// new tool goes at the end to leave every existing session's prefix intact.
|
|
7173
7395
|
loadSkillTool,
|
|
7174
7396
|
testJewelTool,
|
|
7175
|
-
researchTool
|
|
7397
|
+
researchTool,
|
|
7398
|
+
reviewExistingProjectTool
|
|
7176
7399
|
];
|
|
7177
7400
|
var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
7178
7401
|
"visualDesignExpert",
|
|
@@ -7182,7 +7405,8 @@ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
7182
7405
|
"specSync",
|
|
7183
7406
|
"runAutomatedBrowserTest",
|
|
7184
7407
|
"askMindStudioSdk",
|
|
7185
|
-
"research"
|
|
7408
|
+
"research",
|
|
7409
|
+
"reviewExistingProject"
|
|
7186
7410
|
]);
|
|
7187
7411
|
function getToolDefinitions() {
|
|
7188
7412
|
return ALL_TOOLS.map((t) => t.definition);
|
|
@@ -8172,7 +8396,9 @@ function walkMdFiles2(dir) {
|
|
|
8172
8396
|
for (const entry of entries) {
|
|
8173
8397
|
const full = path12.join(dir, entry.name);
|
|
8174
8398
|
if (entry.isDirectory()) {
|
|
8175
|
-
|
|
8399
|
+
if (!entry.name.startsWith(".")) {
|
|
8400
|
+
results.push(...walkMdFiles2(full));
|
|
8401
|
+
}
|
|
8176
8402
|
} else if (entry.name.endsWith(".md")) {
|
|
8177
8403
|
results.push(full);
|
|
8178
8404
|
}
|
|
@@ -9572,11 +9798,15 @@ async function runTurn(params) {
|
|
|
9572
9798
|
}
|
|
9573
9799
|
|
|
9574
9800
|
// src/headless/attachments.ts
|
|
9575
|
-
import { mkdirSync, existsSync } from "fs";
|
|
9576
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
9577
|
-
import {
|
|
9801
|
+
import { mkdirSync, existsSync, createWriteStream } from "fs";
|
|
9802
|
+
import { writeFile as writeFile3, stat as stat5 } from "fs/promises";
|
|
9803
|
+
import { Readable } from "stream";
|
|
9804
|
+
import { pipeline } from "stream/promises";
|
|
9805
|
+
import { basename as basename2, join as join6, extname as extname3 } from "path";
|
|
9578
9806
|
var log16 = createLogger("headless:attachments");
|
|
9579
9807
|
var UPLOADS_DIR = "src/.user-uploads";
|
|
9808
|
+
var IMAGE_DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
9809
|
+
var DOCUMENT_DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
9580
9810
|
function filenameFromUrl(url) {
|
|
9581
9811
|
try {
|
|
9582
9812
|
const pathname = new URL(url).pathname;
|
|
@@ -9587,7 +9817,7 @@ function filenameFromUrl(url) {
|
|
|
9587
9817
|
}
|
|
9588
9818
|
}
|
|
9589
9819
|
function resolveUniqueFilename(name, claimed) {
|
|
9590
|
-
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(
|
|
9820
|
+
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join6(UPLOADS_DIR, candidate));
|
|
9591
9821
|
if (isFree(name)) {
|
|
9592
9822
|
return name;
|
|
9593
9823
|
}
|
|
@@ -9622,19 +9852,23 @@ async function persistAttachmentList(attachments) {
|
|
|
9622
9852
|
const results = await Promise.allSettled(
|
|
9623
9853
|
nonVoice.map(async (att, i) => {
|
|
9624
9854
|
const name = names[i];
|
|
9625
|
-
const localPath =
|
|
9855
|
+
const localPath = join6(UPLOADS_DIR, name);
|
|
9856
|
+
const timeoutMs = isImageAttachment(att) ? IMAGE_DOWNLOAD_TIMEOUT_MS : DOCUMENT_DOWNLOAD_TIMEOUT_MS;
|
|
9626
9857
|
const res = await fetch(att.url, {
|
|
9627
|
-
signal: AbortSignal.timeout(
|
|
9858
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
9628
9859
|
});
|
|
9629
|
-
if (!res.ok) {
|
|
9860
|
+
if (!res.ok || !res.body) {
|
|
9630
9861
|
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
9631
9862
|
}
|
|
9632
|
-
|
|
9633
|
-
|
|
9863
|
+
await pipeline(
|
|
9864
|
+
Readable.fromWeb(res.body),
|
|
9865
|
+
createWriteStream(localPath)
|
|
9866
|
+
);
|
|
9867
|
+
const { size } = await stat5(localPath);
|
|
9634
9868
|
log16.info("Attachment saved", {
|
|
9635
9869
|
filename: name,
|
|
9636
9870
|
path: localPath,
|
|
9637
|
-
bytes:
|
|
9871
|
+
bytes: size
|
|
9638
9872
|
});
|
|
9639
9873
|
let extractedTextPath;
|
|
9640
9874
|
if (att.extractedTextUrl) {
|
|
@@ -10505,6 +10739,7 @@ var HeadlessSession = class {
|
|
|
10505
10739
|
name: e.name,
|
|
10506
10740
|
result: e.result,
|
|
10507
10741
|
isError: e.isError,
|
|
10742
|
+
...e.recording && { recording: e.recording },
|
|
10508
10743
|
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
10509
10744
|
},
|
|
10510
10745
|
rid
|
|
@@ -10617,12 +10852,15 @@ var HeadlessSession = class {
|
|
|
10617
10852
|
const questions = Array.isArray(input?.questions) ? input.questions : [];
|
|
10618
10853
|
const batch = [];
|
|
10619
10854
|
const slots = [];
|
|
10855
|
+
let landings = 0;
|
|
10620
10856
|
for (const q of questions) {
|
|
10621
10857
|
if (q?.type !== "file" || typeof q.id !== "string") {
|
|
10622
10858
|
continue;
|
|
10623
10859
|
}
|
|
10624
10860
|
const val = answers[q.id];
|
|
10625
|
-
const
|
|
10861
|
+
const all = Array.isArray(val) ? val : [val];
|
|
10862
|
+
landings += all.filter((v) => v && v.kind === "store").length;
|
|
10863
|
+
const items = all.filter(isDescriptor);
|
|
10626
10864
|
if (items.length === 0) {
|
|
10627
10865
|
continue;
|
|
10628
10866
|
}
|
|
@@ -10641,6 +10879,9 @@ var HeadlessSession = class {
|
|
|
10641
10879
|
}
|
|
10642
10880
|
}
|
|
10643
10881
|
if (batch.length === 0) {
|
|
10882
|
+
if (landings > 0) {
|
|
10883
|
+
log17.info("promptUser store landings passed through", { landings });
|
|
10884
|
+
}
|
|
10644
10885
|
return raw;
|
|
10645
10886
|
}
|
|
10646
10887
|
let results;
|
|
@@ -10984,7 +11225,7 @@ var HeadlessSession = class {
|
|
|
10984
11225
|
* there would strand the chain steps and background results behind it.
|
|
10985
11226
|
*/
|
|
10986
11227
|
async drainQueueLoop() {
|
|
10987
|
-
|
|
11228
|
+
while (true) {
|
|
10988
11229
|
const at = this.queue.firstDeliverableIndex();
|
|
10989
11230
|
if (at === -1) {
|
|
10990
11231
|
return;
|