@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.
Files changed (2) hide show
  1. package/dist/dream.js +309 -206
  2. 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
- ## Environment
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
- There are no restrictions. Use the full power of the environment to build the best possible output. If a spec calls for a React app, scaffold it with a real bundler. If it needs server-side rendering, build it. Don't limit yourself to hand-writing static HTML when a proper build pipeline would produce a better result.
24
+ Read and follow the specs exactly. Do not infer a different architecture than what specs require.
25
25
 
26
- The final output must land in \`.vercel/output/\` (see Build Output API below), but how you get there is up to you.
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
- ## Critical: State Lives on Disk
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
- Each iteration starts with fresh context. You must:
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
- This ensures you can resume from any point if interrupted.
35
+ ## Environment
37
36
 
38
- ## Workflow
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
- Each iteration follows this cycle:
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
- 1. **Read state**: Read all files in \`specs/\` and \`PROGRESS.md\` (if exists)
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
- ## Build Output API
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
- Your output must use [Vercel's Build Output API](https://vercel.com/docs/build-output-api/v3).
52
+ ## Critical: State Lives on Disk
52
53
 
53
- ### Directory Structure
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
- ### Minimal config.json
62
+ ## Workflow
65
63
 
66
- \`\`\`json
67
- {
68
- "version": 3
69
- }
70
- \`\`\`
64
+ Each iteration follows this cycle:
71
65
 
72
- Static files in \`.vercel/output/static/\` are served at the deployment root. Subdirectories are preserved in URLs.
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
- - [ ] Another pending task
82
+
83
+ ## Blockers
84
+ - <none> or explicit blocker with reason
83
85
 
84
86
  ## Notes
85
- Any learnings or context for future iterations.
87
+ Context for next iteration.
86
88
  \`\`\`
87
89
 
88
90
  ## Browser Automation
89
91
 
90
- You have \`agent-browser\` available for testing and verifying your work. Use it via bash commands.
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
- - **Navigate**: \`open <url>\`, \`back\`, \`forward\`, \`reload\`, \`close\`
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
- ### When to Use
120
-
121
- - After building interactive features (games, forms, animations) to verify they work
122
- - To test that the page renders correctly and elements are present
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 **meaningful chunk** of work \u2014 not a single trivial file write, but a coherent unit like:
137
- - Scaffold the project structure and install dependencies
138
- - Implement a full feature or page
139
- - Build out a component system or styling layer
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
- **When you finish your chunk and tasks remain:** update \`PROGRESS.md\` and end your response. Do NOT output the completion signal. The next iteration will continue with fresh context.
110
+ **If tasks remain:** update \`PROGRESS.md\` and stop. Do NOT output completion signal.
147
111
 
148
- **When ALL work is done**, you MUST output the completion signal. Check all of these before signaling:
149
- - Every task in \`PROGRESS.md\` is marked complete \`[x]\`
150
- - All specifications in \`specs/\` are fully implemented
151
- - \`.vercel/output/config.json\` exists with \`"version": 3\`
152
- - All required static files exist in \`.vercel/output/static/\`
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
- This signal is how the system knows you are finished. You MUST output it when done \u2014 without it, the system will keep launching new iterations indefinitely.`;
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.5";
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
- log(`
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
- log(` ${green("+")} specs/2048.md`);
187
+ printAdded("specs/2048.md");
177
188
  } else {
178
- log(` ${dim("\xB7")} specs/ ${dim("already exists")}`);
189
+ printNote(`specs/ ${dim("already exists")}`);
179
190
  }
180
191
  if (result.packageJsonCreated) {
181
- log(` ${green("+")} package.json`);
192
+ printAdded("package.json");
182
193
  } else {
183
- log(` ${dim("\xB7")} package.json ${dim("already exists")}`);
194
+ printNote(`package.json ${dim("already exists")}`);
184
195
  }
185
- log(`
186
- Run ${cyan("pnpm install")} then ${cyan("dream")} to start
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
- log(`
193
- ${bold("\u25B2 dream")} ${dim("\xB7 config")}
194
- `);
195
- log(` ${dim("dir")} ${workDir}`);
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
- log(`
200
- ${red("\u2717")} specs/ not found
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
- log(`
206
- ${dim("specs")} ${dim(`(${specFiles.length})`)}`);
215
+ printLine(
216
+ `
217
+ ${INDENT_MAIN}${dim("specs")} ${dim(`(${specFiles.length})`)}`
218
+ );
207
219
  for (const file of specFiles) {
208
- log(` ${dim("\xB7")} ${file}`);
220
+ printListItem(file);
209
221
  }
210
- log("");
222
+ printLine();
211
223
  });
212
224
  program.command("models").description("List available models and check provider auth").action(async () => {
213
- log(`
214
- ${bold("\u25B2 dream")} ${dim("\xB7 models")}
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
- log(` ${red("\u2717")} Failed to list providers
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
- log(
234
- ` ${icon} ${bold(provider.name)} ${dim(`(${provider.id})`)}${npm}`
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
- log(` ${dim("\xB7")} ${provider.id}/${id} ${dim(name)}`);
249
+ printListItem(`${provider.id}/${id} ${dim(name)}`);
240
250
  }
241
251
  if (models.length === 0) {
242
- log(` ${dim("no models")}`);
252
+ printLine(`${INDENT_LIST}${dim("no models")}`);
243
253
  }
244
- log("");
254
+ printLine();
245
255
  }
246
- log(
247
- ` ${dim("connected")} ${connected.length ? connected.join(", ") : "none"}
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
- log(`
260
- ${red("\u2717")} specs/ not found in ${workDir}
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
- log(`
270
- ${bold("\u25B2 dream")} ${dim("\xB7")} ${title}
271
- `);
272
- log(` ${dim("dir")} ${workDir}`);
273
- log(` ${dim("model")} ${model || dim("default")}`);
274
- log(` ${dim("timeout")} ${formatTime(timeout)}`);
275
- log(` ${dim("max")} ${maxIterations} iterations
276
- `);
277
- log(` ${dim("\u25CC")} Starting OpenCode...`);
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
- log(` ${green("\u25CF")} OpenCode ready`);
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
- log(` ${red("\u2717")} Failed to list providers
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
- log(` ${red("\u2717")} Provider ${bold(providerId)} is not connected`);
323
- log(
324
- ` ${dim("connected")} ${connected.length ? connected.join(", ") : "none"}`
325
- );
326
- log(
353
+ printError(`Provider ${bold(providerId)} is not connected`);
354
+ printKV("connected", connected.length ? connected.join(", ") : "none");
355
+ printLine(
327
356
  `
328
- Run ${cyan("opencode")} and authenticate the ${bold(providerId)} provider
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
- log(
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
- log(
343
- ` ${dim("available")} ${available.slice(0, 5).join(", ")}${available.length > 5 ? ` (+${available.length - 5} more)` : ""}`
369
+ printKV(
370
+ "available",
371
+ `${available.slice(0, 5).join(", ")}${available.length > 5 ? ` (+${available.length - 5} more)` : ""}`
344
372
  );
345
373
  }
346
- log("");
374
+ printLine();
347
375
  server.close();
348
376
  process.exit(1);
349
377
  }
350
- log(` ${green("\u25CF")} Provider ${bold(providerId)} connected
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
- log(`
366
- ${red("\u2717")} Timeout after ${formatTime(elapsed)}
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
- log(` ${cyan(`[${iteration}]`)} Running session...`);
373
- const result = await runSession(client, title, SYSTEM_PROMPT, verbose);
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
- log(
377
- ` ${cyan(`[${iteration}]`)} ${green("\u2713")} Done ${dim(`(${formatTime(iterElapsed)})`)}`
411
+ printLine(
412
+ `${INDENT_MAIN}${cyan(`[${iteration}]`)} ${green("\u2713")} Done ${dim(`(${formatTime(iterElapsed)})`)}`
378
413
  );
379
- log(
414
+ printLine(
380
415
  `
381
- ${green("\u2713")} Completed in ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}
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
- log(
422
+ printLine(
388
423
  `
389
- ${red("\u2717")} Session failed after ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}
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
- log(
395
- ` ${cyan(`[${iteration}]`)} ${dim(`${formatTime(iterElapsed)} \xB7 continuing...`)}
429
+ printLine(
430
+ `${INDENT_MAIN}${cyan(`[${iteration}]`)} ${dim(`${formatTime(iterElapsed)} \xB7 continuing...`)}
396
431
  `
397
432
  );
398
433
  }
399
- log(`
400
- ${red("\u2717")} Max iterations reached
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
- log(` ${dim("creating session...")}`);
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
- log(` ${dim(`session ${sessionId.slice(0, 8)}`)}`);
419
- log(` ${dim("subscribing to events...")}`);
453
+ printSessionDim(`session ${sessionId.slice(0, 8)}`);
454
+ printSessionDim("subscribing to events...");
420
455
  const events = await client.event.subscribe();
421
- log(` ${dim("sending prompt...")}`);
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
- log(
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
- log(dim(` event: ${event.type} [${sid}]`));
476
+ printSessionDim(`event: ${event.type} [${sid}]`);
445
477
  if (event.type !== "server.connected") {
446
- log(
478
+ printLine(
447
479
  dim(
448
- ` ${JSON.stringify(event.properties).slice(0, 200)}`
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
- ${pad}`);
495
+ ${INDENT_SESSION}`);
464
496
  process.stdout.write(
465
- lastOutput !== "text" ? `${pad}${dim(indented)}` : dim(indented)
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
- ${pad}`);
504
+ ${INDENT_SESSION}`);
473
505
  process.stdout.write(
474
- lastOutput !== "text" ? `${pad}${dim(indented)}` : dim(indented)
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
- log(
488
- `${pad}${dim("\u25B8")} ${toolName}${context ? dim(` ${context}`) : ""}`
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
- log(`${pad}${red("\u2717")} ${toolName}: ${state.error}`);
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
- log(`${pad}${green("\u270E")} ${relative}`);
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
- log(`${pad}${red("\u2717")} ${msg}`);
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
- log(`${pad}${dim(`${toolCalls} tools \xB7 ${tokens}${cost}`)}`);
560
+ printSessionDim(`${toolCalls} tools \xB7 ${tokens}${cost}`);
529
561
  if (responseText.length === 0) {
530
- log(`${pad}${red("\u2717")} No response from model`);
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.6",
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": {