luxlabs 1.0.18 → 1.0.20
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 +11 -2
- package/commands/{workflows.js → flows.js} +142 -138
- package/commands/knowledge.js +193 -137
- package/commands/login.js +1 -1
- package/commands/project.js +3 -2
- package/lux.js +6 -6
- package/package.json +2 -2
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
|
|
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 =
|
|
169
|
+
const nextBin = getNextBinPath();
|
|
161
170
|
const nodePath = getNodePath();
|
|
162
171
|
const nodeEnv = getNodeEnv();
|
|
163
172
|
|
|
@@ -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/
|
|
59
|
+
`${apiUrl}/api/flows/${workflowId}/draft`,
|
|
60
60
|
{ headers: getAuthHeaders() }
|
|
61
61
|
);
|
|
62
62
|
return data;
|
|
@@ -68,13 +68,13 @@ async function getWorkflowDraft(workflowId) {
|
|
|
68
68
|
async function saveWorkflowDraft(workflowId, config) {
|
|
69
69
|
const apiUrl = getApiUrl();
|
|
70
70
|
await axios.put(
|
|
71
|
-
`${apiUrl}/api/
|
|
71
|
+
`${apiUrl}/api/flows/${workflowId}/draft`,
|
|
72
72
|
{ config },
|
|
73
73
|
{ headers: getAuthHeaders() }
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
async function
|
|
77
|
+
async function handleFlows(args) {
|
|
78
78
|
// Check authentication
|
|
79
79
|
if (!isAuthenticated()) {
|
|
80
80
|
console.log(
|
|
@@ -89,18 +89,18 @@ async function handleWorkflows(args) {
|
|
|
89
89
|
|
|
90
90
|
if (!command) {
|
|
91
91
|
console.log(`
|
|
92
|
-
${chalk.bold('Usage:')} lux
|
|
92
|
+
${chalk.bold('Usage:')} lux flows <command> [args]
|
|
93
93
|
|
|
94
94
|
${chalk.bold('Commands:')}
|
|
95
|
-
list List all
|
|
96
|
-
get <id> Get
|
|
97
|
-
status <id> Show sync status for a
|
|
98
|
-
create <name> [desc] [--publish] Create
|
|
99
|
-
init <name> [desc] [--publish] Initialize
|
|
95
|
+
list List all flows (from local storage)
|
|
96
|
+
get <id> Get flow details with config
|
|
97
|
+
status <id> Show sync status for a flow
|
|
98
|
+
create <name> [desc] [--publish] Create flow (use --publish to publish immediately)
|
|
99
|
+
init <name> [desc] [--publish] Initialize flow (alias for create)
|
|
100
100
|
save <id> <config-file> Save draft config from file (local)
|
|
101
|
-
sync Sync published
|
|
102
|
-
publish <id> Publish
|
|
103
|
-
delete <id> Delete a local
|
|
101
|
+
sync Sync published flows from cloud
|
|
102
|
+
publish <id> Publish flow to cloud
|
|
103
|
+
delete <id> Delete a local flow
|
|
104
104
|
diff <id> Show local vs published differences
|
|
105
105
|
|
|
106
106
|
${chalk.bold('Execution History:')}
|
|
@@ -128,33 +128,33 @@ ${chalk.bold('Execution Commands:')}
|
|
|
128
128
|
last <id> Show the last execution result
|
|
129
129
|
|
|
130
130
|
${chalk.bold('Examples:')}
|
|
131
|
-
lux
|
|
132
|
-
lux
|
|
133
|
-
lux
|
|
134
|
-
lux
|
|
135
|
-
lux
|
|
136
|
-
lux
|
|
137
|
-
lux
|
|
138
|
-
lux
|
|
139
|
-
lux
|
|
140
|
-
lux
|
|
131
|
+
lux flows list
|
|
132
|
+
lux flows get flow_123
|
|
133
|
+
lux flows status flow_123
|
|
134
|
+
lux flows create "My Flow" "Description"
|
|
135
|
+
lux flows create "My Flow" --publish # Create and publish immediately
|
|
136
|
+
lux flows init "My Flow" --publish
|
|
137
|
+
lux flows save flow_123 ./config.json
|
|
138
|
+
lux flows sync
|
|
139
|
+
lux flows publish flow_123
|
|
140
|
+
lux flows diff flow_123
|
|
141
141
|
|
|
142
142
|
${chalk.bold('Execution history:')}
|
|
143
|
-
lux
|
|
144
|
-
lux
|
|
145
|
-
lux
|
|
146
|
-
lux
|
|
143
|
+
lux flows executions my-flow-id
|
|
144
|
+
lux flows executions my-flow-id --limit 50
|
|
145
|
+
lux flows execution my-flow-id exec_abc123
|
|
146
|
+
lux flows node my-flow-id exec_abc123 node-1
|
|
147
147
|
|
|
148
|
-
${chalk.bold('Webhook
|
|
149
|
-
lux
|
|
150
|
-
lux
|
|
151
|
-
lux
|
|
152
|
-
lux
|
|
148
|
+
${chalk.bold('Webhook:')}
|
|
149
|
+
lux flows webhook-url flow_123
|
|
150
|
+
lux flows webhook-listen flow_123
|
|
151
|
+
lux flows webhook-poll flow_123
|
|
152
|
+
lux flows webhook-accept flow_123
|
|
153
153
|
|
|
154
154
|
${chalk.bold('Execution debugging:')}
|
|
155
|
-
lux
|
|
156
|
-
lux
|
|
157
|
-
lux
|
|
155
|
+
lux flows executions my-flow # List recent executions
|
|
156
|
+
lux flows last my-flow # Show last execution result
|
|
157
|
+
lux flows test my-flow --data '{"name":"test"}' # Test with data
|
|
158
158
|
`);
|
|
159
159
|
process.exit(0);
|
|
160
160
|
}
|
|
@@ -165,14 +165,14 @@ ${chalk.bold('Examples:')}
|
|
|
165
165
|
try {
|
|
166
166
|
switch (command) {
|
|
167
167
|
case 'list': {
|
|
168
|
-
info('Loading
|
|
168
|
+
info('Loading flows from local storage...');
|
|
169
169
|
const localFlows = listLocalFlows();
|
|
170
170
|
|
|
171
171
|
if (localFlows.length === 0) {
|
|
172
|
-
console.log('\n(No local
|
|
173
|
-
console.log(chalk.gray('Run "lux
|
|
172
|
+
console.log('\n(No local flows found)');
|
|
173
|
+
console.log(chalk.gray('Run "lux flows sync" to sync from cloud, or "lux flows create" to create one.\n'));
|
|
174
174
|
} else {
|
|
175
|
-
console.log(`\nFound ${localFlows.length}
|
|
175
|
+
console.log(`\nFound ${localFlows.length} flow(s):\n`);
|
|
176
176
|
|
|
177
177
|
// Color-code sync status
|
|
178
178
|
const statusColors = {
|
|
@@ -203,12 +203,12 @@ ${chalk.bold('Examples:')}
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
case 'status': {
|
|
206
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
207
|
-
const
|
|
206
|
+
requireArgs(args.slice(1), 1, 'lux flows status <id>');
|
|
207
|
+
const flowId = args[1];
|
|
208
208
|
|
|
209
|
-
const flow = loadLocalFlow(
|
|
209
|
+
const flow = loadLocalFlow(flowId);
|
|
210
210
|
if (!flow) {
|
|
211
|
-
error(`
|
|
211
|
+
error(`Flow not found locally: ${flowId}`);
|
|
212
212
|
break;
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -231,8 +231,8 @@ ${chalk.bold('Examples:')}
|
|
|
231
231
|
conflict: chalk.red,
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
-
console.log(`\n📝
|
|
235
|
-
console.log(` ID: ${
|
|
234
|
+
console.log(`\n📝 Flow: ${flow.name}`);
|
|
235
|
+
console.log(` ID: ${flowId}`);
|
|
236
236
|
console.log(` Sync Status: ${statusColors[syncStatus](syncStatus)}`);
|
|
237
237
|
console.log(` Local Version: ${flow.localVersion || 1}`);
|
|
238
238
|
console.log(` Published Version: ${flow.publishedVersion || '(never published)'}`);
|
|
@@ -243,19 +243,19 @@ ${chalk.bold('Examples:')}
|
|
|
243
243
|
console.log(` Edges: ${flow.edges?.length || 0}`);
|
|
244
244
|
|
|
245
245
|
if (syncStatus === 'conflict') {
|
|
246
|
-
console.log(chalk.red('\n⚠️ This
|
|
246
|
+
console.log(chalk.red('\n⚠️ This flow has conflicts with the cloud version.'));
|
|
247
247
|
console.log(chalk.gray(' Use the Lux Studio app to resolve conflicts.\n'));
|
|
248
248
|
} else if (syncStatus === 'dirty') {
|
|
249
|
-
console.log(chalk.yellow('\n📤 This
|
|
250
|
-
console.log(chalk.gray(' Run "lux
|
|
249
|
+
console.log(chalk.yellow('\n📤 This flow has unpublished local changes.'));
|
|
250
|
+
console.log(chalk.gray(' Run "lux flows publish ' + flowId + '" to publish.\n'));
|
|
251
251
|
}
|
|
252
252
|
break;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
case 'sync': {
|
|
256
|
-
info('Syncing
|
|
256
|
+
info('Syncing flows from cloud...');
|
|
257
257
|
|
|
258
|
-
const { data } = await axios.get(`${apiUrl}/api/
|
|
258
|
+
const { data } = await axios.get(`${apiUrl}/api/flows?include_config=true`, {
|
|
259
259
|
headers: getAuthHeaders(),
|
|
260
260
|
});
|
|
261
261
|
|
|
@@ -357,18 +357,18 @@ ${chalk.bold('Examples:')}
|
|
|
357
357
|
console.log(` New from cloud: ${newFromCloud}`);
|
|
358
358
|
if (conflicts > 0) {
|
|
359
359
|
console.log(chalk.red(` Conflicts: ${conflicts}`));
|
|
360
|
-
console.log(chalk.gray('\n Use "lux
|
|
360
|
+
console.log(chalk.gray('\n Use "lux flows list" to see flows with conflicts.'));
|
|
361
361
|
}
|
|
362
362
|
break;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
case 'delete': {
|
|
366
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
367
|
-
const
|
|
366
|
+
requireArgs(args.slice(1), 1, 'lux flows delete <id>');
|
|
367
|
+
const flowId = args[1];
|
|
368
368
|
|
|
369
|
-
const flow = loadLocalFlow(
|
|
369
|
+
const flow = loadLocalFlow(flowId);
|
|
370
370
|
if (!flow) {
|
|
371
|
-
error(`
|
|
371
|
+
error(`Flow not found locally: ${flowId}`);
|
|
372
372
|
break;
|
|
373
373
|
}
|
|
374
374
|
|
|
@@ -377,23 +377,23 @@ ${chalk.bold('Examples:')}
|
|
|
377
377
|
console.log(chalk.gray(' This only deletes the local copy. The published version remains in the cloud.\n'));
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
deleteLocalFlow(
|
|
381
|
-
success(`Deleted local
|
|
380
|
+
deleteLocalFlow(flowId);
|
|
381
|
+
success(`Deleted local flow: ${flow.name}`);
|
|
382
382
|
break;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
case 'get': {
|
|
386
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
387
|
-
const
|
|
386
|
+
requireArgs(args.slice(1), 1, 'lux flows get <id>');
|
|
387
|
+
const flowId = args[1];
|
|
388
388
|
|
|
389
|
-
info(`Loading
|
|
389
|
+
info(`Loading flow: ${flowId}`);
|
|
390
390
|
const { data } = await axios.get(
|
|
391
|
-
`${apiUrl}/api/
|
|
391
|
+
`${apiUrl}/api/flows/${flowId}`,
|
|
392
392
|
{ headers: getAuthHeaders() }
|
|
393
393
|
);
|
|
394
394
|
|
|
395
395
|
if (!data.workflow) {
|
|
396
|
-
error(`
|
|
396
|
+
error(`Flow not found: ${flowId}`);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
const workflow = data.workflow;
|
|
@@ -421,7 +421,7 @@ ${chalk.bold('Examples:')}
|
|
|
421
421
|
args.splice(publishFlagIndex, 1); // Remove flag from args
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
424
|
+
requireArgs(args.slice(1), 1, 'lux flows create <name> [description] [--publish]');
|
|
425
425
|
const name = args[1];
|
|
426
426
|
const description = args[2] || '';
|
|
427
427
|
|
|
@@ -454,15 +454,15 @@ ${chalk.bold('Examples:')}
|
|
|
454
454
|
updatedAt: now,
|
|
455
455
|
};
|
|
456
456
|
|
|
457
|
-
info(`Creating local
|
|
457
|
+
info(`Creating local flow: ${name}`);
|
|
458
458
|
|
|
459
459
|
if (saveLocalFlow(flowId, newFlow)) {
|
|
460
|
-
success(`
|
|
460
|
+
success(`Flow created locally!`);
|
|
461
461
|
console.log(` ID: ${flowId}`);
|
|
462
462
|
|
|
463
463
|
// If --publish flag was provided, publish immediately
|
|
464
464
|
if (shouldPublish) {
|
|
465
|
-
info(`Publishing
|
|
465
|
+
info(`Publishing flow to cloud...`);
|
|
466
466
|
|
|
467
467
|
try {
|
|
468
468
|
const publishConfig = {
|
|
@@ -472,9 +472,9 @@ ${chalk.bold('Examples:')}
|
|
|
472
472
|
metadata: newFlow.metadata || {},
|
|
473
473
|
};
|
|
474
474
|
|
|
475
|
-
// Step 1: Create
|
|
475
|
+
// Step 1: Create flow in cloud database (this generates a cloud UUID)
|
|
476
476
|
const createResponse = await axios.post(
|
|
477
|
-
`${apiUrl}/api/
|
|
477
|
+
`${apiUrl}/api/flows`,
|
|
478
478
|
{
|
|
479
479
|
name: newFlow.name,
|
|
480
480
|
description: newFlow.description,
|
|
@@ -485,14 +485,14 @@ ${chalk.bold('Examples:')}
|
|
|
485
485
|
|
|
486
486
|
const cloudId = createResponse.data.workflow?.id || createResponse.data.workflow?.workflow_id;
|
|
487
487
|
if (!cloudId) {
|
|
488
|
-
throw new Error('No
|
|
488
|
+
throw new Error('No flow ID returned from cloud');
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
info(`Created in cloud with ID: ${cloudId}`);
|
|
492
492
|
|
|
493
|
-
// Step 2: Publish the
|
|
493
|
+
// Step 2: Publish the flow (copies draft config to published)
|
|
494
494
|
const publishResponse = await axios.post(
|
|
495
|
-
`${apiUrl}/api/
|
|
495
|
+
`${apiUrl}/api/flows/${cloudId}/publish`,
|
|
496
496
|
{},
|
|
497
497
|
{ headers: getAuthHeaders() }
|
|
498
498
|
);
|
|
@@ -515,44 +515,46 @@ ${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
|
|
|
521
|
-
success('
|
|
523
|
+
success('Flow published!');
|
|
522
524
|
console.log(` Cloud ID: ${cloudId}`);
|
|
523
525
|
console.log(` Status: ${chalk.green('published')}`);
|
|
524
526
|
console.log(` Version: ${newVersion}`);
|
|
525
527
|
} catch (publishError) {
|
|
526
528
|
error(`Failed to publish: ${publishError.response?.data?.error || publishError.message}`);
|
|
527
529
|
console.log(` Status: ${chalk.gray('draft')}`);
|
|
528
|
-
console.log(chalk.gray(`\nRun "lux
|
|
530
|
+
console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to try again.`));
|
|
529
531
|
}
|
|
530
532
|
} else {
|
|
531
533
|
console.log(` Status: ${chalk.gray('draft')}`);
|
|
532
|
-
console.log(chalk.gray(`\n This
|
|
534
|
+
console.log(chalk.gray(`\n This flow is local only. Run "lux flows publish ${flowId}" to publish to cloud.`));
|
|
533
535
|
}
|
|
534
536
|
} else {
|
|
535
|
-
error('Failed to create
|
|
537
|
+
error('Failed to create flow. Make sure you are authenticated.');
|
|
536
538
|
}
|
|
537
539
|
break;
|
|
538
540
|
}
|
|
539
541
|
|
|
540
542
|
case 'save': {
|
|
541
|
-
requireArgs(args.slice(1), 2, 'lux
|
|
542
|
-
const
|
|
543
|
+
requireArgs(args.slice(1), 2, 'lux flows save <id> <config-file>');
|
|
544
|
+
const flowId = args[1];
|
|
543
545
|
const configFile = args[2];
|
|
544
546
|
|
|
545
547
|
// Load existing local flow
|
|
546
|
-
const existingFlow = loadLocalFlow(
|
|
548
|
+
const existingFlow = loadLocalFlow(flowId);
|
|
547
549
|
if (!existingFlow) {
|
|
548
|
-
error(`
|
|
549
|
-
console.log(chalk.gray('Run "lux
|
|
550
|
+
error(`Flow not found locally: ${flowId}`);
|
|
551
|
+
console.log(chalk.gray('Run "lux flows sync" to sync from cloud first.'));
|
|
550
552
|
break;
|
|
551
553
|
}
|
|
552
554
|
|
|
553
555
|
info(`Reading config from: ${configFile}`);
|
|
554
556
|
const configJson = readFile(configFile);
|
|
555
|
-
const config = parseJson(configJson, '
|
|
557
|
+
const config = parseJson(configJson, 'flow config');
|
|
556
558
|
|
|
557
559
|
// Update local flow with new config
|
|
558
560
|
const updatedFlow = {
|
|
@@ -563,9 +565,9 @@ ${chalk.bold('Examples:')}
|
|
|
563
565
|
updatedAt: new Date().toISOString(),
|
|
564
566
|
};
|
|
565
567
|
|
|
566
|
-
info(`Saving to local storage: ${
|
|
567
|
-
if (saveLocalFlow(
|
|
568
|
-
success('
|
|
568
|
+
info(`Saving to local storage: ${flowId}`);
|
|
569
|
+
if (saveLocalFlow(flowId, updatedFlow)) {
|
|
570
|
+
success('Flow saved locally!');
|
|
569
571
|
console.log(` Local Version: ${updatedFlow.localVersion}`);
|
|
570
572
|
|
|
571
573
|
// Show sync status
|
|
@@ -576,22 +578,22 @@ ${chalk.bold('Examples:')}
|
|
|
576
578
|
console.log(` Status: ${chalk.yellow('dirty')} (has unpublished changes)`);
|
|
577
579
|
}
|
|
578
580
|
|
|
579
|
-
console.log(chalk.gray(`\nRun "lux
|
|
581
|
+
console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish to cloud.`));
|
|
580
582
|
} else {
|
|
581
|
-
error('Failed to save
|
|
583
|
+
error('Failed to save flow locally.');
|
|
582
584
|
}
|
|
583
585
|
break;
|
|
584
586
|
}
|
|
585
587
|
|
|
586
588
|
case 'publish': {
|
|
587
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
588
|
-
const
|
|
589
|
+
requireArgs(args.slice(1), 1, 'lux flows publish <id>');
|
|
590
|
+
const flowId = args[1];
|
|
589
591
|
|
|
590
592
|
// Load local flow
|
|
591
|
-
const localFlow = loadLocalFlow(
|
|
593
|
+
const localFlow = loadLocalFlow(flowId);
|
|
592
594
|
if (!localFlow) {
|
|
593
|
-
error(`
|
|
594
|
-
console.log(chalk.gray('Run "lux
|
|
595
|
+
error(`Flow not found locally: ${flowId}`);
|
|
596
|
+
console.log(chalk.gray('Run "lux flows sync" to sync from cloud first.'));
|
|
595
597
|
break;
|
|
596
598
|
}
|
|
597
599
|
|
|
@@ -599,12 +601,12 @@ ${chalk.bold('Examples:')}
|
|
|
599
601
|
if (localFlow.cloudVersion && localFlow.publishedVersion &&
|
|
600
602
|
localFlow.cloudVersion > localFlow.publishedVersion &&
|
|
601
603
|
localFlow.localVersion > localFlow.publishedVersion) {
|
|
602
|
-
error('Cannot publish:
|
|
604
|
+
error('Cannot publish: flow has conflicts with cloud version.');
|
|
603
605
|
console.log(chalk.gray('Resolve conflicts in Lux Studio first.'));
|
|
604
606
|
break;
|
|
605
607
|
}
|
|
606
608
|
|
|
607
|
-
info(`Publishing
|
|
609
|
+
info(`Publishing flow: ${flowId}`);
|
|
608
610
|
|
|
609
611
|
// Publish directly to cloud with config in request body
|
|
610
612
|
const publishConfig = {
|
|
@@ -615,7 +617,7 @@ ${chalk.bold('Examples:')}
|
|
|
615
617
|
};
|
|
616
618
|
|
|
617
619
|
const { data } = await axios.post(
|
|
618
|
-
`${apiUrl}/api/
|
|
620
|
+
`${apiUrl}/api/flows/${flowId}/publish`,
|
|
619
621
|
{
|
|
620
622
|
name: localFlow.name,
|
|
621
623
|
description: localFlow.description,
|
|
@@ -642,10 +644,12 @@ ${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
|
-
saveLocalFlow(
|
|
650
|
+
saveLocalFlow(flowId, updatedFlow);
|
|
647
651
|
|
|
648
|
-
success('
|
|
652
|
+
success('Flow published!');
|
|
649
653
|
console.log(` Name: ${localFlow.name}`);
|
|
650
654
|
console.log(` Version: ${newVersion}`);
|
|
651
655
|
console.log(` Status: ${chalk.green('synced')}`);
|
|
@@ -653,17 +657,17 @@ ${chalk.bold('Examples:')}
|
|
|
653
657
|
}
|
|
654
658
|
|
|
655
659
|
case 'diff': {
|
|
656
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
657
|
-
const
|
|
660
|
+
requireArgs(args.slice(1), 1, 'lux flows diff <id>');
|
|
661
|
+
const flowId = args[1];
|
|
658
662
|
|
|
659
663
|
// Load local flow
|
|
660
|
-
const localFlow = loadLocalFlow(
|
|
664
|
+
const localFlow = loadLocalFlow(flowId);
|
|
661
665
|
if (!localFlow) {
|
|
662
|
-
error(`
|
|
666
|
+
error(`Flow not found locally: ${flowId}`);
|
|
663
667
|
break;
|
|
664
668
|
}
|
|
665
669
|
|
|
666
|
-
info(`Comparing local vs published for: ${
|
|
670
|
+
info(`Comparing local vs published for: ${flowId}`);
|
|
667
671
|
|
|
668
672
|
const localConfig = {
|
|
669
673
|
nodes: localFlow.nodes || [],
|
|
@@ -672,12 +676,12 @@ ${chalk.bold('Examples:')}
|
|
|
672
676
|
|
|
673
677
|
// Check if never published
|
|
674
678
|
if (!localFlow.publishedVersion) {
|
|
675
|
-
console.log('\n📝 NEW
|
|
679
|
+
console.log('\n📝 NEW FLOW (not yet published)\n');
|
|
676
680
|
console.log(` Name: ${localFlow.name}`);
|
|
677
681
|
console.log(` Local Version: ${localFlow.localVersion || 1}`);
|
|
678
682
|
console.log(` Nodes: ${localConfig.nodes.length}`);
|
|
679
683
|
console.log(` Edges: ${localConfig.edges.length}`);
|
|
680
|
-
console.log(chalk.gray(`\nRun "lux
|
|
684
|
+
console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish.\n`));
|
|
681
685
|
break;
|
|
682
686
|
}
|
|
683
687
|
|
|
@@ -709,17 +713,17 @@ ${chalk.bold('Examples:')}
|
|
|
709
713
|
console.log(` Published Version: ${localFlow.publishedVersion}`);
|
|
710
714
|
console.log(` Nodes: ${localConfig.nodes.length}`);
|
|
711
715
|
console.log(` Edges: ${localConfig.edges.length}`);
|
|
712
|
-
console.log(chalk.gray(`\nRun "lux
|
|
716
|
+
console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish changes.\n`));
|
|
713
717
|
}
|
|
714
718
|
break;
|
|
715
719
|
}
|
|
716
720
|
|
|
717
721
|
case 'webhook-url': {
|
|
718
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
719
|
-
const
|
|
722
|
+
requireArgs(args.slice(1), 1, 'lux flows webhook-url <flow-id>');
|
|
723
|
+
const flowId = args[1];
|
|
720
724
|
|
|
721
|
-
info(`Getting webhook URL for: ${
|
|
722
|
-
const tokenData = await getWebhookToken(
|
|
725
|
+
info(`Getting webhook URL for: ${flowId}`);
|
|
726
|
+
const tokenData = await getWebhookToken(flowId);
|
|
723
727
|
|
|
724
728
|
console.log(`\n📍 Webhook URL:\n${tokenData.webhookUrl}\n`);
|
|
725
729
|
console.log(`📊 Status:`);
|
|
@@ -727,17 +731,17 @@ ${chalk.bold('Examples:')}
|
|
|
727
731
|
console.log(` Webhook Trigger Enabled: ${tokenData.webhookTrigger ? '✅ Yes' : '❌ No'}\n`);
|
|
728
732
|
|
|
729
733
|
if (!tokenData.formatConfirmed) {
|
|
730
|
-
console.log(chalk.yellow('ℹ️ Run "lux
|
|
734
|
+
console.log(chalk.yellow('ℹ️ Run "lux flows webhook-listen" to capture a sample webhook'));
|
|
731
735
|
}
|
|
732
736
|
break;
|
|
733
737
|
}
|
|
734
738
|
|
|
735
739
|
case 'webhook-listen': {
|
|
736
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
737
|
-
const
|
|
740
|
+
requireArgs(args.slice(1), 1, 'lux flows webhook-listen <flow-id>');
|
|
741
|
+
const flowId = args[1];
|
|
738
742
|
|
|
739
|
-
info(`Starting webhook listening session for: ${
|
|
740
|
-
const tokenData = await getWebhookToken(
|
|
743
|
+
info(`Starting webhook listening session for: ${flowId}`);
|
|
744
|
+
const tokenData = await getWebhookToken(flowId);
|
|
741
745
|
|
|
742
746
|
// Start listening session
|
|
743
747
|
await axios.post(`${WEBHOOK_WORKER_URL}/listening/start/${tokenData.token}`);
|
|
@@ -746,15 +750,15 @@ ${chalk.bold('Examples:')}
|
|
|
746
750
|
console.log(`\n📡 Now listening for webhooks at:\n${tokenData.webhookUrl}\n`);
|
|
747
751
|
console.log('Session expires in 5 minutes.');
|
|
748
752
|
console.log(`\nSend a webhook request to the URL above, then run:`);
|
|
749
|
-
console.log(chalk.white(` lux
|
|
753
|
+
console.log(chalk.white(` lux flows webhook-poll ${flowId}\n`));
|
|
750
754
|
break;
|
|
751
755
|
}
|
|
752
756
|
|
|
753
757
|
case 'webhook-poll': {
|
|
754
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
755
|
-
const
|
|
758
|
+
requireArgs(args.slice(1), 1, 'lux flows webhook-poll <flow-id>');
|
|
759
|
+
const flowId = args[1];
|
|
756
760
|
|
|
757
|
-
const tokenData = await getWebhookToken(
|
|
761
|
+
const tokenData = await getWebhookToken(flowId);
|
|
758
762
|
const pollData = await getCapturedPayload(tokenData.token);
|
|
759
763
|
|
|
760
764
|
// Return raw JSON response
|
|
@@ -763,20 +767,20 @@ ${chalk.bold('Examples:')}
|
|
|
763
767
|
}
|
|
764
768
|
|
|
765
769
|
case 'webhook-accept': {
|
|
766
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
767
|
-
const
|
|
770
|
+
requireArgs(args.slice(1), 1, 'lux flows webhook-accept <flow-id>');
|
|
771
|
+
const flowId = args[1];
|
|
768
772
|
|
|
769
|
-
info(`Accepting webhook format for: ${
|
|
773
|
+
info(`Accepting webhook format for: ${flowId}`);
|
|
770
774
|
|
|
771
775
|
// Step 1: Get webhook token
|
|
772
|
-
const tokenData = await getWebhookToken(
|
|
776
|
+
const tokenData = await getWebhookToken(flowId);
|
|
773
777
|
|
|
774
778
|
// Step 2: Poll for captured payload
|
|
775
779
|
const pollData = await getCapturedPayload(tokenData.token);
|
|
776
780
|
|
|
777
781
|
// Check if payload was captured
|
|
778
782
|
if (pollData.waiting || !pollData.payload) {
|
|
779
|
-
error('No webhook captured. Run "lux
|
|
783
|
+
error('No webhook captured. Run "lux flows webhook-listen" first and send a test webhook.');
|
|
780
784
|
break;
|
|
781
785
|
}
|
|
782
786
|
|
|
@@ -851,11 +855,11 @@ ${chalk.bold('Examples:')}
|
|
|
851
855
|
}
|
|
852
856
|
|
|
853
857
|
case 'webhook-decline': {
|
|
854
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
855
|
-
const
|
|
858
|
+
requireArgs(args.slice(1), 1, 'lux flows webhook-decline <flow-id>');
|
|
859
|
+
const flowId = args[1];
|
|
856
860
|
|
|
857
|
-
info(`Declining webhook format for: ${
|
|
858
|
-
const tokenData = await getWebhookToken(
|
|
861
|
+
info(`Declining webhook format for: ${flowId}`);
|
|
862
|
+
const tokenData = await getWebhookToken(flowId);
|
|
859
863
|
|
|
860
864
|
// Clear webhook schema from database
|
|
861
865
|
info('Clearing webhook schema...');
|
|
@@ -870,14 +874,14 @@ ${chalk.bold('Examples:')}
|
|
|
870
874
|
success('Webhook format declined!');
|
|
871
875
|
console.log('\n✅ Schema cleared from database');
|
|
872
876
|
console.log('✅ Listening session has been reset');
|
|
873
|
-
console.log(`\nRun "lux
|
|
877
|
+
console.log(`\nRun "lux flows webhook-listen ${flowId}" to try again.\n`);
|
|
874
878
|
break;
|
|
875
879
|
}
|
|
876
880
|
|
|
877
881
|
// ============ EXECUTION HISTORY COMMANDS ============
|
|
878
882
|
|
|
879
883
|
case 'executions': {
|
|
880
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
884
|
+
requireArgs(args.slice(1), 1, 'lux flows executions <flow-id> [--limit N]');
|
|
881
885
|
const flowId = args[1];
|
|
882
886
|
|
|
883
887
|
// Parse --limit flag
|
|
@@ -912,7 +916,7 @@ ${chalk.bold('Examples:')}
|
|
|
912
916
|
|
|
913
917
|
if (executions.length === 0) {
|
|
914
918
|
console.log('\n(No executions found for this flow)');
|
|
915
|
-
console.log(chalk.gray('Run the flow from the interface or use "lux
|
|
919
|
+
console.log(chalk.gray('Run the flow from the interface or use "lux flows test" to create an execution.\n'));
|
|
916
920
|
break;
|
|
917
921
|
}
|
|
918
922
|
|
|
@@ -954,12 +958,12 @@ ${chalk.bold('Examples:')}
|
|
|
954
958
|
console.log(chalk.dim(`Use --limit N to see more.\n`));
|
|
955
959
|
}
|
|
956
960
|
|
|
957
|
-
console.log(chalk.gray(`\nTo see full details: lux
|
|
961
|
+
console.log(chalk.gray(`\nTo see full details: lux flows execution ${flowId} <execution-id>\n`));
|
|
958
962
|
break;
|
|
959
963
|
}
|
|
960
964
|
|
|
961
965
|
case 'execution': {
|
|
962
|
-
requireArgs(args.slice(1), 2, 'lux
|
|
966
|
+
requireArgs(args.slice(1), 2, 'lux flows execution <flow-id> <execution-id>');
|
|
963
967
|
const flowId = args[1];
|
|
964
968
|
const executionId = args[2];
|
|
965
969
|
const jsonOutput = args.includes('--json');
|
|
@@ -1056,14 +1060,14 @@ ${chalk.bold('Examples:')}
|
|
|
1056
1060
|
|
|
1057
1061
|
formatTable(nodeFormatted);
|
|
1058
1062
|
|
|
1059
|
-
console.log(chalk.gray(`\nTo see node details: lux
|
|
1063
|
+
console.log(chalk.gray(`\nTo see node details: lux flows node ${flowId} ${executionId} <node-id>\n`));
|
|
1060
1064
|
}
|
|
1061
1065
|
|
|
1062
1066
|
break;
|
|
1063
1067
|
}
|
|
1064
1068
|
|
|
1065
1069
|
case 'node': {
|
|
1066
|
-
requireArgs(args.slice(1), 3, 'lux
|
|
1070
|
+
requireArgs(args.slice(1), 3, 'lux flows node <flow-id> <execution-id> <node-id>');
|
|
1067
1071
|
const flowId = args[1];
|
|
1068
1072
|
const executionId = args[2];
|
|
1069
1073
|
const nodeId = args[3];
|
|
@@ -1163,7 +1167,7 @@ ${chalk.bold('Examples:')}
|
|
|
1163
1167
|
}
|
|
1164
1168
|
|
|
1165
1169
|
case 'last': {
|
|
1166
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
1170
|
+
requireArgs(args.slice(1), 1, 'lux flows last <flow-id>');
|
|
1167
1171
|
const flowId = args[1];
|
|
1168
1172
|
const jsonOutput = args.includes('--json');
|
|
1169
1173
|
|
|
@@ -1188,7 +1192,7 @@ ${chalk.bold('Examples:')}
|
|
|
1188
1192
|
const executions = listData.executions || [];
|
|
1189
1193
|
if (executions.length === 0) {
|
|
1190
1194
|
console.log('\n(No executions found for this flow)');
|
|
1191
|
-
console.log(chalk.gray('Run the flow from the interface or use "lux
|
|
1195
|
+
console.log(chalk.gray('Run the flow from the interface or use "lux flows test" to create an execution.\n'));
|
|
1192
1196
|
break;
|
|
1193
1197
|
}
|
|
1194
1198
|
|
|
@@ -1259,7 +1263,7 @@ ${chalk.bold('Examples:')}
|
|
|
1259
1263
|
}
|
|
1260
1264
|
|
|
1261
1265
|
case 'test': {
|
|
1262
|
-
requireArgs(args.slice(1), 1, 'lux
|
|
1266
|
+
requireArgs(args.slice(1), 1, 'lux flows test <flow-id> [--data <json>] [--data-file <path>]');
|
|
1263
1267
|
const flowId = args[1];
|
|
1264
1268
|
const jsonOutput = args.includes('--json');
|
|
1265
1269
|
|
|
@@ -1285,7 +1289,7 @@ ${chalk.bold('Examples:')}
|
|
|
1285
1289
|
const flowData = loadLocalFlow(flowId);
|
|
1286
1290
|
if (!flowData) {
|
|
1287
1291
|
error(`Flow not found locally: ${flowId}`);
|
|
1288
|
-
console.log(chalk.gray('Run "lux
|
|
1292
|
+
console.log(chalk.gray('Run "lux flows sync" to sync from cloud first.'));
|
|
1289
1293
|
break;
|
|
1290
1294
|
}
|
|
1291
1295
|
|
|
@@ -1363,7 +1367,7 @@ ${chalk.bold('Examples:')}
|
|
|
1363
1367
|
|
|
1364
1368
|
default:
|
|
1365
1369
|
error(
|
|
1366
|
-
`Unknown command: ${command}\n\nRun 'lux
|
|
1370
|
+
`Unknown command: ${command}\n\nRun 'lux flows' to see available commands`
|
|
1367
1371
|
);
|
|
1368
1372
|
}
|
|
1369
1373
|
} catch (err) {
|
|
@@ -1373,4 +1377,4 @@ ${chalk.bold('Examples:')}
|
|
|
1373
1377
|
}
|
|
1374
1378
|
}
|
|
1375
1379
|
|
|
1376
|
-
module.exports = {
|
|
1380
|
+
module.exports = { handleFlows };
|
package/commands/knowledge.js
CHANGED
|
@@ -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 <
|
|
40
|
-
url <
|
|
41
|
-
upload <file
|
|
42
|
-
delete <
|
|
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 <
|
|
47
|
-
folders delete <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
|
|
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
|
|
54
|
-
lux knowledge folders create
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
76
|
-
const
|
|
86
|
+
const tree = data.tree || [];
|
|
87
|
+
const totalFiles = data.totalFiles || 0;
|
|
77
88
|
|
|
78
|
-
if (
|
|
89
|
+
if (tree.length === 0) {
|
|
79
90
|
console.log('\n(No knowledge items found)\n');
|
|
80
91
|
return;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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 <
|
|
107
|
-
const
|
|
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: ${
|
|
120
|
+
info(`Loading document: ${filePath}`);
|
|
110
121
|
const { data } = await axios.get(
|
|
111
|
-
`${
|
|
122
|
+
`${studioApiUrl}/api/projects/${projectId}/knowledge/${filePath}`,
|
|
112
123
|
{ headers: getAuthHeaders() }
|
|
113
124
|
);
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
148
|
+
// Show content preview for text files
|
|
149
|
+
if (data.encoding === 'utf-8' && data.content) {
|
|
133
150
|
console.log(`\n${chalk.bold('Content:')}`);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 <
|
|
144
|
-
const
|
|
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: ${
|
|
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
|
-
`${
|
|
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(
|
|
154
|
-
console.log(`\
|
|
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
|
|
160
|
-
const
|
|
161
|
-
const
|
|
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(
|
|
165
|
-
error(`File not found: ${
|
|
195
|
+
if (!fs.existsSync(localFilePath)) {
|
|
196
|
+
error(`File not found: ${localFilePath}`);
|
|
166
197
|
return;
|
|
167
198
|
}
|
|
168
199
|
|
|
169
|
-
const fileName = path.basename(
|
|
170
|
-
const fileBuffer = fs.readFileSync(
|
|
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
|
|
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
|
-
`${
|
|
197
|
-
{ fileName,
|
|
227
|
+
`${studioApiUrl}/api/projects/${projectId}/knowledge/upload-url`,
|
|
228
|
+
{ path: fileName, contentType, folder },
|
|
198
229
|
{ headers: getAuthHeaders() }
|
|
199
230
|
);
|
|
200
231
|
|
|
201
|
-
|
|
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':
|
|
241
|
+
'Content-Type': contentType,
|
|
206
242
|
},
|
|
207
243
|
maxBodyLength: Infinity,
|
|
208
244
|
maxContentLength: Infinity,
|
|
209
245
|
});
|
|
210
246
|
|
|
211
|
-
// Step 3:
|
|
212
|
-
info('
|
|
213
|
-
const { data:
|
|
214
|
-
`${
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 <
|
|
236
|
-
const
|
|
271
|
+
requireArgs(args.slice(1), 1, 'lux knowledge delete <file-path>');
|
|
272
|
+
const filePath = args[1];
|
|
237
273
|
|
|
238
|
-
info(`Deleting
|
|
239
|
-
await axios.delete(
|
|
240
|
-
`${
|
|
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(
|
|
254
|
-
|
|
255
|
-
|
|
294
|
+
const { data } = await axios.get(
|
|
295
|
+
`${studioApiUrl}/api/projects/${projectId}/knowledge`,
|
|
296
|
+
{ headers: getAuthHeaders() }
|
|
297
|
+
);
|
|
256
298
|
|
|
257
|
-
if (!data.
|
|
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 ${
|
|
261
|
-
formatTable(
|
|
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 <
|
|
273
|
-
const
|
|
274
|
-
|
|
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
|
-
|
|
336
|
+
// In R2, folders are virtual - they're created by the folder endpoint
|
|
290
337
|
const { data } = await axios.post(
|
|
291
|
-
`${
|
|
292
|
-
{
|
|
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(`
|
|
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-
|
|
305
|
-
const
|
|
354
|
+
requireArgs(args.slice(2), 1, 'lux knowledge folders delete <folder-path>');
|
|
355
|
+
const folderPath = args[2];
|
|
306
356
|
|
|
307
|
-
info(`Deleting folder: ${
|
|
308
|
-
await axios.delete(
|
|
309
|
-
`${
|
|
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
|
-
|
|
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/
|
|
180
|
+
const response = await axios.get(`${apiUrl}/api/flows`, {
|
|
181
181
|
headers: {
|
|
182
182
|
Authorization: `Bearer ${apiKey}`,
|
|
183
183
|
'X-Org-Id': extractOrgIdFromKey(apiKey),
|
package/commands/project.js
CHANGED
|
@@ -231,7 +231,8 @@ async function deployProject(projectId) {
|
|
|
231
231
|
createGitignore(projectDir);
|
|
232
232
|
|
|
233
233
|
// Always configure git user (required for commits)
|
|
234
|
-
|
|
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/
|
|
500
|
+
`${apiUrl}/api/flows/${flowId}/publish`,
|
|
500
501
|
{
|
|
501
502
|
// Wrap flow data in config object as expected by /publish
|
|
502
503
|
config: {
|
package/lux.js
CHANGED
|
@@ -11,7 +11,7 @@ const { dev } = require('./commands/dev');
|
|
|
11
11
|
const { handleInterface } = require('./commands/interface');
|
|
12
12
|
const { handleData } = require('./commands/data');
|
|
13
13
|
const { handleStorage } = require('./commands/storage');
|
|
14
|
-
const {
|
|
14
|
+
const { handleFlows } = require('./commands/flows');
|
|
15
15
|
const { handleSecrets } = require('./commands/secrets');
|
|
16
16
|
const { handleAgents } = require('./commands/agents');
|
|
17
17
|
const { handleKnowledge } = require('./commands/knowledge');
|
|
@@ -96,14 +96,14 @@ program
|
|
|
96
96
|
handleStorage(subcommand ? [subcommand, ...(args || [])] : []);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
//
|
|
99
|
+
// Flows commands (with short alias 'f')
|
|
100
100
|
program
|
|
101
|
-
.command('
|
|
102
|
-
.
|
|
103
|
-
.description('
|
|
101
|
+
.command('flows [subcommand] [args...]')
|
|
102
|
+
.alias('f')
|
|
103
|
+
.description('Flow management (list, get, create/init, save, publish, diff)')
|
|
104
104
|
.allowUnknownOption()
|
|
105
105
|
.action((subcommand, args) => {
|
|
106
|
-
|
|
106
|
+
handleFlows(subcommand ? [subcommand, ...(args || [])] : []);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
// Secrets commands
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luxlabs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"deploy",
|
|
25
25
|
"interfaces",
|
|
26
26
|
"ai",
|
|
27
|
-
"
|
|
27
|
+
"flows",
|
|
28
28
|
"agents"
|
|
29
29
|
],
|
|
30
30
|
"repository": {
|