@lovable.dev/sdk 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -18
- package/dist/index.d.ts +160 -1
- package/dist/index.js +203 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,18 +24,104 @@ const workspaces = await client.listWorkspaces();
|
|
|
24
24
|
|
|
25
25
|
// 1. Create a project
|
|
26
26
|
const project = await client.createProject(workspaces[0].id, {
|
|
27
|
-
description: "
|
|
27
|
+
description: "Best todo app",
|
|
28
|
+
initialMessage: "Create a todo app with authentication"
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
// 2.
|
|
31
|
+
// 2. Wait for the AI response and get the preview URL
|
|
32
|
+
const response = await client.waitForResponse(project.id);
|
|
33
|
+
console.log(response.content); // AI's response text
|
|
34
|
+
console.log(response.messageId); // AI message ID (for traces)
|
|
35
|
+
console.log(response.previewUrl); // Preview URL for the project
|
|
36
|
+
|
|
37
|
+
// 3. Send a follow-up chat message
|
|
31
38
|
await client.chat(project.id, {
|
|
32
|
-
message: "
|
|
39
|
+
message: "Add a footer",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 4. Send a message with file attachments
|
|
43
|
+
import { readFile } from "fs/promises";
|
|
44
|
+
const imageData = await readFile("design.png");
|
|
45
|
+
await client.chat(project.id, {
|
|
46
|
+
message: "Update the hero section to match this design",
|
|
47
|
+
files: [{ name: "design.png", data: imageData, type: "image/png" }],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 5. Publish the project and get the live URL
|
|
51
|
+
await client.publish(project.id);
|
|
52
|
+
const published = await client.waitForProjectPublished(project.id);
|
|
53
|
+
console.log(published.url); // Live public URL
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Remixing a project at a specific message
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const client = new LovableClient({ apiKey: "lov_your-api-key" });
|
|
60
|
+
|
|
61
|
+
// Remix a project at the state just before a specific message
|
|
62
|
+
const jobId = await client.remixProject("source-project-id", {
|
|
63
|
+
workspaceId: "target-workspace-id",
|
|
64
|
+
messageId: "message-id-to-snapshot-at",
|
|
65
|
+
// remixMode: "including", // use "including" to keep the message and its AI response
|
|
66
|
+
includeHistory: true,
|
|
67
|
+
includeCustomKnowledge: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Wait for the remix to complete
|
|
71
|
+
const { projectId } = await client.waitForRemix("source-project-id", jobId, {
|
|
72
|
+
onProgress: (status, step) => console.log(`Remix: ${status}`, step),
|
|
33
73
|
});
|
|
34
74
|
|
|
35
|
-
|
|
75
|
+
console.log(`Remixed project: ${projectId}`);
|
|
76
|
+
|
|
77
|
+
// Send a follow-up message to the remixed project
|
|
78
|
+
await client.chat(projectId, { message: "Add dark mode" });
|
|
79
|
+
const response = await client.waitForResponse(projectId);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Using a custom model
|
|
83
|
+
|
|
84
|
+
You can route the main agent to a custom OpenAI-compatible endpoint for eval and RL workflows:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
await client.chat(project.id, {
|
|
88
|
+
message: "Add a dark mode toggle",
|
|
89
|
+
customModel: {
|
|
90
|
+
endpoint: "https://my-vllm.example.com/v1",
|
|
91
|
+
apiKey: "sk-...",
|
|
92
|
+
modelName: "meta-llama/Llama-3.3-70B-Instruct",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Fetching message traces
|
|
98
|
+
|
|
99
|
+
Retrieve Braintrust trace spans for a specific AI response:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
36
102
|
const response = await client.waitForResponse(project.id);
|
|
37
|
-
|
|
38
|
-
|
|
103
|
+
|
|
104
|
+
// Fetch all traces for the message
|
|
105
|
+
const traces = await client.getMessageTraces(project.id, response.messageId);
|
|
106
|
+
console.log(traces.spans);
|
|
107
|
+
|
|
108
|
+
// Filter by purpose (e.g. only the main agent span)
|
|
109
|
+
const agentTraces = await client.getMessageTraces(project.id, response.messageId, {
|
|
110
|
+
purposes: ["main_agent"],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Batch fetch traces for multiple messages
|
|
114
|
+
const result = await client.getMessageTracesBatch([
|
|
115
|
+
{ projectId: "proj-1", messageId: "msg-1" },
|
|
116
|
+
{ projectId: "proj-2", messageId: "msg-2" },
|
|
117
|
+
], { purposes: ["main_agent"], concurrency: 5 });
|
|
118
|
+
|
|
119
|
+
for (const [messageId, trace] of result.traces) {
|
|
120
|
+
console.log(messageId, trace.spans.length);
|
|
121
|
+
}
|
|
122
|
+
for (const [messageId, error] of result.errors) {
|
|
123
|
+
console.error(messageId, error.message);
|
|
124
|
+
}
|
|
39
125
|
```
|
|
40
126
|
|
|
41
127
|
## API Reference
|
|
@@ -81,6 +167,7 @@ Options:
|
|
|
81
167
|
- `visibility` (optional): Project visibility (`"draft"` | `"private"` | `"public"`)
|
|
82
168
|
- `templateProjectId` (optional): ID of a template project to clone
|
|
83
169
|
- `initialMessage` (optional): Initial chat message to send to the AI agent
|
|
170
|
+
- `files` (optional): Array of files to attach (browser `File` objects or `FileInput` objects)
|
|
84
171
|
|
|
85
172
|
##### `chat(projectId: string, options): Promise<void>`
|
|
86
173
|
|
|
@@ -90,6 +177,8 @@ Options:
|
|
|
90
177
|
|
|
91
178
|
- `message` (required): The message to send
|
|
92
179
|
- `chatOnly` (optional): If true, only chat without making code changes
|
|
180
|
+
- `files` (optional): Array of files to attach (browser `File` objects or `FileInput` objects)
|
|
181
|
+
- `customModel` (optional): Route the main agent to a custom OpenAI-compatible endpoint (see `CustomModelConfig`)
|
|
93
182
|
|
|
94
183
|
Note: This is an asynchronous operation. The API accepts the message and processes it in the background. Use `waitForResponse()` to wait for the AI's reply.
|
|
95
184
|
|
|
@@ -102,6 +191,7 @@ Use this after `chat()` or after `createProject()` with `initialMessage`.
|
|
|
102
191
|
Returns:
|
|
103
192
|
|
|
104
193
|
- `content` (string): The AI's full response text
|
|
194
|
+
- `messageId` (string): The AI message ID (use with `getMessageTraces()`)
|
|
105
195
|
- `previewUrl` (string): The project's preview URL
|
|
106
196
|
|
|
107
197
|
Options:
|
|
@@ -114,26 +204,23 @@ Throws an error if the stream fails or timeout is reached.
|
|
|
114
204
|
|
|
115
205
|
Get the preview URL for a project. This is a synchronous method that constructs the URL from the project ID.
|
|
116
206
|
|
|
117
|
-
##### `
|
|
207
|
+
##### `publish(projectId: string, options?): Promise<DeploymentResponse>`
|
|
118
208
|
|
|
119
|
-
|
|
209
|
+
Publish (deploy) a project to make it publicly accessible. The deployment runs asynchronously — use `waitForProjectPublished()` to wait for completion.
|
|
120
210
|
|
|
121
211
|
Options:
|
|
122
212
|
|
|
123
|
-
- `
|
|
124
|
-
- `role` (optional): Role to assign (`"admin"` | `"collaborator"` | `"member"` | `"viewer"`)
|
|
213
|
+
- `name` (optional): Custom slug for the published URL
|
|
125
214
|
|
|
126
|
-
|
|
215
|
+
Returns:
|
|
127
216
|
|
|
128
|
-
|
|
217
|
+
- `status` (string): Deployment status
|
|
218
|
+
- `deployment_id` (string): The deployment ID
|
|
219
|
+
- `url` (string): The published URL (may not be available until deployment completes)
|
|
129
220
|
|
|
130
|
-
##### `
|
|
221
|
+
##### `getPublishedUrl(projectId: string): Promise<string | null>`
|
|
131
222
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
##### `getProject(projectId: string): Promise<ProjectResponse>`
|
|
135
|
-
|
|
136
|
-
Get project details by ID.
|
|
223
|
+
Get the published URL for a project, or `null` if not published. Fetches the latest project details to check publication status.
|
|
137
224
|
|
|
138
225
|
##### `waitForProjectReady(projectId: string, options?): Promise<ProjectResponse>`
|
|
139
226
|
|
|
@@ -159,6 +246,91 @@ Options:
|
|
|
159
246
|
|
|
160
247
|
Throws an error if timeout is reached.
|
|
161
248
|
|
|
249
|
+
##### `remixProject(sourceProjectId: string, options): Promise<string>`
|
|
250
|
+
|
|
251
|
+
Remix (fork) an existing project, optionally at a specific message point in time.
|
|
252
|
+
|
|
253
|
+
When `messageId` is provided, the remix captures the project state as it was just before that message was processed (default `remixMode: "before"`). Set `remixMode: "including"` to include the message and its AI response in the remix. Without `messageId`, the full current state is remixed.
|
|
254
|
+
|
|
255
|
+
Returns the remix job ID for polling with `waitForRemix()`.
|
|
256
|
+
|
|
257
|
+
Options:
|
|
258
|
+
|
|
259
|
+
- `workspaceId` (required): Target workspace for the new project
|
|
260
|
+
- `messageId` (optional): Message ID to snapshot at — by default the remix reflects the project state just before this message
|
|
261
|
+
- `remixMode` (optional): `"before"` (default) captures state before the message; `"including"` captures state after the message and its AI response
|
|
262
|
+
- `includeHistory` (optional): Whether to preserve chat history
|
|
263
|
+
- `includeCustomKnowledge` (optional): Whether to copy custom instructions/knowledge
|
|
264
|
+
- `initialMessage` (optional): Initial chat message to send after remix completes
|
|
265
|
+
- `integrationParameters` (optional): Integration-specific parameter values
|
|
266
|
+
|
|
267
|
+
##### `waitForRemix(sourceProjectId: string, jobId: string, options?): Promise<RemixResult>`
|
|
268
|
+
|
|
269
|
+
Wait for a remix operation to complete. Polls until the job finishes.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
|
|
273
|
+
- `projectId` (string): The ID of the newly created project
|
|
274
|
+
|
|
275
|
+
Options:
|
|
276
|
+
|
|
277
|
+
- `pollInterval` (optional): Time between polls in ms (default: 2000)
|
|
278
|
+
- `timeout` (optional): Maximum time to wait in ms (default: 300000 = 5 minutes)
|
|
279
|
+
- `onProgress` (optional): Callback with `(status, step?)` for progress updates
|
|
280
|
+
|
|
281
|
+
Throws an error if the remix fails or timeout is reached.
|
|
282
|
+
|
|
283
|
+
##### `getMessageTraces(projectId: string, messageId: string, options?): Promise<MessageTracesResponse>`
|
|
284
|
+
|
|
285
|
+
Fetch Braintrust trace spans for a specific chat message. The `messageId` is available from the `ChatResponse` returned by `waitForResponse()`.
|
|
286
|
+
|
|
287
|
+
When a purpose has multiple spans (e.g. `main_agent` across turns), only the last span is returned — it contains the full accumulated context.
|
|
288
|
+
|
|
289
|
+
Options:
|
|
290
|
+
|
|
291
|
+
- `purposes` (optional): Filter spans by purpose (e.g. `["main_agent", "knowledge_rag"]`)
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
|
|
295
|
+
- `message_id` (string): The message ID
|
|
296
|
+
- `braintrust_span_id` (string): The Braintrust span ID
|
|
297
|
+
- `root_span_id` (string): The root span ID
|
|
298
|
+
- `spans` (TraceSpan[]): The filtered trace spans
|
|
299
|
+
|
|
300
|
+
##### `getMessageTracesBatch(queries, options?): Promise<BatchTracesResult>`
|
|
301
|
+
|
|
302
|
+
Fetch traces for multiple messages across projects in parallel.
|
|
303
|
+
|
|
304
|
+
- `queries`: Array of `{ projectId, messageId }` to fetch
|
|
305
|
+
- `options.purposes` (optional): Filter spans by purpose (applied to all queries)
|
|
306
|
+
- `options.concurrency` (optional): Max parallel requests (default: 5)
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
|
|
310
|
+
- `traces`: Map of messageId → `MessageTracesResponse`
|
|
311
|
+
- `errors`: Map of messageId → `Error` (for failed requests)
|
|
312
|
+
|
|
313
|
+
##### `inviteCollaborator(workspaceId: string, options): Promise<WorkspaceMembershipResponse>`
|
|
314
|
+
|
|
315
|
+
Invite a user to a workspace.
|
|
316
|
+
|
|
317
|
+
Options:
|
|
318
|
+
|
|
319
|
+
- `email` (required): Email address of the user to invite
|
|
320
|
+
- `role` (optional): Role to assign (`"admin"` | `"collaborator"` | `"member"` | `"viewer"`)
|
|
321
|
+
|
|
322
|
+
##### `listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMembershipResponse[]>`
|
|
323
|
+
|
|
324
|
+
List all members of a workspace.
|
|
325
|
+
|
|
326
|
+
##### `removeWorkspaceMember(workspaceId: string, userId: string): Promise<void>`
|
|
327
|
+
|
|
328
|
+
Remove a member from a workspace.
|
|
329
|
+
|
|
330
|
+
##### `getProject(projectId: string): Promise<ProjectResponse>`
|
|
331
|
+
|
|
332
|
+
Get project details by ID.
|
|
333
|
+
|
|
162
334
|
## Types
|
|
163
335
|
|
|
164
336
|
The SDK exports TypeScript types for all API responses. See `src/types.ts` for the full list.
|
|
@@ -168,6 +340,44 @@ import type {
|
|
|
168
340
|
WorkspaceWithMembership,
|
|
169
341
|
ProjectResponse,
|
|
170
342
|
CreateProjectOptions,
|
|
343
|
+
ChatResponse,
|
|
344
|
+
CustomModelConfig,
|
|
345
|
+
FileInput,
|
|
346
|
+
TracePurpose,
|
|
347
|
+
TraceSpan,
|
|
348
|
+
MessageTracesResponse,
|
|
171
349
|
// ... etc
|
|
172
350
|
} from "@lovable.dev/sdk";
|
|
173
351
|
```
|
|
352
|
+
|
|
353
|
+
### `FileInput`
|
|
354
|
+
|
|
355
|
+
For Node.js or non-browser environments, use `FileInput` instead of the browser `File` API:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
interface FileInput {
|
|
359
|
+
name: string; // Original file name (e.g., "screenshot.png")
|
|
360
|
+
data: Blob | ArrayBuffer | Uint8Array; // File contents
|
|
361
|
+
type: string; // MIME type (e.g., "image/png")
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### `CustomModelConfig`
|
|
366
|
+
|
|
367
|
+
Configuration for routing the main agent to a custom OpenAI-compatible endpoint:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
interface CustomModelConfig {
|
|
371
|
+
endpoint: string; // Base URL (e.g., "https://my-vllm.example.com/v1")
|
|
372
|
+
apiKey: string; // API key for the endpoint
|
|
373
|
+
modelName: string; // Model identifier (e.g., "meta-llama/Llama-3.3-70B-Instruct")
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### `TracePurpose`
|
|
378
|
+
|
|
379
|
+
Available trace span purposes:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
type TracePurpose = "main_agent" | "codebase_rag" | "knowledge_rag" | "review";
|
|
383
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -116,6 +116,30 @@ interface ProjectResponse {
|
|
|
116
116
|
feature_rank?: number;
|
|
117
117
|
feature_source?: string;
|
|
118
118
|
}
|
|
119
|
+
interface UnscopedFile {
|
|
120
|
+
file_id: string;
|
|
121
|
+
type: "user_upload";
|
|
122
|
+
file_name?: string;
|
|
123
|
+
mime_type?: string;
|
|
124
|
+
}
|
|
125
|
+
interface FileInput {
|
|
126
|
+
name: string;
|
|
127
|
+
data: Blob | ArrayBuffer | Uint8Array;
|
|
128
|
+
type: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Configuration to use a custom OpenAI-compatible model as the main agent.
|
|
132
|
+
* Enables eval and RL workflows where candidate models are benchmarked
|
|
133
|
+
* against the full Lovable agent pipeline.
|
|
134
|
+
*/
|
|
135
|
+
interface CustomModelConfig {
|
|
136
|
+
/** Base URL of the OpenAI-compatible API (e.g. "https://my-vllm.example.com/v1") */
|
|
137
|
+
endpoint: string;
|
|
138
|
+
/** API key for the custom endpoint */
|
|
139
|
+
apiKey: string;
|
|
140
|
+
/** Model identifier sent in the request (e.g. "meta-llama/Llama-3.3-70B-Instruct") */
|
|
141
|
+
modelName: string;
|
|
142
|
+
}
|
|
119
143
|
interface ChatRequest {
|
|
120
144
|
id: string;
|
|
121
145
|
message: string;
|
|
@@ -130,6 +154,10 @@ interface ChatRequest {
|
|
|
130
154
|
view_description?: string;
|
|
131
155
|
prev_session_id?: string;
|
|
132
156
|
is_creation?: boolean;
|
|
157
|
+
files?: UnscopedFile[];
|
|
158
|
+
custom_model_endpoint?: string;
|
|
159
|
+
custom_model_api_key?: string;
|
|
160
|
+
custom_model_name?: string;
|
|
133
161
|
}
|
|
134
162
|
interface CreateProjectBody {
|
|
135
163
|
description: string;
|
|
@@ -154,6 +182,7 @@ interface CreateProjectOptions {
|
|
|
154
182
|
visibility?: ProjectVisibility;
|
|
155
183
|
templateProjectId?: string;
|
|
156
184
|
initialMessage?: string;
|
|
185
|
+
files?: (File | FileInput)[];
|
|
157
186
|
}
|
|
158
187
|
interface InviteCollaboratorOptions {
|
|
159
188
|
email: string;
|
|
@@ -162,6 +191,8 @@ interface InviteCollaboratorOptions {
|
|
|
162
191
|
interface ChatMessageOptions {
|
|
163
192
|
message: string;
|
|
164
193
|
chatOnly?: boolean;
|
|
194
|
+
files?: (File | FileInput)[];
|
|
195
|
+
customModel?: CustomModelConfig;
|
|
165
196
|
}
|
|
166
197
|
interface LovableClientOptions {
|
|
167
198
|
apiKey: string;
|
|
@@ -185,10 +216,72 @@ interface DeploymentResponse {
|
|
|
185
216
|
interface ChatResponse {
|
|
186
217
|
content: string;
|
|
187
218
|
previewUrl: string;
|
|
219
|
+
messageId: string;
|
|
188
220
|
}
|
|
189
221
|
interface ChatResponseOptions {
|
|
190
222
|
timeout?: number;
|
|
191
223
|
}
|
|
224
|
+
type TracePurpose = "main_agent" | "codebase_rag" | "knowledge_rag" | "review";
|
|
225
|
+
interface TraceSpan {
|
|
226
|
+
span_id: string;
|
|
227
|
+
root_span_id?: string;
|
|
228
|
+
span_parents?: unknown;
|
|
229
|
+
span_name?: string;
|
|
230
|
+
span_type?: string;
|
|
231
|
+
purpose?: string;
|
|
232
|
+
subpurpose?: string;
|
|
233
|
+
created?: string;
|
|
234
|
+
input?: unknown;
|
|
235
|
+
output?: unknown;
|
|
236
|
+
error?: unknown;
|
|
237
|
+
metadata?: unknown;
|
|
238
|
+
metrics?: unknown;
|
|
239
|
+
scores?: unknown;
|
|
240
|
+
tags?: unknown;
|
|
241
|
+
}
|
|
242
|
+
interface MessageTracesResponse {
|
|
243
|
+
message_id: string;
|
|
244
|
+
braintrust_span_id: string;
|
|
245
|
+
root_span_id: string;
|
|
246
|
+
response_message_id?: string;
|
|
247
|
+
spans: TraceSpan[];
|
|
248
|
+
}
|
|
249
|
+
interface GetMessageTracesOptions {
|
|
250
|
+
purposes?: TracePurpose[];
|
|
251
|
+
}
|
|
252
|
+
interface TraceQuery {
|
|
253
|
+
projectId: string;
|
|
254
|
+
messageId: string;
|
|
255
|
+
}
|
|
256
|
+
interface BatchTracesResult {
|
|
257
|
+
traces: Map<string, MessageTracesResponse>;
|
|
258
|
+
errors: Map<string, Error>;
|
|
259
|
+
}
|
|
260
|
+
type RemixJobStatus = "unknown" | "preparing" | "running" | "completed" | "error";
|
|
261
|
+
type RemixJobStep = "starting" | "creating_new_project" | "remixing_integration" | "finalizing" | "completed";
|
|
262
|
+
interface RemixJobStepInfo {
|
|
263
|
+
step: RemixJobStep;
|
|
264
|
+
integration_name?: string;
|
|
265
|
+
status?: string;
|
|
266
|
+
}
|
|
267
|
+
type RemixMode = "before" | "including";
|
|
268
|
+
interface RemixProjectOptions {
|
|
269
|
+
workspaceId: string;
|
|
270
|
+
messageId?: string;
|
|
271
|
+
remixMode?: RemixMode;
|
|
272
|
+
includeHistory?: boolean;
|
|
273
|
+
includeCustomKnowledge?: boolean;
|
|
274
|
+
initialMessage?: string;
|
|
275
|
+
integrationParameters?: Record<string, Record<string, string>>;
|
|
276
|
+
}
|
|
277
|
+
interface RemixResult {
|
|
278
|
+
projectId: string;
|
|
279
|
+
}
|
|
280
|
+
interface RemixWaitOptions {
|
|
281
|
+
pollInterval?: number;
|
|
282
|
+
timeout?: number;
|
|
283
|
+
onProgress?: (status: RemixJobStatus, step?: RemixJobStepInfo) => void;
|
|
284
|
+
}
|
|
192
285
|
|
|
193
286
|
declare class LovableClient {
|
|
194
287
|
private readonly apiKey;
|
|
@@ -269,6 +362,39 @@ declare class LovableClient {
|
|
|
269
362
|
publish(projectId: string, options?: {
|
|
270
363
|
name?: string;
|
|
271
364
|
}): Promise<DeploymentResponse>;
|
|
365
|
+
/**
|
|
366
|
+
* Remix (fork) an existing project, optionally at a specific message point in time.
|
|
367
|
+
*
|
|
368
|
+
* When `messageId` is provided, the remix captures the project state as it was
|
|
369
|
+
* just before that message was processed (default). Set `remixMode: "including"`
|
|
370
|
+
* to include the message and its AI response in the remix.
|
|
371
|
+
* Without `messageId`, the full current state is remixed.
|
|
372
|
+
*
|
|
373
|
+
* @param sourceProjectId - The project to remix from
|
|
374
|
+
* @param options.workspaceId - Target workspace for the new project
|
|
375
|
+
* @param options.messageId - Optional message ID to snapshot at
|
|
376
|
+
* @param options.remixMode - "before" (default): state before the message; "including": state after the message and its AI response
|
|
377
|
+
* @param options.includeHistory - Whether to preserve chat history (default: false)
|
|
378
|
+
* @param options.includeCustomKnowledge - Whether to copy custom instructions (default: false)
|
|
379
|
+
* @param options.initialMessage - Optional initial message to send after remix
|
|
380
|
+
* @param options.integrationParameters - Integration-specific parameters
|
|
381
|
+
* @returns The remix job ID for polling progress
|
|
382
|
+
*/
|
|
383
|
+
remixProject(sourceProjectId: string, options: RemixProjectOptions): Promise<string>;
|
|
384
|
+
/**
|
|
385
|
+
* Wait for a remix operation to complete.
|
|
386
|
+
*
|
|
387
|
+
* Polls the remix progress endpoint until the job reaches "completed" or "error" status.
|
|
388
|
+
*
|
|
389
|
+
* @param sourceProjectId - The source project ID (used for the progress endpoint)
|
|
390
|
+
* @param jobId - The job ID returned by `remixProject()`
|
|
391
|
+
* @param options.pollInterval - Time between polls in ms (default: 2000)
|
|
392
|
+
* @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
|
|
393
|
+
* @param options.onProgress - Optional callback for status/step updates
|
|
394
|
+
* @returns The new project ID
|
|
395
|
+
* @throws Error if the remix fails or timeout is reached
|
|
396
|
+
*/
|
|
397
|
+
waitForRemix(sourceProjectId: string, jobId: string, options?: RemixWaitOptions): Promise<RemixResult>;
|
|
272
398
|
/**
|
|
273
399
|
* Wait for a project to reach "completed" status.
|
|
274
400
|
*
|
|
@@ -299,6 +425,39 @@ declare class LovableClient {
|
|
|
299
425
|
* @throws Error if the stream fails or timeout is reached
|
|
300
426
|
*/
|
|
301
427
|
waitForResponse(projectId: string, options?: ChatResponseOptions): Promise<ChatResponse>;
|
|
428
|
+
/**
|
|
429
|
+
* Fetch Braintrust traces for a specific chat message.
|
|
430
|
+
*
|
|
431
|
+
* Returns the trace spans associated with the AI response message.
|
|
432
|
+
* The messageId is available from the ChatResponse returned by waitForResponse().
|
|
433
|
+
*
|
|
434
|
+
* Use the `purposes` option to filter which span types are returned.
|
|
435
|
+
* When a purpose has multiple spans (e.g. main_agent across turns),
|
|
436
|
+
* only the last span is returned — it contains the full accumulated context.
|
|
437
|
+
*
|
|
438
|
+
* @param projectId - The project ID
|
|
439
|
+
* @param messageId - The AI message ID (from ChatResponse.messageId)
|
|
440
|
+
* @param options.purposes - Filter spans by purpose (e.g. ["main_agent", "knowledge_rag"])
|
|
441
|
+
* @returns The trace data including filtered spans
|
|
442
|
+
*/
|
|
443
|
+
getMessageTraces(projectId: string, messageId: string, options?: GetMessageTracesOptions): Promise<MessageTracesResponse>;
|
|
444
|
+
/**
|
|
445
|
+
* Fetch traces for multiple messages across projects in parallel.
|
|
446
|
+
*
|
|
447
|
+
* Fires concurrent requests (up to `concurrency` at a time) and collects
|
|
448
|
+
* results. Failed requests are captured in `errors` instead of throwing.
|
|
449
|
+
*
|
|
450
|
+
* @param queries - Array of { projectId, messageId } to fetch
|
|
451
|
+
* @param options.purposes - Filter spans by purpose (applied to all queries)
|
|
452
|
+
* @param options.concurrency - Max parallel requests (default: 5)
|
|
453
|
+
* @returns Object with `traces` map (keyed by messageId) and `errors` map
|
|
454
|
+
*/
|
|
455
|
+
getMessageTracesBatch(queries: TraceQuery[], options?: GetMessageTracesOptions & {
|
|
456
|
+
concurrency?: number;
|
|
457
|
+
}): Promise<BatchTracesResult>;
|
|
458
|
+
private isFileInput;
|
|
459
|
+
private uploadFile;
|
|
460
|
+
private uploadFiles;
|
|
302
461
|
private consumeSSEStream;
|
|
303
462
|
/**
|
|
304
463
|
* Wait for a project to be published (deployed).
|
|
@@ -315,4 +474,4 @@ declare class LovableClient {
|
|
|
315
474
|
waitForProjectPublished(projectId: string, options?: WaitOptions): Promise<ProjectResponse>;
|
|
316
475
|
}
|
|
317
476
|
|
|
318
|
-
export { type AddUserToWorkspaceInputBody, type ChatMessageOptions, type ChatRequest, type ChatResponse, type ChatResponseOptions, type CreateProjectBody, type CreateProjectOptions, type DeploymentResponse, type InviteCollaboratorOptions, LovableClient, type LovableClientOptions, type LovableError, type MemberRole, type ProjectResponse, type ProjectStatus, type ProjectVisibility, type WaitOptions, type WorkspaceMembership, type WorkspaceMembershipResponse, type WorkspaceWithMembership };
|
|
477
|
+
export { type AddUserToWorkspaceInputBody, type BatchTracesResult, type ChatMessageOptions, type ChatRequest, type ChatResponse, type ChatResponseOptions, type CreateProjectBody, type CreateProjectOptions, type CustomModelConfig, type DeploymentResponse, type FileInput, type GetMessageTracesOptions, type InviteCollaboratorOptions, LovableClient, type LovableClientOptions, type LovableError, type MemberRole, type MessageTracesResponse, type ProjectResponse, type ProjectStatus, type ProjectVisibility, type RemixJobStatus, type RemixJobStep, type RemixJobStepInfo, type RemixMode, type RemixProjectOptions, type RemixResult, type RemixWaitOptions, type TracePurpose, type TraceQuery, type TraceSpan, type UnscopedFile, type WaitOptions, type WorkspaceMembership, type WorkspaceMembershipResponse, type WorkspaceWithMembership };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
var DEFAULT_BASE_URL = "https://api.lovable.dev";
|
|
3
3
|
function normalizeBaseUrl(url) {
|
|
4
|
-
if (!url)
|
|
4
|
+
if (!url)
|
|
5
|
+
return DEFAULT_BASE_URL;
|
|
5
6
|
let normalized = url.replace(/\/$/, "");
|
|
6
7
|
if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) {
|
|
7
8
|
const isLocalhost = normalized.startsWith("localhost") || normalized.startsWith("127.0.0.1");
|
|
@@ -42,7 +43,7 @@ var LovableClient = class {
|
|
|
42
43
|
error.detail = errorBody?.detail;
|
|
43
44
|
throw error;
|
|
44
45
|
}
|
|
45
|
-
if (response.status === 204) {
|
|
46
|
+
if (response.status === 204 || response.status === 202) {
|
|
46
47
|
return void 0;
|
|
47
48
|
}
|
|
48
49
|
return response.json();
|
|
@@ -65,8 +66,10 @@ var LovableClient = class {
|
|
|
65
66
|
*/
|
|
66
67
|
async listProjects(workspaceId, options) {
|
|
67
68
|
const params = new URLSearchParams();
|
|
68
|
-
if (options?.limit)
|
|
69
|
-
|
|
69
|
+
if (options?.limit)
|
|
70
|
+
params.set("limit", options.limit.toString());
|
|
71
|
+
if (options?.visibility)
|
|
72
|
+
params.set("visibility", options.visibility);
|
|
70
73
|
const query = params.toString();
|
|
71
74
|
const path = `/workspaces/${workspaceId}/projects${query ? `?${query}` : ""}`;
|
|
72
75
|
const response = await this.request("GET", path);
|
|
@@ -82,12 +85,17 @@ var LovableClient = class {
|
|
|
82
85
|
visibility: options.visibility ?? "private",
|
|
83
86
|
template_project_id: options.templateProjectId
|
|
84
87
|
};
|
|
85
|
-
|
|
88
|
+
let uploadedFiles;
|
|
89
|
+
if (options.files?.length) {
|
|
90
|
+
uploadedFiles = await this.uploadFiles(options.files);
|
|
91
|
+
}
|
|
92
|
+
if (options.initialMessage || uploadedFiles) {
|
|
86
93
|
body.initial_message = {
|
|
87
94
|
id: crypto.randomUUID(),
|
|
88
|
-
message: options.initialMessage,
|
|
95
|
+
message: options.initialMessage ?? options.description,
|
|
89
96
|
chat_only: false,
|
|
90
|
-
headless: true
|
|
97
|
+
headless: true,
|
|
98
|
+
files: uploadedFiles
|
|
91
99
|
};
|
|
92
100
|
}
|
|
93
101
|
return this.request("POST", `/workspaces/${workspaceId}/projects`, body);
|
|
@@ -99,11 +107,21 @@ var LovableClient = class {
|
|
|
99
107
|
* asynchronous - the API accepts the message and processes it in the background.
|
|
100
108
|
*/
|
|
101
109
|
async chat(projectId, options) {
|
|
110
|
+
let uploadedFiles;
|
|
111
|
+
if (options.files?.length) {
|
|
112
|
+
uploadedFiles = await this.uploadFiles(options.files);
|
|
113
|
+
}
|
|
102
114
|
const body = {
|
|
103
115
|
id: crypto.randomUUID(),
|
|
104
116
|
message: options.message,
|
|
105
117
|
chat_only: options.chatOnly ?? false,
|
|
106
|
-
headless: true
|
|
118
|
+
headless: true,
|
|
119
|
+
files: uploadedFiles,
|
|
120
|
+
...options.customModel && {
|
|
121
|
+
custom_model_endpoint: options.customModel.endpoint,
|
|
122
|
+
custom_model_api_key: options.customModel.apiKey,
|
|
123
|
+
custom_model_name: options.customModel.modelName
|
|
124
|
+
}
|
|
107
125
|
};
|
|
108
126
|
await this.request("POST", `/projects/${projectId}/chat`, body);
|
|
109
127
|
}
|
|
@@ -175,6 +193,81 @@ var LovableClient = class {
|
|
|
175
193
|
name: options?.name
|
|
176
194
|
});
|
|
177
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Remix (fork) an existing project, optionally at a specific message point in time.
|
|
198
|
+
*
|
|
199
|
+
* When `messageId` is provided, the remix captures the project state as it was
|
|
200
|
+
* just before that message was processed (default). Set `remixMode: "including"`
|
|
201
|
+
* to include the message and its AI response in the remix.
|
|
202
|
+
* Without `messageId`, the full current state is remixed.
|
|
203
|
+
*
|
|
204
|
+
* @param sourceProjectId - The project to remix from
|
|
205
|
+
* @param options.workspaceId - Target workspace for the new project
|
|
206
|
+
* @param options.messageId - Optional message ID to snapshot at
|
|
207
|
+
* @param options.remixMode - "before" (default): state before the message; "including": state after the message and its AI response
|
|
208
|
+
* @param options.includeHistory - Whether to preserve chat history (default: false)
|
|
209
|
+
* @param options.includeCustomKnowledge - Whether to copy custom instructions (default: false)
|
|
210
|
+
* @param options.initialMessage - Optional initial message to send after remix
|
|
211
|
+
* @param options.integrationParameters - Integration-specific parameters
|
|
212
|
+
* @returns The remix job ID for polling progress
|
|
213
|
+
*/
|
|
214
|
+
async remixProject(sourceProjectId, options) {
|
|
215
|
+
const body = {
|
|
216
|
+
workspace_id: options.workspaceId,
|
|
217
|
+
include_history: options.includeHistory,
|
|
218
|
+
include_custom_knowledge: options.includeCustomKnowledge,
|
|
219
|
+
integration_parameters: options.integrationParameters
|
|
220
|
+
};
|
|
221
|
+
if (options.messageId) {
|
|
222
|
+
body.message_id = options.messageId;
|
|
223
|
+
body.remix_mode = options.remixMode ?? "before";
|
|
224
|
+
}
|
|
225
|
+
if (options.initialMessage) {
|
|
226
|
+
body.initial_message = {
|
|
227
|
+
id: crypto.randomUUID(),
|
|
228
|
+
message: options.initialMessage,
|
|
229
|
+
chat_only: false,
|
|
230
|
+
headless: true
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const response = await this.request("POST", `/projects/${sourceProjectId}/remix/init`, body);
|
|
234
|
+
return response.job_id;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Wait for a remix operation to complete.
|
|
238
|
+
*
|
|
239
|
+
* Polls the remix progress endpoint until the job reaches "completed" or "error" status.
|
|
240
|
+
*
|
|
241
|
+
* @param sourceProjectId - The source project ID (used for the progress endpoint)
|
|
242
|
+
* @param jobId - The job ID returned by `remixProject()`
|
|
243
|
+
* @param options.pollInterval - Time between polls in ms (default: 2000)
|
|
244
|
+
* @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)
|
|
245
|
+
* @param options.onProgress - Optional callback for status/step updates
|
|
246
|
+
* @returns The new project ID
|
|
247
|
+
* @throws Error if the remix fails or timeout is reached
|
|
248
|
+
*/
|
|
249
|
+
async waitForRemix(sourceProjectId, jobId, options) {
|
|
250
|
+
const pollInterval = options?.pollInterval ?? 2e3;
|
|
251
|
+
const timeout = options?.timeout ?? 3e5;
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
while (true) {
|
|
254
|
+
const progress = await this.request(
|
|
255
|
+
"GET",
|
|
256
|
+
`/projects/${sourceProjectId}/remix/progress?job_id=${encodeURIComponent(jobId)}`
|
|
257
|
+
);
|
|
258
|
+
options?.onProgress?.(progress.status, progress.step);
|
|
259
|
+
if (progress.status === "completed" && progress.result) {
|
|
260
|
+
return { projectId: progress.result.project_id };
|
|
261
|
+
}
|
|
262
|
+
if (progress.status === "error") {
|
|
263
|
+
throw new Error(progress.error_message ?? "Remix failed");
|
|
264
|
+
}
|
|
265
|
+
if (Date.now() - startTime > timeout) {
|
|
266
|
+
throw new Error(`Timeout waiting for remix of project ${sourceProjectId}`);
|
|
267
|
+
}
|
|
268
|
+
await sleep(pollInterval);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
178
271
|
/**
|
|
179
272
|
* Wait for a project to reach "completed" status.
|
|
180
273
|
*
|
|
@@ -242,9 +335,10 @@ var LovableClient = class {
|
|
|
242
335
|
if (!response.body) {
|
|
243
336
|
throw new Error("Response body is not readable");
|
|
244
337
|
}
|
|
245
|
-
const
|
|
338
|
+
const result = await this.consumeSSEStream(response.body);
|
|
246
339
|
return {
|
|
247
|
-
content,
|
|
340
|
+
content: result.content,
|
|
341
|
+
messageId: result.messageId,
|
|
248
342
|
previewUrl: this.getPreviewUrl(projectId)
|
|
249
343
|
};
|
|
250
344
|
} catch (err) {
|
|
@@ -256,20 +350,106 @@ var LovableClient = class {
|
|
|
256
350
|
clearTimeout(timeoutId);
|
|
257
351
|
}
|
|
258
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Fetch Braintrust traces for a specific chat message.
|
|
355
|
+
*
|
|
356
|
+
* Returns the trace spans associated with the AI response message.
|
|
357
|
+
* The messageId is available from the ChatResponse returned by waitForResponse().
|
|
358
|
+
*
|
|
359
|
+
* Use the `purposes` option to filter which span types are returned.
|
|
360
|
+
* When a purpose has multiple spans (e.g. main_agent across turns),
|
|
361
|
+
* only the last span is returned — it contains the full accumulated context.
|
|
362
|
+
*
|
|
363
|
+
* @param projectId - The project ID
|
|
364
|
+
* @param messageId - The AI message ID (from ChatResponse.messageId)
|
|
365
|
+
* @param options.purposes - Filter spans by purpose (e.g. ["main_agent", "knowledge_rag"])
|
|
366
|
+
* @returns The trace data including filtered spans
|
|
367
|
+
*/
|
|
368
|
+
async getMessageTraces(projectId, messageId, options) {
|
|
369
|
+
const params = new URLSearchParams();
|
|
370
|
+
if (options?.purposes?.length) {
|
|
371
|
+
params.set("purposes", options.purposes.join(","));
|
|
372
|
+
}
|
|
373
|
+
const query = params.toString();
|
|
374
|
+
const path = `/projects/${projectId}/messages/${messageId}/traces${query ? `?${query}` : ""}`;
|
|
375
|
+
return this.request("GET", path);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Fetch traces for multiple messages across projects in parallel.
|
|
379
|
+
*
|
|
380
|
+
* Fires concurrent requests (up to `concurrency` at a time) and collects
|
|
381
|
+
* results. Failed requests are captured in `errors` instead of throwing.
|
|
382
|
+
*
|
|
383
|
+
* @param queries - Array of { projectId, messageId } to fetch
|
|
384
|
+
* @param options.purposes - Filter spans by purpose (applied to all queries)
|
|
385
|
+
* @param options.concurrency - Max parallel requests (default: 5)
|
|
386
|
+
* @returns Object with `traces` map (keyed by messageId) and `errors` map
|
|
387
|
+
*/
|
|
388
|
+
async getMessageTracesBatch(queries, options) {
|
|
389
|
+
const concurrency = options?.concurrency ?? 5;
|
|
390
|
+
const traces = /* @__PURE__ */ new Map();
|
|
391
|
+
const errors = /* @__PURE__ */ new Map();
|
|
392
|
+
const purposeOpts = options?.purposes ? { purposes: options.purposes } : void 0;
|
|
393
|
+
const pending = [...queries];
|
|
394
|
+
const executing = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const query of pending) {
|
|
396
|
+
const task = this.getMessageTraces(query.projectId, query.messageId, purposeOpts).then((result) => {
|
|
397
|
+
traces.set(query.messageId, result);
|
|
398
|
+
}).catch((err) => {
|
|
399
|
+
errors.set(query.messageId, err instanceof Error ? err : new Error(String(err)));
|
|
400
|
+
}).finally(() => {
|
|
401
|
+
executing.delete(task);
|
|
402
|
+
});
|
|
403
|
+
executing.add(task);
|
|
404
|
+
if (executing.size >= concurrency) {
|
|
405
|
+
await Promise.race(executing);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
await Promise.all(executing);
|
|
409
|
+
return { traces, errors };
|
|
410
|
+
}
|
|
411
|
+
isFileInput(file) {
|
|
412
|
+
return "data" in file;
|
|
413
|
+
}
|
|
414
|
+
async uploadFile(file) {
|
|
415
|
+
const fileId = crypto.randomUUID();
|
|
416
|
+
const fileName = this.isFileInput(file) ? file.name : file.name;
|
|
417
|
+
const mimeType = this.isFileInput(file) ? file.type : file.type;
|
|
418
|
+
const body = this.isFileInput(file) ? file.data : file;
|
|
419
|
+
const { url } = await this.request("POST", "/files/generate-upload-url", {
|
|
420
|
+
file_name: fileId,
|
|
421
|
+
content_type: mimeType
|
|
422
|
+
});
|
|
423
|
+
const uploadResponse = await fetch(url, {
|
|
424
|
+
method: "PUT",
|
|
425
|
+
body,
|
|
426
|
+
headers: { "Content-Type": mimeType }
|
|
427
|
+
});
|
|
428
|
+
if (!uploadResponse.ok) {
|
|
429
|
+
throw new Error(`File upload failed for "${fileName}": HTTP ${uploadResponse.status}`);
|
|
430
|
+
}
|
|
431
|
+
return { file_id: fileId, type: "user_upload", file_name: fileName, mime_type: mimeType };
|
|
432
|
+
}
|
|
433
|
+
async uploadFiles(files) {
|
|
434
|
+
return Promise.all(files.map((file) => this.uploadFile(file)));
|
|
435
|
+
}
|
|
259
436
|
async consumeSSEStream(body) {
|
|
260
437
|
const reader = body.getReader();
|
|
261
438
|
const decoder = new TextDecoder();
|
|
262
439
|
let buffer = "";
|
|
263
440
|
let content = "";
|
|
441
|
+
let messageId = "";
|
|
264
442
|
try {
|
|
265
443
|
while (true) {
|
|
266
444
|
const { done, value } = await reader.read();
|
|
267
|
-
if (done)
|
|
445
|
+
if (done)
|
|
446
|
+
break;
|
|
268
447
|
buffer += decoder.decode(value, { stream: true });
|
|
269
448
|
const parts = buffer.split("\n\n");
|
|
270
449
|
buffer = parts.pop() ?? "";
|
|
271
450
|
for (const part of parts) {
|
|
272
|
-
if (!part.trim())
|
|
451
|
+
if (!part.trim())
|
|
452
|
+
continue;
|
|
273
453
|
const lines = part.split("\n");
|
|
274
454
|
let eventType = "";
|
|
275
455
|
let eventData = "";
|
|
@@ -286,8 +466,11 @@ var LovableClient = class {
|
|
|
286
466
|
if (typeof data.content === "string") {
|
|
287
467
|
content += data.content;
|
|
288
468
|
}
|
|
469
|
+
if (typeof data.message_id === "string" && data.message_id) {
|
|
470
|
+
messageId = data.message_id;
|
|
471
|
+
}
|
|
289
472
|
if (data.is_final) {
|
|
290
|
-
return content;
|
|
473
|
+
return { content, messageId };
|
|
291
474
|
}
|
|
292
475
|
} catch {
|
|
293
476
|
}
|
|
@@ -296,7 +479,8 @@ var LovableClient = class {
|
|
|
296
479
|
let detail = "Stream error from server";
|
|
297
480
|
try {
|
|
298
481
|
const data = JSON.parse(eventData);
|
|
299
|
-
if (data.message)
|
|
482
|
+
if (data.message)
|
|
483
|
+
detail = data.message;
|
|
300
484
|
} catch {
|
|
301
485
|
}
|
|
302
486
|
throw new Error(detail);
|
|
@@ -304,9 +488,9 @@ var LovableClient = class {
|
|
|
304
488
|
}
|
|
305
489
|
}
|
|
306
490
|
} finally {
|
|
307
|
-
reader.cancel();
|
|
491
|
+
void reader.cancel();
|
|
308
492
|
}
|
|
309
|
-
return content;
|
|
493
|
+
return { content, messageId };
|
|
310
494
|
}
|
|
311
495
|
/**
|
|
312
496
|
* Wait for a project to be published (deployed).
|
|
@@ -330,6 +514,9 @@ var LovableClient = class {
|
|
|
330
514
|
if (project.is_published && project.url) {
|
|
331
515
|
return project;
|
|
332
516
|
}
|
|
517
|
+
if (project.status === "failed") {
|
|
518
|
+
throw new Error(`Project ${projectId} failed to build`);
|
|
519
|
+
}
|
|
333
520
|
if (Date.now() - startTime > timeout) {
|
|
334
521
|
throw new Error(`Timeout waiting for project ${projectId} to be published`);
|
|
335
522
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type {\n LovableClientOptions,\n LovableError,\n CreateProjectOptions,\n InviteCollaboratorOptions,\n ChatMessageOptions,\n WaitOptions,\n WorkspaceWithMembership,\n ProjectResponse,\n WorkspaceMembershipResponse,\n CreateProjectBody,\n ChatRequest,\n AddUserToWorkspaceInputBody,\n GetWorkspacesResponse,\n GetWorkspaceProjectsResponse,\n DeploymentResponse,\n ChatResponse,\n ChatResponseOptions,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.lovable.dev\";\n\nfunction normalizeBaseUrl(url: string | undefined): string {\n if (!url) return DEFAULT_BASE_URL;\n\n let normalized = url.replace(/\\/$/, \"\");\n\n // Add http:// for localhost URLs without protocol\n if (!normalized.startsWith(\"http://\") && !normalized.startsWith(\"https://\")) {\n const isLocalhost = normalized.startsWith(\"localhost\") || normalized.startsWith(\"127.0.0.1\");\n normalized = isLocalhost ? `http://${normalized}` : `https://${normalized}`;\n }\n\n return normalized;\n}\n\nexport class LovableClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(options: LovableClientOptions) {\n if (!options.apiKey) {\n throw new Error(\"API key is required\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = normalizeBaseUrl(options.baseUrl);\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n\n const headers: Record<string, string> = {\n \"Lovable-API-Key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n let errorBody: { title?: string; detail?: string } | undefined;\n try {\n errorBody = (await response.json()) as { title?: string; detail?: string };\n } catch {\n // Ignore JSON parse errors\n }\n\n const error = new Error(errorBody?.title ?? `HTTP ${response.status}: ${response.statusText}`) as LovableError;\n error.status = response.status;\n error.type = errorBody?.title;\n error.detail = errorBody?.detail;\n throw error;\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * List all workspaces the authenticated user has access to\n */\n async listWorkspaces(): Promise<WorkspaceWithMembership[]> {\n const response = await this.request<GetWorkspacesResponse>(\"GET\", \"/user/workspaces\");\n return response.workspaces ?? [];\n }\n\n /**\n * Get a specific workspace by ID\n */\n async getWorkspace(workspaceId: string): Promise<WorkspaceWithMembership> {\n return this.request<WorkspaceWithMembership>(\"GET\", `/user/workspaces/${workspaceId}`);\n }\n\n /**\n * List projects in a workspace\n */\n async listProjects(\n workspaceId: string,\n options?: { limit?: number; visibility?: \"all\" | \"personal\" | \"public\" | \"workspace\" },\n ): Promise<ProjectResponse[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.visibility) params.set(\"visibility\", options.visibility);\n\n const query = params.toString();\n const path = `/workspaces/${workspaceId}/projects${query ? `?${query}` : \"\"}`;\n\n const response = await this.request<GetWorkspaceProjectsResponse>(\"GET\", path);\n return response.projects ?? [];\n }\n\n /**\n * Create a new project in a workspace\n */\n async createProject(workspaceId: string, options: CreateProjectOptions): Promise<ProjectResponse> {\n const body: CreateProjectBody = {\n description: options.description,\n tech_stack: options.techStack ?? \"\",\n visibility: options.visibility ?? \"private\",\n template_project_id: options.templateProjectId,\n };\n\n if (options.initialMessage) {\n body.initial_message = {\n id: crypto.randomUUID(),\n message: options.initialMessage,\n chat_only: false,\n headless: true,\n };\n }\n\n return this.request<ProjectResponse>(\"POST\", `/workspaces/${workspaceId}/projects`, body);\n }\n\n /**\n * Send a chat message to a project\n *\n * Note: This sends a message to the project's AI agent. The response is\n * asynchronous - the API accepts the message and processes it in the background.\n */\n async chat(projectId: string, options: ChatMessageOptions): Promise<void> {\n const body: ChatRequest = {\n id: crypto.randomUUID(),\n message: options.message,\n chat_only: options.chatOnly ?? false,\n headless: true,\n };\n\n await this.request<void>(\"POST\", `/projects/${projectId}/chat`, body);\n }\n\n /**\n * Invite a user to a workspace as a collaborator\n */\n async inviteCollaborator(\n workspaceId: string,\n options: InviteCollaboratorOptions,\n ): Promise<WorkspaceMembershipResponse> {\n const body: AddUserToWorkspaceInputBody = {\n email: options.email,\n role: options.role ?? \"member\",\n };\n\n return this.request<WorkspaceMembershipResponse>(\"POST\", `/workspaces/${workspaceId}/memberships`, body);\n }\n\n /**\n * List members of a workspace\n */\n async listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMembershipResponse[]> {\n const response = await this.request<{\n memberships: WorkspaceMembershipResponse[] | null;\n }>(\"GET\", `/workspaces/${workspaceId}/memberships`);\n return response.memberships ?? [];\n }\n\n /**\n * Remove a member from a workspace\n */\n async removeWorkspaceMember(workspaceId: string, userId: string): Promise<void> {\n await this.request<void>(\"DELETE\", `/workspaces/${workspaceId}/memberships/${userId}`);\n }\n\n /**\n * Get project details by ID\n */\n async getProject(projectId: string): Promise<ProjectResponse> {\n return this.request<ProjectResponse>(\"GET\", `/projects/${projectId}/details`);\n }\n\n /**\n * Get the preview URL for a project.\n *\n * The preview URL is available once the project reaches \"completed\" status.\n * This URL allows viewing the project in development mode.\n *\n * @param projectId - The project ID\n * @returns The preview URL\n */\n getPreviewUrl(projectId: string): string {\n return `https://id-preview--${projectId}.lovable.app`;\n }\n\n /**\n * Get the published URL for a project (if published).\n *\n * Returns the public URL if the project has been published, or null if not.\n *\n * @param projectId - The project ID\n * @returns The published URL or null if not published\n */\n async getPublishedUrl(projectId: string): Promise<string | null> {\n const project = await this.getProject(projectId);\n return project.is_published && project.url ? project.url : null;\n }\n\n /**\n * Publish a project.\n *\n * This triggers a deployment which makes the project publicly accessible.\n * The deployment runs asynchronously - use waitForProjectPublished() to wait for completion.\n *\n * @param projectId - The project ID to publish\n * @param options.name - Optional custom slug for the published URL\n * @returns Deployment info including deployment ID\n */\n async publish(projectId: string, options?: { name?: string }): Promise<DeploymentResponse> {\n return this.request<DeploymentResponse>(\"POST\", `/projects/${projectId}/deployments`, {\n name: options?.name,\n });\n }\n\n /**\n * Wait for a project to reach \"completed\" status.\n *\n * Projects start in \"in_progress\" status while being created/built.\n * This method polls until the status becomes \"completed\" or \"failed\".\n * A successful completion means the project's preview is ready to view.\n *\n * @param projectId - The project ID to wait for\n * @param options.pollInterval - Time between polls in ms (default: 2000)\n * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)\n * @param options.onProgress - Optional callback for status updates\n * @returns The completed project\n * @throws Error if project fails or timeout is reached\n */\n async waitForProjectReady(projectId: string, options?: WaitOptions): Promise<ProjectResponse> {\n const pollInterval = options?.pollInterval ?? 2000;\n const timeout = options?.timeout ?? 300000;\n const startTime = Date.now();\n\n while (true) {\n const project = await this.getProject(projectId);\n options?.onProgress?.(project);\n\n if (project.status === \"completed\") {\n return project;\n }\n\n if (project.status === \"failed\") {\n throw new Error(`Project ${projectId} failed to build`);\n }\n\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for project ${projectId} to be ready`);\n }\n\n await sleep(pollInterval);\n }\n }\n\n /**\n * Wait for the AI response to a chat message.\n *\n * Connects to the project's message stream (SSE) and accumulates the\n * response content until the message is complete. Returns the full\n * response text along with the project's preview URL.\n *\n * Use this after `chat()` or after `createProject()` with `initialMessage`.\n *\n * @param projectId - The project ID to listen for\n * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)\n * @returns The AI response content and preview URL\n * @throws Error if the stream fails or timeout is reached\n */\n async waitForResponse(projectId: string, options?: ChatResponseOptions): Promise<ChatResponse> {\n const timeout = options?.timeout ?? 300000;\n const url = `${this.baseUrl}/projects/${projectId}/latest-message`;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(url, {\n headers: {\n \"Lovable-API-Key\": this.apiKey,\n },\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const error = new Error(`Failed to connect to message stream: HTTP ${response.status}`) as LovableError;\n error.status = response.status;\n throw error;\n }\n\n if (!response.body) {\n throw new Error(\"Response body is not readable\");\n }\n\n const content = await this.consumeSSEStream(response.body);\n return {\n content,\n previewUrl: this.getPreviewUrl(projectId),\n };\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new Error(`Timeout waiting for response on project ${projectId}`);\n }\n throw err;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n private async consumeSSEStream(body: ReadableStream<Uint8Array>): Promise<string> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let content = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n // SSE events are separated by double newlines\n const parts = buffer.split(\"\\n\\n\");\n buffer = parts.pop() ?? \"\";\n\n for (const part of parts) {\n if (!part.trim()) continue;\n\n const lines = part.split(\"\\n\");\n let eventType = \"\";\n let eventData = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n eventType = line.slice(7);\n } else if (line.startsWith(\"data: \")) {\n eventData = line.slice(6);\n }\n }\n\n if (eventType === \"message\" && eventData) {\n try {\n const data = JSON.parse(eventData);\n if (typeof data.content === \"string\") {\n content += data.content;\n }\n if (data.is_final) {\n return content;\n }\n } catch {\n // Skip non-JSON data lines\n }\n }\n\n if (eventType === \"error\") {\n let detail = \"Stream error from server\";\n try {\n const data = JSON.parse(eventData);\n if (data.message) detail = data.message;\n } catch {\n // use default message\n }\n throw new Error(detail);\n }\n }\n }\n } finally {\n reader.cancel();\n }\n\n return content;\n }\n\n /**\n * Wait for a project to be published (deployed).\n *\n * This method polls until the project has `is_published: true` and a `url`.\n *\n * @param projectId - The project ID to wait for\n * @param options.pollInterval - Time between polls in ms (default: 3000)\n * @param options.timeout - Maximum time to wait in ms (default: 600000 = 10 minutes)\n * @param options.onProgress - Optional callback for status updates\n * @returns The published project with URL\n * @throws Error if timeout is reached\n */\n async waitForProjectPublished(projectId: string, options?: WaitOptions): Promise<ProjectResponse> {\n const pollInterval = options?.pollInterval ?? 3000;\n const timeout = options?.timeout ?? 600000;\n const startTime = Date.now();\n\n while (true) {\n const project = await this.getProject(projectId);\n options?.onProgress?.(project);\n\n if (project.is_published && project.url) {\n return project;\n }\n\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for project ${projectId} to be published`);\n }\n\n await sleep(pollInterval);\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type {\n LovableClientOptions,\n LovableError,\n CreateProjectOptions,\n InviteCollaboratorOptions,\n ChatMessageOptions,\n WaitOptions,\n WorkspaceWithMembership,\n ProjectResponse,\n WorkspaceMembershipResponse,\n CreateProjectBody,\n ChatRequest,\n AddUserToWorkspaceInputBody,\n GetWorkspacesResponse,\n GetWorkspaceProjectsResponse,\n DeploymentResponse,\n ChatResponse,\n ChatResponseOptions,\n UnscopedFile,\n FileInput,\n RemixProjectOptions,\n RemixInitBody,\n RemixInitResponse,\n RemixProgressResponse,\n RemixResult,\n RemixWaitOptions,\n MessageTracesResponse,\n GetMessageTracesOptions,\n TraceQuery,\n BatchTracesResult,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.lovable.dev\";\n\nfunction normalizeBaseUrl(url: string | undefined): string {\n if (!url) return DEFAULT_BASE_URL;\n\n let normalized = url.replace(/\\/$/, \"\");\n\n // Add http:// for localhost URLs without protocol\n if (!normalized.startsWith(\"http://\") && !normalized.startsWith(\"https://\")) {\n const isLocalhost = normalized.startsWith(\"localhost\") || normalized.startsWith(\"127.0.0.1\");\n normalized = isLocalhost ? `http://${normalized}` : `https://${normalized}`;\n }\n\n return normalized;\n}\n\nexport class LovableClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(options: LovableClientOptions) {\n if (!options.apiKey) {\n throw new Error(\"API key is required\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = normalizeBaseUrl(options.baseUrl);\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n\n const headers: Record<string, string> = {\n \"Lovable-API-Key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n let errorBody: { title?: string; detail?: string } | undefined;\n try {\n errorBody = (await response.json()) as { title?: string; detail?: string };\n } catch {\n // Ignore JSON parse errors\n }\n\n const error = new Error(errorBody?.title ?? `HTTP ${response.status}: ${response.statusText}`) as LovableError;\n error.status = response.status;\n error.type = errorBody?.title;\n error.detail = errorBody?.detail;\n throw error;\n }\n\n if (response.status === 204 || response.status === 202) {\n return undefined as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * List all workspaces the authenticated user has access to\n */\n async listWorkspaces(): Promise<WorkspaceWithMembership[]> {\n const response = await this.request<GetWorkspacesResponse>(\"GET\", \"/user/workspaces\");\n return response.workspaces ?? [];\n }\n\n /**\n * Get a specific workspace by ID\n */\n async getWorkspace(workspaceId: string): Promise<WorkspaceWithMembership> {\n return this.request<WorkspaceWithMembership>(\"GET\", `/user/workspaces/${workspaceId}`);\n }\n\n /**\n * List projects in a workspace\n */\n async listProjects(\n workspaceId: string,\n options?: { limit?: number; visibility?: \"all\" | \"personal\" | \"public\" | \"workspace\" },\n ): Promise<ProjectResponse[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.visibility) params.set(\"visibility\", options.visibility);\n\n const query = params.toString();\n const path = `/workspaces/${workspaceId}/projects${query ? `?${query}` : \"\"}`;\n\n const response = await this.request<GetWorkspaceProjectsResponse>(\"GET\", path);\n return response.projects ?? [];\n }\n\n /**\n * Create a new project in a workspace\n */\n async createProject(workspaceId: string, options: CreateProjectOptions): Promise<ProjectResponse> {\n const body: CreateProjectBody = {\n description: options.description,\n tech_stack: options.techStack ?? \"\",\n visibility: options.visibility ?? \"private\",\n template_project_id: options.templateProjectId,\n };\n\n let uploadedFiles: UnscopedFile[] | undefined;\n if (options.files?.length) {\n uploadedFiles = await this.uploadFiles(options.files);\n }\n\n if (options.initialMessage || uploadedFiles) {\n body.initial_message = {\n id: crypto.randomUUID(),\n message: options.initialMessage ?? options.description,\n chat_only: false,\n headless: true,\n files: uploadedFiles,\n };\n }\n\n return this.request<ProjectResponse>(\"POST\", `/workspaces/${workspaceId}/projects`, body);\n }\n\n /**\n * Send a chat message to a project\n *\n * Note: This sends a message to the project's AI agent. The response is\n * asynchronous - the API accepts the message and processes it in the background.\n */\n async chat(projectId: string, options: ChatMessageOptions): Promise<void> {\n let uploadedFiles: UnscopedFile[] | undefined;\n if (options.files?.length) {\n uploadedFiles = await this.uploadFiles(options.files);\n }\n\n const body: ChatRequest = {\n id: crypto.randomUUID(),\n message: options.message,\n chat_only: options.chatOnly ?? false,\n headless: true,\n files: uploadedFiles,\n ...(options.customModel && {\n custom_model_endpoint: options.customModel.endpoint,\n custom_model_api_key: options.customModel.apiKey,\n custom_model_name: options.customModel.modelName,\n }),\n };\n\n await this.request<void>(\"POST\", `/projects/${projectId}/chat`, body);\n }\n\n /**\n * Invite a user to a workspace as a collaborator\n */\n async inviteCollaborator(\n workspaceId: string,\n options: InviteCollaboratorOptions,\n ): Promise<WorkspaceMembershipResponse> {\n const body: AddUserToWorkspaceInputBody = {\n email: options.email,\n role: options.role ?? \"member\",\n };\n\n return this.request<WorkspaceMembershipResponse>(\"POST\", `/workspaces/${workspaceId}/memberships`, body);\n }\n\n /**\n * List members of a workspace\n */\n async listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMembershipResponse[]> {\n const response = await this.request<{\n memberships: WorkspaceMembershipResponse[] | null;\n }>(\"GET\", `/workspaces/${workspaceId}/memberships`);\n return response.memberships ?? [];\n }\n\n /**\n * Remove a member from a workspace\n */\n async removeWorkspaceMember(workspaceId: string, userId: string): Promise<void> {\n await this.request<void>(\"DELETE\", `/workspaces/${workspaceId}/memberships/${userId}`);\n }\n\n /**\n * Get project details by ID\n */\n async getProject(projectId: string): Promise<ProjectResponse> {\n return this.request<ProjectResponse>(\"GET\", `/projects/${projectId}/details`);\n }\n\n /**\n * Get the preview URL for a project.\n *\n * The preview URL is available once the project reaches \"completed\" status.\n * This URL allows viewing the project in development mode.\n *\n * @param projectId - The project ID\n * @returns The preview URL\n */\n getPreviewUrl(projectId: string): string {\n return `https://id-preview--${projectId}.lovable.app`;\n }\n\n /**\n * Get the published URL for a project (if published).\n *\n * Returns the public URL if the project has been published, or null if not.\n *\n * @param projectId - The project ID\n * @returns The published URL or null if not published\n */\n async getPublishedUrl(projectId: string): Promise<string | null> {\n const project = await this.getProject(projectId);\n return project.is_published && project.url ? project.url : null;\n }\n\n /**\n * Publish a project.\n *\n * This triggers a deployment which makes the project publicly accessible.\n * The deployment runs asynchronously - use waitForProjectPublished() to wait for completion.\n *\n * @param projectId - The project ID to publish\n * @param options.name - Optional custom slug for the published URL\n * @returns Deployment info including deployment ID\n */\n async publish(projectId: string, options?: { name?: string }): Promise<DeploymentResponse> {\n return this.request<DeploymentResponse>(\"POST\", `/projects/${projectId}/deployments`, {\n name: options?.name,\n });\n }\n\n /**\n * Remix (fork) an existing project, optionally at a specific message point in time.\n *\n * When `messageId` is provided, the remix captures the project state as it was\n * just before that message was processed (default). Set `remixMode: \"including\"`\n * to include the message and its AI response in the remix.\n * Without `messageId`, the full current state is remixed.\n *\n * @param sourceProjectId - The project to remix from\n * @param options.workspaceId - Target workspace for the new project\n * @param options.messageId - Optional message ID to snapshot at\n * @param options.remixMode - \"before\" (default): state before the message; \"including\": state after the message and its AI response\n * @param options.includeHistory - Whether to preserve chat history (default: false)\n * @param options.includeCustomKnowledge - Whether to copy custom instructions (default: false)\n * @param options.initialMessage - Optional initial message to send after remix\n * @param options.integrationParameters - Integration-specific parameters\n * @returns The remix job ID for polling progress\n */\n async remixProject(sourceProjectId: string, options: RemixProjectOptions): Promise<string> {\n const body: RemixInitBody = {\n workspace_id: options.workspaceId,\n include_history: options.includeHistory,\n include_custom_knowledge: options.includeCustomKnowledge,\n integration_parameters: options.integrationParameters,\n };\n\n if (options.messageId) {\n body.message_id = options.messageId;\n body.remix_mode = options.remixMode ?? \"before\";\n }\n\n if (options.initialMessage) {\n body.initial_message = {\n id: crypto.randomUUID(),\n message: options.initialMessage,\n chat_only: false,\n headless: true,\n };\n }\n\n const response = await this.request<RemixInitResponse>(\"POST\", `/projects/${sourceProjectId}/remix/init`, body);\n return response.job_id;\n }\n\n /**\n * Wait for a remix operation to complete.\n *\n * Polls the remix progress endpoint until the job reaches \"completed\" or \"error\" status.\n *\n * @param sourceProjectId - The source project ID (used for the progress endpoint)\n * @param jobId - The job ID returned by `remixProject()`\n * @param options.pollInterval - Time between polls in ms (default: 2000)\n * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)\n * @param options.onProgress - Optional callback for status/step updates\n * @returns The new project ID\n * @throws Error if the remix fails or timeout is reached\n */\n async waitForRemix(sourceProjectId: string, jobId: string, options?: RemixWaitOptions): Promise<RemixResult> {\n const pollInterval = options?.pollInterval ?? 2000;\n const timeout = options?.timeout ?? 300000;\n const startTime = Date.now();\n\n while (true) {\n const progress = await this.request<RemixProgressResponse>(\n \"GET\",\n `/projects/${sourceProjectId}/remix/progress?job_id=${encodeURIComponent(jobId)}`,\n );\n\n options?.onProgress?.(progress.status, progress.step);\n\n if (progress.status === \"completed\" && progress.result) {\n return { projectId: progress.result.project_id };\n }\n\n if (progress.status === \"error\") {\n throw new Error(progress.error_message ?? \"Remix failed\");\n }\n\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for remix of project ${sourceProjectId}`);\n }\n\n await sleep(pollInterval);\n }\n }\n\n /**\n * Wait for a project to reach \"completed\" status.\n *\n * Projects start in \"in_progress\" status while being created/built.\n * This method polls until the status becomes \"completed\" or \"failed\".\n * A successful completion means the project's preview is ready to view.\n *\n * @param projectId - The project ID to wait for\n * @param options.pollInterval - Time between polls in ms (default: 2000)\n * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)\n * @param options.onProgress - Optional callback for status updates\n * @returns The completed project\n * @throws Error if project fails or timeout is reached\n */\n async waitForProjectReady(projectId: string, options?: WaitOptions): Promise<ProjectResponse> {\n const pollInterval = options?.pollInterval ?? 2000;\n const timeout = options?.timeout ?? 300000;\n const startTime = Date.now();\n\n while (true) {\n const project = await this.getProject(projectId);\n options?.onProgress?.(project);\n\n if (project.status === \"completed\") {\n return project;\n }\n\n if (project.status === \"failed\") {\n throw new Error(`Project ${projectId} failed to build`);\n }\n\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for project ${projectId} to be ready`);\n }\n\n await sleep(pollInterval);\n }\n }\n\n /**\n * Wait for the AI response to a chat message.\n *\n * Connects to the project's message stream (SSE) and accumulates the\n * response content until the message is complete. Returns the full\n * response text along with the project's preview URL.\n *\n * Use this after `chat()` or after `createProject()` with `initialMessage`.\n *\n * @param projectId - The project ID to listen for\n * @param options.timeout - Maximum time to wait in ms (default: 300000 = 5 minutes)\n * @returns The AI response content and preview URL\n * @throws Error if the stream fails or timeout is reached\n */\n async waitForResponse(projectId: string, options?: ChatResponseOptions): Promise<ChatResponse> {\n const timeout = options?.timeout ?? 300000;\n const url = `${this.baseUrl}/projects/${projectId}/latest-message`;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(url, {\n headers: {\n \"Lovable-API-Key\": this.apiKey,\n },\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const error = new Error(`Failed to connect to message stream: HTTP ${response.status}`) as LovableError;\n error.status = response.status;\n throw error;\n }\n\n if (!response.body) {\n throw new Error(\"Response body is not readable\");\n }\n\n const result = await this.consumeSSEStream(response.body);\n return {\n content: result.content,\n messageId: result.messageId,\n previewUrl: this.getPreviewUrl(projectId),\n };\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new Error(`Timeout waiting for response on project ${projectId}`);\n }\n throw err;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Fetch Braintrust traces for a specific chat message.\n *\n * Returns the trace spans associated with the AI response message.\n * The messageId is available from the ChatResponse returned by waitForResponse().\n *\n * Use the `purposes` option to filter which span types are returned.\n * When a purpose has multiple spans (e.g. main_agent across turns),\n * only the last span is returned — it contains the full accumulated context.\n *\n * @param projectId - The project ID\n * @param messageId - The AI message ID (from ChatResponse.messageId)\n * @param options.purposes - Filter spans by purpose (e.g. [\"main_agent\", \"knowledge_rag\"])\n * @returns The trace data including filtered spans\n */\n async getMessageTraces(\n projectId: string,\n messageId: string,\n options?: GetMessageTracesOptions,\n ): Promise<MessageTracesResponse> {\n const params = new URLSearchParams();\n if (options?.purposes?.length) {\n params.set(\"purposes\", options.purposes.join(\",\"));\n }\n const query = params.toString();\n const path = `/projects/${projectId}/messages/${messageId}/traces${query ? `?${query}` : \"\"}`;\n return this.request<MessageTracesResponse>(\"GET\", path);\n }\n\n /**\n * Fetch traces for multiple messages across projects in parallel.\n *\n * Fires concurrent requests (up to `concurrency` at a time) and collects\n * results. Failed requests are captured in `errors` instead of throwing.\n *\n * @param queries - Array of { projectId, messageId } to fetch\n * @param options.purposes - Filter spans by purpose (applied to all queries)\n * @param options.concurrency - Max parallel requests (default: 5)\n * @returns Object with `traces` map (keyed by messageId) and `errors` map\n */\n async getMessageTracesBatch(\n queries: TraceQuery[],\n options?: GetMessageTracesOptions & { concurrency?: number },\n ): Promise<BatchTracesResult> {\n const concurrency = options?.concurrency ?? 5;\n const traces = new Map<string, MessageTracesResponse>();\n const errors = new Map<string, Error>();\n const purposeOpts = options?.purposes ? { purposes: options.purposes } : undefined;\n\n const pending = [...queries];\n const executing = new Set<Promise<void>>();\n\n for (const query of pending) {\n const task = this.getMessageTraces(query.projectId, query.messageId, purposeOpts)\n .then((result) => {\n traces.set(query.messageId, result);\n })\n .catch((err) => {\n errors.set(query.messageId, err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n executing.delete(task);\n });\n\n executing.add(task);\n\n if (executing.size >= concurrency) {\n await Promise.race(executing);\n }\n }\n\n await Promise.all(executing);\n\n return { traces, errors };\n }\n\n private isFileInput(file: File | FileInput): file is FileInput {\n return \"data\" in file;\n }\n\n private async uploadFile(file: File | FileInput): Promise<UnscopedFile> {\n const fileId = crypto.randomUUID();\n const fileName = this.isFileInput(file) ? file.name : file.name;\n const mimeType = this.isFileInput(file) ? file.type : file.type;\n const body = this.isFileInput(file) ? file.data : file;\n\n const { url } = await this.request<{ url: string }>(\"POST\", \"/files/generate-upload-url\", {\n file_name: fileId,\n content_type: mimeType,\n });\n\n const uploadResponse = await fetch(url, {\n method: \"PUT\",\n body,\n headers: { \"Content-Type\": mimeType },\n });\n if (!uploadResponse.ok) {\n throw new Error(`File upload failed for \"${fileName}\": HTTP ${uploadResponse.status}`);\n }\n\n return { file_id: fileId, type: \"user_upload\", file_name: fileName, mime_type: mimeType };\n }\n\n private async uploadFiles(files: (File | FileInput)[]): Promise<UnscopedFile[]> {\n return Promise.all(files.map((file) => this.uploadFile(file)));\n }\n\n private async consumeSSEStream(body: ReadableStream<Uint8Array>): Promise<{ content: string; messageId: string }> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let content = \"\";\n let messageId = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n // SSE events are separated by double newlines\n const parts = buffer.split(\"\\n\\n\");\n buffer = parts.pop() ?? \"\";\n\n for (const part of parts) {\n if (!part.trim()) continue;\n\n const lines = part.split(\"\\n\");\n let eventType = \"\";\n let eventData = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n eventType = line.slice(7);\n } else if (line.startsWith(\"data: \")) {\n eventData = line.slice(6);\n }\n }\n\n if (eventType === \"message\" && eventData) {\n try {\n const data = JSON.parse(eventData);\n if (typeof data.content === \"string\") {\n content += data.content;\n }\n if (typeof data.message_id === \"string\" && data.message_id) {\n messageId = data.message_id;\n }\n if (data.is_final) {\n return { content, messageId };\n }\n } catch {\n // Skip non-JSON data lines\n }\n }\n\n if (eventType === \"error\") {\n let detail = \"Stream error from server\";\n try {\n const data = JSON.parse(eventData);\n if (data.message) detail = data.message;\n } catch {\n // use default message\n }\n throw new Error(detail);\n }\n }\n }\n } finally {\n void reader.cancel();\n }\n\n return { content, messageId };\n }\n\n /**\n * Wait for a project to be published (deployed).\n *\n * This method polls until the project has `is_published: true` and a `url`.\n *\n * @param projectId - The project ID to wait for\n * @param options.pollInterval - Time between polls in ms (default: 3000)\n * @param options.timeout - Maximum time to wait in ms (default: 600000 = 10 minutes)\n * @param options.onProgress - Optional callback for status updates\n * @returns The published project with URL\n * @throws Error if timeout is reached\n */\n async waitForProjectPublished(projectId: string, options?: WaitOptions): Promise<ProjectResponse> {\n const pollInterval = options?.pollInterval ?? 3000;\n const timeout = options?.timeout ?? 600000;\n const startTime = Date.now();\n\n while (true) {\n const project = await this.getProject(projectId);\n options?.onProgress?.(project);\n\n if (project.is_published && project.url) {\n return project;\n }\n\n if (project.status === \"failed\") {\n throw new Error(`Project ${projectId} failed to build`);\n }\n\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for project ${projectId} to be published`);\n }\n\n await sleep(pollInterval);\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
|