luxlabs 1.0.19 → 1.0.21

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/commands/dev.js CHANGED
@@ -124,12 +124,21 @@ async function dev(options = {}) {
124
124
  }
125
125
  }
126
126
 
127
+ /**
128
+ * Get the correct next binary path for the current platform
129
+ * Points directly to Next.js CLI JS file to bypass .bin shell script wrappers (cross-platform)
130
+ */
131
+ function getNextBinPath() {
132
+ return './node_modules/next/dist/bin/next';
133
+ }
134
+
127
135
  /**
128
136
  * Check and install npm dependencies if needed
129
137
  */
130
138
  async function checkDependencies() {
131
139
  const nodeModulesExists = fs.existsSync('node_modules');
132
- const nextBinExists = fs.existsSync('./node_modules/.bin/next');
140
+ const nextBinPath = getNextBinPath();
141
+ const nextBinExists = fs.existsSync(nextBinPath);
133
142
 
134
143
  if (!nodeModulesExists || !nextBinExists) {
135
144
  console.log(chalk.yellow('📦 Installing dependencies...'));
@@ -157,7 +166,7 @@ async function startDevServer(port) {
157
166
  return new Promise((resolve, reject) => {
158
167
  console.log(chalk.dim(`Starting Next.js on port ${port}...`));
159
168
 
160
- const nextBin = './node_modules/.bin/next';
169
+ const nextBin = getNextBinPath();
161
170
  const nodePath = getNodePath();
162
171
  const nodeEnv = getNodeEnv();
163
172
 
package/commands/flows.js CHANGED
@@ -56,7 +56,7 @@ async function getCapturedPayload(token) {
56
56
  async function getWorkflowDraft(workflowId) {
57
57
  const apiUrl = getApiUrl();
58
58
  const { data } = await axios.get(
59
- `${apiUrl}/api/workflows/${workflowId}/draft`,
59
+ `${apiUrl}/api/flows/${workflowId}/draft`,
60
60
  { headers: getAuthHeaders() }
61
61
  );
62
62
  return data;
@@ -68,7 +68,7 @@ async function getWorkflowDraft(workflowId) {
68
68
  async function saveWorkflowDraft(workflowId, config) {
69
69
  const apiUrl = getApiUrl();
70
70
  await axios.put(
71
- `${apiUrl}/api/workflows/${workflowId}/draft`,
71
+ `${apiUrl}/api/flows/${workflowId}/draft`,
72
72
  { config },
73
73
  { headers: getAuthHeaders() }
74
74
  );
@@ -255,7 +255,7 @@ ${chalk.bold('Examples:')}
255
255
  case 'sync': {
256
256
  info('Syncing flows from cloud...');
257
257
 
258
- const { data } = await axios.get(`${apiUrl}/api/workflows?include_config=true`, {
258
+ const { data } = await axios.get(`${apiUrl}/api/flows?include_config=true`, {
259
259
  headers: getAuthHeaders(),
260
260
  });
261
261
 
@@ -388,7 +388,7 @@ ${chalk.bold('Examples:')}
388
388
 
389
389
  info(`Loading flow: ${flowId}`);
390
390
  const { data } = await axios.get(
391
- `${apiUrl}/api/workflows/${flowId}`,
391
+ `${apiUrl}/api/flows/${flowId}`,
392
392
  { headers: getAuthHeaders() }
393
393
  );
394
394
 
@@ -472,9 +472,9 @@ ${chalk.bold('Examples:')}
472
472
  metadata: newFlow.metadata || {},
473
473
  };
474
474
 
475
- // Step 1: Create workflow in cloud database (this generates a cloud UUID)
475
+ // Step 1: Create flow in cloud database (this generates a cloud UUID)
476
476
  const createResponse = await axios.post(
477
- `${apiUrl}/api/workflows`,
477
+ `${apiUrl}/api/flows`,
478
478
  {
479
479
  name: newFlow.name,
480
480
  description: newFlow.description,
@@ -490,9 +490,9 @@ ${chalk.bold('Examples:')}
490
490
 
491
491
  info(`Created in cloud with ID: ${cloudId}`);
492
492
 
493
- // Step 2: Publish the workflow (copies draft config to published)
493
+ // Step 2: Publish the flow (copies draft config to published)
494
494
  const publishResponse = await axios.post(
495
- `${apiUrl}/api/workflows/${cloudId}/publish`,
495
+ `${apiUrl}/api/flows/${cloudId}/publish`,
496
496
  {},
497
497
  { headers: getAuthHeaders() }
498
498
  );
@@ -515,6 +515,8 @@ ${chalk.bold('Examples:')}
515
515
  // Cloud sync tracking - matches markFlowPublished()
516
516
  cloudPublishedAt: now,
517
517
  cloudStatus: 'published',
518
+ // Track who deployed (for teammate detection)
519
+ lastDeployedBy: publishResponse.data.triggeredBy || null,
518
520
  };
519
521
  saveLocalFlow(flowId, updatedFlow);
520
522
 
@@ -615,7 +617,7 @@ ${chalk.bold('Examples:')}
615
617
  };
616
618
 
617
619
  const { data } = await axios.post(
618
- `${apiUrl}/api/workflows/${flowId}/publish`,
620
+ `${apiUrl}/api/flows/${flowId}/publish`,
619
621
  {
620
622
  name: localFlow.name,
621
623
  description: localFlow.description,
@@ -642,6 +644,8 @@ ${chalk.bold('Examples:')}
642
644
  // Cloud sync tracking - matches markFlowPublished()
643
645
  cloudPublishedAt: now,
644
646
  cloudStatus: 'published',
647
+ // Track who deployed (for teammate detection)
648
+ lastDeployedBy: data.triggeredBy || null,
645
649
  };
646
650
  saveLocalFlow(flowId, updatedFlow);
647
651
 
@@ -5,9 +5,15 @@ const path = require('path');
5
5
  const FormData = require('form-data');
6
6
  const {
7
7
  getApiUrl,
8
+ getStudioApiUrl,
8
9
  getAuthHeaders,
9
10
  isAuthenticated,
11
+ getProjectId,
12
+ getOrgId,
10
13
  } = require('../lib/config');
14
+
15
+ // Public R2 URL for the lux-knowledge bucket
16
+ const R2_PUBLIC_URL = 'https://pub-12ee5415430243b7bfc167df804f52e2.r2.dev';
11
17
  const {
12
18
  error,
13
19
  success,
@@ -36,138 +42,163 @@ ${chalk.bold('Usage:')} lux knowledge <command> [args]
36
42
 
37
43
  ${chalk.bold('Document Commands:')}
38
44
  list List all documents and folders
39
- get <document-id> Get document details and content
40
- url <document-id> Get public URL for a document
41
- upload <file-path> [folder-id] Upload a file to knowledge base
42
- delete <document-id> Delete a document
45
+ get <file-path> Get document details and content
46
+ url <file-path> Get public URL for a document
47
+ upload <local-file> [folder] Upload a file to knowledge base
48
+ delete <file-path> Delete a document
43
49
 
44
50
  ${chalk.bold('Folder Commands:')}
45
51
  folders list List all folders
46
- folders create <name> [parent-id] Create a new folder
47
- folders delete <folder-id> Delete a folder
52
+ folders create <folder-path> Create a new folder
53
+ folders delete <folder-path> Delete a folder and all contents
48
54
 
49
55
  ${chalk.bold('Examples:')}
50
56
  lux knowledge list
51
- lux knowledge get doc_123abc
57
+ lux knowledge get custom-designs/blanks/blank-white-front.png
58
+ lux knowledge url custom-designs/blanks/blank-white-front.png
52
59
  lux knowledge upload ./data.json
53
- lux knowledge upload ./image.png folder_xyz
54
- lux knowledge folders create "Product Docs"
60
+ lux knowledge upload ./image.png custom-designs/blanks
61
+ lux knowledge folders create product-docs
62
+ lux knowledge folders delete product-docs
55
63
  `);
56
64
  process.exit(0);
57
65
  }
58
66
 
59
67
  const apiUrl = getApiUrl();
68
+ const studioApiUrl = getStudioApiUrl();
69
+ const projectId = getProjectId();
60
70
 
61
71
  try {
62
72
  // ============ DOCUMENT COMMANDS ============
63
73
  if (command === 'list') {
64
74
  info('Loading knowledge base...');
65
75
 
66
- const [foldersRes, docsRes] = await Promise.all([
67
- axios.get(`${apiUrl}/api/knowledge/folders`, {
68
- headers: getAuthHeaders(),
69
- }),
70
- axios.get(`${apiUrl}/api/knowledge/documents`, {
71
- headers: getAuthHeaders(),
72
- }),
73
- ]);
76
+ const { data } = await axios.get(
77
+ `${studioApiUrl}/api/projects/${projectId}/knowledge`,
78
+ { headers: getAuthHeaders() }
79
+ );
80
+
81
+ if (!data.success) {
82
+ error(data.error || 'Failed to list knowledge');
83
+ return;
84
+ }
74
85
 
75
- const folders = foldersRes.data.folders || [];
76
- const documents = docsRes.data.documents || [];
86
+ const tree = data.tree || [];
87
+ const totalFiles = data.totalFiles || 0;
77
88
 
78
- if (folders.length === 0 && documents.length === 0) {
89
+ if (tree.length === 0) {
79
90
  console.log('\n(No knowledge items found)\n');
80
91
  return;
81
92
  }
82
93
 
83
- if (folders.length > 0) {
84
- console.log(`\n${chalk.bold('Folders')} (${folders.length}):\n`);
85
- formatTable(folders.map(f => ({
86
- id: f.id,
87
- name: f.name,
88
- path: f.path,
89
- })));
94
+ console.log(`\n${chalk.bold('Knowledge Base')} (${totalFiles} files):\n`);
95
+
96
+ // Print tree structure
97
+ function printTree(nodes, indent = '') {
98
+ for (const node of nodes) {
99
+ if (node.type === 'folder') {
100
+ console.log(`${indent}${chalk.blue('📁')} ${chalk.bold(node.name)}/`);
101
+ if (node.children && node.children.length > 0) {
102
+ printTree(node.children, indent + ' ');
103
+ }
104
+ } else {
105
+ const size = node.size ? ` (${formatFileSize(node.size)})` : '';
106
+ console.log(`${indent}${chalk.gray('📄')} ${node.name}${chalk.gray(size)}`);
107
+ }
108
+ }
90
109
  }
91
110
 
92
- if (documents.length > 0) {
93
- console.log(`\n${chalk.bold('Documents')} (${documents.length}):\n`);
94
- formatTable(documents.map(d => ({
95
- id: d.id,
96
- name: d.name,
97
- type: d.document_type || d.file_type || 'unknown',
98
- size: formatFileSize(d.file_size),
99
- folder: d.folder_id || '(root)',
100
- })));
101
- }
111
+ printTree(tree);
102
112
  console.log('');
103
113
  }
104
114
 
105
115
  else if (command === 'get') {
106
- requireArgs(args.slice(1), 1, 'lux knowledge get <document-id>');
107
- const docId = args[1];
116
+ requireArgs(args.slice(1), 1, 'lux knowledge get <file-path>');
117
+ const filePath = args[1];
118
+ const orgId = getOrgId();
108
119
 
109
- info(`Loading document: ${docId}`);
120
+ info(`Loading document: ${filePath}`);
110
121
  const { data } = await axios.get(
111
- `${apiUrl}/api/knowledge/documents/${docId}`,
122
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/${filePath}`,
112
123
  { headers: getAuthHeaders() }
113
124
  );
114
125
 
115
- const doc = data.document;
116
- console.log(`\n${chalk.bold('Document Details:')}`);
117
- console.log(` ID: ${doc.id}`);
118
- console.log(` Name: ${doc.name}`);
119
- console.log(` Type: ${doc.document_type || doc.file_type || 'unknown'}`);
120
- console.log(` Size: ${formatFileSize(doc.file_size)}`);
121
- console.log(` Folder: ${doc.folder_id || '(root)'}`);
122
-
123
- if (doc.public_url) {
124
- console.log(` URL: ${doc.public_url}`);
126
+ if (!data.success) {
127
+ error(data.error || 'Failed to get file');
128
+ return;
125
129
  }
126
130
 
127
- if (doc.image_width && doc.image_height) {
128
- console.log(` Dimensions: ${doc.image_width}x${doc.image_height}`);
129
- console.log(` Est. Tokens: ${doc.estimated_tokens || 'N/A'}`);
131
+ // Extract filename from path
132
+ const fileName = filePath.split('/').pop() || filePath;
133
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
134
+
135
+ console.log(`\n${chalk.bold('Document Details:')}`);
136
+ console.log(` Path: ${filePath}`);
137
+ console.log(` Name: ${fileName}`);
138
+ console.log(` Size: ${formatFileSize(data.size)}`);
139
+ console.log(` Encoding: ${data.encoding}`);
140
+ console.log(` Modified: ${data.modifiedAt || 'N/A'}`);
141
+
142
+ // Show public URL for binary files
143
+ if (data.encoding === 'base64') {
144
+ const publicUrl = `${R2_PUBLIC_URL}/${orgId}/${projectId}/knowledge/${filePath}`;
145
+ console.log(` URL: ${publicUrl}`);
130
146
  }
131
147
 
132
- if (doc.content) {
148
+ // Show content preview for text files
149
+ if (data.encoding === 'utf-8' && data.content) {
133
150
  console.log(`\n${chalk.bold('Content:')}`);
134
- console.log(doc.content.substring(0, 2000));
135
- if (doc.content.length > 2000) {
136
- console.log(chalk.gray(`\n... (${doc.content.length - 2000} more characters)`));
151
+ const preview = data.content.substring(0, 2000);
152
+ console.log(preview);
153
+ if (data.content.length > 2000) {
154
+ console.log(chalk.gray(`\n... (${data.content.length - 2000} more characters)`));
137
155
  }
138
156
  }
139
157
  console.log('');
140
158
  }
141
159
 
142
160
  else if (command === 'url') {
143
- requireArgs(args.slice(1), 1, 'lux knowledge url <document-id>');
144
- const docId = args[1];
161
+ requireArgs(args.slice(1), 1, 'lux knowledge url <file-path>');
162
+ const filePath = args[1];
163
+ const orgId = getOrgId();
145
164
 
146
- info(`Getting URL for document: ${docId}`);
165
+ info(`Getting URL for document: ${filePath}`);
166
+
167
+ // Verify the file exists by fetching from the project-based knowledge API
147
168
  const { data } = await axios.get(
148
- `${apiUrl}/api/knowledge/documents/${docId}/presigned-url`,
169
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/${filePath}`,
149
170
  { headers: getAuthHeaders() }
150
171
  );
151
172
 
173
+ if (!data.success) {
174
+ error(data.error || 'Failed to get file');
175
+ return;
176
+ }
177
+
178
+ // Construct the public R2 URL
179
+ // Format: https://pub-{bucket-id}.r2.dev/{orgId}/{projectId}/knowledge/{filePath}
180
+ const publicUrl = `${R2_PUBLIC_URL}/${orgId}/${projectId}/knowledge/${filePath}`;
181
+
152
182
  console.log(`\n${chalk.bold('Public URL:')}`);
153
- console.log(data.presignedUrl);
154
- console.log(`\nDocument: ${data.document.name} (${formatFileSize(data.document.file_size)})`);
183
+ console.log(publicUrl);
184
+ console.log(`\nFile: ${filePath} (${formatFileSize(data.size)})`);
155
185
  console.log('');
156
186
  }
157
187
 
158
188
  else if (command === 'upload') {
159
- requireArgs(args.slice(1), 1, 'lux knowledge upload <file-path> [folder-id]');
160
- const filePath = args[1];
161
- const folderId = args[2] || null;
189
+ requireArgs(args.slice(1), 1, 'lux knowledge upload <local-file> [folder]');
190
+ const localFilePath = args[1];
191
+ const folder = args[2] || '';
192
+ const orgId = getOrgId();
162
193
 
163
194
  // Check file exists
164
- if (!fs.existsSync(filePath)) {
165
- error(`File not found: ${filePath}`);
195
+ if (!fs.existsSync(localFilePath)) {
196
+ error(`File not found: ${localFilePath}`);
166
197
  return;
167
198
  }
168
199
 
169
- const fileName = path.basename(filePath);
170
- const fileBuffer = fs.readFileSync(filePath);
200
+ const fileName = path.basename(localFilePath);
201
+ const fileBuffer = fs.readFileSync(localFilePath);
171
202
  const fileSize = fileBuffer.length;
172
203
 
173
204
  // Detect MIME type
@@ -187,60 +218,70 @@ ${chalk.bold('Examples:')}
187
218
  '.webp': 'image/webp',
188
219
  '.svg': 'image/svg+xml',
189
220
  };
190
- const fileType = mimeTypes[ext] || 'application/octet-stream';
221
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
191
222
 
192
223
  info(`Uploading: ${fileName} (${formatFileSize(fileSize)})`);
193
224
 
194
- // Step 1: Get presigned upload URL
225
+ // Step 1: Get presigned upload URL from the project-based API
195
226
  const { data: uploadData } = await axios.post(
196
- `${apiUrl}/api/knowledge/upload`,
197
- { fileName, fileType },
227
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/upload-url`,
228
+ { path: fileName, contentType, folder },
198
229
  { headers: getAuthHeaders() }
199
230
  );
200
231
 
201
- // Step 2: Upload to R2
232
+ if (!uploadData.success) {
233
+ error(uploadData.error || 'Failed to get upload URL');
234
+ return;
235
+ }
236
+
237
+ // Step 2: Upload directly to R2 using presigned URL
202
238
  info('Uploading to storage...');
203
239
  await axios.put(uploadData.uploadUrl, fileBuffer, {
204
240
  headers: {
205
- 'Content-Type': fileType,
241
+ 'Content-Type': contentType,
206
242
  },
207
243
  maxBodyLength: Infinity,
208
244
  maxContentLength: Infinity,
209
245
  });
210
246
 
211
- // Step 3: Complete upload (save to database)
212
- info('Saving document metadata...');
213
- const { data: completeData } = await axios.put(
214
- `${apiUrl}/api/knowledge/upload`,
215
- {
216
- filePath: uploadData.filePath,
217
- fileName,
218
- fileType,
219
- folderId,
220
- fileSize,
221
- },
247
+ // Step 3: Confirm upload completed
248
+ info('Confirming upload...');
249
+ const { data: confirmData } = await axios.post(
250
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/confirm-upload`,
251
+ { path: uploadData.path, size: fileSize },
222
252
  { headers: getAuthHeaders() }
223
253
  );
224
254
 
225
- success('Document uploaded!');
226
- console.log(` ID: ${completeData.document.id}`);
227
- console.log(` Name: ${completeData.document.name}`);
228
- if (completeData.document.public_url) {
229
- console.log(` URL: ${completeData.document.public_url}`);
255
+ if (!confirmData.success) {
256
+ error(confirmData.error || 'Failed to confirm upload');
257
+ return;
230
258
  }
259
+
260
+ // Construct public URL
261
+ const publicUrl = `${R2_PUBLIC_URL}/${orgId}/${projectId}/knowledge/${uploadData.path}`;
262
+
263
+ success('Document uploaded!');
264
+ console.log(` Path: ${uploadData.path}`);
265
+ console.log(` Size: ${formatFileSize(fileSize)}`);
266
+ console.log(` URL: ${publicUrl}`);
231
267
  console.log('');
232
268
  }
233
269
 
234
270
  else if (command === 'delete') {
235
- requireArgs(args.slice(1), 1, 'lux knowledge delete <document-id>');
236
- const docId = args[1];
271
+ requireArgs(args.slice(1), 1, 'lux knowledge delete <file-path>');
272
+ const filePath = args[1];
237
273
 
238
- info(`Deleting document: ${docId}`);
239
- await axios.delete(
240
- `${apiUrl}/api/knowledge/documents/${docId}`,
274
+ info(`Deleting: ${filePath}`);
275
+ const { data } = await axios.delete(
276
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/${filePath}`,
241
277
  { headers: getAuthHeaders() }
242
278
  );
243
279
 
280
+ if (!data.success) {
281
+ error(data.error || 'Failed to delete file');
282
+ return;
283
+ }
284
+
244
285
  success('Document deleted!');
245
286
  }
246
287
 
@@ -250,67 +291,82 @@ ${chalk.bold('Examples:')}
250
291
 
251
292
  if (!subCommand || subCommand === 'list') {
252
293
  info('Loading folders...');
253
- const { data } = await axios.get(`${apiUrl}/api/knowledge/folders`, {
254
- headers: getAuthHeaders(),
255
- });
294
+ const { data } = await axios.get(
295
+ `${studioApiUrl}/api/projects/${projectId}/knowledge`,
296
+ { headers: getAuthHeaders() }
297
+ );
256
298
 
257
- if (!data.folders || data.folders.length === 0) {
299
+ if (!data.success) {
300
+ error(data.error || 'Failed to list knowledge');
301
+ return;
302
+ }
303
+
304
+ // Extract folders from tree
305
+ const folders = [];
306
+ function collectFolders(nodes, parentPath = '') {
307
+ for (const node of nodes) {
308
+ if (node.type === 'folder') {
309
+ folders.push({
310
+ name: node.name,
311
+ path: node.path,
312
+ });
313
+ if (node.children && node.children.length > 0) {
314
+ collectFolders(node.children, node.path);
315
+ }
316
+ }
317
+ }
318
+ }
319
+ collectFolders(data.tree || []);
320
+
321
+ if (folders.length === 0) {
258
322
  console.log('\n(No folders found)\n');
259
323
  } else {
260
- console.log(`\nFound ${data.folders.length} folder(s):\n`);
261
- formatTable(data.folders.map(f => ({
262
- id: f.id,
263
- name: f.name,
264
- path: f.path,
265
- parent: f.parent_id || '(root)',
266
- })));
324
+ console.log(`\nFound ${folders.length} folder(s):\n`);
325
+ formatTable(folders);
267
326
  console.log('');
268
327
  }
269
328
  }
270
329
 
271
330
  else if (subCommand === 'create') {
272
- requireArgs(args.slice(2), 1, 'lux knowledge folders create <name> [parent-id]');
273
- const name = args[2];
274
- const parentId = args[3] || null;
275
-
276
- // Calculate path
277
- let folderPath = `/${name}`;
278
- if (parentId) {
279
- // Fetch parent to get its path
280
- const { data: foldersData } = await axios.get(`${apiUrl}/api/knowledge/folders`, {
281
- headers: getAuthHeaders(),
282
- });
283
- const parent = (foldersData.folders || []).find(f => f.id === parentId);
284
- if (parent) {
285
- folderPath = `${parent.path}/${name}`;
286
- }
287
- }
331
+ requireArgs(args.slice(2), 1, 'lux knowledge folders create <folder-path>');
332
+ const folderPath = args[2];
333
+
334
+ info(`Creating folder: ${folderPath}`);
288
335
 
289
- info(`Creating folder: ${name}`);
336
+ // In R2, folders are virtual - they're created by the folder endpoint
290
337
  const { data } = await axios.post(
291
- `${apiUrl}/api/knowledge/folders`,
292
- { name, parent_id: parentId, path: folderPath },
338
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/folder/${folderPath}`,
339
+ {},
293
340
  { headers: getAuthHeaders() }
294
341
  );
295
342
 
343
+ if (!data.success) {
344
+ error(data.error || 'Failed to create folder');
345
+ return;
346
+ }
347
+
296
348
  success('Folder created!');
297
- console.log(` ID: ${data.folder.id}`);
298
- console.log(` Name: ${data.folder.name}`);
299
- console.log(` Path: ${data.folder.path}`);
349
+ console.log(` Path: ${data.path}`);
300
350
  console.log('');
301
351
  }
302
352
 
303
353
  else if (subCommand === 'delete') {
304
- requireArgs(args.slice(2), 1, 'lux knowledge folders delete <folder-id>');
305
- const folderId = args[2];
354
+ requireArgs(args.slice(2), 1, 'lux knowledge folders delete <folder-path>');
355
+ const folderPath = args[2];
306
356
 
307
- info(`Deleting folder: ${folderId}`);
308
- await axios.delete(
309
- `${apiUrl}/api/knowledge/folders?id=${folderId}`,
357
+ info(`Deleting folder: ${folderPath}`);
358
+ const { data } = await axios.delete(
359
+ `${studioApiUrl}/api/projects/${projectId}/knowledge/${folderPath}`,
310
360
  { headers: getAuthHeaders() }
311
361
  );
312
362
 
313
- success('Folder deleted!');
363
+ if (!data.success) {
364
+ error(data.error || 'Failed to delete folder');
365
+ return;
366
+ }
367
+
368
+ const deletedCount = data.deletedCount || 1;
369
+ success(`Folder deleted! (${deletedCount} item${deletedCount > 1 ? 's' : ''} removed)`);
314
370
  }
315
371
 
316
372
  else {
package/commands/login.js CHANGED
@@ -177,7 +177,7 @@ async function manualLogin(providedKey) {
177
177
  const apiUrl = getApiUrl();
178
178
 
179
179
  // Validate the API key by making a test request
180
- const response = await axios.get(`${apiUrl}/api/workflows`, {
180
+ const response = await axios.get(`${apiUrl}/api/flows`, {
181
181
  headers: {
182
182
  Authorization: `Bearer ${apiKey}`,
183
183
  'X-Org-Id': extractOrgIdFromKey(apiKey),
@@ -231,7 +231,8 @@ async function deployProject(projectId) {
231
231
  createGitignore(projectDir);
232
232
 
233
233
  // Always configure git user (required for commits)
234
- execSync('git config user.email "deploy@uselux.ai"', { cwd: projectDir, stdio: 'pipe' });
234
+ // Use jason@uselux.ai so Vercel recognizes the commit author as a team member
235
+ execSync('git config user.email "jason@uselux.ai"', { cwd: projectDir, stdio: 'pipe' });
235
236
  execSync('git config user.name "Lux Deploy"', { cwd: projectDir, stdio: 'pipe' });
236
237
 
237
238
  gitSpinner.succeed('Git repository ready');
@@ -496,7 +497,7 @@ async function deployProject(projectId) {
496
497
  // Deploy to cloud API using /publish endpoint
497
498
  // The publish endpoint supports CLI auth and will create the flow if it doesn't exist
498
499
  const { data: deployData } = await axios.post(
499
- `${apiUrl}/api/workflows/${flowId}/publish`,
500
+ `${apiUrl}/api/flows/${flowId}/publish`,
500
501
  {
501
502
  // Wrap flow data in config object as expected by /publish
502
503
  config: {
package/lib/config.js CHANGED
@@ -4,48 +4,78 @@ const os = require('os');
4
4
 
5
5
  // Shared storage directory (used by both Electron app and CLI)
6
6
  const LUX_STUDIO_DIR = path.join(os.homedir(), '.lux-studio');
7
- const ACTIVE_ORG_FILE = path.join(LUX_STUDIO_DIR, 'active-org.json');
7
+ const UNIFIED_CONFIG_FILE = path.join(LUX_STUDIO_DIR, 'config.json');
8
8
  const INTERFACE_FILE = '.lux/interface.json';
9
9
 
10
+ // Cache for unified config to avoid repeated file reads
11
+ let _unifiedConfigCache = null;
12
+ let _unifiedConfigMtime = null;
13
+
10
14
  /**
11
- * Load active org from shared storage
12
- * Returns { orgId, orgName } or null
15
+ * Load the unified config file (~/.lux-studio/config.json)
16
+ * This is the single source of truth used by both Electron app and CLI
13
17
  */
14
- function loadActiveOrg() {
15
- if (!fs.existsSync(ACTIVE_ORG_FILE)) {
18
+ function loadUnifiedConfig() {
19
+ if (!fs.existsSync(UNIFIED_CONFIG_FILE)) {
16
20
  return null;
17
21
  }
18
22
 
19
23
  try {
20
- const content = fs.readFileSync(ACTIVE_ORG_FILE, 'utf8');
21
- return JSON.parse(content);
24
+ // Check if file has changed since last read
25
+ const stats = fs.statSync(UNIFIED_CONFIG_FILE);
26
+ if (_unifiedConfigCache && _unifiedConfigMtime && stats.mtimeMs === _unifiedConfigMtime) {
27
+ return _unifiedConfigCache;
28
+ }
29
+
30
+ const content = fs.readFileSync(UNIFIED_CONFIG_FILE, 'utf8');
31
+ _unifiedConfigCache = JSON.parse(content);
32
+ _unifiedConfigMtime = stats.mtimeMs;
33
+ return _unifiedConfigCache;
22
34
  } catch (error) {
23
35
  return null;
24
36
  }
25
37
  }
26
38
 
27
39
  /**
28
- * Load credentials for a specific org
29
- * Returns { apiKey, orgId, orgName } or null
40
+ * Load active org from unified config
41
+ * Returns { orgId, orgName } or null
30
42
  */
31
- function loadOrgCredentials(orgId) {
32
- const credentialsPath = path.join(LUX_STUDIO_DIR, orgId, 'credentials.json');
33
-
34
- if (!fs.existsSync(credentialsPath)) {
43
+ function loadActiveOrg() {
44
+ const unifiedConfig = loadUnifiedConfig();
45
+ if (!unifiedConfig || !unifiedConfig.currentOrg) {
35
46
  return null;
36
47
  }
37
48
 
38
- try {
39
- const content = fs.readFileSync(credentialsPath, 'utf8');
40
- return JSON.parse(content);
41
- } catch (error) {
49
+ const orgId = unifiedConfig.currentOrg;
50
+ const orgData = unifiedConfig.orgs?.[orgId];
51
+
52
+ return {
53
+ orgId,
54
+ orgName: orgData?.name || null,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Load credentials for a specific org from unified config
60
+ * Returns { apiKey, orgId, orgName } or null
61
+ */
62
+ function loadOrgCredentials(orgId) {
63
+ const unifiedConfig = loadUnifiedConfig();
64
+ if (!unifiedConfig || !unifiedConfig.orgs?.[orgId]) {
42
65
  return null;
43
66
  }
67
+
68
+ const orgData = unifiedConfig.orgs[orgId];
69
+ return {
70
+ apiKey: orgData.apiKey,
71
+ orgId: orgId,
72
+ orgName: orgData.name || null,
73
+ };
44
74
  }
45
75
 
46
76
  /**
47
77
  * Load config for the currently active org
48
- * Reads from ~/.lux-studio/active-org.json to get org, then loads credentials
78
+ * Reads from unified ~/.lux-studio/config.json
49
79
  */
50
80
  function loadConfig() {
51
81
  // Check for env vars first (for CI/automation)
@@ -56,22 +86,22 @@ function loadConfig() {
56
86
  };
57
87
  }
58
88
 
59
- // Load active org from shared storage
60
- const activeOrg = loadActiveOrg();
61
- if (!activeOrg || !activeOrg.orgId) {
89
+ const unifiedConfig = loadUnifiedConfig();
90
+ if (!unifiedConfig || !unifiedConfig.currentOrg) {
62
91
  return null;
63
92
  }
64
93
 
65
- // Load credentials for that org
66
- const credentials = loadOrgCredentials(activeOrg.orgId);
67
- if (!credentials) {
94
+ const orgId = unifiedConfig.currentOrg;
95
+ const orgData = unifiedConfig.orgs?.[orgId];
96
+
97
+ if (!orgData || !orgData.apiKey) {
68
98
  return null;
69
99
  }
70
100
 
71
101
  return {
72
- apiKey: credentials.apiKey,
73
- orgId: credentials.orgId,
74
- orgName: credentials.orgName,
102
+ apiKey: orgData.apiKey,
103
+ orgId: orgId,
104
+ orgName: orgData.name || null,
75
105
  };
76
106
  }
77
107
 
@@ -283,22 +313,14 @@ function deleteLocalFlow(flowId) {
283
313
  }
284
314
 
285
315
  /**
286
- * Get the current project ID from org config
316
+ * Get the current project ID from unified config
287
317
  * Defaults to 'default' if not set
288
318
  */
289
319
  function getProjectId() {
290
- const orgId = getOrgId();
291
- if (!orgId) return 'default';
320
+ const unifiedConfig = loadUnifiedConfig();
321
+ if (!unifiedConfig) return 'default';
292
322
 
293
- const orgConfigPath = path.join(LUX_STUDIO_DIR, orgId, 'config.json');
294
- if (!fs.existsSync(orgConfigPath)) return 'default';
295
-
296
- try {
297
- const orgConfig = JSON.parse(fs.readFileSync(orgConfigPath, 'utf8'));
298
- return orgConfig.currentProject || 'default';
299
- } catch {
300
- return 'default';
301
- }
323
+ return unifiedConfig.currentProject || 'default';
302
324
  }
303
325
 
304
326
  /**
@@ -392,6 +414,7 @@ module.exports = {
392
414
  saveConfig,
393
415
  loadActiveOrg,
394
416
  loadOrgCredentials,
417
+ loadUnifiedConfig,
395
418
  loadInterfaceConfig,
396
419
  saveInterfaceConfig,
397
420
  getApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxlabs",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "CLI tool for Lux - Upload and deploy interfaces from your terminal",
5
5
  "author": "Jason Henkel <jason@uselux.ai>",
6
6
  "license": "SEE LICENSE IN LICENSE",