@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.
- package/README.md +61 -24
- package/dist/api-client.js +1 -1
- package/dist/commands/add-server.js +13 -6
- package/dist/commands/alerts.d.ts +5 -0
- package/dist/commands/alerts.js +43 -15
- package/dist/commands/audiences.d.ts +3 -0
- package/dist/commands/audiences.js +34 -7
- package/dist/commands/events.d.ts +6 -0
- package/dist/commands/events.js +182 -1
- package/dist/commands/experiments.d.ts +4 -0
- package/dist/commands/experiments.js +46 -15
- package/dist/commands/funnels.d.ts +17 -0
- package/dist/commands/funnels.js +203 -0
- package/dist/commands/goals.d.ts +18 -0
- package/dist/commands/goals.js +214 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +74 -4
- package/dist/commands/sourcemap.d.ts +17 -5
- package/dist/commands/sourcemap.js +73 -6
- package/dist/commands/watch.d.ts +45 -0
- package/dist/commands/watch.js +258 -0
- package/dist/frameworks/detect.js +29 -7
- package/dist/index.js +158 -13
- package/package.json +1 -1
- package/scripts/gurulu-agentic-install.mjs +225 -0
- package/scripts/gurulu-scan.lib.cjs +539 -19
- package/scripts/patches/astro.patch.cjs +1 -0
- package/scripts/patches/auto-instrument/hono.cjs +381 -0
- package/scripts/patches/auto-instrument/index.cjs +2 -0
- package/scripts/patches/auto-instrument/nextjs-app-router.cjs +13 -4
- package/scripts/patches/express.patch.cjs +2 -2
- package/scripts/patches/fastify.patch.cjs +1 -0
- package/scripts/patches/nestjs.patch.cjs +1 -0
- package/scripts/patches/nextjs-app-router.patch.cjs +2 -2
- package/scripts/patches/nextjs-pages.patch.cjs +1 -0
- package/scripts/patches/remix.patch.cjs +1 -0
- package/scripts/patches/sveltekit.patch.cjs +1 -0
- package/scripts/patches/vite-react.patch.cjs +1 -0
- package/scripts/patches/vue.patch.cjs +1 -0
package/dist/index.js
CHANGED
|
@@ -30,12 +30,17 @@ const identity_1 = require("./commands/identity");
|
|
|
30
30
|
const playground_1 = require("./commands/playground");
|
|
31
31
|
// Phase 20 W3 C4 — audit log tail/export
|
|
32
32
|
const audit_1 = require("./commands/audit");
|
|
33
|
+
// Goals & Funnels CRUD
|
|
34
|
+
const goals_1 = require("./commands/goals");
|
|
35
|
+
const funnels_1 = require("./commands/funnels");
|
|
33
36
|
// Gurulu Chat — NL → SQL analytics
|
|
34
37
|
const chat_1 = require("./commands/chat");
|
|
35
38
|
// Error tracking — source map upload
|
|
36
39
|
const sourcemap_1 = require("./commands/sourcemap");
|
|
37
40
|
// Phase 21 — database connect
|
|
38
41
|
const db_1 = require("./commands/db");
|
|
42
|
+
// Sprint B Group VII B14 — live event stream
|
|
43
|
+
const watch_1 = require("./commands/watch");
|
|
39
44
|
(0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
40
45
|
.scriptName('gurulu')
|
|
41
46
|
.option('profile', { type: 'string', describe: 'Use a specific profile (default: personal)' })
|
|
@@ -153,13 +158,19 @@ const db_1 = require("./commands/db");
|
|
|
153
158
|
yes: args.yes,
|
|
154
159
|
profile: args.profile,
|
|
155
160
|
}))
|
|
156
|
-
.command('events <action>', '
|
|
157
|
-
.positional('action', { type: 'string', describe: 'list | tail' })
|
|
161
|
+
.command('events <action>', 'Manage events (list, tail, schema, define, verify, templates)', (y) => y
|
|
162
|
+
.positional('action', { type: 'string', describe: 'list | tail | schema | define | verify | templates' })
|
|
158
163
|
.option('site', { type: 'string', describe: 'Site ID' })
|
|
159
|
-
.option('event-name', { type: 'string', describe: '
|
|
164
|
+
.option('event-name', { type: 'string', describe: 'Event name (for schema/define/list)' })
|
|
160
165
|
.option('filter', { type: 'string', describe: 'Filter expression (event_name:...)' })
|
|
161
166
|
.option('since', { type: 'string', describe: 'ISO timestamp or 1h/24h/7d' })
|
|
162
167
|
.option('limit', { type: 'number', describe: 'Max rows (1..500)' })
|
|
168
|
+
.option('display-name', { type: 'string', describe: 'Human-readable display name (define)' })
|
|
169
|
+
.option('description', { type: 'string', describe: 'Event description (define)' })
|
|
170
|
+
.option('category', { type: 'string', describe: 'Category: acquisition|activation|retention|revenue|referral|compliance|support|engagement (define)' })
|
|
171
|
+
.option('properties', { type: 'string', describe: 'Property schema as JSON array (define)' })
|
|
172
|
+
.option('event', { type: 'string', describe: 'Check a specific event only (verify)' })
|
|
173
|
+
.option('vertical', { type: 'string', describe: 'Business vertical for templates (ecommerce, saas, fintech, igaming, etc.)' })
|
|
163
174
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => {
|
|
164
175
|
(0, events_1.eventsCommand)({
|
|
165
176
|
action: args.action,
|
|
@@ -170,6 +181,12 @@ const db_1 = require("./commands/db");
|
|
|
170
181
|
limit: args.limit,
|
|
171
182
|
json: args.json,
|
|
172
183
|
profile: args.profile,
|
|
184
|
+
displayName: args['display-name'],
|
|
185
|
+
description: args.description,
|
|
186
|
+
category: args.category,
|
|
187
|
+
properties: args.properties,
|
|
188
|
+
event: args.event,
|
|
189
|
+
vertical: args.vertical,
|
|
173
190
|
});
|
|
174
191
|
})
|
|
175
192
|
.command('status', 'Check SDK health and connection', (y) => y
|
|
@@ -208,7 +225,7 @@ const db_1 = require("./commands/db");
|
|
|
208
225
|
.option('skip-env', { type: 'boolean', describe: 'Skip .env file merge' })
|
|
209
226
|
.option('yes', { type: 'boolean', alias: 'y', describe: 'Non-interactive (assume yes)' })
|
|
210
227
|
.option('ingest-url', { type: 'string', describe: 'Override ingest base URL' })
|
|
211
|
-
.option('verify', { type: 'boolean', default:
|
|
228
|
+
.option('verify', { type: 'boolean', default: false, describe: 'Live smoke test after install (requires playwright-core)' })
|
|
212
229
|
.option('skip-intent', { type: 'boolean', describe: 'Skip install-time intent discovery' })
|
|
213
230
|
.option('intent-dry-run', { type: 'boolean', describe: 'Show intent proposal without pre-seeding' })
|
|
214
231
|
.option('vertical', { type: 'string', describe: 'Vertical hint for intent analyzer' })
|
|
@@ -269,12 +286,28 @@ const db_1 = require("./commands/db");
|
|
|
269
286
|
});
|
|
270
287
|
})
|
|
271
288
|
// ── Phase 19.5 W2 — read-surface subcommands ─────────────────────────
|
|
272
|
-
.command('audiences <action> [target]', '
|
|
273
|
-
.positional('action', { type: 'string', describe: 'list | show' })
|
|
289
|
+
.command('audiences <action> [target]', 'Manage audiences (list, show, create, update, delete)', (y) => y
|
|
290
|
+
.positional('action', { type: 'string', describe: 'list | show | create | update | delete' })
|
|
274
291
|
.positional('target', { type: 'string', describe: 'Audience name or id' })
|
|
292
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
293
|
+
.option('id', { type: 'string', describe: 'Audience ID (for update/delete)' })
|
|
294
|
+
.option('name', { type: 'string', describe: 'Audience name' })
|
|
295
|
+
.option('description', { type: 'string', describe: 'Audience description' })
|
|
296
|
+
.option('rules', { type: 'string', describe: 'Audience rules (JSON array)' })
|
|
297
|
+
.option('from-file', { type: 'string', describe: 'Load payload from JSON file' })
|
|
298
|
+
.option('dry-run', { type: 'boolean', describe: 'Show what would be done' })
|
|
299
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
275
300
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, audiences_1.audiencesCommand)({
|
|
276
301
|
action: args.action,
|
|
277
302
|
target: args.target,
|
|
303
|
+
site: args.site,
|
|
304
|
+
id: args.id,
|
|
305
|
+
name: args.name,
|
|
306
|
+
description: args.description,
|
|
307
|
+
rules: args.rules,
|
|
308
|
+
fromFile: args['from-file'],
|
|
309
|
+
dryRun: args['dry-run'],
|
|
310
|
+
yes: args.yes,
|
|
278
311
|
json: args.json,
|
|
279
312
|
profile: args.profile,
|
|
280
313
|
}))
|
|
@@ -287,20 +320,44 @@ const db_1 = require("./commands/db");
|
|
|
287
320
|
json: args.json,
|
|
288
321
|
profile: args.profile,
|
|
289
322
|
}))
|
|
290
|
-
.command('alerts <action> [sub] [target]', '
|
|
291
|
-
.positional('action', { type: 'string', describe: 'list | show | channels' })
|
|
323
|
+
.command('alerts <action> [sub] [target]', 'Manage alerts (list, show, create, update, delete, channels)', (y) => y
|
|
324
|
+
.positional('action', { type: 'string', describe: 'list | show | create | update | delete | channels' })
|
|
292
325
|
.positional('sub', { type: 'string', describe: 'Subaction (e.g. list for channels)' })
|
|
293
326
|
.positional('target', { type: 'string', describe: 'Alert id (for show)' })
|
|
327
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
328
|
+
.option('id', { type: 'string', describe: 'Alert ID (for update/delete)' })
|
|
329
|
+
.option('name', { type: 'string', describe: 'Alert rule name' })
|
|
330
|
+
.option('type', { type: 'string', describe: 'Alert type (threshold, anomaly)' })
|
|
331
|
+
.option('metric', { type: 'string', describe: 'Alert metric' })
|
|
332
|
+
.option('threshold-type', { type: 'string', describe: 'Threshold type (count_per_minute, value, etc.)' })
|
|
333
|
+
.option('threshold-value', { type: 'number', describe: 'Threshold value' })
|
|
334
|
+
.option('channel', { type: 'string', describe: 'Channel ID for alert delivery' })
|
|
294
335
|
.option('severity', { type: 'string', describe: 'low | medium | high | critical' })
|
|
295
336
|
.option('acknowledged', { type: 'string', describe: 'true | false' })
|
|
337
|
+
.option('note', { type: 'string', describe: 'Note for update' })
|
|
296
338
|
.option('limit', { type: 'number', describe: 'Max rows (1..200)' })
|
|
339
|
+
.option('from-file', { type: 'string', describe: 'Load payload from JSON file' })
|
|
340
|
+
.option('dry-run', { type: 'boolean', describe: 'Show what would be done' })
|
|
341
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
297
342
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, alerts_1.alertsCommand)({
|
|
298
343
|
action: args.action,
|
|
299
344
|
sub: args.sub,
|
|
300
345
|
target: args.target,
|
|
346
|
+
site: args.site,
|
|
347
|
+
id: args.id,
|
|
348
|
+
name: args.name,
|
|
349
|
+
type: args.type,
|
|
350
|
+
metric: args.metric,
|
|
351
|
+
thresholdType: args['threshold-type'],
|
|
352
|
+
thresholdValue: args['threshold-value'],
|
|
353
|
+
channel: args.channel,
|
|
301
354
|
severity: args.severity,
|
|
302
355
|
acknowledged: args.acknowledged,
|
|
356
|
+
note: args.note,
|
|
303
357
|
limit: args.limit,
|
|
358
|
+
fromFile: args['from-file'],
|
|
359
|
+
dryRun: args['dry-run'],
|
|
360
|
+
yes: args.yes,
|
|
304
361
|
json: args.json,
|
|
305
362
|
profile: args.profile,
|
|
306
363
|
}))
|
|
@@ -322,14 +379,82 @@ const db_1 = require("./commands/db");
|
|
|
322
379
|
json: args.json,
|
|
323
380
|
profile: args.profile,
|
|
324
381
|
}))
|
|
325
|
-
.command('experiments <action> [target]', '
|
|
326
|
-
.positional('action', { type: 'string', describe: 'list | show | results' })
|
|
382
|
+
.command('experiments <action> [target]', 'Manage experiments (list, show, results, create, update, delete, start, stop)', (y) => y
|
|
383
|
+
.positional('action', { type: 'string', describe: 'list | show | results | create | update | delete | start | stop' })
|
|
327
384
|
.positional('target', { type: 'string', describe: 'Experiment key or id' })
|
|
385
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
386
|
+
.option('id', { type: 'string', describe: 'Experiment ID (for update/delete)' })
|
|
387
|
+
.option('key', { type: 'string', describe: 'Experiment key (for create)' })
|
|
388
|
+
.option('name', { type: 'string', describe: 'Experiment name' })
|
|
389
|
+
.option('description', { type: 'string', describe: 'Experiment description' })
|
|
390
|
+
.option('variants', { type: 'string', describe: 'Variants as JSON array' })
|
|
391
|
+
.option('goal', { type: 'string', describe: 'Goal ID to track conversions' })
|
|
328
392
|
.option('conversion-event', { type: 'string', describe: 'Conversion event name' })
|
|
393
|
+
.option('from-file', { type: 'string', describe: 'Load payload from JSON file' })
|
|
394
|
+
.option('dry-run', { type: 'boolean', describe: 'Show what would be done' })
|
|
395
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
329
396
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, experiments_1.experimentsCommand)({
|
|
330
397
|
action: args.action,
|
|
331
398
|
target: args.target,
|
|
399
|
+
site: args.site,
|
|
400
|
+
id: args.id,
|
|
401
|
+
key: args.key,
|
|
402
|
+
name: args.name,
|
|
403
|
+
description: args.description,
|
|
404
|
+
variants: args.variants,
|
|
405
|
+
goal: args.goal,
|
|
332
406
|
conversionEvent: args['conversion-event'],
|
|
407
|
+
fromFile: args['from-file'],
|
|
408
|
+
dryRun: args['dry-run'],
|
|
409
|
+
yes: args.yes,
|
|
410
|
+
json: args.json,
|
|
411
|
+
profile: args.profile,
|
|
412
|
+
}))
|
|
413
|
+
.command('goals <action> [target]', 'Manage conversion goals (list, show, create, update, delete)', (y) => y
|
|
414
|
+
.positional('action', { type: 'string', describe: 'list | show | create | update | delete' })
|
|
415
|
+
.positional('target', { type: 'string', describe: 'Goal id' })
|
|
416
|
+
.option('site', { type: 'string', describe: 'Site ID (for list/create)' })
|
|
417
|
+
.option('name', { type: 'string', describe: 'Goal name' })
|
|
418
|
+
.option('description', { type: 'string', describe: 'Goal description' })
|
|
419
|
+
.option('event', { type: 'string', describe: 'Event name for the goal' })
|
|
420
|
+
.option('properties-filter', { type: 'string', describe: 'Properties filter (JSON)' })
|
|
421
|
+
.option('from-file', { type: 'string', describe: 'Load payload from JSON file' })
|
|
422
|
+
.option('dry-run', { type: 'boolean', describe: 'Show what would be done' })
|
|
423
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
424
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, goals_1.goalsCommand)({
|
|
425
|
+
action: args.action,
|
|
426
|
+
target: args.target,
|
|
427
|
+
site: args.site,
|
|
428
|
+
name: args.name,
|
|
429
|
+
description: args.description,
|
|
430
|
+
event: args.event,
|
|
431
|
+
propertiesFilter: args['properties-filter'],
|
|
432
|
+
fromFile: args['from-file'],
|
|
433
|
+
dryRun: args['dry-run'],
|
|
434
|
+
yes: args.yes,
|
|
435
|
+
json: args.json,
|
|
436
|
+
profile: args.profile,
|
|
437
|
+
}))
|
|
438
|
+
.command('funnels <action> [target]', 'Manage conversion funnels (list, show, create, update, delete)', (y) => y
|
|
439
|
+
.positional('action', { type: 'string', describe: 'list | show | create | update | delete' })
|
|
440
|
+
.positional('target', { type: 'string', describe: 'Funnel id' })
|
|
441
|
+
.option('site', { type: 'string', describe: 'Site ID (for list/create)' })
|
|
442
|
+
.option('name', { type: 'string', describe: 'Funnel name' })
|
|
443
|
+
.option('description', { type: 'string', describe: 'Funnel description' })
|
|
444
|
+
.option('steps', { type: 'string', describe: 'Comma-separated event names for funnel steps' })
|
|
445
|
+
.option('from-file', { type: 'string', describe: 'Load payload from JSON file' })
|
|
446
|
+
.option('dry-run', { type: 'boolean', describe: 'Show what would be done' })
|
|
447
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
448
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, funnels_1.funnelsCommand)({
|
|
449
|
+
action: args.action,
|
|
450
|
+
target: args.target,
|
|
451
|
+
site: args.site,
|
|
452
|
+
name: args.name,
|
|
453
|
+
description: args.description,
|
|
454
|
+
steps: args.steps,
|
|
455
|
+
fromFile: args['from-file'],
|
|
456
|
+
dryRun: args['dry-run'],
|
|
457
|
+
yes: args.yes,
|
|
333
458
|
json: args.json,
|
|
334
459
|
profile: args.profile,
|
|
335
460
|
}))
|
|
@@ -386,17 +511,25 @@ const db_1 = require("./commands/db");
|
|
|
386
511
|
profile: args.profile,
|
|
387
512
|
}))
|
|
388
513
|
// ── Error tracking — source map upload ────────────────────────────────
|
|
389
|
-
.command('sourcemap <action>', 'Upload source maps for error deobfuscation (upload)', (y) => y
|
|
514
|
+
.command('sourcemap <action>', 'Upload source maps for error deobfuscation (upload). Supports web/server JS .map and native dSYM/ProGuard.', (y) => y
|
|
390
515
|
.positional('action', { type: 'string', describe: 'upload' })
|
|
391
|
-
.option('release', { type: 'string', describe: 'Release version (e.g. v1.0.0)' })
|
|
392
|
-
.option('
|
|
516
|
+
.option('release', { type: 'string', describe: 'Release version (e.g. v1.0.0). Web/server only — alias of --version.' })
|
|
517
|
+
.option('version', { type: 'string', describe: 'App version (e.g. 1.4.2). Required for native uploads.' })
|
|
518
|
+
.option('dir', { type: 'string', describe: 'Directory containing .map files (web/server)' })
|
|
519
|
+
.option('file', { type: 'string', describe: 'Path to dSYM zip (iOS) or mapping.txt (Android)' })
|
|
520
|
+
.option('bundle-id', { type: 'string', describe: 'iOS bundleId or Android applicationId (native only)' })
|
|
393
521
|
.option('site', { type: 'string', describe: 'Site ID' })
|
|
522
|
+
.option('platform', { type: 'string', choices: ['web', 'server', 'ios', 'android'], default: 'web', describe: 'Platform: web, server, ios, or android' })
|
|
394
523
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, sourcemap_1.sourcemapCommand)({
|
|
395
524
|
action: args.action,
|
|
396
525
|
release: args.release,
|
|
526
|
+
version: args.version,
|
|
397
527
|
dir: args.dir,
|
|
528
|
+
file: args.file,
|
|
529
|
+
bundleId: args['bundle-id'],
|
|
398
530
|
site: args.site,
|
|
399
531
|
json: args.json,
|
|
532
|
+
platform: args.platform,
|
|
400
533
|
profile: args.profile,
|
|
401
534
|
}))
|
|
402
535
|
// ── Phase 21 — database connect ────────────────────────────────────────
|
|
@@ -426,6 +559,18 @@ const db_1 = require("./commands/db");
|
|
|
426
559
|
noInteractive: args['no-interactive'],
|
|
427
560
|
json: args.json,
|
|
428
561
|
profile: args.profile,
|
|
562
|
+
}))
|
|
563
|
+
// ── Sprint B Group VII B14 — live event tail ─────────────────────────
|
|
564
|
+
.command('watch', 'Stream events live for the active tenant (SSE)', (y) => y
|
|
565
|
+
.option('site', { type: 'string', describe: 'Filter by site ID' })
|
|
566
|
+
.option('types', { type: 'string', describe: 'Comma-separated event names (e.g. $purchase,page_view)' })
|
|
567
|
+
.option('tail', { type: 'number', describe: 'Show last N events from the past 24h before streaming' })
|
|
568
|
+
.option('json', { type: 'boolean', describe: 'Emit one JSON line per event' }), (args) => (0, watch_1.watchCommand)({
|
|
569
|
+
site: args.site,
|
|
570
|
+
types: args.types,
|
|
571
|
+
tail: args.tail,
|
|
572
|
+
json: args.json,
|
|
573
|
+
profile: args.profile,
|
|
429
574
|
}))
|
|
430
575
|
.demandCommand(1, 'Run gurulu --help for available commands')
|
|
431
576
|
.strict()
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
import { readFileSync, statSync, existsSync } from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { posix as posixPath } from 'node:path';
|
|
19
20
|
|
|
20
21
|
const require = createRequire(import.meta.url);
|
|
21
22
|
const lib = require('./gurulu-agentic-install.lib.cjs');
|
|
@@ -92,6 +93,8 @@ function printHelp(out = process.stdout) {
|
|
|
92
93
|
' --framework <f> auto|nextjs-app|nextjs-pages|vite-react|express',
|
|
93
94
|
' --auto-instrument (Phase 18.7) Also write gurulu.track() calls into route handlers',
|
|
94
95
|
' --intent-result <path> JSON file with InstallIntent output (feeds --auto-instrument)',
|
|
96
|
+
' --token <key> CLI auth token for LLM property extraction API',
|
|
97
|
+
' --api-url <url> Base URL for the Gurulu API (default: https://gurulu.io)',
|
|
95
98
|
' --quiet Suppress non-error output',
|
|
96
99
|
' -h, --help Show this help',
|
|
97
100
|
'',
|
|
@@ -109,6 +112,156 @@ async function readScanInput(scanFile) {
|
|
|
109
112
|
return JSON.parse(readFileSync(scanFile, 'utf8'));
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Return a placeholder JS expression for a property that lacks an example
|
|
117
|
+
* value. The placeholder includes a TODO comment so developers know to fill
|
|
118
|
+
* in the real expression from their route handler context.
|
|
119
|
+
*/
|
|
120
|
+
function getPropertyPlaceholder(type, name) {
|
|
121
|
+
switch (type) {
|
|
122
|
+
case 'string': return `'' /* TODO: ${name} */`;
|
|
123
|
+
case 'number': return `0 /* TODO: ${name} */`;
|
|
124
|
+
case 'boolean': return `false /* TODO: ${name} */`;
|
|
125
|
+
default: return `undefined /* TODO: ${name} */`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Route resolution: map "POST /api/checkout" → actual file path on disk.
|
|
131
|
+
// Reuses the candidate patterns from the auto-instrument modules.
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
function parseRouteString(routeStr) {
|
|
135
|
+
if (!routeStr || typeof routeStr !== 'string') return null;
|
|
136
|
+
const m = routeStr.trim().match(/^([A-Z]+)\s+(\/.*)$/);
|
|
137
|
+
if (!m) return null;
|
|
138
|
+
return { method: m[1].toUpperCase(), urlPath: m[2].replace(/\/+$/, '').split('?')[0] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveRouteFile(routeStr, framework, repoRoot) {
|
|
142
|
+
const parsed = parseRouteString(routeStr);
|
|
143
|
+
if (!parsed) return null;
|
|
144
|
+
const { urlPath } = parsed;
|
|
145
|
+
const segments = urlPath.split('/').filter(Boolean);
|
|
146
|
+
|
|
147
|
+
// Build candidate file paths based on framework.
|
|
148
|
+
const candidates = [];
|
|
149
|
+
|
|
150
|
+
// Next.js App Router patterns.
|
|
151
|
+
if (!framework || /next/i.test(framework)) {
|
|
152
|
+
const exts = ['ts', 'tsx', 'js', 'jsx'];
|
|
153
|
+
for (const ext of exts) {
|
|
154
|
+
candidates.push(posixPath.join('src', 'app', ...segments, `route.${ext}`));
|
|
155
|
+
candidates.push(posixPath.join('app', ...segments, `route.${ext}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Next.js Pages API patterns.
|
|
160
|
+
if (!framework || /next.*pages/i.test(framework)) {
|
|
161
|
+
const exts = ['ts', 'tsx', 'js', 'jsx'];
|
|
162
|
+
for (const ext of exts) {
|
|
163
|
+
candidates.push(posixPath.join('src', 'pages', ...segments, `index.${ext}`));
|
|
164
|
+
candidates.push(posixPath.join('pages', ...segments, `index.${ext}`));
|
|
165
|
+
candidates.push(posixPath.join('src', 'pages', ...segments.slice(0, -1), `${segments[segments.length - 1]}.${ext}`));
|
|
166
|
+
candidates.push(posixPath.join('pages', ...segments.slice(0, -1), `${segments[segments.length - 1]}.${ext}`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Express / Fastify / generic patterns.
|
|
171
|
+
if (!framework || /express|fastify|nest|generic/i.test(framework)) {
|
|
172
|
+
const roots = ['src/routes', 'routes', 'src/api', 'api', 'src/controllers', 'controllers', 'src', 'app'];
|
|
173
|
+
const exts = ['ts', 'js', 'mjs', 'cjs'];
|
|
174
|
+
for (const root of roots) {
|
|
175
|
+
for (const ext of exts) {
|
|
176
|
+
// e.g. src/routes/checkout.ts
|
|
177
|
+
candidates.push(posixPath.join(root, ...segments.slice(1), `index.${ext}`));
|
|
178
|
+
if (segments.length > 1) {
|
|
179
|
+
candidates.push(posixPath.join(root, ...segments.slice(1, -1), `${segments[segments.length - 1]}.${ext}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// SvelteKit patterns.
|
|
186
|
+
if (!framework || /svelte/i.test(framework)) {
|
|
187
|
+
const exts = ['ts', 'js'];
|
|
188
|
+
for (const ext of exts) {
|
|
189
|
+
candidates.push(posixPath.join('src', 'routes', ...segments, `+server.${ext}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Remix patterns.
|
|
194
|
+
if (!framework || /remix/i.test(framework)) {
|
|
195
|
+
const exts = ['ts', 'tsx', 'js', 'jsx'];
|
|
196
|
+
for (const ext of exts) {
|
|
197
|
+
candidates.push(posixPath.join('app', 'routes', segments.join('.') + `.${ext}`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find first existing candidate.
|
|
202
|
+
for (const rel of candidates) {
|
|
203
|
+
const abs = path.join(repoRoot, rel);
|
|
204
|
+
if (existsSync(abs)) return abs;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// LLM property extraction via the server API endpoint.
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
async function tryLlmExtraction(events, { repoRoot, framework, ingestUrl, token, siteId, log }) {
|
|
215
|
+
let llmHits = 0;
|
|
216
|
+
for (const event of events) {
|
|
217
|
+
// Skip if already has extracted properties.
|
|
218
|
+
if (event.extractedProperties && event.extractedProperties.length > 0) continue;
|
|
219
|
+
|
|
220
|
+
const routeStr = event.source && event.source.route;
|
|
221
|
+
if (!routeStr) continue;
|
|
222
|
+
|
|
223
|
+
const routeFile = resolveRouteFile(routeStr, framework, repoRoot);
|
|
224
|
+
if (!routeFile) continue;
|
|
225
|
+
|
|
226
|
+
let handlerSource;
|
|
227
|
+
try {
|
|
228
|
+
handlerSource = readFileSync(routeFile, 'utf-8');
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Cap at 4KB to keep LLM calls reasonable.
|
|
234
|
+
const clipped = handlerSource.slice(0, 4096);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(`${ingestUrl}/api/cli/install/extract-properties`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: {
|
|
240
|
+
Authorization: `Bearer ${token}`,
|
|
241
|
+
'Content-Type': 'application/json',
|
|
242
|
+
},
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
siteId,
|
|
245
|
+
eventName: event.name,
|
|
246
|
+
handlerSource: clipped,
|
|
247
|
+
framework,
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
if (res.ok) {
|
|
251
|
+
const data = await res.json();
|
|
252
|
+
if (data.properties && data.properties.length > 0 && data.confidence >= 0.7) {
|
|
253
|
+
event.extractedProperties = data.properties;
|
|
254
|
+
llmHits++;
|
|
255
|
+
log(` LLM extracted ${data.properties.length} property(ies) for ${event.name} (confidence: ${data.confidence.toFixed(2)})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// LLM extraction failed — fall through to placeholder.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return llmHits;
|
|
263
|
+
}
|
|
264
|
+
|
|
112
265
|
async function runPatchMode(repoRoot, args) {
|
|
113
266
|
const log = args.quiet ? () => {} : (m) => process.stdout.write(m + '\n');
|
|
114
267
|
if (args.rollback) {
|
|
@@ -160,6 +313,41 @@ async function runPatchMode(repoRoot, args) {
|
|
|
160
313
|
const intent = JSON.parse(readFileSync(args['intent-result'], 'utf8'));
|
|
161
314
|
const acceptedEvents = (intent && intent.accepted && intent.accepted.events) || [];
|
|
162
315
|
const frameworkName = (patcher && patcher.name) || framework;
|
|
316
|
+
|
|
317
|
+
// Phase 20 — Try LLM-based property extraction first.
|
|
318
|
+
const ingestUrl = args['ingest-url'] || args['api-url'] || 'https://gurulu.io';
|
|
319
|
+
const token = args['token'] || args['api-key'] || '';
|
|
320
|
+
const siteId = args['site-id'] || '';
|
|
321
|
+
if (token) {
|
|
322
|
+
try {
|
|
323
|
+
const hits = await tryLlmExtraction(acceptedEvents, {
|
|
324
|
+
repoRoot,
|
|
325
|
+
framework: frameworkName,
|
|
326
|
+
ingestUrl,
|
|
327
|
+
token,
|
|
328
|
+
siteId,
|
|
329
|
+
log,
|
|
330
|
+
});
|
|
331
|
+
if (hits > 0) log(`LLM property extraction: ${hits} event(s) enriched`);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
log(`LLM property extraction failed (using placeholders): ${err.message || err}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Placeholder fallback for events that LLM didn't cover.
|
|
338
|
+
for (const event of acceptedEvents) {
|
|
339
|
+
if (event.extractedProperties && event.extractedProperties.length > 0) continue;
|
|
340
|
+
if (!event.propertySchema || !event.propertySchema.length) continue;
|
|
341
|
+
event.extractedProperties = event.propertySchema
|
|
342
|
+
.filter((p) => p.required)
|
|
343
|
+
.map((p) => ({
|
|
344
|
+
name: p.name,
|
|
345
|
+
type: p.type || 'unknown',
|
|
346
|
+
source: p.example !== undefined && p.example !== null
|
|
347
|
+
? JSON.stringify(p.example)
|
|
348
|
+
: getPropertyPlaceholder(p.type, p.name),
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
163
351
|
const aiResult = patches.autoInstrumentDispatch(
|
|
164
352
|
frameworkName,
|
|
165
353
|
{ repoRoot },
|
|
@@ -212,7 +400,44 @@ async function runPatchMode(repoRoot, args) {
|
|
|
212
400
|
log('Auto-instrument: no accepted events to instrument');
|
|
213
401
|
return;
|
|
214
402
|
}
|
|
403
|
+
|
|
215
404
|
const frameworkName = (patcher && patcher.name) || framework;
|
|
405
|
+
|
|
406
|
+
// Phase 20 — Try LLM-based property extraction first.
|
|
407
|
+
const ingestUrl = args['ingest-url'] || args['api-url'] || 'https://gurulu.io';
|
|
408
|
+
const token = args['token'] || args['api-key'] || '';
|
|
409
|
+
const siteId = args['site-id'] || '';
|
|
410
|
+
if (token) {
|
|
411
|
+
try {
|
|
412
|
+
const hits = await tryLlmExtraction(acceptedEvents, {
|
|
413
|
+
repoRoot,
|
|
414
|
+
framework: frameworkName,
|
|
415
|
+
ingestUrl,
|
|
416
|
+
token,
|
|
417
|
+
siteId,
|
|
418
|
+
log,
|
|
419
|
+
});
|
|
420
|
+
if (hits > 0) log(`LLM property extraction: ${hits} event(s) enriched`);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
log(`LLM property extraction failed (using placeholders): ${err.message || err}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Placeholder fallback: enrich events with extractedProperties from
|
|
427
|
+
// propertySchema for any events the LLM didn't cover.
|
|
428
|
+
for (const event of acceptedEvents) {
|
|
429
|
+
if (event.extractedProperties && event.extractedProperties.length > 0) continue;
|
|
430
|
+
if (!event.propertySchema || !event.propertySchema.length) continue;
|
|
431
|
+
event.extractedProperties = event.propertySchema
|
|
432
|
+
.filter((p) => p.required)
|
|
433
|
+
.map((p) => ({
|
|
434
|
+
name: p.name,
|
|
435
|
+
type: p.type || 'unknown',
|
|
436
|
+
source: p.example !== undefined && p.example !== null
|
|
437
|
+
? JSON.stringify(p.example)
|
|
438
|
+
: getPropertyPlaceholder(p.type, p.name),
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
216
441
|
const aiResult = patches.autoInstrumentDispatch(
|
|
217
442
|
frameworkName,
|
|
218
443
|
{ repoRoot },
|