@gurulu/cli 0.3.4 β†’ 0.4.1

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.
Files changed (39) hide show
  1. package/README.md +61 -24
  2. package/dist/api-client.js +1 -1
  3. package/dist/commands/add-server.js +13 -6
  4. package/dist/commands/alerts.d.ts +5 -0
  5. package/dist/commands/alerts.js +43 -15
  6. package/dist/commands/audiences.d.ts +3 -0
  7. package/dist/commands/audiences.js +34 -7
  8. package/dist/commands/events.d.ts +6 -0
  9. package/dist/commands/events.js +182 -1
  10. package/dist/commands/experiments.d.ts +4 -0
  11. package/dist/commands/experiments.js +46 -15
  12. package/dist/commands/funnels.d.ts +17 -0
  13. package/dist/commands/funnels.js +203 -0
  14. package/dist/commands/goals.d.ts +18 -0
  15. package/dist/commands/goals.js +214 -0
  16. package/dist/commands/install.d.ts +8 -0
  17. package/dist/commands/install.js +74 -4
  18. package/dist/commands/sourcemap.d.ts +17 -5
  19. package/dist/commands/sourcemap.js +73 -6
  20. package/dist/commands/watch.d.ts +45 -0
  21. package/dist/commands/watch.js +258 -0
  22. package/dist/frameworks/detect.js +29 -7
  23. package/dist/index.js +158 -13
  24. package/package.json +1 -1
  25. package/scripts/gurulu-agentic-install.mjs +225 -0
  26. package/scripts/gurulu-scan.lib.cjs +539 -19
  27. package/scripts/patches/astro.patch.cjs +1 -0
  28. package/scripts/patches/auto-instrument/hono.cjs +381 -0
  29. package/scripts/patches/auto-instrument/index.cjs +2 -0
  30. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +13 -4
  31. package/scripts/patches/express.patch.cjs +2 -2
  32. package/scripts/patches/fastify.patch.cjs +1 -0
  33. package/scripts/patches/nestjs.patch.cjs +1 -0
  34. package/scripts/patches/nextjs-app-router.patch.cjs +2 -2
  35. package/scripts/patches/nextjs-pages.patch.cjs +1 -0
  36. package/scripts/patches/remix.patch.cjs +1 -0
  37. package/scripts/patches/sveltekit.patch.cjs +1 -0
  38. package/scripts/patches/vite-react.patch.cjs +1 -0
  39. package/scripts/patches/vue.patch.cjs +1 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gurulu/cli
2
2
 
3
- CLI wizard for setting up Gurulu.io analytics in any project.
3
+ CLI for Gurulu.io β€” setup, diagnostics, data exploration, and AI-powered analytics from your terminal.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -10,31 +10,76 @@ npx @gurulu/cli init
10
10
 
11
11
  ## Commands
12
12
 
13
+ ### Authentication
14
+
15
+ | Command | Description |
16
+ |---------|-------------|
17
+ | `gurulu auth` | Authenticate via device-link flow (or `--key` for manual) |
18
+ | `gurulu login` | Authenticate with API key |
19
+ | `gurulu logout` | Remove a stored profile |
20
+ | `gurulu whoami` | Show current authentication state |
21
+
22
+ ### Setup & Diagnostics
23
+
13
24
  | Command | Description |
14
25
  |---------|-------------|
15
26
  | `gurulu init` | Set up Gurulu analytics (auto-detects framework) |
16
- | `gurulu login` | Authenticate with your Personal API Key |
17
- | `gurulu events` | List detected events from your site |
18
- | `gurulu status` | Quick health check |
19
- | `gurulu doctor` | Comprehensive diagnostics |
27
+ | `gurulu install [path]` | Install Gurulu tracker in a repository |
20
28
  | `gurulu add-server` | Add server-side SDK (@gurulu/node) |
29
+ | `gurulu status` | Check SDK health and connection |
30
+ | `gurulu doctor` | Diagnose setup issues |
31
+ | `gurulu config <action>` | Manage CLI profiles (list, use, show, delete) |
32
+
33
+ ### Sites & Keys
34
+
35
+ | Command | Description |
36
+ |---------|-------------|
37
+ | `gurulu sites <action>` | Manage sites (list, create, show, delete, rotate-token) |
38
+ | `gurulu api-keys <action>` | Manage API keys (list, create, revoke, rotate) |
39
+
40
+ ### Data & Events
41
+
42
+ | Command | Description |
43
+ |---------|-------------|
44
+ | `gurulu events <action>` | View ingested events (list, tail) |
45
+ | `gurulu insights <action>` | View daily insights (today, history, weekly) |
46
+ | `gurulu chat [question]` | Ask analytics questions in natural language (NL -> SQL) |
47
+
48
+ ### Identity & Audiences
49
+
50
+ | Command | Description |
51
+ |---------|-------------|
52
+ | `gurulu identity <action>` | View identity state (decay, transfers, cdc-sources) |
53
+ | `gurulu audiences <action>` | View audiences (list, show) |
54
+ | `gurulu experiments <action>` | View experiments (list, show, results) |
55
+
56
+ ### Integrations & Export
57
+
58
+ | Command | Description |
59
+ |---------|-------------|
60
+ | `gurulu warehouse <action>` | Warehouse exports (BigQuery) |
61
+ | `gurulu warehouses <action>` | View warehouse exports (list, runs) |
62
+ | `gurulu destinations <action>` | View activation destinations (list, show) |
63
+ | `gurulu db <action>` | Connect, list, sync, or remove database sources |
64
+
65
+ ### Monitoring & Debugging
66
+
67
+ | Command | Description |
68
+ |---------|-------------|
69
+ | `gurulu alerts <action>` | View anomaly alerts (list, show, channels) |
70
+ | `gurulu sourcemap <action>` | Upload source maps for error deobfuscation |
71
+ | `gurulu audit <action>` | Stream or export the CLI audit log (tail, export) |
72
+ | `gurulu playground <action>` | View playground sessions (list) |
21
73
 
22
74
  ## Supported Frameworks
23
75
 
24
- - Next.js (App Router & Pages Router)
25
- - React (Vite & CRA)
26
- - Vue 3
27
- - Nuxt 3
28
- - Svelte & SvelteKit
29
- - Astro
30
- - Express
31
- - NestJS
32
- - Plain HTML
76
+ Next.js (App & Pages Router), React (Vite & CRA), Vue 3, Nuxt 3, Svelte & SvelteKit, Astro, Express, NestJS, Plain HTML.
33
77
 
34
78
  ## Authentication
35
79
 
36
80
  ```bash
37
- gurulu login --api-key pak_live_xxxxx
81
+ gurulu auth # Device-link flow (recommended)
82
+ gurulu login --api-key pak_live_x # Manual API key
38
83
  ```
39
84
 
40
85
  Or set the `GURULU_API_KEY` environment variable.
@@ -52,15 +97,7 @@ gurulu init --site-id abc123 --token tok_xxx --no-interactive
52
97
  Use `--json` flag for machine-readable output:
53
98
 
54
99
  ```bash
55
- gurulu events --json
100
+ gurulu events list --json
56
101
  gurulu status --json
57
102
  gurulu doctor --json
58
103
  ```
59
-
60
- ## Development
61
-
62
- ```bash
63
- npm install
64
- npm run build
65
- node bin/gurulu.js --help
66
- ```
@@ -97,7 +97,7 @@ async function cliApi(path, init = {}) {
97
97
  if (!init.skipAuth && profile) {
98
98
  headers.set('authorization', `Bearer ${profile.secret_key}`);
99
99
  }
100
- if (init.body && !headers.has('content-type')) {
100
+ if (init.body && !headers.has('content-type') && !(init.body instanceof FormData)) {
101
101
  headers.set('content-type', 'application/json');
102
102
  }
103
103
  let res;
@@ -46,11 +46,18 @@ async function addServerCommand(args) {
46
46
  let serverApiKey = process.env.GURULU_SERVER_API_KEY;
47
47
  if (!serverApiKey && profile) {
48
48
  try {
49
- const data = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(siteId)}`, { preloadedProfile: profile });
50
- serverApiKey = data.site?.publishableKey || '';
49
+ (0, ui_1.step)('Creating server API key...');
50
+ const data = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(siteId)}/server-keys`, {
51
+ preloadedProfile: profile,
52
+ json: { name: `cli-${new Date().toISOString().slice(0, 10)}` },
53
+ });
54
+ serverApiKey = data.key || '';
55
+ if (serverApiKey) {
56
+ (0, ui_1.success)(`Server API key created (${data.keyId})`);
57
+ }
51
58
  }
52
59
  catch (err) {
53
- (0, ui_1.warn)(`Could not fetch server credentials: ${err.message}`);
60
+ (0, ui_1.warn)(`Could not create server API key: ${err.message}`);
54
61
  }
55
62
  }
56
63
  if (!serverApiKey && !args.noInteractive) {
@@ -89,9 +96,9 @@ async function addServerCommand(args) {
89
96
  }
90
97
  // Step 2: Create server config file
91
98
  const configFilePath = path_1.default.join(projectDir, 'src', 'lib', 'gurulu-server.ts');
92
- const configCode = `import { GuruluNode } from '@gurulu/node';
99
+ const configCode = `import { Gurulu } from '@gurulu/node';
93
100
 
94
- export const gurulu = new GuruluNode({
101
+ export const gurulu = new Gurulu({
95
102
  siteId: process.env.GURULU_SITE_ID || '${siteId}',
96
103
  apiKey: process.env.GURULU_SERVER_API_KEY || '',
97
104
  });
@@ -134,7 +141,7 @@ export const gurulu = new GuruluNode({
134
141
  console.log('');
135
142
  (0, ui_1.step)('Import gurulu from src/lib/gurulu-server.ts in your API routes');
136
143
  (0, ui_1.step)(`Use ${(0, ui_1.cyan)('gurulu.track(event, properties)')} to send server-side events`);
137
- (0, ui_1.step)(`Use ${(0, ui_1.cyan)('gurulu.identify(userId, traits)')} to identify users`);
144
+ (0, ui_1.step)(`Use ${(0, ui_1.cyan)("await gurulu.identify({ userId: '...', anonymousId: '...', traits: {} })")} to identify users`);
138
145
  (0, ui_1.step)(`Run ${(0, ui_1.cyan)('gurulu doctor')} to verify the setup`);
139
146
  console.log('');
140
147
  (0, ui_1.success)('Server SDK setup complete!');
@@ -12,9 +12,14 @@ export interface AlertsArgs {
12
12
  json?: boolean;
13
13
  profile?: string;
14
14
  fromFile?: string;
15
+ site?: string;
16
+ id?: string;
15
17
  name?: string;
16
18
  type?: string;
17
19
  metric?: string;
20
+ thresholdType?: string;
21
+ thresholdValue?: number;
22
+ channel?: string;
18
23
  note?: string;
19
24
  yes?: boolean;
20
25
  dryRun?: boolean;
@@ -33,6 +33,8 @@ async function alertsCommand(args) {
33
33
  }
34
34
  async function listCmd(args) {
35
35
  const qs = new URLSearchParams();
36
+ if (args.site)
37
+ qs.set('siteId', args.site);
36
38
  if (args.severity)
37
39
  qs.set('severity', args.severity);
38
40
  if (args.acknowledged)
@@ -81,12 +83,28 @@ async function createCmd(args) {
81
83
  let payload = {};
82
84
  if (args.fromFile)
83
85
  payload = (0, from_file_1.loadFromFile)(args.fromFile);
86
+ if (args.site)
87
+ payload.siteId = args.site;
88
+ if (args.name)
89
+ payload.name = args.name;
90
+ if (args.type)
91
+ payload.type = args.type;
84
92
  if (args.metric)
85
93
  payload.metric = args.metric;
86
94
  if (args.severity)
87
95
  payload.severity = args.severity;
88
- if (!payload.metric || !payload.severity) {
89
- (0, ui_1.error)('metric and severity are required.');
96
+ if (args.thresholdType)
97
+ payload.thresholdType = args.thresholdType;
98
+ if (args.thresholdValue !== undefined)
99
+ payload.thresholdValue = args.thresholdValue;
100
+ if (args.channel)
101
+ payload.channelId = args.channel;
102
+ if (!payload.name) {
103
+ (0, ui_1.error)('name is required (pass --name or --from-file)');
104
+ process.exit(1);
105
+ }
106
+ if (!payload.siteId) {
107
+ (0, ui_1.error)('site is required (pass --site)');
90
108
  process.exit(1);
91
109
  }
92
110
  if (args.dryRun) {
@@ -98,7 +116,7 @@ async function createCmd(args) {
98
116
  (0, dry_run_1.printDryRun)(body, args.json);
99
117
  return;
100
118
  }
101
- const ok = await (0, confirm_1.promptConfirm)(`Create alert '${payload.metric}'?`, {
119
+ const ok = await (0, confirm_1.promptConfirm)(`Create alert '${payload.name}'?`, {
102
120
  yes: args.yes,
103
121
  defaultYes: true,
104
122
  });
@@ -116,51 +134,61 @@ async function createCmd(args) {
116
134
  (0, ui_1.success)(`Created alert ${body.alert?.id ?? ''}`);
117
135
  }
118
136
  async function updateCmd(args) {
119
- if (!args.target) {
120
- (0, ui_1.error)('Usage: gurulu alerts update <id> [--note ...] [--ack]');
137
+ const resolveTarget = args.id || args.target;
138
+ if (!resolveTarget) {
139
+ (0, ui_1.error)('Usage: gurulu alerts update <id> [--id ...] [--name ...] [--threshold-value ...]');
121
140
  process.exit(1);
122
141
  }
123
142
  const payload = {};
143
+ if (args.name)
144
+ payload.name = args.name;
124
145
  if (args.note !== undefined)
125
146
  payload.note = args.note;
147
+ if (args.thresholdType)
148
+ payload.thresholdType = args.thresholdType;
149
+ if (args.thresholdValue !== undefined)
150
+ payload.thresholdValue = args.thresholdValue;
151
+ if (args.channel)
152
+ payload.channelId = args.channel;
126
153
  if (args.ack === true)
127
154
  payload.acknowledge = true;
128
155
  if (args.dryRun) {
129
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
156
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
130
157
  (0, dry_run_1.printDryRun)(body, args.json);
131
158
  return;
132
159
  }
133
- const ok = await (0, confirm_1.promptConfirm)(`Update alert '${args.target}'?`, {
160
+ const ok = await (0, confirm_1.promptConfirm)(`Update alert '${resolveTarget}'?`, {
134
161
  yes: args.yes,
135
162
  defaultYes: true,
136
163
  });
137
164
  if (!ok)
138
165
  return (0, ui_1.info)('Aborted.');
139
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}`, { profile: args.profile, method: 'PATCH', json: payload });
166
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}`, { profile: args.profile, method: 'PATCH', json: payload });
140
167
  if (args.json) {
141
168
  process.stdout.write(JSON.stringify(body, null, 2) + '\n');
142
169
  return;
143
170
  }
144
- (0, ui_1.success)(`Updated alert ${args.target}`);
171
+ (0, ui_1.success)(`Updated alert ${resolveTarget}`);
145
172
  }
146
173
  async function deleteCmd(args) {
147
- if (!args.target) {
148
- (0, ui_1.error)('Usage: gurulu alerts delete <id>');
174
+ const resolveTarget = args.id || args.target;
175
+ if (!resolveTarget) {
176
+ (0, ui_1.error)('Usage: gurulu alerts delete <id> [--id ...]');
149
177
  process.exit(1);
150
178
  }
151
179
  if (args.dryRun) {
152
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
180
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
153
181
  (0, dry_run_1.printDryRun)(body, args.json);
154
182
  return;
155
183
  }
156
- const ok = await (0, confirm_1.promptConfirm)(`About to delete alert '${args.target}'. Continue?`, { yes: args.yes, defaultYes: false });
184
+ const ok = await (0, confirm_1.promptConfirm)(`About to delete alert '${resolveTarget}'. Continue?`, { yes: args.yes, defaultYes: false });
157
185
  if (!ok)
158
186
  return (0, ui_1.info)('Aborted.');
159
- await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}`, {
187
+ await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}`, {
160
188
  profile: args.profile,
161
189
  method: 'DELETE',
162
190
  });
163
- (0, ui_1.success)(`Deleted alert ${args.target}`);
191
+ (0, ui_1.success)(`Deleted alert ${resolveTarget}`);
164
192
  }
165
193
  // ── channels ──────────────────────────────────────────────────────────────
166
194
  async function channelsCmd(args) {
@@ -8,8 +8,11 @@ export interface AudiencesArgs {
8
8
  json?: boolean;
9
9
  profile?: string;
10
10
  fromFile?: string;
11
+ site?: string;
12
+ id?: string;
11
13
  name?: string;
12
14
  description?: string;
15
+ rules?: string;
13
16
  yes?: boolean;
14
17
  dryRun?: boolean;
15
18
  }
@@ -30,7 +30,8 @@ async function audiencesCommand(args) {
30
30
  }
31
31
  }
32
32
  async function listCmd(args) {
33
- const body = await (0, api_client_1.cliApiJson)('/api/cli/audiences', {
33
+ const qs = args.site ? `?siteId=${encodeURIComponent(args.site)}` : '';
34
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences${qs}`, {
34
35
  profile: args.profile,
35
36
  });
36
37
  const audiences = body.audiences || [];
@@ -88,12 +89,27 @@ async function createCmd(args) {
88
89
  }
89
90
  if (args.name)
90
91
  payload.name = args.name;
92
+ if (args.site)
93
+ payload.siteId = args.site;
91
94
  if (args.description !== undefined)
92
95
  payload.description = args.description;
96
+ if (args.rules) {
97
+ try {
98
+ payload.rules = JSON.parse(args.rules);
99
+ }
100
+ catch {
101
+ (0, ui_1.error)('--rules must be valid JSON');
102
+ process.exit(1);
103
+ }
104
+ }
93
105
  if (!payload.name) {
94
106
  (0, ui_1.error)('name is required (pass --name or --from-file)');
95
107
  process.exit(1);
96
108
  }
109
+ if (!payload.siteId) {
110
+ (0, ui_1.error)('site is required (pass --site)');
111
+ process.exit(1);
112
+ }
97
113
  const qs = args.dryRun ? '?dryRun=1' : '';
98
114
  if (args.dryRun) {
99
115
  const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences${qs}`, {
@@ -121,11 +137,12 @@ async function createCmd(args) {
121
137
  (0, ui_1.success)(`Created audience ${body.audience?.id ?? ''}`);
122
138
  }
123
139
  async function updateCmd(args) {
124
- if (!args.target) {
125
- (0, ui_1.error)('Usage: gurulu audiences update <name-or-id> [--from-file <path>] [--name ...]');
140
+ const resolveTarget = args.id || args.target;
141
+ if (!resolveTarget) {
142
+ (0, ui_1.error)('Usage: gurulu audiences update <name-or-id> [--id ...] [--name ...] [--rules ...]');
126
143
  process.exit(1);
127
144
  }
128
- const id = await resolveId(args.target, args.profile);
145
+ const id = await resolveId(resolveTarget, args.profile);
129
146
  let payload = {};
130
147
  if (args.fromFile) {
131
148
  payload = (0, from_file_1.loadFromFile)(args.fromFile);
@@ -134,6 +151,15 @@ async function updateCmd(args) {
134
151
  payload.name = args.name;
135
152
  if (args.description !== undefined)
136
153
  payload.description = args.description;
154
+ if (args.rules) {
155
+ try {
156
+ payload.rules = JSON.parse(args.rules);
157
+ }
158
+ catch {
159
+ (0, ui_1.error)('--rules must be valid JSON');
160
+ process.exit(1);
161
+ }
162
+ }
137
163
  const qs = args.dryRun ? '?dryRun=1' : '';
138
164
  if (args.dryRun) {
139
165
  const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences/${encodeURIComponent(id)}${qs}`, { profile: args.profile, method: 'PATCH', json: payload });
@@ -156,11 +182,12 @@ async function updateCmd(args) {
156
182
  (0, ui_1.success)(`Updated audience ${id}`);
157
183
  }
158
184
  async function deleteCmd(args) {
159
- if (!args.target) {
160
- (0, ui_1.error)('Usage: gurulu audiences delete <name-or-id>');
185
+ const resolveTarget = args.id || args.target;
186
+ if (!resolveTarget) {
187
+ (0, ui_1.error)('Usage: gurulu audiences delete <name-or-id> [--id ...]');
161
188
  process.exit(1);
162
189
  }
163
- const id = await resolveId(args.target, args.profile);
190
+ const id = await resolveId(resolveTarget, args.profile);
164
191
  const qs = args.dryRun ? '?dryRun=1' : '';
165
192
  if (args.dryRun) {
166
193
  const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences/${encodeURIComponent(id)}${qs}`, { profile: args.profile, method: 'DELETE' });
@@ -17,6 +17,12 @@ export interface EventsArgs {
17
17
  limit?: number;
18
18
  json?: boolean;
19
19
  profile?: string;
20
+ displayName?: string;
21
+ description?: string;
22
+ category?: string;
23
+ properties?: string;
24
+ event?: string;
25
+ vertical?: string;
20
26
  }
21
27
  export declare function eventsCommand(args: EventsArgs): Promise<void>;
22
28
  export declare function legacyEventsCommand(args: {
@@ -22,9 +22,17 @@ async function eventsCommand(args) {
22
22
  return listCmd(args);
23
23
  case 'tail':
24
24
  return tailCmd(args);
25
+ case 'schema':
26
+ return schemaCmd(args);
27
+ case 'define':
28
+ return defineCmd(args);
29
+ case 'verify':
30
+ return verifyCmd(args);
31
+ case 'templates':
32
+ return templatesCmd(args);
25
33
  default:
26
34
  (0, ui_1.error)(`Unknown events action: ${action}`);
27
- (0, ui_1.info)('Usage: gurulu events [list|tail]');
35
+ (0, ui_1.info)('Usage: gurulu events [list|tail|schema|define|verify|templates]');
28
36
  process.exit(1);
29
37
  }
30
38
  }
@@ -112,6 +120,179 @@ async function tailCmd(args) {
112
120
  process.stdout.write(formatFrame(text, !!args.json));
113
121
  }
114
122
  }
123
+ /* ── schema: show property schema & implementation examples ───────── */
124
+ async function schemaCmd(args) {
125
+ if (!args.eventName) {
126
+ (0, ui_1.error)('Event name is required. Usage: gurulu events schema --event-name <name> --site <id>');
127
+ process.exit(1);
128
+ }
129
+ if (!args.site) {
130
+ (0, ui_1.error)('--site is required for schema lookup.');
131
+ process.exit(1);
132
+ }
133
+ const path = `/api/cli/events/schema/${encodeURIComponent(args.eventName)}?siteId=${args.site}`;
134
+ const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
135
+ if (args.json) {
136
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
137
+ return;
138
+ }
139
+ console.log((0, ui_1.bold)(`\nEvent: ${data.eventName}`));
140
+ if (data.source)
141
+ console.log(`Source: ${data.source}`);
142
+ if (data.description)
143
+ console.log(`Description: ${data.description}`);
144
+ if (data.isRevenue)
145
+ console.log((0, ui_1.yellow)(`Revenue event: ${data.revenueNote || 'yes'}`));
146
+ if (data.propertySchema?.length) {
147
+ console.log((0, ui_1.bold)('\nProperties:'));
148
+ for (const p of data.propertySchema) {
149
+ const req = p.required ? (0, ui_1.red)('REQUIRED') : (0, ui_1.dim)('optional');
150
+ const fmt = p.format ? (0, ui_1.cyan)(` [${p.format}]`) : '';
151
+ const ex = p.example !== undefined ? (0, ui_1.dim)(` e.g. ${JSON.stringify(p.example)}`) : '';
152
+ console.log(` ${p.name}: ${(0, ui_1.cyan)(p.type)} (${req})${fmt} β€” ${p.description || ''}${ex}`);
153
+ }
154
+ }
155
+ if (data.implementation && Object.keys(data.implementation).length) {
156
+ console.log((0, ui_1.bold)('\nImplementation:'));
157
+ for (const [platform, code] of Object.entries(data.implementation)) {
158
+ console.log(` ${(0, ui_1.cyan)(`[${platform}]`)}: ${code}`);
159
+ }
160
+ }
161
+ console.log('');
162
+ }
163
+ /* ── define: create a new custom event definition ────────────────── */
164
+ async function defineCmd(args) {
165
+ if (!args.site) {
166
+ (0, ui_1.error)('--site is required.');
167
+ process.exit(1);
168
+ }
169
+ if (!args.eventName) {
170
+ (0, ui_1.error)('--event-name is required.');
171
+ process.exit(1);
172
+ }
173
+ if (!args.displayName) {
174
+ (0, ui_1.error)('--display-name is required.');
175
+ process.exit(1);
176
+ }
177
+ let propertySchema = [];
178
+ if (args.properties) {
179
+ try {
180
+ propertySchema = JSON.parse(args.properties);
181
+ }
182
+ catch {
183
+ (0, ui_1.error)('Invalid JSON for --properties');
184
+ process.exit(1);
185
+ }
186
+ }
187
+ const eventName = args.eventName.startsWith('$') ? args.eventName : `$${args.eventName}`;
188
+ const body = {
189
+ siteId: args.site,
190
+ eventName,
191
+ displayName: args.displayName,
192
+ description: args.description || '',
193
+ intentName: args.category || 'engagement',
194
+ fingerprint: { propertySchema },
195
+ };
196
+ const data = await (0, api_client_1.cliApiJson)('/api/cli/events/definitions', { profile: args.profile, method: 'POST', json: body });
197
+ if (args.json) {
198
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
199
+ return;
200
+ }
201
+ (0, ui_1.success)(`Event ${eventName} defined successfully`);
202
+ (0, ui_1.info)(`Run: gurulu events schema --event-name ${eventName} --site ${args.site} to see implementation examples`);
203
+ }
204
+ /* ── verify: check event health / quality ────────────────────────── */
205
+ async function verifyCmd(args) {
206
+ if (!args.site) {
207
+ (0, ui_1.error)('--site is required.');
208
+ process.exit(1);
209
+ }
210
+ const qs = new URLSearchParams();
211
+ qs.set('siteId', args.site);
212
+ if (args.event)
213
+ qs.set('eventName', args.event);
214
+ const path = `/api/cli/events/health?${qs.toString()}`;
215
+ const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
216
+ if (args.json) {
217
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
218
+ return;
219
+ }
220
+ console.log((0, ui_1.bold)('\nEvent Health Report'));
221
+ console.log(`Site: ${args.site}`);
222
+ console.log('');
223
+ if (Array.isArray(data.events)) {
224
+ for (const ev of data.events) {
225
+ const statusIcon = ev.status === 'active'
226
+ ? (0, ui_1.green)('●')
227
+ : ev.status === 'stale'
228
+ ? (0, ui_1.yellow)('●')
229
+ : (0, ui_1.red)('●');
230
+ console.log(`${statusIcon} ${ev.event_name} β€” ${ev.total_count ?? 0} events (last: ${ev.last_seen ?? 'never'})`);
231
+ if (ev.missing_properties?.length) {
232
+ console.log((0, ui_1.yellow)(` Missing required properties: ${ev.missing_properties.join(', ')}`));
233
+ }
234
+ }
235
+ }
236
+ else if (data.total_events !== undefined) {
237
+ console.log(`Total events (24h): ${data.total_events}`);
238
+ console.log(`Unique event types: ${data.unique_types ?? 'N/A'}`);
239
+ }
240
+ console.log('');
241
+ }
242
+ /* ── templates: list vertical event template catalogs ───────────── */
243
+ async function templatesCmd(args) {
244
+ const qs = new URLSearchParams();
245
+ if (args.vertical)
246
+ qs.set('vertical', args.vertical);
247
+ const path = `/api/events/templates${qs.toString() ? `?${qs.toString()}` : ''}`;
248
+ const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
249
+ if (args.json) {
250
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
251
+ return;
252
+ }
253
+ if (data.error) {
254
+ (0, ui_1.error)(data.error);
255
+ process.exit(1);
256
+ }
257
+ // List mode β€” no vertical specified
258
+ if (data.verticals) {
259
+ console.log((0, ui_1.bold)('\nAvailable Verticals\n'));
260
+ for (const v of data.verticals) {
261
+ console.log(` ${(0, ui_1.cyan)(v.id.padEnd(14))} ${v.name.padEnd(30)} ${(0, ui_1.dim)(`${v.eventCount} events`)}`);
262
+ }
263
+ console.log('');
264
+ (0, ui_1.info)('Usage: gurulu events templates --vertical <id>');
265
+ return;
266
+ }
267
+ // Detail mode β€” specific vertical
268
+ console.log((0, ui_1.bold)(`\nVertical: ${data.vertical}`));
269
+ console.log(`Confidence: ${data.confidence}\n`);
270
+ if (data.events?.length) {
271
+ console.log((0, ui_1.bold)('Events:\n'));
272
+ for (const ev of data.events) {
273
+ console.log(` ${(0, ui_1.green)(ev.name)} ${(0, ui_1.dim)(`[${ev.category}]`)}`);
274
+ console.log(` ${(0, ui_1.dim)(ev.reasoning)}`);
275
+ if (ev.propertySchema?.length) {
276
+ for (const p of ev.propertySchema) {
277
+ const req = p.required ? (0, ui_1.red)('REQUIRED') : (0, ui_1.dim)('optional');
278
+ const fmt = p.format ? (0, ui_1.cyan)(` [${p.format}]`) : '';
279
+ const ex = p.example !== undefined ? (0, ui_1.dim)(` e.g. ${JSON.stringify(p.example)}`) : '';
280
+ console.log(` ${p.name}: ${(0, ui_1.cyan)(p.type)} (${req})${fmt}${ex}`);
281
+ }
282
+ }
283
+ console.log('');
284
+ }
285
+ }
286
+ if (data.funnels?.length) {
287
+ console.log((0, ui_1.bold)('Funnels:\n'));
288
+ for (const f of data.funnels) {
289
+ console.log(` ${(0, ui_1.yellow)(f.name)} ${(0, ui_1.dim)(`[${f.category}]`)}`);
290
+ console.log(` Steps: ${f.steps.join(' β†’ ')}`);
291
+ console.log(` ${(0, ui_1.dim)(f.reasoning)}`);
292
+ console.log('');
293
+ }
294
+ }
295
+ }
115
296
  function formatFrame(raw, json) {
116
297
  if (json)
117
298
  return raw;
@@ -9,9 +9,13 @@ export interface ExperimentsArgs {
9
9
  json?: boolean;
10
10
  profile?: string;
11
11
  fromFile?: string;
12
+ site?: string;
13
+ id?: string;
12
14
  key?: string;
13
15
  name?: string;
14
16
  description?: string;
17
+ variants?: string;
18
+ goal?: string;
15
19
  yes?: boolean;
16
20
  dryRun?: boolean;
17
21
  }