@lovable.dev/sdk 1.1.1 → 1.2.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/dist/index.js CHANGED
@@ -1,1266 +1,1218 @@
1
- // src/client.ts
2
1
  import createClient from "openapi-fetch";
3
-
4
- // src/retryFetch.ts
5
- var RETRY_DELAYS_MS = [100, 300, 500];
6
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
- function parseRetryAfterMs(headers) {
8
- const raw = headers.get("retry-after");
9
- if (!raw) return void 0;
10
- const seconds = Number(raw);
11
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
12
- const dateMs = Date.parse(raw);
13
- if (!Number.isNaN(dateMs)) {
14
- const delta = dateMs - Date.now();
15
- return delta > 0 ? delta : 0;
16
- }
17
- return void 0;
2
+ //#region src/retryFetch.ts
3
+ const RETRY_DELAYS_MS = [
4
+ 100,
5
+ 300,
6
+ 500
7
+ ];
8
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
+ function parseRetryAfterMs$1(headers) {
10
+ const raw = headers.get("retry-after");
11
+ if (!raw) return void 0;
12
+ const seconds = Number(raw);
13
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
14
+ const dateMs = Date.parse(raw);
15
+ if (!Number.isNaN(dateMs)) {
16
+ const delta = dateMs - Date.now();
17
+ return delta > 0 ? delta : 0;
18
+ }
18
19
  }
20
+ /**
21
+ * Wraps a fetch implementation with retry on HTTP 429. Up to three retries
22
+ * at 100ms / 300ms / 500ms; if the server sends `Retry-After` and it's
23
+ * larger than the planned delay, we honor the server value. The Request is
24
+ * cloned before each attempt so its body remains replayable.
25
+ *
26
+ * Matches openapi-fetch's `fetch` option signature: a single `Request` in,
27
+ * `Promise<Response>` out.
28
+ */
19
29
  function makeRetryFetch(baseFetch = (input) => globalThis.fetch(input)) {
20
- return async (input) => {
21
- const clone = () => input.clone();
22
- let response = await baseFetch(clone());
23
- for (const planned of RETRY_DELAYS_MS) {
24
- if (response.status !== 429) break;
25
- const serverDelay = parseRetryAfterMs(response.headers);
26
- await sleep(Math.max(planned, serverDelay ?? 0));
27
- response = await baseFetch(clone());
28
- }
29
- return response;
30
- };
30
+ return async (input) => {
31
+ const clone = () => input.clone();
32
+ let response = await baseFetch(clone());
33
+ for (const planned of RETRY_DELAYS_MS) {
34
+ if (response.status !== 429) break;
35
+ const serverDelay = parseRetryAfterMs$1(response.headers);
36
+ await sleep$1(Math.max(planned, serverDelay ?? 0));
37
+ response = await baseFetch(clone());
38
+ }
39
+ return response;
40
+ };
31
41
  }
32
-
33
- // src/types.ts
42
+ //#endregion
43
+ //#region src/types.ts
34
44
  var ApiError = class extends Error {
35
- status;
36
- type;
37
- detail;
38
- props;
39
- rateLimit;
40
- constructor(status, message, type, detail, props, rateLimit) {
41
- super(message);
42
- this.status = status;
43
- this.type = type;
44
- this.detail = detail;
45
- this.props = props;
46
- this.rateLimit = rateLimit;
47
- }
45
+ status;
46
+ type;
47
+ detail;
48
+ props;
49
+ rateLimit;
50
+ constructor(status, message, type, detail, props, rateLimit) {
51
+ super(message);
52
+ this.status = status;
53
+ this.type = type;
54
+ this.detail = detail;
55
+ this.props = props;
56
+ this.rateLimit = rateLimit;
57
+ }
48
58
  };
49
-
50
- // src/client.ts
51
- var DEFAULT_BASE_URL = "https://api.lovable.dev";
52
- var PUBLIC_API_CURSOR_VERSION = 1;
59
+ //#endregion
60
+ //#region src/client.ts
61
+ const DEFAULT_BASE_URL = "https://api.lovable.dev";
62
+ const PUBLIC_API_CURSOR_VERSION = 1;
53
63
  function normalizeBaseUrl(url) {
54
- if (!url) return DEFAULT_BASE_URL;
55
- const normalized = url.replace(/\/$/, "");
56
- if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) {
57
- throw new Error(`baseUrl must include a protocol (http:// or https://). Got: "${url}"`);
58
- }
59
- return normalized;
64
+ if (!url) return DEFAULT_BASE_URL;
65
+ const normalized = url.replace(/\/$/, "");
66
+ if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) throw new Error(`baseUrl must include a protocol (http:// or https://). Got: "${url}"`);
67
+ return normalized;
60
68
  }
61
69
  function encodePublicCursor(id) {
62
- const raw = JSON.stringify({ v: PUBLIC_API_CURSOR_VERSION, id });
63
- const bytes = new TextEncoder().encode(raw);
64
- let binary = "";
65
- for (const byte of bytes) {
66
- binary += String.fromCharCode(byte);
67
- }
68
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
70
+ const raw = JSON.stringify({
71
+ v: PUBLIC_API_CURSOR_VERSION,
72
+ id
73
+ });
74
+ const bytes = new TextEncoder().encode(raw);
75
+ let binary = "";
76
+ for (const byte of bytes) binary += String.fromCharCode(byte);
77
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
69
78
  }
70
79
  function normalizeWorkspaceList(body) {
71
- return body.data ?? body.workspaces ?? [];
80
+ return body.data ?? body.workspaces ?? [];
72
81
  }
73
82
  function cursorHasMore(body) {
74
- return body.pagination?.has_more ?? body.has_more ?? false;
83
+ return body.pagination?.has_more ?? body.has_more ?? false;
75
84
  }
76
85
  function requireCreateProjectId(project) {
77
- if (!project.id) throw new Error("Create project response missing project ID");
78
- return project;
86
+ if (!project.id) throw new Error("Create project response missing project ID");
87
+ return project;
79
88
  }
80
- var errorMiddleware = {
81
- async onResponse({ response }) {
82
- if (response.ok) return;
83
- let errorBody;
84
- try {
85
- errorBody = await response.clone().json();
86
- } catch {
87
- }
88
- const message = buildErrorMessage(errorBody, response.status, response.statusText);
89
- const type = errorBody?.type ?? errorBody?.title;
90
- const detail = errorBody?.detail ?? errorBody?.details;
91
- throw new ApiError(response.status, message, type, detail, errorBody?.props, parseRateLimitInfo(response.headers));
92
- }
93
- };
89
+ /**
90
+ * openapi-fetch middleware that converts non-2xx Response objects into the
91
+ * SDK's ApiError, preserving the existing error contract (status/type/detail/
92
+ * props) used by every other method in this client.
93
+ */
94
+ const errorMiddleware = { async onResponse({ response }) {
95
+ if (response.ok) return;
96
+ let errorBody;
97
+ try {
98
+ errorBody = await response.clone().json();
99
+ } catch {}
100
+ const message = buildErrorMessage(errorBody, response.status, response.statusText);
101
+ const type = errorBody?.type ?? errorBody?.title;
102
+ const detail = errorBody?.detail ?? errorBody?.details;
103
+ throw new ApiError(response.status, message, type, detail, errorBody?.props, parseRateLimitInfo(response.headers));
104
+ } };
94
105
  function parseRateLimitInfo(headers) {
95
- const limit = parsePositiveInt(headers.get("x-ratelimit-limit"));
96
- const remaining = parsePositiveInt(headers.get("x-ratelimit-remaining"));
97
- const retryAfterMs = parseRetryAfterMs2(headers.get("retry-after"));
98
- if (limit == null && remaining == null && retryAfterMs == null) return void 0;
99
- return { limit, remaining, retryAfterMs };
106
+ const limit = parsePositiveInt(headers.get("x-ratelimit-limit"));
107
+ const remaining = parsePositiveInt(headers.get("x-ratelimit-remaining"));
108
+ const retryAfterMs = parseRetryAfterMs(headers.get("retry-after"));
109
+ if (limit == null && remaining == null && retryAfterMs == null) return void 0;
110
+ return {
111
+ limit,
112
+ remaining,
113
+ retryAfterMs
114
+ };
100
115
  }
101
116
  function parsePositiveInt(raw) {
102
- if (raw == null) return void 0;
103
- const n = Number(raw);
104
- return Number.isFinite(n) && n >= 0 ? n : void 0;
117
+ if (raw == null) return void 0;
118
+ const n = Number(raw);
119
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
105
120
  }
106
- function parseRetryAfterMs2(raw) {
107
- if (!raw) return void 0;
108
- const seconds = Number(raw);
109
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
110
- const dateMs = Date.parse(raw);
111
- if (!Number.isNaN(dateMs)) {
112
- const delta = dateMs - Date.now();
113
- return delta > 0 ? delta : 0;
114
- }
115
- return void 0;
121
+ function parseRetryAfterMs(raw) {
122
+ if (!raw) return void 0;
123
+ const seconds = Number(raw);
124
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
125
+ const dateMs = Date.parse(raw);
126
+ if (!Number.isNaN(dateMs)) {
127
+ const delta = dateMs - Date.now();
128
+ return delta > 0 ? delta : 0;
129
+ }
116
130
  }
117
131
  function buildErrorMessage(body, status, statusText) {
118
- return body?.message || body?.title || (statusText ? `HTTP ${status}: ${statusText}` : `HTTP ${status}`);
132
+ return body?.message || body?.title || (statusText ? `HTTP ${status}: ${statusText}` : `HTTP ${status}`);
119
133
  }
120
134
  var LovableClient = class {
121
- authHeaders;
122
- baseUrl;
123
- extraHeaders;
124
- clientSource;
125
- typedClient;
126
- constructor(options) {
127
- const hasApiKey = !!options.apiKey;
128
- const hasBearerToken = !!options.bearerToken;
129
- if (!hasApiKey && !hasBearerToken) {
130
- throw new Error("Either apiKey or bearerToken is required");
131
- }
132
- if (hasApiKey && hasBearerToken) {
133
- throw new Error("Provide either apiKey or bearerToken, not both");
134
- }
135
- this.authHeaders = hasApiKey ? { "Lovable-API-Key": options.apiKey } : { Authorization: `Bearer ${options.bearerToken}` };
136
- this.baseUrl = normalizeBaseUrl(options.baseUrl);
137
- this.extraHeaders = options.headers ?? {};
138
- this.clientSource = options.clientSource ?? "sdk";
139
- this.typedClient = createClient({
140
- baseUrl: this.baseUrl,
141
- headers: {
142
- "X-Client-Source": this.clientSource,
143
- ...this.authHeaders,
144
- ...this.extraHeaders,
145
- Accept: "application/json"
146
- },
147
- fetch: makeRetryFetch()
148
- });
149
- this.typedClient.use(errorMiddleware);
150
- }
151
- /**
152
- * Type-safe access to any documented API route, driven by the auto-generated
153
- * OpenAPI schema. Path / query params and request bodies are checked at compile
154
- * time; calling an unknown path is a type error.
155
- *
156
- * Non-2xx responses throw `ApiError`; on success, `data` is set on the result.
157
- */
158
- get typed() {
159
- return this.typedClient;
160
- }
161
- /**
162
- * Get the current authenticated user and their workspaces.
163
- * Useful for validating an API key and discovering workspace IDs.
164
- */
165
- async me() {
166
- const { data } = await this.typed.GET("/v1/me");
167
- return {
168
- ...data,
169
- workspaces: normalizeWorkspaceList(
170
- data
171
- )
172
- };
173
- }
174
- /**
175
- * List workspaces the authenticated user has access to.
176
- */
177
- async listWorkspaces(options = {}) {
178
- const { data } = await this.typed.GET("/v1/workspaces", {
179
- params: { query: options }
180
- });
181
- return { ...data, workspaces: normalizeWorkspaceList(data) };
182
- }
183
- /**
184
- * Get a specific workspace by ID
185
- */
186
- async getWorkspace(workspaceId) {
187
- const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}", {
188
- params: { path: { workspace_id: workspaceId } }
189
- });
190
- return data.workspace;
191
- }
192
- /**
193
- * List projects in a workspace.
194
- * Supports full-text search, filtering by visibility/publish status/folder/creator,
195
- * and cursor pagination.
196
- */
197
- async listProjects(workspaceId, options) {
198
- const { data } = await this.typed.GET("/v1/projects", {
199
- params: {
200
- query: {
201
- workspace_id: workspaceId,
202
- q: options?.query,
203
- visibility: options?.visibility,
204
- publish_status: options?.publish_status,
205
- folder_id: options?.folder_id,
206
- folder_ids: options?.folder_ids,
207
- user_id: options?.user_id,
208
- type: options?.type,
209
- include_risk: options?.include_risk,
210
- search_fields: options?.search_fields,
211
- viewed_by_me: options?.viewed_by_me,
212
- cursor: options?.cursor,
213
- limit: options?.limit
214
- }
215
- }
216
- });
217
- const projects = data.projects ?? data.data ?? null;
218
- const total = data.total;
219
- return {
220
- ...data,
221
- projects,
222
- ...total === void 0 ? {} : { total },
223
- has_more: data.pagination?.has_more ?? data.has_more
224
- };
225
- }
226
- /**
227
- * Create a new project in a workspace
228
- */
229
- async createProject(workspaceId, options) {
230
- let fileRefs;
231
- let ephemeralFileRefs;
232
- if (options.uploadedFiles?.length) {
233
- ({ fileRefs, ephemeralFileRefs } = this.partitionUploadedFiles(options.uploadedFiles));
234
- } else if (options.files?.length) {
235
- ephemeralFileRefs = await this.uploadEphemeralFiles(options.files);
236
- }
237
- const body = {
238
- description: options.description,
239
- template_project_id: options.templateProjectId
240
- };
241
- if (options.projectName) {
242
- body.display_name = options.projectName;
243
- }
244
- if (options.visibility) {
245
- body.visibility = options.visibility;
246
- }
247
- if (options.techStack) {
248
- body.tech_stack = options.techStack;
249
- }
250
- if (options.sandboxTemplate) {
251
- body.sandbox_template = options.sandboxTemplate;
252
- }
253
- if (options.selectedLibraries?.length) {
254
- body.selected_libraries = options.selectedLibraries;
255
- }
256
- if (options.initialMessage) {
257
- body.initial_message = options.initialMessage;
258
- }
259
- if (fileRefs?.length) {
260
- body.files = fileRefs;
261
- }
262
- if (options.fileUrls?.length) {
263
- body.file_urls = options.fileUrls;
264
- }
265
- if (ephemeralFileRefs?.length) {
266
- body.ephemeral_files = ephemeralFileRefs;
267
- }
268
- const { data } = await this.typed.POST("/v1/projects", {
269
- body: { ...body, workspace_id: workspaceId }
270
- });
271
- return requireCreateProjectId(data);
272
- }
273
- /**
274
- * Send a chat message to a project.
275
- *
276
- * The API accepts the message and processes it in the background.
277
- * Returns the message ID and status. Use `waitForMessageCompletion()`
278
- * to poll for the AI response.
279
- */
280
- async chat(projectId, options) {
281
- let fileRefs;
282
- let ephemeralFileRefs;
283
- if (options.uploadedFiles?.length) {
284
- ({ fileRefs, ephemeralFileRefs } = this.partitionUploadedFiles(options.uploadedFiles));
285
- } else if (options.files?.length) {
286
- fileRefs = await this.uploadProjectFiles(projectId, options.files);
287
- }
288
- const body = {
289
- message: options.message
290
- };
291
- if (options.variantId) {
292
- body.variant_id = options.variantId;
293
- }
294
- if (fileRefs) {
295
- body.files = fileRefs;
296
- }
297
- if (ephemeralFileRefs) {
298
- body.ephemeral_files = ephemeralFileRefs;
299
- }
300
- if (options.planMode) {
301
- body.plan_mode = true;
302
- }
303
- if (options.continuation) {
304
- body.continuation = options.continuation;
305
- }
306
- const { data } = await this.typed.POST("/v1/messages", {
307
- body: { ...body, project_id: projectId }
308
- });
309
- return data;
310
- }
311
- /**
312
- * Create an independent variant from the project's current main branch, or from a full baseSha when provided.
313
- */
314
- async createVariant(projectId, options = {}) {
315
- const { data } = await this.typed.POST("/v1/projects/{project_id}/variants", {
316
- params: { path: { project_id: projectId } },
317
- body: { label: options.label, base_sha: options.baseSha }
318
- });
319
- return data;
320
- }
321
- /**
322
- * Get project details by ID
323
- */
324
- async getProject(projectId) {
325
- const { data } = await this.typed.GET("/v1/projects/{project_id}", {
326
- params: { path: { project_id: projectId } }
327
- });
328
- return data;
329
- }
330
- /**
331
- * Create an anonymous, one-hour static preview URL for one exact HTTPS parent origin.
332
- */
333
- async createEmbedUrl(projectId, parentOrigin) {
334
- const { data } = await this.typed.POST("/v1/projects/{project_id}/embed-url", {
335
- params: { path: { project_id: projectId } },
336
- body: { parent_origin: parentOrigin }
337
- });
338
- return data;
339
- }
340
- /**
341
- * Update supported project fields.
342
- */
343
- async updateProject(projectId, options) {
344
- const { data } = await this.typed.PATCH("/v1/projects/{project_id}", {
345
- params: { path: { project_id: projectId } },
346
- body: options
347
- });
348
- return data;
349
- }
350
- /**
351
- * Soft-delete a project. Repeated deletes are treated as successful.
352
- */
353
- async deleteProject(projectId) {
354
- await this.typed.DELETE("/v1/projects/{project_id}", {
355
- params: { path: { project_id: projectId } }
356
- });
357
- }
358
- /**
359
- * Get the preview URL for a project.
360
- *
361
- * The preview URL is available once the project reaches "completed" status.
362
- * This URL allows viewing the project in development mode.
363
- *
364
- * @param projectId - The project ID
365
- * @returns The preview URL
366
- */
367
- getPreviewUrl(projectId) {
368
- return `https://id-preview--${projectId}.lovable.app`;
369
- }
370
- /**
371
- * Get the published URL for a project (if published).
372
- *
373
- * Returns the public URL if the project has been published, or null if not.
374
- *
375
- * @param projectId - The project ID
376
- * @returns The published URL or null if not published
377
- */
378
- async getPublishedUrl(projectId) {
379
- const project = await this.getProject(projectId);
380
- return project.is_published && project.url ? project.url : null;
381
- }
382
- /**
383
- * Get the cloud database status for a project.
384
- *
385
- * @param projectId - The project ID
386
- * @returns Whether the database is enabled and which stack is used
387
- */
388
- async getDatabaseStatus(projectId) {
389
- const { data } = await this.typed.GET("/v1/database", {
390
- params: { query: { project_id: projectId } }
391
- });
392
- return data;
393
- }
394
- /**
395
- * Enable (provision) a cloud database for a project.
396
- *
397
- * This triggers database provisioning which takes 30-60 seconds.
398
- * The call blocks until provisioning completes.
399
- *
400
- * @param projectId - The project ID
401
- * @returns The database status after enablement
402
- */
403
- async enableDatabase(projectId) {
404
- const { data } = await this.typed.POST("/v1/database/enable", {
405
- body: { project_id: projectId }
406
- });
407
- return data;
408
- }
409
- /**
410
- * Execute a SQL query against the project's cloud database.
411
- *
412
- * Supports SELECT, INSERT, UPDATE, DELETE, and DDL statements.
413
- * The database must be enabled first (see enableDatabase).
414
- *
415
- * @param projectId - The project ID
416
- * @param sql - SQL query to execute
417
- * @returns Query result rows as JSON objects
418
- */
419
- async queryDatabase(projectId, sql) {
420
- const compat = this.typedClient;
421
- const { data } = await compat.POST("/v1/database/query", {
422
- body: { project_id: projectId, sql }
423
- });
424
- return data;
425
- }
426
- // ---------------------------------------------------------------------------
427
- // Messages
428
- // ---------------------------------------------------------------------------
429
- /**
430
- * Get a message by ID. Returns the message content, status, and (for user messages)
431
- * the AI response if available.
432
- *
433
- * Pass `waitSeconds` to long-poll: the server holds the request until the
434
- * message reaches a terminal state (completed / stopped / error / awaiting_input) or the
435
- * duration elapses. This replaces client-side polling for `waitForMessageCompletion`.
436
- */
437
- async getMessage(projectId, messageId, options) {
438
- const waitSeconds = options?.waitSeconds;
439
- const query = {
440
- wait: waitSeconds && waitSeconds > 0 ? `${Math.floor(waitSeconds)}s` : void 0,
441
- thread_id: options?.threadId
442
- };
443
- const { data } = await this.typed.GET("/v1/messages/{message_id}", {
444
- params: { path: { message_id: messageId }, query: { ...query, project_id: projectId } }
445
- });
446
- return data;
447
- }
448
- /**
449
- * List recent messages in a project, newest first. Use `cursor` from the
450
- * previous page's `pagination.next_cursor` to paginate through history.
451
- */
452
- async listMessages(projectId, params) {
453
- const cursor = params?.cursor ?? (params?.before ? encodePublicCursor(params.before) : void 0);
454
- const { data } = await this.typed.GET("/v1/messages", {
455
- params: {
456
- query: { project_id: projectId, limit: params?.limit, cursor }
457
- }
458
- });
459
- return {
460
- ...data,
461
- messages: data.data,
462
- has_more: cursorHasMore(data)
463
- };
464
- }
465
- /**
466
- * Wait for the AI response to reach a terminal status (completed, stopped, error, awaiting_input)
467
- * or for `timeout` to elapse.
468
- *
469
- * Primary path is SSE against `/v1/messages/{message_id}/stream`: one
470
- * held connection that pushes a snapshot on every relevant change and closes
471
- * on terminal. If SSE isn't reachable (proxy strips text/event-stream, server
472
- * returns 404 / 415 / 501) we fall back to the long-poll JSON endpoint on the
473
- * same URL. Both paths share the same `MessageCompletionResult` shape.
474
- */
475
- async waitForMessageCompletion(projectId, messageId, options) {
476
- const timeout = options?.timeout ?? 6e5;
477
- const deadline = Date.now() + timeout;
478
- const sse = await this.waitForMessageCompletionViaSSE(projectId, messageId, deadline, options?.threadId);
479
- if (sse.kind === "result") {
480
- return sse.result;
481
- }
482
- if (Date.now() >= deadline) {
483
- return timeoutResult(messageId, timeout);
484
- }
485
- return this.waitForMessageCompletionViaLongPoll(projectId, messageId, deadline, timeout, options);
486
- }
487
- /**
488
- * @deprecated Use `chat()` or `createProject()`'s returned `message_id`,
489
- * then call `waitForMessageCompletion(projectId, messageId)`.
490
- *
491
- * Throws when the turn pauses for human input (`awaiting_input`) — the
492
- * legacy `ChatResponse` shape cannot carry resume metadata. HITL-capable
493
- * flows need `waitForMessageCompletion` plus `respondToTool`.
494
- */
495
- async waitForResponse(projectId, options) {
496
- const messages = await this.listMessages(projectId, { limit: 10 });
497
- const latest = messages.messages?.find((message) => message.role === "user");
498
- if (!latest?.message_id) {
499
- throw new Error(`No messages found for project ${projectId}`);
500
- }
501
- const completionOptions = options?.timeout === void 0 ? void 0 : { timeout: options.timeout };
502
- const result = await this.waitForMessageCompletion(projectId, latest.message_id, completionOptions);
503
- if (result.status !== "completed" && result.status !== "stopped") {
504
- throw new Error(result.error ?? `Message ${latest.message_id} did not complete (status: ${result.status})`);
505
- }
506
- return {
507
- content: result.content,
508
- messageId: result.message_id,
509
- previewUrl: this.getPreviewUrl(projectId)
510
- };
511
- }
512
- async waitForMessageCompletionViaSSE(projectId, messageId, deadline, threadId) {
513
- const remaining = deadline - Date.now();
514
- if (remaining <= 0) return { kind: "timeout" };
515
- const url = new URL(`${this.baseUrl}/v1/messages/${encodeURIComponent(messageId)}/stream`);
516
- url.searchParams.set("project_id", projectId);
517
- if (threadId) {
518
- url.searchParams.set("thread_id", threadId);
519
- }
520
- const controller = new AbortController();
521
- const timeoutId = setTimeout(() => controller.abort(), remaining);
522
- let response;
523
- try {
524
- response = await fetch(url, {
525
- headers: {
526
- ...this.authHeaders,
527
- ...this.extraHeaders,
528
- "X-Client-Source": this.clientSource,
529
- Accept: "text/event-stream"
530
- },
531
- signal: controller.signal
532
- });
533
- } catch (err) {
534
- clearTimeout(timeoutId);
535
- if (isAbortError(err)) return { kind: "timeout" };
536
- return { kind: "fallback" };
537
- }
538
- if (!response.ok) {
539
- clearTimeout(timeoutId);
540
- if (response.status === 404 || response.status === 415 || response.status === 501) {
541
- await cancelResponseBody(response);
542
- return { kind: "fallback" };
543
- }
544
- if (response.status >= 500) {
545
- await cancelResponseBody(response);
546
- return { kind: "fallback" };
547
- }
548
- const detail = await safeReadText(response);
549
- throw new ApiError(response.status, detail || `HTTP ${response.status}`);
550
- }
551
- if (!response.body) {
552
- clearTimeout(timeoutId);
553
- return { kind: "fallback" };
554
- }
555
- try {
556
- for await (const frame of parseSSEFrames(response.body)) {
557
- if (!frame.data) continue;
558
- let snapshot;
559
- try {
560
- snapshot = JSON.parse(frame.data);
561
- } catch {
562
- continue;
563
- }
564
- const queuedResult = queuedExitResult(snapshot, messageId);
565
- if (queuedResult) return { kind: "result", result: queuedResult };
566
- const terminal = terminalResultFromMessage(snapshot, messageId);
567
- if (terminal) {
568
- return { kind: "result", result: terminal };
569
- }
570
- }
571
- return { kind: "fallback" };
572
- } catch (err) {
573
- if (isAbortError(err)) return { kind: "timeout" };
574
- return { kind: "fallback" };
575
- } finally {
576
- clearTimeout(timeoutId);
577
- controller.abort();
578
- }
579
- }
580
- async waitForMessageCompletionViaLongPoll(projectId, messageId, deadline, totalTimeoutMs, options) {
581
- const waitSeconds = Math.max(1, Math.min(55, options?.waitSeconds ?? 30));
582
- const transientBackoffMs = 1e3;
583
- let notFoundSince = null;
584
- const notFoundGraceMs = 15e3;
585
- while (Date.now() < deadline) {
586
- const remainingMs = deadline - Date.now();
587
- const perCallSeconds = Math.min(waitSeconds, Math.floor(remainingMs / 1e3));
588
- const waitOptions = perCallSeconds > 0 ? { waitSeconds: perCallSeconds } : void 0;
589
- const callStartedAt = Date.now();
590
- try {
591
- const msg = await this.getMessage(projectId, messageId, { ...waitOptions, threadId: options?.threadId });
592
- notFoundSince = null;
593
- const queuedResult = queuedExitResult(msg, messageId);
594
- if (queuedResult) return queuedResult;
595
- const terminal = terminalResultFromMessage(msg, messageId);
596
- if (terminal) return terminal;
597
- if (Date.now() - callStartedAt < transientBackoffMs) {
598
- await sleep2(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
599
- }
600
- } catch (err) {
601
- if (err instanceof ApiError) {
602
- if (err.status === 404) {
603
- if (notFoundSince === null) {
604
- notFoundSince = Date.now();
605
- }
606
- if (Date.now() - notFoundSince < notFoundGraceMs) {
607
- await sleep2(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
608
- continue;
609
- }
610
- return {
611
- status: "error",
612
- message_id: messageId,
613
- content: "",
614
- error: "Message not found. It may have been deleted from the queue."
615
- };
616
- }
617
- if (err.status < 500) {
618
- throw err;
619
- }
620
- }
621
- await sleep2(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
622
- }
623
- }
624
- return timeoutResult(messageId, totalTimeoutMs);
625
- }
626
- // ---------------------------------------------------------------------------
627
- // Knowledge
628
- // ---------------------------------------------------------------------------
629
- /** Get workspace knowledge (custom instructions for the AI agent). */
630
- async getWorkspaceKnowledge(workspaceId) {
631
- const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/knowledge", {
632
- params: { path: { workspace_id: workspaceId } }
633
- });
634
- return data;
635
- }
636
- /** Set workspace knowledge. Max 10,000 characters. */
637
- async setWorkspaceKnowledge(workspaceId, content) {
638
- const { data } = await this.typed.PUT("/v1/workspaces/{workspace_id}/knowledge", {
639
- params: { path: { workspace_id: workspaceId } },
640
- body: { content }
641
- });
642
- return data;
643
- }
644
- // ---------------------------------------------------------------------------
645
- // Workspace skills
646
- // ---------------------------------------------------------------------------
647
- /** List workspace skills. */
648
- async listWorkspaceSkills(workspaceId, options = {}) {
649
- const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/skills", {
650
- params: {
651
- path: { workspace_id: workspaceId },
652
- query: { include_markdown: options.includeMarkdown, limit: options.limit, offset: options.offset }
653
- }
654
- });
655
- return { ...data, skills: data.skills ?? [] };
656
- }
657
- /** Get a single workspace skill, including SKILL.md contents. */
658
- async getWorkspaceSkill(workspaceId, skillName) {
659
- const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/skills/{skill_name}", {
660
- params: { path: { workspace_id: workspaceId, skill_name: skillName } }
661
- });
662
- return data;
663
- }
664
- /** Create a workspace skill from full SKILL.md markdown. */
665
- async createWorkspaceSkill(workspaceId, skillName, markdown) {
666
- const { data } = await this.typed.POST("/v1/workspaces/{workspace_id}/skills/{skill_name}", {
667
- params: { path: { workspace_id: workspaceId, skill_name: skillName } },
668
- body: { markdown }
669
- });
670
- return data;
671
- }
672
- /** Update a workspace skill by replacing its SKILL.md markdown. */
673
- async updateWorkspaceSkill(workspaceId, skillName, markdown) {
674
- const { data } = await this.typed.PUT("/v1/workspaces/{workspace_id}/skills/{skill_name}", {
675
- params: { path: { workspace_id: workspaceId, skill_name: skillName } },
676
- body: { markdown }
677
- });
678
- return data;
679
- }
680
- /** Delete a workspace skill. */
681
- async deleteWorkspaceSkill(workspaceId, skillName) {
682
- const { data } = await this.typed.DELETE("/v1/workspaces/{workspace_id}/skills/{skill_name}", {
683
- params: { path: { workspace_id: workspaceId, skill_name: skillName } }
684
- });
685
- return data;
686
- }
687
- // ---------------------------------------------------------------------------
688
- // Project skills
689
- // ---------------------------------------------------------------------------
690
- /** List project skills, including whether each skill is enabled. */
691
- async listProjectSkills(projectId, options = {}) {
692
- const { data } = await this.typed.GET("/v1/skills", {
693
- params: { query: { project_id: projectId, limit: options.limit, cursor: options.cursor } }
694
- });
695
- const skills = data.skills ?? data.data ?? [];
696
- return { ...data, skills };
697
- }
698
- /** Enable or disable a project skill without removing it from the project repo. */
699
- async setProjectSkillEnabled(projectId, skillName, enabled) {
700
- const { data } = await this.typed.PATCH("/v1/skills/{skill_name}", {
701
- params: { path: { skill_name: skillName } },
702
- body: { project_id: projectId, enabled }
703
- });
704
- return data;
705
- }
706
- /** Get project knowledge (custom instructions for the AI agent). */
707
- async getProjectKnowledge(projectId) {
708
- const { data } = await this.typed.GET("/v1/knowledge", {
709
- params: { query: { project_id: projectId } }
710
- });
711
- return data;
712
- }
713
- /** Set project knowledge. Max 10,000 characters. */
714
- async setProjectKnowledge(projectId, content) {
715
- const { data } = await this.typed.PUT("/v1/knowledge", {
716
- body: { project_id: projectId, content }
717
- });
718
- return data;
719
- }
720
- // ---------------------------------------------------------------------------
721
- // Git operations
722
- // ---------------------------------------------------------------------------
723
- /**
724
- * Get the structured diff for a message or commit.
725
- * Pass `messageId` to get the diff for a specific AI message,
726
- * or `sha` for a specific commit.
727
- */
728
- async getDiff(projectId, params) {
729
- const { data } = await this.typed.GET("/v1/git/diff", {
730
- params: {
731
- query: { project_id: projectId, message_id: params.messageId, sha: params.sha, base_sha: params.baseSha }
732
- }
733
- });
734
- return data;
735
- }
736
- async listFiles(projectId, refOrOptions, options = {}) {
737
- const ref = typeof refOrOptions === "string" ? refOrOptions : void 0;
738
- const pagination = typeof refOrOptions === "string" ? options : refOrOptions ?? options;
739
- const { data } = await this.typed.GET("/v1/git/files", {
740
- params: { query: { project_id: projectId, ref, limit: pagination.limit, cursor: pagination.cursor } }
741
- });
742
- const files = data.data ?? [];
743
- return { ...data, data: files, files };
744
- }
745
- /** Read the raw content of a single file. Omitting ref uses the API default. */
746
- async readFile(projectId, path, ref) {
747
- const { data } = await this.typed.GET("/v1/git/files/{path}", {
748
- params: { path: { path }, query: { project_id: projectId, ref } },
749
- parseAs: "text"
750
- });
751
- return data;
752
- }
753
- // ---------------------------------------------------------------------------
754
- // Edits
755
- // ---------------------------------------------------------------------------
756
- /** List the edit history of a project. */
757
- async listEdits(projectId, params) {
758
- const { data } = await this.typed.GET("/v1/edits", {
759
- params: {
760
- query: { project_id: projectId, limit: params?.limit, before: params?.before }
761
- }
762
- });
763
- return data;
764
- }
765
- // ---------------------------------------------------------------------------
766
- // File upload
767
- // ---------------------------------------------------------------------------
768
- /** Get an ephemeral presigned URL for uploading a file before a project exists. */
769
- async getFileUploadUrl(params) {
770
- return this.getEphemeralFileUploadUrl({ content_type: params.content_type });
771
- }
772
- /** Get a project-scoped presigned URL for uploading a file. Returns the upload URL, file ID, and required PUT headers. */
773
- async getProjectFileUploadUrl(projectId, params) {
774
- const { data } = await this.typed.POST("/v1/project-files/upload-url", {
775
- body: { project_id: projectId, ...params }
776
- });
777
- return data;
778
- }
779
- /** Get an ephemeral presigned URL for uploading a file before a project exists. */
780
- async getEphemeralFileUploadUrl(params) {
781
- const { data } = await this.typed.POST("/v1/files/ephemeral-upload-url", { body: params });
782
- return data;
783
- }
784
- // ---------------------------------------------------------------------------
785
- // Visibility
786
- // ---------------------------------------------------------------------------
787
- /** Set a project's visibility (draft, private, workspace_view, or public). */
788
- async setProjectVisibility(projectId, visibility) {
789
- return this.updateProject(projectId, { visibility });
790
- }
791
- /** Set a folder's visibility (personal or workspace). */
792
- async setFolderVisibility(workspaceId, folderId, visibility) {
793
- const { data } = await this.typed.PUT("/v1/workspaces/{workspace_id}/folders/{folder_id}/visibility", {
794
- params: { path: { workspace_id: workspaceId, folder_id: folderId } },
795
- body: { visibility }
796
- });
797
- return data;
798
- }
799
- /** Move projects into a folder, removing existing folder memberships first. */
800
- async moveProjectsToFolder(workspaceId, folderId, projectIds) {
801
- const { data } = await this.typed.POST("/v1/workspaces/{workspace_id}/folders/{folder_id}/projects/move", {
802
- params: { path: { workspace_id: workspaceId, folder_id: folderId } },
803
- body: { project_ids: projectIds }
804
- });
805
- return data;
806
- }
807
- // ---------------------------------------------------------------------------
808
- // Library & template projects
809
- // ---------------------------------------------------------------------------
810
- /** List available design system library projects in a workspace. */
811
- async listLibraryProjects(workspaceId, options = {}) {
812
- const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/available-library-projects", {
813
- params: { path: { workspace_id: workspaceId }, query: options }
814
- });
815
- return { ...data, libraries: data.libraries ?? [] };
816
- }
817
- /** List available template projects in a workspace. */
818
- async listTemplateProjects(workspaceId, options = {}) {
819
- const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/available-template-projects", {
820
- params: { path: { workspace_id: workspaceId }, query: options }
821
- });
822
- return { ...data, templates: data.templates ?? [] };
823
- }
824
- // ---------------------------------------------------------------------------
825
- // Connectors (MCP servers)
826
- // ---------------------------------------------------------------------------
827
- /** List all connectors in a workspace. */
828
- async listConnectors(workspaceId, options = {}) {
829
- const { data } = await this.typed.GET("/v1/connectors", {
830
- params: {
831
- query: {
832
- workspace_id: workspaceId,
833
- type: options.type,
834
- status: options.status,
835
- limit: options.limit,
836
- cursor: options.cursor
837
- }
838
- }
839
- });
840
- const connectors = data.connectors ?? data.data ?? [];
841
- return { ...data, data: connectors, connectors, has_more: cursorHasMore(data) };
842
- }
843
- /** Add a connector to a workspace. The server URL is tested before saving. */
844
- async addConnector(workspaceId, body) {
845
- const { data } = await this.typed.POST("/v1/connectors", {
846
- body: { ...body, workspace_id: workspaceId }
847
- });
848
- return data;
849
- }
850
- /** Remove a connector from a workspace. */
851
- async removeConnector(workspaceId, connectorId) {
852
- const { data } = await this.typed.DELETE("/v1/connectors/{connector_id}", {
853
- params: { path: { connector_id: connectorId }, query: { workspace_id: workspaceId } }
854
- });
855
- return data;
856
- }
857
- /** Browse available connector templates. */
858
- async listAvailableConnectors(workspaceId, options = {}) {
859
- const { data } = await this.typed.GET("/v1/available-connectors", {
860
- params: { query: { workspace_id: workspaceId, limit: options.limit, cursor: options.cursor } }
861
- });
862
- const catalog = data.catalog ?? data.data ?? [];
863
- return { ...data, data: catalog, catalog, has_more: cursorHasMore(data) };
864
- }
865
- // ---------------------------------------------------------------------------
866
- // Connectors
867
- // ---------------------------------------------------------------------------
868
- /** List standard (OAuth-based) connectors in a workspace. */
869
- async listStandardConnectors(workspaceId, options = {}) {
870
- const { data } = await this.typed.GET("/v1/connectors", {
871
- params: { query: { workspace_id: workspaceId, type: "standard", limit: options.limit, cursor: options.cursor } }
872
- });
873
- const connectors = data.connectors ?? data.data ?? [];
874
- return { ...data, data: connectors, connectors, has_more: cursorHasMore(data) };
875
- }
876
- /** List seamless (zero-config) connectors in a workspace. */
877
- async listSeamlessConnectors(workspaceId, options = {}) {
878
- const { data } = await this.typed.GET("/v1/connectors", {
879
- params: { query: { workspace_id: workspaceId, type: "seamless", limit: options.limit, cursor: options.cursor } }
880
- });
881
- const connectors = data.connectors ?? data.data ?? [];
882
- return { ...data, data: connectors, connectors, has_more: cursorHasMore(data) };
883
- }
884
- /** List MCP connectors in a workspace. */
885
- async listMCPConnectors(workspaceId, options = {}) {
886
- const { data } = await this.typed.GET("/v1/connectors", {
887
- params: { query: { workspace_id: workspaceId, type: "mcp", limit: options.limit, cursor: options.cursor } }
888
- });
889
- const connectors = data.connectors ?? data.data ?? [];
890
- return { ...data, data: connectors, connectors, has_more: cursorHasMore(data) };
891
- }
892
- /** List authenticated connections (accounts) in a workspace. */
893
- async listConnections(workspaceId, params = {}) {
894
- const { data } = await this.typed.GET("/v1/connections", {
895
- params: {
896
- query: {
897
- workspace_id: workspaceId,
898
- connector_id: params.connector_id,
899
- limit: params.limit,
900
- cursor: params.cursor
901
- }
902
- }
903
- });
904
- const connections = data.data ?? [];
905
- return { ...data, data: connections, connections, has_more: cursorHasMore(data) };
906
- }
907
- // ---------------------------------------------------------------------------
908
- // Analytics
909
- // ---------------------------------------------------------------------------
910
- /** Get historical analytics for a published project. */
911
- async getProjectAnalytics(projectId, params) {
912
- const { data } = await this.typed.GET("/v1/analytics", {
913
- params: {
914
- query: {
915
- project_id: projectId,
916
- startDate: params.startDate,
917
- endDate: params.endDate,
918
- granularity: params.granularity
919
- }
920
- }
921
- });
922
- return data;
923
- }
924
- /** Get real-time visitor trend for a published project. */
925
- async getProjectAnalyticsTrend(projectId) {
926
- const { data } = await this.typed.GET("/v1/analytics/trend", {
927
- params: { query: { project_id: projectId } }
928
- });
929
- return data;
930
- }
931
- /**
932
- * Publish a project.
933
- *
934
- * This triggers a deployment which makes the project publicly accessible.
935
- * The deployment runs asynchronously - use waitForProjectPublished() to wait for completion.
936
- *
937
- * @param projectId - The project ID to publish
938
- * @param options.name - Optional custom slug for the published URL
939
- * @returns Deployment info including deployment ID
940
- */
941
- async publish(projectId, options) {
942
- const { data } = await this.typed.POST("/v1/deployments", {
943
- body: { project_id: projectId, name: options?.name }
944
- });
945
- return data;
946
- }
947
- /**
948
- * Remix (fork) an existing project, optionally at a specific message point in time.
949
- *
950
- * When `messageId` is provided, the remix captures the project state after
951
- * that message and its AI response by default. Set `remixMode: "before"` to
952
- * start before the message was processed.
953
- * Without `messageId`, the full current state is remixed.
954
- *
955
- * @param sourceProjectId - The project to remix from
956
- * @param options.workspaceId - Target workspace for the new project
957
- * @param options.messageId - Optional message ID to snapshot at
958
- * @param options.remixMode - "including" (server default): state after the message and its AI response; "before": state before the message
959
- * @param options.includeHistory - Whether to preserve chat history (default: false)
960
- * @param options.includeCustomKnowledge - Whether to copy custom instructions (default: false)
961
- * @param options.initialMessage - Optional initial message to send after remix
962
- * @param options.description - Optional custom description for the new project
963
- * @returns The remix job ID for polling progress
964
- */
965
- async remixProject(sourceProjectId, options) {
966
- const body = {
967
- workspace_id: options.workspaceId,
968
- source_project_id: sourceProjectId,
969
- include_history: options.includeHistory,
970
- include_custom_knowledge: options.includeCustomKnowledge,
971
- description: options.description,
972
- display_name: options.projectName,
973
- skip_initial_remix_message: options.skipInitialRemixMessage,
974
- skip_integrations: options.skipIntegrations
975
- };
976
- if (options.messageId) {
977
- body.message_id = options.messageId;
978
- if (options.remixMode) {
979
- body.remix_mode = options.remixMode;
980
- }
981
- }
982
- if (options.initialMessage) {
983
- body.initial_message = options.initialMessage;
984
- }
985
- const { data } = await this.typed.POST("/v1/projects", {
986
- body
987
- });
988
- const remix = data;
989
- if (!remix?.job_id) {
990
- throw new Error("Failed to get job ID from remix create");
991
- }
992
- return remix.job_id;
993
- }
994
- /**
995
- * Wait for a remix operation to complete.
996
- *
997
- * Polls the remix progress endpoint until the job reaches "completed" or "error" status.
998
- *
999
- * @param sourceProjectId - The source project ID (used for the progress endpoint)
1000
- * @param jobId - The job ID returned by `remixProject()`
1001
- * @param options.pollInterval - Time between polls in ms (default: 2000)
1002
- * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
1003
- * @param options.onProgress - Optional callback for status/step updates
1004
- * @returns The new project ID
1005
- * @throws Error if the remix fails or timeout is reached
1006
- */
1007
- async waitForRemix(sourceProjectId, jobId, options) {
1008
- const pollInterval = options?.pollInterval ?? 2e3;
1009
- const timeout = options?.timeout ?? 3e5;
1010
- const startTime = Date.now();
1011
- while (true) {
1012
- const { data } = await this.typed.GET("/v1/projects/{project_id}/remix/progress", {
1013
- params: {
1014
- path: { project_id: sourceProjectId },
1015
- query: { job_id: jobId }
1016
- }
1017
- });
1018
- const progress = data;
1019
- const status = progress.status;
1020
- options?.onProgress?.(status, progress.step);
1021
- if (status === "completed" && progress.result) {
1022
- return { projectId: progress.result.project_id };
1023
- }
1024
- if (status === "error") {
1025
- throw new Error(progress.error_message ?? "Remix failed");
1026
- }
1027
- if (Date.now() - startTime > timeout) {
1028
- throw new Error(`Timeout waiting for remix of project ${sourceProjectId}`);
1029
- }
1030
- await sleep2(pollInterval);
1031
- }
1032
- }
1033
- /**
1034
- * Wait for a project to reach "completed" status.
1035
- *
1036
- * Projects start in "in_progress" status while being created/built.
1037
- * This method polls until the status becomes "completed" or "failed".
1038
- * A successful completion means the project's preview is ready to view.
1039
- *
1040
- * @param projectId - The project ID to wait for
1041
- * @param options.pollInterval - Time between polls in ms (default: 2000)
1042
- * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
1043
- * @param options.onProgress - Optional callback for status updates
1044
- * @returns The completed project
1045
- * @throws Error if project fails or timeout is reached
1046
- */
1047
- async waitForProjectReady(projectId, options) {
1048
- const pollInterval = options?.pollInterval ?? 2e3;
1049
- const timeout = options?.timeout ?? 3e5;
1050
- const startTime = Date.now();
1051
- while (true) {
1052
- const project = await this.getProject(projectId);
1053
- options?.onProgress?.(project);
1054
- if (project.status === "completed") {
1055
- return project;
1056
- }
1057
- if (project.status === "failed") {
1058
- throw new Error(`Project ${projectId} failed to build`);
1059
- }
1060
- if (Date.now() - startTime > timeout) {
1061
- throw new Error(`Timeout waiting for project ${projectId} to be ready`);
1062
- }
1063
- await sleep2(pollInterval);
1064
- }
1065
- }
1066
- isFileInput(file) {
1067
- return "data" in file;
1068
- }
1069
- async uploadFile(file, getUploadUrl) {
1070
- const fileName = this.isFileInput(file) ? file.name : file.name;
1071
- const mimeType = this.isFileInput(file) ? file.type : file.type;
1072
- const body = this.isFileInput(file) ? file.data : file;
1073
- const uploadUrl = await getUploadUrl(fileName, mimeType);
1074
- const { url, file_id: objectPath, headers } = uploadUrl;
1075
- const uploadResponse = await fetch(url, {
1076
- method: "PUT",
1077
- body,
1078
- headers: { "Content-Type": mimeType, ...headers }
1079
- });
1080
- if (!uploadResponse.ok) {
1081
- throw new Error(`File upload failed for "${fileName}": HTTP ${uploadResponse.status}`);
1082
- }
1083
- return { file_id: objectPath, type: "user_upload", file_name: fileName, mime_type: mimeType };
1084
- }
1085
- async uploadProjectFiles(projectId, files) {
1086
- return Promise.all(
1087
- files.map(
1088
- (file) => this.uploadFile(
1089
- file,
1090
- (_fileName, mimeType) => this.getProjectFileUploadUrl(projectId, { content_type: mimeType })
1091
- )
1092
- )
1093
- );
1094
- }
1095
- async uploadEphemeralFiles(files) {
1096
- return Promise.all(
1097
- files.map(
1098
- (file) => this.uploadFile(file, (_fileName, mimeType) => this.getEphemeralFileUploadUrl({ content_type: mimeType }))
1099
- )
1100
- );
1101
- }
1102
- partitionUploadedFiles(files) {
1103
- const fileRefs = [];
1104
- const ephemeralFileRefs = [];
1105
- for (const file of files) {
1106
- if (file.file_id.startsWith("ephemeral/")) {
1107
- ephemeralFileRefs.push(file);
1108
- } else {
1109
- fileRefs.push(file);
1110
- }
1111
- }
1112
- return {
1113
- fileRefs: fileRefs.length > 0 ? fileRefs : void 0,
1114
- ephemeralFileRefs: ephemeralFileRefs.length > 0 ? ephemeralFileRefs : void 0
1115
- };
1116
- }
1117
- /**
1118
- * Wait for a project to be published (deployed).
1119
- *
1120
- * This method polls until the project has `is_published: true` and a `url`.
1121
- *
1122
- * @param projectId - The project ID to wait for
1123
- * @param options.pollInterval - Time between polls in ms (default: 3000)
1124
- * @param options.timeout - Maximum time to wait in ms (default: 600000 = 10 minutes)
1125
- * @param options.onProgress - Optional callback for status updates
1126
- * @returns The published project with URL
1127
- * @throws Error if timeout is reached
1128
- */
1129
- async waitForProjectPublished(projectId, options) {
1130
- const pollInterval = options?.pollInterval ?? 3e3;
1131
- const timeout = options?.timeout ?? 6e5;
1132
- const startTime = Date.now();
1133
- while (true) {
1134
- const project = await this.getProject(projectId);
1135
- options?.onProgress?.(project);
1136
- if (project.is_published && project.url) {
1137
- return project;
1138
- }
1139
- if (project.status === "failed") {
1140
- throw new Error(`Project ${projectId} failed to build`);
1141
- }
1142
- if (Date.now() - startTime > timeout) {
1143
- throw new Error(`Timeout waiting for project ${projectId} to be published`);
1144
- }
1145
- await sleep2(pollInterval);
1146
- }
1147
- }
135
+ authHeaders;
136
+ baseUrl;
137
+ extraHeaders;
138
+ clientSource;
139
+ typedClient;
140
+ constructor(options) {
141
+ const hasApiKey = !!options.apiKey;
142
+ const hasBearerToken = !!options.bearerToken;
143
+ if (!hasApiKey && !hasBearerToken) throw new Error("Either apiKey or bearerToken is required");
144
+ if (hasApiKey && hasBearerToken) throw new Error("Provide either apiKey or bearerToken, not both");
145
+ this.authHeaders = hasApiKey ? { "Lovable-API-Key": options.apiKey } : { Authorization: `Bearer ${options.bearerToken}` };
146
+ this.baseUrl = normalizeBaseUrl(options.baseUrl);
147
+ this.extraHeaders = options.headers ?? {};
148
+ this.clientSource = options.clientSource ?? "sdk";
149
+ this.typedClient = createClient({
150
+ baseUrl: this.baseUrl,
151
+ headers: {
152
+ "X-Client-Source": this.clientSource,
153
+ ...this.authHeaders,
154
+ ...this.extraHeaders,
155
+ Accept: "application/json"
156
+ },
157
+ fetch: makeRetryFetch()
158
+ });
159
+ this.typedClient.use(errorMiddleware);
160
+ }
161
+ /**
162
+ * Type-safe access to any documented API route, driven by the auto-generated
163
+ * OpenAPI schema. Path / query params and request bodies are checked at compile
164
+ * time; calling an unknown path is a type error.
165
+ *
166
+ * Non-2xx responses throw `ApiError`; on success, `data` is set on the result.
167
+ */
168
+ get typed() {
169
+ return this.typedClient;
170
+ }
171
+ /**
172
+ * Get the current authenticated user and their workspaces.
173
+ * Useful for validating an API key and discovering workspace IDs.
174
+ */
175
+ async me() {
176
+ const { data } = await this.typed.GET("/v1/me");
177
+ return {
178
+ ...data,
179
+ workspaces: normalizeWorkspaceList(data)
180
+ };
181
+ }
182
+ /**
183
+ * List workspaces the authenticated user has access to.
184
+ */
185
+ async listWorkspaces(options = {}) {
186
+ const { data } = await this.typed.GET("/v1/workspaces", { params: { query: options } });
187
+ return {
188
+ ...data,
189
+ workspaces: normalizeWorkspaceList(data)
190
+ };
191
+ }
192
+ /**
193
+ * Get a specific workspace by ID
194
+ */
195
+ async getWorkspace(workspaceId) {
196
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}", { params: { path: { workspace_id: workspaceId } } });
197
+ return data.workspace;
198
+ }
199
+ /**
200
+ * List projects in a workspace.
201
+ * Supports full-text search, filtering by visibility/publish status/folder/creator,
202
+ * and cursor pagination.
203
+ */
204
+ async listProjects(workspaceId, options) {
205
+ const { data } = await this.typed.GET("/v1/projects", { params: { query: {
206
+ workspace_id: workspaceId,
207
+ q: options?.query,
208
+ visibility: options?.visibility,
209
+ publish_status: options?.publish_status,
210
+ folder_id: options?.folder_id,
211
+ folder_ids: options?.folder_ids,
212
+ user_id: options?.user_id,
213
+ type: options?.type,
214
+ include_risk: options?.include_risk,
215
+ search_fields: options?.search_fields,
216
+ viewed_by_me: options?.viewed_by_me,
217
+ cursor: options?.cursor,
218
+ limit: options?.limit
219
+ } } });
220
+ const projects = data.projects ?? data.data ?? null;
221
+ const total = data.total;
222
+ return {
223
+ ...data,
224
+ projects,
225
+ ...total === void 0 ? {} : { total },
226
+ has_more: data.pagination?.has_more ?? data.has_more
227
+ };
228
+ }
229
+ /**
230
+ * Create a new project in a workspace
231
+ */
232
+ async createProject(workspaceId, options) {
233
+ let fileRefs;
234
+ let ephemeralFileRefs;
235
+ if (options.uploadedFiles?.length) ({fileRefs, ephemeralFileRefs} = this.partitionUploadedFiles(options.uploadedFiles));
236
+ else if (options.files?.length) ephemeralFileRefs = await this.uploadEphemeralFiles(options.files);
237
+ const body = {
238
+ description: options.description,
239
+ template_project_id: options.templateProjectId
240
+ };
241
+ if (options.projectName) body.display_name = options.projectName;
242
+ if (options.visibility) body.visibility = options.visibility;
243
+ if (options.techStack) body.tech_stack = options.techStack;
244
+ if (options.sandboxTemplate) body.sandbox_template = options.sandboxTemplate;
245
+ if (options.selectedLibraries?.length) body.selected_libraries = options.selectedLibraries;
246
+ if (options.initialMessage) body.initial_message = options.initialMessage;
247
+ if (fileRefs?.length) body.files = fileRefs;
248
+ if (options.fileUrls?.length) body.file_urls = options.fileUrls;
249
+ if (ephemeralFileRefs?.length) body.ephemeral_files = ephemeralFileRefs;
250
+ const { data } = await this.typed.POST("/v1/projects", { body: {
251
+ ...body,
252
+ workspace_id: workspaceId
253
+ } });
254
+ return requireCreateProjectId(data);
255
+ }
256
+ /**
257
+ * Send a chat message to a project.
258
+ *
259
+ * The API accepts the message and processes it in the background.
260
+ * Returns the message ID and status. Use `waitForMessageCompletion()`
261
+ * to poll for the AI response.
262
+ */
263
+ async chat(projectId, options) {
264
+ let fileRefs;
265
+ let ephemeralFileRefs;
266
+ if (options.uploadedFiles?.length) ({fileRefs, ephemeralFileRefs} = this.partitionUploadedFiles(options.uploadedFiles));
267
+ else if (options.files?.length) fileRefs = await this.uploadProjectFiles(projectId, options.files);
268
+ const body = { message: options.message };
269
+ if (options.variantId) body.variant_id = options.variantId;
270
+ if (fileRefs) body.files = fileRefs;
271
+ if (ephemeralFileRefs) body.ephemeral_files = ephemeralFileRefs;
272
+ if (options.planMode) body.plan_mode = true;
273
+ if (options.continuation) body.continuation = options.continuation;
274
+ const { data } = await this.typed.POST("/v1/messages", { body: {
275
+ ...body,
276
+ project_id: projectId
277
+ } });
278
+ return data;
279
+ }
280
+ /**
281
+ * Create an independent variant from the project's current main branch, or from a full baseSha when provided.
282
+ */
283
+ async createVariant(projectId, options = {}) {
284
+ const { data } = await this.typed.POST("/v1/projects/{project_id}/variants", {
285
+ params: { path: { project_id: projectId } },
286
+ body: {
287
+ label: options.label,
288
+ base_sha: options.baseSha
289
+ }
290
+ });
291
+ return data;
292
+ }
293
+ /**
294
+ * Get project details by ID
295
+ */
296
+ async getProject(projectId) {
297
+ const { data } = await this.typed.GET("/v1/projects/{project_id}", { params: { path: { project_id: projectId } } });
298
+ return data;
299
+ }
300
+ /**
301
+ * Create an anonymous, one-hour static preview URL for one exact HTTPS parent origin.
302
+ */
303
+ async createEmbedUrl(projectId, parentOrigin) {
304
+ const { data } = await this.typed.POST("/v1/projects/{project_id}/embed-url", {
305
+ params: { path: { project_id: projectId } },
306
+ body: { parent_origin: parentOrigin }
307
+ });
308
+ return data;
309
+ }
310
+ /**
311
+ * Update supported project fields.
312
+ */
313
+ async updateProject(projectId, options) {
314
+ const { data } = await this.typed.PATCH("/v1/projects/{project_id}", {
315
+ params: { path: { project_id: projectId } },
316
+ body: options
317
+ });
318
+ return data;
319
+ }
320
+ /**
321
+ * Soft-delete a project. Repeated deletes are treated as successful.
322
+ */
323
+ async deleteProject(projectId) {
324
+ await this.typed.DELETE("/v1/projects/{project_id}", { params: { path: { project_id: projectId } } });
325
+ }
326
+ /**
327
+ * Get the preview URL for a project.
328
+ *
329
+ * The preview URL is available once the project reaches "completed" status.
330
+ * This URL allows viewing the project in development mode.
331
+ *
332
+ * @param projectId - The project ID
333
+ * @returns The preview URL
334
+ */
335
+ getPreviewUrl(projectId) {
336
+ return `https://id-preview--${projectId}.lovable.app`;
337
+ }
338
+ /**
339
+ * Get the published URL for a project (if published).
340
+ *
341
+ * Returns the public URL if the project has been published, or null if not.
342
+ *
343
+ * @param projectId - The project ID
344
+ * @returns The published URL or null if not published
345
+ */
346
+ async getPublishedUrl(projectId) {
347
+ const project = await this.getProject(projectId);
348
+ return project.is_published && project.url ? project.url : null;
349
+ }
350
+ /**
351
+ * Get the cloud database status for a project.
352
+ *
353
+ * @param projectId - The project ID
354
+ * @returns Whether the database is enabled and which stack is used
355
+ */
356
+ async getDatabaseStatus(projectId) {
357
+ const { data } = await this.typed.GET("/v1/database", { params: { query: { project_id: projectId } } });
358
+ return data;
359
+ }
360
+ /**
361
+ * Enable (provision) a cloud database for a project.
362
+ *
363
+ * This triggers database provisioning which takes 30-60 seconds.
364
+ * The call blocks until provisioning completes.
365
+ *
366
+ * @param projectId - The project ID
367
+ * @returns The database status after enablement
368
+ */
369
+ async enableDatabase(projectId) {
370
+ const { data } = await this.typed.POST("/v1/database/enable", { body: { project_id: projectId } });
371
+ return data;
372
+ }
373
+ /**
374
+ * Execute a SQL query against the project's cloud database.
375
+ *
376
+ * Supports SELECT, INSERT, UPDATE, DELETE, and DDL statements.
377
+ * The database must be enabled first (see enableDatabase).
378
+ *
379
+ * @param projectId - The project ID
380
+ * @param sql - SQL query to execute
381
+ * @returns Query result rows as JSON objects
382
+ */
383
+ async queryDatabase(projectId, sql) {
384
+ const { data } = await this.typedClient.POST("/v1/database/query", { body: {
385
+ project_id: projectId,
386
+ sql
387
+ } });
388
+ return data;
389
+ }
390
+ /**
391
+ * Get a message by ID. Returns the message content, status, and (for user messages)
392
+ * the AI response if available.
393
+ *
394
+ * Pass `waitSeconds` to long-poll: the server holds the request until the
395
+ * message reaches a terminal state (completed / stopped / error / awaiting_input) or the
396
+ * duration elapses. This replaces client-side polling for `waitForMessageCompletion`.
397
+ */
398
+ async getMessage(projectId, messageId, options) {
399
+ const waitSeconds = options?.waitSeconds;
400
+ const query = {
401
+ wait: waitSeconds && waitSeconds > 0 ? `${Math.floor(waitSeconds)}s` : void 0,
402
+ thread_id: options?.threadId
403
+ };
404
+ const { data } = await this.typed.GET("/v1/messages/{message_id}", { params: {
405
+ path: { message_id: messageId },
406
+ query: {
407
+ ...query,
408
+ project_id: projectId
409
+ }
410
+ } });
411
+ return data;
412
+ }
413
+ /**
414
+ * List recent messages in a project, newest first. Use `cursor` from the
415
+ * previous page's `pagination.next_cursor` to paginate through history.
416
+ */
417
+ async listMessages(projectId, params) {
418
+ const cursor = params?.cursor ?? (params?.before ? encodePublicCursor(params.before) : void 0);
419
+ const { data } = await this.typed.GET("/v1/messages", { params: { query: {
420
+ project_id: projectId,
421
+ limit: params?.limit,
422
+ cursor
423
+ } } });
424
+ return {
425
+ ...data,
426
+ messages: data.data,
427
+ has_more: cursorHasMore(data)
428
+ };
429
+ }
430
+ /**
431
+ * Wait for the AI response to reach a terminal status (completed, stopped, error, awaiting_input)
432
+ * or for `timeout` to elapse.
433
+ *
434
+ * Primary path is SSE against `/v1/messages/{message_id}/stream`: one
435
+ * held connection that pushes a snapshot on every relevant change and closes
436
+ * on terminal. If SSE isn't reachable (proxy strips text/event-stream, server
437
+ * returns 404 / 415 / 501) we fall back to the long-poll JSON endpoint on the
438
+ * same URL. Both paths share the same `MessageCompletionResult` shape.
439
+ */
440
+ async waitForMessageCompletion(projectId, messageId, options) {
441
+ const timeout = options?.timeout ?? 6e5;
442
+ const deadline = Date.now() + timeout;
443
+ const sse = await this.waitForMessageCompletionViaSSE(projectId, messageId, deadline, options?.threadId);
444
+ if (sse.kind === "result") return sse.result;
445
+ if (Date.now() >= deadline) return timeoutResult(messageId, timeout);
446
+ return this.waitForMessageCompletionViaLongPoll(projectId, messageId, deadline, timeout, options);
447
+ }
448
+ /**
449
+ * @deprecated Use `chat()` or `createProject()`'s returned `message_id`,
450
+ * then call `waitForMessageCompletion(projectId, messageId)`.
451
+ *
452
+ * Throws when the turn pauses for human input (`awaiting_input`) — the
453
+ * legacy `ChatResponse` shape cannot carry resume metadata. HITL-capable
454
+ * flows need `waitForMessageCompletion` plus `respondToTool`.
455
+ */
456
+ async waitForResponse(projectId, options) {
457
+ const latest = (await this.listMessages(projectId, { limit: 10 })).messages?.find((message) => message.role === "user");
458
+ if (!latest?.message_id) throw new Error(`No messages found for project ${projectId}`);
459
+ const completionOptions = options?.timeout === void 0 ? void 0 : { timeout: options.timeout };
460
+ const result = await this.waitForMessageCompletion(projectId, latest.message_id, completionOptions);
461
+ if (result.status !== "completed" && result.status !== "stopped") throw new Error(result.error ?? `Message ${latest.message_id} did not complete (status: ${result.status})`);
462
+ return {
463
+ content: result.content,
464
+ messageId: result.message_id,
465
+ previewUrl: this.getPreviewUrl(projectId)
466
+ };
467
+ }
468
+ async waitForMessageCompletionViaSSE(projectId, messageId, deadline, threadId) {
469
+ const remaining = deadline - Date.now();
470
+ if (remaining <= 0) return { kind: "timeout" };
471
+ const url = new URL(`${this.baseUrl}/v1/messages/${encodeURIComponent(messageId)}/stream`);
472
+ url.searchParams.set("project_id", projectId);
473
+ if (threadId) url.searchParams.set("thread_id", threadId);
474
+ const controller = new AbortController();
475
+ const timeoutId = setTimeout(() => controller.abort(), remaining);
476
+ let response;
477
+ try {
478
+ response = await fetch(url, {
479
+ headers: {
480
+ ...this.authHeaders,
481
+ ...this.extraHeaders,
482
+ "X-Client-Source": this.clientSource,
483
+ Accept: "text/event-stream"
484
+ },
485
+ signal: controller.signal
486
+ });
487
+ } catch (err) {
488
+ clearTimeout(timeoutId);
489
+ if (isAbortError(err)) return { kind: "timeout" };
490
+ return { kind: "fallback" };
491
+ }
492
+ if (!response.ok) {
493
+ clearTimeout(timeoutId);
494
+ if (response.status === 404 || response.status === 415 || response.status === 501) {
495
+ await cancelResponseBody(response);
496
+ return { kind: "fallback" };
497
+ }
498
+ if (response.status >= 500) {
499
+ await cancelResponseBody(response);
500
+ return { kind: "fallback" };
501
+ }
502
+ const detail = await safeReadText(response);
503
+ throw new ApiError(response.status, detail || `HTTP ${response.status}`);
504
+ }
505
+ if (!response.body) {
506
+ clearTimeout(timeoutId);
507
+ return { kind: "fallback" };
508
+ }
509
+ try {
510
+ for await (const frame of parseSSEFrames(response.body)) {
511
+ if (!frame.data) continue;
512
+ let snapshot;
513
+ try {
514
+ snapshot = JSON.parse(frame.data);
515
+ } catch {
516
+ continue;
517
+ }
518
+ const queuedResult = queuedExitResult(snapshot, messageId);
519
+ if (queuedResult) return {
520
+ kind: "result",
521
+ result: queuedResult
522
+ };
523
+ const terminal = terminalResultFromMessage(snapshot, messageId);
524
+ if (terminal) return {
525
+ kind: "result",
526
+ result: terminal
527
+ };
528
+ }
529
+ return { kind: "fallback" };
530
+ } catch (err) {
531
+ if (isAbortError(err)) return { kind: "timeout" };
532
+ return { kind: "fallback" };
533
+ } finally {
534
+ clearTimeout(timeoutId);
535
+ controller.abort();
536
+ }
537
+ }
538
+ async waitForMessageCompletionViaLongPoll(projectId, messageId, deadline, totalTimeoutMs, options) {
539
+ const waitSeconds = Math.max(1, Math.min(55, options?.waitSeconds ?? 30));
540
+ const transientBackoffMs = 1e3;
541
+ let notFoundSince = null;
542
+ const notFoundGraceMs = 15e3;
543
+ while (Date.now() < deadline) {
544
+ const remainingMs = deadline - Date.now();
545
+ const perCallSeconds = Math.min(waitSeconds, Math.floor(remainingMs / 1e3));
546
+ const waitOptions = perCallSeconds > 0 ? { waitSeconds: perCallSeconds } : void 0;
547
+ const callStartedAt = Date.now();
548
+ try {
549
+ const msg = await this.getMessage(projectId, messageId, {
550
+ ...waitOptions,
551
+ threadId: options?.threadId
552
+ });
553
+ notFoundSince = null;
554
+ const queuedResult = queuedExitResult(msg, messageId);
555
+ if (queuedResult) return queuedResult;
556
+ const terminal = terminalResultFromMessage(msg, messageId);
557
+ if (terminal) return terminal;
558
+ if (Date.now() - callStartedAt < transientBackoffMs) await sleep(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
559
+ } catch (err) {
560
+ if (err instanceof ApiError) {
561
+ if (err.status === 404) {
562
+ if (notFoundSince === null) notFoundSince = Date.now();
563
+ if (Date.now() - notFoundSince < notFoundGraceMs) {
564
+ await sleep(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
565
+ continue;
566
+ }
567
+ return {
568
+ status: "error",
569
+ message_id: messageId,
570
+ content: "",
571
+ error: "Message not found. It may have been deleted from the queue."
572
+ };
573
+ }
574
+ if (err.status < 500) throw err;
575
+ }
576
+ await sleep(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
577
+ }
578
+ }
579
+ return timeoutResult(messageId, totalTimeoutMs);
580
+ }
581
+ /** Get workspace knowledge (custom instructions for the AI agent). */
582
+ async getWorkspaceKnowledge(workspaceId) {
583
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/knowledge", { params: { path: { workspace_id: workspaceId } } });
584
+ return data;
585
+ }
586
+ /** Set workspace knowledge. Max 10,000 characters. */
587
+ async setWorkspaceKnowledge(workspaceId, content) {
588
+ const { data } = await this.typed.PUT("/v1/workspaces/{workspace_id}/knowledge", {
589
+ params: { path: { workspace_id: workspaceId } },
590
+ body: { content }
591
+ });
592
+ return data;
593
+ }
594
+ /** List workspace skills. */
595
+ async listWorkspaceSkills(workspaceId, options = {}) {
596
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/skills", { params: {
597
+ path: { workspace_id: workspaceId },
598
+ query: {
599
+ include_markdown: options.includeMarkdown,
600
+ limit: options.limit,
601
+ offset: options.offset
602
+ }
603
+ } });
604
+ return {
605
+ ...data,
606
+ skills: data.skills ?? []
607
+ };
608
+ }
609
+ /** Get a single workspace skill, including SKILL.md contents. */
610
+ async getWorkspaceSkill(workspaceId, skillName) {
611
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/skills/{skill_name}", { params: { path: {
612
+ workspace_id: workspaceId,
613
+ skill_name: skillName
614
+ } } });
615
+ return data;
616
+ }
617
+ /** Create a workspace skill from full SKILL.md markdown. */
618
+ async createWorkspaceSkill(workspaceId, skillName, markdown) {
619
+ const { data } = await this.typed.POST("/v1/workspaces/{workspace_id}/skills/{skill_name}", {
620
+ params: { path: {
621
+ workspace_id: workspaceId,
622
+ skill_name: skillName
623
+ } },
624
+ body: { markdown }
625
+ });
626
+ return data;
627
+ }
628
+ /** Update a workspace skill by replacing its SKILL.md markdown. */
629
+ async updateWorkspaceSkill(workspaceId, skillName, markdown) {
630
+ const { data } = await this.typed.PUT("/v1/workspaces/{workspace_id}/skills/{skill_name}", {
631
+ params: { path: {
632
+ workspace_id: workspaceId,
633
+ skill_name: skillName
634
+ } },
635
+ body: { markdown }
636
+ });
637
+ return data;
638
+ }
639
+ /** Delete a workspace skill. */
640
+ async deleteWorkspaceSkill(workspaceId, skillName) {
641
+ const { data } = await this.typed.DELETE("/v1/workspaces/{workspace_id}/skills/{skill_name}", { params: { path: {
642
+ workspace_id: workspaceId,
643
+ skill_name: skillName
644
+ } } });
645
+ return data;
646
+ }
647
+ /** List project skills, including whether each skill is enabled. */
648
+ async listProjectSkills(projectId, options = {}) {
649
+ const { data } = await this.typed.GET("/v1/skills", { params: { query: {
650
+ project_id: projectId,
651
+ limit: options.limit,
652
+ cursor: options.cursor
653
+ } } });
654
+ const skills = data.skills ?? data.data ?? [];
655
+ return {
656
+ ...data,
657
+ skills
658
+ };
659
+ }
660
+ /** Enable or disable a project skill without removing it from the project repo. */
661
+ async setProjectSkillEnabled(projectId, skillName, enabled) {
662
+ const { data } = await this.typed.PATCH("/v1/skills/{skill_name}", {
663
+ params: { path: { skill_name: skillName } },
664
+ body: {
665
+ project_id: projectId,
666
+ enabled
667
+ }
668
+ });
669
+ return data;
670
+ }
671
+ /** Get project knowledge (custom instructions for the AI agent). */
672
+ async getProjectKnowledge(projectId) {
673
+ const { data } = await this.typed.GET("/v1/knowledge", { params: { query: { project_id: projectId } } });
674
+ return data;
675
+ }
676
+ /** Set project knowledge. Max 10,000 characters. */
677
+ async setProjectKnowledge(projectId, content) {
678
+ const { data } = await this.typed.PUT("/v1/knowledge", { body: {
679
+ project_id: projectId,
680
+ content
681
+ } });
682
+ return data;
683
+ }
684
+ /**
685
+ * Get the structured diff for a message or commit.
686
+ * Pass `messageId` to get the diff for a specific AI message,
687
+ * or `sha` for a specific commit.
688
+ */
689
+ async getDiff(projectId, params) {
690
+ const { data } = await this.typed.GET("/v1/git/diff", { params: { query: {
691
+ project_id: projectId,
692
+ message_id: params.messageId,
693
+ sha: params.sha,
694
+ base_sha: params.baseSha
695
+ } } });
696
+ return data;
697
+ }
698
+ async listFiles(projectId, refOrOptions, options = {}) {
699
+ const ref = typeof refOrOptions === "string" ? refOrOptions : void 0;
700
+ const pagination = typeof refOrOptions === "string" ? options : refOrOptions ?? options;
701
+ const { data } = await this.typed.GET("/v1/git/files", { params: { query: {
702
+ project_id: projectId,
703
+ ref,
704
+ limit: pagination.limit,
705
+ cursor: pagination.cursor
706
+ } } });
707
+ const files = data.data ?? [];
708
+ return {
709
+ ...data,
710
+ data: files,
711
+ files
712
+ };
713
+ }
714
+ /** Read the raw content of a single file. Omitting ref uses the API default. */
715
+ async readFile(projectId, path, ref) {
716
+ const { data } = await this.typed.GET("/v1/git/files/{path}", {
717
+ params: {
718
+ path: { path },
719
+ query: {
720
+ project_id: projectId,
721
+ ref
722
+ }
723
+ },
724
+ parseAs: "text"
725
+ });
726
+ return data;
727
+ }
728
+ /** List the edit history of a project. */
729
+ async listEdits(projectId, params) {
730
+ const { data } = await this.typed.GET("/v1/edits", { params: { query: {
731
+ project_id: projectId,
732
+ limit: params?.limit,
733
+ before: params?.before
734
+ } } });
735
+ return data;
736
+ }
737
+ /** Get an ephemeral presigned URL for uploading a file before a project exists. */
738
+ async getFileUploadUrl(params) {
739
+ return this.getEphemeralFileUploadUrl({ content_type: params.content_type });
740
+ }
741
+ /** Get a project-scoped presigned URL for uploading a file. Returns the upload URL, file ID, and required PUT headers. */
742
+ async getProjectFileUploadUrl(projectId, params) {
743
+ const { data } = await this.typed.POST("/v1/project-files/upload-url", { body: {
744
+ project_id: projectId,
745
+ ...params
746
+ } });
747
+ return data;
748
+ }
749
+ /** Get an ephemeral presigned URL for uploading a file before a project exists. */
750
+ async getEphemeralFileUploadUrl(params) {
751
+ const { data } = await this.typed.POST("/v1/files/ephemeral-upload-url", { body: params });
752
+ return data;
753
+ }
754
+ /** Set a project's visibility (draft, private, workspace_view, or public). */
755
+ async setProjectVisibility(projectId, visibility) {
756
+ return this.updateProject(projectId, { visibility });
757
+ }
758
+ /** Set a folder's visibility (personal or workspace). */
759
+ async setFolderVisibility(workspaceId, folderId, visibility) {
760
+ const { data } = await this.typed.PUT("/v1/workspaces/{workspace_id}/folders/{folder_id}/visibility", {
761
+ params: { path: {
762
+ workspace_id: workspaceId,
763
+ folder_id: folderId
764
+ } },
765
+ body: { visibility }
766
+ });
767
+ return data;
768
+ }
769
+ /** Move projects into a folder, removing existing folder memberships first. */
770
+ async moveProjectsToFolder(workspaceId, folderId, projectIds) {
771
+ const { data } = await this.typed.POST("/v1/workspaces/{workspace_id}/folders/{folder_id}/projects/move", {
772
+ params: { path: {
773
+ workspace_id: workspaceId,
774
+ folder_id: folderId
775
+ } },
776
+ body: { project_ids: projectIds }
777
+ });
778
+ return data;
779
+ }
780
+ /** List available design system library projects in a workspace. */
781
+ async listLibraryProjects(workspaceId, options = {}) {
782
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/available-library-projects", { params: {
783
+ path: { workspace_id: workspaceId },
784
+ query: options
785
+ } });
786
+ return {
787
+ ...data,
788
+ libraries: data.libraries ?? []
789
+ };
790
+ }
791
+ /** List available template projects in a workspace. */
792
+ async listTemplateProjects(workspaceId, options = {}) {
793
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}/available-template-projects", { params: {
794
+ path: { workspace_id: workspaceId },
795
+ query: options
796
+ } });
797
+ return {
798
+ ...data,
799
+ templates: data.templates ?? []
800
+ };
801
+ }
802
+ /** List all connectors in a workspace. */
803
+ async listConnectors(workspaceId, options = {}) {
804
+ const { data } = await this.typed.GET("/v1/connectors", { params: { query: {
805
+ workspace_id: workspaceId,
806
+ type: options.type,
807
+ status: options.status,
808
+ limit: options.limit,
809
+ cursor: options.cursor
810
+ } } });
811
+ const connectors = data.connectors ?? data.data ?? [];
812
+ return {
813
+ ...data,
814
+ data: connectors,
815
+ connectors,
816
+ has_more: cursorHasMore(data)
817
+ };
818
+ }
819
+ /** Add a connector to a workspace. The server URL is tested before saving. */
820
+ async addConnector(workspaceId, body) {
821
+ const { data } = await this.typed.POST("/v1/connectors", { body: {
822
+ ...body,
823
+ workspace_id: workspaceId
824
+ } });
825
+ return data;
826
+ }
827
+ /** Remove a connector from a workspace. */
828
+ async removeConnector(workspaceId, connectorId) {
829
+ const { data } = await this.typed.DELETE("/v1/connectors/{connector_id}", { params: {
830
+ path: { connector_id: connectorId },
831
+ query: { workspace_id: workspaceId }
832
+ } });
833
+ return data;
834
+ }
835
+ /** Browse available connector templates. */
836
+ async listAvailableConnectors(workspaceId, options = {}) {
837
+ const { data } = await this.typed.GET("/v1/available-connectors", { params: { query: {
838
+ workspace_id: workspaceId,
839
+ limit: options.limit,
840
+ cursor: options.cursor
841
+ } } });
842
+ const catalog = data.catalog ?? data.data ?? [];
843
+ return {
844
+ ...data,
845
+ data: catalog,
846
+ catalog,
847
+ has_more: cursorHasMore(data)
848
+ };
849
+ }
850
+ /** List standard (OAuth-based) connectors in a workspace. */
851
+ async listStandardConnectors(workspaceId, options = {}) {
852
+ const { data } = await this.typed.GET("/v1/connectors", { params: { query: {
853
+ workspace_id: workspaceId,
854
+ type: "standard",
855
+ limit: options.limit,
856
+ cursor: options.cursor
857
+ } } });
858
+ const connectors = data.connectors ?? data.data ?? [];
859
+ return {
860
+ ...data,
861
+ data: connectors,
862
+ connectors,
863
+ has_more: cursorHasMore(data)
864
+ };
865
+ }
866
+ /** List seamless (zero-config) connectors in a workspace. */
867
+ async listSeamlessConnectors(workspaceId, options = {}) {
868
+ const { data } = await this.typed.GET("/v1/connectors", { params: { query: {
869
+ workspace_id: workspaceId,
870
+ type: "seamless",
871
+ limit: options.limit,
872
+ cursor: options.cursor
873
+ } } });
874
+ const connectors = data.connectors ?? data.data ?? [];
875
+ return {
876
+ ...data,
877
+ data: connectors,
878
+ connectors,
879
+ has_more: cursorHasMore(data)
880
+ };
881
+ }
882
+ /** List MCP connectors in a workspace. */
883
+ async listMCPConnectors(workspaceId, options = {}) {
884
+ const { data } = await this.typed.GET("/v1/connectors", { params: { query: {
885
+ workspace_id: workspaceId,
886
+ type: "mcp",
887
+ limit: options.limit,
888
+ cursor: options.cursor
889
+ } } });
890
+ const connectors = data.connectors ?? data.data ?? [];
891
+ return {
892
+ ...data,
893
+ data: connectors,
894
+ connectors,
895
+ has_more: cursorHasMore(data)
896
+ };
897
+ }
898
+ /** List authenticated connections (accounts) in a workspace. */
899
+ async listConnections(workspaceId, params = {}) {
900
+ const { data } = await this.typed.GET("/v1/connections", { params: { query: {
901
+ workspace_id: workspaceId,
902
+ connector_id: params.connector_id,
903
+ limit: params.limit,
904
+ cursor: params.cursor
905
+ } } });
906
+ const connections = data.data ?? [];
907
+ return {
908
+ ...data,
909
+ data: connections,
910
+ connections,
911
+ has_more: cursorHasMore(data)
912
+ };
913
+ }
914
+ /** Get historical analytics for a published project. */
915
+ async getProjectAnalytics(projectId, params) {
916
+ const { data } = await this.typed.GET("/v1/analytics", { params: { query: {
917
+ project_id: projectId,
918
+ startDate: params.startDate,
919
+ endDate: params.endDate,
920
+ granularity: params.granularity
921
+ } } });
922
+ return data;
923
+ }
924
+ /** Get real-time visitor trend for a published project. */
925
+ async getProjectAnalyticsTrend(projectId) {
926
+ const { data } = await this.typed.GET("/v1/analytics/trend", { params: { query: { project_id: projectId } } });
927
+ return data;
928
+ }
929
+ /**
930
+ * Publish a project.
931
+ *
932
+ * This triggers a deployment which makes the project publicly accessible.
933
+ * The deployment runs asynchronously - use waitForProjectPublished() to wait for completion.
934
+ *
935
+ * @param projectId - The project ID to publish
936
+ * @param options.name - Optional custom slug for the published URL
937
+ * @returns Deployment info including deployment ID
938
+ */
939
+ async publish(projectId, options) {
940
+ const { data } = await this.typed.POST("/v1/deployments", { body: {
941
+ project_id: projectId,
942
+ name: options?.name
943
+ } });
944
+ return data;
945
+ }
946
+ /**
947
+ * Remix (fork) an existing project, optionally at a specific message point in time.
948
+ *
949
+ * When `messageId` is provided, the remix captures the project state after
950
+ * that message and its AI response by default. Set `remixMode: "before"` to
951
+ * start before the message was processed.
952
+ * Without `messageId`, the full current state is remixed.
953
+ *
954
+ * @param sourceProjectId - The project to remix from
955
+ * @param options.workspaceId - Target workspace for the new project
956
+ * @param options.messageId - Optional message ID to snapshot at
957
+ * @param options.remixMode - "including" (server default): state after the message and its AI response; "before": state before the message
958
+ * @param options.includeHistory - Whether to preserve chat history (default: false)
959
+ * @param options.includeCustomKnowledge - Whether to copy custom instructions (default: false)
960
+ * @param options.initialMessage - Optional initial message to send after remix
961
+ * @param options.description - Optional custom description for the new project
962
+ * @returns The remix job ID for polling progress
963
+ */
964
+ async remixProject(sourceProjectId, options) {
965
+ const body = {
966
+ workspace_id: options.workspaceId,
967
+ source_project_id: sourceProjectId,
968
+ include_history: options.includeHistory,
969
+ include_custom_knowledge: options.includeCustomKnowledge,
970
+ description: options.description,
971
+ display_name: options.projectName,
972
+ skip_initial_remix_message: options.skipInitialRemixMessage,
973
+ skip_integrations: options.skipIntegrations
974
+ };
975
+ if (options.messageId) {
976
+ body.message_id = options.messageId;
977
+ if (options.remixMode) body.remix_mode = options.remixMode;
978
+ }
979
+ if (options.initialMessage) body.initial_message = options.initialMessage;
980
+ const { data } = await this.typed.POST("/v1/projects", { body });
981
+ const remix = data;
982
+ if (!remix?.job_id) throw new Error("Failed to get job ID from remix create");
983
+ return remix.job_id;
984
+ }
985
+ /**
986
+ * Wait for a remix operation to complete.
987
+ *
988
+ * Polls the remix progress endpoint until the job reaches "completed" or "error" status.
989
+ *
990
+ * @param sourceProjectId - The source project ID (used for the progress endpoint)
991
+ * @param jobId - The job ID returned by `remixProject()`
992
+ * @param options.pollInterval - Time between polls in ms (default: 2000)
993
+ * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
994
+ * @param options.onProgress - Optional callback for status/step updates
995
+ * @returns The new project ID
996
+ * @throws Error if the remix fails or timeout is reached
997
+ */
998
+ async waitForRemix(sourceProjectId, jobId, options) {
999
+ const pollInterval = options?.pollInterval ?? 2e3;
1000
+ const timeout = options?.timeout ?? 3e5;
1001
+ const startTime = Date.now();
1002
+ while (true) {
1003
+ const { data } = await this.typed.GET("/v1/projects/{project_id}/remix/progress", { params: {
1004
+ path: { project_id: sourceProjectId },
1005
+ query: { job_id: jobId }
1006
+ } });
1007
+ const progress = data;
1008
+ const status = progress.status;
1009
+ options?.onProgress?.(status, progress.step);
1010
+ if (status === "completed" && progress.result) return { projectId: progress.result.project_id };
1011
+ if (status === "error") throw new Error(progress.error_message ?? "Remix failed");
1012
+ if (Date.now() - startTime > timeout) throw new Error(`Timeout waiting for remix of project ${sourceProjectId}`);
1013
+ await sleep(pollInterval);
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Wait for a project to reach "completed" status.
1018
+ *
1019
+ * Projects start in "in_progress" status while being created/built.
1020
+ * This method polls until the status becomes "completed" or "failed".
1021
+ * A successful completion means the project's preview is ready to view.
1022
+ *
1023
+ * @param projectId - The project ID to wait for
1024
+ * @param options.pollInterval - Time between polls in ms (default: 2000)
1025
+ * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
1026
+ * @param options.onProgress - Optional callback for status updates
1027
+ * @returns The completed project
1028
+ * @throws Error if project fails or timeout is reached
1029
+ */
1030
+ async waitForProjectReady(projectId, options) {
1031
+ const pollInterval = options?.pollInterval ?? 2e3;
1032
+ const timeout = options?.timeout ?? 3e5;
1033
+ const startTime = Date.now();
1034
+ while (true) {
1035
+ const project = await this.getProject(projectId);
1036
+ options?.onProgress?.(project);
1037
+ if (project.status === "completed") return project;
1038
+ if (project.status === "failed") throw new Error(`Project ${projectId} failed to build`);
1039
+ if (Date.now() - startTime > timeout) throw new Error(`Timeout waiting for project ${projectId} to be ready`);
1040
+ await sleep(pollInterval);
1041
+ }
1042
+ }
1043
+ isFileInput(file) {
1044
+ return "data" in file;
1045
+ }
1046
+ async uploadFile(file, getUploadUrl) {
1047
+ const fileName = this.isFileInput(file) ? file.name : file.name;
1048
+ const mimeType = this.isFileInput(file) ? file.type : file.type;
1049
+ const body = this.isFileInput(file) ? file.data : file;
1050
+ const { url, file_id: objectPath, headers } = await getUploadUrl(fileName, mimeType);
1051
+ const uploadResponse = await fetch(url, {
1052
+ method: "PUT",
1053
+ body,
1054
+ headers: {
1055
+ "Content-Type": mimeType,
1056
+ ...headers
1057
+ }
1058
+ });
1059
+ if (!uploadResponse.ok) throw new Error(`File upload failed for "${fileName}": HTTP ${uploadResponse.status}`);
1060
+ return {
1061
+ file_id: objectPath,
1062
+ type: "user_upload",
1063
+ file_name: fileName,
1064
+ mime_type: mimeType
1065
+ };
1066
+ }
1067
+ async uploadProjectFiles(projectId, files) {
1068
+ return Promise.all(files.map((file) => this.uploadFile(file, (_fileName, mimeType) => this.getProjectFileUploadUrl(projectId, { content_type: mimeType }))));
1069
+ }
1070
+ async uploadEphemeralFiles(files) {
1071
+ return Promise.all(files.map((file) => this.uploadFile(file, (_fileName, mimeType) => this.getEphemeralFileUploadUrl({ content_type: mimeType }))));
1072
+ }
1073
+ partitionUploadedFiles(files) {
1074
+ const fileRefs = [];
1075
+ const ephemeralFileRefs = [];
1076
+ for (const file of files) if (file.file_id.startsWith("ephemeral/")) ephemeralFileRefs.push(file);
1077
+ else fileRefs.push(file);
1078
+ return {
1079
+ fileRefs: fileRefs.length > 0 ? fileRefs : void 0,
1080
+ ephemeralFileRefs: ephemeralFileRefs.length > 0 ? ephemeralFileRefs : void 0
1081
+ };
1082
+ }
1083
+ /**
1084
+ * Wait for a project to be published (deployed).
1085
+ *
1086
+ * This method polls until the project has `is_published: true` and a `url`.
1087
+ *
1088
+ * @param projectId - The project ID to wait for
1089
+ * @param options.pollInterval - Time between polls in ms (default: 3000)
1090
+ * @param options.timeout - Maximum time to wait in ms (default: 600000 = 10 minutes)
1091
+ * @param options.onProgress - Optional callback for status updates
1092
+ * @returns The published project with URL
1093
+ * @throws Error if timeout is reached
1094
+ */
1095
+ async waitForProjectPublished(projectId, options) {
1096
+ const pollInterval = options?.pollInterval ?? 3e3;
1097
+ const timeout = options?.timeout ?? 6e5;
1098
+ const startTime = Date.now();
1099
+ while (true) {
1100
+ const project = await this.getProject(projectId);
1101
+ options?.onProgress?.(project);
1102
+ if (project.is_published && project.url) return project;
1103
+ if (project.status === "failed") throw new Error(`Project ${projectId} failed to build`);
1104
+ if (Date.now() - startTime > timeout) throw new Error(`Timeout waiting for project ${projectId} to be published`);
1105
+ await sleep(pollInterval);
1106
+ }
1107
+ }
1148
1108
  };
1149
- function sleep2(ms) {
1150
- return new Promise((resolve) => setTimeout(resolve, ms));
1109
+ function sleep(ms) {
1110
+ return new Promise((resolve) => setTimeout(resolve, ms));
1151
1111
  }
1152
1112
  function isAbortError(err) {
1153
- return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
1113
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
1154
1114
  }
1155
1115
  async function safeReadText(response) {
1156
- try {
1157
- return await response.text();
1158
- } catch {
1159
- return "";
1160
- }
1116
+ try {
1117
+ return await response.text();
1118
+ } catch {
1119
+ return "";
1120
+ }
1161
1121
  }
1162
1122
  async function cancelResponseBody(response) {
1163
- try {
1164
- await response.body?.cancel();
1165
- } catch {
1166
- }
1123
+ try {
1124
+ await response.body?.cancel();
1125
+ } catch {}
1167
1126
  }
1168
1127
  async function* parseSSEFrames(body) {
1169
- const reader = body.getReader();
1170
- const decoder = new TextDecoder();
1171
- let buffer = "";
1172
- try {
1173
- while (true) {
1174
- const { done, value } = await reader.read();
1175
- if (done) break;
1176
- buffer += decoder.decode(value, { stream: true });
1177
- let sep = buffer.indexOf("\n\n");
1178
- while (sep !== -1) {
1179
- const raw = buffer.slice(0, sep);
1180
- buffer = buffer.slice(sep + 2);
1181
- sep = buffer.indexOf("\n\n");
1182
- let event = "message";
1183
- const dataLines = [];
1184
- for (const line of raw.split("\n")) {
1185
- if (!line || line.startsWith(":")) continue;
1186
- if (line.startsWith("event:")) {
1187
- event = line.slice(6).trimStart();
1188
- } else if (line.startsWith("data:")) {
1189
- dataLines.push(line.slice(5).trimStart());
1190
- }
1191
- }
1192
- if (dataLines.length === 0) continue;
1193
- yield { event, data: dataLines.join("\n") };
1194
- }
1195
- }
1196
- } finally {
1197
- try {
1198
- reader.releaseLock();
1199
- } catch {
1200
- }
1201
- try {
1202
- await body.cancel();
1203
- } catch {
1204
- }
1205
- }
1128
+ const reader = body.getReader();
1129
+ const decoder = new TextDecoder();
1130
+ let buffer = "";
1131
+ try {
1132
+ while (true) {
1133
+ const { done, value } = await reader.read();
1134
+ if (done) break;
1135
+ buffer += decoder.decode(value, { stream: true });
1136
+ let sep = buffer.indexOf("\n\n");
1137
+ while (sep !== -1) {
1138
+ const raw = buffer.slice(0, sep);
1139
+ buffer = buffer.slice(sep + 2);
1140
+ sep = buffer.indexOf("\n\n");
1141
+ let event = "message";
1142
+ const dataLines = [];
1143
+ for (const line of raw.split("\n")) {
1144
+ if (!line || line.startsWith(":")) continue;
1145
+ if (line.startsWith("event:")) event = line.slice(6).trimStart();
1146
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
1147
+ }
1148
+ if (dataLines.length === 0) continue;
1149
+ yield {
1150
+ event,
1151
+ data: dataLines.join("\n")
1152
+ };
1153
+ }
1154
+ }
1155
+ } finally {
1156
+ try {
1157
+ reader.releaseLock();
1158
+ } catch {}
1159
+ try {
1160
+ await body.cancel();
1161
+ } catch {}
1162
+ }
1206
1163
  }
1207
1164
  function terminalResultFromMessage(msg, fallbackMessageId) {
1208
- const ai = msg.response;
1209
- if (ai && isTerminalAIStatus(ai.status)) {
1210
- return {
1211
- status: messageCompletionStatus(ai.status),
1212
- message_id: ai.message_id || fallbackMessageId,
1213
- content: ai.content,
1214
- edit_id: ai.edit_id,
1215
- commit_sha: ai.commit_sha,
1216
- summary: ai.summary,
1217
- cost_credits: ai.cost_credits,
1218
- awaiting_input: ai.awaiting_input,
1219
- ...ai.status === "error" ? { error: "Agent reported an error." } : {}
1220
- };
1221
- }
1222
- if (msg.role === "assistant" && isTerminalAIStatus(msg.status)) {
1223
- return {
1224
- status: messageCompletionStatus(msg.status),
1225
- message_id: msg.message_id || fallbackMessageId,
1226
- content: msg.content,
1227
- edit_id: msg.edit_id,
1228
- commit_sha: msg.commit_sha,
1229
- summary: msg.summary,
1230
- cost_credits: msg.cost_credits,
1231
- awaiting_input: msg.awaiting_input,
1232
- ...msg.status === "error" ? { error: "Agent reported an error." } : {}
1233
- };
1234
- }
1235
- return null;
1165
+ const ai = msg.response;
1166
+ if (ai && isTerminalAIStatus(ai.status)) return {
1167
+ status: messageCompletionStatus(ai.status),
1168
+ message_id: ai.message_id || fallbackMessageId,
1169
+ content: ai.content,
1170
+ edit_id: ai.edit_id,
1171
+ commit_sha: ai.commit_sha,
1172
+ summary: ai.summary,
1173
+ cost_credits: ai.cost_credits,
1174
+ awaiting_input: ai.awaiting_input,
1175
+ ...ai.status === "error" ? { error: "Agent reported an error." } : {}
1176
+ };
1177
+ if (msg.role === "assistant" && isTerminalAIStatus(msg.status)) return {
1178
+ status: messageCompletionStatus(msg.status),
1179
+ message_id: msg.message_id || fallbackMessageId,
1180
+ content: msg.content,
1181
+ edit_id: msg.edit_id,
1182
+ commit_sha: msg.commit_sha,
1183
+ summary: msg.summary,
1184
+ cost_credits: msg.cost_credits,
1185
+ awaiting_input: msg.awaiting_input,
1186
+ ...msg.status === "error" ? { error: "Agent reported an error." } : {}
1187
+ };
1188
+ return null;
1236
1189
  }
1237
1190
  function messageCompletionStatus(status) {
1238
- return status === "completed" || status === "stopped" || status === "awaiting_input" ? status : "error";
1191
+ return status === "completed" || status === "stopped" || status === "awaiting_input" ? status : "error";
1239
1192
  }
1240
1193
  function isTerminalAIStatus(status) {
1241
- return status === "completed" || status === "stopped" || status === "error" || status === "awaiting_input";
1194
+ return status === "completed" || status === "stopped" || status === "error" || status === "awaiting_input";
1242
1195
  }
1243
1196
  function queuedExitResult(msg, fallbackMessageId) {
1244
- if (msg.status !== "queued") return null;
1245
- if (!msg.queue_paused) return null;
1246
- if (msg.queue_pause_reason === "hitl_tool") return null;
1247
- return {
1248
- status: "error",
1249
- message_id: fallbackMessageId,
1250
- content: "",
1251
- error: `Message is queued (position ${msg.queue_position ?? "unknown"}) but the queue is paused` + (msg.queue_pause_reason ? ` (reason: ${msg.queue_pause_reason})` : "") + `. Unpause the queue in the Lovable editor, or use wait=false to return immediately.`
1252
- };
1197
+ if (msg.status !== "queued") return null;
1198
+ if (!msg.queue_paused) return null;
1199
+ if (msg.queue_pause_reason === "hitl_tool") return null;
1200
+ return {
1201
+ status: "error",
1202
+ message_id: fallbackMessageId,
1203
+ content: "",
1204
+ error: `Message is queued (position ${msg.queue_position ?? "unknown"}) but the queue is paused` + (msg.queue_pause_reason ? ` (reason: ${msg.queue_pause_reason})` : "") + `. Unpause the queue in the Lovable editor, or use wait=false to return immediately.`
1205
+ };
1253
1206
  }
1254
1207
  function timeoutResult(messageId, timeoutMs) {
1255
- return {
1256
- status: "timeout",
1257
- message_id: messageId,
1258
- content: "",
1259
- error: `Agent did not finish within ${Math.max(0, Math.round(timeoutMs / 1e3))}s`
1260
- };
1208
+ return {
1209
+ status: "timeout",
1210
+ message_id: messageId,
1211
+ content: "",
1212
+ error: `Agent did not finish within ${Math.max(0, Math.round(timeoutMs / 1e3))}s`
1213
+ };
1261
1214
  }
1262
- export {
1263
- ApiError,
1264
- LovableClient
1265
- };
1215
+ //#endregion
1216
+ export { ApiError, LovableClient };
1217
+
1266
1218
  //# sourceMappingURL=index.js.map