Skip to content

Commit 6319326

Browse files
authored
Merge pull request #307 from cloudflare/main
Release: main -> demo
2 parents 526c056 + ebe25a6 commit 6319326

File tree

21 files changed

+2661
-180
lines changed

21 files changed

+2661
-180
lines changed

sdk/README.md

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,21 @@ const session = await client.build('Build a weather dashboard', {
9696

9797
### `client.connect(agentId)`
9898

99-
Connect to an existing app session.
99+
Connect to an existing app session. State is automatically restored from the agent, including:
100+
- Phase timeline with completion status
101+
- Generated files
102+
- Agent metadata (query, projectName, behaviorType, etc.)
100103

101104
```ts
102105
const session = await client.connect('agent-id-here', {
103106
credentials: { ... }, // Optional
104107
});
105108
await session.connect();
109+
110+
// State is now seeded from the agent
111+
console.log('Original query:', session.state.get().query);
112+
console.log('Phases:', session.phases.list());
113+
console.log('Files:', session.files.listPaths());
106114
```
107115

108116
## App Management
@@ -215,6 +223,71 @@ const snapshot = session.files.snapshot(); // { 'src/App.tsx': '...', ... }
215223
const tree = session.files.tree(); // Nested file tree structure
216224
```
217225

226+
### Phase Timeline
227+
228+
Access the full phase timeline for phasic builds. The timeline is automatically seeded when connecting to an existing agent and updated as phases progress.
229+
230+
```ts
231+
// Get all phases
232+
const phases = session.phases.list();
233+
// [{ id: 'phase-0', name: 'Core Setup', status: 'completed', files: [...] }, ...]
234+
235+
// Get current active phase
236+
const current = session.phases.current();
237+
if (current) {
238+
console.log(`Working on: ${current.name}`);
239+
console.log(`Status: ${current.status}`); // 'generating' | 'implementing' | 'validating'
240+
}
241+
242+
// Get completed phases
243+
const done = session.phases.completed();
244+
console.log(`Progress: ${done.length}/${session.phases.count()}`);
245+
246+
// Check if all phases are done
247+
if (session.phases.allCompleted()) {
248+
console.log('Build complete!');
249+
}
250+
251+
// Get phase by ID
252+
const phase = session.phases.get('phase-0');
253+
254+
// Subscribe to phase changes
255+
const unsubscribe = session.phases.onChange((event) => {
256+
console.log(`Phase ${event.type}:`, event.phase.name);
257+
console.log(`Status: ${event.phase.status}`);
258+
console.log(`Total phases: ${event.allPhases.length}`);
259+
});
260+
// Later: unsubscribe();
261+
```
262+
263+
The `onChange` callback receives a `PhaseTimelineEvent`:
264+
265+
```ts
266+
type PhaseTimelineEvent = {
267+
type: 'added' | 'updated'; // New phase vs status/file change
268+
phase: PhaseInfo; // The affected phase
269+
allPhases: PhaseInfo[]; // All phases after this change
270+
};
271+
```
272+
273+
Each phase contains:
274+
275+
```ts
276+
type PhaseInfo = {
277+
id: string; // 'phase-0', 'phase-1', etc.
278+
name: string; // 'Core Setup', 'Authentication', etc.
279+
description: string; // What the phase accomplishes
280+
status: PhaseStatus; // 'pending' | 'generating' | 'implementing' | 'validating' | 'completed' | 'cancelled'
281+
files: PhaseFile[]; // Files in this phase
282+
};
283+
284+
type PhaseFile = {
285+
path: string; // 'src/App.tsx'
286+
purpose: string; // 'Main application component'
287+
status: PhaseFileStatus; // 'pending' | 'generating' | 'completed' | 'cancelled'
288+
};
289+
```
290+
218291
### State
219292

220293
```ts
@@ -226,6 +299,16 @@ console.log(state.phase); // { status: 'idle' | 'generating' | ... }
226299
console.log(state.preview); // Preview deployment state
227300
console.log(state.cloudflare); // Cloudflare deployment state
228301

302+
// Phase timeline (array of all phases)
303+
console.log(state.phases); // [{ id, name, status, files }, ...]
304+
305+
// Agent metadata (seeded from agent_connected)
306+
console.log(state.behaviorType); // 'phasic' | 'agentic'
307+
console.log(state.projectType); // 'app' | 'workflow' | etc.
308+
console.log(state.query); // Original user prompt
309+
console.log(state.projectName); // Project name from blueprint
310+
console.log(state.shouldBeGenerating); // Whether agent is actively generating
311+
229312
// Subscribe to changes
230313
session.state.onChange((next, prev) => {
231314
console.log('State changed:', next);
@@ -324,10 +407,24 @@ All types are exported:
324407

325408
```ts
326409
import type {
410+
// Client & Session
327411
VibeClientOptions,
328412
BuildOptions,
329413
BuildSession,
330414
SessionState,
415+
SessionFiles,
416+
SessionPhases,
417+
418+
// Phase Timeline
419+
PhaseInfo,
420+
PhaseFile,
421+
PhaseStatus,
422+
PhaseFileStatus,
423+
PhaseEventType,
424+
PhaseTimelineEvent,
425+
PhaseTimelineChangeType,
426+
427+
// API
331428
ApiResponse,
332429
AppDetails,
333430
Credentials,

sdk/bun.lock

Lines changed: 175 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cf-vibesdk/sdk",
3-
"version": "0.0.4",
3+
"version": "0.0.9",
44
"type": "module",
55
"exports": {
66
".": {
@@ -19,16 +19,18 @@
1919
},
2020
"scripts": {
2121
"build": "rm -rf ./dist && bun build ./src/index.ts --outdir ./dist --target browser",
22-
"bundle-types": "dts-bundle-generator --export-referenced-types false --project ./tsconfig.protocol.json -o ./dist/index.d.ts ./src/index.ts",
22+
"bundle-types": "dts-bundle-generator --export-referenced-types false --no-check --project ./tsconfig.protocol.json -o ./dist/index.d.ts ./src/index.ts && bun run scripts/expand-drizzle-types.ts",
2323
"typecheck": "tsc -p ./tsconfig.json --noEmit",
2424
"test": "bun test test/*.test.ts",
25-
"test:integration": "bun test --timeout 600000 test/integration/*.test.ts"
25+
"test:integration": "bun test --timeout 600000 test/integration/*.test.ts",
26+
"package": "bun run typecheck && bun run test && bun run build && bun run bundle-types"
2627
},
2728
"devDependencies": {
2829
"@cloudflare/workers-types": "^4.20241218.0",
2930
"@types/node": "^25.0.3",
3031
"dts-bundle-generator": "^9.5.1",
3132
"miniflare": "^4.20251217.0",
33+
"puppeteer": "^24.8.0",
3234
"typescript": "^5.9.3",
3335
"wrangler": "^4.14.1"
3436
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Post-processes bundled .d.ts to expand Drizzle $inferSelect types.
3+
*/
4+
5+
import { readFileSync, writeFileSync } from 'fs';
6+
7+
const DTS_PATH = './dist/index.d.ts';
8+
9+
interface ColumnInfo {
10+
name: string;
11+
dataType: string;
12+
notNull: boolean;
13+
}
14+
15+
interface TableInfo {
16+
columns: Record<string, ColumnInfo>;
17+
}
18+
19+
// Store expanded types for later reference
20+
const expandedTypes = new Map<string, TableInfo>();
21+
22+
/**
23+
* Parse a table definition from the .d.ts content to extract column info
24+
*/
25+
function parseTableDefinition(content: string, tableName: string): TableInfo | null {
26+
// Match: declare const tableName: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
27+
const tableRegex = new RegExp(
28+
`declare const ${tableName}:\\s*import\\("drizzle-orm/sqlite-core"\\)\\.SQLiteTableWithColumns<\\{[^}]*columns:\\s*\\{`,
29+
's'
30+
);
31+
32+
const tableMatch = content.match(tableRegex);
33+
if (!tableMatch) {
34+
return null;
35+
}
36+
37+
const startIndex = tableMatch.index! + tableMatch[0].length;
38+
39+
// Find the columns block by tracking brace depth
40+
let braceDepth = 1;
41+
let endIndex = startIndex;
42+
for (let i = startIndex; i < content.length && braceDepth > 0; i++) {
43+
if (content[i] === '{') braceDepth++;
44+
if (content[i] === '}') braceDepth--;
45+
endIndex = i;
46+
}
47+
48+
const columnsBlock = content.slice(startIndex, endIndex);
49+
50+
// Parse each column
51+
const columns: Record<string, ColumnInfo> = {};
52+
53+
// Match column definitions: columnName: import("drizzle-orm/sqlite-core").SQLiteColumn<{...}>
54+
const columnRegex = /(\w+):\s*import\("drizzle-orm\/sqlite-core"\)\.SQLiteColumn<\{([^>]+)\}>/gs;
55+
let match;
56+
57+
while ((match = columnRegex.exec(columnsBlock)) !== null) {
58+
const columnName = match[1];
59+
const columnDef = match[2];
60+
61+
// Extract data type
62+
const dataMatch = columnDef.match(/data:\s*([^;]+);/);
63+
// Extract notNull
64+
const notNullMatch = columnDef.match(/notNull:\s*(true|false)/);
65+
66+
if (dataMatch) {
67+
const dataType = dataMatch[1].trim();
68+
69+
columns[columnName] = {
70+
name: columnName,
71+
dataType,
72+
notNull: notNullMatch ? notNullMatch[1] === 'true' : false,
73+
};
74+
}
75+
}
76+
77+
return { columns };
78+
}
79+
80+
/**
81+
* Generate an expanded type string from column info
82+
* @param serializeDates - if true, convert Date to string (for Serialized<T> types)
83+
*/
84+
function generateExpandedType(tableInfo: TableInfo, serializeDates = false): string {
85+
const fields = Object.entries(tableInfo.columns)
86+
.map(([name, col]) => {
87+
let dataType = col.dataType;
88+
89+
// Convert Date to string for serialized types
90+
if (serializeDates && dataType === 'Date') {
91+
dataType = 'string';
92+
}
93+
94+
const type = col.notNull ? dataType : `${dataType} | null`;
95+
return `\t${name}: ${type};`;
96+
})
97+
.join('\n');
98+
99+
return `{\n${fields}\n}`;
100+
}
101+
102+
/**
103+
* Find all tables referenced by $inferSelect in the content
104+
*/
105+
function findInferSelectReferences(content: string): Map<string, string[]> {
106+
const refs = new Map<string, string[]>();
107+
108+
// Match: type TypeName = typeof tableName.$inferSelect;
109+
const regex = /type\s+(\w+)\s*=\s*typeof\s+(\w+)\.\$inferSelect;/g;
110+
let match;
111+
112+
while ((match = regex.exec(content)) !== null) {
113+
const typeName = match[1];
114+
const tableName = match[2];
115+
116+
if (!refs.has(tableName)) {
117+
refs.set(tableName, []);
118+
}
119+
refs.get(tableName)!.push(typeName);
120+
}
121+
122+
return refs;
123+
}
124+
125+
/**
126+
* Find Serialized<TypeName> patterns where TypeName is an expanded type
127+
*/
128+
function expandSerializedTypes(content: string): string {
129+
// Find patterns like: type Foo = Serialized<Bar>;
130+
// where Bar is one of our expanded types
131+
132+
for (const [typeName, tableInfo] of expandedTypes) {
133+
// Match: Serialized<TypeName>
134+
const serializedRegex = new RegExp(`Serialized<${typeName}>`, 'g');
135+
136+
if (serializedRegex.test(content)) {
137+
const serializedExpanded = generateExpandedType(tableInfo, true);
138+
content = content.replace(serializedRegex, serializedExpanded);
139+
console.log(` Expanded Serialized<${typeName}>`);
140+
}
141+
}
142+
143+
return content;
144+
}
145+
146+
function main() {
147+
console.log('Expanding Drizzle $inferSelect types...');
148+
149+
let content = readFileSync(DTS_PATH, 'utf-8');
150+
151+
// Find all $inferSelect references
152+
const inferSelectRefs = findInferSelectReferences(content);
153+
console.log(`Found ${inferSelectRefs.size} tables with $inferSelect references`);
154+
155+
// Process each table
156+
for (const [tableName, typeNames] of inferSelectRefs) {
157+
console.log(` Processing table: ${tableName} (types: ${typeNames.join(', ')})`);
158+
159+
const tableInfo = parseTableDefinition(content, tableName);
160+
if (!tableInfo) {
161+
console.warn(` Warning: Could not parse table definition for ${tableName}`);
162+
continue;
163+
}
164+
165+
const columnCount = Object.keys(tableInfo.columns).length;
166+
console.log(` Found ${columnCount} columns`);
167+
168+
const expandedType = generateExpandedType(tableInfo);
169+
170+
// Replace each type alias with the expanded type
171+
for (const typeName of typeNames) {
172+
const oldDecl = `type ${typeName} = typeof ${tableName}.$inferSelect;`;
173+
const newDecl = `type ${typeName} = ${expandedType}`;
174+
175+
if (content.includes(oldDecl)) {
176+
content = content.replace(oldDecl, newDecl);
177+
console.log(` Replaced: ${typeName}`);
178+
179+
// Store for Serialized expansion
180+
expandedTypes.set(typeName, tableInfo);
181+
}
182+
}
183+
}
184+
185+
// Also remove $inferInsert references (replace with unknown for now)
186+
const inferInsertRegex = /type\s+(\w+)\s*=\s*typeof\s+(\w+)\.\$inferInsert;/g;
187+
content = content.replace(inferInsertRegex, (match, typeName, tableName) => {
188+
console.log(` Removing $inferInsert reference: ${typeName}`);
189+
return `type ${typeName} = Record<string, unknown>;`;
190+
});
191+
192+
// Now expand Serialized<T> types
193+
console.log('\nExpanding Serialized<T> types...');
194+
content = expandSerializedTypes(content);
195+
196+
writeFileSync(DTS_PATH, content);
197+
console.log('\nDone!');
198+
}
199+
200+
main();

sdk/src/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ export class VibeClient {
102102
{ method: 'GET', headers: await this.http.headers() }
103103
);
104104

105-
if (!data.success) {
106-
throw new Error(data.error.message);
105+
if (!data.success || !data.data) {
106+
throw new Error(data.error?.message ?? 'Failed to connect to agent');
107107
}
108108

109109
const start: BuildStartEvent = {
@@ -122,7 +122,7 @@ export class VibeClient {
122122
listPublic: async (query: PublicAppsQuery = {}) => {
123123
const qs = toQueryString({
124124
limit: query.limit,
125-
page: query.page,
125+
offset: query.offset,
126126
sort: query.sort,
127127
order: query.order,
128128
period: query.period,

0 commit comments

Comments
 (0)