@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.
@@ -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 four phases to building: planning, coding, verifying, polishing. Execute each phase in order in a single turn.
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, then tell `productVision` what was done so it can update the roadmap to reflect the progress. Give the user a summary of what was done, then call `compactConversation` to summarize the build session and free up context.
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 { writeFileSync, readFileSync, unlinkSync } from "fs";
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
- {{static/team.md}}
361
+ <team>
362
+ {{static/team.md}}
363
+ </team>
437
364
 
438
365
  <code_authoring_instructions>
439
366
  {{static/coding.md}}
440
- ${isLspConfigured() ? `<typescript_lsp>
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
- <current_project_onboarding_state>
392
+ <current_project_onboarding_state>
464
393
  ${onboardingState ?? "onboardingFinished"}
465
- </current_project_onboarding_state>
466
- </project_onboarding>
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 log3 = createLogger("api");
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
- log3.info("API request", {
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
- log3.warn("Request aborted", {
437
+ log2.warn("Request aborted", {
504
438
  requestId,
505
439
  ...subAgentId && { subAgentId }
506
440
  });
507
441
  throw err;
508
442
  }
509
- log3.error("Network error", {
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
- log3.info("API response", {
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
- log3.error("API error", {
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
- log3.error("Stream stalled", {
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
- log3.info("Stream complete", {
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
- log3.warn("Retrying", {
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 log4 = createLogger("compaction");
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
- log4.info("Compaction complete", { summaries: summaries.length });
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
- log4.info("Generating summary", {
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
- log4.error("Summary generation failed", { name, error: event.error });
829
+ log3.error("Summary generation failed", { name, error: event.error });
879
830
  return null;
880
831
  }
881
832
  }
882
833
  if (!summaryText.trim()) {
883
- log4.warn("Empty summary generated", { name });
834
+ log3.warn("Empty summary generated", { name });
884
835
  return null;
885
836
  }
886
- log4.info("Summary generated", { name, summaryLength: summaryText.length });
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 `@@automated::${triggerName}@@
6039
- ${body}`;
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
- console.warn(
6391
- `[headless] Message has ${attachments.length} attachment(s):`,
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 ?? "";