@cf-vibesdk/sdk 0.0.2 → 0.0.4

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
@@ -1,6 +1,6 @@
1
1
  # @cf-vibesdk/sdk
2
2
 
3
- Type-safe client SDK for the VibeSDK platform.
3
+ Client SDK for the VibeSDK platform.
4
4
 
5
5
  ## Install
6
6
 
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,7 @@ 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);
38
68
  if (resp.status === 401) {
39
69
  throw new Error(`HTTP 401 for /api/auth/exchange-api-key: invalid API key (regenerate in Settings \u2192 API Keys). ${text || ""}`.trim());
40
70
  }
@@ -60,21 +90,59 @@ class HttpClient {
60
90
  }
61
91
  async fetchJson(path, init) {
62
92
  const url = `${this.baseUrl}${path}`;
63
- const resp = await this.fetchFn(url, init);
64
- if (!resp.ok) {
65
- const text = await resp.text().catch(() => "");
66
- 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
+ }
67
116
  }
68
- return await resp.json();
117
+ throw lastError ?? new Error(`Failed after ${this.retryCfg.maxRetries} retries`);
69
118
  }
70
119
  async fetchRaw(path, init) {
71
120
  const url = `${this.baseUrl}${path}`;
72
- const resp = await this.fetchFn(url, init);
73
- if (!resp.ok) {
74
- const text = await resp.text().catch(() => "");
75
- 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
+ }
76
144
  }
77
- return resp;
145
+ throw lastError ?? new Error(`Failed after ${this.retryCfg.maxRetries} retries`);
78
146
  }
79
147
  }
80
148
 
@@ -137,19 +205,25 @@ class TypedEmitter {
137
205
  for (const cb of set)
138
206
  cb(payload);
139
207
  }
208
+ clear() {
209
+ this.listeners.clear();
210
+ this.anyListeners.clear();
211
+ }
140
212
  }
141
213
 
142
214
  // src/state.ts
143
215
  var INITIAL_STATE = {
216
+ connection: "disconnected",
144
217
  generation: { status: "idle" },
145
218
  phase: { status: "idle" },
146
219
  preview: { status: "idle" },
147
220
  cloudflare: { status: "idle" }
148
221
  };
149
222
  function extractPhaseInfo(msg) {
223
+ const phase = msg?.phase;
150
224
  return {
151
- name: msg.phase?.name,
152
- description: msg.phase?.description
225
+ name: phase?.name,
226
+ description: phase?.description
153
227
  };
154
228
  }
155
229
 
@@ -162,6 +236,9 @@ class SessionStateStore {
162
236
  onChange(cb) {
163
237
  return this.emitter.on("change", ({ prev, next }) => cb(next, prev));
164
238
  }
239
+ setConnection(state) {
240
+ this.setState({ connection: state });
241
+ }
165
242
  applyWsMessage(msg) {
166
243
  switch (msg.type) {
167
244
  case "conversation_state": {
@@ -176,29 +253,57 @@ class SessionStateStore {
176
253
  }
177
254
  case "generation_started": {
178
255
  const m = msg;
179
- this.setState({ generation: { status: "running", totalFiles: m.totalFiles } });
256
+ this.setState({
257
+ generation: { status: "running", totalFiles: m.totalFiles, filesGenerated: 0 },
258
+ currentFile: undefined
259
+ });
180
260
  break;
181
261
  }
182
262
  case "generation_complete": {
183
263
  const m = msg;
184
264
  const previewURL = m.previewURL;
265
+ const prev = this.state.generation;
266
+ const filesGenerated = "filesGenerated" in prev ? prev.filesGenerated : 0;
185
267
  this.setState({
186
268
  generation: {
187
269
  status: "complete",
188
270
  instanceId: m.instanceId,
189
- previewURL
271
+ previewURL,
272
+ filesGenerated
190
273
  },
274
+ currentFile: undefined,
191
275
  ...previewURL ? { previewUrl: previewURL } : {}
192
276
  });
193
277
  break;
194
278
  }
195
279
  case "generation_stopped": {
196
280
  const m = msg;
197
- this.setState({ generation: { status: "stopped", instanceId: m.instanceId } });
281
+ const prev = this.state.generation;
282
+ const filesGenerated = "filesGenerated" in prev ? prev.filesGenerated : 0;
283
+ this.setState({
284
+ generation: { status: "stopped", instanceId: m.instanceId, filesGenerated }
285
+ });
198
286
  break;
199
287
  }
200
288
  case "generation_resumed": {
201
- this.setState({ generation: { status: "running" } });
289
+ const prev = this.state.generation;
290
+ const filesGenerated = "filesGenerated" in prev ? prev.filesGenerated : 0;
291
+ this.setState({ generation: { status: "running", filesGenerated } });
292
+ break;
293
+ }
294
+ case "file_generating": {
295
+ const m = msg;
296
+ this.setState({ currentFile: m.filePath });
297
+ break;
298
+ }
299
+ case "file_generated": {
300
+ const prev = this.state.generation;
301
+ if (prev.status === "running" || prev.status === "stopped") {
302
+ this.setState({
303
+ generation: { ...prev, filesGenerated: prev.filesGenerated + 1 },
304
+ currentFile: undefined
305
+ });
306
+ }
202
307
  break;
203
308
  }
204
309
  case "phase_generating": {
@@ -299,6 +404,10 @@ class SessionStateStore {
299
404
  this.state = next;
300
405
  this.emitter.emit("change", { prev, next });
301
406
  }
407
+ clear() {
408
+ this.state = INITIAL_STATE;
409
+ this.emitter.clear();
410
+ }
302
411
  }
303
412
 
304
413
  // src/ws.ts
@@ -308,23 +417,15 @@ function toWsCloseEvent(ev) {
308
417
  reason: typeof ev.reason === "string" ? ev.reason : ""
309
418
  };
310
419
  }
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
- }
420
+ var WS_RETRY_DEFAULTS = {
421
+ enabled: true,
422
+ initialDelayMs: 1000,
423
+ maxDelayMs: 30000,
424
+ maxRetries: Infinity
425
+ };
325
426
  function createAgentConnection(url, options = {}) {
326
427
  const emitter = new TypedEmitter;
327
- const retryCfg = normalizeRetryConfig(options.retry);
428
+ const retryCfg = normalizeRetryConfig(options.retry, WS_RETRY_DEFAULTS);
328
429
  const headers = { ...options.headers ?? {} };
329
430
  if (options.origin)
330
431
  headers.Origin = options.origin;
@@ -501,7 +602,7 @@ function createAgentConnection(url, options = {}) {
501
602
  }
502
603
  if (ws.on) {
503
604
  ws.on("open", () => onOpen());
504
- ws.on("close", (code, reason) => onClose({ code, reason }));
605
+ ws.on("close", (code, reason) => onClose({ code: typeof code === "number" ? code : undefined, reason: typeof reason === "string" ? reason : undefined }));
505
606
  ws.on("error", (error) => onError(error));
506
607
  ws.on("message", (data) => onMessage(data));
507
608
  }
@@ -514,8 +615,12 @@ function createAgentConnection(url, options = {}) {
514
615
  return;
515
616
  }
516
617
  pendingSends.push(data);
517
- if (pendingSends.length > maxPendingSends)
618
+ if (pendingSends.length > maxPendingSends) {
518
619
  pendingSends.shift();
620
+ emitter.emit("ws:error", {
621
+ error: new Error(`Message queue overflow: dropped oldest message (queue size: ${maxPendingSends})`)
622
+ });
623
+ }
519
624
  }
520
625
  function close() {
521
626
  closedByUser = true;
@@ -553,25 +658,24 @@ function createAgentConnection(url, options = {}) {
553
658
  function isRecord(value) {
554
659
  return typeof value === "object" && value !== null;
555
660
  }
661
+ function isFileOutputType(value) {
662
+ if (!isRecord(value))
663
+ return false;
664
+ return typeof value.filePath === "string" && typeof value.fileContents === "string";
665
+ }
556
666
  function extractGeneratedFilesFromState(state) {
557
667
  const out = [];
558
668
  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
- }
669
+ if (!isFileOutputType(file))
670
+ continue;
671
+ out.push({ path: file.filePath, content: file.fileContents });
564
672
  }
565
673
  return out;
566
674
  }
567
675
  function extractGeneratedFileFromMessageFile(file) {
568
- if (!isRecord(file))
676
+ if (!isFileOutputType(file))
569
677
  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 };
678
+ return { path: file.filePath, content: file.fileContents };
575
679
  }
576
680
 
577
681
  class WorkspaceStore {
@@ -624,6 +728,10 @@ class WorkspaceStore {
624
728
  break;
625
729
  }
626
730
  }
731
+ clear() {
732
+ this.files.clear();
733
+ this.emitter.clear();
734
+ }
627
735
  }
628
736
 
629
737
  // src/session.ts
@@ -713,11 +821,18 @@ class BuildSession {
713
821
  ...Object.keys(headers).length ? { headers } : {},
714
822
  ...webSocketFactory ? { webSocketFactory } : {}
715
823
  };
824
+ this.state.setConnection("connecting");
716
825
  this.connection = createAgentConnection(this.websocketUrl, connectOptions);
717
826
  this.connection.on("ws:message", (m) => {
718
827
  this.workspace.applyWsMessage(m);
719
828
  this.state.applyWsMessage(m);
720
829
  });
830
+ this.connection.on("ws:open", () => {
831
+ this.state.setConnection("connected");
832
+ });
833
+ this.connection.on("ws:close", () => {
834
+ this.state.setConnection("disconnected");
835
+ });
721
836
  const credentials = agentOptions.credentials ?? this.init.defaultCredentials;
722
837
  const shouldRequestConversationState = autoRequestConversationState ?? true;
723
838
  this.connection.on("ws:open", () => {
@@ -843,6 +958,8 @@ class BuildSession {
843
958
  close() {
844
959
  this.connection?.close();
845
960
  this.connection = null;
961
+ this.workspace.clear();
962
+ this.state.clear();
846
963
  }
847
964
  assertConnected() {
848
965
  if (!this.connection) {
@@ -983,11 +1100,114 @@ class PhasicClient extends VibeClient {
983
1100
  return super.build(prompt, { ...options, behaviorType: options.behaviorType ?? "phasic" });
984
1101
  }
985
1102
  }
1103
+ // src/blueprint.ts
1104
+ function isRecord2(v) {
1105
+ return Boolean(v) && typeof v === "object" && !Array.isArray(v);
1106
+ }
1107
+ function blueprintToMarkdown(bp) {
1108
+ const lines = [];
1109
+ const title = bp.title ?? bp.projectName ?? "Blueprint";
1110
+ lines.push(`# ${title}`);
1111
+ if (bp.description) {
1112
+ lines.push("");
1113
+ lines.push(bp.description);
1114
+ }
1115
+ if (bp.frameworks?.length) {
1116
+ lines.push("");
1117
+ lines.push("## Frameworks");
1118
+ for (const f of bp.frameworks)
1119
+ lines.push(`- ${f}`);
1120
+ }
1121
+ if (bp.detailedDescription) {
1122
+ lines.push("");
1123
+ lines.push("## Details");
1124
+ lines.push(bp.detailedDescription);
1125
+ }
1126
+ if (bp.views?.length) {
1127
+ lines.push("");
1128
+ lines.push("## Views");
1129
+ for (const v of bp.views)
1130
+ lines.push(`- **${v.name}**: ${v.description}`);
1131
+ }
1132
+ if (bp.plan?.length) {
1133
+ lines.push("");
1134
+ lines.push("## Plan");
1135
+ bp.plan.forEach((s, idx) => lines.push(`${idx + 1}. ${s}`));
1136
+ }
1137
+ if (bp.implementationRoadmap?.length) {
1138
+ lines.push("");
1139
+ lines.push("## Roadmap");
1140
+ for (const p of bp.implementationRoadmap)
1141
+ lines.push(`- **${p.phase}**: ${p.description}`);
1142
+ }
1143
+ return lines.join(`
1144
+ `);
1145
+ }
1146
+ function extractJsonStringField(raw, key) {
1147
+ const re = new RegExp(`"${key}"\\s*:\\s*"([^"\\n\\r]*)"`);
1148
+ const m = re.exec(raw);
1149
+ return m?.[1] ?? null;
1150
+ }
1151
+
1152
+ class BlueprintStreamParser {
1153
+ buffer = "";
1154
+ append(chunk) {
1155
+ this.buffer += chunk;
1156
+ return this.toMarkdown();
1157
+ }
1158
+ toMarkdown() {
1159
+ const startsLikeJson = /^\s*[\[{]/.test(this.buffer);
1160
+ if (!startsLikeJson) {
1161
+ return this.buffer;
1162
+ }
1163
+ try {
1164
+ const parsed = JSON.parse(this.buffer);
1165
+ if (isRecord2(parsed)) {
1166
+ return blueprintToMarkdown(parsed);
1167
+ }
1168
+ } catch {}
1169
+ const title = extractJsonStringField(this.buffer, "title") ?? extractJsonStringField(this.buffer, "projectName") ?? "Blueprint";
1170
+ const desc = extractJsonStringField(this.buffer, "description");
1171
+ const lines = [`# ${title}`, "", desc ? desc : "*Generating blueprint...*"];
1172
+ return lines.join(`
1173
+ `);
1174
+ }
1175
+ getRaw() {
1176
+ return this.buffer;
1177
+ }
1178
+ clear() {
1179
+ this.buffer = "";
1180
+ }
1181
+ }
1182
+ // src/utils.ts
1183
+ class TimeoutError extends Error {
1184
+ constructor(message) {
1185
+ super(message);
1186
+ this.name = "TimeoutError";
1187
+ }
1188
+ }
1189
+ async function withTimeout(promise, ms, message = "Operation timed out") {
1190
+ let timeoutId;
1191
+ const timeout = new Promise((_, reject) => {
1192
+ timeoutId = setTimeout(() => reject(new TimeoutError(message)), ms);
1193
+ });
1194
+ try {
1195
+ return await Promise.race([promise, timeout]);
1196
+ } finally {
1197
+ if (timeoutId !== undefined)
1198
+ clearTimeout(timeoutId);
1199
+ }
1200
+ }
986
1201
  export {
1202
+ withTimeout,
1203
+ isRecord2 as isRecord,
1204
+ blueprintToMarkdown,
987
1205
  WorkspaceStore,
988
1206
  VibeClient,
1207
+ TimeoutError,
989
1208
  SessionStateStore,
990
1209
  PhasicClient,
991
1210
  BuildSession,
1211
+ BlueprintStreamParser,
992
1212
  AgenticClient
993
1213
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cf-vibesdk/sdk",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -34,5 +34,3 @@
34
34
  "typescript": "^5.9.3"
35
35
  }
36
36
  }
37
-
38
-