@cf-vibesdk/sdk 0.0.8 → 0.1.0

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 CHANGED
@@ -94,6 +94,53 @@ const session = await client.build('Build a weather dashboard', {
94
94
  });
95
95
  ```
96
96
 
97
+ #### Blueprint Streaming
98
+
99
+ The server streams blueprint chunks as the AI generates the project plan. By default, `build()` waits for all chunks before returning:
100
+
101
+ ```ts
102
+ // Default behavior: waits for blueprint to complete
103
+ const session = await client.build('Build a todo app', {
104
+ onBlueprintChunk: (chunk) => {
105
+ // Called in real-time as chunks arrive
106
+ blueprintText += chunk;
107
+ },
108
+ });
109
+ // Session returned after all blueprint chunks received
110
+ ```
111
+
112
+ For faster startup, set `waitForBlueprint: false` to return immediately and stream chunks in the background:
113
+
114
+ ```ts
115
+ const session = await client.build('Build a todo app', {
116
+ waitForBlueprint: false, // Return immediately after start event
117
+ onBlueprintChunk: (chunk) => {
118
+ // Still called in real-time as chunks arrive
119
+ blueprintText += chunk;
120
+ },
121
+ onBlueprintError: (error) => {
122
+ // Called if streaming fails (session is auto-closed)
123
+ console.error('Blueprint streaming failed:', error);
124
+ },
125
+ });
126
+ // Session returned immediately, blueprint streams in background
127
+ ```
128
+
129
+ Use the `BlueprintStreamParser` utility to convert chunks to readable Markdown:
130
+
131
+ ```ts
132
+ import { BlueprintStreamParser } from '@cf-vibesdk/sdk';
133
+
134
+ const parser = new BlueprintStreamParser();
135
+ const session = await client.build('Build a todo app', {
136
+ waitForBlueprint: false,
137
+ onBlueprintChunk: (chunk) => {
138
+ const markdown = parser.append(chunk);
139
+ console.log(markdown); // Rendered as Markdown
140
+ },
141
+ });
142
+ ```
143
+
97
144
  ### `client.connect(agentId)`
98
145
 
99
146
  Connect to an existing app session. State is automatically restored from the agent, including:
@@ -250,6 +297,24 @@ if (session.phases.allCompleted()) {
250
297
 
251
298
  // Get phase by ID
252
299
  const phase = session.phases.get('phase-0');
300
+
301
+ // Subscribe to phase changes
302
+ const unsubscribe = session.phases.onChange((event) => {
303
+ console.log(`Phase ${event.type}:`, event.phase.name);
304
+ console.log(`Status: ${event.phase.status}`);
305
+ console.log(`Total phases: ${event.allPhases.length}`);
306
+ });
307
+ // Later: unsubscribe();
308
+ ```
309
+
310
+ The `onChange` callback receives a `PhaseTimelineEvent`:
311
+
312
+ ```ts
313
+ type PhaseTimelineEvent = {
314
+ type: 'added' | 'updated'; // New phase vs status/file change
315
+ phase: PhaseInfo; // The affected phase
316
+ allPhases: PhaseInfo[]; // All phases after this change
317
+ };
253
318
  ```
254
319
 
255
320
  Each phase contains:
@@ -304,14 +369,14 @@ session.workspace.onChange((change) => {
304
369
 
305
370
  ## WebSocket Reliability
306
371
 
307
- Connections automatically reconnect with exponential backoff.
372
+ Connections automatically reconnect with exponential backoff. The first reconnect attempt is **immediate** (0ms delay) for fast recovery from brief network blips, followed by exponential backoff starting at 200ms.
308
373
 
309
374
  ```ts
310
375
  // Custom retry config
311
376
  await session.connect({
312
377
  retry: {
313
378
  enabled: true, // Default: true
314
- initialDelayMs: 1000, // Default: 1000
379
+ initialDelayMs: 200, // Default: 200 (used from 2nd attempt onward)
315
380
  maxDelayMs: 30000, // Default: 30000
316
381
  maxRetries: 10, // Default: Infinity
317
382
  },
@@ -321,6 +386,35 @@ await session.connect({
321
386
  await session.connect({ retry: { enabled: false } });
322
387
  ```
323
388
 
389
+ ### Auto-Preview on Reconnect
390
+
391
+ When connecting to an **existing app** (via `client.connect()`), the SDK automatically sends a `preview` message on connect and reconnect to ensure the preview deployment stays active:
392
+
393
+ ```ts
394
+ // Existing app - auto-preview is enabled by default
395
+ const session = await client.connect('agent-id');
396
+ await session.connect(); // Sends preview message automatically
397
+
398
+ // Override for new builds or disable
399
+ await session.connect({ autoRequestPreview: false });
400
+
401
+ // Force enable for new builds
402
+ const session = await client.build('Build a todo app');
403
+ await session.connect({ autoRequestPreview: true });
404
+ ```
405
+
406
+ ### Reconnect Events
407
+
408
+ ```ts
409
+ session.on('ws:reconnecting', ({ attempt, delayMs, reason }) => {
410
+ console.log(`Reconnecting (attempt ${attempt}, delay ${delayMs}ms, reason: ${reason})`);
411
+ });
412
+
413
+ session.on('ws:reconnected', () => {
414
+ console.log('Reconnected successfully');
415
+ });
416
+ ```
417
+
324
418
  ## HTTP Retry
325
419
 
326
420
  HTTP requests automatically retry on 5xx errors.
@@ -403,6 +497,8 @@ import type {
403
497
  PhaseStatus,
404
498
  PhaseFileStatus,
405
499
  PhaseEventType,
500
+ PhaseTimelineEvent,
501
+ PhaseTimelineChangeType,
406
502
 
407
503
  // API
408
504
  ApiResponse,
package/dist/index.d.ts CHANGED
@@ -5453,7 +5453,21 @@ type CodeGenArgs$1 = CodeGenArgs;
5453
5453
  export type BuildOptions = Omit<CodeGenArgs$1, "query"> & {
5454
5454
  autoConnect?: boolean;
5455
5455
  autoGenerate?: boolean;
5456
+ /**
5457
+ * Called for each blueprint chunk as it streams from the server.
5458
+ */
5456
5459
  onBlueprintChunk?: (chunk: string) => void;
5460
+ /**
5461
+ * Called if blueprint streaming fails. The session will be closed automatically.
5462
+ * Only relevant when `waitForBlueprint` is false.
5463
+ */
5464
+ onBlueprintError?: (error: Error) => void;
5465
+ /**
5466
+ * If true (default), `build()` waits for all blueprint chunks before returning.
5467
+ * If false, `build()` returns immediately after receiving the start event,
5468
+ * and blueprint chunks stream in the background via `onBlueprintChunk`.
5469
+ */
5470
+ waitForBlueprint?: boolean;
5457
5471
  };
5458
5472
  type TemplateFiles = Record<string, string>;
5459
5473
  export type BuildStartEvent = {
@@ -5554,6 +5568,7 @@ export type AgentEventMap = {
5554
5568
  delayMs: number;
5555
5569
  reason: "close" | "error";
5556
5570
  };
5571
+ "ws:reconnected": undefined;
5557
5572
  "ws:raw": {
5558
5573
  raw: unknown;
5559
5574
  };
@@ -5568,6 +5583,8 @@ export type AgentEventMap = {
5568
5583
  error: {
5569
5584
  error: string;
5570
5585
  };
5586
+ /** Emitted when the phase timeline changes (phase added or updated). */
5587
+ phases: PhaseTimelineEvent;
5571
5588
  };
5572
5589
  /**
5573
5590
  * URL provider for WebSocket connections.
@@ -5657,6 +5674,18 @@ export type SessionPhases = {
5657
5674
  /** Check if all phases are completed. */
5658
5675
  allCompleted: () => boolean;
5659
5676
  };
5677
+ /**
5678
+ * Event emitted when the phase timeline changes.
5679
+ */
5680
+ export type PhaseTimelineChangeType = "added" | "updated";
5681
+ export type PhaseTimelineEvent = {
5682
+ /** Type of change: 'added' for new phase, 'updated' for status/file changes. */
5683
+ type: PhaseTimelineChangeType;
5684
+ /** The phase that was added or updated. */
5685
+ phase: PhaseInfo;
5686
+ /** All phases in the timeline after this change. */
5687
+ allPhases: PhaseInfo[];
5688
+ };
5660
5689
  export type SessionDeployable = {
5661
5690
  files: number;
5662
5691
  reason: "generation_complete" | "phase_validated";
@@ -5759,6 +5788,11 @@ export declare class SessionStateStore {
5759
5788
  private emitter;
5760
5789
  get(): SessionState;
5761
5790
  onChange(cb: (next: SessionState, prev: SessionState) => void): () => void;
5791
+ /**
5792
+ * Subscribe to phase timeline changes.
5793
+ * Fires when a phase is added or when a phase's status/files change.
5794
+ */
5795
+ onPhaseChange(cb: (event: PhaseTimelineEvent) => void): () => void;
5762
5796
  setConnection(state: ConnectionState): void;
5763
5797
  applyWsMessage(msg: AgentWsServerMessage): void;
5764
5798
  /**
@@ -5770,6 +5804,14 @@ export declare class SessionStateStore {
5770
5804
  */
5771
5805
  private updateOrAddPhase;
5772
5806
  private setState;
5807
+ /**
5808
+ * Compare old and new phases arrays and emit change events.
5809
+ */
5810
+ private emitPhaseChanges;
5811
+ /**
5812
+ * Check if a phase has meaningfully changed (status or file statuses).
5813
+ */
5814
+ private hasPhaseChanged;
5773
5815
  clear(): void;
5774
5816
  }
5775
5817
  type WorkspaceChange = {
@@ -5822,12 +5864,19 @@ type WaitUntilReadyOptions = WaitOptions;
5822
5864
  type BuildSessionConnectOptions = Omit<AgentConnectionOptions, "credentials"> & {
5823
5865
  /** If true (default), send `get_conversation_state` on socket open. */
5824
5866
  autoRequestConversationState?: boolean;
5867
+ /**
5868
+ * If true, send `preview` message on connect and reconnect to ensure preview exists.
5869
+ * Defaults to true for existing apps (via `client.connect()`), false for new builds.
5870
+ */
5871
+ autoRequestPreview?: boolean;
5825
5872
  /** Credentials to send via session_init after connection. */
5826
5873
  credentials?: Credentials;
5827
5874
  };
5828
5875
  type BuildSessionInit = {
5829
5876
  httpClient: HttpClient;
5830
5877
  defaultCredentials?: Credentials;
5878
+ /** True if this session is for an existing app (via client.connect). */
5879
+ isExistingApp?: boolean;
5831
5880
  };
5832
5881
  export declare class BuildSession {
5833
5882
  private init;
@@ -5856,6 +5905,12 @@ export declare class BuildSession {
5856
5905
  count: () => number;
5857
5906
  /** Check if all phases are completed. */
5858
5907
  allCompleted: () => boolean;
5908
+ /**
5909
+ * Subscribe to phase timeline changes.
5910
+ * Fires when a phase is added or when a phase's status/files change.
5911
+ * @returns Unsubscribe function.
5912
+ */
5913
+ onChange: (cb: (event: PhaseTimelineEvent) => void) => (() => void);
5859
5914
  };
5860
5915
  readonly wait: {
5861
5916
  generationStarted: (options?: WaitOptions) => Promise<{
@@ -5915,6 +5970,10 @@ export declare class VibeClient {
5915
5970
  get baseUrl(): string;
5916
5971
  /**
5917
5972
  * Creates a new agent/app from a prompt and returns a BuildSession.
5973
+ *
5974
+ * By default, waits for all blueprint chunks before returning. Set
5975
+ * `waitForBlueprint: false` to return immediately after the start event,
5976
+ * with blueprint chunks streaming in the background.
5918
5977
  */
5919
5978
  build(prompt: string, options?: BuildOptions): Promise<BuildSession>;
5920
5979
  /** Connect to an existing agent/app by id. */
package/dist/index.js CHANGED
@@ -292,6 +292,9 @@ class SessionStateStore {
292
292
  onChange(cb) {
293
293
  return this.emitter.on("change", ({ prev, next }) => cb(next, prev));
294
294
  }
295
+ onPhaseChange(cb) {
296
+ return this.emitter.on("phaseChange", cb);
297
+ }
295
298
  setConnection(state) {
296
299
  this.setState({ connection: state });
297
300
  }
@@ -406,7 +409,7 @@ class SessionStateStore {
406
409
  const m = msg;
407
410
  const phaseInfo = extractPhaseInfo(m);
408
411
  const phaseFiles = extractPhaseFiles(m);
409
- const phases = this.updateOrAddPhase(phaseInfo, "validating", phaseFiles);
412
+ const phases = this.updateOrAddPhase(phaseInfo, "completed", phaseFiles);
410
413
  this.setState({
411
414
  phase: { status: "implemented", ...phaseInfo },
412
415
  phases
@@ -428,7 +431,7 @@ class SessionStateStore {
428
431
  const m = msg;
429
432
  const phaseInfo = extractPhaseInfo(m);
430
433
  const phaseFiles = extractPhaseFiles(m);
431
- const phases = this.updateOrAddPhase(phaseInfo, "completed", phaseFiles);
434
+ const phases = this.updateOrAddPhase(phaseInfo, "validating", phaseFiles);
432
435
  this.setState({
433
436
  phase: { status: "validated", ...phaseInfo },
434
437
  phases
@@ -524,7 +527,7 @@ class SessionStateStore {
524
527
  const files = (phaseFiles ?? []).map((f) => ({
525
528
  path: f.path,
526
529
  purpose: f.purpose,
527
- status: status === "completed" ? "completed" : "generating"
530
+ status: status === "completed" ? "completed" : "pending"
528
531
  }));
529
532
  if (existingIndex >= 0) {
530
533
  phases[existingIndex] = {
@@ -549,6 +552,38 @@ class SessionStateStore {
549
552
  const next = { ...prev, ...patch };
550
553
  this.state = next;
551
554
  this.emitter.emit("change", { prev, next });
555
+ if (patch.phases && patch.phases !== prev.phases) {
556
+ this.emitPhaseChanges(prev.phases, patch.phases);
557
+ }
558
+ }
559
+ emitPhaseChanges(prevPhases, nextPhases) {
560
+ for (const phase of nextPhases) {
561
+ const prevPhase = prevPhases.find((p) => p.id === phase.id);
562
+ if (!prevPhase) {
563
+ this.emitter.emit("phaseChange", {
564
+ type: "added",
565
+ phase,
566
+ allPhases: nextPhases
567
+ });
568
+ } else if (this.hasPhaseChanged(prevPhase, phase)) {
569
+ this.emitter.emit("phaseChange", {
570
+ type: "updated",
571
+ phase,
572
+ allPhases: nextPhases
573
+ });
574
+ }
575
+ }
576
+ }
577
+ hasPhaseChanged(prev, next) {
578
+ if (prev.status !== next.status)
579
+ return true;
580
+ if (prev.files.length !== next.files.length)
581
+ return true;
582
+ for (let i = 0;i < prev.files.length; i++) {
583
+ if (prev.files[i].status !== next.files[i].status)
584
+ return true;
585
+ }
586
+ return false;
552
587
  }
553
588
  clear() {
554
589
  this.state = INITIAL_STATE;
@@ -583,7 +618,7 @@ async function withTimeout(promise, ms, message = "Operation timed out") {
583
618
  // src/ws.ts
584
619
  var WS_RETRY_DEFAULTS = {
585
620
  enabled: true,
586
- initialDelayMs: 1000,
621
+ initialDelayMs: 200,
587
622
  maxDelayMs: 30000,
588
623
  maxRetries: Infinity
589
624
  };
@@ -595,6 +630,7 @@ function createAgentConnection(getUrl, options = {}) {
595
630
  let closedByUser = false;
596
631
  let reconnectAttempts = 0;
597
632
  let reconnectTimer = null;
633
+ let hasConnectedBefore = false;
598
634
  const pendingSends = [];
599
635
  const maxPendingSends = 1000;
600
636
  function clearReconnectTimer() {
@@ -620,22 +656,34 @@ function createAgentConnection(getUrl, options = {}) {
620
656
  return;
621
657
  if (reconnectTimer)
622
658
  return;
623
- const delayMs = computeBackoffMs(reconnectAttempts, retryCfg);
659
+ const delayMs = reconnectAttempts === 0 ? 0 : computeBackoffMs(reconnectAttempts, retryCfg);
624
660
  emitter.emit("ws:reconnecting", {
625
661
  attempt: reconnectAttempts + 1,
626
662
  delayMs,
627
663
  reason
628
664
  });
629
665
  reconnectAttempts += 1;
630
- reconnectTimer = setTimeout(() => {
631
- reconnectTimer = null;
632
- connectNow();
633
- }, delayMs);
666
+ if (delayMs === 0) {
667
+ reconnectTimer = setTimeout(() => {
668
+ reconnectTimer = null;
669
+ connectNow();
670
+ }, 0);
671
+ } else {
672
+ reconnectTimer = setTimeout(() => {
673
+ reconnectTimer = null;
674
+ connectNow();
675
+ }, delayMs);
676
+ }
634
677
  }
635
678
  function onOpen() {
679
+ const isReconnect = hasConnectedBefore;
680
+ hasConnectedBefore = true;
636
681
  isOpen = true;
637
682
  reconnectAttempts = 0;
638
683
  emitter.emit("ws:open", undefined);
684
+ if (isReconnect) {
685
+ emitter.emit("ws:reconnected", undefined);
686
+ }
639
687
  flushPendingSends();
640
688
  }
641
689
  function onClose(e) {
@@ -946,7 +994,8 @@ class BuildSession {
946
994
  completed: () => this.state.get().phases.filter((p) => p.status === "completed"),
947
995
  get: (id) => this.state.get().phases.find((p) => p.id === id),
948
996
  count: () => this.state.get().phases.length,
949
- allCompleted: () => this.state.get().phases.length > 0 && this.state.get().phases.every((p) => p.status === "completed")
997
+ allCompleted: () => this.state.get().phases.length > 0 && this.state.get().phases.every((p) => p.status === "completed"),
998
+ onChange: (cb) => this.state.onPhaseChange(cb)
950
999
  };
951
1000
  wait = {
952
1001
  generationStarted: (options = {}) => this.waitForGenerationStarted(options),
@@ -969,7 +1018,7 @@ class BuildSession {
969
1018
  async connect(options = {}) {
970
1019
  if (this.connection)
971
1020
  return this.connection;
972
- const { autoRequestConversationState, credentials, ...connectionOptions } = options;
1021
+ const { autoRequestConversationState, autoRequestPreview, credentials, ...connectionOptions } = options;
973
1022
  const getUrl = async () => {
974
1023
  const { ticket } = await this.init.httpClient.getWsTicket(this.agentId);
975
1024
  const base = this.websocketUrl;
@@ -990,6 +1039,7 @@ class BuildSession {
990
1039
  });
991
1040
  const sessionCredentials = credentials ?? this.init.defaultCredentials;
992
1041
  const shouldRequestConversationState = autoRequestConversationState ?? true;
1042
+ const shouldRequestPreview = autoRequestPreview ?? (this.init.isExistingApp ?? false);
993
1043
  this.connection.on("ws:open", () => {
994
1044
  if (sessionCredentials) {
995
1045
  this.connection?.send({
@@ -1000,6 +1050,14 @@ class BuildSession {
1000
1050
  if (shouldRequestConversationState) {
1001
1051
  this.connection?.send({ type: "get_conversation_state" });
1002
1052
  }
1053
+ if (shouldRequestPreview) {
1054
+ this.connection?.send({ type: "preview" });
1055
+ }
1056
+ });
1057
+ this.connection.on("ws:reconnected", () => {
1058
+ if (shouldRequestPreview) {
1059
+ this.connection?.send({ type: "preview" });
1060
+ }
1003
1061
  });
1004
1062
  return this.connection;
1005
1063
  }
@@ -1162,24 +1220,35 @@ class VibeClient {
1162
1220
  if (!resp.body) {
1163
1221
  throw new Error("Missing response body from /api/agent");
1164
1222
  }
1165
- let start = null;
1166
- for await (const obj of parseNdjsonStream(resp.body)) {
1167
- if (!start) {
1168
- start = obj;
1169
- continue;
1170
- }
1171
- const o = obj;
1172
- if (typeof o.chunk === "string") {
1173
- options.onBlueprintChunk?.(o.chunk);
1174
- }
1175
- }
1176
- if (!start) {
1223
+ const iterator = parseNdjsonStream(resp.body)[Symbol.asyncIterator]();
1224
+ const { value: start, done } = await iterator.next();
1225
+ if (done || !start) {
1177
1226
  throw new Error("No start event received from /api/agent");
1178
1227
  }
1179
1228
  const session = new BuildSession(start, {
1180
1229
  httpClient: this.http,
1181
1230
  ...options.credentials ? { defaultCredentials: options.credentials } : {}
1182
1231
  });
1232
+ const waitForBlueprint = options.waitForBlueprint ?? true;
1233
+ const processChunks = async () => {
1234
+ let result = await iterator.next();
1235
+ while (!result.done) {
1236
+ const obj = result.value;
1237
+ if (typeof obj.chunk === "string") {
1238
+ options.onBlueprintChunk?.(obj.chunk);
1239
+ }
1240
+ result = await iterator.next();
1241
+ }
1242
+ };
1243
+ if (waitForBlueprint) {
1244
+ await processChunks();
1245
+ } else {
1246
+ processChunks().catch((error) => {
1247
+ const err = error instanceof Error ? error : new Error(String(error));
1248
+ options.onBlueprintError?.(err);
1249
+ session.close();
1250
+ });
1251
+ }
1183
1252
  if (options.autoConnect ?? true) {
1184
1253
  await session.connect();
1185
1254
  if (options.autoGenerate ?? true) {
@@ -1199,6 +1268,7 @@ class VibeClient {
1199
1268
  };
1200
1269
  return new BuildSession(start, {
1201
1270
  httpClient: this.http,
1271
+ isExistingApp: true,
1202
1272
  ...options.credentials ? { defaultCredentials: options.credentials } : {}
1203
1273
  });
1204
1274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cf-vibesdk/sdk",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {