@lovable.dev/sdk 0.1.4 → 0.1.7

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,3 +1,18 @@
1
+ // src/types.ts
2
+ var ApiError = class extends Error {
3
+ status;
4
+ type;
5
+ detail;
6
+ props;
7
+ constructor(status, message, type, detail, props) {
8
+ super(message);
9
+ this.status = status;
10
+ this.type = type;
11
+ this.detail = detail;
12
+ this.props = props;
13
+ }
14
+ };
15
+
1
16
  // src/client.ts
2
17
  var DEFAULT_BASE_URL = "https://api.lovable.dev";
3
18
  function normalizeBaseUrl(url) {
@@ -9,25 +24,36 @@ function normalizeBaseUrl(url) {
9
24
  return normalized;
10
25
  }
11
26
  var LovableClient = class {
12
- apiKey;
27
+ authHeaders;
13
28
  baseUrl;
29
+ extraHeaders;
14
30
  constructor(options) {
15
- if (!options.apiKey) {
16
- throw new Error("API key is required");
31
+ const hasApiKey = !!options.apiKey;
32
+ const hasBearerToken = !!options.bearerToken;
33
+ if (!hasApiKey && !hasBearerToken) {
34
+ throw new Error("Either apiKey or bearerToken is required");
35
+ }
36
+ if (hasApiKey && hasBearerToken) {
37
+ throw new Error("Provide either apiKey or bearerToken, not both");
17
38
  }
18
- this.apiKey = options.apiKey;
39
+ this.authHeaders = hasApiKey ? { "Lovable-API-Key": options.apiKey } : { Authorization: `Bearer ${options.bearerToken}` };
19
40
  this.baseUrl = normalizeBaseUrl(options.baseUrl);
41
+ this.extraHeaders = options.headers ?? {};
20
42
  }
21
- async request(method, path, body) {
43
+ async rawRequest(method, path, body) {
22
44
  const url = `${this.baseUrl}${path}`;
23
45
  const headers = {
24
- "Lovable-API-Key": this.apiKey,
25
- "Content-Type": "application/json"
46
+ ...this.authHeaders,
47
+ ...this.extraHeaders,
48
+ Accept: "application/json"
26
49
  };
50
+ if (body !== void 0) {
51
+ headers["Content-Type"] = "application/json";
52
+ }
27
53
  const response = await fetch(url, {
28
54
  method,
29
55
  headers,
30
- body: body ? JSON.stringify(body) : void 0
56
+ body: body !== void 0 ? JSON.stringify(body) : void 0
31
57
  });
32
58
  if (!response.ok) {
33
59
  let errorBody;
@@ -35,17 +61,24 @@ var LovableClient = class {
35
61
  errorBody = await response.json();
36
62
  } catch {
37
63
  }
38
- const error = new Error(errorBody?.title ?? `HTTP ${response.status}: ${response.statusText}`);
39
- error.status = response.status;
40
- error.type = errorBody?.title;
41
- error.detail = errorBody?.detail;
42
- throw error;
64
+ const message = errorBody?.message ?? errorBody?.title ?? `HTTP ${response.status}: ${response.statusText}`;
65
+ const type = errorBody?.type ?? errorBody?.title;
66
+ const detail = errorBody?.detail ?? errorBody?.details;
67
+ throw new ApiError(response.status, message, type, detail, errorBody?.props);
43
68
  }
69
+ return response;
70
+ }
71
+ async request(method, path, body) {
72
+ const response = await this.rawRequest(method, path, body);
44
73
  if (response.status === 204) {
45
74
  return void 0;
46
75
  }
47
76
  return response.json();
48
77
  }
78
+ async requestText(method, path) {
79
+ const response = await this.rawRequest(method, path);
80
+ return response.text();
81
+ }
49
82
  /**
50
83
  * Get the current authenticated user and their workspaces.
51
84
  * Useful for validating an API key and discovering workspace IDs.
@@ -68,25 +101,36 @@ var LovableClient = class {
68
101
  return response.workspace;
69
102
  }
70
103
  /**
71
- * List projects in a workspace
104
+ * List projects in a workspace.
105
+ * Supports full-text search, filtering by visibility/publish status/folder/creator,
106
+ * and pagination via offset or cursor.
72
107
  */
73
108
  async listProjects(workspaceId, options) {
74
109
  const params = new URLSearchParams();
75
- if (options?.limit) params.set("limit", options.limit.toString());
76
- if (options?.cursor) params.set("cursor", options.cursor);
110
+ if (options?.query) params.set("q", options.query);
77
111
  if (options?.visibility) params.set("visibility", options.visibility);
112
+ if (options?.publish_status) params.set("publish_status", options.publish_status);
113
+ if (options?.folder_id) params.set("folder_id", options.folder_id);
114
+ if (options?.user_id) params.set("user_id", options.user_id);
115
+ if (options?.sort_by) params.set("sort_by", options.sort_by);
116
+ if (options?.sort_order) params.set("sort_order", options.sort_order);
117
+ if (options?.viewed_by_me) params.set("viewed_by_me", "true");
118
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
119
+ if (options?.offset !== void 0) params.set("offset", String(options.offset));
120
+ if (options?.cursor) params.set("cursor", options.cursor);
78
121
  const query = params.toString();
79
122
  const path = `/v1/workspaces/${workspaceId}/projects${query ? `?${query}` : ""}`;
80
- const response = await this.request("GET", path);
81
- return response.projects ?? [];
123
+ return this.request("GET", path);
82
124
  }
83
125
  /**
84
126
  * Create a new project in a workspace
85
127
  */
86
128
  async createProject(workspaceId, options) {
87
- let uploadedFiles;
88
- if (options.files?.length) {
89
- uploadedFiles = await this.uploadFiles(options.files);
129
+ let fileRefs;
130
+ if (options.uploadedFiles?.length) {
131
+ fileRefs = options.uploadedFiles;
132
+ } else if (options.files?.length) {
133
+ fileRefs = await this.uploadFiles(options.files);
90
134
  }
91
135
  const body = {
92
136
  description: options.description,
@@ -96,39 +140,40 @@ var LovableClient = class {
96
140
  if (options.techStack) {
97
141
  body.tech_stack = options.techStack;
98
142
  }
99
- const hasFiles = uploadedFiles && uploadedFiles.length > 0;
100
- if (options.initialMessage && !hasFiles) {
143
+ if (options.selectedLibraries?.length) {
144
+ body.selected_libraries = options.selectedLibraries;
145
+ }
146
+ if (options.initialMessage) {
101
147
  body.initial_message = options.initialMessage;
102
148
  }
103
- const project = await this.request("POST", `/v1/workspaces/${workspaceId}/projects`, body);
104
- if (hasFiles && options.initialMessage) {
105
- const chatBody = {
106
- message: options.initialMessage,
107
- files: uploadedFiles
108
- };
109
- await this.request("POST", `/v1/projects/${project.id}/messages`, chatBody);
149
+ if (fileRefs?.length) {
150
+ body.files = fileRefs;
110
151
  }
152
+ const project = await this.request("POST", `/v1/workspaces/${workspaceId}/projects`, body);
111
153
  return project;
112
154
  }
113
155
  /**
114
- * Send a chat message to a project
156
+ * Send a chat message to a project.
115
157
  *
116
- * Note: This sends a message to the project's AI agent. The response is
117
- * asynchronous - the API accepts the message and processes it in the background.
158
+ * The API accepts the message and processes it in the background.
159
+ * Returns the message ID and status. Use `waitForMessageCompletion()`
160
+ * to poll for the AI response, or `waitForResponse()` for SSE streaming.
118
161
  */
119
162
  async chat(projectId, options) {
120
- let uploadedFiles;
121
- if (options.files?.length) {
122
- uploadedFiles = await this.uploadFiles(options.files);
163
+ let fileRefs;
164
+ if (options.uploadedFiles?.length) {
165
+ fileRefs = options.uploadedFiles;
166
+ } else if (options.files?.length) {
167
+ fileRefs = await this.uploadFiles(options.files);
123
168
  }
124
169
  const body = {
125
170
  message: options.message
126
171
  };
127
- if (uploadedFiles) {
128
- body.files = uploadedFiles;
172
+ if (fileRefs) {
173
+ body.files = fileRefs;
129
174
  }
130
- if (options.chatOnly) {
131
- body.chat_only = true;
175
+ if (options.planMode) {
176
+ body.plan_mode = true;
132
177
  }
133
178
  if (options.customModel) {
134
179
  body.custom_model_endpoint = options.customModel.endpoint;
@@ -141,7 +186,7 @@ var LovableClient = class {
141
186
  if (options.continuation) {
142
187
  body.continuation = options.continuation;
143
188
  }
144
- await this.request("POST", `/v1/projects/${projectId}/messages`, body);
189
+ return this.request("POST", `/v1/projects/${projectId}/messages`, body);
145
190
  }
146
191
  /**
147
192
  * Invite a user to a workspace as a collaborator
@@ -243,6 +288,260 @@ var LovableClient = class {
243
288
  async getDatabaseConnectionInfo(projectId) {
244
289
  return this.request("GET", `/v1/projects/${projectId}/database/connection-info`);
245
290
  }
291
+ // ---------------------------------------------------------------------------
292
+ // Messages
293
+ // ---------------------------------------------------------------------------
294
+ /**
295
+ * Get a message by ID. Returns the message content, status, and (for user messages)
296
+ * the AI response if available. Use to poll for completion after `chat()`.
297
+ */
298
+ async getMessage(projectId, messageId) {
299
+ return this.request("GET", `/v1/projects/${projectId}/messages/${messageId}`);
300
+ }
301
+ /**
302
+ * Poll for message completion. Waits until the AI response reaches a terminal
303
+ * status (completed, stopped) or the timeout expires.
304
+ *
305
+ * Handles edge cases: 404 grace period for race conditions between queue
306
+ * dequeue and event creation, and queue pause detection.
307
+ */
308
+ async waitForMessageCompletion(projectId, messageId, options) {
309
+ const pollInterval = options?.pollInterval ?? 3e3;
310
+ const timeout = options?.timeout ?? 6e5;
311
+ const deadline = Date.now() + timeout;
312
+ let notFoundSince = null;
313
+ const notFoundGraceMs = 15e3;
314
+ while (Date.now() < deadline) {
315
+ try {
316
+ const msg = await this.getMessage(projectId, messageId);
317
+ notFoundSince = null;
318
+ if (msg.status === "queued") {
319
+ if (msg.queue_paused && msg.queue_pause_reason !== "hitl_tool") {
320
+ return {
321
+ status: "error",
322
+ message_id: messageId,
323
+ content: "",
324
+ 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.`
325
+ };
326
+ }
327
+ await sleep(pollInterval);
328
+ continue;
329
+ }
330
+ const ai = msg.response;
331
+ if (ai) {
332
+ if (ai.status === "completed" || ai.status === "stopped") {
333
+ return {
334
+ status: ai.status === "completed" ? "completed" : "error",
335
+ message_id: ai.message_id,
336
+ content: ai.content,
337
+ edit_id: ai.edit_id,
338
+ commit_sha: ai.commit_sha,
339
+ summary: ai.summary,
340
+ cost_credits: ai.cost_credits
341
+ };
342
+ }
343
+ }
344
+ if (msg.role === "assistant") {
345
+ if (msg.status === "completed" || msg.status === "stopped") {
346
+ return {
347
+ status: msg.status === "completed" ? "completed" : "error",
348
+ message_id: msg.message_id,
349
+ content: msg.content,
350
+ edit_id: msg.edit_id,
351
+ commit_sha: msg.commit_sha,
352
+ summary: msg.summary,
353
+ cost_credits: msg.cost_credits
354
+ };
355
+ }
356
+ }
357
+ await sleep(pollInterval);
358
+ } catch (err) {
359
+ if (err instanceof ApiError && err.status === 404) {
360
+ if (notFoundSince === null) {
361
+ notFoundSince = Date.now();
362
+ }
363
+ if (Date.now() - notFoundSince < notFoundGraceMs) {
364
+ await sleep(pollInterval);
365
+ continue;
366
+ }
367
+ return {
368
+ status: "error",
369
+ message_id: messageId,
370
+ content: "",
371
+ error: "Message not found. It may have been deleted from the queue."
372
+ };
373
+ }
374
+ await sleep(pollInterval);
375
+ }
376
+ }
377
+ return {
378
+ status: "timeout",
379
+ message_id: messageId,
380
+ content: "",
381
+ error: `Agent did not finish within ${timeout / 1e3}s`
382
+ };
383
+ }
384
+ // ---------------------------------------------------------------------------
385
+ // Knowledge
386
+ // ---------------------------------------------------------------------------
387
+ /** Get workspace knowledge (custom instructions for the AI agent). */
388
+ async getWorkspaceKnowledge(workspaceId) {
389
+ return this.request("GET", `/v1/workspaces/${workspaceId}/knowledge`);
390
+ }
391
+ /** Set workspace knowledge. Max 10,000 characters. */
392
+ async setWorkspaceKnowledge(workspaceId, content) {
393
+ return this.request("PUT", `/v1/workspaces/${workspaceId}/knowledge`, { content });
394
+ }
395
+ /** Get project knowledge (custom instructions for the AI agent). */
396
+ async getProjectKnowledge(projectId) {
397
+ return this.request("GET", `/v1/projects/${projectId}/knowledge`);
398
+ }
399
+ /** Set project knowledge. Max 10,000 characters. */
400
+ async setProjectKnowledge(projectId, content) {
401
+ return this.request("PUT", `/v1/projects/${projectId}/knowledge`, { content });
402
+ }
403
+ // ---------------------------------------------------------------------------
404
+ // Git operations
405
+ // ---------------------------------------------------------------------------
406
+ /**
407
+ * Get the structured diff for a message or commit.
408
+ * Pass `messageId` to get the diff for a specific AI message,
409
+ * or `sha` for a specific commit.
410
+ */
411
+ async getDiff(projectId, params) {
412
+ const qs = new URLSearchParams();
413
+ if (params.messageId) qs.set("message_id", params.messageId);
414
+ if (params.sha) qs.set("sha", params.sha);
415
+ if (params.baseSha) qs.set("base_sha", params.baseSha);
416
+ return this.request("GET", `/v1/projects/${projectId}/git/diff?${qs.toString()}`);
417
+ }
418
+ /** List all files in a project at a specific git ref. */
419
+ async listFiles(projectId, ref) {
420
+ const qs = new URLSearchParams({ ref });
421
+ return this.request("GET", `/v1/projects/${projectId}/git/files?${qs.toString()}`);
422
+ }
423
+ /** Read the raw content of a single file at a specific git ref. Returns text. */
424
+ async readFile(projectId, path, ref) {
425
+ const qs = new URLSearchParams({ path, ref });
426
+ return this.requestText("GET", `/v1/projects/${projectId}/git/file?${qs.toString()}`);
427
+ }
428
+ // ---------------------------------------------------------------------------
429
+ // Edits
430
+ // ---------------------------------------------------------------------------
431
+ /** List the edit history of a project. */
432
+ async listEdits(projectId, params) {
433
+ const qs = new URLSearchParams();
434
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
435
+ if (params?.before) qs.set("before", params.before);
436
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
437
+ return this.request("GET", `/v1/projects/${projectId}/edits${suffix}`);
438
+ }
439
+ // ---------------------------------------------------------------------------
440
+ // File upload
441
+ // ---------------------------------------------------------------------------
442
+ /** Get a presigned URL for uploading a file. Returns the upload URL and file ID. */
443
+ async getFileUploadUrl(params) {
444
+ return this.request("POST", "/v1/files/upload-url", params);
445
+ }
446
+ // ---------------------------------------------------------------------------
447
+ // Visibility
448
+ // ---------------------------------------------------------------------------
449
+ /** Set a project's visibility (draft, private, or public). */
450
+ async setProjectVisibility(projectId, visibility) {
451
+ return this.request("PUT", `/v1/projects/${projectId}/visibility`, { visibility });
452
+ }
453
+ /** Set a folder's visibility (personal or workspace). */
454
+ async setFolderVisibility(workspaceId, folderId, visibility) {
455
+ return this.request("PUT", `/v1/workspaces/${workspaceId}/folders/${folderId}/visibility`, {
456
+ visibility
457
+ });
458
+ }
459
+ // ---------------------------------------------------------------------------
460
+ // Library & template projects
461
+ // ---------------------------------------------------------------------------
462
+ /** List available design system library projects in a workspace. */
463
+ async listLibraryProjects(workspaceId) {
464
+ return this.request(
465
+ "GET",
466
+ `/v1/workspaces/${workspaceId}/available-library-projects`
467
+ );
468
+ }
469
+ /** List available template projects in a workspace. */
470
+ async listTemplateProjects(workspaceId) {
471
+ return this.request(
472
+ "GET",
473
+ `/v1/workspaces/${workspaceId}/available-template-projects`
474
+ );
475
+ }
476
+ // ---------------------------------------------------------------------------
477
+ // Connectors (MCP servers)
478
+ // ---------------------------------------------------------------------------
479
+ /** List all connectors in a workspace. */
480
+ async listConnectors(workspaceId) {
481
+ return this.request("GET", `/v1/workspaces/${workspaceId}/connectors`);
482
+ }
483
+ /** Add a connector to a workspace. The server URL is tested before saving. */
484
+ async addConnector(workspaceId, body) {
485
+ return this.request("POST", `/v1/workspaces/${workspaceId}/connectors`, body);
486
+ }
487
+ /** Remove a connector from a workspace. */
488
+ async removeConnector(workspaceId, connectorId) {
489
+ return this.request("DELETE", `/v1/workspaces/${workspaceId}/connectors/${connectorId}`);
490
+ }
491
+ /** Browse available connector templates. */
492
+ async listAvailableConnectors(workspaceId) {
493
+ return this.request(
494
+ "GET",
495
+ `/v1/workspaces/${workspaceId}/available-connectors`
496
+ );
497
+ }
498
+ // ---------------------------------------------------------------------------
499
+ // Connectors
500
+ // ---------------------------------------------------------------------------
501
+ /** List standard (OAuth-based) connectors in a workspace. */
502
+ async listStandardConnectors(workspaceId) {
503
+ return this.request(
504
+ "GET",
505
+ `/v1/workspaces/${workspaceId}/connectors/standard`
506
+ );
507
+ }
508
+ /** List seamless (zero-config) connectors in a workspace. */
509
+ async listSeamlessConnectors(workspaceId) {
510
+ return this.request(
511
+ "GET",
512
+ `/v1/workspaces/${workspaceId}/connectors/seamless`
513
+ );
514
+ }
515
+ /** List MCP connectors in a workspace. */
516
+ async listMCPConnectors(workspaceId) {
517
+ return this.request(
518
+ "GET",
519
+ `/v1/workspaces/${workspaceId}/connectors/mcp`
520
+ );
521
+ }
522
+ /** List authenticated connections (accounts) in a workspace. */
523
+ async listConnections(workspaceId, params) {
524
+ const qs = new URLSearchParams();
525
+ if (params?.connector_id) qs.set("connector_id", params.connector_id);
526
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
527
+ return this.request("GET", `/v1/workspaces/${workspaceId}/connections${suffix}`);
528
+ }
529
+ // ---------------------------------------------------------------------------
530
+ // Analytics
531
+ // ---------------------------------------------------------------------------
532
+ /** Get historical analytics for a published project. */
533
+ async getProjectAnalytics(projectId, params) {
534
+ const qs = new URLSearchParams({
535
+ startDate: params.startDate,
536
+ endDate: params.endDate
537
+ });
538
+ if (params.granularity) qs.set("granularity", params.granularity);
539
+ return this.request("GET", `/v1/projects/${projectId}/analytics?${qs.toString()}`);
540
+ }
541
+ /** Get real-time visitor trend for a published project. */
542
+ async getProjectAnalyticsTrend(projectId) {
543
+ return this.request("GET", `/v1/projects/${projectId}/analytics/trend`);
544
+ }
246
545
  /**
247
546
  * Publish a project.
248
547
  *
@@ -280,9 +579,9 @@ var LovableClient = class {
280
579
  workspace_id: options.workspaceId,
281
580
  include_history: options.includeHistory,
282
581
  include_custom_knowledge: options.includeCustomKnowledge,
582
+ project_name: options.projectName,
283
583
  skip_initial_remix_message: options.skipInitialRemixMessage,
284
- skip_integrations: options.skipIntegrations,
285
- include_agent_state: options.includeAgentState
584
+ skip_integrations: options.skipIntegrations
286
585
  };
287
586
  if (options.messageId) {
288
587
  body.message_id = options.messageId;
@@ -296,7 +595,7 @@ var LovableClient = class {
296
595
  headless: true
297
596
  };
298
597
  }
299
- const response = await this.request("POST", `/projects/${sourceProjectId}/remix/init`, body);
598
+ const response = await this.request("POST", `/v1/projects/${sourceProjectId}/remix/init`, body);
300
599
  return response.job_id;
301
600
  }
302
601
  /**
@@ -319,7 +618,7 @@ var LovableClient = class {
319
618
  while (true) {
320
619
  const progress = await this.request(
321
620
  "GET",
322
- `/projects/${sourceProjectId}/remix/progress?job_id=${encodeURIComponent(jobId)}`
621
+ `/v1/projects/${sourceProjectId}/remix/progress?job_id=${encodeURIComponent(jobId)}`
323
622
  );
324
623
  options?.onProgress?.(progress.status, progress.step);
325
624
  if (progress.status === "completed" && progress.result) {
@@ -389,14 +688,13 @@ var LovableClient = class {
389
688
  try {
390
689
  const response = await fetch(url, {
391
690
  headers: {
392
- "Lovable-API-Key": this.apiKey
691
+ ...this.authHeaders,
692
+ ...this.extraHeaders
393
693
  },
394
694
  signal: controller.signal
395
695
  });
396
696
  if (!response.ok) {
397
- const error = new Error(`Failed to connect to message stream: HTTP ${response.status}`);
398
- error.status = response.status;
399
- throw error;
697
+ throw new ApiError(response.status, `Failed to connect to message stream: HTTP ${response.status}`);
400
698
  }
401
699
  if (!response.body) {
402
700
  throw new Error("Response body is not readable");
@@ -609,6 +907,7 @@ function sleep(ms) {
609
907
  return new Promise((resolve) => setTimeout(resolve, ms));
610
908
  }
611
909
  export {
910
+ ApiError,
612
911
  LovableClient
613
912
  };
614
913
  //# sourceMappingURL=index.js.map