luxlabs 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,24 +3,11 @@ const path = require('path');
3
3
 
4
4
  /**
5
5
  * Get the templates directory path
6
- * Tries multiple paths to support both:
7
- * - Electron app: electron/bin/lux-cli/commands/interface/ 4 levels up
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
- const candidates = [
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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "luxlabs",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -3,6 +3,9 @@
3
3
  import { useEffect } from "react";
4
4
  import { useSession, signOut } from "@/lib/auth-client";
5
5
  import { useRouter } from "next/navigation";
6
+ import { createLux } from "@/lib/lux";
7
+
8
+ const lux = createLux("Dashboard");
6
9
 
7
10
  export default function DashboardPage() {
8
11
  const { data: session, isPending } = useSession();
@@ -16,7 +19,7 @@ export default function DashboardPage() {
16
19
 
17
20
  if (isPending) {
18
21
  return (
19
- <div className="min-h-screen flex items-center justify-center">
22
+ <div className="min-h-screen flex items-center justify-center" data-lux={lux("loading")}>
20
23
  <p>Loading...</p>
21
24
  </div>
22
25
  );
@@ -32,17 +35,18 @@ export default function DashboardPage() {
32
35
  }
33
36
 
34
37
  return (
35
- <div className="min-h-screen bg-zinc-50 p-8">
36
- <div className="max-w-2xl mx-auto bg-white p-8 rounded-lg shadow-md">
37
- <h1 className="text-2xl font-semibold mb-4">Dashboard</h1>
38
- <p className="text-zinc-600 mb-2">
38
+ <div className="min-h-screen bg-zinc-50 p-8" data-lux={lux("container")}>
39
+ <div className="max-w-2xl mx-auto bg-white p-8 rounded-lg shadow-md" data-lux={lux("card")}>
40
+ <h1 className="text-2xl font-semibold mb-4" data-lux={lux("title")}>Dashboard</h1>
41
+ <p className="text-zinc-600 mb-2" data-lux={lux("welcome")}>
39
42
  Welcome, <span className="font-medium">{session.user.name}</span>!
40
43
  </p>
41
- <p className="text-zinc-600 mb-6">
44
+ <p className="text-zinc-600 mb-6" data-lux={lux("email")}>
42
45
  Email: {session.user.email}
43
46
  </p>
44
47
  <button
45
48
  onClick={handleSignOut}
49
+ data-lux={lux("signout-button")}
46
50
  className="px-4 py-2 bg-black text-white rounded-md hover:bg-zinc-800"
47
51
  >
48
52
  Sign Out
@@ -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
- {children}
32
+ <PostHogProvider>
33
+ <Suspense fallback={null}>
34
+ <PostHogPageView />
35
+ </Suspense>
36
+ {children}
37
+ </PostHogProvider>
31
38
  </body>
32
39
  </html>
33
40
  );
@@ -1,36 +1,41 @@
1
1
  import Link from "next/link";
2
+ import { createLux } from "@/lib/lux";
3
+
4
+ const lux = createLux("Home");
2
5
 
3
6
  export default function Home() {
4
7
  return (
5
- <div className="min-h-screen flex flex-col bg-white">
8
+ <div className="min-h-screen flex flex-col bg-white" data-lux={lux("container")}>
6
9
  <main className="flex-1 flex flex-col items-center justify-center px-6">
7
10
  <div className="max-w-2xl w-full text-center">
8
- <p className="text-sm font-medium tracking-widest text-zinc-400 uppercase mb-4">
11
+ <p className="text-sm font-medium tracking-widest text-zinc-400 uppercase mb-4" data-lux={lux("tagline")}>
9
12
  Boilerplate
10
13
  </p>
11
- <h1 className="text-6xl font-semibold tracking-tight text-black mb-6">
14
+ <h1 className="text-6xl font-semibold tracking-tight text-black mb-6" data-lux={lux("title")}>
12
15
  Lux
13
16
  </h1>
14
- <p className="text-lg text-zinc-500 mb-12 max-w-md mx-auto">
17
+ <p className="text-lg text-zinc-500 mb-12 max-w-md mx-auto" data-lux={lux("description")}>
15
18
  Authentication, database, and UI components. Everything you need to start building.
16
19
  </p>
17
20
 
18
21
  <div className="flex gap-4 justify-center mb-16">
19
22
  <Link
20
23
  href="/sign-in"
24
+ data-lux={lux("signin-button")}
21
25
  className="px-8 py-3 bg-black text-white text-sm font-medium rounded-full hover:bg-zinc-800 transition-colors"
22
26
  >
23
27
  Sign In
24
28
  </Link>
25
29
  <Link
26
30
  href="/sign-up"
31
+ data-lux={lux("signup-button")}
27
32
  className="px-8 py-3 border border-zinc-200 text-black text-sm font-medium rounded-full hover:bg-zinc-50 transition-colors"
28
33
  >
29
34
  Sign Up
30
35
  </Link>
31
36
  </div>
32
37
 
33
- <div className="flex items-center justify-center gap-8 text-sm text-zinc-400">
38
+ <div className="flex items-center justify-center gap-8 text-sm text-zinc-400" data-lux={lux("features")}>
34
39
  <span>Next.js</span>
35
40
  <span className="w-1 h-1 bg-zinc-300 rounded-full" />
36
41
  <span>Auth</span>
@@ -42,7 +47,7 @@ export default function Home() {
42
47
  </div>
43
48
  </main>
44
49
 
45
- <footer className="py-6 text-center text-sm text-zinc-400">
50
+ <footer className="py-6 text-center text-sm text-zinc-400" data-lux={lux("footer")}>
46
51
  <p>Ready to ship.</p>
47
52
  </footer>
48
53
  </div>
@@ -3,6 +3,9 @@
3
3
  import { useSession, signOut } from "@/lib/auth-client";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useEffect } from "react";
6
+ import { createLux } from "@/lib/lux";
7
+
8
+ const lux = createLux("Settings");
6
9
 
7
10
  export default function SettingsPage() {
8
11
  const { data: session, isPending } = useSession();
@@ -16,7 +19,7 @@ export default function SettingsPage() {
16
19
 
17
20
  if (isPending) {
18
21
  return (
19
- <div className="min-h-screen flex items-center justify-center">
22
+ <div className="min-h-screen flex items-center justify-center" data-lux={lux("loading")}>
20
23
  <div className="text-gray-500">Loading...</div>
21
24
  </div>
22
25
  );
@@ -32,25 +35,25 @@ export default function SettingsPage() {
32
35
  };
33
36
 
34
37
  return (
35
- <div className="min-h-screen bg-gray-50">
38
+ <div className="min-h-screen bg-gray-50" data-lux={lux("container")}>
36
39
  <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
37
40
  <div className="mb-8">
38
- <h1 className="text-3xl font-bold text-gray-900">Settings</h1>
39
- <p className="mt-2 text-sm text-gray-600">
41
+ <h1 className="text-3xl font-bold text-gray-900" data-lux={lux("title")}>Settings</h1>
42
+ <p className="mt-2 text-sm text-gray-600" data-lux={lux("subtitle")}>
40
43
  Manage your account settings
41
44
  </p>
42
45
  </div>
43
46
 
44
- <div className="bg-white shadow rounded-lg p-6">
45
- <h2 className="text-lg font-medium text-gray-900 mb-4">Account</h2>
47
+ <div className="bg-white shadow rounded-lg p-6" data-lux={lux("card")}>
48
+ <h2 className="text-lg font-medium text-gray-900 mb-4" data-lux={lux("section-title")}>Account</h2>
46
49
 
47
50
  <div className="space-y-4">
48
- <div>
51
+ <div data-lux={lux("name-field")}>
49
52
  <label className="block text-sm font-medium text-gray-700">Name</label>
50
53
  <p className="mt-1 text-sm text-gray-900">{session.user.name}</p>
51
54
  </div>
52
55
 
53
- <div>
56
+ <div data-lux={lux("email-field")}>
54
57
  <label className="block text-sm font-medium text-gray-700">Email</label>
55
58
  <p className="mt-1 text-sm text-gray-900">{session.user.email}</p>
56
59
  </div>
@@ -59,6 +62,7 @@ export default function SettingsPage() {
59
62
  <div className="mt-6 pt-6 border-t border-gray-200">
60
63
  <button
61
64
  onClick={handleSignOut}
65
+ data-lux={lux("signout-button")}
62
66
  className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
63
67
  >
64
68
  Sign Out
@@ -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
+ }
@@ -11,6 +11,7 @@
11
11
  "dependencies": {
12
12
  "@radix-ui/react-slot": "^1.2.4",
13
13
  "better-auth": "^1.4.5",
14
+ "posthog-js": "^1.252.0",
14
15
  "class-variance-authority": "^0.7.1",
15
16
  "clsx": "^2.1.1",
16
17
  "date-fns": "^4.1.0",