@mindstudio-ai/remy 0.1.199 → 0.1.201
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/README.md +16 -4
- package/dist/automatedActions/debugRequest.md +1 -1
- package/dist/automatedActions/feelingLucky.md +1 -1
- package/dist/automatedActions/postBuildPolish.md +4 -3
- package/dist/brandExtraction/extract.md +1 -1
- package/dist/compaction/conversation.md +1 -1
- package/dist/headless.d.ts +9 -7
- package/dist/headless.js +193 -55
- package/dist/index.js +206 -60
- package/dist/prompt/.notes.md +1 -1
- package/dist/prompt/compiled/README.md +2 -2
- package/dist/prompt/compiled/auth.md +2 -2
- package/dist/prompt/compiled/design.md +2 -2
- package/dist/prompt/compiled/interfaces.md +1 -1
- package/dist/prompt/compiled/platform.md +2 -2
- package/dist/prompt/compiled/sdk-actions.md +2 -2
- package/dist/prompt/static/coding.md +1 -1
- package/dist/prompt/static/identity.md +1 -1
- package/dist/prompt/static/instructions.md +1 -0
- package/dist/prompt/static/intake.md +1 -1
- package/dist/subagents/codeSanityCheck/prompt.md +2 -2
- package/dist/subagents/designExpert/prompts/frontend-design-notes.md +2 -2
- package/dist/subagents/designExpert/prompts/ui-patterns.md +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Remy
|
|
2
2
|
|
|
3
|
-
A spec-building and coding agent for
|
|
3
|
+
A spec-building and coding agent for building apps.
|
|
4
4
|
|
|
5
|
-
Remy helps users design, spec, build, and iterate on
|
|
5
|
+
Remy helps users design, spec, build, and iterate on app projects. It runs locally in a terminal or as a headless subprocess in the sandbox. It has tools for reading/writing specs and code, running shell commands, searching code, prompting users with structured forms, and (in the sandbox) TypeScript language server integration. LLM calls are routed through the Remy platform for billing and model routing.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ Remy helps users design, spec, build, and iterate on MindStudio projects. It run
|
|
|
11
11
|
mindstudio login
|
|
12
12
|
|
|
13
13
|
# Navigate to your project
|
|
14
|
-
cd my-
|
|
14
|
+
cd my-app
|
|
15
15
|
|
|
16
16
|
# Run remy
|
|
17
17
|
npx remy
|
|
@@ -59,6 +59,7 @@ Tool availability depends on the project's onboarding state, sent by the sandbox
|
|
|
59
59
|
| `visualDesignExpert` | Visual design expert for fonts, colors, palettes, gradients, layouts, imagery, and icons (sub-agent) |
|
|
60
60
|
| `productVision` | Owns the product roadmap — creates/updates/deletes roadmap items in `src/roadmap/` (sub-agent) |
|
|
61
61
|
| `codeSanityCheck` | Quick readonly sanity check on architecture and package choices before building (sub-agent) |
|
|
62
|
+
| `writeBuildOverview` | Generate/refresh the Build Overview (`src/overview.html`) — a single-page plain-language reference of everything the app contains; you author the copy, the design expert lays it out |
|
|
62
63
|
|
|
63
64
|
### Spec Tools
|
|
64
65
|
|
|
@@ -307,14 +308,24 @@ Return the full conversation history.
|
|
|
307
308
|
|
|
308
309
|
Messages with `hidden: true` were generated by `runCommand` actions and should not be displayed in the UI.
|
|
309
310
|
|
|
311
|
+
The response also includes `queuedMessages` — the current pending-queue snapshot (possibly empty) — so a client can render the queue immediately on connect/reconnect. Live changes after that arrive via `queue_changed`.
|
|
312
|
+
|
|
310
313
|
#### `cancel`
|
|
311
314
|
|
|
312
|
-
Cancel the current turn. The cancel command gets `completed(success:true)`. The in-flight message command (if any) gets its own `completed(success:false, error:"cancelled")`.
|
|
315
|
+
Cancel the current turn. The cancel command gets `completed(success:true)`; any queued messages are flushed and returned on it as `cancelledMessages`. The in-flight message command (if any) gets its own `completed(success:false, error:"cancelled")`.
|
|
313
316
|
|
|
314
317
|
```json
|
|
315
318
|
{"action": "cancel", "requestId": "r3"}
|
|
316
319
|
```
|
|
317
320
|
|
|
321
|
+
#### `cancelQueued`
|
|
322
|
+
|
|
323
|
+
Cancel pending **queued** messages without touching the in-flight turn. Omit `id` to cancel all queued user messages, or pass `id` (the `requestId` of a queued message) to cancel just that one. Only user messages are cancellable — chained and background messages are part of a system chain and are never removed. Responds with `completed(success:true, cancelledQueued:[...])` listing what was removed; a `queue_changed` event also fires with the updated queue.
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{"action": "cancelQueued", "requestId": "r6", "id": "r2"}
|
|
327
|
+
```
|
|
328
|
+
|
|
318
329
|
#### `clear`
|
|
319
330
|
|
|
320
331
|
Clear conversation history and delete the session file.
|
|
@@ -341,6 +352,7 @@ Events are emitted as newline-delimited JSON. Command responses include `request
|
|
|
341
352
|
|-------|--------|-------------|
|
|
342
353
|
| `ready` | | Headless mode initialized, ready for input |
|
|
343
354
|
| `session_restored` | `messageCount` | Previous session loaded |
|
|
355
|
+
| `queue_changed` | `queuedMessages` | Queue contents changed (any push/shift/drain/cancel). Carries the full current snapshot — an empty array when the queue drains. The live queue-state signal; for the initial snapshot on connect/reconnect, read `queuedMessages` from `get_history`. |
|
|
344
356
|
| `stopping` | | Shutdown initiated |
|
|
345
357
|
| `stopped` | | Shutdown complete |
|
|
346
358
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
trigger: debugRequest
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
This is an automated message triggered by the user having clicked "Debug" in the "Request" log detail in
|
|
5
|
+
This is an automated message triggered by the user having clicked "Debug" in the "Request" log detail in the Remy UI. Find the request in .logs/requests.ndjson by its ID. If there is an error, fix it immediately - otherwise, explain the request at a high-level in non-technical/natural language and see what the user wishes to do with it. Remember, the user can't see this message, so keep that in mind when responding.
|
|
6
6
|
|
|
7
7
|
<request_id>
|
|
8
8
|
{{requestId}}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
trigger: feelingLucky
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
This is an automated message triggered by the user having clicked "I'm feeling lucky" in the
|
|
5
|
+
This is an automated message triggered by the user having clicked "I'm feeling lucky" in the Remy UI and selecting a category from a list of app archetypes. They can't see this message. They're likely new to Remy - this might even be their first time using it. They haven't described a specific idea yet and more than likely don't actually have anything specific in mind — they're exploring what they can build and clicked on something that looked interesting. Your job is to ask questions to help them conjure the seed of an idea that is unique, specific, and interesting, and will be compelling to build as an MVP. Don't build something generic - help the user make something unique that allows you to truly show off the power of what you can build. Stay focused on web interfaces for now, unless the user specifically wants a different interface type. When you've aligned on something to build, focus on making it visually beautiful and highly usable from a UX perspective - at this point in the user's experience they're going to be much more "wowed" by something that looks pretty than by something with a ton of backend complexity.
|
|
6
6
|
|
|
7
7
|
<selected_category>
|
|
8
8
|
{{title}}
|
|
@@ -13,6 +13,7 @@ The visual assets — photography, generated images, brand colors, typography
|
|
|
13
13
|
|
|
14
14
|
## Finalizing
|
|
15
15
|
When everything is working and polished:
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
18
|
-
3. Call `
|
|
16
|
+
1. Write the Build Overview. Author the full copy of everything this build actually produced — the data stores and what each holds, the backend operations, the interfaces, any access and role boundaries, background jobs, and seeded scenarios, including the parts the user can't see — in plain, calm, present-tense language, with real names and exact counts. Then call `writeBuildOverview` to render it. Do this now, before compacting, while the build is still fresh in context.
|
|
17
|
+
2. Use `productVision` to mark the MVP roadmap item as done.
|
|
18
|
+
3. Call `compactConversation` to summarize the build session and free up context for the next phase of work.
|
|
19
|
+
4. Call `setProjectOnboardingState({ state: "buildComplete" })`. This triggers the reveal experience on the frontend, where the user sees their finished app and its Build Overview for the first time.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You extract a structured `AppBrand` JSON object from the spec files of
|
|
1
|
+
You extract a structured `AppBrand` JSON object from the spec files of an app project.
|
|
2
2
|
|
|
3
3
|
Your output is read by a frontend renderer that uses the brand to style internal documents (implementation plans, sync plans, publish plans) with a "letterhead" treatment — a wordmark, accent color, paper-tone background, and branded fonts. Every field is optional. The renderer falls back to generic styling when a field is missing or invalid.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You are summarizing a conversation between a user and an AI coding agent called Remy that builds
|
|
1
|
+
You are summarizing a conversation between a user and an AI coding agent called Remy that builds apps. Your summary will replace the conversation history — the agent will only see your summary plus any new messages, so it must capture everything needed to continue working.
|
|
2
2
|
|
|
3
3
|
Write a tight, factual summary in bullet points. Cover what matters for picking the work back up:
|
|
4
4
|
- What the user is trying to do, and any key decisions, constraints, or preferences they've stated.
|
package/dist/headless.d.ts
CHANGED
|
@@ -68,14 +68,9 @@ declare class HeadlessSession {
|
|
|
68
68
|
start(): Promise<void>;
|
|
69
69
|
private shutdown;
|
|
70
70
|
private emit;
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
* `queuedMessages` if the queue has items (sandbox uses this to know the
|
|
74
|
-
* agent is still busy with pipeline work).
|
|
75
|
-
*/
|
|
71
|
+
/** Emit a `completed` event and mark completedEmitted. Queue state is
|
|
72
|
+
* surfaced separately via the `queue_changed` event, not on `completed`. */
|
|
76
73
|
private emitCompleted;
|
|
77
|
-
/** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
|
|
78
|
-
private queueFields;
|
|
79
74
|
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
80
75
|
private dispatchSimple;
|
|
81
76
|
/** Persist sessionStats + queue snapshot to .remy-stats.json. */
|
|
@@ -138,6 +133,13 @@ declare class HeadlessSession {
|
|
|
138
133
|
private handleChangeModels;
|
|
139
134
|
/** Cancel the running turn and drain the queue. Returns the drained items. */
|
|
140
135
|
private handleCancel;
|
|
136
|
+
/**
|
|
137
|
+
* Remove pending queued messages — all user messages, or one by id.
|
|
138
|
+
* Only `source: 'user'` items are removable; chained and background
|
|
139
|
+
* messages are part of a system chain and are never cancellable. Does
|
|
140
|
+
* not affect the in-flight turn (use `cancel` for that).
|
|
141
|
+
*/
|
|
142
|
+
private handleCancelQueued;
|
|
141
143
|
private handleStdinLine;
|
|
142
144
|
}
|
|
143
145
|
|
package/dist/headless.js
CHANGED
|
@@ -4680,9 +4680,9 @@ ${parts.join("\n\n")}
|
|
|
4680
4680
|
}
|
|
4681
4681
|
function loadPlatformBrief() {
|
|
4682
4682
|
return `<platform_brief>
|
|
4683
|
-
## What is a
|
|
4683
|
+
## What is a Remy app?
|
|
4684
4684
|
|
|
4685
|
-
A
|
|
4685
|
+
A Remy app is a managed full-stack TypeScript project with three layers: a spec (natural language in src/), a backend contract (methods, tables, roles in dist/), and one or more interfaces (web, API, bots, cron, etc.). The spec is the source of truth; code is derived from it.
|
|
4686
4686
|
|
|
4687
4687
|
This is a capable, stable platform used in production by 100k+ users. Build with confidence \u2014 you're building production-grade apps, not fragile prototypes.
|
|
4688
4688
|
|
|
@@ -4727,7 +4727,7 @@ TypeScript running in a sandboxed environment. Any npm package can be installed.
|
|
|
4727
4727
|
|
|
4728
4728
|
The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (OpenAI, Anthropic, Google, Meta, Mistral, and more) and 1000+ integrations (email, SMS, Slack, HubSpot, Google Workspace, web scraping, image/video generation, media processing, and much more) with zero configuration \u2014 credentials are handled automatically in the execution environment. No API keys needed. This SDK is robust and battle-tested in production.
|
|
4729
4729
|
|
|
4730
|
-
## What
|
|
4730
|
+
## What Remy apps are NOT good for
|
|
4731
4731
|
|
|
4732
4732
|
- Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
|
|
4733
4733
|
- Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
|
|
@@ -5391,6 +5391,94 @@ var scrapeWebUrlTool = {
|
|
|
5391
5391
|
}
|
|
5392
5392
|
};
|
|
5393
5393
|
|
|
5394
|
+
// src/tools/spec/writeBuildOverview.ts
|
|
5395
|
+
import fs20 from "fs";
|
|
5396
|
+
var OVERVIEW_FILE = "src/overview.html";
|
|
5397
|
+
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).
|
|
5398
|
+
|
|
5399
|
+
Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand. If <current_overview> is non-empty, use it as your starting point and preserve its established skin, updating only what the copy changed.
|
|
5400
|
+
|
|
5401
|
+
### The single hard rule
|
|
5402
|
+
Preserve every fact in <overview_copy> exactly \u2014 every number, name, label, and claim. Do not add, drop, soften, reword, or invent any fact. You own layout, typography, and visual design only; the substance and the words are fixed. A single wrong number breaks this document's entire purpose.
|
|
5403
|
+
|
|
5404
|
+
### What it is (and is not)
|
|
5405
|
+
- A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
|
|
5406
|
+
- Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
|
|
5407
|
+
- NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
|
|
5408
|
+
- NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
|
|
5409
|
+
- NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
|
|
5410
|
+
- No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
|
|
5411
|
+
|
|
5412
|
+
### Constraints
|
|
5413
|
+
- A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
|
|
5414
|
+
- Responsive: fills the embedded panel width and collapses gracefully at narrow widths.
|
|
5415
|
+
|
|
5416
|
+
Respond only with the complete HTML file and absolutely no other text. Your response will be written directly to src/overview.html.`;
|
|
5417
|
+
var OVERVIEW_SHELL = `<!DOCTYPE html>
|
|
5418
|
+
<html lang="en">
|
|
5419
|
+
<head>
|
|
5420
|
+
<meta charset="utf-8">
|
|
5421
|
+
<!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
|
|
5422
|
+
static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
|
|
5423
|
+
The user-scalable=no viewport is intentional; leave it. -->
|
|
5424
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
5425
|
+
<title>Build Overview</title>
|
|
5426
|
+
<!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
|
|
5427
|
+
<style>
|
|
5428
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
5429
|
+
</style>
|
|
5430
|
+
</head>
|
|
5431
|
+
<body>
|
|
5432
|
+
<!-- Compose the Build Overview here. Layout, sections, type, and styling are
|
|
5433
|
+
yours \u2014 keep everything inline and self-contained, brand-skinned from the
|
|
5434
|
+
app's spec. -->
|
|
5435
|
+
</body>
|
|
5436
|
+
</html>`;
|
|
5437
|
+
var buildOverviewTool = {
|
|
5438
|
+
clearable: false,
|
|
5439
|
+
definition: {
|
|
5440
|
+
name: "writeBuildOverview",
|
|
5441
|
+
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 without altering any of your facts. Generate it at the end of a build and refresh it after meaningful work.",
|
|
5442
|
+
inputSchema: {
|
|
5443
|
+
type: "object",
|
|
5444
|
+
properties: {
|
|
5445
|
+
content: {
|
|
5446
|
+
type: "string",
|
|
5447
|
+
description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand without changing any fact."
|
|
5448
|
+
}
|
|
5449
|
+
},
|
|
5450
|
+
required: ["content"]
|
|
5451
|
+
}
|
|
5452
|
+
},
|
|
5453
|
+
async execute(input, context) {
|
|
5454
|
+
if (!context) {
|
|
5455
|
+
return "Error: writeBuildOverview requires execution context for design expert delegation";
|
|
5456
|
+
}
|
|
5457
|
+
const content = (input.content ?? "").trim();
|
|
5458
|
+
if (!content) {
|
|
5459
|
+
return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
|
|
5460
|
+
}
|
|
5461
|
+
try {
|
|
5462
|
+
const existing = fs20.existsSync(OVERVIEW_FILE) ? fs20.readFileSync(OVERVIEW_FILE, "utf-8").trim() : "";
|
|
5463
|
+
const currentOverview = existing || OVERVIEW_SHELL;
|
|
5464
|
+
const task = `<overview_copy>${content}</overview_copy>
|
|
5465
|
+
|
|
5466
|
+
<current_overview>${currentOverview}</current_overview>
|
|
5467
|
+
|
|
5468
|
+
${DESIGN_BRIEF}`;
|
|
5469
|
+
const result = await designExpertTool.execute({ task }, context);
|
|
5470
|
+
const htmlMatch = result.match(
|
|
5471
|
+
/```(?:html|wireframe)\n([\s\S]*?)```/
|
|
5472
|
+
);
|
|
5473
|
+
const html = htmlMatch ? htmlMatch[1].trim() : result;
|
|
5474
|
+
fs20.writeFileSync(OVERVIEW_FILE, html, "utf-8");
|
|
5475
|
+
return "Build overview written successfully to src/overview.html.";
|
|
5476
|
+
} catch (err) {
|
|
5477
|
+
return `Error generating build overview: ${err.message}`;
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
};
|
|
5481
|
+
|
|
5394
5482
|
// src/tools/index.ts
|
|
5395
5483
|
function deriveContext(parent, toolCallId) {
|
|
5396
5484
|
return { ...parent, toolCallId };
|
|
@@ -5407,6 +5495,7 @@ var ALL_TOOLS = [
|
|
|
5407
5495
|
designExpertTool,
|
|
5408
5496
|
productVisionTool,
|
|
5409
5497
|
codeSanityCheckTool,
|
|
5498
|
+
buildOverviewTool,
|
|
5410
5499
|
compactConversationTool,
|
|
5411
5500
|
// Post-onboarding
|
|
5412
5501
|
presentPublishPlanTool,
|
|
@@ -5789,7 +5878,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
5789
5878
|
}
|
|
5790
5879
|
|
|
5791
5880
|
// src/brandExtraction/index.ts
|
|
5792
|
-
import
|
|
5881
|
+
import fs21 from "fs";
|
|
5793
5882
|
import path10 from "path";
|
|
5794
5883
|
import { createHash } from "crypto";
|
|
5795
5884
|
var log9 = createLogger("brandExtraction");
|
|
@@ -5838,7 +5927,7 @@ function sha256(input) {
|
|
|
5838
5927
|
}
|
|
5839
5928
|
function readSafe(filePath) {
|
|
5840
5929
|
try {
|
|
5841
|
-
return
|
|
5930
|
+
return fs21.readFileSync(filePath, "utf-8");
|
|
5842
5931
|
} catch {
|
|
5843
5932
|
return "";
|
|
5844
5933
|
}
|
|
@@ -5846,7 +5935,7 @@ function readSafe(filePath) {
|
|
|
5846
5935
|
function walkMdFiles3(dir) {
|
|
5847
5936
|
const results = [];
|
|
5848
5937
|
try {
|
|
5849
|
-
const entries =
|
|
5938
|
+
const entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
5850
5939
|
for (const entry of entries) {
|
|
5851
5940
|
const full = path10.join(dir, entry.name);
|
|
5852
5941
|
if (entry.isDirectory()) {
|
|
@@ -5861,7 +5950,7 @@ function walkMdFiles3(dir) {
|
|
|
5861
5950
|
}
|
|
5862
5951
|
function parseFrontmatter3(filePath) {
|
|
5863
5952
|
try {
|
|
5864
|
-
const content =
|
|
5953
|
+
const content = fs21.readFileSync(filePath, "utf-8");
|
|
5865
5954
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
5866
5955
|
if (!match) {
|
|
5867
5956
|
return { type: "" };
|
|
@@ -6040,14 +6129,14 @@ function pickFont(raw) {
|
|
|
6040
6129
|
}
|
|
6041
6130
|
function persistBrand(brand, inputHash) {
|
|
6042
6131
|
const tmp = `${BRAND_FILE}.tmp`;
|
|
6043
|
-
|
|
6044
|
-
|
|
6132
|
+
fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
|
|
6133
|
+
fs21.renameSync(tmp, BRAND_FILE);
|
|
6045
6134
|
const cache = { inputHash, generatedAt: Date.now() };
|
|
6046
|
-
|
|
6135
|
+
fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
6047
6136
|
}
|
|
6048
6137
|
function readCache() {
|
|
6049
6138
|
try {
|
|
6050
|
-
const raw =
|
|
6139
|
+
const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
|
|
6051
6140
|
const parsed = JSON.parse(raw);
|
|
6052
6141
|
if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
|
|
6053
6142
|
return parsed;
|
|
@@ -6080,14 +6169,14 @@ function triggerBrandExtraction(apiConfig, model) {
|
|
|
6080
6169
|
}
|
|
6081
6170
|
|
|
6082
6171
|
// src/session.ts
|
|
6083
|
-
import
|
|
6172
|
+
import fs22 from "fs";
|
|
6084
6173
|
import path11 from "path";
|
|
6085
6174
|
var log11 = createLogger("session");
|
|
6086
6175
|
var SESSION_FILE = ".remy-session.json";
|
|
6087
6176
|
var ARCHIVE_DIR = ".logs/sessions";
|
|
6088
6177
|
function loadSession(state) {
|
|
6089
6178
|
try {
|
|
6090
|
-
const raw =
|
|
6179
|
+
const raw = fs22.readFileSync(SESSION_FILE, "utf-8");
|
|
6091
6180
|
const data = JSON.parse(raw);
|
|
6092
6181
|
if (data.models && typeof data.models === "object") {
|
|
6093
6182
|
state.models = data.models;
|
|
@@ -6146,7 +6235,7 @@ function saveSession(state) {
|
|
|
6146
6235
|
if (state.models && Object.keys(state.models).length > 0) {
|
|
6147
6236
|
payload.models = state.models;
|
|
6148
6237
|
}
|
|
6149
|
-
|
|
6238
|
+
fs22.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
|
|
6150
6239
|
log11.info("Session saved", { messageCount: state.messages.length });
|
|
6151
6240
|
} catch (err) {
|
|
6152
6241
|
log11.warn("Session save failed", { error: err.message });
|
|
@@ -6155,11 +6244,11 @@ function saveSession(state) {
|
|
|
6155
6244
|
function clearSession(state) {
|
|
6156
6245
|
state.messages = [];
|
|
6157
6246
|
try {
|
|
6158
|
-
if (
|
|
6159
|
-
|
|
6247
|
+
if (fs22.existsSync(SESSION_FILE)) {
|
|
6248
|
+
fs22.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
6160
6249
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6161
6250
|
const dest = path11.join(ARCHIVE_DIR, `cleared-${ts}.json`);
|
|
6162
|
-
|
|
6251
|
+
fs22.renameSync(SESSION_FILE, dest);
|
|
6163
6252
|
log11.info("Session archived on clear", { dest });
|
|
6164
6253
|
}
|
|
6165
6254
|
} catch (err) {
|
|
@@ -6167,7 +6256,7 @@ function clearSession(state) {
|
|
|
6167
6256
|
error: err.message
|
|
6168
6257
|
});
|
|
6169
6258
|
try {
|
|
6170
|
-
|
|
6259
|
+
fs22.unlinkSync(SESSION_FILE);
|
|
6171
6260
|
} catch {
|
|
6172
6261
|
}
|
|
6173
6262
|
}
|
|
@@ -7159,6 +7248,24 @@ var MessageQueue = class {
|
|
|
7159
7248
|
this.onChange?.();
|
|
7160
7249
|
return all;
|
|
7161
7250
|
}
|
|
7251
|
+
/**
|
|
7252
|
+
* Remove all items matching `predicate`. Fires onChange only if something
|
|
7253
|
+
* was removed. Returns the removed items.
|
|
7254
|
+
*/
|
|
7255
|
+
removeWhere(predicate) {
|
|
7256
|
+
const removed = [];
|
|
7257
|
+
this.items = this.items.filter((item) => {
|
|
7258
|
+
if (predicate(item)) {
|
|
7259
|
+
removed.push(item);
|
|
7260
|
+
return false;
|
|
7261
|
+
}
|
|
7262
|
+
return true;
|
|
7263
|
+
});
|
|
7264
|
+
if (removed.length > 0) {
|
|
7265
|
+
this.onChange?.();
|
|
7266
|
+
}
|
|
7267
|
+
return removed;
|
|
7268
|
+
}
|
|
7162
7269
|
/** Copy of current queue contents (for surfacing on events). */
|
|
7163
7270
|
snapshot() {
|
|
7164
7271
|
return [...this.items];
|
|
@@ -7209,6 +7316,24 @@ function resolveAction(text) {
|
|
|
7209
7316
|
next
|
|
7210
7317
|
};
|
|
7211
7318
|
}
|
|
7319
|
+
function getActionChain(startName) {
|
|
7320
|
+
const chain = [];
|
|
7321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7322
|
+
let name = startName;
|
|
7323
|
+
while (name && !seen.has(name)) {
|
|
7324
|
+
seen.add(name);
|
|
7325
|
+
chain.push(name);
|
|
7326
|
+
let body;
|
|
7327
|
+
try {
|
|
7328
|
+
body = readAsset("automatedActions", `${name}.md`);
|
|
7329
|
+
} catch {
|
|
7330
|
+
break;
|
|
7331
|
+
}
|
|
7332
|
+
const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
7333
|
+
name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
|
|
7334
|
+
}
|
|
7335
|
+
return chain;
|
|
7336
|
+
}
|
|
7212
7337
|
|
|
7213
7338
|
// src/headless/index.ts
|
|
7214
7339
|
var log14 = createLogger("headless");
|
|
@@ -7279,14 +7404,16 @@ var HeadlessSession = class {
|
|
|
7279
7404
|
baseUrl: this.opts.baseUrl
|
|
7280
7405
|
});
|
|
7281
7406
|
const resumed = loadSession(this.state);
|
|
7282
|
-
this.queue = new MessageQueue(loadQueue(), () =>
|
|
7407
|
+
this.queue = new MessageQueue(loadQueue(), () => {
|
|
7408
|
+
this.persistStats();
|
|
7409
|
+
this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
|
|
7410
|
+
});
|
|
7283
7411
|
if (resumed) {
|
|
7284
7412
|
this.emit("session_restored", {
|
|
7285
7413
|
messageCount: this.state.messages.length,
|
|
7286
7414
|
...this.state.models && { models: this.state.models },
|
|
7287
7415
|
modelSurfaces: MODEL_SURFACES,
|
|
7288
|
-
allowedModelsByType: ALLOWED_MODELS_BY_TYPE
|
|
7289
|
-
...this.queueFields()
|
|
7416
|
+
allowedModelsByType: ALLOWED_MODELS_BY_TYPE
|
|
7290
7417
|
});
|
|
7291
7418
|
}
|
|
7292
7419
|
triggerBrandExtraction(
|
|
@@ -7336,7 +7463,7 @@ var HeadlessSession = class {
|
|
|
7336
7463
|
});
|
|
7337
7464
|
process.on("SIGTERM", this.shutdown);
|
|
7338
7465
|
process.on("SIGINT", this.shutdown);
|
|
7339
|
-
this.emit("ready"
|
|
7466
|
+
this.emit("ready");
|
|
7340
7467
|
}
|
|
7341
7468
|
shutdown = () => {
|
|
7342
7469
|
this.emit("stopping");
|
|
@@ -7360,19 +7487,12 @@ var HeadlessSession = class {
|
|
|
7360
7487
|
}
|
|
7361
7488
|
process.stdout.write(line);
|
|
7362
7489
|
}
|
|
7363
|
-
/**
|
|
7364
|
-
*
|
|
7365
|
-
* `queuedMessages` if the queue has items (sandbox uses this to know the
|
|
7366
|
-
* agent is still busy with pipeline work).
|
|
7367
|
-
*/
|
|
7490
|
+
/** Emit a `completed` event and mark completedEmitted. Queue state is
|
|
7491
|
+
* surfaced separately via the `queue_changed` event, not on `completed`. */
|
|
7368
7492
|
emitCompleted(rid, data) {
|
|
7369
|
-
this.emit("completed", { ...data
|
|
7493
|
+
this.emit("completed", { ...data }, rid);
|
|
7370
7494
|
this.completedEmitted = true;
|
|
7371
7495
|
}
|
|
7372
|
-
/** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
|
|
7373
|
-
queueFields() {
|
|
7374
|
-
return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
|
|
7375
|
-
}
|
|
7376
7496
|
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
7377
7497
|
dispatchSimple(requestId, eventName, handler) {
|
|
7378
7498
|
try {
|
|
@@ -7550,11 +7670,7 @@ var HeadlessSession = class {
|
|
|
7550
7670
|
});
|
|
7551
7671
|
return;
|
|
7552
7672
|
case "turn_cancelled": {
|
|
7553
|
-
this.emit(
|
|
7554
|
-
"completed",
|
|
7555
|
-
{ success: false, error: "cancelled", ...this.queueFields() },
|
|
7556
|
-
rid
|
|
7557
|
-
);
|
|
7673
|
+
this.emit("completed", { success: false, error: "cancelled" }, rid);
|
|
7558
7674
|
this.completedEmitted = true;
|
|
7559
7675
|
return;
|
|
7560
7676
|
}
|
|
@@ -7678,7 +7794,7 @@ var HeadlessSession = class {
|
|
|
7678
7794
|
* message — `running` stays held across the queue drain so no user
|
|
7679
7795
|
* message can slip in mid-pipeline.
|
|
7680
7796
|
*/
|
|
7681
|
-
async runSingleTurn(parsed, requestId) {
|
|
7797
|
+
async runSingleTurn(parsed, requestId, fromChain = false) {
|
|
7682
7798
|
this.currentRequestId = requestId;
|
|
7683
7799
|
this.currentAbort = new AbortController();
|
|
7684
7800
|
this.completedEmitted = false;
|
|
@@ -7726,16 +7842,18 @@ var HeadlessSession = class {
|
|
|
7726
7842
|
onboardingState,
|
|
7727
7843
|
parsed.viewContext
|
|
7728
7844
|
);
|
|
7729
|
-
if (resolved?.next) {
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7845
|
+
if (resolved?.next && !fromChain) {
|
|
7846
|
+
for (const step of getActionChain(resolved.next)) {
|
|
7847
|
+
this.queue.push({
|
|
7848
|
+
command: {
|
|
7849
|
+
action: "message",
|
|
7850
|
+
text: sentinel(step),
|
|
7851
|
+
onboardingState
|
|
7852
|
+
},
|
|
7853
|
+
source: "chain",
|
|
7854
|
+
enqueuedAt: Date.now()
|
|
7855
|
+
});
|
|
7856
|
+
}
|
|
7739
7857
|
}
|
|
7740
7858
|
try {
|
|
7741
7859
|
await runTurn({
|
|
@@ -7793,11 +7911,6 @@ var HeadlessSession = class {
|
|
|
7793
7911
|
source: "user",
|
|
7794
7912
|
enqueuedAt: Date.now()
|
|
7795
7913
|
});
|
|
7796
|
-
this.emit(
|
|
7797
|
-
"queued",
|
|
7798
|
-
{ position: this.queue.length, ...this.queueFields() },
|
|
7799
|
-
requestId
|
|
7800
|
-
);
|
|
7801
7914
|
return;
|
|
7802
7915
|
}
|
|
7803
7916
|
this.running = true;
|
|
@@ -7845,7 +7958,7 @@ var HeadlessSession = class {
|
|
|
7845
7958
|
continue;
|
|
7846
7959
|
}
|
|
7847
7960
|
const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
|
|
7848
|
-
await this.runSingleTurn(next.command, nextRid);
|
|
7961
|
+
await this.runSingleTurn(next.command, nextRid, next.source === "chain");
|
|
7849
7962
|
}
|
|
7850
7963
|
}
|
|
7851
7964
|
/**
|
|
@@ -7909,6 +8022,17 @@ var HeadlessSession = class {
|
|
|
7909
8022
|
}
|
|
7910
8023
|
return this.queue.drain();
|
|
7911
8024
|
}
|
|
8025
|
+
/**
|
|
8026
|
+
* Remove pending queued messages — all user messages, or one by id.
|
|
8027
|
+
* Only `source: 'user'` items are removable; chained and background
|
|
8028
|
+
* messages are part of a system chain and are never cancellable. Does
|
|
8029
|
+
* not affect the in-flight turn (use `cancel` for that).
|
|
8030
|
+
*/
|
|
8031
|
+
handleCancelQueued(id) {
|
|
8032
|
+
return this.queue.removeWhere(
|
|
8033
|
+
(item) => item.source === "user" && (id === void 0 || item.command.requestId === id)
|
|
8034
|
+
);
|
|
8035
|
+
}
|
|
7912
8036
|
//////////////////////////////////////////////////////////////////////////////
|
|
7913
8037
|
// Stdin router
|
|
7914
8038
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -7973,7 +8097,11 @@ var HeadlessSession = class {
|
|
|
7973
8097
|
...this.state.models && { models: this.state.models },
|
|
7974
8098
|
modelSurfaces: MODEL_SURFACES,
|
|
7975
8099
|
allowedModelsByType: ALLOWED_MODELS_BY_TYPE,
|
|
7976
|
-
|
|
8100
|
+
// Current queue snapshot for connect/reconnect — get_history is the
|
|
8101
|
+
// on-demand "current state" query. Always an array (possibly empty),
|
|
8102
|
+
// matching the queue_changed convention so the client reconciles the
|
|
8103
|
+
// same way from both; live mutations are pushed via queue_changed.
|
|
8104
|
+
queuedMessages: this.queue.snapshot()
|
|
7977
8105
|
}));
|
|
7978
8106
|
return;
|
|
7979
8107
|
}
|
|
@@ -8017,6 +8145,16 @@ var HeadlessSession = class {
|
|
|
8017
8145
|
);
|
|
8018
8146
|
return;
|
|
8019
8147
|
}
|
|
8148
|
+
if (action === "cancelQueued") {
|
|
8149
|
+
const id = parsed.id;
|
|
8150
|
+
const removed = this.handleCancelQueued(id);
|
|
8151
|
+
this.emit(
|
|
8152
|
+
"completed",
|
|
8153
|
+
{ success: true, cancelledQueued: removed },
|
|
8154
|
+
requestId
|
|
8155
|
+
);
|
|
8156
|
+
return;
|
|
8157
|
+
}
|
|
8020
8158
|
if (action === "stop_tool") {
|
|
8021
8159
|
const id = parsed.id;
|
|
8022
8160
|
const mode = parsed.mode ?? "hard";
|