@vercel/dream 0.2.0 → 0.2.2

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 ADDED
@@ -0,0 +1,133 @@
1
+ # @vercel/dream
2
+
3
+ Write a spec. Get a deployed app.
4
+
5
+ Dream is a CLI that turns markdown specifications into fully built applications on Vercel. It runs an AI agent in a loop, reading your specs, writing code, and producing a deployable build — no scaffolding, no boilerplate, no manual steps.
6
+
7
+ ```
8
+ pnpm add @vercel/dream
9
+ ```
10
+
11
+ ## How it works
12
+
13
+ 1. You write a specification in `specs/`
14
+ 2. `dream` reads it, plans the work, and starts building
15
+ 3. The agent loops — reading, coding, verifying — until every requirement is met
16
+ 4. Output lands in `.vercel/output/` ready to deploy
17
+
18
+ ```
19
+ ▲ dream · my-app
20
+
21
+ dir /Users/you/code/my-app
22
+ model vercel/anthropic/claude-opus-4.5
23
+ timeout 60.0m
24
+ max 100 iterations
25
+
26
+ ● OpenCode ready
27
+ ● Provider vercel connected
28
+
29
+ [1] Running session...
30
+ ▸ read specs/app.md
31
+ ▸ write PROGRESS.md
32
+
33
+ Starting with the HTML structure and core game logic.
34
+
35
+ ▸ write .vercel/output/config.json
36
+ ▸ write .vercel/output/static/index.html
37
+ ✎ .vercel/output/static/index.html
38
+ ▸ write .vercel/output/static/styles.css
39
+ ✎ .vercel/output/static/styles.css
40
+ ▸ write .vercel/output/static/app.js
41
+ ✎ .vercel/output/static/app.js
42
+
43
+ All tasks complete. Verifying output structure.
44
+
45
+ ▸ glob .vercel/output/**/*
46
+ 12 tools · 48.2k→3.1k · $0.12
47
+ [1] ✓ Done (34.2s)
48
+
49
+ ✓ Completed in 1 iteration(s) (34.2s)
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```bash
55
+ # Scaffold a new project
56
+ dream init my-app
57
+ cd my-app
58
+
59
+ # Write your spec
60
+ cat > specs/app.md << 'EOF'
61
+ # Landing Page
62
+
63
+ A minimal dark-themed landing page with:
64
+ - Hero section with animated gradient title
65
+ - Feature grid (3 columns)
66
+ - Email signup form
67
+ EOF
68
+
69
+ # Build it
70
+ pnpm install
71
+ dream
72
+ ```
73
+
74
+ ## Specs
75
+
76
+ Specs are markdown files in the `specs/` directory. Write what you want — the more detail, the better the output. The agent reads every `.md` file in the directory.
77
+
78
+ ```
79
+ specs/
80
+ ├── app.md # Main application spec
81
+ ├── design.md # Visual design requirements
82
+ └── accessibility.md # A11y requirements
83
+ ```
84
+
85
+ ## Output
86
+
87
+ Dream produces [Vercel Build Output API v3](https://vercel.com/docs/build-output-api/v3) — static files in `.vercel/output/static/` with a `config.json`. Deploy to Vercel or serve anywhere.
88
+
89
+ ## Commands
90
+
91
+ | Command | Description |
92
+ |---------|-------------|
93
+ | `dream` | Build the project from specs |
94
+ | `dream init` | Scaffold a new dream project |
95
+ | `dream models` | List available models and auth status |
96
+ | `dream config` | Show project configuration |
97
+
98
+ ## Options
99
+
100
+ | Flag | Description | Default |
101
+ |------|-------------|---------|
102
+ | `-m, --model` | Model in `provider/model` format | `vercel/anthropic/claude-opus-4.5` |
103
+ | `-t, --timeout` | Timeout in milliseconds | `3600000` (60m) |
104
+ | `-i, --max-iterations` | Maximum agent loops | `100` |
105
+ | `-v, --verbose` | Show all events | `false` |
106
+ | `-d, --dir` | Working directory | `.` |
107
+
108
+ ## Authentication
109
+
110
+ Dream uses the [Vercel AI Gateway](https://vercel.com/ai-gateway). Authenticate with either:
111
+
112
+ **OIDC token** (Vercel deployments & local dev):
113
+ ```bash
114
+ vercel env pull # writes .env.local with VERCEL_OIDC_TOKEN
115
+ source .env.local
116
+ dream
117
+ ```
118
+
119
+ **API key**:
120
+ ```bash
121
+ export VERCEL_API_KEY=your_key
122
+ dream
123
+ ```
124
+
125
+ ## Requirements
126
+
127
+ - [OpenCode](https://opencode.ai) installed and available on PATH
128
+ - Node.js 18+
129
+ - A Vercel account with AI Gateway access
130
+
131
+ ## License
132
+
133
+ MIT
package/dist/dream.js ADDED
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/dream.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { createOpencode } from "@opencode-ai/sdk";
7
+ import { program } from "commander";
8
+ var STOP_WORD = "<DREAM DONE>";
9
+ var SYSTEM_PROMPT = `# Dream Agent
10
+
11
+ You are an autonomous agent building a project from specifications. You run in a loop until completion.
12
+
13
+ ## Critical: State Lives on Disk
14
+
15
+ Each iteration starts with fresh context. You must:
16
+ - Read specifications from the \`specs/\` directory in the current working directory
17
+ - Track your progress in a \`PROGRESS.md\` file (create it on first run)
18
+ - On each iteration, read \`PROGRESS.md\` to understand what's done and what remains
19
+ - Update \`PROGRESS.md\` after completing each task
20
+
21
+ This ensures you can resume from any point if interrupted.
22
+
23
+ ## Workflow
24
+
25
+ 1. **Read state**: Read all files in \`specs/\` and \`PROGRESS.md\` (if exists)
26
+ 2. **Plan**: If no \`PROGRESS.md\`, create it with a task breakdown from the specs
27
+ 3. **Execute**: Work on the next incomplete task
28
+ 4. **Update**: Mark the task complete in \`PROGRESS.md\`
29
+ 5. **Verify**: Check your work meets the spec requirements
30
+ 6. **Repeat or complete**: If tasks remain, continue. If all done, output completion signal.
31
+
32
+ ## Build Output API
33
+
34
+ Your output must use [Vercel's Build Output API](https://vercel.com/docs/build-output-api/v3).
35
+
36
+ ### Directory Structure
37
+
38
+ \`\`\`
39
+ .vercel/output/
40
+ \u251C\u2500\u2500 config.json # Required: { "version": 3 }
41
+ \u2514\u2500\u2500 static/ # Static files served from root (/)
42
+ \u251C\u2500\u2500 index.html
43
+ \u251C\u2500\u2500 styles.css
44
+ \u2514\u2500\u2500 ...
45
+ \`\`\`
46
+
47
+ ### Minimal config.json
48
+
49
+ \`\`\`json
50
+ {
51
+ "version": 3
52
+ }
53
+ \`\`\`
54
+
55
+ Static files in \`.vercel/output/static/\` are served at the deployment root. Subdirectories are preserved in URLs.
56
+
57
+ ## PROGRESS.md Format
58
+
59
+ \`\`\`markdown
60
+ # Progress
61
+
62
+ ## Tasks
63
+ - [x] Completed task
64
+ - [ ] Pending task
65
+ - [ ] Another pending task
66
+
67
+ ## Notes
68
+ Any learnings or context for future iterations.
69
+ \`\`\`
70
+
71
+ ## Completion
72
+
73
+ **Only output the completion signal when ALL of the following are true:**
74
+ - Every task in \`PROGRESS.md\` is marked complete \`[x]\`
75
+ - All specifications in \`specs/\` are fully implemented
76
+ - \`.vercel/output/config.json\` exists with \`"version": 3\`
77
+ - All required static files exist in \`.vercel/output/static/\`
78
+
79
+ When complete, output exactly:
80
+
81
+ ${STOP_WORD}
82
+
83
+ Do NOT output this signal if any work remains. Continue iterating until the specs are fully met.`;
84
+ var DEFAULT_TIMEOUT = 36e5;
85
+ var DEFAULT_MAX_ITERATIONS = 100;
86
+ var DEFAULT_MODEL = "vercel/anthropic/claude-opus-4.5";
87
+ var dim = (s) => `\x1B[2m${s}\x1B[22m`;
88
+ var bold = (s) => `\x1B[1m${s}\x1B[22m`;
89
+ var green = (s) => `\x1B[32m${s}\x1B[39m`;
90
+ var red = (s) => `\x1B[31m${s}\x1B[39m`;
91
+ var cyan = (s) => `\x1B[36m${s}\x1B[39m`;
92
+ var log = console.log;
93
+ program.name("dream").description("Run OpenCode in a loop until specs are complete").version("0.1.0").option("-d, --dir <directory>", "Working directory", ".");
94
+ program.command("init").description("Initialize a new dream project").action(() => {
95
+ const workDir = path.resolve(program.opts().dir);
96
+ const specsDir = path.join(workDir, "specs");
97
+ const packageJsonPath = path.join(workDir, "package.json");
98
+ log(`
99
+ ${bold("\u25B2 dream")} ${dim("\xB7 init")}
100
+ `);
101
+ if (!fs.existsSync(workDir)) {
102
+ fs.mkdirSync(workDir, { recursive: true });
103
+ }
104
+ if (!fs.existsSync(specsDir)) {
105
+ fs.mkdirSync(specsDir);
106
+ fs.writeFileSync(
107
+ path.join(specsDir, "sample.md"),
108
+ "# Sample Spec\n\n## Context\n\nDescribe what you want to build here.\n\n## Tasks\n\n- [ ] First task\n- [ ] Second task\n"
109
+ );
110
+ log(` ${green("+")} specs/sample.md`);
111
+ } else {
112
+ log(` ${dim("\xB7")} specs/ ${dim("already exists")}`);
113
+ }
114
+ if (!fs.existsSync(packageJsonPath)) {
115
+ const pkg = {
116
+ name: path.basename(workDir),
117
+ version: "0.1.0",
118
+ private: true,
119
+ scripts: { build: "dream" },
120
+ dependencies: { "@vercel/dream": "^0.1.0" }
121
+ };
122
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, " ")}
123
+ `);
124
+ log(` ${green("+")} package.json`);
125
+ } else {
126
+ log(` ${dim("\xB7")} package.json ${dim("already exists")}`);
127
+ }
128
+ log(`
129
+ Run ${cyan("pnpm install")} then ${cyan("dream")} to start
130
+ `);
131
+ });
132
+ program.command("config").description("Show project configuration and specs").action(() => {
133
+ const workDir = path.resolve(program.opts().dir);
134
+ const specsDir = path.join(workDir, "specs");
135
+ log(`
136
+ ${bold("\u25B2 dream")} ${dim("\xB7 config")}
137
+ `);
138
+ log(` ${dim("dir")} ${workDir}`);
139
+ log(` ${dim("timeout")} ${formatTime(DEFAULT_TIMEOUT)}`);
140
+ log(` ${dim("max")} ${DEFAULT_MAX_ITERATIONS} iterations`);
141
+ if (!fs.existsSync(specsDir)) {
142
+ log(`
143
+ ${red("\u2717")} specs/ not found
144
+ `);
145
+ return;
146
+ }
147
+ const specFiles = fs.readdirSync(specsDir).filter((f) => f.endsWith(".md"));
148
+ log(`
149
+ ${dim("specs")} ${dim(`(${specFiles.length})`)}`);
150
+ for (const file of specFiles) {
151
+ log(` ${dim("\xB7")} ${file}`);
152
+ }
153
+ log("");
154
+ });
155
+ program.command("models").description("List available models and check provider auth").action(async () => {
156
+ log(`
157
+ ${bold("\u25B2 dream")} ${dim("\xB7 models")}
158
+ `);
159
+ log(` ${dim("\u25CC")} Starting OpenCode...`);
160
+ const { client, server } = await createOpencode({
161
+ port: 0,
162
+ config: { enabled_providers: ["vercel"] }
163
+ });
164
+ try {
165
+ const res = await client.provider.list();
166
+ if (res.error) {
167
+ log(` ${red("\u2717")} Failed to list providers
168
+ `);
169
+ return;
170
+ }
171
+ const { all, connected } = res.data;
172
+ for (const provider of all) {
173
+ const isConnected = connected.includes(provider.id);
174
+ const icon = isConnected ? green("\u25CF") : red("\u25CB");
175
+ const npm = provider.npm ? dim(` npm:${provider.npm}`) : "";
176
+ log(
177
+ ` ${icon} ${bold(provider.name)} ${dim(`(${provider.id})`)}${npm}`
178
+ );
179
+ const models = Object.entries(provider.models);
180
+ for (const [id, model] of models) {
181
+ const name = model.name ?? id;
182
+ log(` ${dim("\xB7")} ${provider.id}/${id} ${dim(name)}`);
183
+ }
184
+ if (models.length === 0) {
185
+ log(` ${dim("no models")}`);
186
+ }
187
+ log("");
188
+ }
189
+ log(
190
+ ` ${dim("connected")} ${connected.length ? connected.join(", ") : "none"}
191
+ `
192
+ );
193
+ } finally {
194
+ server.close();
195
+ process.exit(0);
196
+ }
197
+ });
198
+ 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) => {
199
+ const workDir = path.resolve(opts.dir);
200
+ const specsDir = path.join(workDir, "specs");
201
+ if (!fs.existsSync(specsDir)) {
202
+ log(`
203
+ ${red("\u2717")} specs/ not found in ${workDir}
204
+ `);
205
+ process.exit(1);
206
+ }
207
+ const timeout = opts.timeout ? Number.parseInt(opts.timeout, 10) : DEFAULT_TIMEOUT;
208
+ const maxIterations = opts.maxIterations ? Number.parseInt(opts.maxIterations, 10) : DEFAULT_MAX_ITERATIONS;
209
+ const verbose = opts.verbose ?? false;
210
+ const model = opts.model ?? process.env.DREAM_MODEL ?? DEFAULT_MODEL;
211
+ const title = path.basename(workDir);
212
+ log(`
213
+ ${bold("\u25B2 dream")} ${dim("\xB7")} ${title}
214
+ `);
215
+ log(` ${dim("dir")} ${workDir}`);
216
+ log(` ${dim("model")} ${model || dim("default")}`);
217
+ log(` ${dim("timeout")} ${formatTime(timeout)}`);
218
+ log(` ${dim("max")} ${maxIterations} iterations
219
+ `);
220
+ log(` ${dim("\u25CC")} Starting OpenCode...`);
221
+ const oidcToken = process.env.VERCEL_OIDC_TOKEN;
222
+ const { client, server } = await createOpencode({
223
+ port: 0,
224
+ config: {
225
+ model,
226
+ permission: {
227
+ edit: "allow",
228
+ bash: "allow",
229
+ webfetch: "allow",
230
+ doom_loop: "allow",
231
+ external_directory: "allow"
232
+ },
233
+ provider: {
234
+ vercel: {
235
+ env: ["VERCEL_API_KEY", "VERCEL_OIDC_TOKEN"],
236
+ ...oidcToken && {
237
+ options: {
238
+ apiKey: oidcToken,
239
+ headers: {
240
+ "ai-gateway-auth-method": "oidc"
241
+ }
242
+ }
243
+ }
244
+ }
245
+ },
246
+ enabled_providers: ["vercel"]
247
+ }
248
+ });
249
+ log(` ${green("\u25CF")} OpenCode ready`);
250
+ const providerId = model?.split("/")[0];
251
+ if (providerId) {
252
+ const providers = await client.provider.list();
253
+ if (providers.error) {
254
+ log(` ${red("\u2717")} Failed to list providers
255
+ `);
256
+ server.close();
257
+ process.exit(1);
258
+ }
259
+ const connected = providers.data.connected ?? [];
260
+ if (!connected.includes(providerId)) {
261
+ log(` ${red("\u2717")} Provider ${bold(providerId)} is not connected`);
262
+ log(
263
+ ` ${dim("connected")} ${connected.length ? connected.join(", ") : "none"}`
264
+ );
265
+ log(
266
+ `
267
+ Run ${cyan("opencode")} and authenticate the ${bold(providerId)} provider
268
+ `
269
+ );
270
+ server.close();
271
+ process.exit(1);
272
+ }
273
+ const provider = providers.data.all.find((p) => p.id === providerId);
274
+ const modelId = model.split("/").slice(1).join("/");
275
+ if (provider && modelId && !provider.models[modelId]) {
276
+ log(
277
+ ` ${red("\u2717")} Model ${bold(modelId)} not found in ${bold(providerId)}`
278
+ );
279
+ const available = Object.keys(provider.models);
280
+ if (available.length) {
281
+ log(
282
+ ` ${dim("available")} ${available.slice(0, 5).join(", ")}${available.length > 5 ? ` (+${available.length - 5} more)` : ""}`
283
+ );
284
+ }
285
+ log("");
286
+ server.close();
287
+ process.exit(1);
288
+ }
289
+ log(` ${green("\u25CF")} Provider ${bold(providerId)} connected
290
+ `);
291
+ }
292
+ const cleanup = () => {
293
+ server.close();
294
+ process.exit(1);
295
+ };
296
+ process.on("SIGINT", cleanup);
297
+ process.on("SIGTERM", cleanup);
298
+ const startTime = Date.now();
299
+ let iteration = 0;
300
+ try {
301
+ while (iteration < maxIterations) {
302
+ const elapsed = Date.now() - startTime;
303
+ if (elapsed >= timeout) {
304
+ log(`
305
+ ${red("\u2717")} Timeout after ${formatTime(elapsed)}
306
+ `);
307
+ process.exit(1);
308
+ }
309
+ iteration++;
310
+ const iterStart = Date.now();
311
+ log(` ${cyan(`[${iteration}]`)} Running session...`);
312
+ const result = await runSession(client, title, SYSTEM_PROMPT, verbose);
313
+ const iterElapsed = Date.now() - iterStart;
314
+ if (result === "done") {
315
+ log(
316
+ ` ${cyan(`[${iteration}]`)} ${green("\u2713")} Done ${dim(`(${formatTime(iterElapsed)})`)}`
317
+ );
318
+ log(
319
+ `
320
+ ${green("\u2713")} Completed in ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}
321
+ `
322
+ );
323
+ process.exit(0);
324
+ }
325
+ if (result === "error") {
326
+ log(
327
+ `
328
+ ${red("\u2717")} Session failed after ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}
329
+ `
330
+ );
331
+ process.exit(1);
332
+ }
333
+ log(
334
+ ` ${cyan(`[${iteration}]`)} ${dim(`${formatTime(iterElapsed)} \xB7 continuing...`)}
335
+ `
336
+ );
337
+ }
338
+ log(`
339
+ ${red("\u2717")} Max iterations reached
340
+ `);
341
+ process.exit(1);
342
+ } finally {
343
+ server.close();
344
+ }
345
+ });
346
+ async function runSession(client, title, systemPrompt, verbose) {
347
+ log(` ${dim("creating session...")}`);
348
+ const sessionResponse = await client.session.create({
349
+ body: { title: `Dream: ${title}` }
350
+ });
351
+ if (sessionResponse.error) {
352
+ throw new Error(
353
+ `Failed to create session: ${JSON.stringify(sessionResponse.error.errors)}`
354
+ );
355
+ }
356
+ const sessionId = sessionResponse.data.id;
357
+ log(` ${dim(`session ${sessionId.slice(0, 8)}`)}`);
358
+ log(` ${dim("subscribing to events...")}`);
359
+ const events = await client.event.subscribe();
360
+ log(` ${dim("sending prompt...")}`);
361
+ const promptResponse = await client.session.promptAsync({
362
+ path: { id: sessionId },
363
+ body: {
364
+ parts: [{ type: "text", text: systemPrompt }]
365
+ }
366
+ });
367
+ if (promptResponse.error) {
368
+ log(
369
+ ` ${red("\u2717")} prompt error: ${JSON.stringify(promptResponse.error)}`
370
+ );
371
+ return "error";
372
+ }
373
+ let responseText = "";
374
+ let toolCalls = 0;
375
+ let totalCost = 0;
376
+ let totalTokensIn = 0;
377
+ let totalTokensOut = 0;
378
+ const seenTools = /* @__PURE__ */ new Set();
379
+ let lastOutput = "none";
380
+ const pad = " ";
381
+ for await (const event of events.stream) {
382
+ const props = event.properties;
383
+ if (verbose) {
384
+ const sid = props.sessionID ? props.sessionID.slice(0, 8) : "global";
385
+ log(dim(` event: ${event.type} [${sid}]`));
386
+ if (event.type !== "server.connected") {
387
+ log(
388
+ dim(
389
+ ` ${JSON.stringify(event.properties).slice(0, 200)}`
390
+ )
391
+ );
392
+ }
393
+ }
394
+ if (props.sessionID && props.sessionID !== sessionId) {
395
+ continue;
396
+ }
397
+ if (event.type === "message.part.updated") {
398
+ const { part } = event.properties;
399
+ const delta = event.properties.delta;
400
+ if (part.type === "text" && delta) {
401
+ responseText += delta;
402
+ if (lastOutput === "tool") process.stdout.write("\n");
403
+ const indented = delta.replace(/\n/g, `
404
+ ${pad}`);
405
+ process.stdout.write(
406
+ lastOutput !== "text" ? `${pad}${dim(indented)}` : dim(indented)
407
+ );
408
+ lastOutput = "text";
409
+ }
410
+ if (part.type === "reasoning" && delta) {
411
+ if (lastOutput === "tool") process.stdout.write("\n");
412
+ const indented = delta.replace(/\n/g, `
413
+ ${pad}`);
414
+ process.stdout.write(
415
+ lastOutput !== "text" ? `${pad}${dim(indented)}` : dim(indented)
416
+ );
417
+ lastOutput = "text";
418
+ }
419
+ if (part.type === "tool") {
420
+ const callID = part.callID;
421
+ const toolName = part.tool;
422
+ const state = part.state;
423
+ if (state.status === "running" && !seenTools.has(callID)) {
424
+ seenTools.add(callID);
425
+ toolCalls++;
426
+ if (lastOutput === "text") process.stdout.write("\n\n");
427
+ const context = toolContext(toolName, state.input) ?? state.title;
428
+ log(
429
+ `${pad}${dim("\u25B8")} ${toolName}${context ? dim(` ${context}`) : ""}`
430
+ );
431
+ lastOutput = "tool";
432
+ }
433
+ if (state.status === "error") {
434
+ if (lastOutput === "text") process.stdout.write("\n");
435
+ log(`${pad}${red("\u2717")} ${toolName}: ${state.error}`);
436
+ lastOutput = "tool";
437
+ }
438
+ }
439
+ if (part.type === "step-finish") {
440
+ const step = part;
441
+ totalCost += step.cost ?? 0;
442
+ totalTokensIn += step.tokens?.input ?? 0;
443
+ totalTokensOut += step.tokens?.output ?? 0;
444
+ }
445
+ }
446
+ if (event.type === "file.edited") {
447
+ const file = event.properties.file;
448
+ if (file) {
449
+ if (lastOutput === "text") process.stdout.write("\n");
450
+ const relative = file.replace(`${process.cwd()}/`, "");
451
+ log(`${pad}${green("\u270E")} ${relative}`);
452
+ lastOutput = "tool";
453
+ }
454
+ }
455
+ if (event.type === "session.error") {
456
+ const errProps = event.properties;
457
+ const msg = errProps.error?.data?.message ?? errProps.error?.name ?? "session error";
458
+ if (lastOutput === "text") process.stdout.write("\n");
459
+ log(`${pad}${red("\u2717")} ${msg}`);
460
+ return "error";
461
+ }
462
+ if (event.type === "session.idle") {
463
+ break;
464
+ }
465
+ }
466
+ if (lastOutput === "text") process.stdout.write("\n");
467
+ const tokens = `${formatTokens(totalTokensIn)}\u2192${formatTokens(totalTokensOut)}`;
468
+ const cost = totalCost > 0 ? ` \xB7 $${totalCost.toFixed(2)}` : "";
469
+ log(`${pad}${dim(`${toolCalls} tools \xB7 ${tokens}${cost}`)}`);
470
+ if (responseText.length === 0) {
471
+ log(`${pad}${red("\u2717")} No response from model`);
472
+ return "error";
473
+ }
474
+ return responseText.includes(STOP_WORD) ? "done" : "continue";
475
+ }
476
+ function formatTime(ms) {
477
+ if (ms < 1e3) return `${ms}ms`;
478
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
479
+ return `${(ms / 6e4).toFixed(1)}m`;
480
+ }
481
+ function formatTokens(n) {
482
+ if (n < 1e3) return `${n}`;
483
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
484
+ return `${(n / 1e6).toFixed(1)}M`;
485
+ }
486
+ function toolContext(tool, input) {
487
+ if (!input) return void 0;
488
+ const filePath = input.filePath;
489
+ const rel = filePath?.replace(`${process.cwd()}/`, "");
490
+ switch (tool) {
491
+ case "read":
492
+ case "write":
493
+ case "edit":
494
+ return rel;
495
+ case "bash": {
496
+ const cmd = input.command;
497
+ if (!cmd) return void 0;
498
+ return cmd.length > 60 ? `${cmd.slice(0, 60)}\u2026` : cmd;
499
+ }
500
+ case "glob":
501
+ case "grep":
502
+ return input.pattern;
503
+ case "fetch":
504
+ return input.url;
505
+ default:
506
+ return void 0;
507
+ }
508
+ }
509
+ await program.parseAsync();
package/package.json CHANGED
@@ -1,35 +1,43 @@
1
1
  {
2
- "name": "@vercel/dream",
3
- "version": "0.2.0",
4
- "description": "A CLI that runs OpenCode in a loop until specs are complete",
5
- "type": "module",
6
- "bin": {
7
- "dream": "./bin/dream.ts"
8
- },
9
- "files": ["bin"],
10
- "scripts": {
11
- "check": "biome check .",
12
- "check:fix": "biome check --write .",
13
- "version": "changeset version",
14
- "release": "changeset publish"
15
- },
16
- "dependencies": {
17
- "@ai-sdk/gateway": "^3.0.39",
18
- "@opencode-ai/sdk": "^1.1.0",
19
- "commander": "^12.0.0",
20
- "tsx": "^4.0.0"
21
- },
22
- "peerDependencies": {
23
- "opencode-ai": ">=1.0.0"
24
- },
25
- "devDependencies": {
26
- "@biomejs/biome": "^1.9.0",
27
- "@changesets/cli": "^2.29.8",
28
- "@types/node": "^22.0.0",
29
- "lefthook": "^2.1.0",
30
- "typescript": "^5.4.0"
31
- },
32
- "keywords": ["cli", "opencode", "ai", "automation"],
33
- "author": "",
34
- "license": "MIT"
35
- }
2
+ "name": "@vercel/dream",
3
+ "version": "0.2.2",
4
+ "description": "A CLI that runs OpenCode in a loop until specs are complete",
5
+ "type": "module",
6
+ "bin": {
7
+ "dream": "./dist/dream.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "dependencies": {
13
+ "@ai-sdk/gateway": "^3.0.39",
14
+ "@opencode-ai/sdk": "^1.1.0",
15
+ "commander": "^12.0.0",
16
+ "opencode-ai": ">=1.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@biomejs/biome": "^1.9.0",
20
+ "@changesets/cli": "^2.29.8",
21
+ "@types/node": "^22.0.0",
22
+ "lefthook": "^2.1.0",
23
+ "tsup": "^8.5.1",
24
+ "tsx": "^4.0.0",
25
+ "typescript": "^5.4.0"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "opencode",
30
+ "ai",
31
+ "automation"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsx bin/dream.ts",
38
+ "check": "biome check .",
39
+ "check:fix": "biome check --write .",
40
+ "version": "changeset version",
41
+ "release": "pnpm build && changeset publish"
42
+ }
43
+ }
package/bin/dream.ts DELETED
@@ -1,573 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { type OpencodeClient, createOpencode } from "@opencode-ai/sdk";
5
- import { program } from "commander";
6
-
7
- const STOP_WORD = "<DREAM DONE>";
8
-
9
- const SYSTEM_PROMPT = `# Dream Agent
10
-
11
- You are an autonomous agent building a project from specifications. You run in a loop until completion.
12
-
13
- ## Critical: State Lives on Disk
14
-
15
- Each iteration starts with fresh context. You must:
16
- - Read specifications from the \`specs/\` directory in the current working directory
17
- - Track your progress in a \`PROGRESS.md\` file (create it on first run)
18
- - On each iteration, read \`PROGRESS.md\` to understand what's done and what remains
19
- - Update \`PROGRESS.md\` after completing each task
20
-
21
- This ensures you can resume from any point if interrupted.
22
-
23
- ## Workflow
24
-
25
- 1. **Read state**: Read all files in \`specs/\` and \`PROGRESS.md\` (if exists)
26
- 2. **Plan**: If no \`PROGRESS.md\`, create it with a task breakdown from the specs
27
- 3. **Execute**: Work on the next incomplete task
28
- 4. **Update**: Mark the task complete in \`PROGRESS.md\`
29
- 5. **Verify**: Check your work meets the spec requirements
30
- 6. **Repeat or complete**: If tasks remain, continue. If all done, output completion signal.
31
-
32
- ## Build Output API
33
-
34
- Your output must use [Vercel's Build Output API](https://vercel.com/docs/build-output-api/v3).
35
-
36
- ### Directory Structure
37
-
38
- \`\`\`
39
- .vercel/output/
40
- ├── config.json # Required: { "version": 3 }
41
- └── static/ # Static files served from root (/)
42
- ├── index.html
43
- ├── styles.css
44
- └── ...
45
- \`\`\`
46
-
47
- ### Minimal config.json
48
-
49
- \`\`\`json
50
- {
51
- "version": 3
52
- }
53
- \`\`\`
54
-
55
- Static files in \`.vercel/output/static/\` are served at the deployment root. Subdirectories are preserved in URLs.
56
-
57
- ## PROGRESS.md Format
58
-
59
- \`\`\`markdown
60
- # Progress
61
-
62
- ## Tasks
63
- - [x] Completed task
64
- - [ ] Pending task
65
- - [ ] Another pending task
66
-
67
- ## Notes
68
- Any learnings or context for future iterations.
69
- \`\`\`
70
-
71
- ## Completion
72
-
73
- **Only output the completion signal when ALL of the following are true:**
74
- - Every task in \`PROGRESS.md\` is marked complete \`[x]\`
75
- - All specifications in \`specs/\` are fully implemented
76
- - \`.vercel/output/config.json\` exists with \`"version": 3\`
77
- - All required static files exist in \`.vercel/output/static/\`
78
-
79
- When complete, output exactly:
80
-
81
- ${STOP_WORD}
82
-
83
- Do NOT output this signal if any work remains. Continue iterating until the specs are fully met.`;
84
- const DEFAULT_TIMEOUT = 3600000;
85
- const DEFAULT_MAX_ITERATIONS = 100;
86
- const DEFAULT_MODEL = "vercel/anthropic/claude-opus-4.5";
87
-
88
- const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
89
- const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
90
- const green = (s: string) => `\x1b[32m${s}\x1b[39m`;
91
- const red = (s: string) => `\x1b[31m${s}\x1b[39m`;
92
- const cyan = (s: string) => `\x1b[36m${s}\x1b[39m`;
93
- const log = console.log;
94
-
95
- program
96
- .name("dream")
97
- .description("Run OpenCode in a loop until specs are complete")
98
- .version("0.1.0")
99
- .option("-d, --dir <directory>", "Working directory", ".");
100
-
101
- program
102
- .command("init")
103
- .description("Initialize a new dream project")
104
- .action(() => {
105
- const workDir = path.resolve(program.opts().dir);
106
- const specsDir = path.join(workDir, "specs");
107
- const packageJsonPath = path.join(workDir, "package.json");
108
-
109
- log(`\n ${bold("▲ dream")} ${dim("· init")}\n`);
110
-
111
- if (!fs.existsSync(workDir)) {
112
- fs.mkdirSync(workDir, { recursive: true });
113
- }
114
-
115
- if (!fs.existsSync(specsDir)) {
116
- fs.mkdirSync(specsDir);
117
- fs.writeFileSync(
118
- path.join(specsDir, "sample.md"),
119
- "# Sample Spec\n\n## Context\n\nDescribe what you want to build here.\n\n## Tasks\n\n- [ ] First task\n- [ ] Second task\n",
120
- );
121
- log(` ${green("+")} specs/sample.md`);
122
- } else {
123
- log(` ${dim("·")} specs/ ${dim("already exists")}`);
124
- }
125
-
126
- if (!fs.existsSync(packageJsonPath)) {
127
- const pkg = {
128
- name: path.basename(workDir),
129
- version: "0.1.0",
130
- private: true,
131
- scripts: { build: "dream" },
132
- dependencies: { "@vercel/dream": "^0.1.0" },
133
- };
134
- fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, "\t")}\n`);
135
- log(` ${green("+")} package.json`);
136
- } else {
137
- log(` ${dim("·")} package.json ${dim("already exists")}`);
138
- }
139
-
140
- log(`\n Run ${cyan("pnpm install")} then ${cyan("dream")} to start\n`);
141
- });
142
-
143
- program
144
- .command("config")
145
- .description("Show project configuration and specs")
146
- .action(() => {
147
- const workDir = path.resolve(program.opts().dir);
148
- const specsDir = path.join(workDir, "specs");
149
-
150
- log(`\n ${bold("▲ dream")} ${dim("· config")}\n`);
151
- log(` ${dim("dir")} ${workDir}`);
152
- log(` ${dim("timeout")} ${formatTime(DEFAULT_TIMEOUT)}`);
153
- log(` ${dim("max")} ${DEFAULT_MAX_ITERATIONS} iterations`);
154
-
155
- if (!fs.existsSync(specsDir)) {
156
- log(`\n ${red("✗")} specs/ not found\n`);
157
- return;
158
- }
159
-
160
- const specFiles = fs.readdirSync(specsDir).filter((f) => f.endsWith(".md"));
161
- log(`\n ${dim("specs")} ${dim(`(${specFiles.length})`)}`);
162
- for (const file of specFiles) {
163
- log(` ${dim("·")} ${file}`);
164
- }
165
- log("");
166
- });
167
-
168
- program
169
- .command("models")
170
- .description("List available models and check provider auth")
171
- .action(async () => {
172
- log(`\n ${bold("▲ dream")} ${dim("· models")}\n`);
173
- log(` ${dim("◌")} Starting OpenCode...`);
174
- const { client, server } = await createOpencode({
175
- port: 0,
176
- config: { enabled_providers: ["vercel"] },
177
- });
178
-
179
- try {
180
- const res = await client.provider.list();
181
- if (res.error) {
182
- log(` ${red("✗")} Failed to list providers\n`);
183
- return;
184
- }
185
-
186
- const { all, connected } = res.data;
187
- for (const provider of all) {
188
- const isConnected = connected.includes(provider.id);
189
- const icon = isConnected ? green("●") : red("○");
190
- const npm = provider.npm ? dim(` npm:${provider.npm}`) : "";
191
- log(
192
- ` ${icon} ${bold(provider.name)} ${dim(`(${provider.id})`)}${npm}`,
193
- );
194
-
195
- const models = Object.entries(provider.models);
196
- for (const [id, model] of models) {
197
- const name = (model as { name?: string }).name ?? id;
198
- log(` ${dim("·")} ${provider.id}/${id} ${dim(name)}`);
199
- }
200
- if (models.length === 0) {
201
- log(` ${dim("no models")}`);
202
- }
203
- log("");
204
- }
205
-
206
- log(
207
- ` ${dim("connected")} ${connected.length ? connected.join(", ") : "none"}\n`,
208
- );
209
- } finally {
210
- server.close();
211
- process.exit(0);
212
- }
213
- });
214
-
215
- program
216
- .option("-m, --model <model>", "Model to use (provider/model format)")
217
- .option("-t, --timeout <ms>", "Timeout in milliseconds")
218
- .option("-i, --max-iterations <n>", "Maximum iterations")
219
- .option("-v, --verbose", "Verbose output")
220
- .action(async (opts) => {
221
- const workDir = path.resolve(opts.dir);
222
- const specsDir = path.join(workDir, "specs");
223
-
224
- if (!fs.existsSync(specsDir)) {
225
- log(`\n ${red("✗")} specs/ not found in ${workDir}\n`);
226
- process.exit(1);
227
- }
228
-
229
- const timeout = opts.timeout
230
- ? Number.parseInt(opts.timeout, 10)
231
- : DEFAULT_TIMEOUT;
232
- const maxIterations = opts.maxIterations
233
- ? Number.parseInt(opts.maxIterations, 10)
234
- : DEFAULT_MAX_ITERATIONS;
235
- const verbose = opts.verbose ?? false;
236
- const model = opts.model ?? process.env.DREAM_MODEL ?? DEFAULT_MODEL;
237
- const title = path.basename(workDir);
238
-
239
- log(`\n ${bold("▲ dream")} ${dim("·")} ${title}\n`);
240
- log(` ${dim("dir")} ${workDir}`);
241
- log(` ${dim("model")} ${model || dim("default")}`);
242
- log(` ${dim("timeout")} ${formatTime(timeout)}`);
243
- log(` ${dim("max")} ${maxIterations} iterations\n`);
244
-
245
- log(` ${dim("◌")} Starting OpenCode...`);
246
- const oidcToken = process.env.VERCEL_OIDC_TOKEN;
247
- const { client, server } = await createOpencode({
248
- port: 0,
249
- config: {
250
- model,
251
- permission: {
252
- edit: "allow",
253
- bash: "allow",
254
- webfetch: "allow",
255
- doom_loop: "allow",
256
- external_directory: "allow",
257
- },
258
- provider: {
259
- vercel: {
260
- env: ["VERCEL_API_KEY", "VERCEL_OIDC_TOKEN"],
261
- ...(oidcToken && {
262
- options: {
263
- apiKey: oidcToken,
264
- headers: {
265
- "ai-gateway-auth-method": "oidc",
266
- },
267
- },
268
- }),
269
- },
270
- },
271
- enabled_providers: ["vercel"],
272
- },
273
- });
274
- log(` ${green("●")} OpenCode ready`);
275
-
276
- const providerId = model?.split("/")[0];
277
- if (providerId) {
278
- const providers = await client.provider.list();
279
- if (providers.error) {
280
- log(` ${red("✗")} Failed to list providers\n`);
281
- server.close();
282
- process.exit(1);
283
- }
284
- const connected = providers.data.connected ?? [];
285
- if (!connected.includes(providerId)) {
286
- log(` ${red("✗")} Provider ${bold(providerId)} is not connected`);
287
- log(
288
- ` ${dim("connected")} ${connected.length ? connected.join(", ") : "none"}`,
289
- );
290
- log(
291
- `\n Run ${cyan("opencode")} and authenticate the ${bold(providerId)} provider\n`,
292
- );
293
- server.close();
294
- process.exit(1);
295
- }
296
- const provider = providers.data.all.find((p) => p.id === providerId);
297
- const modelId = model.split("/").slice(1).join("/");
298
- if (provider && modelId && !provider.models[modelId]) {
299
- log(
300
- ` ${red("✗")} Model ${bold(modelId)} not found in ${bold(providerId)}`,
301
- );
302
- const available = Object.keys(provider.models);
303
- if (available.length) {
304
- log(
305
- ` ${dim("available")} ${available.slice(0, 5).join(", ")}${available.length > 5 ? ` (+${available.length - 5} more)` : ""}`,
306
- );
307
- }
308
- log("");
309
- server.close();
310
- process.exit(1);
311
- }
312
- log(` ${green("●")} Provider ${bold(providerId)} connected\n`);
313
- }
314
-
315
- const cleanup = () => {
316
- server.close();
317
- process.exit(1);
318
- };
319
- process.on("SIGINT", cleanup);
320
- process.on("SIGTERM", cleanup);
321
-
322
- const startTime = Date.now();
323
- let iteration = 0;
324
-
325
- try {
326
- while (iteration < maxIterations) {
327
- const elapsed = Date.now() - startTime;
328
- if (elapsed >= timeout) {
329
- log(`\n ${red("✗")} Timeout after ${formatTime(elapsed)}\n`);
330
- process.exit(1);
331
- }
332
-
333
- iteration++;
334
- const iterStart = Date.now();
335
- log(` ${cyan(`[${iteration}]`)} Running session...`);
336
-
337
- const result = await runSession(client, title, SYSTEM_PROMPT, verbose);
338
- const iterElapsed = Date.now() - iterStart;
339
-
340
- if (result === "done") {
341
- log(
342
- ` ${cyan(`[${iteration}]`)} ${green("✓")} Done ${dim(`(${formatTime(iterElapsed)})`)}`,
343
- );
344
- log(
345
- `\n ${green("✓")} Completed in ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}\n`,
346
- );
347
- process.exit(0);
348
- }
349
-
350
- if (result === "error") {
351
- log(
352
- `\n ${red("✗")} Session failed after ${bold(String(iteration))} iteration(s) ${dim(`(${formatTime(Date.now() - startTime)})`)}\n`,
353
- );
354
- process.exit(1);
355
- }
356
-
357
- log(
358
- ` ${cyan(`[${iteration}]`)} ${dim(`${formatTime(iterElapsed)} · continuing...`)}\n`,
359
- );
360
- }
361
-
362
- log(`\n ${red("✗")} Max iterations reached\n`);
363
- process.exit(1);
364
- } finally {
365
- server.close();
366
- }
367
- });
368
-
369
- async function runSession(
370
- client: OpencodeClient,
371
- title: string,
372
- systemPrompt: string,
373
- verbose: boolean,
374
- ): Promise<"done" | "continue" | "error"> {
375
- log(` ${dim("creating session...")}`);
376
- const sessionResponse = await client.session.create({
377
- body: { title: `Dream: ${title}` },
378
- });
379
-
380
- if (sessionResponse.error) {
381
- throw new Error(
382
- `Failed to create session: ${JSON.stringify(sessionResponse.error.errors)}`,
383
- );
384
- }
385
-
386
- const sessionId = sessionResponse.data.id;
387
- log(` ${dim(`session ${sessionId.slice(0, 8)}`)}`);
388
-
389
- log(` ${dim("subscribing to events...")}`);
390
- const events = await client.event.subscribe();
391
-
392
- log(` ${dim("sending prompt...")}`);
393
- const promptResponse = await client.session.promptAsync({
394
- path: { id: sessionId },
395
- body: {
396
- parts: [{ type: "text", text: systemPrompt }],
397
- },
398
- });
399
-
400
- if (promptResponse.error) {
401
- log(
402
- ` ${red("✗")} prompt error: ${JSON.stringify(promptResponse.error)}`,
403
- );
404
- return "error";
405
- }
406
-
407
- let responseText = "";
408
- let toolCalls = 0;
409
- let totalCost = 0;
410
- let totalTokensIn = 0;
411
- let totalTokensOut = 0;
412
- const seenTools = new Set<string>();
413
- let lastOutput: "text" | "tool" | "none" = "none";
414
-
415
- const pad = " ";
416
-
417
- for await (const event of events.stream) {
418
- const props = event.properties as { sessionID?: string };
419
-
420
- if (verbose) {
421
- const sid = props.sessionID ? props.sessionID.slice(0, 8) : "global";
422
- log(dim(` event: ${event.type} [${sid}]`));
423
- if (event.type !== "server.connected") {
424
- log(
425
- dim(
426
- ` ${JSON.stringify(event.properties).slice(0, 200)}`,
427
- ),
428
- );
429
- }
430
- }
431
-
432
- if (props.sessionID && props.sessionID !== sessionId) {
433
- continue;
434
- }
435
-
436
- if (event.type === "message.part.updated") {
437
- const { part } = event.properties;
438
- const delta = event.properties.delta as string | undefined;
439
- if (part.type === "text" && delta) {
440
- responseText += delta;
441
- if (lastOutput === "tool") process.stdout.write("\n");
442
- const indented = delta.replace(/\n/g, `\n${pad}`);
443
- process.stdout.write(
444
- lastOutput !== "text" ? `${pad}${dim(indented)}` : dim(indented),
445
- );
446
- lastOutput = "text";
447
- }
448
- if (part.type === "reasoning" && delta) {
449
- if (lastOutput === "tool") process.stdout.write("\n");
450
- const indented = delta.replace(/\n/g, `\n${pad}`);
451
- process.stdout.write(
452
- lastOutput !== "text" ? `${pad}${dim(indented)}` : dim(indented),
453
- );
454
- lastOutput = "text";
455
- }
456
- if (part.type === "tool") {
457
- const callID = part.callID as string;
458
- const toolName = part.tool as string;
459
- const state = part.state as {
460
- status: string;
461
- title?: string;
462
- input?: Record<string, unknown>;
463
- output?: string;
464
- error?: string;
465
- };
466
- if (state.status === "running" && !seenTools.has(callID)) {
467
- seenTools.add(callID);
468
- toolCalls++;
469
- if (lastOutput === "text") process.stdout.write("\n\n");
470
- const context = toolContext(toolName, state.input) ?? state.title;
471
- log(
472
- `${pad}${dim("▸")} ${toolName}${context ? dim(` ${context}`) : ""}`,
473
- );
474
- lastOutput = "tool";
475
- }
476
- if (state.status === "error") {
477
- if (lastOutput === "text") process.stdout.write("\n");
478
- log(`${pad}${red("✗")} ${toolName}: ${state.error}`);
479
- lastOutput = "tool";
480
- }
481
- }
482
- if (part.type === "step-finish") {
483
- const step = part as {
484
- cost?: number;
485
- tokens?: { input: number; output: number };
486
- };
487
- totalCost += step.cost ?? 0;
488
- totalTokensIn += step.tokens?.input ?? 0;
489
- totalTokensOut += step.tokens?.output ?? 0;
490
- }
491
- }
492
-
493
- if (event.type === "file.edited") {
494
- const file = (event.properties as { file?: string }).file;
495
- if (file) {
496
- if (lastOutput === "text") process.stdout.write("\n");
497
- const relative = file.replace(`${process.cwd()}/`, "");
498
- log(`${pad}${green("✎")} ${relative}`);
499
- lastOutput = "tool";
500
- }
501
- }
502
-
503
- if (event.type === "session.error") {
504
- const errProps = event.properties as {
505
- error?: { name?: string; data?: { message?: string } };
506
- };
507
- const msg =
508
- errProps.error?.data?.message ??
509
- errProps.error?.name ??
510
- "session error";
511
- if (lastOutput === "text") process.stdout.write("\n");
512
- log(`${pad}${red("✗")} ${msg}`);
513
- return "error";
514
- }
515
-
516
- if (event.type === "session.idle") {
517
- break;
518
- }
519
- }
520
-
521
- if (lastOutput === "text") process.stdout.write("\n");
522
- const tokens = `${formatTokens(totalTokensIn)}→${formatTokens(totalTokensOut)}`;
523
- const cost = totalCost > 0 ? ` · $${totalCost.toFixed(2)}` : "";
524
- log(`${pad}${dim(`${toolCalls} tools · ${tokens}${cost}`)}`);
525
-
526
- if (responseText.length === 0) {
527
- log(`${pad}${red("✗")} No response from model`);
528
- return "error";
529
- }
530
-
531
- return responseText.includes(STOP_WORD) ? "done" : "continue";
532
- }
533
-
534
- function formatTime(ms: number): string {
535
- if (ms < 1000) return `${ms}ms`;
536
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
537
- return `${(ms / 60000).toFixed(1)}m`;
538
- }
539
-
540
- function formatTokens(n: number): string {
541
- if (n < 1000) return `${n}`;
542
- if (n < 1000000) return `${(n / 1000).toFixed(1)}k`;
543
- return `${(n / 1000000).toFixed(1)}M`;
544
- }
545
-
546
- function toolContext(
547
- tool: string,
548
- input?: Record<string, unknown>,
549
- ): string | undefined {
550
- if (!input) return undefined;
551
- const filePath = input.filePath as string | undefined;
552
- const rel = filePath?.replace(`${process.cwd()}/`, "");
553
- switch (tool) {
554
- case "read":
555
- case "write":
556
- case "edit":
557
- return rel;
558
- case "bash": {
559
- const cmd = input.command as string | undefined;
560
- if (!cmd) return undefined;
561
- return cmd.length > 60 ? `${cmd.slice(0, 60)}…` : cmd;
562
- }
563
- case "glob":
564
- case "grep":
565
- return input.pattern as string | undefined;
566
- case "fetch":
567
- return input.url as string | undefined;
568
- default:
569
- return undefined;
570
- }
571
- }
572
-
573
- await program.parseAsync();