@whisperr/wizard 0.1.6 → 0.1.8
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/dist/index.js +481 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -200,6 +200,19 @@ async function detect3(ctx) {
|
|
|
200
200
|
let confidence = 0.4;
|
|
201
201
|
if (dependsOn(pkgJson, ["next"])) return null;
|
|
202
202
|
if (dependsOn(pkgJson, ["react-native", "expo"])) return null;
|
|
203
|
+
const hasUi = dependsOn(pkgJson, ["react", "vue", "svelte", "@angular/core"]) || await ctx.exists("index.html");
|
|
204
|
+
const isBackend = dependsOn(pkgJson, [
|
|
205
|
+
"express",
|
|
206
|
+
"fastify",
|
|
207
|
+
"koa",
|
|
208
|
+
"@nestjs/core",
|
|
209
|
+
"hapi",
|
|
210
|
+
"@hapi/hapi",
|
|
211
|
+
"restify",
|
|
212
|
+
"h3",
|
|
213
|
+
"@adonisjs/core"
|
|
214
|
+
]);
|
|
215
|
+
if (isBackend && !hasUi) return null;
|
|
203
216
|
if (dependsOn(pkgJson, ["react", "vue", "svelte", "@angular/core"])) {
|
|
204
217
|
evidence.push("frontend framework dependency");
|
|
205
218
|
confidence += 0.4;
|
|
@@ -255,14 +268,372 @@ var webPlaybook = {
|
|
|
255
268
|
verifyCommand: void 0
|
|
256
269
|
};
|
|
257
270
|
|
|
258
|
-
// src/core/playbooks/
|
|
271
|
+
// src/core/playbooks/node.ts
|
|
259
272
|
var target4 = {
|
|
273
|
+
id: "node",
|
|
274
|
+
displayName: "Node.js (backend)",
|
|
275
|
+
language: "typescript",
|
|
276
|
+
availability: "available"
|
|
277
|
+
};
|
|
278
|
+
function dependsOn2(pkgJson, names) {
|
|
279
|
+
try {
|
|
280
|
+
const pkg = JSON.parse(pkgJson);
|
|
281
|
+
const all = {
|
|
282
|
+
...pkg.dependencies ?? {},
|
|
283
|
+
...pkg.devDependencies ?? {}
|
|
284
|
+
};
|
|
285
|
+
return names.some((n) => n in all);
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
var BACKEND_FRAMEWORKS = [
|
|
291
|
+
"express",
|
|
292
|
+
"fastify",
|
|
293
|
+
"koa",
|
|
294
|
+
"@nestjs/core",
|
|
295
|
+
"hapi",
|
|
296
|
+
"@hapi/hapi",
|
|
297
|
+
"restify",
|
|
298
|
+
"hono",
|
|
299
|
+
"h3",
|
|
300
|
+
"@adonisjs/core"
|
|
301
|
+
];
|
|
302
|
+
var FRONTEND_FRAMEWORKS = [
|
|
303
|
+
"react",
|
|
304
|
+
"vue",
|
|
305
|
+
"svelte",
|
|
306
|
+
"@angular/core",
|
|
307
|
+
"next",
|
|
308
|
+
"react-native",
|
|
309
|
+
"expo"
|
|
310
|
+
];
|
|
311
|
+
var SERVER_ENTRYPOINTS = [
|
|
312
|
+
"server.js",
|
|
313
|
+
"server.ts",
|
|
314
|
+
"src/server.ts",
|
|
315
|
+
"src/server.js",
|
|
316
|
+
"app.js",
|
|
317
|
+
"src/app.ts",
|
|
318
|
+
"src/index.ts",
|
|
319
|
+
"index.js"
|
|
320
|
+
];
|
|
321
|
+
async function detect4(ctx) {
|
|
322
|
+
if (!await ctx.exists("package.json")) return null;
|
|
323
|
+
const pkgJson = await ctx.read("package.json");
|
|
324
|
+
const evidence = ["package.json"];
|
|
325
|
+
let confidence = 0;
|
|
326
|
+
if (dependsOn2(pkgJson, BACKEND_FRAMEWORKS)) {
|
|
327
|
+
evidence.push("backend framework dependency");
|
|
328
|
+
confidence += 0.7;
|
|
329
|
+
}
|
|
330
|
+
const hasFrontend = dependsOn2(pkgJson, FRONTEND_FRAMEWORKS);
|
|
331
|
+
const hasIndexHtml = await ctx.exists("index.html");
|
|
332
|
+
if ((hasFrontend || hasIndexHtml) && confidence === 0) return null;
|
|
333
|
+
for (const f of SERVER_ENTRYPOINTS) {
|
|
334
|
+
if (await ctx.exists(f)) {
|
|
335
|
+
evidence.push(f);
|
|
336
|
+
confidence += 0.15;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (confidence === 0 && !hasFrontend && !hasIndexHtml) {
|
|
341
|
+
try {
|
|
342
|
+
const pkg = JSON.parse(pkgJson);
|
|
343
|
+
const start = String(pkg.scripts?.start ?? "");
|
|
344
|
+
if (/\b(node|nodemon|ts-node|tsx)\b/.test(start)) {
|
|
345
|
+
evidence.push("node start script");
|
|
346
|
+
confidence += 0.5;
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (confidence === 0) return null;
|
|
352
|
+
return { target: target4, confidence: Math.min(confidence, 0.95), evidence };
|
|
353
|
+
}
|
|
354
|
+
var systemPrompt4 = `
|
|
355
|
+
## SDK: Whisperr for Node (server-side) \u2014 package \`@whisperr/node\` (+ \`@whisperr/node/express\` for Express)
|
|
356
|
+
|
|
357
|
+
This is a BACKEND surface. Instrument events at the real SERVER-SIDE moment they
|
|
358
|
+
happen \u2014 inside request handlers, webhook handlers, billing logic, queue/cron
|
|
359
|
+
jobs, and service methods. The events in your plan are the ones that originate on
|
|
360
|
+
the server (payments, subscription/lifecycle changes, server-computed
|
|
361
|
+
thresholds); they will NOT be in any UI. Do not look for click/screen handlers.
|
|
362
|
+
|
|
363
|
+
The ONE thing that's different from the browser SDK: \`track()\` and
|
|
364
|
+
\`identify()\` take the end-user id as the FIRST argument \u2014 the server has no
|
|
365
|
+
persisted session to infer it from. Source the real user id at each call site.
|
|
366
|
+
|
|
367
|
+
1) Install (detect pnpm/yarn/npm from the lockfile):
|
|
368
|
+
<pm> add @whisperr/node
|
|
369
|
+
|
|
370
|
+
2) Create one shared client module, e.g. src/whisperr.ts (or .js):
|
|
371
|
+
import { createWhisperr } from '@whisperr/node';
|
|
372
|
+
export const whisperr = createWhisperr({
|
|
373
|
+
apiKey: process.env.WHISPERR_API_KEY!,
|
|
374
|
+
baseUrl: '<INGESTION_BASE_URL from manifest>', // omit if it's the default
|
|
375
|
+
});
|
|
376
|
+
Put WHISPERR_API_KEY in the app's server config and add it to .env /
|
|
377
|
+
.env.example. This is a SERVER env var \u2014 never a public/VITE_/NEXT_PUBLIC_ one.
|
|
378
|
+
|
|
379
|
+
3) identify(externalUserId, { ... }). Call where you already know the user \u2014
|
|
380
|
+
after signup, or when user-level traits/contact channels change:
|
|
381
|
+
whisperr.identify(user.id, { traits: { plan: user.plan }, email: user.email });
|
|
382
|
+
Use the same stable id you use everywhere else for that user, so server and
|
|
383
|
+
client events land on one timeline.
|
|
384
|
+
|
|
385
|
+
4) track(externalUserId, eventType, properties). For each plan event, find the
|
|
386
|
+
real server-side moment and source the user id from what's in scope:
|
|
387
|
+
- In a request handler: whisperr.track(req.user.id, 'plan_upgraded', { plan });
|
|
388
|
+
- In a webhook/cron/job: there's no request \u2014 get the id from the domain
|
|
389
|
+
object (e.g. subscription.userId, or map the Stripe customer to your user):
|
|
390
|
+
whisperr.track(subscription.userId, 'subscription_cancelled', { reason });
|
|
391
|
+
Copy event_type verbatim (snake_case) from the plan. If you genuinely can't
|
|
392
|
+
source a user id at the only sensible site, leave a one-line
|
|
393
|
+
\`// TODO(whisperr): need user id to track <event>\` and move on.
|
|
394
|
+
|
|
395
|
+
5) Express only \u2014 if this app uses Express, mount the adapter AFTER auth so the
|
|
396
|
+
user is resolvable, then use the per-request helper at call sites:
|
|
397
|
+
import { whisperrExpress } from '@whisperr/node/express';
|
|
398
|
+
app.use(whisperrExpress(whisperr)); // pass { resolveUser } if your auth isn't req.user.id
|
|
399
|
+
// ...later, in a route:
|
|
400
|
+
req.whisperr.track('payment_failed', { amount_cents });
|
|
401
|
+
|
|
402
|
+
6) Lifecycle. Flush on graceful shutdown so queued events aren't lost:
|
|
403
|
+
process.on('SIGTERM', () => { whisperr.shutdown(); });
|
|
404
|
+
In serverless handlers (Lambda/Vercel/Cloud Functions), \`await whisperr.flush()\`
|
|
405
|
+
before returning, since the runtime freezes after the response.
|
|
406
|
+
|
|
407
|
+
Notes / gotchas:
|
|
408
|
+
- The client queues + batches + retries automatically. Fire-and-forget is fine;
|
|
409
|
+
do NOT await track()/identify() \u2014 only await flush()/shutdown().
|
|
410
|
+
- track()/identify() no-op silently if the user id is empty \u2014 make sure you pass
|
|
411
|
+
a real id.
|
|
412
|
+
- One client instance per process (the exported singleton). Don't create a new
|
|
413
|
+
client per request.
|
|
414
|
+
`.trim();
|
|
415
|
+
var nodePlaybook = {
|
|
416
|
+
target: target4,
|
|
417
|
+
detect: detect4,
|
|
418
|
+
packageRef: "@whisperr/node (npm)",
|
|
419
|
+
systemPrompt: systemPrompt4,
|
|
420
|
+
verifyCommand: void 0
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/core/playbooks/python.ts
|
|
424
|
+
var target5 = {
|
|
425
|
+
id: "python",
|
|
426
|
+
displayName: "Python (backend)",
|
|
427
|
+
language: "python",
|
|
428
|
+
availability: "available"
|
|
429
|
+
};
|
|
430
|
+
var PROJECT_FILES = [
|
|
431
|
+
"pyproject.toml",
|
|
432
|
+
"requirements.txt",
|
|
433
|
+
"setup.py",
|
|
434
|
+
"setup.cfg",
|
|
435
|
+
"Pipfile"
|
|
436
|
+
];
|
|
437
|
+
var WEB_FRAMEWORKS = [
|
|
438
|
+
"django",
|
|
439
|
+
"flask",
|
|
440
|
+
"fastapi",
|
|
441
|
+
"starlette",
|
|
442
|
+
"sanic",
|
|
443
|
+
"aiohttp",
|
|
444
|
+
"tornado",
|
|
445
|
+
"bottle",
|
|
446
|
+
"falcon",
|
|
447
|
+
"litestar"
|
|
448
|
+
];
|
|
449
|
+
async function detect5(ctx) {
|
|
450
|
+
const present = [];
|
|
451
|
+
for (const f of PROJECT_FILES) {
|
|
452
|
+
if (await ctx.exists(f)) present.push(f);
|
|
453
|
+
}
|
|
454
|
+
const hasManage = await ctx.exists("manage.py");
|
|
455
|
+
if (present.length === 0 && !hasManage) return null;
|
|
456
|
+
const evidence = [...present];
|
|
457
|
+
let confidence = present.length ? 0.5 : 0.3;
|
|
458
|
+
let deps = "";
|
|
459
|
+
for (const f of present) deps += (await ctx.read(f)).toLowerCase() + "\n";
|
|
460
|
+
if (WEB_FRAMEWORKS.some((fw) => deps.includes(fw))) {
|
|
461
|
+
evidence.push("python web framework");
|
|
462
|
+
confidence += 0.35;
|
|
463
|
+
}
|
|
464
|
+
if (hasManage) {
|
|
465
|
+
evidence.push("manage.py");
|
|
466
|
+
confidence += 0.2;
|
|
467
|
+
}
|
|
468
|
+
return { target: target5, confidence: Math.min(confidence, 0.95), evidence };
|
|
469
|
+
}
|
|
470
|
+
var systemPrompt5 = `
|
|
471
|
+
## SDK: Whisperr for Python (server-side) \u2014 package \`whisperr\` (Django middleware in \`whisperr.django\`)
|
|
472
|
+
|
|
473
|
+
This is a BACKEND surface. Instrument events at the real SERVER-SIDE moment they
|
|
474
|
+
happen \u2014 inside views/request handlers, webhook handlers, billing logic, Celery
|
|
475
|
+
tasks, and management commands. The events in your plan originate on the server
|
|
476
|
+
(payments, subscription/lifecycle changes, server-computed thresholds); they will
|
|
477
|
+
NOT be in any UI. Do not look for click/screen handlers.
|
|
478
|
+
|
|
479
|
+
The ONE thing that's different from a browser SDK: \`track()\` and \`identify()\`
|
|
480
|
+
take the end-user id as the FIRST argument \u2014 the server has no persisted session
|
|
481
|
+
to infer it from. Source the real user id at each call site.
|
|
482
|
+
|
|
483
|
+
1) Install (detect the project's tool \u2014 pip/poetry/pipenv/uv from the lockfile or
|
|
484
|
+
pyproject): add \`whisperr\` (use \`whisperr[django]\` on a Django project).
|
|
485
|
+
|
|
486
|
+
2) Create one shared client, e.g. app/whisperr_client.py:
|
|
487
|
+
import os
|
|
488
|
+
from whisperr import Whisperr
|
|
489
|
+
whisperr = Whisperr(
|
|
490
|
+
api_key=os.environ["WHISPERR_API_KEY"],
|
|
491
|
+
base_url="<INGESTION_BASE_URL from manifest>", # omit if it's the default
|
|
492
|
+
)
|
|
493
|
+
Put WHISPERR_API_KEY in the app's server config / .env (server-side secret).
|
|
494
|
+
|
|
495
|
+
3) identify(external_user_id, ...). Where you already know the user (signup, or
|
|
496
|
+
when user-level traits/contact channels change):
|
|
497
|
+
whisperr.identify(user.id, traits={"plan": user.plan}, email=user.email)
|
|
498
|
+
|
|
499
|
+
4) track(external_user_id, event_type, properties). Find the real server-side
|
|
500
|
+
moment and source the user id from what's in scope:
|
|
501
|
+
- In a handler/view: whisperr.track(request.user.id, "plan_upgraded", {"plan": plan})
|
|
502
|
+
- In a webhook/task: there's no request \u2014 get the id from the domain object
|
|
503
|
+
(e.g. subscription.user_id, or map the Stripe customer to your user):
|
|
504
|
+
whisperr.track(subscription.user_id, "subscription_cancelled", {"reason": reason})
|
|
505
|
+
Copy event_type verbatim (snake_case) from the plan. If you genuinely can't
|
|
506
|
+
source a user id at the only sensible site, leave a one-line
|
|
507
|
+
\`# TODO(whisperr): need user id to track <event>\` and move on.
|
|
508
|
+
|
|
509
|
+
5) Django only \u2014 register the middleware AFTER the auth middleware in settings.py,
|
|
510
|
+
then use the per-request helper in views:
|
|
511
|
+
# settings.py
|
|
512
|
+
WHISPERR_API_KEY = os.environ["WHISPERR_API_KEY"]
|
|
513
|
+
MIDDLEWARE = [ ..., "django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
514
|
+
"whisperr.django.WhisperrMiddleware" ]
|
|
515
|
+
# views.py
|
|
516
|
+
request.whisperr.track("payment_failed", {"amount_cents": amount})
|
|
517
|
+
For Celery/webhook/management-command code (no request), use the shared client
|
|
518
|
+
or whisperr.django.get_client() with the user id from your data.
|
|
519
|
+
|
|
520
|
+
6) Lifecycle. The client flushes in the background and on interpreter exit
|
|
521
|
+
(atexit). In short-lived processes (management commands, scripts, serverless),
|
|
522
|
+
call \`whisperr.flush()\` before returning so queued events aren't lost.
|
|
523
|
+
|
|
524
|
+
Notes / gotchas:
|
|
525
|
+
- The client queues + batches + retries automatically on a daemon thread. Fire-
|
|
526
|
+
and-forget is fine; do NOT block on track()/identify() \u2014 only flush()/shutdown().
|
|
527
|
+
- track()/identify() no-op silently if the user id is empty \u2014 pass a real id.
|
|
528
|
+
- One client instance per process. Don't construct a new client per request.
|
|
529
|
+
`.trim();
|
|
530
|
+
var pythonPlaybook = {
|
|
531
|
+
target: target5,
|
|
532
|
+
detect: detect5,
|
|
533
|
+
packageRef: "whisperr (PyPI)",
|
|
534
|
+
systemPrompt: systemPrompt5,
|
|
535
|
+
verifyCommand: void 0
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/core/playbooks/php.ts
|
|
539
|
+
var target6 = {
|
|
540
|
+
id: "php",
|
|
541
|
+
displayName: "PHP (backend)",
|
|
542
|
+
language: "php",
|
|
543
|
+
availability: "available"
|
|
544
|
+
};
|
|
545
|
+
var FRAMEWORKS = [
|
|
546
|
+
"laravel/framework",
|
|
547
|
+
"symfony/",
|
|
548
|
+
"slim/slim",
|
|
549
|
+
"laminas/",
|
|
550
|
+
"cakephp/",
|
|
551
|
+
"yiisoft/",
|
|
552
|
+
"codeigniter4/"
|
|
553
|
+
];
|
|
554
|
+
async function detect6(ctx) {
|
|
555
|
+
if (!await ctx.exists("composer.json")) return null;
|
|
556
|
+
const composer = (await ctx.read("composer.json")).toLowerCase();
|
|
557
|
+
const evidence = ["composer.json"];
|
|
558
|
+
let confidence = 0.5;
|
|
559
|
+
if (FRAMEWORKS.some((f) => composer.includes(f))) {
|
|
560
|
+
evidence.push("php framework");
|
|
561
|
+
confidence += 0.35;
|
|
562
|
+
}
|
|
563
|
+
if (await ctx.exists("artisan")) {
|
|
564
|
+
evidence.push("artisan (Laravel)");
|
|
565
|
+
confidence += 0.15;
|
|
566
|
+
}
|
|
567
|
+
return { target: target6, confidence: Math.min(confidence, 0.95), evidence };
|
|
568
|
+
}
|
|
569
|
+
var systemPrompt6 = `
|
|
570
|
+
## SDK: Whisperr for PHP (server-side) \u2014 package \`whisperr/php\` (Laravel facade + service provider included)
|
|
571
|
+
|
|
572
|
+
This is a BACKEND surface. Instrument events at the real SERVER-SIDE moment they
|
|
573
|
+
happen \u2014 inside controllers/request handlers, webhook handlers, billing logic,
|
|
574
|
+
queued jobs, and console commands. The events in your plan originate on the
|
|
575
|
+
server (payments, subscription/lifecycle changes, server-computed thresholds);
|
|
576
|
+
they will NOT be in any UI. Do not look for click handlers / Blade views.
|
|
577
|
+
|
|
578
|
+
The ONE thing that's different from a browser SDK: \`track()\` and \`identify()\`
|
|
579
|
+
take the end-user id as the FIRST argument \u2014 the server has no session to infer
|
|
580
|
+
it from. Source the real user id at each call site.
|
|
581
|
+
|
|
582
|
+
1) Install:
|
|
583
|
+
composer require whisperr/php
|
|
584
|
+
|
|
585
|
+
2) Config. Set the key in the environment (.env), never hardcoded:
|
|
586
|
+
WHISPERR_API_KEY=<INGESTION_API_KEY from manifest>
|
|
587
|
+
On Laravel the service provider + \`Whisperr\` facade auto-register (package
|
|
588
|
+
discovery). Optionally publish config: \`php artisan vendor:publish --tag=whisperr-config\`.
|
|
589
|
+
On non-Laravel PHP, construct one client and reuse it:
|
|
590
|
+
use Whisperr\\Whisperr;
|
|
591
|
+
$whisperr = new Whisperr(['api_key' => getenv('WHISPERR_API_KEY')]);
|
|
592
|
+
|
|
593
|
+
3) identify(externalUserId, [...]). Where you already know the user (after login/
|
|
594
|
+
signup, or when user-level traits/contact channels change):
|
|
595
|
+
use Whisperr\\Laravel\\Facades\\Whisperr;
|
|
596
|
+
Whisperr::identify(auth()->id(), ['traits' => ['plan' => $user->plan], 'email' => $user->email]);
|
|
597
|
+
|
|
598
|
+
4) track(externalUserId, eventType, properties). Find the real server-side moment
|
|
599
|
+
and source the user id from what's in scope:
|
|
600
|
+
- In a controller: Whisperr::track(auth()->id(), 'plan_upgraded', ['plan' => $plan]);
|
|
601
|
+
- In a webhook/job: no auth() \u2014 get the id from the domain object
|
|
602
|
+
(e.g. $subscription->user_id, or map the Stripe customer to your user):
|
|
603
|
+
Whisperr::track($subscription->user_id, 'subscription_cancelled', ['reason' => $reason]);
|
|
604
|
+
Copy event_type verbatim (snake_case) from the plan. If you genuinely can't
|
|
605
|
+
source a user id at the only sensible site, leave a one-line
|
|
606
|
+
\`// TODO(whisperr): need user id to track <event>\` and move on.
|
|
607
|
+
On plain PHP use \`$whisperr->track(...)\` on your shared instance instead of the facade.
|
|
608
|
+
|
|
609
|
+
5) Lifecycle. On Laravel the buffered events flush automatically AFTER the
|
|
610
|
+
response is sent (the service provider hooks \`terminating\`), so tracking adds
|
|
611
|
+
no response latency. The client also flushes on shutdown. In short-lived
|
|
612
|
+
scripts/commands call \`Whisperr::flush()\` (or \`$whisperr->flush()\`) before exit.
|
|
613
|
+
|
|
614
|
+
Notes / gotchas:
|
|
615
|
+
- Events buffer in memory during the request and send in a batch on flush. Fire-
|
|
616
|
+
and-forget is fine; don't block on track()/identify().
|
|
617
|
+
- track()/identify() no-op silently if the user id is empty \u2014 pass a real id
|
|
618
|
+
(guard webhook code where auth() is null).
|
|
619
|
+
- Use the same stable user id everywhere so server + client events line up.
|
|
620
|
+
`.trim();
|
|
621
|
+
var phpPlaybook = {
|
|
622
|
+
target: target6,
|
|
623
|
+
detect: detect6,
|
|
624
|
+
packageRef: "whisperr/php (Packagist)",
|
|
625
|
+
systemPrompt: systemPrompt6,
|
|
626
|
+
verifyCommand: void 0
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// src/core/playbooks/react-native.ts
|
|
630
|
+
var target7 = {
|
|
260
631
|
id: "react-native",
|
|
261
632
|
displayName: "React Native",
|
|
262
633
|
language: "typescript",
|
|
263
634
|
availability: "planned"
|
|
264
635
|
};
|
|
265
|
-
async function
|
|
636
|
+
async function detect7(ctx) {
|
|
266
637
|
if (!await ctx.exists("package.json")) return null;
|
|
267
638
|
const pkgJson = await ctx.read("package.json");
|
|
268
639
|
let match = false;
|
|
@@ -280,9 +651,9 @@ async function detect4(ctx) {
|
|
|
280
651
|
evidence.push("app.json");
|
|
281
652
|
confidence += 0.1;
|
|
282
653
|
}
|
|
283
|
-
return { target:
|
|
654
|
+
return { target: target7, confidence: Math.min(confidence, 1), evidence };
|
|
284
655
|
}
|
|
285
|
-
var
|
|
656
|
+
var systemPrompt7 = `
|
|
286
657
|
## SDK: Whisperr for React Native \u2014 package \`@whisperr/react-native\`
|
|
287
658
|
|
|
288
659
|
1) Install \`@whisperr/react-native\` with the repo's package manager. If it has
|
|
@@ -305,20 +676,20 @@ Notes:
|
|
|
305
676
|
if one exists; otherwise a constants file is acceptable.
|
|
306
677
|
`.trim();
|
|
307
678
|
var reactNativePlaybook = {
|
|
308
|
-
target:
|
|
309
|
-
detect:
|
|
679
|
+
target: target7,
|
|
680
|
+
detect: detect7,
|
|
310
681
|
packageRef: "@whisperr/react-native (npm)",
|
|
311
|
-
systemPrompt:
|
|
682
|
+
systemPrompt: systemPrompt7
|
|
312
683
|
};
|
|
313
684
|
|
|
314
685
|
// src/core/playbooks/swift.ts
|
|
315
|
-
var
|
|
686
|
+
var target8 = {
|
|
316
687
|
id: "swift",
|
|
317
688
|
displayName: "Swift (iOS)",
|
|
318
689
|
language: "swift",
|
|
319
690
|
availability: "planned"
|
|
320
691
|
};
|
|
321
|
-
async function
|
|
692
|
+
async function detect8(ctx) {
|
|
322
693
|
const evidence = [];
|
|
323
694
|
let confidence = 0;
|
|
324
695
|
if (await ctx.exists("Package.swift")) {
|
|
@@ -339,9 +710,9 @@ async function detect5(ctx) {
|
|
|
339
710
|
confidence += 0.2;
|
|
340
711
|
}
|
|
341
712
|
if (confidence === 0) return null;
|
|
342
|
-
return { target:
|
|
713
|
+
return { target: target8, confidence: Math.min(confidence, 1), evidence };
|
|
343
714
|
}
|
|
344
|
-
var
|
|
715
|
+
var systemPrompt8 = `
|
|
345
716
|
## SDK: Whisperr for Swift (iOS) \u2014 Swift Package \`Whisperr\`
|
|
346
717
|
|
|
347
718
|
1) Add the Swift Package dependency (github.com/WhisperrAI/whisperr-swift). If the
|
|
@@ -369,10 +740,10 @@ Notes:
|
|
|
369
740
|
- Do not attempt to run xcodebuild; verification is the human's step here.
|
|
370
741
|
`.trim();
|
|
371
742
|
var swiftPlaybook = {
|
|
372
|
-
target:
|
|
373
|
-
detect:
|
|
743
|
+
target: target8,
|
|
744
|
+
detect: detect8,
|
|
374
745
|
packageRef: "Whisperr (Swift Package)",
|
|
375
|
-
systemPrompt:
|
|
746
|
+
systemPrompt: systemPrompt8
|
|
376
747
|
};
|
|
377
748
|
|
|
378
749
|
// src/core/playbooks/index.ts
|
|
@@ -381,6 +752,9 @@ var ALL_PLAYBOOKS = [
|
|
|
381
752
|
nextjsPlaybook,
|
|
382
753
|
reactNativePlaybook,
|
|
383
754
|
webPlaybook,
|
|
755
|
+
nodePlaybook,
|
|
756
|
+
pythonPlaybook,
|
|
757
|
+
phpPlaybook,
|
|
384
758
|
swiftPlaybook
|
|
385
759
|
];
|
|
386
760
|
function playbookByTargetId(id) {
|
|
@@ -614,7 +988,7 @@ in the code. You will NOT find everything \u2014 some events live server-side or
|
|
|
614
988
|
aren't in this codebase at all. That is expected and fine. Get what is here, get
|
|
615
989
|
it right, report the rest. Know exactly what you're doing; never hunt blindly.
|
|
616
990
|
|
|
617
|
-
|
|
991
|
+
Every step costs time and the user is watching. Work FAST and DECISIVELY.
|
|
618
992
|
|
|
619
993
|
EFFICIENCY \u2014 non-negotiable:
|
|
620
994
|
- Use Grep and Glob to find code. Do NOT read whole files; read the smallest
|
|
@@ -645,8 +1019,16 @@ CORRECTNESS:
|
|
|
645
1019
|
- Use the EXACT snake_case event names given. Never invent or rename events.
|
|
646
1020
|
- Only place an event where you're confident the user action truly happens.
|
|
647
1021
|
|
|
648
|
-
|
|
649
|
-
|
|
1022
|
+
FINAL SUMMARY (this is shown verbatim to a non-technical customer in a
|
|
1023
|
+
dashboard \u2014 write for a human, not a changelog):
|
|
1024
|
+
- Plain prose and simple short lines ONLY. Do NOT use Markdown: no #/##
|
|
1025
|
+
headings, no **bold**, no backticks, no code fences, no tables, no "---".
|
|
1026
|
+
- Keep it tight: 3\u20136 short sentences or dash-prefixed lines. Lead with what now
|
|
1027
|
+
works ("Installed the SDK and wired identify() on login."). Then one line per
|
|
1028
|
+
group of events wired (you don't need to list all 28 individually \u2014 group
|
|
1029
|
+
them, e.g. "Wired the 6 subscription + signup events in your auth and billing
|
|
1030
|
+
controllers."). End with what you deliberately left as follow-ups and why.
|
|
1031
|
+
- Mention concrete file names in prose if helpful, but no diffs, no snippets.
|
|
650
1032
|
`.trim();
|
|
651
1033
|
function renderIdentifyBrief(m) {
|
|
652
1034
|
const lines = [];
|
|
@@ -681,7 +1063,7 @@ function renderEventsBrief(m) {
|
|
|
681
1063
|
lines.push("");
|
|
682
1064
|
}
|
|
683
1065
|
lines.push(
|
|
684
|
-
`${events.length} candidate events (highest-impact first). For each: fire track('<event_type>', { ...properties }) at the real moment it happens, and attach every listed property you can source from values in scope there. Skip an event entirely if it has no clear
|
|
1066
|
+
`${events.length} candidate events (highest-impact first). For each: fire track('<event_type>', { ...properties }) at the real moment it happens, and attach every listed property you can source from values in scope there. Skip an event entirely if it has no clear trigger in this codebase.`
|
|
685
1067
|
);
|
|
686
1068
|
if (events.some((e) => e.coverage?.length)) {
|
|
687
1069
|
lines.push("");
|
|
@@ -721,7 +1103,7 @@ function coverageNote(coverage) {
|
|
|
721
1103
|
async function runIntegrationAgent(opts) {
|
|
722
1104
|
const { repoPath, config, session, playbook, manifest, progress } = opts;
|
|
723
1105
|
applyModelAuthEnv(config, session);
|
|
724
|
-
const
|
|
1106
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
725
1107
|
const started = Date.now();
|
|
726
1108
|
let costUsd = 0;
|
|
727
1109
|
const summaries = [];
|
|
@@ -750,7 +1132,7 @@ async function runIntegrationAgent(opts) {
|
|
|
750
1132
|
].join("\n");
|
|
751
1133
|
const core = await runPass({
|
|
752
1134
|
prompt: corePrompt,
|
|
753
|
-
systemPrompt:
|
|
1135
|
+
systemPrompt: systemPrompt9,
|
|
754
1136
|
repoPath,
|
|
755
1137
|
model: config.model,
|
|
756
1138
|
maxTurns: 35,
|
|
@@ -779,7 +1161,7 @@ ${core.summary}`);
|
|
|
779
1161
|
].join("\n");
|
|
780
1162
|
const events = await runPass({
|
|
781
1163
|
prompt: eventsPrompt,
|
|
782
|
-
systemPrompt:
|
|
1164
|
+
systemPrompt: systemPrompt9,
|
|
783
1165
|
repoPath,
|
|
784
1166
|
model: config.model,
|
|
785
1167
|
maxTurns: config.maxTurns,
|
|
@@ -799,7 +1181,7 @@ ${events.summary}`);
|
|
|
799
1181
|
};
|
|
800
1182
|
}
|
|
801
1183
|
async function runPass(opts) {
|
|
802
|
-
const { prompt, systemPrompt:
|
|
1184
|
+
const { prompt, systemPrompt: systemPrompt9, repoPath, model, maxTurns, progress } = opts;
|
|
803
1185
|
let summary = "";
|
|
804
1186
|
let costUsd = 0;
|
|
805
1187
|
let ok = false;
|
|
@@ -809,7 +1191,7 @@ async function runPass(opts) {
|
|
|
809
1191
|
options: {
|
|
810
1192
|
model,
|
|
811
1193
|
cwd: repoPath,
|
|
812
|
-
systemPrompt:
|
|
1194
|
+
systemPrompt: systemPrompt9,
|
|
813
1195
|
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
814
1196
|
permissionMode: "bypassPermissions",
|
|
815
1197
|
maxTurns,
|
|
@@ -920,6 +1302,21 @@ function revertHint(checkpoint) {
|
|
|
920
1302
|
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
921
1303
|
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
922
1304
|
}
|
|
1305
|
+
async function isWorkingTreeClean(repoPath) {
|
|
1306
|
+
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1307
|
+
return status.ok && status.stdout.trim() === "";
|
|
1308
|
+
}
|
|
1309
|
+
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
1310
|
+
if (!checkpoint.isRepo) return false;
|
|
1311
|
+
if (checkpoint.baseRef) {
|
|
1312
|
+
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1313
|
+
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1314
|
+
return reset.ok && clean2.ok;
|
|
1315
|
+
}
|
|
1316
|
+
const restore = await run(repoPath, ["restore", "."]);
|
|
1317
|
+
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1318
|
+
return restore.ok && clean.ok;
|
|
1319
|
+
}
|
|
923
1320
|
async function repoFingerprint(repoPath) {
|
|
924
1321
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
925
1322
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
@@ -932,7 +1329,9 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
932
1329
|
const wired = /* @__PURE__ */ new Map();
|
|
933
1330
|
const patterns = eventTypes.map((e) => ({
|
|
934
1331
|
eventType: e,
|
|
935
|
-
re: new RegExp(
|
|
1332
|
+
re: new RegExp(
|
|
1333
|
+
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1334
|
+
)
|
|
936
1335
|
}));
|
|
937
1336
|
for (const file of files) {
|
|
938
1337
|
let content = "";
|
|
@@ -1079,8 +1478,24 @@ Flutter is live today; ${theme.bright(
|
|
|
1079
1478
|
);
|
|
1080
1479
|
const checkpoint = await takeCheckpoint(repoPath);
|
|
1081
1480
|
if (!checkpoint.isRepo) {
|
|
1481
|
+
if (!options.force) {
|
|
1482
|
+
p.cancel(
|
|
1483
|
+
theme.alert("Not a git repository.") + " The wizard edits your code and relies on git to undo safely.\n" + theme.muted("Run `git init` and commit first, or re-run with --force to proceed without a safety net.")
|
|
1484
|
+
);
|
|
1485
|
+
return 1;
|
|
1486
|
+
}
|
|
1082
1487
|
p.log.warn(
|
|
1083
|
-
theme.warn("not a git repo") + theme.muted(" \u2014
|
|
1488
|
+
theme.warn("not a git repo") + theme.muted(" \u2014 proceeding with --force; there is no automatic undo.")
|
|
1489
|
+
);
|
|
1490
|
+
} else if (!await isWorkingTreeClean(repoPath)) {
|
|
1491
|
+
if (!options.force) {
|
|
1492
|
+
p.cancel(
|
|
1493
|
+
theme.alert("Your working tree has uncommitted changes.") + " Commit or stash them first so the wizard's edits stay isolated and reversible.\n" + theme.muted("Or re-run with --force to proceed anyway.")
|
|
1494
|
+
);
|
|
1495
|
+
return 1;
|
|
1496
|
+
}
|
|
1497
|
+
p.log.warn(
|
|
1498
|
+
theme.warn("uncommitted changes present") + theme.muted(" \u2014 proceeding with --force; a revert would also drop your own changes.")
|
|
1084
1499
|
);
|
|
1085
1500
|
}
|
|
1086
1501
|
const spin = p.spinner();
|
|
@@ -1116,6 +1531,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1116
1531
|
clearInterval(tick);
|
|
1117
1532
|
if (!outcome) {
|
|
1118
1533
|
spin.stop(theme.alert("Integration stopped"));
|
|
1534
|
+
await maybeRevert(repoPath, checkpoint, "The run stopped before finishing.");
|
|
1119
1535
|
return 1;
|
|
1120
1536
|
}
|
|
1121
1537
|
const stopLabel = !outcome.coreOk ? theme.warn("Stopped early \u2014 partial setup is in your working tree") : outcome.eventsComplete ? theme.success("Integration complete") : theme.success("Core integration done") + theme.muted(" \u2014 some events left as follow-ups");
|
|
@@ -1127,6 +1543,17 @@ Flutter is live today; ${theme.bright(
|
|
|
1127
1543
|
if (outcome.summary.trim()) {
|
|
1128
1544
|
p.log.message(outcome.summary.trim());
|
|
1129
1545
|
}
|
|
1546
|
+
if (!outcome.coreOk) {
|
|
1547
|
+
const reverted = await maybeRevert(
|
|
1548
|
+
repoPath,
|
|
1549
|
+
checkpoint,
|
|
1550
|
+
"The core setup didn't complete, so these changes may be incomplete."
|
|
1551
|
+
);
|
|
1552
|
+
if (reverted) {
|
|
1553
|
+
p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
1554
|
+
return 1;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1130
1557
|
const wiredMap = await scanWiredEvents(
|
|
1131
1558
|
repoPath,
|
|
1132
1559
|
files,
|
|
@@ -1148,7 +1575,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1148
1575
|
});
|
|
1149
1576
|
p.log.info(
|
|
1150
1577
|
theme.muted(
|
|
1151
|
-
`${wiredMap.size}/${manifest.events.length} events wired \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7
|
|
1578
|
+
`${wiredMap.size}/${manifest.events.length} events wired \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ${Math.round(outcome.durationMs / 1e3)}s`
|
|
1152
1579
|
)
|
|
1153
1580
|
);
|
|
1154
1581
|
if (!config.offline) {
|
|
@@ -1183,6 +1610,29 @@ Flutter is live today; ${theme.bright(
|
|
|
1183
1610
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
1184
1611
|
return 0;
|
|
1185
1612
|
}
|
|
1613
|
+
async function maybeRevert(repoPath, checkpoint, reason) {
|
|
1614
|
+
if (!checkpoint.isRepo) return false;
|
|
1615
|
+
const files = await changedFiles(repoPath, checkpoint);
|
|
1616
|
+
if (files.length === 0) return false;
|
|
1617
|
+
const doRevert = await p.confirm({
|
|
1618
|
+
message: `${reason} Revert all of the wizard's changes now?`,
|
|
1619
|
+
initialValue: true
|
|
1620
|
+
});
|
|
1621
|
+
if (p.isCancel(doRevert) || !doRevert) {
|
|
1622
|
+
const hint = revertHint(checkpoint);
|
|
1623
|
+
if (hint) p.log.info(theme.muted(`Left in your working tree. Undo anytime: ${hint}`));
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
const ok = await revertToCheckpoint(repoPath, checkpoint);
|
|
1627
|
+
if (ok) {
|
|
1628
|
+
p.log.success(theme.success("Reverted to your pre-wizard state."));
|
|
1629
|
+
} else {
|
|
1630
|
+
p.log.warn(
|
|
1631
|
+
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
return ok;
|
|
1635
|
+
}
|
|
1186
1636
|
async function chooseTarget(detections) {
|
|
1187
1637
|
const top = detections[0];
|
|
1188
1638
|
if (top && (detections.length === 1 || top.confidence >= 0.9)) {
|
|
@@ -1274,6 +1724,9 @@ function parseArgs(argv) {
|
|
|
1274
1724
|
case "--offline":
|
|
1275
1725
|
opts.offline = true;
|
|
1276
1726
|
break;
|
|
1727
|
+
case "--force":
|
|
1728
|
+
opts.force = true;
|
|
1729
|
+
break;
|
|
1277
1730
|
case "--api":
|
|
1278
1731
|
opts.apiBaseUrl = args[++i];
|
|
1279
1732
|
break;
|
|
@@ -1299,6 +1752,7 @@ function printHelp() {
|
|
|
1299
1752
|
"",
|
|
1300
1753
|
` ${theme.bright("Options")}`,
|
|
1301
1754
|
" --offline Use a demo manifest, no account/browser needed",
|
|
1755
|
+
" --force Proceed without a clean git tree (no safe undo)",
|
|
1302
1756
|
" --api <url> Override the Whisperr API base URL",
|
|
1303
1757
|
" --model <id> Override the coding-agent model",
|
|
1304
1758
|
" -h, --help Show this help",
|
|
@@ -1317,7 +1771,7 @@ async function main() {
|
|
|
1317
1771
|
return;
|
|
1318
1772
|
}
|
|
1319
1773
|
if ("version" in parsed) {
|
|
1320
|
-
console.log("0.1.
|
|
1774
|
+
console.log("0.1.8");
|
|
1321
1775
|
return;
|
|
1322
1776
|
}
|
|
1323
1777
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Whisperr Wizard — one command to integrate the Whisperr SDK into your app. Authenticates with your onboarded account and uses an AI coding agent to wire up identify() and your business events automatically.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|