@cf-vibesdk/sdk 0.0.1 → 0.0.3

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
+ 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;
@@ -1326,6 +1326,12 @@ export interface AgentConnectionData {
1326
1326
  agentId: string;
1327
1327
  }
1328
1328
  export type AgentPreviewResponse = PreviewType;
1329
+ type RetryConfig = {
1330
+ enabled?: boolean;
1331
+ initialDelayMs?: number;
1332
+ maxDelayMs?: number;
1333
+ maxRetries?: number;
1334
+ };
1329
1335
  type BehaviorType$1 = BehaviorType;
1330
1336
  type ProjectType$1 = ProjectType;
1331
1337
  export type Credentials = NonNullable<CodeGenArgs["credentials"]>;
@@ -1427,13 +1433,24 @@ export type AgentEventMap = {
1427
1433
  "ws:error": {
1428
1434
  error: unknown;
1429
1435
  };
1430
- /** Raw server->client message */
1436
+ "ws:reconnecting": {
1437
+ attempt: number;
1438
+ delayMs: number;
1439
+ reason: "close" | "error";
1440
+ };
1441
+ /** Server payload that isn't a well-formed typed message. */
1442
+ "ws:raw": {
1443
+ raw: unknown;
1444
+ };
1445
+ /** Raw server->client message (typed) */
1431
1446
  "ws:message": AgentWsServerMessage;
1432
1447
  connected: WsMessageOf<"agent_connected">;
1433
1448
  conversation: WsMessageOf<"conversation_response" | "conversation_state">;
1434
1449
  phase: WsMessageOf<"phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated">;
1435
1450
  file: WsMessageOf<"file_chunk_generated" | "file_generated" | "file_generating" | "file_regenerating" | "file_regenerated">;
1451
+ generation: WsMessageOf<"generation_started" | "generation_complete" | "generation_stopped" | "generation_resumed">;
1436
1452
  preview: WsMessageOf<"deployment_completed" | "deployment_started" | "deployment_failed">;
1453
+ cloudflare: WsMessageOf<"cloudflare_deployment_started" | "cloudflare_deployment_completed" | "cloudflare_deployment_error">;
1437
1454
  /** User-friendly error derived from `{ type: 'error' }` message */
1438
1455
  error: {
1439
1456
  error: string;
@@ -1454,6 +1471,16 @@ export type AgentConnectionOptions = {
1454
1471
  headers?: Record<string, string>;
1455
1472
  /** Optional WebSocket factory for Node/Bun runtimes. */
1456
1473
  webSocketFactory?: (url: string, protocols?: string | string[], headers?: Record<string, string>) => WebSocketLike;
1474
+ /**
1475
+ * Auto-reconnect config (enabled by default).
1476
+ * Set `{ enabled: false }` to disable.
1477
+ */
1478
+ retry?: {
1479
+ enabled?: boolean;
1480
+ initialDelayMs?: number;
1481
+ maxDelayMs?: number;
1482
+ maxRetries?: number;
1483
+ };
1457
1484
  };
1458
1485
  export type AgentConnection = {
1459
1486
  send: (msg: AgentWsClientMessage) => void;
@@ -1462,6 +1489,34 @@ export type AgentConnection = {
1462
1489
  onAny: (cb: (event: keyof AgentEventMap, payload: AgentEventMap[keyof AgentEventMap]) => void) => () => void;
1463
1490
  waitFor: <K extends keyof AgentEventMap>(event: K, predicate?: (payload: AgentEventMap[K]) => boolean, timeoutMs?: number) => Promise<AgentEventMap[K]>;
1464
1491
  };
1492
+ type FileTreeNode$1 = {
1493
+ type: "dir";
1494
+ name: string;
1495
+ path: string;
1496
+ children: FileTreeNode$1[];
1497
+ } | {
1498
+ type: "file";
1499
+ name: string;
1500
+ path: string;
1501
+ };
1502
+ export type SessionFiles = {
1503
+ listPaths: () => string[];
1504
+ read: (path: string) => string | null;
1505
+ snapshot: () => Record<string, string>;
1506
+ tree: () => FileTreeNode$1[];
1507
+ };
1508
+ export type WaitOptions = {
1509
+ timeoutMs?: number;
1510
+ };
1511
+ export type PhaseEventType = "phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated";
1512
+ export type WaitForPhaseOptions = WaitOptions & {
1513
+ type: PhaseEventType;
1514
+ };
1515
+ export type SessionDeployable = {
1516
+ files: number;
1517
+ reason: "generation_complete" | "phase_validated";
1518
+ previewUrl?: string;
1519
+ };
1465
1520
  export type VibeClientOptions = {
1466
1521
  baseUrl: string;
1467
1522
  /**
@@ -1476,9 +1531,106 @@ export type VibeClientOptions = {
1476
1531
  /** Optional WebSocket factory for Node/Bun runtimes. */
1477
1532
  webSocketFactory?: AgentConnectionOptions["webSocketFactory"];
1478
1533
  fetchFn?: typeof fetch;
1534
+ /** HTTP retry configuration for transient failures (5xx, network errors). */
1535
+ retry?: RetryConfig;
1479
1536
  };
1480
- type WaitUntilReadyOptions = {
1481
- timeoutMs?: number;
1537
+ type GenerationState = {
1538
+ status: "idle";
1539
+ } | {
1540
+ status: "running";
1541
+ totalFiles?: number;
1542
+ } | {
1543
+ status: "stopped";
1544
+ instanceId?: string;
1545
+ } | {
1546
+ status: "complete";
1547
+ instanceId?: string;
1548
+ previewURL?: string;
1549
+ };
1550
+ type PhaseState$1 = {
1551
+ status: "idle";
1552
+ } | {
1553
+ status: "generating" | "generated" | "implementing" | "implemented" | "validating" | "validated";
1554
+ name?: string;
1555
+ description?: string;
1556
+ };
1557
+ type PreviewDeploymentState = {
1558
+ status: "idle";
1559
+ } | {
1560
+ status: "running";
1561
+ } | {
1562
+ status: "failed";
1563
+ error: string;
1564
+ } | {
1565
+ status: "complete";
1566
+ previewURL: string;
1567
+ tunnelURL: string;
1568
+ instanceId: string;
1569
+ };
1570
+ type CloudflareDeploymentState = {
1571
+ status: "idle";
1572
+ } | {
1573
+ status: "running";
1574
+ instanceId?: string;
1575
+ } | {
1576
+ status: "failed";
1577
+ error: string;
1578
+ instanceId?: string;
1579
+ } | {
1580
+ status: "complete";
1581
+ deploymentUrl: string;
1582
+ instanceId: string;
1583
+ workersUrl?: string;
1584
+ };
1585
+ type ConversationState$1 = WsMessageOf<"conversation_state">["state"];
1586
+ export type SessionState = {
1587
+ conversationState?: ConversationState$1;
1588
+ lastConversationResponse?: WsMessageOf<"conversation_response">;
1589
+ generation: GenerationState;
1590
+ phase: PhaseState$1;
1591
+ /** Best-known preview url (from agent_connected, generation_complete, deployment_completed). */
1592
+ previewUrl?: string;
1593
+ preview: PreviewDeploymentState;
1594
+ cloudflare: CloudflareDeploymentState;
1595
+ lastError?: string;
1596
+ };
1597
+ export declare class SessionStateStore {
1598
+ private state;
1599
+ private emitter;
1600
+ get(): SessionState;
1601
+ onChange(cb: (next: SessionState, prev: SessionState) => void): () => void;
1602
+ applyWsMessage(msg: AgentWsServerMessage): void;
1603
+ private setState;
1604
+ clear(): void;
1605
+ }
1606
+ type WorkspaceChange = {
1607
+ type: "reset";
1608
+ files: number;
1609
+ } | {
1610
+ type: "upsert";
1611
+ path: string;
1612
+ } | {
1613
+ type: "delete";
1614
+ path: string;
1615
+ };
1616
+ export declare class WorkspaceStore {
1617
+ private files;
1618
+ private emitter;
1619
+ paths(): string[];
1620
+ read(path: string): string | null;
1621
+ snapshot(): Record<string, string>;
1622
+ onChange(cb: (change: WorkspaceChange) => void): () => void;
1623
+ /** Apply authoritative snapshot from an AgentState. */
1624
+ applyStateSnapshot(state: AgentState): void;
1625
+ /** Apply a single file upsert from WS file events. */
1626
+ applyFileUpsert(file: unknown): void;
1627
+ applyWsMessage(msg: AgentWsServerMessage): void;
1628
+ clear(): void;
1629
+ }
1630
+ type WaitUntilReadyOptions = WaitOptions;
1631
+ type BuildSessionConnectOptions = AgentConnectionOptions & {
1632
+ /** If true (default), send `get_conversation_state` on socket open. */
1633
+ autoRequestConversationState?: boolean;
1482
1634
  };
1483
1635
  type BuildSessionInit = {
1484
1636
  getAuthToken?: () => string | undefined;
@@ -1492,9 +1644,28 @@ export declare class BuildSession {
1492
1644
  readonly behaviorType: BehaviorType$1 | undefined;
1493
1645
  readonly projectType: ProjectType$1 | string | undefined;
1494
1646
  private connection;
1647
+ readonly workspace: WorkspaceStore;
1648
+ readonly state: SessionStateStore;
1649
+ readonly files: SessionFiles;
1650
+ readonly wait: {
1651
+ generationStarted: (options?: WaitOptions) => Promise<{
1652
+ type: "generation_started";
1653
+ message: string;
1654
+ totalFiles: number;
1655
+ }>;
1656
+ generationComplete: (options?: WaitOptions) => Promise<{
1657
+ type: "generation_complete";
1658
+ instanceId?: string;
1659
+ previewURL?: string;
1660
+ }>;
1661
+ phase: (options: WaitForPhaseOptions) => Promise<WsMessageOf<PhaseEventType>>;
1662
+ deployable: (options?: WaitOptions) => Promise<SessionDeployable>;
1663
+ previewDeployed: (options?: WaitOptions) => Promise<DeploymentCompletedMessage>;
1664
+ cloudflareDeployed: (options?: WaitOptions) => Promise<CloudflareDeploymentCompletedMessage>;
1665
+ };
1495
1666
  constructor(clientOptions: VibeClientOptions, start: BuildStartEvent, init?: BuildSessionInit);
1496
1667
  isConnected(): boolean;
1497
- connect(options?: AgentConnectionOptions): AgentConnection;
1668
+ connect(options?: BuildSessionConnectOptions): AgentConnection;
1498
1669
  startGeneration(): void;
1499
1670
  stop(): void;
1500
1671
  followUp(message: string, options?: {
@@ -1503,6 +1674,19 @@ export declare class BuildSession {
1503
1674
  requestConversationState(): void;
1504
1675
  deployPreview(): void;
1505
1676
  deployCloudflare(): void;
1677
+ resume(): void;
1678
+ clearConversation(): void;
1679
+ private getDefaultTimeoutMs;
1680
+ private waitForWsMessage;
1681
+ waitForGenerationStarted(options?: WaitOptions): Promise<WsMessageOf<"generation_started">>;
1682
+ waitForGenerationComplete(options?: WaitOptions): Promise<WsMessageOf<"generation_complete">>;
1683
+ waitForPhase(options: WaitForPhaseOptions): Promise<WsMessageOf<PhaseEventType>>;
1684
+ waitForDeployable(options?: WaitOptions): Promise<SessionDeployable>;
1685
+ waitForPreviewDeployed(options?: WaitOptions): Promise<WsMessageOf<"deployment_completed">>;
1686
+ waitForCloudflareDeployed(options?: WaitOptions): Promise<WsMessageOf<"cloudflare_deployment_completed">>;
1687
+ /**
1688
+ * Legacy alias. Prefer `session.wait.generationStarted()`.
1689
+ */
1506
1690
  waitUntilReady(options?: WaitUntilReadyOptions): Promise<void>;
1507
1691
  on: AgentConnection["on"];
1508
1692
  onAny: AgentConnection["onAny"];
@@ -1556,6 +1740,7 @@ export declare class PhasicClient extends VibeClient {
1556
1740
  export {
1557
1741
  BehaviorType$1 as BehaviorType,
1558
1742
  CodeGenArgs$1 as CodeGenArgs,
1743
+ FileTreeNode$1 as FileTreeNode,
1559
1744
  ProjectType$1 as ProjectType,
1560
1745
  };
1561
1746
 
package/dist/index.js CHANGED
@@ -1,10 +1,40 @@
1
1
  // @bun
2
+ // src/retry.ts
3
+ function normalizeRetryConfig(retry, defaults) {
4
+ return {
5
+ enabled: retry?.enabled ?? defaults.enabled,
6
+ initialDelayMs: retry?.initialDelayMs ?? defaults.initialDelayMs,
7
+ maxDelayMs: retry?.maxDelayMs ?? defaults.maxDelayMs,
8
+ maxRetries: retry?.maxRetries ?? defaults.maxRetries
9
+ };
10
+ }
11
+ function computeBackoffMs(attempt, cfg) {
12
+ const base = Math.min(cfg.maxDelayMs, cfg.initialDelayMs * Math.pow(2, Math.max(0, attempt)));
13
+ const jitter = base * 0.2;
14
+ return Math.max(0, Math.floor(base - jitter + Math.random() * jitter * 2));
15
+ }
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+
2
20
  // src/http.ts
21
+ var HTTP_RETRY_DEFAULTS = {
22
+ enabled: true,
23
+ initialDelayMs: 1000,
24
+ maxDelayMs: 1e4,
25
+ maxRetries: 3
26
+ };
27
+ function isRetryableStatus(status) {
28
+ return status >= 500 && status < 600;
29
+ }
30
+
3
31
  class HttpClient {
4
32
  opts;
5
33
  cachedAccessToken = null;
34
+ retryCfg;
6
35
  constructor(opts) {
7
36
  this.opts = opts;
37
+ this.retryCfg = normalizeRetryConfig(opts.retry, HTTP_RETRY_DEFAULTS);
8
38
  }
9
39
  get baseUrl() {
10
40
  return this.opts.baseUrl.replace(/\/$/, "");
@@ -34,7 +64,10 @@ class HttpClient {
34
64
  }
35
65
  });
36
66
  if (!resp.ok) {
37
- const text = await resp.text().catch(() => "");
67
+ const text = (await resp.text().catch(() => "")).slice(0, 1000);
68
+ if (resp.status === 401) {
69
+ throw new Error(`HTTP 401 for /api/auth/exchange-api-key: invalid API key (regenerate in Settings \u2192 API Keys). ${text || ""}`.trim());
70
+ }
38
71
  throw new Error(`HTTP ${resp.status} for /api/auth/exchange-api-key: ${text || resp.statusText}`);
39
72
  }
40
73
  const parsed = await resp.json();
@@ -57,21 +90,59 @@ class HttpClient {
57
90
  }
58
91
  async fetchJson(path, init) {
59
92
  const url = `${this.baseUrl}${path}`;
60
- const resp = await this.fetchFn(url, init);
61
- if (!resp.ok) {
62
- const text = await resp.text().catch(() => "");
63
- throw new Error(`HTTP ${resp.status} for ${path}: ${text || resp.statusText}`);
93
+ let lastError = null;
94
+ for (let attempt = 0;attempt <= this.retryCfg.maxRetries; attempt++) {
95
+ try {
96
+ const resp = await this.fetchFn(url, init);
97
+ if (!resp.ok) {
98
+ const text = (await resp.text().catch(() => "")).slice(0, 1000);
99
+ const error = new Error(`HTTP ${resp.status} for ${path}: ${text || resp.statusText}`);
100
+ if (this.retryCfg.enabled && isRetryableStatus(resp.status) && attempt < this.retryCfg.maxRetries) {
101
+ lastError = error;
102
+ await sleep(computeBackoffMs(attempt, this.retryCfg));
103
+ continue;
104
+ }
105
+ throw error;
106
+ }
107
+ return await resp.json();
108
+ } catch (error) {
109
+ if (error instanceof TypeError && this.retryCfg.enabled && attempt < this.retryCfg.maxRetries) {
110
+ lastError = error;
111
+ await sleep(computeBackoffMs(attempt, this.retryCfg));
112
+ continue;
113
+ }
114
+ throw error;
115
+ }
64
116
  }
65
- return await resp.json();
117
+ throw lastError ?? new Error(`Failed after ${this.retryCfg.maxRetries} retries`);
66
118
  }
67
119
  async fetchRaw(path, init) {
68
120
  const url = `${this.baseUrl}${path}`;
69
- const resp = await this.fetchFn(url, init);
70
- if (!resp.ok) {
71
- const text = await resp.text().catch(() => "");
72
- throw new Error(`HTTP ${resp.status} for ${path}: ${text || resp.statusText}`);
121
+ let lastError = null;
122
+ for (let attempt = 0;attempt <= this.retryCfg.maxRetries; attempt++) {
123
+ try {
124
+ const resp = await this.fetchFn(url, init);
125
+ if (!resp.ok) {
126
+ const text = (await resp.text().catch(() => "")).slice(0, 1000);
127
+ const error = new Error(`HTTP ${resp.status} for ${path}: ${text || resp.statusText}`);
128
+ if (this.retryCfg.enabled && isRetryableStatus(resp.status) && attempt < this.retryCfg.maxRetries) {
129
+ lastError = error;
130
+ await sleep(computeBackoffMs(attempt, this.retryCfg));
131
+ continue;
132
+ }
133
+ throw error;
134
+ }
135
+ return resp;
136
+ } catch (error) {
137
+ if (error instanceof TypeError && this.retryCfg.enabled && attempt < this.retryCfg.maxRetries) {
138
+ lastError = error;
139
+ await sleep(computeBackoffMs(attempt, this.retryCfg));
140
+ continue;
141
+ }
142
+ throw error;
143
+ }
73
144
  }
74
- return resp;
145
+ throw lastError ?? new Error(`Failed after ${this.retryCfg.maxRetries} retries`);
75
146
  }
76
147
  }
77
148
 
@@ -134,6 +205,177 @@ class TypedEmitter {
134
205
  for (const cb of set)
135
206
  cb(payload);
136
207
  }
208
+ clear() {
209
+ this.listeners.clear();
210
+ this.anyListeners.clear();
211
+ }
212
+ }
213
+
214
+ // src/state.ts
215
+ var INITIAL_STATE = {
216
+ generation: { status: "idle" },
217
+ phase: { status: "idle" },
218
+ preview: { status: "idle" },
219
+ cloudflare: { status: "idle" }
220
+ };
221
+ function extractPhaseInfo(msg) {
222
+ const phase = msg?.phase;
223
+ return {
224
+ name: phase?.name,
225
+ description: phase?.description
226
+ };
227
+ }
228
+
229
+ class SessionStateStore {
230
+ state = INITIAL_STATE;
231
+ emitter = new TypedEmitter;
232
+ get() {
233
+ return this.state;
234
+ }
235
+ onChange(cb) {
236
+ return this.emitter.on("change", ({ prev, next }) => cb(next, prev));
237
+ }
238
+ applyWsMessage(msg) {
239
+ switch (msg.type) {
240
+ case "conversation_state": {
241
+ const m = msg;
242
+ this.setState({ conversationState: m.state });
243
+ break;
244
+ }
245
+ case "conversation_response": {
246
+ const m = msg;
247
+ this.setState({ lastConversationResponse: m });
248
+ break;
249
+ }
250
+ case "generation_started": {
251
+ const m = msg;
252
+ this.setState({ generation: { status: "running", totalFiles: m.totalFiles } });
253
+ break;
254
+ }
255
+ case "generation_complete": {
256
+ const m = msg;
257
+ const previewURL = m.previewURL;
258
+ this.setState({
259
+ generation: {
260
+ status: "complete",
261
+ instanceId: m.instanceId,
262
+ previewURL
263
+ },
264
+ ...previewURL ? { previewUrl: previewURL } : {}
265
+ });
266
+ break;
267
+ }
268
+ case "generation_stopped": {
269
+ const m = msg;
270
+ this.setState({ generation: { status: "stopped", instanceId: m.instanceId } });
271
+ break;
272
+ }
273
+ case "generation_resumed": {
274
+ this.setState({ generation: { status: "running" } });
275
+ break;
276
+ }
277
+ case "phase_generating": {
278
+ const m = msg;
279
+ this.setState({ phase: { status: "generating", ...extractPhaseInfo(m) } });
280
+ break;
281
+ }
282
+ case "phase_generated": {
283
+ const m = msg;
284
+ this.setState({ phase: { status: "generated", ...extractPhaseInfo(m) } });
285
+ break;
286
+ }
287
+ case "phase_implementing": {
288
+ const m = msg;
289
+ this.setState({ phase: { status: "implementing", ...extractPhaseInfo(m) } });
290
+ break;
291
+ }
292
+ case "phase_implemented": {
293
+ const m = msg;
294
+ this.setState({ phase: { status: "implemented", ...extractPhaseInfo(m) } });
295
+ break;
296
+ }
297
+ case "phase_validating": {
298
+ const m = msg;
299
+ this.setState({ phase: { status: "validating", ...extractPhaseInfo(m) } });
300
+ break;
301
+ }
302
+ case "phase_validated": {
303
+ const m = msg;
304
+ this.setState({ phase: { status: "validated", ...extractPhaseInfo(m) } });
305
+ break;
306
+ }
307
+ case "deployment_started": {
308
+ this.setState({ preview: { status: "running" } });
309
+ break;
310
+ }
311
+ case "deployment_failed": {
312
+ const m = msg;
313
+ this.setState({ preview: { status: "failed", error: m.error } });
314
+ break;
315
+ }
316
+ case "deployment_completed": {
317
+ const m = msg;
318
+ this.setState({
319
+ previewUrl: m.previewURL,
320
+ preview: {
321
+ status: "complete",
322
+ previewURL: m.previewURL,
323
+ tunnelURL: m.tunnelURL,
324
+ instanceId: m.instanceId
325
+ }
326
+ });
327
+ break;
328
+ }
329
+ case "cloudflare_deployment_started": {
330
+ const m = msg;
331
+ this.setState({ cloudflare: { status: "running", instanceId: m.instanceId } });
332
+ break;
333
+ }
334
+ case "cloudflare_deployment_error": {
335
+ const m = msg;
336
+ this.setState({
337
+ cloudflare: { status: "failed", error: m.error, instanceId: m.instanceId }
338
+ });
339
+ break;
340
+ }
341
+ case "cloudflare_deployment_completed": {
342
+ const m = msg;
343
+ this.setState({
344
+ cloudflare: {
345
+ status: "complete",
346
+ deploymentUrl: m.deploymentUrl,
347
+ workersUrl: m.workersUrl,
348
+ instanceId: m.instanceId
349
+ }
350
+ });
351
+ break;
352
+ }
353
+ case "agent_connected": {
354
+ const m = msg;
355
+ const previewUrl = m.previewUrl;
356
+ if (previewUrl)
357
+ this.setState({ previewUrl });
358
+ break;
359
+ }
360
+ case "error": {
361
+ const m = msg;
362
+ this.setState({ lastError: m.error });
363
+ break;
364
+ }
365
+ default:
366
+ break;
367
+ }
368
+ }
369
+ setState(patch) {
370
+ const prev = this.state;
371
+ const next = { ...prev, ...patch };
372
+ this.state = next;
373
+ this.emitter.emit("change", { prev, next });
374
+ }
375
+ clear() {
376
+ this.state = INITIAL_STATE;
377
+ this.emitter.clear();
378
+ }
137
379
  }
138
380
 
139
381
  // src/ws.ts
@@ -143,39 +385,121 @@ function toWsCloseEvent(ev) {
143
385
  reason: typeof ev.reason === "string" ? ev.reason : ""
144
386
  };
145
387
  }
388
+ var WS_RETRY_DEFAULTS = {
389
+ enabled: true,
390
+ initialDelayMs: 1000,
391
+ maxDelayMs: 30000,
392
+ maxRetries: Infinity
393
+ };
146
394
  function createAgentConnection(url, options = {}) {
147
395
  const emitter = new TypedEmitter;
396
+ const retryCfg = normalizeRetryConfig(options.retry, WS_RETRY_DEFAULTS);
148
397
  const headers = { ...options.headers ?? {} };
149
398
  if (options.origin)
150
399
  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
- });
400
+ let ws = null;
401
+ let isOpen = false;
402
+ let closedByUser = false;
403
+ let reconnectAttempts = 0;
404
+ let reconnectTimer = null;
405
+ const pendingSends = [];
406
+ const maxPendingSends = 1000;
407
+ function clearReconnectTimer() {
408
+ if (!reconnectTimer)
409
+ return;
410
+ clearTimeout(reconnectTimer);
411
+ reconnectTimer = null;
412
+ }
413
+ function makeWebSocket() {
414
+ if (options.webSocketFactory)
415
+ return options.webSocketFactory(url, undefined, headers);
416
+ return new WebSocket(url);
417
+ }
418
+ function flushPendingSends() {
419
+ if (!ws || !isOpen)
420
+ return;
421
+ for (const data of pendingSends)
422
+ ws.send(data);
423
+ pendingSends.length = 0;
424
+ }
425
+ function scheduleReconnect(reason) {
426
+ if (closedByUser)
427
+ return;
428
+ if (!retryCfg.enabled)
429
+ return;
430
+ if (reconnectAttempts >= retryCfg.maxRetries)
431
+ return;
432
+ if (reconnectTimer)
433
+ return;
434
+ const delayMs = computeBackoffMs(reconnectAttempts, retryCfg);
435
+ emitter.emit("ws:reconnecting", {
436
+ attempt: reconnectAttempts + 1,
437
+ delayMs,
438
+ reason
172
439
  });
173
- ws.on("error", (error) => emitter.emit("ws:error", { error }));
174
- ws.on("message", (data) => handleMessage(data));
440
+ reconnectAttempts += 1;
441
+ reconnectTimer = setTimeout(() => {
442
+ reconnectTimer = null;
443
+ connectNow();
444
+ }, delayMs);
445
+ }
446
+ function onOpen() {
447
+ isOpen = true;
448
+ reconnectAttempts = 0;
449
+ emitter.emit("ws:open", undefined);
450
+ flushPendingSends();
451
+ }
452
+ function onClose(ev) {
453
+ isOpen = false;
454
+ emitter.emit("ws:close", toWsCloseEvent(ev));
455
+ scheduleReconnect("close");
456
+ }
457
+ function onError(error) {
458
+ emitter.emit("ws:error", { error });
459
+ scheduleReconnect("error");
460
+ }
461
+ function looksLikeAgentState(obj) {
462
+ if (!obj || typeof obj !== "object")
463
+ return false;
464
+ const behaviorType = obj.behaviorType;
465
+ const projectType = obj.projectType;
466
+ return typeof behaviorType === "string" && typeof projectType === "string";
467
+ }
468
+ function normalizeServerPayload(raw) {
469
+ if (!raw || typeof raw !== "object")
470
+ return null;
471
+ const t = raw.type;
472
+ if (typeof t === "string") {
473
+ const trimmed = t.trim();
474
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
475
+ try {
476
+ const inner = JSON.parse(trimmed);
477
+ const normalizedInner = normalizeServerPayload(inner);
478
+ if (normalizedInner)
479
+ return normalizedInner;
480
+ emitter.emit("ws:raw", { raw: inner });
481
+ return null;
482
+ } catch {}
483
+ }
484
+ return raw;
485
+ }
486
+ const state = raw.state;
487
+ if (looksLikeAgentState(state)) {
488
+ return { type: "cf_agent_state", state };
489
+ }
490
+ if (looksLikeAgentState(raw)) {
491
+ return { type: "cf_agent_state", state: raw };
492
+ }
493
+ return null;
175
494
  }
176
- function handleMessage(data) {
495
+ function onMessage(data) {
177
496
  try {
178
- const parsed = JSON.parse(String(data));
497
+ const raw = JSON.parse(String(data));
498
+ const parsed = normalizeServerPayload(raw);
499
+ if (!parsed) {
500
+ emitter.emit("ws:raw", { raw });
501
+ return;
502
+ }
179
503
  emitter.emit("ws:message", parsed);
180
504
  switch (parsed.type) {
181
505
  case "agent_connected":
@@ -200,11 +524,22 @@ function createAgentConnection(url, options = {}) {
200
524
  case "file_regenerated":
201
525
  emitter.emit("file", parsed);
202
526
  break;
527
+ case "generation_started":
528
+ case "generation_complete":
529
+ case "generation_stopped":
530
+ case "generation_resumed":
531
+ emitter.emit("generation", parsed);
532
+ break;
203
533
  case "deployment_completed":
204
534
  case "deployment_started":
205
535
  case "deployment_failed":
206
536
  emitter.emit("preview", parsed);
207
537
  break;
538
+ case "cloudflare_deployment_started":
539
+ case "cloudflare_deployment_completed":
540
+ case "cloudflare_deployment_error":
541
+ emitter.emit("cloudflare", parsed);
542
+ break;
208
543
  case "error":
209
544
  emitter.emit("error", { error: String(parsed.error ?? "Unknown error") });
210
545
  break;
@@ -212,14 +547,56 @@ function createAgentConnection(url, options = {}) {
212
547
  break;
213
548
  }
214
549
  } catch (error) {
215
- emitter.emit("ws:error", { error });
550
+ onError(error);
216
551
  }
217
552
  }
553
+ function connectNow() {
554
+ if (closedByUser)
555
+ return;
556
+ clearReconnectTimer();
557
+ try {
558
+ ws = makeWebSocket();
559
+ } catch (error) {
560
+ onError(error);
561
+ scheduleReconnect("error");
562
+ return;
563
+ }
564
+ if (ws.addEventListener) {
565
+ ws.addEventListener("open", () => onOpen());
566
+ ws.addEventListener("close", (ev) => onClose(ev));
567
+ ws.addEventListener("error", (ev) => onError(ev));
568
+ ws.addEventListener("message", (ev) => onMessage(ev.data));
569
+ return;
570
+ }
571
+ if (ws.on) {
572
+ ws.on("open", () => onOpen());
573
+ ws.on("close", (code, reason) => onClose({ code: typeof code === "number" ? code : undefined, reason: typeof reason === "string" ? reason : undefined }));
574
+ ws.on("error", (error) => onError(error));
575
+ ws.on("message", (data) => onMessage(data));
576
+ }
577
+ }
578
+ connectNow();
218
579
  function send(msg) {
219
- ws.send(JSON.stringify(msg));
580
+ const data = JSON.stringify(msg);
581
+ if (isOpen && ws) {
582
+ ws.send(data);
583
+ return;
584
+ }
585
+ pendingSends.push(data);
586
+ if (pendingSends.length > maxPendingSends) {
587
+ pendingSends.shift();
588
+ emitter.emit("ws:error", {
589
+ error: new Error(`Message queue overflow: dropped oldest message (queue size: ${maxPendingSends})`)
590
+ });
591
+ }
220
592
  }
221
593
  function close() {
222
- ws.close();
594
+ closedByUser = true;
595
+ isOpen = false;
596
+ pendingSends.length = 0;
597
+ clearReconnectTimer();
598
+ ws?.close();
599
+ ws = null;
223
600
  }
224
601
  async function waitFor(event, predicate, timeoutMs = 60000) {
225
602
  return new Promise((resolve, reject) => {
@@ -245,7 +622,121 @@ function createAgentConnection(url, options = {}) {
245
622
  };
246
623
  }
247
624
 
625
+ // src/workspace.ts
626
+ function isRecord(value) {
627
+ return typeof value === "object" && value !== null;
628
+ }
629
+ function isFileOutputType(value) {
630
+ if (!isRecord(value))
631
+ return false;
632
+ return typeof value.filePath === "string" && typeof value.fileContents === "string";
633
+ }
634
+ function extractGeneratedFilesFromState(state) {
635
+ const out = [];
636
+ for (const file of Object.values(state.generatedFilesMap ?? {})) {
637
+ if (!isFileOutputType(file))
638
+ continue;
639
+ out.push({ path: file.filePath, content: file.fileContents });
640
+ }
641
+ return out;
642
+ }
643
+ function extractGeneratedFileFromMessageFile(file) {
644
+ if (!isFileOutputType(file))
645
+ return null;
646
+ return { path: file.filePath, content: file.fileContents };
647
+ }
648
+
649
+ class WorkspaceStore {
650
+ files = new Map;
651
+ emitter = new TypedEmitter;
652
+ paths() {
653
+ return Array.from(this.files.keys()).sort();
654
+ }
655
+ read(path) {
656
+ return this.files.get(path) ?? null;
657
+ }
658
+ snapshot() {
659
+ const out = {};
660
+ for (const [path, content] of this.files.entries())
661
+ out[path] = content;
662
+ return out;
663
+ }
664
+ onChange(cb) {
665
+ return this.emitter.on("change", cb);
666
+ }
667
+ applyStateSnapshot(state) {
668
+ this.files.clear();
669
+ for (const f of extractGeneratedFilesFromState(state)) {
670
+ this.files.set(f.path, f.content);
671
+ }
672
+ this.emitter.emit("change", { type: "reset", files: this.files.size });
673
+ }
674
+ applyFileUpsert(file) {
675
+ const f = extractGeneratedFileFromMessageFile(file);
676
+ if (!f)
677
+ return;
678
+ this.files.set(f.path, f.content);
679
+ this.emitter.emit("change", { type: "upsert", path: f.path });
680
+ }
681
+ applyWsMessage(msg) {
682
+ switch (msg.type) {
683
+ case "agent_connected":
684
+ this.applyStateSnapshot(msg.state);
685
+ break;
686
+ case "cf_agent_state":
687
+ this.applyStateSnapshot(msg.state);
688
+ break;
689
+ case "file_generated":
690
+ this.applyFileUpsert(msg.file);
691
+ break;
692
+ case "file_regenerated":
693
+ this.applyFileUpsert(msg.file);
694
+ break;
695
+ default:
696
+ break;
697
+ }
698
+ }
699
+ clear() {
700
+ this.files.clear();
701
+ this.emitter.clear();
702
+ }
703
+ }
704
+
248
705
  // src/session.ts
706
+ function buildFileTree(paths) {
707
+ const root = { name: "", path: "", dirs: new Map, files: [] };
708
+ for (const p of paths) {
709
+ const parts = p.split("/").filter(Boolean);
710
+ let curr = root;
711
+ for (let i = 0;i < parts.length; i += 1) {
712
+ const part = parts[i];
713
+ const isLast = i === parts.length - 1;
714
+ if (isLast) {
715
+ curr.files.push({ type: "file", name: part, path: p });
716
+ continue;
717
+ }
718
+ const nextPath = curr.path ? `${curr.path}/${part}` : part;
719
+ let next = curr.dirs.get(part);
720
+ if (!next) {
721
+ next = { name: part, path: nextPath, dirs: new Map, files: [] };
722
+ curr.dirs.set(part, next);
723
+ }
724
+ curr = next;
725
+ }
726
+ }
727
+ function toNodes(dir) {
728
+ const dirs = Array.from(dir.dirs.values()).sort((a, b) => a.name.localeCompare(b.name)).map((d) => ({
729
+ type: "dir",
730
+ name: d.name,
731
+ path: d.path,
732
+ children: toNodes(d)
733
+ }));
734
+ const files = dir.files.sort((a, b) => a.name.localeCompare(b.name));
735
+ return [...dirs, ...files];
736
+ }
737
+ return toNodes(root);
738
+ }
739
+
249
740
  class BuildSession {
250
741
  clientOptions;
251
742
  init;
@@ -254,6 +745,22 @@ class BuildSession {
254
745
  behaviorType;
255
746
  projectType;
256
747
  connection = null;
748
+ workspace = new WorkspaceStore;
749
+ state = new SessionStateStore;
750
+ files = {
751
+ listPaths: () => this.workspace.paths(),
752
+ read: (path) => this.workspace.read(path),
753
+ snapshot: () => this.workspace.snapshot(),
754
+ tree: () => buildFileTree(this.workspace.paths())
755
+ };
756
+ wait = {
757
+ generationStarted: (options = {}) => this.waitForGenerationStarted(options),
758
+ generationComplete: (options = {}) => this.waitForGenerationComplete(options),
759
+ phase: (options) => this.waitForPhase(options),
760
+ deployable: (options = {}) => this.waitForDeployable(options),
761
+ previewDeployed: (options = {}) => this.waitForPreviewDeployed(options),
762
+ cloudflareDeployed: (options = {}) => this.waitForCloudflareDeployed(options)
763
+ };
257
764
  constructor(clientOptions, start, init = {}) {
258
765
  this.clientOptions = clientOptions;
259
766
  this.init = init;
@@ -268,29 +775,38 @@ class BuildSession {
268
775
  connect(options = {}) {
269
776
  if (this.connection)
270
777
  return this.connection;
271
- const origin = options.origin ?? this.clientOptions.websocketOrigin;
272
- const webSocketFactory = options.webSocketFactory ?? this.clientOptions.webSocketFactory;
273
- const headers = { ...options.headers ?? {} };
778
+ const { autoRequestConversationState, ...agentOptions } = options;
779
+ const origin = agentOptions.origin ?? this.clientOptions.websocketOrigin;
780
+ const webSocketFactory = agentOptions.webSocketFactory ?? this.clientOptions.webSocketFactory;
781
+ const headers = { ...agentOptions.headers ?? {} };
274
782
  const token = this.init.getAuthToken?.();
275
783
  if (token && !headers.Authorization) {
276
784
  headers.Authorization = `Bearer ${token}`;
277
785
  }
278
786
  const connectOptions = {
279
- ...options,
787
+ ...agentOptions,
280
788
  ...origin ? { origin } : {},
281
789
  ...Object.keys(headers).length ? { headers } : {},
282
790
  ...webSocketFactory ? { webSocketFactory } : {}
283
791
  };
284
792
  this.connection = createAgentConnection(this.websocketUrl, connectOptions);
285
- const credentials = options.credentials ?? this.init.defaultCredentials;
286
- if (credentials) {
287
- this.connection.on("ws:open", () => {
793
+ this.connection.on("ws:message", (m) => {
794
+ this.workspace.applyWsMessage(m);
795
+ this.state.applyWsMessage(m);
796
+ });
797
+ const credentials = agentOptions.credentials ?? this.init.defaultCredentials;
798
+ const shouldRequestConversationState = autoRequestConversationState ?? true;
799
+ this.connection.on("ws:open", () => {
800
+ if (credentials) {
288
801
  this.connection?.send({
289
802
  type: "session_init",
290
803
  credentials
291
804
  });
292
- });
293
- }
805
+ }
806
+ if (shouldRequestConversationState) {
807
+ this.connection?.send({ type: "get_conversation_state" });
808
+ }
809
+ });
294
810
  return this.connection;
295
811
  }
296
812
  startGeneration() {
@@ -321,15 +837,65 @@ class BuildSession {
321
837
  this.assertConnected();
322
838
  this.connection.send({ type: "deploy" });
323
839
  }
324
- async waitUntilReady(options = {}) {
840
+ resume() {
325
841
  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;
842
+ this.connection.send({ type: "resume_generation" });
843
+ }
844
+ clearConversation() {
845
+ this.assertConnected();
846
+ this.connection.send({ type: "clear_conversation" });
847
+ }
848
+ getDefaultTimeoutMs() {
849
+ return 10 * 60000;
850
+ }
851
+ async waitForWsMessage(predicate, timeoutMs) {
852
+ this.assertConnected();
853
+ return await this.connection.waitFor("ws:message", predicate, timeoutMs);
854
+ }
855
+ async waitForGenerationStarted(options = {}) {
856
+ return await this.waitForMessageType("generation_started", options.timeoutMs ?? this.getDefaultTimeoutMs());
857
+ }
858
+ async waitForGenerationComplete(options = {}) {
859
+ return await this.waitForMessageType("generation_complete", options.timeoutMs ?? this.getDefaultTimeoutMs());
860
+ }
861
+ async waitForPhase(options) {
862
+ return await this.waitForMessageType(options.type, options.timeoutMs ?? this.getDefaultTimeoutMs());
863
+ }
864
+ async waitForDeployable(options = {}) {
865
+ const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
866
+ if (this.behaviorType === "phasic") {
867
+ await this.waitForPhase({ type: "phase_validated", timeoutMs });
868
+ return {
869
+ files: this.workspace.paths().length,
870
+ reason: "phase_validated",
871
+ previewUrl: this.state.get().previewUrl
872
+ };
873
+ }
874
+ await this.waitForGenerationComplete({ timeoutMs });
875
+ return {
876
+ files: this.workspace.paths().length,
877
+ reason: "generation_complete",
878
+ previewUrl: this.state.get().previewUrl
879
+ };
880
+ }
881
+ async waitForPreviewDeployed(options = {}) {
882
+ const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
883
+ const msg = await this.waitForWsMessage((m) => m.type === "deployment_completed" || m.type === "deployment_failed", timeoutMs);
884
+ if (msg.type === "deployment_failed") {
885
+ throw new Error(msg.error);
886
+ }
887
+ return msg;
888
+ }
889
+ async waitForCloudflareDeployed(options = {}) {
890
+ const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
891
+ const msg = await this.waitForWsMessage((m) => m.type === "cloudflare_deployment_completed" || m.type === "cloudflare_deployment_error", timeoutMs);
892
+ if (msg.type === "cloudflare_deployment_error") {
893
+ throw new Error(msg.error);
331
894
  }
332
- await this.connection.waitFor("conversation", (payload) => payload.type === "conversation_response", timeoutMs);
895
+ return msg;
896
+ }
897
+ async waitUntilReady(options = {}) {
898
+ await this.waitForGenerationStarted(options);
333
899
  }
334
900
  on = (event, cb) => {
335
901
  this.assertConnected();
@@ -348,11 +914,13 @@ class BuildSession {
348
914
  }
349
915
  async waitForMessageType(type, timeoutMs) {
350
916
  this.assertConnected();
351
- return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs);
917
+ return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs ?? this.getDefaultTimeoutMs());
352
918
  }
353
919
  close() {
354
920
  this.connection?.close();
355
921
  this.connection = null;
922
+ this.workspace.clear();
923
+ this.state.clear();
356
924
  }
357
925
  assertConnected() {
358
926
  if (!this.connection) {
@@ -494,7 +1062,9 @@ class PhasicClient extends VibeClient {
494
1062
  }
495
1063
  }
496
1064
  export {
1065
+ WorkspaceStore,
497
1066
  VibeClient,
1067
+ SessionStateStore,
498
1068
  PhasicClient,
499
1069
  BuildSession,
500
1070
  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.3",
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
+