@lovable.dev/sdk 0.1.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,23 +1,55 @@
1
1
  // src/client.ts
2
2
  import createClient from "openapi-fetch";
3
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;
18
+ }
19
+ 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
+ };
31
+ }
32
+
4
33
  // src/types.ts
5
34
  var ApiError = class extends Error {
6
35
  status;
7
36
  type;
8
37
  detail;
9
38
  props;
10
- constructor(status, message, type, detail, props) {
39
+ rateLimit;
40
+ constructor(status, message, type, detail, props, rateLimit) {
11
41
  super(message);
12
42
  this.status = status;
13
43
  this.type = type;
14
44
  this.detail = detail;
15
45
  this.props = props;
46
+ this.rateLimit = rateLimit;
16
47
  }
17
48
  };
18
49
 
19
50
  // src/client.ts
20
51
  var DEFAULT_BASE_URL = "https://api.lovable.dev";
52
+ var PUBLIC_API_CURSOR_VERSION = 1;
21
53
  function normalizeBaseUrl(url) {
22
54
  if (!url) return DEFAULT_BASE_URL;
23
55
  const normalized = url.replace(/\/$/, "");
@@ -26,6 +58,25 @@ function normalizeBaseUrl(url) {
26
58
  }
27
59
  return normalized;
28
60
  }
61
+ 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(/=+$/, "");
69
+ }
70
+ function normalizeWorkspaceList(body) {
71
+ return body.data ?? body.workspaces ?? [];
72
+ }
73
+ function cursorHasMore(body) {
74
+ return body.pagination?.has_more ?? body.has_more ?? false;
75
+ }
76
+ function requireCreateProjectId(project) {
77
+ if (!project.id) throw new Error("Create project response missing project ID");
78
+ return project;
79
+ }
29
80
  var errorMiddleware = {
30
81
  async onResponse({ response }) {
31
82
  if (response.ok) return;
@@ -37,9 +88,32 @@ var errorMiddleware = {
37
88
  const message = buildErrorMessage(errorBody, response.status, response.statusText);
38
89
  const type = errorBody?.type ?? errorBody?.title;
39
90
  const detail = errorBody?.detail ?? errorBody?.details;
40
- throw new ApiError(response.status, message, type, detail, errorBody?.props);
91
+ throw new ApiError(response.status, message, type, detail, errorBody?.props, parseRateLimitInfo(response.headers));
41
92
  }
42
93
  };
94
+ 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 };
100
+ }
101
+ 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;
105
+ }
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;
116
+ }
43
117
  function buildErrorMessage(body, status, statusText) {
44
118
  return body?.message || body?.title || (statusText ? `HTTP ${status}: ${statusText}` : `HTTP ${status}`);
45
119
  }
@@ -69,7 +143,8 @@ var LovableClient = class {
69
143
  ...this.authHeaders,
70
144
  ...this.extraHeaders,
71
145
  Accept: "application/json"
72
- }
146
+ },
147
+ fetch: makeRetryFetch()
73
148
  });
74
149
  this.typedClient.use(errorMiddleware);
75
150
  }
@@ -83,112 +158,98 @@ var LovableClient = class {
83
158
  get typed() {
84
159
  return this.typedClient;
85
160
  }
86
- async rawRequest(method, path, body, init) {
87
- const url = `${this.baseUrl}${path}`;
88
- const headers = {
89
- "X-Client-Source": this.clientSource,
90
- ...init?.headers,
91
- ...this.authHeaders,
92
- ...this.extraHeaders,
93
- Accept: "application/json"
94
- };
95
- if (body !== void 0) {
96
- headers["Content-Type"] = "application/json";
97
- }
98
- const response = await fetch(url, {
99
- method,
100
- headers,
101
- body: body !== void 0 ? JSON.stringify(body) : void 0,
102
- signal: init?.signal
103
- });
104
- if (!response.ok) {
105
- let errorBody;
106
- try {
107
- errorBody = await response.json();
108
- } catch {
109
- }
110
- const message = buildErrorMessage(errorBody, response.status, response.statusText);
111
- const type = errorBody?.type ?? errorBody?.title;
112
- const detail = errorBody?.detail ?? errorBody?.details;
113
- throw new ApiError(response.status, message, type, detail, errorBody?.props);
114
- }
115
- return response;
116
- }
117
- async request(method, path, body) {
118
- const response = await this.rawRequest(method, path, body);
119
- if (response.status === 204) {
120
- return void 0;
121
- }
122
- return response.json();
123
- }
124
- async requestText(method, path) {
125
- const response = await this.rawRequest(method, path);
126
- return response.text();
127
- }
128
161
  /**
129
162
  * Get the current authenticated user and their workspaces.
130
163
  * Useful for validating an API key and discovering workspace IDs.
131
164
  */
132
165
  async me() {
133
166
  const { data } = await this.typed.GET("/v1/me");
134
- return { ...data, workspaces: data.workspaces ?? [] };
167
+ return {
168
+ ...data,
169
+ workspaces: normalizeWorkspaceList(
170
+ data
171
+ )
172
+ };
135
173
  }
136
174
  /**
137
- * List all workspaces the authenticated user has access to
175
+ * List workspaces the authenticated user has access to.
138
176
  */
139
- async listWorkspaces() {
140
- const response = await this.request("GET", "/v1/workspaces");
141
- return response.workspaces ?? [];
177
+ async listWorkspaces(options = {}) {
178
+ const { data } = await this.typed.GET("/v1/workspaces", {
179
+ params: { query: options }
180
+ });
181
+ return { ...data, workspaces: normalizeWorkspaceList(data) };
142
182
  }
143
183
  /**
144
184
  * Get a specific workspace by ID
145
185
  */
146
186
  async getWorkspace(workspaceId) {
147
- const response = await this.request("GET", `/v1/workspaces/${workspaceId}`);
148
- return response.workspace;
187
+ const { data } = await this.typed.GET("/v1/workspaces/{workspace_id}", {
188
+ params: { path: { workspace_id: workspaceId } }
189
+ });
190
+ return data.workspace;
149
191
  }
150
192
  /**
151
193
  * List projects in a workspace.
152
194
  * Supports full-text search, filtering by visibility/publish status/folder/creator,
153
- * and pagination via offset or cursor.
195
+ * and cursor pagination.
154
196
  */
155
197
  async listProjects(workspaceId, options) {
156
- const params = new URLSearchParams();
157
- if (options?.query) params.set("q", options.query);
158
- if (options?.visibility) params.set("visibility", options.visibility);
159
- if (options?.publish_status) params.set("publish_status", options.publish_status);
160
- if (options?.folder_id) params.set("folder_id", options.folder_id);
161
- if (options?.user_id) params.set("user_id", options.user_id);
162
- if (options?.sort_by) params.set("sort_by", options.sort_by);
163
- if (options?.sort_order) params.set("sort_order", options.sort_order);
164
- if (options?.viewed_by_me) params.set("viewed_by_me", "true");
165
- if (options?.limit !== void 0) params.set("limit", String(options.limit));
166
- if (options?.offset !== void 0) params.set("offset", String(options.offset));
167
- if (options?.cursor) params.set("cursor", options.cursor);
168
- const query = params.toString();
169
- const path = `/v1/workspaces/${workspaceId}/projects${query ? `?${query}` : ""}`;
170
- return this.request("GET", path);
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
+ };
171
225
  }
172
226
  /**
173
227
  * Create a new project in a workspace
174
228
  */
175
229
  async createProject(workspaceId, options) {
176
230
  let fileRefs;
231
+ let ephemeralFileRefs;
177
232
  if (options.uploadedFiles?.length) {
178
- fileRefs = options.uploadedFiles;
233
+ ({ fileRefs, ephemeralFileRefs } = this.partitionUploadedFiles(options.uploadedFiles));
179
234
  } else if (options.files?.length) {
180
- fileRefs = await this.uploadFiles(options.files);
235
+ ephemeralFileRefs = await this.uploadEphemeralFiles(options.files);
181
236
  }
182
237
  const body = {
183
238
  description: options.description,
184
239
  template_project_id: options.templateProjectId
185
240
  };
241
+ if (options.projectName) {
242
+ body.display_name = options.projectName;
243
+ }
186
244
  if (options.visibility) {
187
245
  body.visibility = options.visibility;
188
246
  }
189
247
  if (options.techStack) {
190
248
  body.tech_stack = options.techStack;
191
249
  }
250
+ if (options.sandboxTemplate) {
251
+ body.sandbox_template = options.sandboxTemplate;
252
+ }
192
253
  if (options.selectedLibraries?.length) {
193
254
  body.selected_libraries = options.selectedLibraries;
194
255
  }
@@ -198,73 +259,101 @@ var LovableClient = class {
198
259
  if (fileRefs?.length) {
199
260
  body.files = fileRefs;
200
261
  }
201
- const project = await this.request("POST", `/v1/workspaces/${workspaceId}/projects`, body);
202
- return project;
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);
203
272
  }
204
273
  /**
205
274
  * Send a chat message to a project.
206
275
  *
207
276
  * The API accepts the message and processes it in the background.
208
277
  * Returns the message ID and status. Use `waitForMessageCompletion()`
209
- * to poll for the AI response, or `waitForResponse()` for SSE streaming.
278
+ * to poll for the AI response.
210
279
  */
211
280
  async chat(projectId, options) {
212
281
  let fileRefs;
282
+ let ephemeralFileRefs;
213
283
  if (options.uploadedFiles?.length) {
214
- fileRefs = options.uploadedFiles;
284
+ ({ fileRefs, ephemeralFileRefs } = this.partitionUploadedFiles(options.uploadedFiles));
215
285
  } else if (options.files?.length) {
216
- fileRefs = await this.uploadFiles(options.files);
286
+ fileRefs = await this.uploadProjectFiles(projectId, options.files);
217
287
  }
218
288
  const body = {
219
289
  message: options.message
220
290
  };
291
+ if (options.variantId) {
292
+ body.variant_id = options.variantId;
293
+ }
221
294
  if (fileRefs) {
222
295
  body.files = fileRefs;
223
296
  }
297
+ if (ephemeralFileRefs) {
298
+ body.ephemeral_files = ephemeralFileRefs;
299
+ }
224
300
  if (options.planMode) {
225
301
  body.plan_mode = true;
226
302
  }
227
- if (options.customModel) {
228
- body.custom_model_endpoint = options.customModel.endpoint;
229
- body.custom_model_api_key = options.customModel.apiKey;
230
- body.custom_model_name = options.customModel.modelName;
231
- }
232
- if (options.customModelDisableRace) {
233
- body.custom_model_disable_race = true;
234
- }
235
303
  if (options.continuation) {
236
304
  body.continuation = options.continuation;
237
305
  }
238
- return this.request("POST", `/v1/projects/${projectId}/messages`, body);
306
+ const { data } = await this.typed.POST("/v1/messages", {
307
+ body: { ...body, project_id: projectId }
308
+ });
309
+ return data;
239
310
  }
240
311
  /**
241
- * Invite a user to a workspace as a collaborator
312
+ * Create an independent variant from the project's current main branch, or from a full baseSha when provided.
242
313
  */
243
- async inviteCollaborator(workspaceId, options) {
244
- const body = {
245
- email: options.email,
246
- role: options.role ?? "member"
247
- };
248
- return this.request("POST", `/workspaces/${workspaceId}/memberships`, body);
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;
249
329
  }
250
330
  /**
251
- * List members of a workspace
331
+ * Create an anonymous, one-hour static preview URL for one exact HTTPS parent origin.
252
332
  */
253
- async listWorkspaceMembers(workspaceId) {
254
- const response = await this.request("GET", `/workspaces/${workspaceId}/memberships`);
255
- return response.memberships ?? [];
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;
256
339
  }
257
340
  /**
258
- * Remove a member from a workspace
341
+ * Update supported project fields.
259
342
  */
260
- async removeWorkspaceMember(workspaceId, userId) {
261
- await this.request("DELETE", `/workspaces/${workspaceId}/memberships/${userId}`);
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;
262
349
  }
263
350
  /**
264
- * Get project details by ID
351
+ * Soft-delete a project. Repeated deletes are treated as successful.
265
352
  */
266
- async getProject(projectId) {
267
- return this.request("GET", `/v1/projects/${projectId}`);
353
+ async deleteProject(projectId) {
354
+ await this.typed.DELETE("/v1/projects/{project_id}", {
355
+ params: { path: { project_id: projectId } }
356
+ });
268
357
  }
269
358
  /**
270
359
  * Get the preview URL for a project.
@@ -297,7 +386,10 @@ var LovableClient = class {
297
386
  * @returns Whether the database is enabled and which stack is used
298
387
  */
299
388
  async getDatabaseStatus(projectId) {
300
- return this.request("GET", `/v1/projects/${projectId}/database`);
389
+ const { data } = await this.typed.GET("/v1/database", {
390
+ params: { query: { project_id: projectId } }
391
+ });
392
+ return data;
301
393
  }
302
394
  /**
303
395
  * Enable (provision) a cloud database for a project.
@@ -309,7 +401,10 @@ var LovableClient = class {
309
401
  * @returns The database status after enablement
310
402
  */
311
403
  async enableDatabase(projectId) {
312
- return this.request("POST", `/v1/projects/${projectId}/database/enable`);
404
+ const { data } = await this.typed.POST("/v1/database/enable", {
405
+ body: { project_id: projectId }
406
+ });
407
+ return data;
313
408
  }
314
409
  /**
315
410
  * Execute a SQL query against the project's cloud database.
@@ -322,111 +417,211 @@ var LovableClient = class {
322
417
  * @returns Query result rows as JSON objects
323
418
  */
324
419
  async queryDatabase(projectId, sql) {
325
- return this.request("POST", `/v1/projects/${projectId}/database/query`, { 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;
326
425
  }
327
426
  // ---------------------------------------------------------------------------
328
427
  // Messages
329
428
  // ---------------------------------------------------------------------------
330
429
  /**
331
430
  * Get a message by ID. Returns the message content, status, and (for user messages)
332
- * the AI response if available. Use to poll for completion after `chat()`.
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`.
333
436
  */
334
- async getMessage(projectId, messageId) {
335
- return this.request("GET", `/v1/projects/${projectId}/messages/${messageId}`);
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;
336
447
  }
337
448
  /**
338
- * List recent messages in a project, newest first. Use `before` (a message ID
339
- * from a prior page) to paginate backwards through history.
449
+ * List recent messages in a project, newest first. Use `cursor` from the
450
+ * previous page's `pagination.next_cursor` to paginate through history.
340
451
  */
341
452
  async listMessages(projectId, params) {
342
- const qs = new URLSearchParams();
343
- if (params?.limit !== void 0) qs.set("limit", String(params.limit));
344
- if (params?.before) qs.set("before", params.before);
345
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
346
- return this.request("GET", `/v1/projects/${projectId}/messages${suffix}`);
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
+ };
347
464
  }
348
465
  /**
349
- * Poll for message completion. Waits until the AI response reaches a terminal
350
- * status (completed, stopped) or the timeout expires.
466
+ * Wait for the AI response to reach a terminal status (completed, stopped, error, awaiting_input)
467
+ * or for `timeout` to elapse.
351
468
  *
352
- * Handles edge cases: 404 grace period for race conditions between queue
353
- * dequeue and event creation, and queue pause detection.
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.
354
474
  */
355
475
  async waitForMessageCompletion(projectId, messageId, options) {
356
- const pollInterval = options?.pollInterval ?? 3e3;
357
476
  const timeout = options?.timeout ?? 6e5;
358
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;
359
583
  let notFoundSince = null;
360
584
  const notFoundGraceMs = 15e3;
361
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();
362
590
  try {
363
- const msg = await this.getMessage(projectId, messageId);
591
+ const msg = await this.getMessage(projectId, messageId, { ...waitOptions, threadId: options?.threadId });
364
592
  notFoundSince = null;
365
- if (msg.status === "queued") {
366
- if (msg.queue_paused && msg.queue_pause_reason !== "hitl_tool") {
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
+ }
367
610
  return {
368
611
  status: "error",
369
612
  message_id: messageId,
370
613
  content: "",
371
- 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.`
614
+ error: "Message not found. It may have been deleted from the queue."
372
615
  };
373
616
  }
374
- await sleep(pollInterval);
375
- continue;
376
- }
377
- const ai = msg.response;
378
- if (ai) {
379
- if (ai.status === "completed" || ai.status === "stopped") {
380
- return {
381
- status: ai.status === "completed" ? "completed" : "error",
382
- message_id: ai.message_id,
383
- content: ai.content,
384
- edit_id: ai.edit_id,
385
- commit_sha: ai.commit_sha,
386
- summary: ai.summary,
387
- cost_credits: ai.cost_credits
388
- };
617
+ if (err.status < 500) {
618
+ throw err;
389
619
  }
390
620
  }
391
- if (msg.role === "assistant") {
392
- if (msg.status === "completed" || msg.status === "stopped") {
393
- return {
394
- status: msg.status === "completed" ? "completed" : "error",
395
- message_id: msg.message_id,
396
- content: msg.content,
397
- edit_id: msg.edit_id,
398
- commit_sha: msg.commit_sha,
399
- summary: msg.summary,
400
- cost_credits: msg.cost_credits
401
- };
402
- }
403
- }
404
- await sleep(pollInterval);
405
- } catch (err) {
406
- if (err instanceof ApiError && err.status === 404) {
407
- if (notFoundSince === null) {
408
- notFoundSince = Date.now();
409
- }
410
- if (Date.now() - notFoundSince < notFoundGraceMs) {
411
- await sleep(pollInterval);
412
- continue;
413
- }
414
- return {
415
- status: "error",
416
- message_id: messageId,
417
- content: "",
418
- error: "Message not found. It may have been deleted from the queue."
419
- };
420
- }
421
- await sleep(pollInterval);
621
+ await sleep2(Math.min(transientBackoffMs, Math.max(0, deadline - Date.now())));
422
622
  }
423
623
  }
424
- return {
425
- status: "timeout",
426
- message_id: messageId,
427
- content: "",
428
- error: `Agent did not finish within ${timeout / 1e3}s`
429
- };
624
+ return timeoutResult(messageId, totalTimeoutMs);
430
625
  }
431
626
  // ---------------------------------------------------------------------------
432
627
  // Knowledge
@@ -440,73 +635,87 @@ var LovableClient = class {
440
635
  }
441
636
  /** Set workspace knowledge. Max 10,000 characters. */
442
637
  async setWorkspaceKnowledge(workspaceId, content) {
443
- return this.request("PUT", `/v1/workspaces/${workspaceId}/knowledge`, { 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;
444
643
  }
445
644
  // ---------------------------------------------------------------------------
446
645
  // Workspace skills
447
646
  // ---------------------------------------------------------------------------
448
647
  /** List workspace skills. */
449
- async listWorkspaceSkills(workspaceId, options) {
450
- const qs = new URLSearchParams();
451
- if (options?.includeMarkdown) qs.set("include_markdown", "true");
452
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
453
- return this.request("GET", `/v1/workspaces/${workspaceId}/skills${suffix}`);
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 ?? [] };
454
656
  }
455
657
  /** Get a single workspace skill, including SKILL.md contents. */
456
658
  async getWorkspaceSkill(workspaceId, skillName) {
457
- return this.request(
458
- "GET",
459
- `/v1/workspaces/${workspaceId}/skills/${encodeURIComponent(skillName)}`
460
- );
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;
461
663
  }
462
664
  /** Create a workspace skill from full SKILL.md markdown. */
463
665
  async createWorkspaceSkill(workspaceId, skillName, markdown) {
464
- return this.request(
465
- "POST",
466
- `/v1/workspaces/${workspaceId}/skills/${encodeURIComponent(skillName)}`,
467
- { markdown }
468
- );
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;
469
671
  }
470
672
  /** Update a workspace skill by replacing its SKILL.md markdown. */
471
673
  async updateWorkspaceSkill(workspaceId, skillName, markdown) {
472
- return this.request(
473
- "PUT",
474
- `/v1/workspaces/${workspaceId}/skills/${encodeURIComponent(skillName)}`,
475
- { markdown }
476
- );
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;
477
679
  }
478
680
  /** Delete a workspace skill. */
479
681
  async deleteWorkspaceSkill(workspaceId, skillName) {
480
- return this.request(
481
- "DELETE",
482
- `/v1/workspaces/${workspaceId}/skills/${encodeURIComponent(skillName)}`
483
- );
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;
484
686
  }
485
687
  // ---------------------------------------------------------------------------
486
688
  // Project skills
487
689
  // ---------------------------------------------------------------------------
488
690
  /** List project skills, including whether each skill is enabled. */
489
- async listProjectSkills(projectId) {
490
- return this.request("GET", `/v1/projects/${projectId}/skills`);
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 };
491
697
  }
492
698
  /** Enable or disable a project skill without removing it from the project repo. */
493
699
  async setProjectSkillEnabled(projectId, skillName, enabled) {
494
- return this.request(
495
- "PUT",
496
- `/v1/projects/${projectId}/skills/${encodeURIComponent(skillName)}/enabled`,
497
- { enabled }
498
- );
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;
499
705
  }
500
706
  /** Get project knowledge (custom instructions for the AI agent). */
501
707
  async getProjectKnowledge(projectId) {
502
- const { data } = await this.typed.GET("/v1/projects/{project_id}/knowledge", {
503
- params: { path: { project_id: projectId } }
708
+ const { data } = await this.typed.GET("/v1/knowledge", {
709
+ params: { query: { project_id: projectId } }
504
710
  });
505
711
  return data;
506
712
  }
507
713
  /** Set project knowledge. Max 10,000 characters. */
508
714
  async setProjectKnowledge(projectId, content) {
509
- return this.request("PUT", `/v1/projects/${projectId}/knowledge`, { content });
715
+ const { data } = await this.typed.PUT("/v1/knowledge", {
716
+ body: { project_id: projectId, content }
717
+ });
718
+ return data;
510
719
  }
511
720
  // ---------------------------------------------------------------------------
512
721
  // Git operations
@@ -517,138 +726,207 @@ var LovableClient = class {
517
726
  * or `sha` for a specific commit.
518
727
  */
519
728
  async getDiff(projectId, params) {
520
- const qs = new URLSearchParams();
521
- if (params.messageId) qs.set("message_id", params.messageId);
522
- if (params.sha) qs.set("sha", params.sha);
523
- if (params.baseSha) qs.set("base_sha", params.baseSha);
524
- return this.request("GET", `/v1/projects/${projectId}/git/diff?${qs.toString()}`);
525
- }
526
- /** List all files in a project at a specific git ref. */
527
- async listFiles(projectId, ref) {
528
- const qs = new URLSearchParams({ ref });
529
- return this.request("GET", `/v1/projects/${projectId}/git/files?${qs.toString()}`);
530
- }
531
- /** Read the raw content of a single file at a specific git ref. Returns text. */
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. */
532
746
  async readFile(projectId, path, ref) {
533
- const qs = new URLSearchParams({ path, ref });
534
- return this.requestText("GET", `/v1/projects/${projectId}/git/file?${qs.toString()}`);
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;
535
752
  }
536
753
  // ---------------------------------------------------------------------------
537
754
  // Edits
538
755
  // ---------------------------------------------------------------------------
539
756
  /** List the edit history of a project. */
540
757
  async listEdits(projectId, params) {
541
- const qs = new URLSearchParams();
542
- if (params?.limit !== void 0) qs.set("limit", String(params.limit));
543
- if (params?.before) qs.set("before", params.before);
544
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
545
- return this.request("GET", `/v1/projects/${projectId}/edits${suffix}`);
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;
546
764
  }
547
765
  // ---------------------------------------------------------------------------
548
766
  // File upload
549
767
  // ---------------------------------------------------------------------------
550
- /** Get a presigned URL for uploading a file. Returns the upload URL and file ID. */
768
+ /** Get an ephemeral presigned URL for uploading a file before a project exists. */
551
769
  async getFileUploadUrl(params) {
552
- return this.request("POST", "/v1/files/upload-url", 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;
553
783
  }
554
784
  // ---------------------------------------------------------------------------
555
785
  // Visibility
556
786
  // ---------------------------------------------------------------------------
557
- /** Set a project's visibility (draft, private, or public). */
787
+ /** Set a project's visibility (draft, private, workspace_view, or public). */
558
788
  async setProjectVisibility(projectId, visibility) {
559
- return this.request("PUT", `/v1/projects/${projectId}/visibility`, { visibility });
789
+ return this.updateProject(projectId, { visibility });
560
790
  }
561
791
  /** Set a folder's visibility (personal or workspace). */
562
792
  async setFolderVisibility(workspaceId, folderId, visibility) {
563
- return this.request("PUT", `/v1/workspaces/${workspaceId}/folders/${folderId}/visibility`, {
564
- 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 }
565
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;
566
806
  }
567
807
  // ---------------------------------------------------------------------------
568
808
  // Library & template projects
569
809
  // ---------------------------------------------------------------------------
570
810
  /** List available design system library projects in a workspace. */
571
- async listLibraryProjects(workspaceId) {
572
- return this.request(
573
- "GET",
574
- `/v1/workspaces/${workspaceId}/available-library-projects`
575
- );
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 ?? [] };
576
816
  }
577
817
  /** List available template projects in a workspace. */
578
- async listTemplateProjects(workspaceId) {
579
- return this.request(
580
- "GET",
581
- `/v1/workspaces/${workspaceId}/available-template-projects`
582
- );
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 ?? [] };
583
823
  }
584
824
  // ---------------------------------------------------------------------------
585
825
  // Connectors (MCP servers)
586
826
  // ---------------------------------------------------------------------------
587
827
  /** List all connectors in a workspace. */
588
- async listConnectors(workspaceId) {
589
- return this.request("GET", `/v1/workspaces/${workspaceId}/connectors`);
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) };
590
842
  }
591
843
  /** Add a connector to a workspace. The server URL is tested before saving. */
592
844
  async addConnector(workspaceId, body) {
593
- return this.request("POST", `/v1/workspaces/${workspaceId}/connectors`, body);
845
+ const { data } = await this.typed.POST("/v1/connectors", {
846
+ body: { ...body, workspace_id: workspaceId }
847
+ });
848
+ return data;
594
849
  }
595
850
  /** Remove a connector from a workspace. */
596
851
  async removeConnector(workspaceId, connectorId) {
597
- return this.request("DELETE", `/v1/workspaces/${workspaceId}/connectors/${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;
598
856
  }
599
857
  /** Browse available connector templates. */
600
- async listAvailableConnectors(workspaceId) {
601
- return this.request(
602
- "GET",
603
- `/v1/workspaces/${workspaceId}/available-connectors`
604
- );
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) };
605
864
  }
606
865
  // ---------------------------------------------------------------------------
607
866
  // Connectors
608
867
  // ---------------------------------------------------------------------------
609
868
  /** List standard (OAuth-based) connectors in a workspace. */
610
- async listStandardConnectors(workspaceId) {
611
- return this.request(
612
- "GET",
613
- `/v1/workspaces/${workspaceId}/connectors/standard`
614
- );
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) };
615
875
  }
616
876
  /** List seamless (zero-config) connectors in a workspace. */
617
- async listSeamlessConnectors(workspaceId) {
618
- return this.request(
619
- "GET",
620
- `/v1/workspaces/${workspaceId}/connectors/seamless`
621
- );
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) };
622
883
  }
623
884
  /** List MCP connectors in a workspace. */
624
- async listMCPConnectors(workspaceId) {
625
- return this.request(
626
- "GET",
627
- `/v1/workspaces/${workspaceId}/connectors/mcp`
628
- );
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) };
629
891
  }
630
892
  /** List authenticated connections (accounts) in a workspace. */
631
- async listConnections(workspaceId, params) {
632
- const qs = new URLSearchParams();
633
- if (params?.connector_id) qs.set("connector_id", params.connector_id);
634
- const suffix = qs.toString() ? `?${qs.toString()}` : "";
635
- return this.request("GET", `/v1/workspaces/${workspaceId}/connections${suffix}`);
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) };
636
906
  }
637
907
  // ---------------------------------------------------------------------------
638
908
  // Analytics
639
909
  // ---------------------------------------------------------------------------
640
910
  /** Get historical analytics for a published project. */
641
911
  async getProjectAnalytics(projectId, params) {
642
- const qs = new URLSearchParams({
643
- startDate: params.startDate,
644
- endDate: params.endDate
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
+ }
645
921
  });
646
- if (params.granularity) qs.set("granularity", params.granularity);
647
- return this.request("GET", `/v1/projects/${projectId}/analytics?${qs.toString()}`);
922
+ return data;
648
923
  }
649
924
  /** Get real-time visitor trend for a published project. */
650
925
  async getProjectAnalyticsTrend(projectId) {
651
- return this.request("GET", `/v1/projects/${projectId}/analytics/trend`);
926
+ const { data } = await this.typed.GET("/v1/analytics/trend", {
927
+ params: { query: { project_id: projectId } }
928
+ });
929
+ return data;
652
930
  }
653
931
  /**
654
932
  * Publish a project.
@@ -661,50 +939,57 @@ var LovableClient = class {
661
939
  * @returns Deployment info including deployment ID
662
940
  */
663
941
  async publish(projectId, options) {
664
- return this.request("POST", `/v1/projects/${projectId}/deployments`, {
665
- name: options?.name
942
+ const { data } = await this.typed.POST("/v1/deployments", {
943
+ body: { project_id: projectId, name: options?.name }
666
944
  });
945
+ return data;
667
946
  }
668
947
  /**
669
948
  * Remix (fork) an existing project, optionally at a specific message point in time.
670
949
  *
671
- * When `messageId` is provided, the remix captures the project state as it was
672
- * just before that message was processed (default). Set `remixMode: "including"`
673
- * to include the message and its AI response in the remix.
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.
674
953
  * Without `messageId`, the full current state is remixed.
675
954
  *
676
955
  * @param sourceProjectId - The project to remix from
677
956
  * @param options.workspaceId - Target workspace for the new project
678
957
  * @param options.messageId - Optional message ID to snapshot at
679
- * @param options.remixMode - "before" (default): state before the message; "including": state after the message and its AI response
958
+ * @param options.remixMode - "including" (server default): state after the message and its AI response; "before": state before the message
680
959
  * @param options.includeHistory - Whether to preserve chat history (default: false)
681
960
  * @param options.includeCustomKnowledge - Whether to copy custom instructions (default: false)
682
961
  * @param options.initialMessage - Optional initial message to send after remix
962
+ * @param options.description - Optional custom description for the new project
683
963
  * @returns The remix job ID for polling progress
684
964
  */
685
965
  async remixProject(sourceProjectId, options) {
686
966
  const body = {
687
967
  workspace_id: options.workspaceId,
968
+ source_project_id: sourceProjectId,
688
969
  include_history: options.includeHistory,
689
970
  include_custom_knowledge: options.includeCustomKnowledge,
690
- project_name: options.projectName,
971
+ description: options.description,
972
+ display_name: options.projectName,
691
973
  skip_initial_remix_message: options.skipInitialRemixMessage,
692
974
  skip_integrations: options.skipIntegrations
693
975
  };
694
976
  if (options.messageId) {
695
977
  body.message_id = options.messageId;
696
- body.remix_mode = options.remixMode ?? "before";
978
+ if (options.remixMode) {
979
+ body.remix_mode = options.remixMode;
980
+ }
697
981
  }
698
982
  if (options.initialMessage) {
699
- body.initial_message = {
700
- id: crypto.randomUUID(),
701
- message: options.initialMessage,
702
- chat_only: false,
703
- headless: true
704
- };
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");
705
991
  }
706
- const response = await this.request("POST", `/v1/projects/${sourceProjectId}/remix/init`, body);
707
- return response.job_id;
992
+ return remix.job_id;
708
993
  }
709
994
  /**
710
995
  * Wait for a remix operation to complete.
@@ -724,21 +1009,25 @@ var LovableClient = class {
724
1009
  const timeout = options?.timeout ?? 3e5;
725
1010
  const startTime = Date.now();
726
1011
  while (true) {
727
- const progress = await this.request(
728
- "GET",
729
- `/v1/projects/${sourceProjectId}/remix/progress?job_id=${encodeURIComponent(jobId)}`
730
- );
731
- options?.onProgress?.(progress.status, progress.step);
732
- if (progress.status === "completed" && progress.result) {
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) {
733
1022
  return { projectId: progress.result.project_id };
734
1023
  }
735
- if (progress.status === "error") {
1024
+ if (status === "error") {
736
1025
  throw new Error(progress.error_message ?? "Remix failed");
737
1026
  }
738
1027
  if (Date.now() - startTime > timeout) {
739
1028
  throw new Error(`Timeout waiting for remix of project ${sourceProjectId}`);
740
1029
  }
741
- await sleep(pollInterval);
1030
+ await sleep2(pollInterval);
742
1031
  }
743
1032
  }
744
1033
  /**
@@ -771,140 +1060,59 @@ var LovableClient = class {
771
1060
  if (Date.now() - startTime > timeout) {
772
1061
  throw new Error(`Timeout waiting for project ${projectId} to be ready`);
773
1062
  }
774
- await sleep(pollInterval);
775
- }
776
- }
777
- /**
778
- * Wait for the AI response to a chat message.
779
- *
780
- * Connects to the project's message stream (SSE) and accumulates the
781
- * response content until the message is complete. Returns the full
782
- * response text along with the project's preview URL.
783
- *
784
- * Use this after `chat()` or after `createProject()` with `initialMessage`.
785
- *
786
- * @param projectId - The project ID to listen for
787
- * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
788
- * @returns The AI response content and preview URL
789
- * @throws Error if the stream fails or timeout is reached
790
- */
791
- async waitForResponse(projectId, options) {
792
- const timeout = options?.timeout ?? 3e5;
793
- const url = `${this.baseUrl}/v1/projects/${projectId}/messages/stream`;
794
- const controller = new AbortController();
795
- const timeoutId = setTimeout(() => controller.abort(), timeout);
796
- try {
797
- const response = await fetch(url, {
798
- headers: {
799
- ...this.authHeaders,
800
- ...this.extraHeaders
801
- },
802
- signal: controller.signal
803
- });
804
- if (!response.ok) {
805
- throw new ApiError(response.status, `Failed to connect to message stream: HTTP ${response.status}`);
806
- }
807
- if (!response.body) {
808
- throw new Error("Response body is not readable");
809
- }
810
- const result = await this.consumeSSEStream(response.body);
811
- return {
812
- content: result.content,
813
- messageId: result.messageId,
814
- previewUrl: this.getPreviewUrl(projectId)
815
- };
816
- } catch (err) {
817
- if (err instanceof DOMException && err.name === "AbortError") {
818
- throw new Error(`Timeout waiting for response on project ${projectId}`);
819
- }
820
- throw err;
821
- } finally {
822
- clearTimeout(timeoutId);
1063
+ await sleep2(pollInterval);
823
1064
  }
824
1065
  }
825
1066
  isFileInput(file) {
826
1067
  return "data" in file;
827
1068
  }
828
- async uploadFile(file) {
1069
+ async uploadFile(file, getUploadUrl) {
829
1070
  const fileName = this.isFileInput(file) ? file.name : file.name;
830
1071
  const mimeType = this.isFileInput(file) ? file.type : file.type;
831
1072
  const body = this.isFileInput(file) ? file.data : file;
832
- const { url, file_id: objectPath } = await this.request(
833
- "POST",
834
- "/v1/files/upload-url",
835
- {
836
- file_name: fileName,
837
- content_type: mimeType
838
- }
839
- );
1073
+ const uploadUrl = await getUploadUrl(fileName, mimeType);
1074
+ const { url, file_id: objectPath, headers } = uploadUrl;
840
1075
  const uploadResponse = await fetch(url, {
841
1076
  method: "PUT",
842
1077
  body,
843
- headers: { "Content-Type": mimeType }
1078
+ headers: { "Content-Type": mimeType, ...headers }
844
1079
  });
845
1080
  if (!uploadResponse.ok) {
846
1081
  throw new Error(`File upload failed for "${fileName}": HTTP ${uploadResponse.status}`);
847
1082
  }
848
1083
  return { file_id: objectPath, type: "user_upload", file_name: fileName, mime_type: mimeType };
849
1084
  }
850
- async uploadFiles(files) {
851
- return Promise.all(files.map((file) => this.uploadFile(file)));
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
+ );
852
1094
  }
853
- async consumeSSEStream(body) {
854
- const reader = body.getReader();
855
- const decoder = new TextDecoder();
856
- let buffer = "";
857
- let content = "";
858
- let messageId = "";
859
- try {
860
- while (true) {
861
- const { done, value } = await reader.read();
862
- if (done) break;
863
- buffer += decoder.decode(value, { stream: true });
864
- const parts = buffer.split("\n\n");
865
- buffer = parts.pop() ?? "";
866
- for (const part of parts) {
867
- if (!part.trim()) continue;
868
- const lines = part.split("\n");
869
- let eventType = "";
870
- let eventData = "";
871
- for (const line of lines) {
872
- if (line.startsWith("event: ")) {
873
- eventType = line.slice(7);
874
- } else if (line.startsWith("data: ")) {
875
- eventData = line.slice(6);
876
- }
877
- }
878
- if (eventType === "message" && eventData) {
879
- try {
880
- const data = JSON.parse(eventData);
881
- if (typeof data.content === "string") {
882
- content += data.content;
883
- }
884
- if (typeof data.message_id === "string" && data.message_id) {
885
- messageId = data.message_id;
886
- }
887
- if (data.is_final) {
888
- return { content, messageId };
889
- }
890
- } catch {
891
- }
892
- }
893
- if (eventType === "error") {
894
- let detail = "Stream error from server";
895
- try {
896
- const data = JSON.parse(eventData);
897
- if (data.message) detail = data.message;
898
- } catch {
899
- }
900
- throw new Error(detail);
901
- }
902
- }
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);
903
1110
  }
904
- } finally {
905
- void reader.cancel();
906
1111
  }
907
- return { content, messageId };
1112
+ return {
1113
+ fileRefs: fileRefs.length > 0 ? fileRefs : void 0,
1114
+ ephemeralFileRefs: ephemeralFileRefs.length > 0 ? ephemeralFileRefs : void 0
1115
+ };
908
1116
  }
909
1117
  /**
910
1118
  * Wait for a project to be published (deployed).
@@ -934,13 +1142,123 @@ var LovableClient = class {
934
1142
  if (Date.now() - startTime > timeout) {
935
1143
  throw new Error(`Timeout waiting for project ${projectId} to be published`);
936
1144
  }
937
- await sleep(pollInterval);
1145
+ await sleep2(pollInterval);
938
1146
  }
939
1147
  }
940
1148
  };
941
- function sleep(ms) {
1149
+ function sleep2(ms) {
942
1150
  return new Promise((resolve) => setTimeout(resolve, ms));
943
1151
  }
1152
+ function isAbortError(err) {
1153
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
1154
+ }
1155
+ async function safeReadText(response) {
1156
+ try {
1157
+ return await response.text();
1158
+ } catch {
1159
+ return "";
1160
+ }
1161
+ }
1162
+ async function cancelResponseBody(response) {
1163
+ try {
1164
+ await response.body?.cancel();
1165
+ } catch {
1166
+ }
1167
+ }
1168
+ 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
+ }
1206
+ }
1207
+ 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;
1236
+ }
1237
+ function messageCompletionStatus(status) {
1238
+ return status === "completed" || status === "stopped" || status === "awaiting_input" ? status : "error";
1239
+ }
1240
+ function isTerminalAIStatus(status) {
1241
+ return status === "completed" || status === "stopped" || status === "error" || status === "awaiting_input";
1242
+ }
1243
+ 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
+ };
1253
+ }
1254
+ 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
+ };
1261
+ }
944
1262
  export {
945
1263
  ApiError,
946
1264
  LovableClient