luxlabs 1.0.4 → 1.0.6
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/interface/boilerplate.js +3 -16
- package/commands/interface/init.js +12 -8
- package/commands/tools.js +299 -0
- package/commands/workflows.js +40 -10
- package/lux.js +10 -0
- package/package.json +1 -1
- package/templates/interface-boilerplate/app/layout.tsx +8 -1
- package/templates/interface-boilerplate/components/providers/posthog-provider.tsx +68 -0
- package/templates/interface-boilerplate/package.json +1 -0
|
@@ -3,24 +3,11 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Get the templates directory path
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - npm package: luxlabs/commands/interface/ → 2 levels up
|
|
6
|
+
* Templates are stored in lux-cli/templates/interface-boilerplate
|
|
7
|
+
* This is the single source of truth - used by both CLI and Electron app
|
|
9
8
|
*/
|
|
10
9
|
function getTemplatesDir() {
|
|
11
|
-
|
|
12
|
-
path.join(__dirname, '../../../../templates/interface-boilerplate'), // Electron app
|
|
13
|
-
path.join(__dirname, '../../templates/interface-boilerplate'), // npm package
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
for (const candidate of candidates) {
|
|
17
|
-
if (fs.existsSync(candidate)) {
|
|
18
|
-
return candidate;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Return first candidate for error message
|
|
23
|
-
return candidates[0];
|
|
10
|
+
return path.join(__dirname, '../../templates/interface-boilerplate');
|
|
24
11
|
}
|
|
25
12
|
|
|
26
13
|
/**
|
|
@@ -157,14 +157,6 @@ async function initInterface(options) {
|
|
|
157
157
|
console.log(chalk.yellow(' ⚠ Vercel project not created (may need VERCEL_TOKEN)'));
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Output parseable format for API wrapper
|
|
161
|
-
console.log(`App created: ${interfaceId}`);
|
|
162
|
-
if (githubRepoUrl) {
|
|
163
|
-
console.log(`GitHub repo: ${githubRepoUrl}`);
|
|
164
|
-
}
|
|
165
|
-
if (vercelProjectId) {
|
|
166
|
-
console.log(`Vercel project: ${vercelProjectId}`);
|
|
167
|
-
}
|
|
168
160
|
console.log(chalk.dim('─'.repeat(50)));
|
|
169
161
|
|
|
170
162
|
// Set up local files and push to GitHub
|
|
@@ -315,6 +307,15 @@ async function initInterface(options) {
|
|
|
315
307
|
}
|
|
316
308
|
console.log(chalk.dim('─'.repeat(50)));
|
|
317
309
|
|
|
310
|
+
// Output parseable format for IPC handler AFTER all steps complete successfully
|
|
311
|
+
console.log(`App created: ${interfaceId}`);
|
|
312
|
+
if (githubRepoUrl) {
|
|
313
|
+
console.log(`GitHub repo: ${githubRepoUrl}`);
|
|
314
|
+
}
|
|
315
|
+
if (vercelProjectId) {
|
|
316
|
+
console.log(`Vercel project: ${vercelProjectId}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
318
319
|
console.log(chalk.green('\n✅ Interface created successfully!\n'));
|
|
319
320
|
console.log(chalk.dim(' Created:'));
|
|
320
321
|
console.log(chalk.dim(' • Registered in system.interfaces'));
|
|
@@ -332,6 +333,9 @@ async function initInterface(options) {
|
|
|
332
333
|
console.log(chalk.dim(' • Edit your code'));
|
|
333
334
|
console.log(chalk.dim(' • Run'), chalk.white('lux i deploy'), chalk.dim('to push to GitHub (auto-deploys to Vercel)\n'));
|
|
334
335
|
} else {
|
|
336
|
+
// No GitHub URL - output parseable format anyway (cloud-only interface)
|
|
337
|
+
console.log(`App created: ${interfaceId}`);
|
|
338
|
+
|
|
335
339
|
console.log(chalk.yellow('\n⚠ No GitHub URL returned - skipping local setup'));
|
|
336
340
|
console.log(chalk.dim('\n Created:'));
|
|
337
341
|
console.log(chalk.dim(' • Registered in system.interfaces'));
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* List and manage AI tools (both default and custom) that agents can use.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const {
|
|
10
|
+
getStudioApiUrl,
|
|
11
|
+
getProjectId,
|
|
12
|
+
isAuthenticated,
|
|
13
|
+
loadConfig,
|
|
14
|
+
} = require('../lib/config');
|
|
15
|
+
const {
|
|
16
|
+
error,
|
|
17
|
+
success,
|
|
18
|
+
info,
|
|
19
|
+
formatTable,
|
|
20
|
+
} = require('../lib/helpers');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get auth headers for Studio API
|
|
24
|
+
*/
|
|
25
|
+
function getStudioAuthHeaders() {
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
if (!config || !config.apiKey || !config.orgId) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
32
|
+
'X-Org-Id': config.orgId,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get tools API URL
|
|
39
|
+
*/
|
|
40
|
+
function getToolsApiUrl() {
|
|
41
|
+
const studioApiUrl = getStudioApiUrl();
|
|
42
|
+
const projectId = getProjectId();
|
|
43
|
+
if (!projectId) {
|
|
44
|
+
throw new Error('No project ID found. Run this command from a Lux Studio project directory.');
|
|
45
|
+
}
|
|
46
|
+
return `${studioApiUrl}/api/projects/${projectId}/tools`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function handleTools(args) {
|
|
50
|
+
// Check authentication
|
|
51
|
+
if (!isAuthenticated()) {
|
|
52
|
+
console.log(
|
|
53
|
+
chalk.red('❌ Not authenticated. Run'),
|
|
54
|
+
chalk.white('lux login'),
|
|
55
|
+
chalk.red('first.')
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const command = args[0];
|
|
61
|
+
|
|
62
|
+
if (!command) {
|
|
63
|
+
console.log(`
|
|
64
|
+
${chalk.bold('Usage:')} lux tools <command> [args]
|
|
65
|
+
|
|
66
|
+
${chalk.bold('Commands:')}
|
|
67
|
+
list List all available tools (default + custom)
|
|
68
|
+
list-default List only default (built-in) tools
|
|
69
|
+
list-custom List only custom tools
|
|
70
|
+
get <tool-id> Get details of a specific tool
|
|
71
|
+
|
|
72
|
+
${chalk.bold('Options:')}
|
|
73
|
+
--json Output in JSON format
|
|
74
|
+
|
|
75
|
+
${chalk.bold('Examples:')}
|
|
76
|
+
lux tools list
|
|
77
|
+
lux tools list-default
|
|
78
|
+
lux tools list-custom
|
|
79
|
+
lux tools get tool_abc123
|
|
80
|
+
lux tools list --json
|
|
81
|
+
`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isJsonOutput = args.includes('--json');
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
switch (command) {
|
|
89
|
+
case 'list': {
|
|
90
|
+
info('Loading tools...');
|
|
91
|
+
const toolsApiUrl = getToolsApiUrl();
|
|
92
|
+
const { data } = await axios.get(toolsApiUrl, {
|
|
93
|
+
headers: getStudioAuthHeaders(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const tools = data.tools || [];
|
|
97
|
+
|
|
98
|
+
if (isJsonOutput) {
|
|
99
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (tools.length === 0) {
|
|
104
|
+
console.log('\n(No tools found)\n');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const defaultTools = tools.filter(t => t.isDefault);
|
|
109
|
+
const customTools = tools.filter(t => !t.isDefault);
|
|
110
|
+
|
|
111
|
+
// Display default tools
|
|
112
|
+
if (defaultTools.length > 0) {
|
|
113
|
+
console.log(`\n${chalk.bold.cyan('Default Tools')} (${defaultTools.length}):\n`);
|
|
114
|
+
formatTable(defaultTools.map(t => ({
|
|
115
|
+
name: t.name,
|
|
116
|
+
method: t.httpMethod,
|
|
117
|
+
category: t.category || '-',
|
|
118
|
+
description: t.description?.substring(0, 60) + (t.description?.length > 60 ? '...' : '') || '-',
|
|
119
|
+
})));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Display custom tools
|
|
123
|
+
if (customTools.length > 0) {
|
|
124
|
+
console.log(`\n${chalk.bold.green('Custom Tools')} (${customTools.length}):\n`);
|
|
125
|
+
formatTable(customTools.map(t => ({
|
|
126
|
+
id: t.id,
|
|
127
|
+
name: t.name,
|
|
128
|
+
method: t.httpMethod,
|
|
129
|
+
endpoint: t.apiEndpoint?.substring(0, 40) + (t.apiEndpoint?.length > 40 ? '...' : '') || '-',
|
|
130
|
+
})));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`\n${chalk.green('✓')} Total: ${tools.length} tool(s) (${defaultTools.length} default, ${customTools.length} custom)\n`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'list-default': {
|
|
138
|
+
info('Loading default tools...');
|
|
139
|
+
const toolsApiUrl = getToolsApiUrl();
|
|
140
|
+
const { data } = await axios.get(toolsApiUrl, {
|
|
141
|
+
headers: getStudioAuthHeaders(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const defaultTools = (data.tools || []).filter(t => t.isDefault);
|
|
145
|
+
|
|
146
|
+
if (isJsonOutput) {
|
|
147
|
+
console.log(JSON.stringify(defaultTools, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (defaultTools.length === 0) {
|
|
152
|
+
console.log('\n(No default tools found)\n');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`\n${chalk.bold('Default Tools')} (${defaultTools.length}):\n`);
|
|
157
|
+
|
|
158
|
+
for (const tool of defaultTools) {
|
|
159
|
+
console.log(chalk.cyan(` ${tool.name}`));
|
|
160
|
+
console.log(chalk.gray(` ${tool.description || 'No description'}`));
|
|
161
|
+
console.log(chalk.gray(` Method: ${tool.httpMethod} | Category: ${tool.category || 'General'}`));
|
|
162
|
+
|
|
163
|
+
// Show input parameters if available
|
|
164
|
+
if (tool.inputSchema?.properties) {
|
|
165
|
+
const params = Object.entries(tool.inputSchema.properties)
|
|
166
|
+
.filter(([key]) => !key.startsWith('orgId') && !key.startsWith('projectId'))
|
|
167
|
+
.map(([key, schema]) => {
|
|
168
|
+
const required = tool.inputSchema.required?.includes(key) ? '*' : '';
|
|
169
|
+
return `${key}${required}`;
|
|
170
|
+
});
|
|
171
|
+
if (params.length > 0) {
|
|
172
|
+
console.log(chalk.gray(` Parameters: ${params.join(', ')}`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
console.log('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`${chalk.green('✓')} ${defaultTools.length} default tool(s)\n`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'list-custom': {
|
|
183
|
+
info('Loading custom tools...');
|
|
184
|
+
const toolsApiUrl = getToolsApiUrl();
|
|
185
|
+
const { data } = await axios.get(toolsApiUrl, {
|
|
186
|
+
headers: getStudioAuthHeaders(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const customTools = (data.tools || []).filter(t => !t.isDefault);
|
|
190
|
+
|
|
191
|
+
if (isJsonOutput) {
|
|
192
|
+
console.log(JSON.stringify(customTools, null, 2));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (customTools.length === 0) {
|
|
197
|
+
console.log('\n(No custom tools found)\n');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`\n${chalk.bold('Custom Tools')} (${customTools.length}):\n`);
|
|
202
|
+
|
|
203
|
+
for (const tool of customTools) {
|
|
204
|
+
console.log(chalk.green(` ${tool.name}`));
|
|
205
|
+
console.log(chalk.gray(` ID: ${tool.id}`));
|
|
206
|
+
console.log(chalk.gray(` ${tool.description || 'No description'}`));
|
|
207
|
+
console.log(chalk.gray(` Endpoint: ${tool.httpMethod} ${tool.apiEndpoint}`));
|
|
208
|
+
if (tool.authCredentialType) {
|
|
209
|
+
console.log(chalk.gray(` Auth: ${tool.authCredentialType}`));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Show input parameters if available
|
|
213
|
+
if (tool.inputSchema?.properties) {
|
|
214
|
+
const params = Object.entries(tool.inputSchema.properties)
|
|
215
|
+
.filter(([key]) => !key.startsWith('orgId') && !key.startsWith('projectId'))
|
|
216
|
+
.map(([key, schema]) => {
|
|
217
|
+
const required = tool.inputSchema.required?.includes(key) ? '*' : '';
|
|
218
|
+
return `${key}${required}`;
|
|
219
|
+
});
|
|
220
|
+
if (params.length > 0) {
|
|
221
|
+
console.log(chalk.gray(` Parameters: ${params.join(', ')}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
console.log('');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`${chalk.green('✓')} ${customTools.length} custom tool(s)\n`);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case 'get': {
|
|
232
|
+
const toolId = args[1];
|
|
233
|
+
if (!toolId) {
|
|
234
|
+
error('Missing tool ID. Usage: lux tools get <tool-id>');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
info(`Loading tool: ${toolId}`);
|
|
239
|
+
const toolsApiUrl = getToolsApiUrl();
|
|
240
|
+
const { data } = await axios.get(`${toolsApiUrl}/${toolId}`, {
|
|
241
|
+
headers: getStudioAuthHeaders(),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const tool = data.tool;
|
|
245
|
+
|
|
246
|
+
if (isJsonOutput) {
|
|
247
|
+
console.log(JSON.stringify(tool, null, 2));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(`\n${chalk.bold('Tool Details')}\n`);
|
|
252
|
+
console.log(` Name: ${chalk.cyan(tool.name)}`);
|
|
253
|
+
console.log(` ID: ${tool.id}`);
|
|
254
|
+
console.log(` Type: ${tool.isDefault ? 'Default' : 'Custom'}`);
|
|
255
|
+
console.log(` Description: ${tool.description || '-'}`);
|
|
256
|
+
console.log(` Method: ${tool.httpMethod}`);
|
|
257
|
+
console.log(` Endpoint: ${tool.apiEndpoint}`);
|
|
258
|
+
if (tool.category) {
|
|
259
|
+
console.log(` Category: ${tool.category}`);
|
|
260
|
+
}
|
|
261
|
+
if (tool.authCredentialType) {
|
|
262
|
+
console.log(` Auth: ${tool.authCredentialType}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Show input schema
|
|
266
|
+
if (tool.inputSchema?.properties) {
|
|
267
|
+
console.log(`\n ${chalk.bold('Parameters:')}`);
|
|
268
|
+
for (const [key, schema] of Object.entries(tool.inputSchema.properties)) {
|
|
269
|
+
if (key.startsWith('orgId') || key.startsWith('projectId')) {
|
|
270
|
+
continue; // Skip system params
|
|
271
|
+
}
|
|
272
|
+
const required = tool.inputSchema.required?.includes(key);
|
|
273
|
+
console.log(` ${key} (${schema.type})${required ? chalk.red(' *required') : ''}`);
|
|
274
|
+
if (schema.description) {
|
|
275
|
+
console.log(chalk.gray(` ${schema.description}`));
|
|
276
|
+
}
|
|
277
|
+
if (schema.enum) {
|
|
278
|
+
console.log(chalk.gray(` Options: ${schema.enum.join(', ')}`));
|
|
279
|
+
}
|
|
280
|
+
if (schema.default !== undefined) {
|
|
281
|
+
console.log(chalk.gray(` Default: ${schema.default}`));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('');
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
default:
|
|
291
|
+
error(`Unknown command: ${command}\n\nRun 'lux tools' to see available commands`);
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const errorMessage = err.response?.data?.error || err.message || 'Unknown error';
|
|
295
|
+
error(errorMessage);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = { handleTools };
|
package/commands/workflows.js
CHANGED
|
@@ -249,19 +249,26 @@ ${chalk.bold('Examples:')}
|
|
|
249
249
|
const now = new Date().toISOString();
|
|
250
250
|
|
|
251
251
|
if (!localFlow) {
|
|
252
|
-
// New from cloud
|
|
252
|
+
// New from cloud - it's published, so set deploy snapshot
|
|
253
|
+
const cloudNodes = cloudFlow.config?.nodes || [];
|
|
254
|
+
const cloudEdges = cloudFlow.config?.edges || [];
|
|
253
255
|
const newFlow = {
|
|
254
256
|
id: cloudFlow.id,
|
|
255
257
|
name: cloudFlow.name,
|
|
256
258
|
description: cloudFlow.description,
|
|
257
|
-
nodes:
|
|
258
|
-
edges:
|
|
259
|
+
nodes: cloudNodes,
|
|
260
|
+
edges: cloudEdges,
|
|
259
261
|
localVersion: cloudFlow.version || 1,
|
|
260
262
|
publishedVersion: cloudFlow.version || 1,
|
|
261
263
|
cloudVersion: cloudFlow.version || 1,
|
|
262
264
|
lastSyncedAt: now,
|
|
263
265
|
createdAt: cloudFlow.updated_at || cloudFlow.created_at,
|
|
264
266
|
updatedAt: cloudFlow.updated_at,
|
|
267
|
+
// Store deploy snapshot since this is a published flow
|
|
268
|
+
deployedNodes: JSON.parse(JSON.stringify(cloudNodes)),
|
|
269
|
+
deployedEdges: JSON.parse(JSON.stringify(cloudEdges)),
|
|
270
|
+
cloudPublishedAt: cloudFlow.updated_at || now,
|
|
271
|
+
cloudStatus: 'published',
|
|
265
272
|
};
|
|
266
273
|
saveLocalFlow(cloudFlow.id, newFlow);
|
|
267
274
|
newFromCloud++;
|
|
@@ -288,18 +295,25 @@ ${chalk.bold('Examples:')}
|
|
|
288
295
|
saveLocalFlow(cloudFlow.id, updatedFlow);
|
|
289
296
|
conflicts++;
|
|
290
297
|
} else if (cloudHasNewerVersion && !hasLocalChanges) {
|
|
291
|
-
// Update from cloud
|
|
298
|
+
// Update from cloud - replace with newer published version
|
|
299
|
+
const cloudNodes = cloudFlow.config?.nodes || [];
|
|
300
|
+
const cloudEdges = cloudFlow.config?.edges || [];
|
|
292
301
|
const updatedFlow = {
|
|
293
302
|
...localFlow,
|
|
294
303
|
name: cloudFlow.name,
|
|
295
304
|
description: cloudFlow.description,
|
|
296
|
-
nodes:
|
|
297
|
-
edges:
|
|
305
|
+
nodes: cloudNodes,
|
|
306
|
+
edges: cloudEdges,
|
|
298
307
|
localVersion: cloudVersion,
|
|
299
308
|
publishedVersion: cloudVersion,
|
|
300
309
|
cloudVersion,
|
|
301
310
|
lastSyncedAt: now,
|
|
302
311
|
updatedAt: cloudFlow.updated_at,
|
|
312
|
+
// Update deploy snapshot to match new published version
|
|
313
|
+
deployedNodes: JSON.parse(JSON.stringify(cloudNodes)),
|
|
314
|
+
deployedEdges: JSON.parse(JSON.stringify(cloudEdges)),
|
|
315
|
+
cloudPublishedAt: cloudFlow.updated_at || now,
|
|
316
|
+
cloudStatus: 'published',
|
|
303
317
|
};
|
|
304
318
|
saveLocalFlow(cloudFlow.id, updatedFlow);
|
|
305
319
|
synced++;
|
|
@@ -461,15 +475,23 @@ ${chalk.bold('Examples:')}
|
|
|
461
475
|
);
|
|
462
476
|
|
|
463
477
|
const newVersion = publishResponse.data.version || 1;
|
|
478
|
+
const now = new Date().toISOString();
|
|
464
479
|
|
|
465
480
|
// Update local storage with published version and cloud ID
|
|
481
|
+
// IMPORTANT: This must match what the UI's markFlowPublished() does exactly
|
|
466
482
|
const updatedFlow = {
|
|
467
483
|
...newFlow,
|
|
468
484
|
cloudId: cloudId,
|
|
469
485
|
publishedVersion: newVersion,
|
|
470
486
|
cloudVersion: newVersion,
|
|
471
|
-
lastPublishedAt:
|
|
472
|
-
lastSyncedAt:
|
|
487
|
+
lastPublishedAt: now,
|
|
488
|
+
lastSyncedAt: now,
|
|
489
|
+
// Store snapshot of deployed config for change tracking
|
|
490
|
+
deployedNodes: JSON.parse(JSON.stringify(newFlow.nodes || [])),
|
|
491
|
+
deployedEdges: JSON.parse(JSON.stringify(newFlow.edges || [])),
|
|
492
|
+
// Cloud sync tracking - matches markFlowPublished()
|
|
493
|
+
cloudPublishedAt: now,
|
|
494
|
+
cloudStatus: 'published',
|
|
473
495
|
};
|
|
474
496
|
saveLocalFlow(flowId, updatedFlow);
|
|
475
497
|
|
|
@@ -580,15 +602,23 @@ ${chalk.bold('Examples:')}
|
|
|
580
602
|
);
|
|
581
603
|
|
|
582
604
|
const newVersion = data.version || (localFlow.publishedVersion || 0) + 1;
|
|
605
|
+
const now = new Date().toISOString();
|
|
583
606
|
|
|
584
607
|
// Update local storage with new published version
|
|
608
|
+
// IMPORTANT: This must match what the UI's markFlowPublished() does exactly
|
|
585
609
|
const updatedFlow = {
|
|
586
610
|
...localFlow,
|
|
587
611
|
publishedVersion: newVersion,
|
|
588
612
|
cloudVersion: newVersion,
|
|
589
|
-
lastPublishedAt:
|
|
590
|
-
lastSyncedAt:
|
|
613
|
+
lastPublishedAt: now,
|
|
614
|
+
lastSyncedAt: now,
|
|
591
615
|
cloudData: undefined, // Clear any stored conflict data
|
|
616
|
+
// Store snapshot of deployed config for change tracking
|
|
617
|
+
deployedNodes: JSON.parse(JSON.stringify(localFlow.nodes || [])),
|
|
618
|
+
deployedEdges: JSON.parse(JSON.stringify(localFlow.edges || [])),
|
|
619
|
+
// Cloud sync tracking - matches markFlowPublished()
|
|
620
|
+
cloudPublishedAt: now,
|
|
621
|
+
cloudStatus: 'published',
|
|
592
622
|
};
|
|
593
623
|
saveLocalFlow(workflowId, updatedFlow);
|
|
594
624
|
|
package/lux.js
CHANGED
|
@@ -22,6 +22,7 @@ const { handleProject } = require('./commands/project');
|
|
|
22
22
|
const { handleServers, handleLogs } = require('./commands/servers');
|
|
23
23
|
const { handleTest } = require('./commands/webview');
|
|
24
24
|
const { handleABTests } = require('./commands/ab-tests');
|
|
25
|
+
const { handleTools } = require('./commands/tools');
|
|
25
26
|
|
|
26
27
|
program
|
|
27
28
|
.name('lux')
|
|
@@ -152,6 +153,15 @@ program
|
|
|
152
153
|
handleABTests(subcommand ? [subcommand, ...(args || [])] : []);
|
|
153
154
|
});
|
|
154
155
|
|
|
156
|
+
// Tools commands - list AI tools (default and custom)
|
|
157
|
+
program
|
|
158
|
+
.command('tools [subcommand] [args...]')
|
|
159
|
+
.description('AI tools management (list, list-default, list-custom, get)')
|
|
160
|
+
.allowUnknownOption()
|
|
161
|
+
.action((subcommand, args) => {
|
|
162
|
+
handleTools(subcommand ? [subcommand, ...(args || [])] : []);
|
|
163
|
+
});
|
|
164
|
+
|
|
155
165
|
// Validate data-lux attributes
|
|
156
166
|
program
|
|
157
167
|
.command('validate-data-lux [interface-id]')
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import { Suspense } from "react";
|
|
4
|
+
import { PostHogProvider, PostHogPageView } from "@/components/providers/posthog-provider";
|
|
3
5
|
import "./globals.css";
|
|
4
6
|
|
|
5
7
|
const geistSans = Geist({
|
|
@@ -27,7 +29,12 @@ export default function RootLayout({
|
|
|
27
29
|
<body
|
|
28
30
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
29
31
|
>
|
|
30
|
-
|
|
32
|
+
<PostHogProvider>
|
|
33
|
+
<Suspense fallback={null}>
|
|
34
|
+
<PostHogPageView />
|
|
35
|
+
</Suspense>
|
|
36
|
+
{children}
|
|
37
|
+
</PostHogProvider>
|
|
31
38
|
</body>
|
|
32
39
|
</html>
|
|
33
40
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import posthog from 'posthog-js'
|
|
4
|
+
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
|
5
|
+
import { useEffect, useRef } from 'react'
|
|
6
|
+
import { usePathname, useSearchParams } from 'next/navigation'
|
|
7
|
+
|
|
8
|
+
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY
|
|
9
|
+
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com'
|
|
10
|
+
const LUX_INTERFACE_ID = process.env.NEXT_PUBLIC_LUX_INTERFACE_ID
|
|
11
|
+
const LUX_ORG_ID = process.env.NEXT_PUBLIC_LUX_ORG_ID
|
|
12
|
+
|
|
13
|
+
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|
14
|
+
const initialized = useRef(false)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!POSTHOG_KEY || initialized.current) {
|
|
18
|
+
if (!POSTHOG_KEY) {
|
|
19
|
+
console.log('[PostHog] No API key configured, skipping initialization')
|
|
20
|
+
}
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
posthog.init(POSTHOG_KEY, {
|
|
25
|
+
api_host: POSTHOG_HOST,
|
|
26
|
+
persistence: 'localStorage',
|
|
27
|
+
capture_pageview: false, // We capture manually for better control
|
|
28
|
+
capture_pageleave: true,
|
|
29
|
+
loaded: (ph) => {
|
|
30
|
+
// Register lux properties with all events for filtering
|
|
31
|
+
const props: Record<string, string> = {}
|
|
32
|
+
if (LUX_INTERFACE_ID) props.$lux_interface_id = LUX_INTERFACE_ID
|
|
33
|
+
if (LUX_ORG_ID) props.$lux_org_id = LUX_ORG_ID
|
|
34
|
+
if (Object.keys(props).length > 0) {
|
|
35
|
+
ph.register(props)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
initialized.current = true
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
if (!POSTHOG_KEY) {
|
|
44
|
+
return <>{children}</>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return <PHProvider client={posthog}>{children}</PHProvider>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function PostHogPageView() {
|
|
51
|
+
const pathname = usePathname()
|
|
52
|
+
const searchParams = useSearchParams()
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!POSTHOG_KEY) return
|
|
56
|
+
|
|
57
|
+
const url = window.origin + pathname
|
|
58
|
+
const search = searchParams?.toString()
|
|
59
|
+
const fullUrl = search ? `${url}?${search}` : url
|
|
60
|
+
|
|
61
|
+
posthog.capture('$pageview', {
|
|
62
|
+
$current_url: fullUrl,
|
|
63
|
+
$pathname: pathname,
|
|
64
|
+
})
|
|
65
|
+
}, [pathname, searchParams])
|
|
66
|
+
|
|
67
|
+
return null
|
|
68
|
+
}
|