@mindstudio-ai/remy 0.1.145 → 0.1.147
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/buildFromInitialSpec.md +2 -10
- package/dist/automatedActions/buildFromRoadmap.md +2 -1
- package/dist/automatedActions/postBuildPolish.md +18 -0
- package/dist/automatedActions/postRoadmapBuild.md +13 -0
- package/dist/automatedActions/publish.md +2 -0
- package/dist/headless.js +262 -116
- package/dist/index.js +265 -121
- package/dist/prompt/compiled/interfaces.md +49 -37
- package/dist/prompt/static/authoring.md +1 -1
- package/dist/prompt/static/instructions.md +2 -2
- package/dist/prompt/static/team.md +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
trigger: buildFromInitialSpec
|
|
3
|
+
next: postBuildPolish
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
This is an automated action triggered by the user pressing "Build" in the editor after reviewing the spec.
|
|
6
7
|
|
|
7
|
-
The user has reviewed the spec and is ready to build. There are
|
|
8
|
+
The user has reviewed the spec and is ready to build. There are three phases: planning, coding, and verifying. Execute each phase in order in a single turn.
|
|
8
9
|
|
|
9
10
|
## Planning
|
|
10
11
|
Think about your approach and then get a quick sanity check from `codeSanityCheck` to make sure you aren't missing anything.
|
|
@@ -21,12 +22,3 @@ Then, build everything in one turn: tables, methods, interfaces, manifest update
|
|
|
21
22
|
- If the app has a web frontend, check the browser logs to make sure there are no errors rendering it.
|
|
22
23
|
- Use `runAutomatedBrowserTest` to smoke-test the main UI flow. The dev database is a disposable snapshot, so don't worry about being destructive. Fix any errors before finishing.
|
|
23
24
|
- If there is a scenario that seeds the app with mock data, use it to present the app to the user with initial data seeded, so they can see and play with the real app. Let the user know they can reset the app using a scenario to empty it if they wish. Showing the user something they can play with immediately is important when it comes to landing a strong first impression.
|
|
24
|
-
|
|
25
|
-
## Polishing
|
|
26
|
-
When verification is complete, take a step back and do an explicit polish pass before verifying. Re-read the spec files and the design expert's guidance, then walk through each frontend file looking for design details that got skipped in the initial build: animations, transitions, hover states, micro-interactions, spring physics, entrance reveals, gesture handling, layout issues, and anything else.
|
|
27
|
-
|
|
28
|
-
The initial build prioritizes getting everything connected and functional, but this pass closes the gap between "it works" and "it feels great." In many ways this is *the* most important part of the initial build, as the user's first experience of the deliverable will set their expectations for every iteration that follows. Don't mess this up.
|
|
29
|
-
|
|
30
|
-
Then, ask the `visualDesignExpert` to take a screenshot and verity that the visual design looks correct. Fix any issues it flags - we want the user's first time seeing the finished product to truly wow them.
|
|
31
|
-
|
|
32
|
-
When everything is working, use `productVision` to mark the MVP roadmap item as done, then call `setProjectOnboardingState({ state: "onboardingFinished" })`. Finally, call `compactConversation` to summarize the build session and free up context for the next phase of work.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
trigger: buildFromRoadmap
|
|
3
|
+
next: postRoadmapBuild
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
This is an automated action triggered by the user pressing "Build Now" on the roadmap item {{path}}
|
|
@@ -12,4 +13,4 @@ Then, put together a plan to build out the feature. Write the plan with `writePl
|
|
|
12
13
|
|
|
13
14
|
When they've approved the plan, be sure to update the spec first - remember, the spec is the source of truth about the product. Then, build everything in one turn, using the spec as the master plan.
|
|
14
15
|
|
|
15
|
-
When you're finished, verify your work
|
|
16
|
+
When you're finished building, verify your work and give the user a summary of what was done.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: postBuildPolish
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
This is an automated follow-up after the initial build. The code is written and verified. Now it's time to polish and finalize so we can deliver something beautiful and magical as the user's first experience with our work.
|
|
6
|
+
|
|
7
|
+
## Polishing
|
|
8
|
+
Take a step back and do an explicit polish pass. Re-read the spec files and the design expert's guidance, then walk through each frontend file looking for design details that got skipped in the initial build: layout animations, transitions, hover states, micro-interactions, spring physics, entrance reveals, gesture handling, layout issues, responsiveness, and anything else. We need this to feel truly amazing and wow the user - it's worth it to take the time to get it right.
|
|
9
|
+
|
|
10
|
+
The initial build prioritizes getting everything connected and functional, but this pass closes the gap between "it works" and "it feels great." In many ways this is *the* most important part of the initial build, as the user's first experience of the deliverable will set their expectations for every iteration that follows. Don't mess this up.
|
|
11
|
+
|
|
12
|
+
When you have finished, ask the `visualDesignExpert` to take a screenshot and verify that the visual design looks correct. Fix any issues it flags. We want the user's first time seeing the finished product to truly wow them.
|
|
13
|
+
|
|
14
|
+
## Finalizing
|
|
15
|
+
When everything is working and polished:
|
|
16
|
+
1. Use `productVision` to mark the MVP roadmap item as done.
|
|
17
|
+
2. Call `setProjectOnboardingState({ state: "onboardingFinished" })`.
|
|
18
|
+
3. Call `compactConversation` to summarize the build session and free up context for the next phase of work.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: postRoadmapBuild
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
This is an automated follow-up after building a roadmap feature. The code is written and verified. Now it's time to polish and finalize.
|
|
6
|
+
|
|
7
|
+
## Polishing
|
|
8
|
+
Take a step back and do an explicit polish pass. Re-read the spec files and the design expert's guidance, then walk through each frontend file you changed looking for design details that got skipped: animations, transitions, hover states, micro-interactions, and anything else that closes the gap between "it works" and "it feels great."
|
|
9
|
+
|
|
10
|
+
## Finalizing
|
|
11
|
+
When everything is working:
|
|
12
|
+
1. Tell `productVision` what was done so it can update the roadmap to reflect the progress.
|
|
13
|
+
2. Call `compactConversation` to summarize the build session and free up context.
|
|
@@ -14,4 +14,6 @@ If approved:
|
|
|
14
14
|
- Use `mindstudio-prod releases status --wait` to poll the build until it completes. Let the user know it's deploying, then report back when it's live.
|
|
15
15
|
- Once deployed, offer to help with next steps. This includes technical steps likesetting up a custom domain (`mindstudio-prod domains`), checking for errors (`mindstudio-prod requests stats`), seeding production data (`mindstudio-prod db`), managing env vars/secrets, or anything else they need for launch. It also includes going above and beyond and helping holistically. If it's the initial deploy, offer to help create collateral to announce the launch (e.g., an image for sharing on social media, text copy for a post, etc); if it's a meaningful incremental update, an annoucement post or something similar - go above and beyond here to help the user see that you care about the product from end-to-end, not just writing code! They will be appreciative, grateful, and pleased with your creativity here. Refer to the design guidance in the spec for how to talk about the product, and consider consulting the design expert to generate images or other marketing collateral.
|
|
16
16
|
|
|
17
|
+
After everything is done, call `compactConversation` to summarize the current session and free up context for the next phase of work.
|
|
18
|
+
|
|
17
19
|
If dismissed, acknowledge and do nothing.
|
package/dist/headless.js
CHANGED
|
@@ -6,7 +6,15 @@ var __export = (target, all) => {
|
|
|
6
6
|
|
|
7
7
|
// src/headless.ts
|
|
8
8
|
import { createInterface } from "readline";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
writeFileSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
existsSync
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { writeFile } from "fs/promises";
|
|
17
|
+
import { basename, join, extname } from "path";
|
|
10
18
|
|
|
11
19
|
// src/logger.ts
|
|
12
20
|
import fs from "fs";
|
|
@@ -139,87 +147,9 @@ function readJsonAsset(fallback, ...segments) {
|
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
// src/tools/_helpers/sidecar.ts
|
|
143
|
-
var log2 = createLogger("sidecar");
|
|
144
|
-
var baseUrl = null;
|
|
145
|
-
function setSidecarBaseUrl(url) {
|
|
146
|
-
baseUrl = url;
|
|
147
|
-
log2.info("Configured", { url });
|
|
148
|
-
}
|
|
149
|
-
function isSidecarConfigured() {
|
|
150
|
-
return baseUrl !== null;
|
|
151
|
-
}
|
|
152
|
-
async function sidecarRequest(endpoint, body = {}, options) {
|
|
153
|
-
if (!baseUrl) {
|
|
154
|
-
throw new Error("Sidecar not available");
|
|
155
|
-
}
|
|
156
|
-
const url = `${baseUrl}${endpoint}`;
|
|
157
|
-
try {
|
|
158
|
-
const res = await fetch(url, {
|
|
159
|
-
method: "POST",
|
|
160
|
-
headers: { "Content-Type": "application/json" },
|
|
161
|
-
body: JSON.stringify(body),
|
|
162
|
-
signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
|
|
163
|
-
});
|
|
164
|
-
if (!res.ok) {
|
|
165
|
-
log2.error("Sidecar error", { endpoint, status: res.status });
|
|
166
|
-
throw new Error(`Sidecar error: ${res.status}`);
|
|
167
|
-
}
|
|
168
|
-
const data = await res.json();
|
|
169
|
-
if (data?.success === false) {
|
|
170
|
-
const code = data.errorCode ? ` [${data.errorCode}]` : "";
|
|
171
|
-
throw new Error(`${data.error || "Unknown error"}${code}`);
|
|
172
|
-
}
|
|
173
|
-
return data;
|
|
174
|
-
} catch (err) {
|
|
175
|
-
if (err.message.startsWith("Sidecar error")) {
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
log2.error("Sidecar connection error", { endpoint, error: err.message });
|
|
179
|
-
throw new Error(`Sidecar connection error: ${err.message}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// src/tools/_helpers/lsp.ts
|
|
184
|
-
var setLspBaseUrl = setSidecarBaseUrl;
|
|
185
|
-
var isLspConfigured = isSidecarConfigured;
|
|
186
|
-
async function lspRequest(endpoint, body) {
|
|
187
|
-
return sidecarRequest(endpoint, body);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
150
|
// src/prompt/static/projectContext.ts
|
|
191
151
|
import fs4 from "fs";
|
|
192
152
|
import path3 from "path";
|
|
193
|
-
var AGENT_INSTRUCTION_FILES = [
|
|
194
|
-
"CLAUDE.md",
|
|
195
|
-
"claude.md",
|
|
196
|
-
".claude/instructions.md",
|
|
197
|
-
"AGENTS.md",
|
|
198
|
-
"agents.md",
|
|
199
|
-
".agents.md",
|
|
200
|
-
"COPILOT.md",
|
|
201
|
-
"copilot.md",
|
|
202
|
-
".copilot-instructions.md",
|
|
203
|
-
".github/copilot-instructions.md",
|
|
204
|
-
"REMY.md",
|
|
205
|
-
"remy.md",
|
|
206
|
-
".cursorrules",
|
|
207
|
-
".cursorules"
|
|
208
|
-
];
|
|
209
|
-
function loadProjectInstructions() {
|
|
210
|
-
for (const file of AGENT_INSTRUCTION_FILES) {
|
|
211
|
-
try {
|
|
212
|
-
const content = fs4.readFileSync(file, "utf-8").trim();
|
|
213
|
-
if (content) {
|
|
214
|
-
return `
|
|
215
|
-
## Project Instructions (${file})
|
|
216
|
-
${content}`;
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return "";
|
|
222
|
-
}
|
|
223
153
|
function loadProjectManifest() {
|
|
224
154
|
try {
|
|
225
155
|
const manifest = fs4.readFileSync("mindstudio.json", "utf-8");
|
|
@@ -346,7 +276,6 @@ function resolveIncludes(template) {
|
|
|
346
276
|
}
|
|
347
277
|
function buildSystemPrompt(onboardingState, viewContext) {
|
|
348
278
|
const projectContext = [
|
|
349
|
-
loadProjectInstructions(),
|
|
350
279
|
loadProjectManifest(),
|
|
351
280
|
loadSpecFileMetadata(),
|
|
352
281
|
loadProjectFileListing()
|
|
@@ -421,29 +350,26 @@ Current date: ${now}
|
|
|
421
350
|
{{compiled/msfm.md}}
|
|
422
351
|
</mindstudio_flavored_markdown_spec_docs>
|
|
423
352
|
|
|
424
|
-
<project_context>
|
|
425
|
-
${projectContext}
|
|
426
|
-
</project_context>
|
|
427
|
-
|
|
428
353
|
<intake_mode_instructions>
|
|
429
|
-
{{static/intake.md}}
|
|
354
|
+
{{static/intake.md}}
|
|
430
355
|
</intake_mode_instructions>
|
|
431
356
|
|
|
432
357
|
<spec_authoring_instructions>
|
|
433
|
-
{{static/authoring.md}}
|
|
358
|
+
{{static/authoring.md}}
|
|
434
359
|
</spec_authoring_instructions>
|
|
435
360
|
|
|
436
|
-
|
|
361
|
+
<team>
|
|
362
|
+
{{static/team.md}}
|
|
363
|
+
</team>
|
|
437
364
|
|
|
438
365
|
<code_authoring_instructions>
|
|
439
366
|
{{static/coding.md}}
|
|
440
|
-
|
|
367
|
+
|
|
368
|
+
<typescript_lsp>
|
|
441
369
|
{{static/lsp.md}}
|
|
442
|
-
</typescript_lsp
|
|
370
|
+
</typescript_lsp>
|
|
443
371
|
</code_authoring_instructions>
|
|
444
372
|
|
|
445
|
-
{{static/instructions.md}}
|
|
446
|
-
${loadPlanStatus()}
|
|
447
373
|
<conversation_summaries>
|
|
448
374
|
Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
|
|
449
375
|
|
|
@@ -457,30 +383,38 @@ New projects progress through four onboarding states. The user might skip this e
|
|
|
457
383
|
- **initialSpecAuthoring**: Writing and refining the first spec. The user can see it in the editor as it streams in and can give feedback to iterate on it. This phase covers both the initial draft and any back-and-forth refinement before code generation.
|
|
458
384
|
- **initialCodegen**: First code generation from the spec. The agent is generating methods, tables, interfaces, manifest updates, and scenarios. This can take a while and involves heavy tool use. The user sees a full-screen build progress view.
|
|
459
385
|
- **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
|
|
386
|
+
</project_onboarding>
|
|
387
|
+
|
|
388
|
+
{{static/instructions.md}}
|
|
460
389
|
|
|
461
390
|
<!-- cache_breakpoint -->
|
|
462
391
|
|
|
463
|
-
|
|
392
|
+
<current_project_onboarding_state>
|
|
464
393
|
${onboardingState ?? "onboardingFinished"}
|
|
465
|
-
|
|
466
|
-
|
|
394
|
+
</current_project_onboarding_state>
|
|
395
|
+
|
|
396
|
+
<project_context>
|
|
397
|
+
${projectContext}
|
|
398
|
+
</project_context>
|
|
467
399
|
|
|
468
400
|
<view_context>
|
|
469
401
|
The user is currently in ${viewContext?.mode ?? "code"} mode.
|
|
470
402
|
${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
|
|
471
403
|
</view_context>
|
|
404
|
+
|
|
405
|
+
${loadPlanStatus()}
|
|
472
406
|
`;
|
|
473
407
|
return resolveIncludes(template);
|
|
474
408
|
}
|
|
475
409
|
|
|
476
410
|
// src/api.ts
|
|
477
|
-
var
|
|
411
|
+
var log2 = createLogger("api");
|
|
478
412
|
async function* streamChat(params) {
|
|
479
413
|
const { baseUrl: baseUrl2, apiKey, signal, requestId, ...body } = params;
|
|
480
414
|
const url = `${baseUrl2}/_internal/v2/agent/remy/chat`;
|
|
481
415
|
const startTime = Date.now();
|
|
482
416
|
const subAgentId = body.subAgentId;
|
|
483
|
-
|
|
417
|
+
log2.info("API request", {
|
|
484
418
|
requestId,
|
|
485
419
|
...subAgentId && { subAgentId },
|
|
486
420
|
model: body.model,
|
|
@@ -500,13 +434,13 @@ async function* streamChat(params) {
|
|
|
500
434
|
});
|
|
501
435
|
} catch (err) {
|
|
502
436
|
if (signal?.aborted) {
|
|
503
|
-
|
|
437
|
+
log2.warn("Request aborted", {
|
|
504
438
|
requestId,
|
|
505
439
|
...subAgentId && { subAgentId }
|
|
506
440
|
});
|
|
507
441
|
throw err;
|
|
508
442
|
}
|
|
509
|
-
|
|
443
|
+
log2.error("Network error", {
|
|
510
444
|
requestId,
|
|
511
445
|
...subAgentId && { subAgentId },
|
|
512
446
|
error: err.message
|
|
@@ -515,7 +449,7 @@ async function* streamChat(params) {
|
|
|
515
449
|
return;
|
|
516
450
|
}
|
|
517
451
|
const ttfb = Date.now() - startTime;
|
|
518
|
-
|
|
452
|
+
log2.info("API response", {
|
|
519
453
|
requestId,
|
|
520
454
|
...subAgentId && { subAgentId },
|
|
521
455
|
status: res.status,
|
|
@@ -533,7 +467,7 @@ async function* streamChat(params) {
|
|
|
533
467
|
}
|
|
534
468
|
} catch {
|
|
535
469
|
}
|
|
536
|
-
|
|
470
|
+
log2.error("API error", {
|
|
537
471
|
requestId,
|
|
538
472
|
...subAgentId && { subAgentId },
|
|
539
473
|
status: res.status,
|
|
@@ -546,6 +480,7 @@ async function* streamChat(params) {
|
|
|
546
480
|
const reader = res.body.getReader();
|
|
547
481
|
const decoder = new TextDecoder();
|
|
548
482
|
let buffer = "";
|
|
483
|
+
let receivedDone = false;
|
|
549
484
|
while (true) {
|
|
550
485
|
let stallTimer;
|
|
551
486
|
let readResult;
|
|
@@ -563,7 +498,7 @@ async function* streamChat(params) {
|
|
|
563
498
|
} catch {
|
|
564
499
|
clearTimeout(stallTimer);
|
|
565
500
|
await reader.cancel();
|
|
566
|
-
|
|
501
|
+
log2.error("Stream stalled", {
|
|
567
502
|
requestId,
|
|
568
503
|
...subAgentId && { subAgentId },
|
|
569
504
|
durationMs: Date.now() - startTime
|
|
@@ -589,7 +524,8 @@ async function* streamChat(params) {
|
|
|
589
524
|
const event = JSON.parse(line.slice(6));
|
|
590
525
|
if (event.type === "done") {
|
|
591
526
|
const elapsed = Date.now() - startTime;
|
|
592
|
-
|
|
527
|
+
receivedDone = true;
|
|
528
|
+
log2.info("Stream complete", {
|
|
593
529
|
requestId,
|
|
594
530
|
...subAgentId && { subAgentId },
|
|
595
531
|
durationMs: elapsed,
|
|
@@ -597,12 +533,27 @@ async function* streamChat(params) {
|
|
|
597
533
|
inputTokens: event.usage.inputTokens,
|
|
598
534
|
outputTokens: event.usage.outputTokens
|
|
599
535
|
});
|
|
536
|
+
} else if (event.type === "error") {
|
|
537
|
+
log2.error("SSE error event", {
|
|
538
|
+
requestId,
|
|
539
|
+
...subAgentId && { subAgentId },
|
|
540
|
+
error: event.error,
|
|
541
|
+
durationMs: Date.now() - startTime
|
|
542
|
+
});
|
|
600
543
|
}
|
|
601
544
|
yield event;
|
|
602
545
|
} catch {
|
|
603
546
|
}
|
|
604
547
|
}
|
|
605
548
|
}
|
|
549
|
+
if (!receivedDone) {
|
|
550
|
+
log2.warn("Stream ended without done event", {
|
|
551
|
+
requestId,
|
|
552
|
+
...subAgentId && { subAgentId },
|
|
553
|
+
durationMs: Date.now() - startTime,
|
|
554
|
+
remainingBuffer: buffer.slice(0, 200)
|
|
555
|
+
});
|
|
556
|
+
}
|
|
606
557
|
if (buffer.startsWith("data: ")) {
|
|
607
558
|
try {
|
|
608
559
|
yield JSON.parse(buffer.slice(6));
|
|
@@ -639,7 +590,7 @@ async function* streamChatWithRetry(params, options) {
|
|
|
639
590
|
return;
|
|
640
591
|
}
|
|
641
592
|
const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;
|
|
642
|
-
|
|
593
|
+
log2.warn("Retrying", {
|
|
643
594
|
requestId: params.requestId,
|
|
644
595
|
attempt: attempt + 1,
|
|
645
596
|
maxRetries: MAX_RETRIES,
|
|
@@ -681,7 +632,7 @@ async function generateBackgroundAck(params) {
|
|
|
681
632
|
}
|
|
682
633
|
|
|
683
634
|
// src/compaction/index.ts
|
|
684
|
-
var
|
|
635
|
+
var log3 = createLogger("compaction");
|
|
685
636
|
var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
|
|
686
637
|
var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
|
|
687
638
|
var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
|
|
@@ -745,7 +696,7 @@ async function compactConversation(messages, apiConfig, system, tools2) {
|
|
|
745
696
|
}
|
|
746
697
|
]
|
|
747
698
|
}));
|
|
748
|
-
|
|
699
|
+
log3.info("Compaction complete", { summaries: summaries.length });
|
|
749
700
|
return checkpointMessages;
|
|
750
701
|
}
|
|
751
702
|
function findSafeInsertionPoint(messages) {
|
|
@@ -849,7 +800,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
849
800
|
if (!serialized.trim()) {
|
|
850
801
|
return null;
|
|
851
802
|
}
|
|
852
|
-
|
|
803
|
+
log3.info("Generating summary", {
|
|
853
804
|
name,
|
|
854
805
|
messageCount: messagesToSummarize.length,
|
|
855
806
|
cacheReuse: !!mainSystem
|
|
@@ -875,15 +826,15 @@ ${serialized}` : serialized;
|
|
|
875
826
|
if (event.type === "text") {
|
|
876
827
|
summaryText += event.text;
|
|
877
828
|
} else if (event.type === "error") {
|
|
878
|
-
|
|
829
|
+
log3.error("Summary generation failed", { name, error: event.error });
|
|
879
830
|
return null;
|
|
880
831
|
}
|
|
881
832
|
}
|
|
882
833
|
if (!summaryText.trim()) {
|
|
883
|
-
|
|
834
|
+
log3.warn("Empty summary generated", { name });
|
|
884
835
|
return null;
|
|
885
836
|
}
|
|
886
|
-
|
|
837
|
+
log3.info("Summary generated", { name, summaryLength: summaryText.length });
|
|
887
838
|
return summaryText.trim();
|
|
888
839
|
}
|
|
889
840
|
|
|
@@ -2439,6 +2390,50 @@ var editsFinishedTool = {
|
|
|
2439
2390
|
}
|
|
2440
2391
|
};
|
|
2441
2392
|
|
|
2393
|
+
// src/tools/_helpers/sidecar.ts
|
|
2394
|
+
var log4 = createLogger("sidecar");
|
|
2395
|
+
var baseUrl = null;
|
|
2396
|
+
function setSidecarBaseUrl(url) {
|
|
2397
|
+
baseUrl = url;
|
|
2398
|
+
log4.info("Configured", { url });
|
|
2399
|
+
}
|
|
2400
|
+
async function sidecarRequest(endpoint, body = {}, options) {
|
|
2401
|
+
if (!baseUrl) {
|
|
2402
|
+
throw new Error("Sidecar not available");
|
|
2403
|
+
}
|
|
2404
|
+
const url = `${baseUrl}${endpoint}`;
|
|
2405
|
+
try {
|
|
2406
|
+
const res = await fetch(url, {
|
|
2407
|
+
method: "POST",
|
|
2408
|
+
headers: { "Content-Type": "application/json" },
|
|
2409
|
+
body: JSON.stringify(body),
|
|
2410
|
+
signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
|
|
2411
|
+
});
|
|
2412
|
+
if (!res.ok) {
|
|
2413
|
+
log4.error("Sidecar error", { endpoint, status: res.status });
|
|
2414
|
+
throw new Error(`Sidecar error: ${res.status}`);
|
|
2415
|
+
}
|
|
2416
|
+
const data = await res.json();
|
|
2417
|
+
if (data?.success === false) {
|
|
2418
|
+
const code = data.errorCode ? ` [${data.errorCode}]` : "";
|
|
2419
|
+
throw new Error(`${data.error || "Unknown error"}${code}`);
|
|
2420
|
+
}
|
|
2421
|
+
return data;
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
if (err.message.startsWith("Sidecar error")) {
|
|
2424
|
+
throw err;
|
|
2425
|
+
}
|
|
2426
|
+
log4.error("Sidecar connection error", { endpoint, error: err.message });
|
|
2427
|
+
throw new Error(`Sidecar connection error: ${err.message}`);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// src/tools/_helpers/lsp.ts
|
|
2432
|
+
var setLspBaseUrl = setSidecarBaseUrl;
|
|
2433
|
+
async function lspRequest(endpoint, body) {
|
|
2434
|
+
return sidecarRequest(endpoint, body);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2442
2437
|
// src/tools/code/lspDiagnostics.ts
|
|
2443
2438
|
var lspDiagnosticsTool = {
|
|
2444
2439
|
clearable: true,
|
|
@@ -6030,13 +6025,24 @@ function resolveAction(text) {
|
|
|
6030
6025
|
}
|
|
6031
6026
|
}
|
|
6032
6027
|
let body = readAsset("automatedActions", `${triggerName}.md`);
|
|
6028
|
+
let next;
|
|
6029
|
+
const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
6030
|
+
if (fmMatch) {
|
|
6031
|
+
const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
|
|
6032
|
+
if (nextMatch) {
|
|
6033
|
+
next = nextMatch[1];
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6033
6036
|
body = body.replace(/^---[\s\S]*?---\s*/, "");
|
|
6034
6037
|
for (const [key, value] of Object.entries(params)) {
|
|
6035
6038
|
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
6036
6039
|
body = body.replaceAll(`{{${key}}}`, str);
|
|
6037
6040
|
}
|
|
6038
|
-
return
|
|
6039
|
-
|
|
6041
|
+
return {
|
|
6042
|
+
message: `@@automated::${triggerName}@@
|
|
6043
|
+
${body}`,
|
|
6044
|
+
next
|
|
6045
|
+
};
|
|
6040
6046
|
}
|
|
6041
6047
|
|
|
6042
6048
|
// src/headless.ts
|
|
@@ -6098,6 +6104,7 @@ async function startHeadless(opts = {}) {
|
|
|
6098
6104
|
let currentRequestId;
|
|
6099
6105
|
let completedEmitted = false;
|
|
6100
6106
|
let turnStart = 0;
|
|
6107
|
+
let pendingNextAction;
|
|
6101
6108
|
const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
6102
6109
|
const pendingTools = /* @__PURE__ */ new Map();
|
|
6103
6110
|
const earlyResults = /* @__PURE__ */ new Map();
|
|
@@ -6248,10 +6255,19 @@ ${xmlParts}
|
|
|
6248
6255
|
applyPendingSummaries();
|
|
6249
6256
|
applyPendingBlockUpdates();
|
|
6250
6257
|
flushBackgroundQueue();
|
|
6258
|
+
if (pendingNextAction) {
|
|
6259
|
+
const next = pendingNextAction;
|
|
6260
|
+
pendingNextAction = void 0;
|
|
6261
|
+
handleMessage(
|
|
6262
|
+
{ action: "message", text: `@@automated::${next}@@` },
|
|
6263
|
+
`chain-${Date.now()}`
|
|
6264
|
+
);
|
|
6265
|
+
}
|
|
6251
6266
|
}, 0);
|
|
6252
6267
|
return;
|
|
6253
6268
|
case "turn_cancelled":
|
|
6254
6269
|
completedEmitted = true;
|
|
6270
|
+
pendingNextAction = void 0;
|
|
6255
6271
|
emit("completed", { success: false, error: "cancelled" }, rid);
|
|
6256
6272
|
return;
|
|
6257
6273
|
// Streaming events — forward with requestId
|
|
@@ -6366,6 +6382,120 @@ ${xmlParts}
|
|
|
6366
6382
|
}
|
|
6367
6383
|
}
|
|
6368
6384
|
toolRegistry.onEvent = onEvent;
|
|
6385
|
+
const UPLOADS_DIR = "src/.user-uploads";
|
|
6386
|
+
function filenameFromUrl(url) {
|
|
6387
|
+
try {
|
|
6388
|
+
const pathname = new URL(url).pathname;
|
|
6389
|
+
const name = basename(pathname);
|
|
6390
|
+
return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
|
|
6391
|
+
} catch {
|
|
6392
|
+
return `upload-${Date.now()}`;
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
function resolveUniqueFilename(name) {
|
|
6396
|
+
if (!existsSync(join(UPLOADS_DIR, name))) {
|
|
6397
|
+
return name;
|
|
6398
|
+
}
|
|
6399
|
+
const ext = extname(name);
|
|
6400
|
+
const base = name.slice(0, name.length - ext.length);
|
|
6401
|
+
let counter = 1;
|
|
6402
|
+
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
6403
|
+
counter++;
|
|
6404
|
+
}
|
|
6405
|
+
return `${base}-${counter}${ext}`;
|
|
6406
|
+
}
|
|
6407
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6408
|
+
".png",
|
|
6409
|
+
".jpg",
|
|
6410
|
+
".jpeg",
|
|
6411
|
+
".gif",
|
|
6412
|
+
".webp",
|
|
6413
|
+
".svg",
|
|
6414
|
+
".bmp",
|
|
6415
|
+
".ico",
|
|
6416
|
+
".tiff",
|
|
6417
|
+
".tif",
|
|
6418
|
+
".avif",
|
|
6419
|
+
".heic",
|
|
6420
|
+
".heif"
|
|
6421
|
+
]);
|
|
6422
|
+
function isImageAttachment(att) {
|
|
6423
|
+
const name = att.filename || filenameFromUrl(att.url);
|
|
6424
|
+
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
6425
|
+
}
|
|
6426
|
+
async function persistAttachments(attachments) {
|
|
6427
|
+
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
6428
|
+
if (nonVoice.length === 0) {
|
|
6429
|
+
return { documents: [], images: [] };
|
|
6430
|
+
}
|
|
6431
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
6432
|
+
const results = await Promise.allSettled(
|
|
6433
|
+
nonVoice.map(async (att) => {
|
|
6434
|
+
const name = resolveUniqueFilename(
|
|
6435
|
+
att.filename || filenameFromUrl(att.url)
|
|
6436
|
+
);
|
|
6437
|
+
const localPath = join(UPLOADS_DIR, name);
|
|
6438
|
+
const res = await fetch(att.url, {
|
|
6439
|
+
signal: AbortSignal.timeout(3e4)
|
|
6440
|
+
});
|
|
6441
|
+
if (!res.ok) {
|
|
6442
|
+
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
6443
|
+
}
|
|
6444
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
6445
|
+
await writeFile(localPath, buffer);
|
|
6446
|
+
log11.info("Attachment saved", {
|
|
6447
|
+
filename: name,
|
|
6448
|
+
path: localPath,
|
|
6449
|
+
bytes: buffer.length
|
|
6450
|
+
});
|
|
6451
|
+
let extractedTextPath;
|
|
6452
|
+
if (att.extractedTextUrl) {
|
|
6453
|
+
try {
|
|
6454
|
+
const textRes = await fetch(att.extractedTextUrl, {
|
|
6455
|
+
signal: AbortSignal.timeout(3e4)
|
|
6456
|
+
});
|
|
6457
|
+
if (textRes.ok) {
|
|
6458
|
+
extractedTextPath = `${localPath}.txt`;
|
|
6459
|
+
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
6460
|
+
log11.info("Extracted text saved", { path: extractedTextPath });
|
|
6461
|
+
}
|
|
6462
|
+
} catch {
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
return { filename: name, localPath, extractedTextPath };
|
|
6466
|
+
})
|
|
6467
|
+
);
|
|
6468
|
+
const settled = results.map((r, i) => ({
|
|
6469
|
+
result: r.status === "fulfilled" ? r.value : null,
|
|
6470
|
+
isImage: isImageAttachment(nonVoice[i])
|
|
6471
|
+
}));
|
|
6472
|
+
return {
|
|
6473
|
+
documents: settled.filter((s) => !s.isImage).map((s) => s.result),
|
|
6474
|
+
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
6475
|
+
};
|
|
6476
|
+
}
|
|
6477
|
+
function buildUploadHeader(results) {
|
|
6478
|
+
const succeeded = results.filter(Boolean);
|
|
6479
|
+
if (succeeded.length === 0) {
|
|
6480
|
+
return "";
|
|
6481
|
+
}
|
|
6482
|
+
if (succeeded.length === 1) {
|
|
6483
|
+
const r = succeeded[0];
|
|
6484
|
+
const parts = [`[Uploaded file: ${r.localPath}`];
|
|
6485
|
+
if (r.extractedTextPath) {
|
|
6486
|
+
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
6487
|
+
}
|
|
6488
|
+
return parts.join(" \u2014 ") + "]";
|
|
6489
|
+
}
|
|
6490
|
+
const lines = succeeded.map((r) => {
|
|
6491
|
+
if (r.extractedTextPath) {
|
|
6492
|
+
return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
|
|
6493
|
+
}
|
|
6494
|
+
return `- ${r.localPath}`;
|
|
6495
|
+
});
|
|
6496
|
+
return `[Uploaded files]
|
|
6497
|
+
${lines.join("\n")}`;
|
|
6498
|
+
}
|
|
6369
6499
|
async function handleMessage(parsed, requestId) {
|
|
6370
6500
|
if (running) {
|
|
6371
6501
|
emit(
|
|
@@ -6387,12 +6517,26 @@ ${xmlParts}
|
|
|
6387
6517
|
turnStart = Date.now();
|
|
6388
6518
|
const attachments = parsed.attachments;
|
|
6389
6519
|
if (attachments?.length) {
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
attachments.map((a) => a.url)
|
|
6393
|
-
);
|
|
6520
|
+
log11.info("Message has attachments", {
|
|
6521
|
+
count: attachments.length,
|
|
6522
|
+
urls: attachments.map((a) => a.url)
|
|
6523
|
+
});
|
|
6394
6524
|
}
|
|
6395
6525
|
let userMessage = parsed.text ?? "";
|
|
6526
|
+
if (attachments?.some((a) => !a.isVoice)) {
|
|
6527
|
+
try {
|
|
6528
|
+
const { documents, images } = await persistAttachments(attachments);
|
|
6529
|
+
const all = [...documents, ...images];
|
|
6530
|
+
const header = buildUploadHeader(all);
|
|
6531
|
+
if (header) {
|
|
6532
|
+
userMessage = userMessage ? `${header}
|
|
6533
|
+
|
|
6534
|
+
${userMessage}` : header;
|
|
6535
|
+
}
|
|
6536
|
+
} catch (err) {
|
|
6537
|
+
log11.warn("Attachment persistence failed", { error: err.message });
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6396
6540
|
let resolved = null;
|
|
6397
6541
|
try {
|
|
6398
6542
|
resolved = resolveAction(userMessage);
|
|
@@ -6404,8 +6548,10 @@ ${xmlParts}
|
|
|
6404
6548
|
);
|
|
6405
6549
|
return;
|
|
6406
6550
|
}
|
|
6551
|
+
pendingNextAction = void 0;
|
|
6407
6552
|
if (resolved !== null) {
|
|
6408
|
-
userMessage = resolved;
|
|
6553
|
+
userMessage = resolved.message;
|
|
6554
|
+
pendingNextAction = resolved.next;
|
|
6409
6555
|
}
|
|
6410
6556
|
const isHidden = resolved !== null || !!parsed.hidden;
|
|
6411
6557
|
const rawText = parsed.text ?? "";
|