@vercel/dream 0.2.6 → 0.2.8
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/dream.js +309 -206
- package/package.json +2 -2
package/dist/dream.js
CHANGED
|
@@ -3,73 +3,73 @@
|
|
|
3
3
|
// bin/dream.ts
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
6
7
|
import { createOpencode } from "@opencode-ai/sdk/v2";
|
|
7
8
|
import { init } from "@vercel/dream-init";
|
|
8
9
|
import { program } from "commander";
|
|
10
|
+
import { $ } from "zx";
|
|
9
11
|
var STOP_WORD = "<DREAM DONE>";
|
|
12
|
+
var PROJECT_PROMPT_FILES = [
|
|
13
|
+
"loop-prompt.md",
|
|
14
|
+
"agent-prompt.md",
|
|
15
|
+
"system-prompt.md",
|
|
16
|
+
"prompt.md"
|
|
17
|
+
];
|
|
10
18
|
var SYSTEM_PROMPT = `# Dream Agent
|
|
11
19
|
|
|
12
20
|
You are an autonomous agent building a project from specifications. You run across multiple iterations, each with a fresh context window. Each iteration you pick up the next chunk of work, complete it, and stop.
|
|
13
21
|
|
|
14
|
-
##
|
|
15
|
-
|
|
16
|
-
You are running in an isolated VM. You have full control of the machine. You can:
|
|
17
|
-
- Install any packages (\`npm install\`, \`apt-get\`, etc.)
|
|
18
|
-
- Start servers and dev processes (\`npm run dev\`, \`python -m http.server\`, etc.)
|
|
19
|
-
- Run build tools (\`npx next build\`, \`npx vite build\`, \`tsc\`, \`esbuild\`, etc.)
|
|
20
|
-
- Use any framework or toolchain \u2014 Next.js, Vite, Astro, plain HTML, whatever fits the spec
|
|
21
|
-
- Listen on any port, spawn background processes, modify system config
|
|
22
|
-
- Read and write anywhere on the filesystem
|
|
22
|
+
## Non-Negotiable Rule: Specs Are the Source of Truth
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Read and follow the specs exactly. Do not infer a different architecture than what specs require.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
If specs require a framework/runtime (for example Next.js App Router, server routes, Supabase), implement that framework/runtime in source code. Do not replace required behavior with static mock pages.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
If a project prompt overlay file exists in \`specs/\`, treat it as additional project-specific constraints. Precedence order is:
|
|
29
|
+
1. This system prompt (highest)
|
|
30
|
+
2. Project prompt overlay
|
|
31
|
+
3. Your own defaults (lowest)
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
- Read specifications from the \`specs/\` directory in the current working directory
|
|
32
|
-
- Track your progress in a \`PROGRESS.md\` file (create it on first run)
|
|
33
|
-
- On each iteration, read \`PROGRESS.md\` to understand what's done and what remains
|
|
34
|
-
- Update \`PROGRESS.md\` after completing each task
|
|
33
|
+
When instructions conflict, obey higher precedence and explicitly note the conflict in \`PROGRESS.md\`.
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
## Environment
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
You are running in an isolated VM with full machine control. You can:
|
|
38
|
+
- Install packages (\`npm install\`, \`apt-get\`, etc.)
|
|
39
|
+
- Start servers/processes (\`npm run dev\`, \`docker\`, etc.)
|
|
40
|
+
- Run build tools (\`npx next build\`, \`npx vite build\`, \`tsc\`, etc.)
|
|
41
|
+
- Read/write filesystem and execute shell commands
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
Environment variables required by the project are already injected into the runtime environment. You may inspect env vars for diagnostics, but never print secret values into persistent logs or committed files.
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
2. **Plan**: If no \`PROGRESS.md\`, create it with a task breakdown from the specs. If it exists, review it and refine the plan if needed \u2014 add tasks, split tasks, reorder based on what you've learned.
|
|
44
|
-
3. **Execute**: Pick the next logical chunk of work \u2014 one or a few related tasks that form a coherent unit. Complete them fully.
|
|
45
|
-
4. **Update**: Mark completed tasks in \`PROGRESS.md\`. Add any notes that will help the next iteration.
|
|
46
|
-
5. **Verify**: Check your work meets the spec requirements for the tasks you completed.
|
|
47
|
-
6. **Stop or complete**: If ALL tasks are now done, output the completion signal. Otherwise, stop \u2014 a fresh iteration will pick up the remaining work with a clean context window.
|
|
45
|
+
## Implementation Artifact Policy
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
1. The deliverable is project source code that satisfies specs.
|
|
48
|
+
2. For Vercel deployment, final build artifacts must conform to Build Output API in \`.vercel/output/\`.
|
|
49
|
+
3. Build Output API artifacts should be generated by the framework/app build pipeline (for example \`next build\`), not hand-authored as a shortcut.
|
|
50
|
+
4. If required prerequisites are missing (for example env vars, credentials, unavailable services), do not produce a static fallback. Record a blocker in \`PROGRESS.md\` and stop the iteration.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
## Critical: State Lives on Disk
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Each iteration starts with fresh context. You must:
|
|
55
|
+
- Read all specifications from \`specs/\`
|
|
56
|
+
- Read \`PROGRESS.md\` if present
|
|
57
|
+
- If \`PROGRESS.md\` is missing, create it immediately
|
|
58
|
+
- Update \`PROGRESS.md\` after completing work
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
.vercel/output/
|
|
57
|
-
\u251C\u2500\u2500 config.json # Required: { "version": 3 }
|
|
58
|
-
\u2514\u2500\u2500 static/ # Static files served from root (/)
|
|
59
|
-
\u251C\u2500\u2500 index.html
|
|
60
|
-
\u251C\u2500\u2500 styles.css
|
|
61
|
-
\u2514\u2500\u2500 ...
|
|
62
|
-
\`\`\`
|
|
60
|
+
This ensures resumeability across fresh context windows.
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
## Workflow
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
"version": 3
|
|
69
|
-
}
|
|
70
|
-
\`\`\`
|
|
64
|
+
Each iteration follows this cycle:
|
|
71
65
|
|
|
72
|
-
|
|
66
|
+
1. **Read state**: Read specs + \`PROGRESS.md\` (if present)
|
|
67
|
+
2. **Plan**: If no \`PROGRESS.md\`, create task breakdown from specs. If present, refine tasks based on current state.
|
|
68
|
+
3. **Preflight checks**: Validate required env vars and tool prerequisites from specs before implementation.
|
|
69
|
+
4. **Execute**: Complete one coherent, meaningful chunk of work.
|
|
70
|
+
5. **Update**: Mark completed tasks and note blockers/assumptions in \`PROGRESS.md\`.
|
|
71
|
+
6. **Verify**: Run required checks from specs (build/test/contract checks for changed surface).
|
|
72
|
+
7. **Stop or complete**: If all tasks are done, output completion signal. Otherwise stop for the next iteration.
|
|
73
73
|
|
|
74
74
|
## PROGRESS.md Format
|
|
75
75
|
|
|
@@ -79,141 +79,151 @@ Static files in \`.vercel/output/static/\` are served at the deployment root. Su
|
|
|
79
79
|
## Tasks
|
|
80
80
|
- [x] Completed task
|
|
81
81
|
- [ ] Pending task
|
|
82
|
-
|
|
82
|
+
|
|
83
|
+
## Blockers
|
|
84
|
+
- <none> or explicit blocker with reason
|
|
83
85
|
|
|
84
86
|
## Notes
|
|
85
|
-
|
|
87
|
+
Context for next iteration.
|
|
86
88
|
\`\`\`
|
|
87
89
|
|
|
88
90
|
## Browser Automation
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
### Core Workflow: Snapshot + Refs
|
|
93
|
-
|
|
94
|
-
\`\`\`bash
|
|
95
|
-
# Navigate to a page
|
|
96
|
-
agent-browser open file://$(pwd)/.vercel/output/static/index.html --allow-file-access
|
|
97
|
-
|
|
98
|
-
# Get interactive elements with refs
|
|
99
|
-
agent-browser snapshot -i
|
|
100
|
-
# Output: - button "Submit" [ref=e1] - textbox "Email" [ref=e2] ...
|
|
101
|
-
|
|
102
|
-
# Interact using refs
|
|
103
|
-
agent-browser fill @e2 "test@example.com"
|
|
104
|
-
agent-browser click @e1
|
|
105
|
-
|
|
106
|
-
# Re-snapshot after any navigation or DOM change (refs invalidate)
|
|
107
|
-
agent-browser snapshot -i
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
|
-
### Key Commands
|
|
92
|
+
Dream bootstraps \`agent-browser\` before the loop starts. Use it for UI verification.
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
- **Interact**: \`click <ref>\`, \`fill <ref> <text>\`, \`type <ref> <text>\`, \`press <key>\`, \`select <ref> <value>\`, \`check <ref>\`, \`hover <ref>\`, \`scroll <dir> [px]\`
|
|
114
|
-
- **Read**: \`snapshot -i\` (interactive elements), \`get text <ref>\`, \`get title\`, \`get url\`
|
|
115
|
-
- **Wait**: \`wait <selector>\`, \`wait <ms>\`, \`wait --text "..."\`, \`wait --load networkidle\`
|
|
116
|
-
- **Screenshot**: \`screenshot [path]\`, \`screenshot --full\`
|
|
117
|
-
- **Debug**: \`console\`, \`errors\`, \`eval <js>\`
|
|
94
|
+
Use it to test running app routes (typically localhost dev server or preview URL), not only static files.
|
|
118
95
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
- To validate user interactions match spec requirements
|
|
124
|
-
- To catch broken layouts, missing elements, or JavaScript errors
|
|
125
|
-
|
|
126
|
-
### Tips
|
|
127
|
-
|
|
128
|
-
- Always use \`--allow-file-access\` when opening \`file://\` URLs
|
|
129
|
-
- Use \`snapshot -i -c\` for compact output (interactive elements, no empty containers)
|
|
130
|
-
- Refs like \`@e1\` are only valid until the next navigation or DOM mutation \u2014 re-snapshot after changes
|
|
131
|
-
- Use \`agent-browser errors\` and \`agent-browser console\` to check for JavaScript issues
|
|
132
|
-
- Use \`screenshot\` for visual verification when the snapshot alone isn't sufficient
|
|
96
|
+
Core commands:
|
|
97
|
+
- Navigate: \`open <url>\`, \`back\`, \`forward\`, \`reload\`
|
|
98
|
+
- Interact: \`snapshot -i\`, \`click <ref>\`, \`fill <ref> <text>\`, \`press <key>\`
|
|
99
|
+
- Debug: \`errors\`, \`console\`, \`screenshot\`
|
|
133
100
|
|
|
134
101
|
## Iteration Sizing
|
|
135
102
|
|
|
136
|
-
Each iteration should complete a
|
|
137
|
-
- Scaffold
|
|
138
|
-
- Implement
|
|
139
|
-
-
|
|
140
|
-
- Wire up interactivity and test it
|
|
141
|
-
|
|
142
|
-
Use your judgment. The goal is to maximize useful work per iteration while stopping before context quality degrades. When in doubt, finish the current logical unit and stop.
|
|
103
|
+
Each iteration should complete a coherent unit (not trivial single-file churn), such as:
|
|
104
|
+
- Scaffold required framework/runtime
|
|
105
|
+
- Implement one full route/feature slice
|
|
106
|
+
- Wire persistence + auth + tests for a bounded surface
|
|
143
107
|
|
|
144
108
|
## Completion
|
|
145
109
|
|
|
146
|
-
**
|
|
110
|
+
**If tasks remain:** update \`PROGRESS.md\` and stop. Do NOT output completion signal.
|
|
147
111
|
|
|
148
|
-
**When
|
|
149
|
-
- Every task in \`PROGRESS.md\` is
|
|
150
|
-
- All
|
|
151
|
-
-
|
|
152
|
-
-
|
|
112
|
+
**When all work is done**, output completion signal only after all are true:
|
|
113
|
+
- Every task in \`PROGRESS.md\` is complete
|
|
114
|
+
- All specs in \`specs/\` are implemented
|
|
115
|
+
- Required verification commands pass
|
|
116
|
+
- No active blockers remain
|
|
117
|
+
- Implementation artifacts are source-code-compliant with specs (not forbidden static fallback)
|
|
118
|
+
- Generated deployment output conforms to Build Output API for the target deploy platform
|
|
153
119
|
|
|
154
120
|
When complete, output exactly this on its own line:
|
|
155
121
|
|
|
156
122
|
${STOP_WORD}
|
|
157
123
|
|
|
158
|
-
|
|
124
|
+
Without this signal, the system keeps launching new iterations.`;
|
|
159
125
|
var DEFAULT_TIMEOUT = 36e5;
|
|
160
126
|
var DEFAULT_MAX_ITERATIONS = 100;
|
|
161
|
-
var DEFAULT_MODEL = "vercel/anthropic/claude-opus-4.
|
|
127
|
+
var DEFAULT_MODEL = "vercel/anthropic/claude-opus-4.6";
|
|
128
|
+
var DEFAULT_BOOTSTRAP_SKILL_REPOS = [
|
|
129
|
+
{ repo: "vercel-labs/agent-browser", skills: ["agent-browser"] },
|
|
130
|
+
{ repo: "vercel-labs/agent-skills", skills: ["react-best-practices"] }
|
|
131
|
+
];
|
|
162
132
|
var dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
163
133
|
var bold = (s) => `\x1B[1m${s}\x1B[22m`;
|
|
164
134
|
var green = (s) => `\x1B[32m${s}\x1B[39m`;
|
|
165
135
|
var red = (s) => `\x1B[31m${s}\x1B[39m`;
|
|
166
136
|
var cyan = (s) => `\x1B[36m${s}\x1B[39m`;
|
|
167
137
|
var log = console.log;
|
|
138
|
+
var INDENT_MAIN = " ";
|
|
139
|
+
var INDENT_LIST = " ";
|
|
140
|
+
var INDENT_SESSION = " ";
|
|
141
|
+
var INDENT_SESSION_DETAIL = " ";
|
|
142
|
+
var KEY_WIDTH = 13;
|
|
143
|
+
function printLine(line = "") {
|
|
144
|
+
log(line);
|
|
145
|
+
}
|
|
146
|
+
function printTitle(topic) {
|
|
147
|
+
printLine(`
|
|
148
|
+
${INDENT_MAIN}${bold("\u25B2 dream")} ${dim("\xB7")} ${topic}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
function printKV(key, value) {
|
|
152
|
+
printLine(`${INDENT_MAIN}${dim(key.padEnd(KEY_WIDTH))}${value}`);
|
|
153
|
+
}
|
|
154
|
+
function printStep(message) {
|
|
155
|
+
printLine(`${INDENT_MAIN}${dim("\u25CC")} ${message}`);
|
|
156
|
+
}
|
|
157
|
+
function printNote(message) {
|
|
158
|
+
printLine(`${INDENT_MAIN}${dim("\xB7")} ${message}`);
|
|
159
|
+
}
|
|
160
|
+
function printListItem(message) {
|
|
161
|
+
printLine(`${INDENT_LIST}${dim("\xB7")} ${message}`);
|
|
162
|
+
}
|
|
163
|
+
function printSuccess(message) {
|
|
164
|
+
printLine(`${INDENT_MAIN}${green("\u25CF")} ${message}`);
|
|
165
|
+
}
|
|
166
|
+
function printError(message) {
|
|
167
|
+
printLine(`${INDENT_MAIN}${red("\u2717")} ${message}`);
|
|
168
|
+
}
|
|
169
|
+
function printAdded(message) {
|
|
170
|
+
printLine(`${INDENT_MAIN}${green("+")} ${message}`);
|
|
171
|
+
}
|
|
172
|
+
function printSession(message) {
|
|
173
|
+
printLine(`${INDENT_SESSION}${message}`);
|
|
174
|
+
}
|
|
175
|
+
function printSessionDim(message) {
|
|
176
|
+
printSession(dim(message));
|
|
177
|
+
}
|
|
178
|
+
function printSessionError(message) {
|
|
179
|
+
printSession(`${red("\u2717")} ${message}`);
|
|
180
|
+
}
|
|
168
181
|
program.name("dream").description("Run OpenCode in a loop until specs are complete").version("0.1.0").option("-d, --dir <directory>", "Working directory", ".");
|
|
169
182
|
program.command("init").description("Initialize a new dream project").action(() => {
|
|
170
183
|
const workDir = path.resolve(program.opts().dir);
|
|
171
|
-
|
|
172
|
-
${bold("\u25B2 dream")} ${dim("\xB7 init")}
|
|
173
|
-
`);
|
|
184
|
+
printTitle("init");
|
|
174
185
|
const result = init({ dir: workDir, version: "^0.2.1" });
|
|
175
186
|
if (result.specsCreated) {
|
|
176
|
-
|
|
187
|
+
printAdded("specs/2048.md");
|
|
177
188
|
} else {
|
|
178
|
-
|
|
189
|
+
printNote(`specs/ ${dim("already exists")}`);
|
|
179
190
|
}
|
|
180
191
|
if (result.packageJsonCreated) {
|
|
181
|
-
|
|
192
|
+
printAdded("package.json");
|
|
182
193
|
} else {
|
|
183
|
-
|
|
194
|
+
printNote(`package.json ${dim("already exists")}`);
|
|
184
195
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
printLine(
|
|
197
|
+
`
|
|
198
|
+
${INDENT_MAIN}Run ${cyan("pnpm install")} then ${cyan("dream")} to start
|
|
199
|
+
`
|
|
200
|
+
);
|
|
188
201
|
});
|
|
189
202
|
program.command("config").description("Show project configuration and specs").action(() => {
|
|
190
203
|
const workDir = path.resolve(program.opts().dir);
|
|
191
204
|
const specsDir = path.join(workDir, "specs");
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
log(` ${dim("timeout")} ${formatTime(DEFAULT_TIMEOUT)}`);
|
|
197
|
-
log(` ${dim("max")} ${DEFAULT_MAX_ITERATIONS} iterations`);
|
|
205
|
+
printTitle("config");
|
|
206
|
+
printKV("dir", workDir);
|
|
207
|
+
printKV("timeout", formatTime(DEFAULT_TIMEOUT));
|
|
208
|
+
printKV("max", `${DEFAULT_MAX_ITERATIONS} iterations`);
|
|
198
209
|
if (!fs.existsSync(specsDir)) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`);
|
|
210
|
+
printError("specs/ not found");
|
|
211
|
+
printLine();
|
|
202
212
|
return;
|
|
203
213
|
}
|
|
204
214
|
const specFiles = fs.readdirSync(specsDir).filter((f) => f.endsWith(".md"));
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
printLine(
|
|
216
|
+
`
|
|
217
|
+
${INDENT_MAIN}${dim("specs")} ${dim(`(${specFiles.length})`)}`
|
|
218
|
+
);
|
|
207
219
|
for (const file of specFiles) {
|
|
208
|
-
|
|
220
|
+
printListItem(file);
|
|
209
221
|
}
|
|
210
|
-
|
|
222
|
+
printLine();
|
|
211
223
|
});
|
|
212
224
|
program.command("models").description("List available models and check provider auth").action(async () => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
`);
|
|
216
|
-
log(` ${dim("\u25CC")} Starting OpenCode...`);
|
|
225
|
+
printTitle("models");
|
|
226
|
+
printStep("Starting OpenCode...");
|
|
217
227
|
const { client, server } = await createOpencode({
|
|
218
228
|
port: 0,
|
|
219
229
|
config: { enabled_providers: ["vercel"] }
|
|
@@ -221,8 +231,8 @@ program.command("models").description("List available models and check provider
|
|
|
221
231
|
try {
|
|
222
232
|
const res = await client.provider.list();
|
|
223
233
|
if (res.error) {
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
printError("Failed to list providers");
|
|
235
|
+
printLine();
|
|
226
236
|
return;
|
|
227
237
|
}
|
|
228
238
|
const { all, connected } = res.data;
|
|
@@ -230,34 +240,32 @@ program.command("models").description("List available models and check provider
|
|
|
230
240
|
const isConnected = connected.includes(provider.id);
|
|
231
241
|
const icon = isConnected ? green("\u25CF") : red("\u25CB");
|
|
232
242
|
const npm = provider.npm ? dim(` npm:${provider.npm}`) : "";
|
|
233
|
-
|
|
234
|
-
|
|
243
|
+
printLine(
|
|
244
|
+
`${INDENT_MAIN}${icon} ${bold(provider.name)} ${dim(`(${provider.id})`)}${npm}`
|
|
235
245
|
);
|
|
236
246
|
const models = Object.entries(provider.models);
|
|
237
247
|
for (const [id, model] of models) {
|
|
238
248
|
const name = model.name ?? id;
|
|
239
|
-
|
|
249
|
+
printListItem(`${provider.id}/${id} ${dim(name)}`);
|
|
240
250
|
}
|
|
241
251
|
if (models.length === 0) {
|
|
242
|
-
|
|
252
|
+
printLine(`${INDENT_LIST}${dim("no models")}`);
|
|
243
253
|
}
|
|
244
|
-
|
|
254
|
+
printLine();
|
|
245
255
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
`
|
|
249
|
-
);
|
|
256
|
+
printKV("connected", connected.length ? connected.join(", ") : "none");
|
|
257
|
+
printLine();
|
|
250
258
|
} finally {
|
|
251
259
|
server.close();
|
|
252
260
|
process.exit(0);
|
|
253
261
|
}
|
|
254
262
|
});
|
|
255
|
-
program.option("-m, --model <model>", "Model to use (provider/model format)").option("-t, --timeout <ms>", "Timeout in milliseconds").option("-i, --max-iterations <n>", "Maximum iterations").option("-v, --verbose", "Verbose output").action(async (opts) => {
|
|
263
|
+
program.option("-m, --model <model>", "Model to use (provider/model format)").option("-t, --timeout <ms>", "Timeout in milliseconds").option("-i, --max-iterations <n>", "Maximum iterations").option("-v, --verbose", "Verbose output").option("--skip-bootstrap", "Skip runtime bootstrap").action(async (opts) => {
|
|
256
264
|
const workDir = path.resolve(opts.dir);
|
|
257
265
|
const specsDir = path.join(workDir, "specs");
|
|
258
266
|
if (!fs.existsSync(specsDir)) {
|
|
259
|
-
|
|
260
|
-
|
|
267
|
+
printLine(`
|
|
268
|
+
${INDENT_MAIN}${red("\u2717")} specs/ not found in ${workDir}
|
|
261
269
|
`);
|
|
262
270
|
process.exit(1);
|
|
263
271
|
}
|
|
@@ -266,15 +274,38 @@ program.option("-m, --model <model>", "Model to use (provider/model format)").op
|
|
|
266
274
|
const verbose = opts.verbose ?? false;
|
|
267
275
|
const model = opts.model ?? process.env.DREAM_MODEL ?? DEFAULT_MODEL;
|
|
268
276
|
const title = path.basename(workDir);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
const projectPrompt = loadProjectPrompt(specsDir);
|
|
278
|
+
const effectivePrompt = projectPrompt ? `${SYSTEM_PROMPT}
|
|
279
|
+
|
|
280
|
+
## Project Prompt Overlay (${projectPrompt.file})
|
|
281
|
+
|
|
282
|
+
${projectPrompt.content}` : SYSTEM_PROMPT;
|
|
283
|
+
printTitle(title);
|
|
284
|
+
printKV("dir", workDir);
|
|
285
|
+
printKV("model", model || dim("default"));
|
|
286
|
+
printKV("timeout", formatTime(timeout));
|
|
287
|
+
printKV("max", `${maxIterations} iterations`);
|
|
288
|
+
printLine();
|
|
289
|
+
if (projectPrompt) {
|
|
290
|
+
printKV("overlay", projectPrompt.file);
|
|
291
|
+
}
|
|
292
|
+
if (opts.skipBootstrap) {
|
|
293
|
+
printKV("bootstrap", dim("skipped"));
|
|
294
|
+
}
|
|
295
|
+
if (!opts.skipBootstrap) {
|
|
296
|
+
try {
|
|
297
|
+
await bootstrapRuntime(workDir);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
const message = error instanceof Error ? error.message : "bootstrap failed";
|
|
300
|
+
printLine(
|
|
301
|
+
`
|
|
302
|
+
${INDENT_MAIN}${red("\u2717")} Runtime bootstrap failed: ${message}
|
|
303
|
+
`
|
|
304
|
+
);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
printStep("Starting OpenCode...");
|
|
278
309
|
const oidcToken = process.env.VERCEL_OIDC_TOKEN;
|
|
279
310
|
const { client, server } = await createOpencode({
|
|
280
311
|
port: 0,
|
|
@@ -307,25 +338,23 @@ program.option("-m, --model <model>", "Model to use (provider/model format)").op
|
|
|
307
338
|
enabled_providers: ["vercel"]
|
|
308
339
|
}
|
|
309
340
|
});
|
|
310
|
-
|
|
341
|
+
printSuccess("OpenCode ready");
|
|
311
342
|
const providerId = model?.split("/")[0];
|
|
312
343
|
if (providerId) {
|
|
313
344
|
const providers = await client.provider.list();
|
|
314
345
|
if (providers.error) {
|
|
315
|
-
|
|
316
|
-
|
|
346
|
+
printError("Failed to list providers");
|
|
347
|
+
printLine();
|
|
317
348
|
server.close();
|
|
318
349
|
process.exit(1);
|
|
319
350
|
}
|
|
320
351
|
const connected = providers.data.connected ?? [];
|
|
321
352
|
if (!connected.includes(providerId)) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
326
|
-
log(
|
|
353
|
+
printError(`Provider ${bold(providerId)} is not connected`);
|
|
354
|
+
printKV("connected", connected.length ? connected.join(", ") : "none");
|
|
355
|
+
printLine(
|
|
327
356
|
`
|
|
328
|
-
|
|
357
|
+
${INDENT_MAIN}Run ${cyan("opencode")} and authenticate the ${bold(providerId)} provider
|
|
329
358
|
`
|
|
330
359
|
);
|
|
331
360
|
server.close();
|
|
@@ -334,21 +363,20 @@ program.option("-m, --model <model>", "Model to use (provider/model format)").op
|
|
|
334
363
|
const provider = providers.data.all.find((p) => p.id === providerId);
|
|
335
364
|
const modelId = model.split("/").slice(1).join("/");
|
|
336
365
|
if (provider && modelId && !provider.models[modelId]) {
|
|
337
|
-
|
|
338
|
-
` ${red("\u2717")} Model ${bold(modelId)} not found in ${bold(providerId)}`
|
|
339
|
-
);
|
|
366
|
+
printError(`Model ${bold(modelId)} not found in ${bold(providerId)}`);
|
|
340
367
|
const available = Object.keys(provider.models);
|
|
341
368
|
if (available.length) {
|
|
342
|
-
|
|
343
|
-
|
|
369
|
+
printKV(
|
|
370
|
+
"available",
|
|
371
|
+
`${available.slice(0, 5).join(", ")}${available.length > 5 ? ` (+${available.length - 5} more)` : ""}`
|
|
344
372
|
);
|
|
345
373
|
}
|
|
346
|
-
|
|
374
|
+
printLine();
|
|
347
375
|
server.close();
|
|
348
376
|
process.exit(1);
|
|
349
377
|
}
|
|
350
|
-
|
|
351
|
-
|
|
378
|
+
printSuccess(`Provider ${bold(providerId)} connected`);
|
|
379
|
+
printLine();
|
|
352
380
|
}
|
|
353
381
|
const cleanup = () => {
|
|
354
382
|
server.close();
|
|
@@ -362,42 +390,49 @@ program.option("-m, --model <model>", "Model to use (provider/model format)").op
|
|
|
362
390
|
while (iteration < maxIterations) {
|
|
363
391
|
const elapsed = Date.now() - startTime;
|
|
364
392
|
if (elapsed >= timeout) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
393
|
+
printLine(
|
|
394
|
+
`
|
|
395
|
+
${INDENT_MAIN}${red("\u2717")} Timeout after ${formatTime(elapsed)}
|
|
396
|
+
`
|
|
397
|
+
);
|
|
368
398
|
process.exit(1);
|
|
369
399
|
}
|
|
370
400
|
iteration++;
|
|
371
401
|
const iterStart = Date.now();
|
|
372
|
-
|
|
373
|
-
const result = await runSession(
|
|
402
|
+
printLine(`${INDENT_MAIN}${cyan(`[${iteration}]`)} Running session...`);
|
|
403
|
+
const result = await runSession(
|
|
404
|
+
client,
|
|
405
|
+
title,
|
|
406
|
+
effectivePrompt,
|
|
407
|
+
verbose
|
|
408
|
+
);
|
|
374
409
|
const iterElapsed = Date.now() - iterStart;
|
|
375
410
|
if (result === "done") {
|
|
376
|
-
|
|
377
|
-
|
|
411
|
+
printLine(
|
|
412
|
+
`${INDENT_MAIN}${cyan(`[${iteration}]`)} ${green("\u2713")} Done ${dim(`(${formatTime(iterElapsed)})`)}`
|
|
378
413
|
);
|
|
379
|
-
|
|
414
|
+
printLine(
|
|
380
415
|
`
|
|
381
|
-
|
|
416
|
+
${INDENT_MAIN}${green("\u2713")} Completed in ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}
|
|
382
417
|
`
|
|
383
418
|
);
|
|
384
419
|
process.exit(0);
|
|
385
420
|
}
|
|
386
421
|
if (result === "error") {
|
|
387
|
-
|
|
422
|
+
printLine(
|
|
388
423
|
`
|
|
389
|
-
|
|
424
|
+
${INDENT_MAIN}${red("\u2717")} Session failed after ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}
|
|
390
425
|
`
|
|
391
426
|
);
|
|
392
427
|
process.exit(1);
|
|
393
428
|
}
|
|
394
|
-
|
|
395
|
-
|
|
429
|
+
printLine(
|
|
430
|
+
`${INDENT_MAIN}${cyan(`[${iteration}]`)} ${dim(`${formatTime(iterElapsed)} \xB7 continuing...`)}
|
|
396
431
|
`
|
|
397
432
|
);
|
|
398
433
|
}
|
|
399
|
-
|
|
400
|
-
|
|
434
|
+
printLine(`
|
|
435
|
+
${INDENT_MAIN}${red("\u2717")} Max iterations reached
|
|
401
436
|
`);
|
|
402
437
|
process.exit(1);
|
|
403
438
|
} finally {
|
|
@@ -405,7 +440,7 @@ program.option("-m, --model <model>", "Model to use (provider/model format)").op
|
|
|
405
440
|
}
|
|
406
441
|
});
|
|
407
442
|
async function runSession(client, title, systemPrompt, verbose) {
|
|
408
|
-
|
|
443
|
+
printSessionDim("creating session...");
|
|
409
444
|
const sessionResponse = await client.session.create({
|
|
410
445
|
title: `Dream: ${title}`
|
|
411
446
|
});
|
|
@@ -415,18 +450,16 @@ async function runSession(client, title, systemPrompt, verbose) {
|
|
|
415
450
|
);
|
|
416
451
|
}
|
|
417
452
|
const sessionId = sessionResponse.data.id;
|
|
418
|
-
|
|
419
|
-
|
|
453
|
+
printSessionDim(`session ${sessionId.slice(0, 8)}`);
|
|
454
|
+
printSessionDim("subscribing to events...");
|
|
420
455
|
const events = await client.event.subscribe();
|
|
421
|
-
|
|
456
|
+
printSessionDim("sending prompt...");
|
|
422
457
|
const promptResponse = await client.session.promptAsync({
|
|
423
458
|
sessionID: sessionId,
|
|
424
459
|
parts: [{ type: "text", text: systemPrompt }]
|
|
425
460
|
});
|
|
426
461
|
if (promptResponse.error) {
|
|
427
|
-
|
|
428
|
-
` ${red("\u2717")} prompt error: ${JSON.stringify(promptResponse.error)}`
|
|
429
|
-
);
|
|
462
|
+
printSessionError(`prompt error: ${JSON.stringify(promptResponse.error)}`);
|
|
430
463
|
return "error";
|
|
431
464
|
}
|
|
432
465
|
let responseText = "";
|
|
@@ -436,16 +469,15 @@ async function runSession(client, title, systemPrompt, verbose) {
|
|
|
436
469
|
let totalTokensOut = 0;
|
|
437
470
|
const seenTools = /* @__PURE__ */ new Set();
|
|
438
471
|
let lastOutput = "none";
|
|
439
|
-
const pad = " ";
|
|
440
472
|
for await (const event of events.stream) {
|
|
441
473
|
const props = event.properties;
|
|
442
474
|
if (verbose) {
|
|
443
475
|
const sid = props.sessionID ? props.sessionID.slice(0, 8) : "global";
|
|
444
|
-
|
|
476
|
+
printSessionDim(`event: ${event.type} [${sid}]`);
|
|
445
477
|
if (event.type !== "server.connected") {
|
|
446
|
-
|
|
478
|
+
printLine(
|
|
447
479
|
dim(
|
|
448
|
-
|
|
480
|
+
`${INDENT_SESSION_DETAIL}${JSON.stringify(event.properties).slice(0, 200)}`
|
|
449
481
|
)
|
|
450
482
|
);
|
|
451
483
|
}
|
|
@@ -460,18 +492,18 @@ async function runSession(client, title, systemPrompt, verbose) {
|
|
|
460
492
|
responseText += delta;
|
|
461
493
|
if (lastOutput === "tool") process.stdout.write("\n");
|
|
462
494
|
const indented = delta.replace(/\n/g, `
|
|
463
|
-
${
|
|
495
|
+
${INDENT_SESSION}`);
|
|
464
496
|
process.stdout.write(
|
|
465
|
-
lastOutput !== "text" ? `${
|
|
497
|
+
lastOutput !== "text" ? `${INDENT_SESSION}${dim(indented)}` : dim(indented)
|
|
466
498
|
);
|
|
467
499
|
lastOutput = "text";
|
|
468
500
|
}
|
|
469
501
|
if (part.type === "reasoning" && delta) {
|
|
470
502
|
if (lastOutput === "tool") process.stdout.write("\n");
|
|
471
503
|
const indented = delta.replace(/\n/g, `
|
|
472
|
-
${
|
|
504
|
+
${INDENT_SESSION}`);
|
|
473
505
|
process.stdout.write(
|
|
474
|
-
lastOutput !== "text" ? `${
|
|
506
|
+
lastOutput !== "text" ? `${INDENT_SESSION}${dim(indented)}` : dim(indented)
|
|
475
507
|
);
|
|
476
508
|
lastOutput = "text";
|
|
477
509
|
}
|
|
@@ -484,14 +516,14 @@ ${pad}`);
|
|
|
484
516
|
toolCalls++;
|
|
485
517
|
if (lastOutput === "text") process.stdout.write("\n\n");
|
|
486
518
|
const context = toolContext(toolName, state.input) ?? state.title;
|
|
487
|
-
|
|
488
|
-
`${
|
|
519
|
+
printSession(
|
|
520
|
+
`${dim("\u25B8")} ${toolName}${context ? dim(` ${context}`) : ""}`
|
|
489
521
|
);
|
|
490
522
|
lastOutput = "tool";
|
|
491
523
|
}
|
|
492
524
|
if (state.status === "error") {
|
|
493
525
|
if (lastOutput === "text") process.stdout.write("\n");
|
|
494
|
-
|
|
526
|
+
printSession(`${red("\u2717")} ${toolName}: ${state.error}`);
|
|
495
527
|
lastOutput = "tool";
|
|
496
528
|
}
|
|
497
529
|
}
|
|
@@ -507,7 +539,7 @@ ${pad}`);
|
|
|
507
539
|
if (file) {
|
|
508
540
|
if (lastOutput === "text") process.stdout.write("\n");
|
|
509
541
|
const relative = file.replace(`${process.cwd()}/`, "");
|
|
510
|
-
|
|
542
|
+
printSession(`${green("\u270E")} ${relative}`);
|
|
511
543
|
lastOutput = "tool";
|
|
512
544
|
}
|
|
513
545
|
}
|
|
@@ -515,7 +547,7 @@ ${pad}`);
|
|
|
515
547
|
const errProps = event.properties;
|
|
516
548
|
const msg = errProps.error?.data?.message ?? errProps.error?.name ?? "session error";
|
|
517
549
|
if (lastOutput === "text") process.stdout.write("\n");
|
|
518
|
-
|
|
550
|
+
printSession(`${red("\u2717")} ${msg}`);
|
|
519
551
|
return "error";
|
|
520
552
|
}
|
|
521
553
|
if (event.type === "session.idle") {
|
|
@@ -525,9 +557,9 @@ ${pad}`);
|
|
|
525
557
|
if (lastOutput === "text") process.stdout.write("\n");
|
|
526
558
|
const tokens = `${formatTokens(totalTokensIn)}\u2192${formatTokens(totalTokensOut)}`;
|
|
527
559
|
const cost = totalCost > 0 ? ` \xB7 $${totalCost.toFixed(2)}` : "";
|
|
528
|
-
|
|
560
|
+
printSessionDim(`${toolCalls} tools \xB7 ${tokens}${cost}`);
|
|
529
561
|
if (responseText.length === 0) {
|
|
530
|
-
|
|
562
|
+
printSessionError("No response from model");
|
|
531
563
|
return "error";
|
|
532
564
|
}
|
|
533
565
|
return responseText.includes(STOP_WORD) ? "done" : "continue";
|
|
@@ -565,4 +597,75 @@ function toolContext(tool, input) {
|
|
|
565
597
|
return void 0;
|
|
566
598
|
}
|
|
567
599
|
}
|
|
600
|
+
function loadProjectPrompt(specsDir) {
|
|
601
|
+
for (const file of PROJECT_PROMPT_FILES) {
|
|
602
|
+
const fullPath = path.join(specsDir, file);
|
|
603
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
604
|
+
const content = fs.readFileSync(fullPath, "utf8").trim();
|
|
605
|
+
if (!content) continue;
|
|
606
|
+
return { file, content };
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
function packageRootFromScript() {
|
|
611
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
612
|
+
return path.resolve(path.dirname(scriptPath), "..");
|
|
613
|
+
}
|
|
614
|
+
function prependLocalBinsToPath(workDir) {
|
|
615
|
+
const packageRoot = packageRootFromScript();
|
|
616
|
+
const binPaths = [
|
|
617
|
+
path.join(workDir, "node_modules", ".bin"),
|
|
618
|
+
path.join(packageRoot, "node_modules", ".bin")
|
|
619
|
+
];
|
|
620
|
+
const currentPath = process.env.PATH ?? "";
|
|
621
|
+
process.env.PATH = [...binPaths, currentPath].filter((entry) => Boolean(entry)).join(path.delimiter);
|
|
622
|
+
}
|
|
623
|
+
async function commandExists(command) {
|
|
624
|
+
try {
|
|
625
|
+
await $`command -v ${command}`;
|
|
626
|
+
return true;
|
|
627
|
+
} catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function installSkills(skillRepos) {
|
|
632
|
+
for (const entry of skillRepos) {
|
|
633
|
+
const skills = entry.skills?.filter((skill) => skill.trim().length > 0) ?? [];
|
|
634
|
+
if (skills.length === 0) {
|
|
635
|
+
printNote(`Installing skills from ${entry.repo}...`);
|
|
636
|
+
await $`npx skills@latest add ${entry.repo} --yes --global`;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
for (const skill of skills) {
|
|
640
|
+
printNote(`Installing skill ${skill} from ${entry.repo}...`);
|
|
641
|
+
await $`npx skills@latest add ${entry.repo} --skill ${skill} --yes --global`;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function bootstrapRuntime(workDir) {
|
|
646
|
+
prependLocalBinsToPath(workDir);
|
|
647
|
+
printStep("Bootstrapping runtime tools...");
|
|
648
|
+
let hasAgentBrowser = await commandExists("agent-browser");
|
|
649
|
+
if (!hasAgentBrowser) {
|
|
650
|
+
printNote("Installing agent-browser globally...");
|
|
651
|
+
await $`npm install --global agent-browser`;
|
|
652
|
+
hasAgentBrowser = await commandExists("agent-browser");
|
|
653
|
+
}
|
|
654
|
+
printNote("Installing browser runtime...");
|
|
655
|
+
if (hasAgentBrowser) {
|
|
656
|
+
await $`agent-browser install`;
|
|
657
|
+
} else {
|
|
658
|
+
printNote("Falling back to npx for agent-browser install...");
|
|
659
|
+
await $`npx --yes agent-browser install`;
|
|
660
|
+
}
|
|
661
|
+
if (process.env.DREAM_SKIP_SKILL_SETUP === "1") {
|
|
662
|
+
printNote("Skipping skill setup (DREAM_SKIP_SKILL_SETUP=1)");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (DEFAULT_BOOTSTRAP_SKILL_REPOS.length === 0) {
|
|
666
|
+
printNote("No bootstrap skills configured");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await installSkills(DEFAULT_BOOTSTRAP_SKILL_REPOS);
|
|
670
|
+
}
|
|
568
671
|
await program.parseAsync();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/dream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "A CLI that runs OpenCode in a loop until specs are complete",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@ai-sdk/gateway": "^3.0.39",
|
|
14
14
|
"@opencode-ai/sdk": "^1.1.0",
|
|
15
|
-
"agent-browser": ">=0.9.0",
|
|
16
15
|
"commander": "^12.0.0",
|
|
17
16
|
"opencode-ai": ">=1.0.0",
|
|
17
|
+
"zx": "^8.8.4",
|
|
18
18
|
"@vercel/dream-init": "0.2.2"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|