@cf-vibesdk/sdk 0.0.1 → 0.0.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,140 @@
1
+ # @cf-vibesdk/sdk
2
+
3
+ Type-safe client SDK for the VibeSDK platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @cf-vibesdk/sdk
9
+ ```
10
+
11
+ ## Quickstart (Bun)
12
+
13
+ ```ts
14
+ import { PhasicClient } from '@cf-vibesdk/sdk';
15
+
16
+ const client = new PhasicClient({
17
+ baseUrl: 'http://localhost:5173',
18
+ apiKey: process.env.VIBESDK_API_KEY!,
19
+ });
20
+
21
+ const session = await client.build('Build a simple hello world page.', {
22
+ projectType: 'app',
23
+ autoGenerate: true,
24
+ });
25
+
26
+ // High-level lifecycle waits
27
+ await session.wait.generationStarted();
28
+ await session.wait.deployable();
29
+
30
+ // Preview deployment (command + awaitable)
31
+ const previewWait = session.wait.previewDeployed();
32
+ session.deployPreview();
33
+ const deployed = await previewWait;
34
+
35
+ console.log('Preview URL:', deployed.previewURL);
36
+
37
+ // Workspace is always kept in sync from agent state + WS events
38
+ console.log(session.files.listPaths());
39
+ console.log(session.files.read('README.md'));
40
+
41
+ session.close();
42
+ ```
43
+
44
+ ## Quickstart (Node)
45
+
46
+ Node requires a WebSocket factory (the browser `WebSocket` global is not available):
47
+
48
+ ```ts
49
+ import { PhasicClient } from '@cf-vibesdk/sdk';
50
+ import { createNodeWebSocketFactory } from '@cf-vibesdk/sdk/node';
51
+
52
+ const client = new PhasicClient({
53
+ baseUrl: 'http://localhost:5173',
54
+ apiKey: process.env.VIBESDK_API_KEY!,
55
+ webSocketFactory: createNodeWebSocketFactory(),
56
+ });
57
+
58
+ const session = await client.build('Build a simple hello world page.', {
59
+ projectType: 'app',
60
+ autoGenerate: true,
61
+ });
62
+
63
+ await session.wait.generationStarted();
64
+ await session.wait.deployable();
65
+ session.close();
66
+ ```
67
+
68
+ ## Authentication
69
+
70
+ Use either:
71
+
72
+ - `apiKey`: a VibeSDK API key (recommended for CLIs and automation)
73
+ - `token`: an already-minted JWT access token
74
+
75
+ When `apiKey` is provided, the SDK exchanges it for a short-lived access token and caches it.
76
+
77
+ ## Workspace (no platform file APIs)
78
+
79
+ The SDK reconstructs and maintains a local view of the codebase using:
80
+
81
+ - `agent_connected.state.generatedFilesMap`
82
+ - `cf_agent_state.state.generatedFilesMap`
83
+ - incremental `file_*` messages
84
+
85
+ APIs:
86
+
87
+ - `session.files.listPaths()`
88
+ - `session.files.read(path)`
89
+ - `session.files.snapshot()`
90
+ - `session.files.tree()`
91
+
92
+ ## Waiting primitives
93
+
94
+ Use high-level waits instead of depending on agent-internal message ordering:
95
+
96
+ - `session.wait.generationStarted()`
97
+ - `session.wait.generationComplete()`
98
+ - `session.wait.deployable()` (phasic resolves on `phase_validated`)
99
+ - `session.wait.previewDeployed()`
100
+ - `session.wait.cloudflareDeployed()`
101
+
102
+ All waits default to a long timeout (10 minutes). You can override per call:
103
+
104
+ ```ts
105
+ await session.wait.generationComplete({ timeoutMs: 2 * 60_000 });
106
+ ```
107
+
108
+ ## Reliable WebSocket connections
109
+
110
+ Connections automatically reconnect with exponential backoff + jitter.
111
+
112
+ Events:
113
+
114
+ - `session.on('ws:reconnecting', ({ attempt, delayMs, reason }) => { ... })`
115
+
116
+ To disable reconnect:
117
+
118
+ ```ts
119
+ session.connect({ retry: { enabled: false } });
120
+ ```
121
+
122
+ ## Low-level access
123
+
124
+ For advanced clients, you can subscribe to the raw typed WS stream:
125
+
126
+ - `session.on('ws:message', (msg) => { ... })`
127
+
128
+ The SDK also exposes `ws:raw` when the platform sends malformed/untyped payloads.
129
+
130
+ ## Tests
131
+
132
+ From `sdk/`:
133
+
134
+ - Unit: `bun run test`
135
+ - Integration (requires local platform + API key): `bun run test:integration`
136
+
137
+ Integration expects:
138
+
139
+ - `VIBESDK_INTEGRATION_API_KEY`
140
+ - optional `VIBESDK_INTEGRATION_BASE_URL` (default `http://localhost:5173`)
package/dist/index.d.ts CHANGED
@@ -398,7 +398,7 @@ declare const TemplateDetailsSchema: z.ZodObject<{
398
398
  renderMode?: "sandbox" | "browser" | undefined;
399
399
  slideDirectory?: string | undefined;
400
400
  }>;
401
- export type TemplateDetails = z.infer<typeof TemplateDetailsSchema>;
401
+ type TemplateDetails = z.infer<typeof TemplateDetailsSchema>;
402
402
  declare const PreviewSchema: z.ZodObject<{
403
403
  runId: z.ZodOptional<z.ZodString>;
404
404
  previewURL: z.ZodOptional<z.ZodString>;
@@ -676,6 +676,14 @@ declare const StaticAnalysisResponseSchema: z.ZodObject<{
676
676
  error?: string | undefined;
677
677
  }>;
678
678
  type StaticAnalysisResponse = z.infer<typeof StaticAnalysisResponseSchema>;
679
+ type ToolCall = {
680
+ id: string;
681
+ type: "function";
682
+ function: {
683
+ name: string;
684
+ arguments: string;
685
+ };
686
+ };
679
687
  type MessageRole = "system" | "user" | "assistant" | "function" | "tool";
680
688
  type TextContent = {
681
689
  type: "text";
@@ -689,14 +697,6 @@ type ImageContent = {
689
697
  };
690
698
  };
691
699
  type MessageContent = string | (TextContent | ImageContent)[] | null;
692
- type ToolCall = {
693
- id: string;
694
- type: "function";
695
- function: {
696
- name: string;
697
- arguments: string;
698
- };
699
- };
700
700
  type Message = {
701
701
  role: MessageRole;
702
702
  content: MessageContent;
@@ -1427,13 +1427,24 @@ export type AgentEventMap = {
1427
1427
  "ws:error": {
1428
1428
  error: unknown;
1429
1429
  };
1430
- /** Raw server->client message */
1430
+ "ws:reconnecting": {
1431
+ attempt: number;
1432
+ delayMs: number;
1433
+ reason: "close" | "error";
1434
+ };
1435
+ /** Server payload that isn't a well-formed typed message. */
1436
+ "ws:raw": {
1437
+ raw: unknown;
1438
+ };
1439
+ /** Raw server->client message (typed) */
1431
1440
  "ws:message": AgentWsServerMessage;
1432
1441
  connected: WsMessageOf<"agent_connected">;
1433
1442
  conversation: WsMessageOf<"conversation_response" | "conversation_state">;
1434
1443
  phase: WsMessageOf<"phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated">;
1435
1444
  file: WsMessageOf<"file_chunk_generated" | "file_generated" | "file_generating" | "file_regenerating" | "file_regenerated">;
1445
+ generation: WsMessageOf<"generation_started" | "generation_complete" | "generation_stopped" | "generation_resumed">;
1436
1446
  preview: WsMessageOf<"deployment_completed" | "deployment_started" | "deployment_failed">;
1447
+ cloudflare: WsMessageOf<"cloudflare_deployment_started" | "cloudflare_deployment_completed" | "cloudflare_deployment_error">;
1437
1448
  /** User-friendly error derived from `{ type: 'error' }` message */
1438
1449
  error: {
1439
1450
  error: string;
@@ -1454,6 +1465,16 @@ export type AgentConnectionOptions = {
1454
1465
  headers?: Record<string, string>;
1455
1466
  /** Optional WebSocket factory for Node/Bun runtimes. */
1456
1467
  webSocketFactory?: (url: string, protocols?: string | string[], headers?: Record<string, string>) => WebSocketLike;
1468
+ /**
1469
+ * Auto-reconnect config (enabled by default).
1470
+ * Set `{ enabled: false }` to disable.
1471
+ */
1472
+ retry?: {
1473
+ enabled?: boolean;
1474
+ initialDelayMs?: number;
1475
+ maxDelayMs?: number;
1476
+ maxRetries?: number;
1477
+ };
1457
1478
  };
1458
1479
  export type AgentConnection = {
1459
1480
  send: (msg: AgentWsClientMessage) => void;
@@ -1462,6 +1483,34 @@ export type AgentConnection = {
1462
1483
  onAny: (cb: (event: keyof AgentEventMap, payload: AgentEventMap[keyof AgentEventMap]) => void) => () => void;
1463
1484
  waitFor: <K extends keyof AgentEventMap>(event: K, predicate?: (payload: AgentEventMap[K]) => boolean, timeoutMs?: number) => Promise<AgentEventMap[K]>;
1464
1485
  };
1486
+ type FileTreeNode$1 = {
1487
+ type: "dir";
1488
+ name: string;
1489
+ path: string;
1490
+ children: FileTreeNode$1[];
1491
+ } | {
1492
+ type: "file";
1493
+ name: string;
1494
+ path: string;
1495
+ };
1496
+ export type SessionFiles = {
1497
+ listPaths: () => string[];
1498
+ read: (path: string) => string | null;
1499
+ snapshot: () => Record<string, string>;
1500
+ tree: () => FileTreeNode$1[];
1501
+ };
1502
+ export type WaitOptions = {
1503
+ timeoutMs?: number;
1504
+ };
1505
+ export type PhaseEventType = "phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated";
1506
+ export type WaitForPhaseOptions = WaitOptions & {
1507
+ type: PhaseEventType;
1508
+ };
1509
+ export type SessionDeployable = {
1510
+ files: number;
1511
+ reason: "generation_complete" | "phase_validated";
1512
+ previewUrl?: string;
1513
+ };
1465
1514
  export type VibeClientOptions = {
1466
1515
  baseUrl: string;
1467
1516
  /**
@@ -1477,8 +1526,101 @@ export type VibeClientOptions = {
1477
1526
  webSocketFactory?: AgentConnectionOptions["webSocketFactory"];
1478
1527
  fetchFn?: typeof fetch;
1479
1528
  };
1480
- type WaitUntilReadyOptions = {
1481
- timeoutMs?: number;
1529
+ type GenerationState = {
1530
+ status: "idle";
1531
+ } | {
1532
+ status: "running";
1533
+ totalFiles?: number;
1534
+ } | {
1535
+ status: "stopped";
1536
+ instanceId?: string;
1537
+ } | {
1538
+ status: "complete";
1539
+ instanceId?: string;
1540
+ previewURL?: string;
1541
+ };
1542
+ type PhaseState$1 = {
1543
+ status: "idle";
1544
+ } | {
1545
+ status: "generating" | "generated" | "implementing" | "implemented" | "validating" | "validated";
1546
+ name?: string;
1547
+ description?: string;
1548
+ };
1549
+ type PreviewDeploymentState = {
1550
+ status: "idle";
1551
+ } | {
1552
+ status: "running";
1553
+ } | {
1554
+ status: "failed";
1555
+ error: string;
1556
+ } | {
1557
+ status: "complete";
1558
+ previewURL: string;
1559
+ tunnelURL: string;
1560
+ instanceId: string;
1561
+ };
1562
+ type CloudflareDeploymentState = {
1563
+ status: "idle";
1564
+ } | {
1565
+ status: "running";
1566
+ instanceId?: string;
1567
+ } | {
1568
+ status: "failed";
1569
+ error: string;
1570
+ instanceId?: string;
1571
+ } | {
1572
+ status: "complete";
1573
+ deploymentUrl: string;
1574
+ instanceId: string;
1575
+ workersUrl?: string;
1576
+ };
1577
+ type ConversationState$1 = WsMessageOf<"conversation_state">["state"];
1578
+ export type SessionState = {
1579
+ conversationState?: ConversationState$1;
1580
+ lastConversationResponse?: WsMessageOf<"conversation_response">;
1581
+ generation: GenerationState;
1582
+ phase: PhaseState$1;
1583
+ /** Best-known preview url (from agent_connected, generation_complete, deployment_completed). */
1584
+ previewUrl?: string;
1585
+ preview: PreviewDeploymentState;
1586
+ cloudflare: CloudflareDeploymentState;
1587
+ lastError?: string;
1588
+ };
1589
+ export declare class SessionStateStore {
1590
+ private state;
1591
+ private emitter;
1592
+ get(): SessionState;
1593
+ onChange(cb: (next: SessionState, prev: SessionState) => void): () => void;
1594
+ applyWsMessage(msg: AgentWsServerMessage): void;
1595
+ private setState;
1596
+ }
1597
+ type WorkspaceChange = {
1598
+ type: "reset";
1599
+ files: number;
1600
+ } | {
1601
+ type: "upsert";
1602
+ path: string;
1603
+ } | {
1604
+ type: "delete";
1605
+ path: string;
1606
+ };
1607
+ export declare class WorkspaceStore {
1608
+ private files;
1609
+ private emitter;
1610
+ paths(): string[];
1611
+ read(path: string): string | null;
1612
+ snapshot(): Record<string, string>;
1613
+ onChange(cb: (change: WorkspaceChange) => void): () => void;
1614
+ /** Apply authoritative snapshot from an AgentState. */
1615
+ applyStateSnapshot(state: AgentState): void;
1616
+ /** Apply a single file upsert from WS file events. */
1617
+ applyFileUpsert(file: unknown): void;
1618
+ applyWsMessage(msg: AgentWsServerMessage): void;
1619
+ }
1620
+ type WaitUntilReadyOptions = WaitOptions;
1621
+ type BuildSessionConnectOptions = AgentConnectionOptions & {
1622
+ /** If true (default), send `get_conversation_state` on socket open. */
1623
+ autoRequestConversationState?: boolean;
1482
1624
  };
1483
1625
  type BuildSessionInit = {
1484
1626
  getAuthToken?: () => string | undefined;
@@ -1492,9 +1634,28 @@ export declare class BuildSession {
1492
1634
  readonly behaviorType: BehaviorType$1 | undefined;
1493
1635
  readonly projectType: ProjectType$1 | string | undefined;
1494
1636
  private connection;
1637
+ readonly workspace: WorkspaceStore;
1638
+ readonly state: SessionStateStore;
1639
+ readonly files: SessionFiles;
1640
+ readonly wait: {
1641
+ generationStarted: (options?: WaitOptions) => Promise<{
1642
+ type: "generation_started";
1643
+ message: string;
1644
+ totalFiles: number;
1645
+ }>;
1646
+ generationComplete: (options?: WaitOptions) => Promise<{
1647
+ type: "generation_complete";
1648
+ instanceId?: string;
1649
+ previewURL?: string;
1650
+ }>;
1651
+ phase: (options: WaitForPhaseOptions) => Promise<WsMessageOf<PhaseEventType>>;
1652
+ deployable: (options?: WaitOptions) => Promise<SessionDeployable>;
1653
+ previewDeployed: (options?: WaitOptions) => Promise<DeploymentCompletedMessage>;
1654
+ cloudflareDeployed: (options?: WaitOptions) => Promise<CloudflareDeploymentCompletedMessage>;
1655
+ };
1495
1656
  constructor(clientOptions: VibeClientOptions, start: BuildStartEvent, init?: BuildSessionInit);
1496
1657
  isConnected(): boolean;
1497
- connect(options?: AgentConnectionOptions): AgentConnection;
1658
+ connect(options?: BuildSessionConnectOptions): AgentConnection;
1498
1659
  startGeneration(): void;
1499
1660
  stop(): void;
1500
1661
  followUp(message: string, options?: {
@@ -1503,6 +1664,19 @@ export declare class BuildSession {
1503
1664
  requestConversationState(): void;
1504
1665
  deployPreview(): void;
1505
1666
  deployCloudflare(): void;
1667
+ resume(): void;
1668
+ clearConversation(): void;
1669
+ private getDefaultTimeoutMs;
1670
+ private waitForWsMessage;
1671
+ waitForGenerationStarted(options?: WaitOptions): Promise<WsMessageOf<"generation_started">>;
1672
+ waitForGenerationComplete(options?: WaitOptions): Promise<WsMessageOf<"generation_complete">>;
1673
+ waitForPhase(options: WaitForPhaseOptions): Promise<WsMessageOf<PhaseEventType>>;
1674
+ waitForDeployable(options?: WaitOptions): Promise<SessionDeployable>;
1675
+ waitForPreviewDeployed(options?: WaitOptions): Promise<WsMessageOf<"deployment_completed">>;
1676
+ waitForCloudflareDeployed(options?: WaitOptions): Promise<WsMessageOf<"cloudflare_deployment_completed">>;
1677
+ /**
1678
+ * Legacy alias. Prefer `session.wait.generationStarted()`.
1679
+ */
1506
1680
  waitUntilReady(options?: WaitUntilReadyOptions): Promise<void>;
1507
1681
  on: AgentConnection["on"];
1508
1682
  onAny: AgentConnection["onAny"];
@@ -1556,6 +1730,7 @@ export declare class PhasicClient extends VibeClient {
1556
1730
  export {
1557
1731
  BehaviorType$1 as BehaviorType,
1558
1732
  CodeGenArgs$1 as CodeGenArgs,
1733
+ FileTreeNode$1 as FileTreeNode,
1559
1734
  ProjectType$1 as ProjectType,
1560
1735
  };
1561
1736
 
package/dist/index.js CHANGED
@@ -35,6 +35,9 @@ class HttpClient {
35
35
  });
36
36
  if (!resp.ok) {
37
37
  const text = await resp.text().catch(() => "");
38
+ if (resp.status === 401) {
39
+ throw new Error(`HTTP 401 for /api/auth/exchange-api-key: invalid API key (regenerate in Settings \u2192 API Keys). ${text || ""}`.trim());
40
+ }
38
41
  throw new Error(`HTTP ${resp.status} for /api/auth/exchange-api-key: ${text || resp.statusText}`);
39
42
  }
40
43
  const parsed = await resp.json();
@@ -136,6 +139,168 @@ class TypedEmitter {
136
139
  }
137
140
  }
138
141
 
142
+ // src/state.ts
143
+ var INITIAL_STATE = {
144
+ generation: { status: "idle" },
145
+ phase: { status: "idle" },
146
+ preview: { status: "idle" },
147
+ cloudflare: { status: "idle" }
148
+ };
149
+ function extractPhaseInfo(msg) {
150
+ return {
151
+ name: msg.phase?.name,
152
+ description: msg.phase?.description
153
+ };
154
+ }
155
+
156
+ class SessionStateStore {
157
+ state = INITIAL_STATE;
158
+ emitter = new TypedEmitter;
159
+ get() {
160
+ return this.state;
161
+ }
162
+ onChange(cb) {
163
+ return this.emitter.on("change", ({ prev, next }) => cb(next, prev));
164
+ }
165
+ applyWsMessage(msg) {
166
+ switch (msg.type) {
167
+ case "conversation_state": {
168
+ const m = msg;
169
+ this.setState({ conversationState: m.state });
170
+ break;
171
+ }
172
+ case "conversation_response": {
173
+ const m = msg;
174
+ this.setState({ lastConversationResponse: m });
175
+ break;
176
+ }
177
+ case "generation_started": {
178
+ const m = msg;
179
+ this.setState({ generation: { status: "running", totalFiles: m.totalFiles } });
180
+ break;
181
+ }
182
+ case "generation_complete": {
183
+ const m = msg;
184
+ const previewURL = m.previewURL;
185
+ this.setState({
186
+ generation: {
187
+ status: "complete",
188
+ instanceId: m.instanceId,
189
+ previewURL
190
+ },
191
+ ...previewURL ? { previewUrl: previewURL } : {}
192
+ });
193
+ break;
194
+ }
195
+ case "generation_stopped": {
196
+ const m = msg;
197
+ this.setState({ generation: { status: "stopped", instanceId: m.instanceId } });
198
+ break;
199
+ }
200
+ case "generation_resumed": {
201
+ this.setState({ generation: { status: "running" } });
202
+ break;
203
+ }
204
+ case "phase_generating": {
205
+ const m = msg;
206
+ this.setState({ phase: { status: "generating", ...extractPhaseInfo(m) } });
207
+ break;
208
+ }
209
+ case "phase_generated": {
210
+ const m = msg;
211
+ this.setState({ phase: { status: "generated", ...extractPhaseInfo(m) } });
212
+ break;
213
+ }
214
+ case "phase_implementing": {
215
+ const m = msg;
216
+ this.setState({ phase: { status: "implementing", ...extractPhaseInfo(m) } });
217
+ break;
218
+ }
219
+ case "phase_implemented": {
220
+ const m = msg;
221
+ this.setState({ phase: { status: "implemented", ...extractPhaseInfo(m) } });
222
+ break;
223
+ }
224
+ case "phase_validating": {
225
+ const m = msg;
226
+ this.setState({ phase: { status: "validating", ...extractPhaseInfo(m) } });
227
+ break;
228
+ }
229
+ case "phase_validated": {
230
+ const m = msg;
231
+ this.setState({ phase: { status: "validated", ...extractPhaseInfo(m) } });
232
+ break;
233
+ }
234
+ case "deployment_started": {
235
+ this.setState({ preview: { status: "running" } });
236
+ break;
237
+ }
238
+ case "deployment_failed": {
239
+ const m = msg;
240
+ this.setState({ preview: { status: "failed", error: m.error } });
241
+ break;
242
+ }
243
+ case "deployment_completed": {
244
+ const m = msg;
245
+ this.setState({
246
+ previewUrl: m.previewURL,
247
+ preview: {
248
+ status: "complete",
249
+ previewURL: m.previewURL,
250
+ tunnelURL: m.tunnelURL,
251
+ instanceId: m.instanceId
252
+ }
253
+ });
254
+ break;
255
+ }
256
+ case "cloudflare_deployment_started": {
257
+ const m = msg;
258
+ this.setState({ cloudflare: { status: "running", instanceId: m.instanceId } });
259
+ break;
260
+ }
261
+ case "cloudflare_deployment_error": {
262
+ const m = msg;
263
+ this.setState({
264
+ cloudflare: { status: "failed", error: m.error, instanceId: m.instanceId }
265
+ });
266
+ break;
267
+ }
268
+ case "cloudflare_deployment_completed": {
269
+ const m = msg;
270
+ this.setState({
271
+ cloudflare: {
272
+ status: "complete",
273
+ deploymentUrl: m.deploymentUrl,
274
+ workersUrl: m.workersUrl,
275
+ instanceId: m.instanceId
276
+ }
277
+ });
278
+ break;
279
+ }
280
+ case "agent_connected": {
281
+ const m = msg;
282
+ const previewUrl = m.previewUrl;
283
+ if (previewUrl)
284
+ this.setState({ previewUrl });
285
+ break;
286
+ }
287
+ case "error": {
288
+ const m = msg;
289
+ this.setState({ lastError: m.error });
290
+ break;
291
+ }
292
+ default:
293
+ break;
294
+ }
295
+ }
296
+ setState(patch) {
297
+ const prev = this.state;
298
+ const next = { ...prev, ...patch };
299
+ this.state = next;
300
+ this.emitter.emit("change", { prev, next });
301
+ }
302
+ }
303
+
139
304
  // src/ws.ts
140
305
  function toWsCloseEvent(ev) {
141
306
  return {
@@ -143,39 +308,129 @@ function toWsCloseEvent(ev) {
143
308
  reason: typeof ev.reason === "string" ? ev.reason : ""
144
309
  };
145
310
  }
311
+ function normalizeRetryConfig(retry) {
312
+ const enabled = retry?.enabled ?? true;
313
+ return {
314
+ enabled,
315
+ initialDelayMs: retry?.initialDelayMs ?? 1000,
316
+ maxDelayMs: retry?.maxDelayMs ?? 30000,
317
+ maxRetries: retry?.maxRetries ?? Infinity
318
+ };
319
+ }
320
+ function computeBackoffMs(attempt, cfg) {
321
+ const base = Math.min(cfg.maxDelayMs, cfg.initialDelayMs * Math.pow(2, Math.max(0, attempt)));
322
+ const jitter = base * 0.2;
323
+ return Math.max(0, Math.floor(base - jitter + Math.random() * jitter * 2));
324
+ }
146
325
  function createAgentConnection(url, options = {}) {
147
326
  const emitter = new TypedEmitter;
327
+ const retryCfg = normalizeRetryConfig(options.retry);
148
328
  const headers = { ...options.headers ?? {} };
149
329
  if (options.origin)
150
330
  headers.Origin = options.origin;
151
- const ws = options.webSocketFactory ? options.webSocketFactory(url, undefined, headers) : new WebSocket(url);
152
- if (ws.addEventListener) {
153
- ws.addEventListener("open", () => {
154
- emitter.emit("ws:open", undefined);
155
- });
156
- ws.addEventListener("close", (ev) => {
157
- emitter.emit("ws:close", toWsCloseEvent(ev));
158
- });
159
- ws.addEventListener("error", (ev) => {
160
- emitter.emit("ws:error", { error: ev });
161
- });
162
- ws.addEventListener("message", (ev) => {
163
- handleMessage(ev.data);
164
- });
165
- } else if (ws.on) {
166
- ws.on("open", () => emitter.emit("ws:open", undefined));
167
- ws.on("close", (code, reason) => {
168
- emitter.emit("ws:close", {
169
- code: typeof code === "number" ? code : 1000,
170
- reason: typeof reason === "string" ? reason : ""
171
- });
331
+ let ws = null;
332
+ let isOpen = false;
333
+ let closedByUser = false;
334
+ let reconnectAttempts = 0;
335
+ let reconnectTimer = null;
336
+ const pendingSends = [];
337
+ const maxPendingSends = 1000;
338
+ function clearReconnectTimer() {
339
+ if (!reconnectTimer)
340
+ return;
341
+ clearTimeout(reconnectTimer);
342
+ reconnectTimer = null;
343
+ }
344
+ function makeWebSocket() {
345
+ if (options.webSocketFactory)
346
+ return options.webSocketFactory(url, undefined, headers);
347
+ return new WebSocket(url);
348
+ }
349
+ function flushPendingSends() {
350
+ if (!ws || !isOpen)
351
+ return;
352
+ for (const data of pendingSends)
353
+ ws.send(data);
354
+ pendingSends.length = 0;
355
+ }
356
+ function scheduleReconnect(reason) {
357
+ if (closedByUser)
358
+ return;
359
+ if (!retryCfg.enabled)
360
+ return;
361
+ if (reconnectAttempts >= retryCfg.maxRetries)
362
+ return;
363
+ if (reconnectTimer)
364
+ return;
365
+ const delayMs = computeBackoffMs(reconnectAttempts, retryCfg);
366
+ emitter.emit("ws:reconnecting", {
367
+ attempt: reconnectAttempts + 1,
368
+ delayMs,
369
+ reason
172
370
  });
173
- ws.on("error", (error) => emitter.emit("ws:error", { error }));
174
- ws.on("message", (data) => handleMessage(data));
371
+ reconnectAttempts += 1;
372
+ reconnectTimer = setTimeout(() => {
373
+ reconnectTimer = null;
374
+ connectNow();
375
+ }, delayMs);
376
+ }
377
+ function onOpen() {
378
+ isOpen = true;
379
+ reconnectAttempts = 0;
380
+ emitter.emit("ws:open", undefined);
381
+ flushPendingSends();
382
+ }
383
+ function onClose(ev) {
384
+ isOpen = false;
385
+ emitter.emit("ws:close", toWsCloseEvent(ev));
386
+ scheduleReconnect("close");
387
+ }
388
+ function onError(error) {
389
+ emitter.emit("ws:error", { error });
390
+ scheduleReconnect("error");
391
+ }
392
+ function looksLikeAgentState(obj) {
393
+ if (!obj || typeof obj !== "object")
394
+ return false;
395
+ const behaviorType = obj.behaviorType;
396
+ const projectType = obj.projectType;
397
+ return typeof behaviorType === "string" && typeof projectType === "string";
398
+ }
399
+ function normalizeServerPayload(raw) {
400
+ if (!raw || typeof raw !== "object")
401
+ return null;
402
+ const t = raw.type;
403
+ if (typeof t === "string") {
404
+ const trimmed = t.trim();
405
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
406
+ try {
407
+ const inner = JSON.parse(trimmed);
408
+ const normalizedInner = normalizeServerPayload(inner);
409
+ if (normalizedInner)
410
+ return normalizedInner;
411
+ emitter.emit("ws:raw", { raw: inner });
412
+ return null;
413
+ } catch {}
414
+ }
415
+ return raw;
416
+ }
417
+ const state = raw.state;
418
+ if (looksLikeAgentState(state)) {
419
+ return { type: "cf_agent_state", state };
420
+ }
421
+ if (looksLikeAgentState(raw)) {
422
+ return { type: "cf_agent_state", state: raw };
423
+ }
424
+ return null;
175
425
  }
176
- function handleMessage(data) {
426
+ function onMessage(data) {
177
427
  try {
178
- const parsed = JSON.parse(String(data));
428
+ const raw = JSON.parse(String(data));
429
+ const parsed = normalizeServerPayload(raw);
430
+ if (!parsed) {
431
+ emitter.emit("ws:raw", { raw });
432
+ return;
433
+ }
179
434
  emitter.emit("ws:message", parsed);
180
435
  switch (parsed.type) {
181
436
  case "agent_connected":
@@ -200,11 +455,22 @@ function createAgentConnection(url, options = {}) {
200
455
  case "file_regenerated":
201
456
  emitter.emit("file", parsed);
202
457
  break;
458
+ case "generation_started":
459
+ case "generation_complete":
460
+ case "generation_stopped":
461
+ case "generation_resumed":
462
+ emitter.emit("generation", parsed);
463
+ break;
203
464
  case "deployment_completed":
204
465
  case "deployment_started":
205
466
  case "deployment_failed":
206
467
  emitter.emit("preview", parsed);
207
468
  break;
469
+ case "cloudflare_deployment_started":
470
+ case "cloudflare_deployment_completed":
471
+ case "cloudflare_deployment_error":
472
+ emitter.emit("cloudflare", parsed);
473
+ break;
208
474
  case "error":
209
475
  emitter.emit("error", { error: String(parsed.error ?? "Unknown error") });
210
476
  break;
@@ -212,14 +478,52 @@ function createAgentConnection(url, options = {}) {
212
478
  break;
213
479
  }
214
480
  } catch (error) {
215
- emitter.emit("ws:error", { error });
481
+ onError(error);
216
482
  }
217
483
  }
484
+ function connectNow() {
485
+ if (closedByUser)
486
+ return;
487
+ clearReconnectTimer();
488
+ try {
489
+ ws = makeWebSocket();
490
+ } catch (error) {
491
+ onError(error);
492
+ scheduleReconnect("error");
493
+ return;
494
+ }
495
+ if (ws.addEventListener) {
496
+ ws.addEventListener("open", () => onOpen());
497
+ ws.addEventListener("close", (ev) => onClose(ev));
498
+ ws.addEventListener("error", (ev) => onError(ev));
499
+ ws.addEventListener("message", (ev) => onMessage(ev.data));
500
+ return;
501
+ }
502
+ if (ws.on) {
503
+ ws.on("open", () => onOpen());
504
+ ws.on("close", (code, reason) => onClose({ code, reason }));
505
+ ws.on("error", (error) => onError(error));
506
+ ws.on("message", (data) => onMessage(data));
507
+ }
508
+ }
509
+ connectNow();
218
510
  function send(msg) {
219
- ws.send(JSON.stringify(msg));
511
+ const data = JSON.stringify(msg);
512
+ if (isOpen && ws) {
513
+ ws.send(data);
514
+ return;
515
+ }
516
+ pendingSends.push(data);
517
+ if (pendingSends.length > maxPendingSends)
518
+ pendingSends.shift();
220
519
  }
221
520
  function close() {
222
- ws.close();
521
+ closedByUser = true;
522
+ isOpen = false;
523
+ pendingSends.length = 0;
524
+ clearReconnectTimer();
525
+ ws?.close();
526
+ ws = null;
223
527
  }
224
528
  async function waitFor(event, predicate, timeoutMs = 60000) {
225
529
  return new Promise((resolve, reject) => {
@@ -245,7 +549,118 @@ function createAgentConnection(url, options = {}) {
245
549
  };
246
550
  }
247
551
 
552
+ // src/workspace.ts
553
+ function isRecord(value) {
554
+ return typeof value === "object" && value !== null;
555
+ }
556
+ function extractGeneratedFilesFromState(state) {
557
+ const out = [];
558
+ for (const file of Object.values(state.generatedFilesMap ?? {})) {
559
+ const path = file.filePath;
560
+ const content = file.fileContents;
561
+ if (typeof path === "string" && typeof content === "string") {
562
+ out.push({ path, content });
563
+ }
564
+ }
565
+ return out;
566
+ }
567
+ function extractGeneratedFileFromMessageFile(file) {
568
+ if (!isRecord(file))
569
+ return null;
570
+ const path = file.filePath;
571
+ const content = file.fileContents;
572
+ if (typeof path !== "string" || typeof content !== "string")
573
+ return null;
574
+ return { path, content };
575
+ }
576
+
577
+ class WorkspaceStore {
578
+ files = new Map;
579
+ emitter = new TypedEmitter;
580
+ paths() {
581
+ return Array.from(this.files.keys()).sort();
582
+ }
583
+ read(path) {
584
+ return this.files.get(path) ?? null;
585
+ }
586
+ snapshot() {
587
+ const out = {};
588
+ for (const [path, content] of this.files.entries())
589
+ out[path] = content;
590
+ return out;
591
+ }
592
+ onChange(cb) {
593
+ return this.emitter.on("change", cb);
594
+ }
595
+ applyStateSnapshot(state) {
596
+ this.files.clear();
597
+ for (const f of extractGeneratedFilesFromState(state)) {
598
+ this.files.set(f.path, f.content);
599
+ }
600
+ this.emitter.emit("change", { type: "reset", files: this.files.size });
601
+ }
602
+ applyFileUpsert(file) {
603
+ const f = extractGeneratedFileFromMessageFile(file);
604
+ if (!f)
605
+ return;
606
+ this.files.set(f.path, f.content);
607
+ this.emitter.emit("change", { type: "upsert", path: f.path });
608
+ }
609
+ applyWsMessage(msg) {
610
+ switch (msg.type) {
611
+ case "agent_connected":
612
+ this.applyStateSnapshot(msg.state);
613
+ break;
614
+ case "cf_agent_state":
615
+ this.applyStateSnapshot(msg.state);
616
+ break;
617
+ case "file_generated":
618
+ this.applyFileUpsert(msg.file);
619
+ break;
620
+ case "file_regenerated":
621
+ this.applyFileUpsert(msg.file);
622
+ break;
623
+ default:
624
+ break;
625
+ }
626
+ }
627
+ }
628
+
248
629
  // src/session.ts
630
+ function buildFileTree(paths) {
631
+ const root = { name: "", path: "", dirs: new Map, files: [] };
632
+ for (const p of paths) {
633
+ const parts = p.split("/").filter(Boolean);
634
+ let curr = root;
635
+ for (let i = 0;i < parts.length; i += 1) {
636
+ const part = parts[i];
637
+ const isLast = i === parts.length - 1;
638
+ if (isLast) {
639
+ curr.files.push({ type: "file", name: part, path: p });
640
+ continue;
641
+ }
642
+ const nextPath = curr.path ? `${curr.path}/${part}` : part;
643
+ let next = curr.dirs.get(part);
644
+ if (!next) {
645
+ next = { name: part, path: nextPath, dirs: new Map, files: [] };
646
+ curr.dirs.set(part, next);
647
+ }
648
+ curr = next;
649
+ }
650
+ }
651
+ function toNodes(dir) {
652
+ const dirs = Array.from(dir.dirs.values()).sort((a, b) => a.name.localeCompare(b.name)).map((d) => ({
653
+ type: "dir",
654
+ name: d.name,
655
+ path: d.path,
656
+ children: toNodes(d)
657
+ }));
658
+ const files = dir.files.sort((a, b) => a.name.localeCompare(b.name));
659
+ return [...dirs, ...files];
660
+ }
661
+ return toNodes(root);
662
+ }
663
+
249
664
  class BuildSession {
250
665
  clientOptions;
251
666
  init;
@@ -254,6 +669,22 @@ class BuildSession {
254
669
  behaviorType;
255
670
  projectType;
256
671
  connection = null;
672
+ workspace = new WorkspaceStore;
673
+ state = new SessionStateStore;
674
+ files = {
675
+ listPaths: () => this.workspace.paths(),
676
+ read: (path) => this.workspace.read(path),
677
+ snapshot: () => this.workspace.snapshot(),
678
+ tree: () => buildFileTree(this.workspace.paths())
679
+ };
680
+ wait = {
681
+ generationStarted: (options = {}) => this.waitForGenerationStarted(options),
682
+ generationComplete: (options = {}) => this.waitForGenerationComplete(options),
683
+ phase: (options) => this.waitForPhase(options),
684
+ deployable: (options = {}) => this.waitForDeployable(options),
685
+ previewDeployed: (options = {}) => this.waitForPreviewDeployed(options),
686
+ cloudflareDeployed: (options = {}) => this.waitForCloudflareDeployed(options)
687
+ };
257
688
  constructor(clientOptions, start, init = {}) {
258
689
  this.clientOptions = clientOptions;
259
690
  this.init = init;
@@ -268,29 +699,38 @@ class BuildSession {
268
699
  connect(options = {}) {
269
700
  if (this.connection)
270
701
  return this.connection;
271
- const origin = options.origin ?? this.clientOptions.websocketOrigin;
272
- const webSocketFactory = options.webSocketFactory ?? this.clientOptions.webSocketFactory;
273
- const headers = { ...options.headers ?? {} };
702
+ const { autoRequestConversationState, ...agentOptions } = options;
703
+ const origin = agentOptions.origin ?? this.clientOptions.websocketOrigin;
704
+ const webSocketFactory = agentOptions.webSocketFactory ?? this.clientOptions.webSocketFactory;
705
+ const headers = { ...agentOptions.headers ?? {} };
274
706
  const token = this.init.getAuthToken?.();
275
707
  if (token && !headers.Authorization) {
276
708
  headers.Authorization = `Bearer ${token}`;
277
709
  }
278
710
  const connectOptions = {
279
- ...options,
711
+ ...agentOptions,
280
712
  ...origin ? { origin } : {},
281
713
  ...Object.keys(headers).length ? { headers } : {},
282
714
  ...webSocketFactory ? { webSocketFactory } : {}
283
715
  };
284
716
  this.connection = createAgentConnection(this.websocketUrl, connectOptions);
285
- const credentials = options.credentials ?? this.init.defaultCredentials;
286
- if (credentials) {
287
- this.connection.on("ws:open", () => {
717
+ this.connection.on("ws:message", (m) => {
718
+ this.workspace.applyWsMessage(m);
719
+ this.state.applyWsMessage(m);
720
+ });
721
+ const credentials = agentOptions.credentials ?? this.init.defaultCredentials;
722
+ const shouldRequestConversationState = autoRequestConversationState ?? true;
723
+ this.connection.on("ws:open", () => {
724
+ if (credentials) {
288
725
  this.connection?.send({
289
726
  type: "session_init",
290
727
  credentials
291
728
  });
292
- });
293
- }
729
+ }
730
+ if (shouldRequestConversationState) {
731
+ this.connection?.send({ type: "get_conversation_state" });
732
+ }
733
+ });
294
734
  return this.connection;
295
735
  }
296
736
  startGeneration() {
@@ -321,15 +761,65 @@ class BuildSession {
321
761
  this.assertConnected();
322
762
  this.connection.send({ type: "deploy" });
323
763
  }
324
- async waitUntilReady(options = {}) {
764
+ resume() {
325
765
  this.assertConnected();
326
- const timeoutMs = options.timeoutMs ?? 120000;
327
- const behavior = this.behaviorType;
328
- if (behavior === "phasic") {
329
- await this.connection.waitFor("phase", (payload) => payload.type === "phase_implementing", timeoutMs);
330
- return;
766
+ this.connection.send({ type: "resume_generation" });
767
+ }
768
+ clearConversation() {
769
+ this.assertConnected();
770
+ this.connection.send({ type: "clear_conversation" });
771
+ }
772
+ getDefaultTimeoutMs() {
773
+ return 10 * 60000;
774
+ }
775
+ async waitForWsMessage(predicate, timeoutMs) {
776
+ this.assertConnected();
777
+ return await this.connection.waitFor("ws:message", predicate, timeoutMs);
778
+ }
779
+ async waitForGenerationStarted(options = {}) {
780
+ return await this.waitForMessageType("generation_started", options.timeoutMs ?? this.getDefaultTimeoutMs());
781
+ }
782
+ async waitForGenerationComplete(options = {}) {
783
+ return await this.waitForMessageType("generation_complete", options.timeoutMs ?? this.getDefaultTimeoutMs());
784
+ }
785
+ async waitForPhase(options) {
786
+ return await this.waitForMessageType(options.type, options.timeoutMs ?? this.getDefaultTimeoutMs());
787
+ }
788
+ async waitForDeployable(options = {}) {
789
+ const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
790
+ if (this.behaviorType === "phasic") {
791
+ await this.waitForPhase({ type: "phase_validated", timeoutMs });
792
+ return {
793
+ files: this.workspace.paths().length,
794
+ reason: "phase_validated",
795
+ previewUrl: this.state.get().previewUrl
796
+ };
797
+ }
798
+ await this.waitForGenerationComplete({ timeoutMs });
799
+ return {
800
+ files: this.workspace.paths().length,
801
+ reason: "generation_complete",
802
+ previewUrl: this.state.get().previewUrl
803
+ };
804
+ }
805
+ async waitForPreviewDeployed(options = {}) {
806
+ const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
807
+ const msg = await this.waitForWsMessage((m) => m.type === "deployment_completed" || m.type === "deployment_failed", timeoutMs);
808
+ if (msg.type === "deployment_failed") {
809
+ throw new Error(msg.error);
331
810
  }
332
- await this.connection.waitFor("conversation", (payload) => payload.type === "conversation_response", timeoutMs);
811
+ return msg;
812
+ }
813
+ async waitForCloudflareDeployed(options = {}) {
814
+ const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
815
+ const msg = await this.waitForWsMessage((m) => m.type === "cloudflare_deployment_completed" || m.type === "cloudflare_deployment_error", timeoutMs);
816
+ if (msg.type === "cloudflare_deployment_error") {
817
+ throw new Error(msg.error);
818
+ }
819
+ return msg;
820
+ }
821
+ async waitUntilReady(options = {}) {
822
+ await this.waitForGenerationStarted(options);
333
823
  }
334
824
  on = (event, cb) => {
335
825
  this.assertConnected();
@@ -348,7 +838,7 @@ class BuildSession {
348
838
  }
349
839
  async waitForMessageType(type, timeoutMs) {
350
840
  this.assertConnected();
351
- return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs);
841
+ return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs ?? this.getDefaultTimeoutMs());
352
842
  }
353
843
  close() {
354
844
  this.connection?.close();
@@ -494,7 +984,9 @@ class PhasicClient extends VibeClient {
494
984
  }
495
985
  }
496
986
  export {
987
+ WorkspaceStore,
497
988
  VibeClient,
989
+ SessionStateStore,
498
990
  PhasicClient,
499
991
  BuildSession,
500
992
  AgenticClient
package/dist/node.d.ts CHANGED
@@ -61,6 +61,16 @@ type AgentConnectionOptions = {
61
61
  headers?: Record<string, string>;
62
62
  /** Optional WebSocket factory for Node/Bun runtimes. */
63
63
  webSocketFactory?: (url: string, protocols?: string | string[], headers?: Record<string, string>) => WebSocketLike;
64
+ /**
65
+ * Auto-reconnect config (enabled by default).
66
+ * Set `{ enabled: false }` to disable.
67
+ */
68
+ retry?: {
69
+ enabled?: boolean;
70
+ initialDelayMs?: number;
71
+ maxDelayMs?: number;
72
+ maxRetries?: number;
73
+ };
64
74
  };
65
75
  export declare function createNodeWebSocketFactory(): NonNullable<AgentConnectionOptions["webSocketFactory"]>;
66
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cf-vibesdk/sdk",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -22,8 +22,8 @@
22
22
  "build": "rm -rf ./dist && bun build ./src/index.ts ./src/node.ts --outdir ./dist --target bun",
23
23
  "bundle-types": "dts-bundle-generator --export-referenced-types false --project ./tsconfig.protocol.json -o ./dist/index.d.ts ./src/index.ts && dts-bundle-generator --export-referenced-types false --project ./tsconfig.protocol.json -o ./dist/node.d.ts ./src/node.ts",
24
24
  "typecheck": "tsc -p ./tsconfig.json --noEmit",
25
- "test": "bun test",
26
- "example:cli": "bun ./src/example/cli.ts"
25
+ "test": "bun test test/*.test.ts",
26
+ "test:integration": "bun test --timeout 600000 test/integration/*.test.ts"
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "^8.18.3"
@@ -35,3 +35,4 @@
35
35
  }
36
36
  }
37
37
 
38
+